在知乎上看到一個問答:為什麼 Go 的 Web 框架速度還不如 Java?連結:https://www.zhihu.com/question/360929863 。
提問者由此問題的根據來自:https://www.techempower.com/benchmarks/#section=data-r18&hw=ph&test=json,不過提問時說:Go 不是編譯型語言嗎,速度怎麼還不如 Java?顯然這個說法有問題。
截個圖看看這個網站的跑分:
這裡主要整理兩個回答。
北南的回答首先呢,Java 也是編譯型的語言。Java 的編譯分為兩個步驟,第一步是從原始碼到 bytecode,也就是.class 文件。我們日常看到的 java jar 包,其實就是 bytecode 的分發形式。第二步是在運行時,java 虛擬機會和 jit 合作,把 bytecode 在需要的時候編譯成 native code。所以說呢,你這個連結裡的跑分比較,其實大家跑的都是編譯後的 native code。
其次,web 框架不單單要看中對語言的執行效率,還要看 IO 的效率。當然這也沒什麼花頭,java 的很多框架和 go 的異步 IO 如出一轍, 說白了還是藉助 linux 作業系統中的 epoll 等來減少進程堵塞。於是很多跑分中,語言的執行效率變得次要,在充分優化和合理使用 nonblocking IO 的情況下,一些解釋型語言也能取得很好的成績。
最後,也是最重要的,其實你看這些跑分,框架的性能都好的不行不行的,遠遠超過一般應用的需要。其實在實際應用中,我們的業務邏輯會更複雜,上下遊服務也會更多,其實就算是好多人瞧不起的增刪改查操作,並發大了也極容易產生性能瓶頸。換句話說,程式語言和框架本身是不容易成為性能瓶頸的。所以,我建議應該更多的從開發難易程度,以及以後項目長期維護的成本上來選擇語言和框架,而不是看跑分。
阿里巴巴淘系技術的回答該回複分析了這個跑分。
各種框架的應用場景不同導致其優化側重點不同,下面我們展開詳細分析。
http server 概述首先描述一下一個簡單的 web server 的請求處理過程:
Net 層讀取數據包後經過 HTTP Decoder 解析協議,再由 Route 找到對應的 Handler 回調,處理業務邏輯後設置相應 Response 的狀態碼等,然後由 HTTP Encoder 編碼相應的 Response,最後由 Net 寫出數據。
而 Net 之下的一層由內核控制,雖然也有很多優化策略,但這裡主要比較 web 框架本身,那麼暫時不考慮 Net 之下的優化。
看了下 techempower 提供的壓測框架源碼[1],各類框架基本上都是基於 epoll 的處理,那麼各類框架的性能差距主要體現在上述這些模塊的性能了。
關於各類壓測的簡述我們再看 techempower 的各項性能排名,有 JSON serialization, Single query, Multiple queries, Cached queries, Fortunes, Data updates 和 Plaintext 這幾大類的排名。
其中 JSON serialization 是對固定的 Json 結構編碼並返回 (message: hello word), Single query 是單次 DB 查詢,Multiple queries 是多次 DB 查詢,Cached queries 是從內存資料庫中獲取多個對象值並以 json 返回,Fortunes 是頁面渲染後返回,Data updates 是對 DB 的寫入,Plaintext 是最簡單的返回固定字符串。
這裡的 json 編碼,DB 操作,頁面渲染和固定字符串返回就是相應的業務邏輯,當業務邏輯越重(耗時越大)時,則相應的業務邏輯逐漸就成為了瓶頸,例如 DB 操作其實主要是在測試相應 DB 庫和 DB 本身處理邏輯的性能,而框架本身的基礎功能消耗隨著業務邏輯的繁重將越來越忽略不計(Round 19[2])中物理機下 Plaintext 下的 QPS 在七百萬級,而 Data updates 在萬級別,相差百倍以上),所以這邊主要分析 Json serialization 和 Plaintext 兩種相對能比較體現出框架本身 http 性能的排名。
在 Round 19[3] Json serialization 中 Java 性能最高的框架是 firenio-http-lite (QPS: 1,587,639),而 Go 最高的是 fasthttp-easyjson-prefork(QPS: 1,336,333),按照這裡面的數據是 Java 性能高。
firenio-http-lite從 fasthttp-easyjson-prefork 的 pprof 看除了 read 和 write 外, json (相當於 Business logic) 佔了 4.5%,fasthttp 自身(HTTP Decoder, HTTP Encoder, Router)佔了 15%,僅看 Json serialization 似乎會有一種 Java 比 Go 性能高的感覺。
fasthttp那我們繼續把業務邏輯簡化,看一下 Plaintext 的排名,Plaintext 模式其實是在使用 HTTP pipeline 模式下壓測的,在 Round 19[4] 中 Java 和 Go 已經幾乎一樣的 QPS 了,在 Round 19 之後的一次測試[5]中 gnet 已經排在所有語言的第二,但是前幾個框架 QPS 其實差別很微小。
這時候其實主要瓶頸都在 net 層,而 go 官方的 net 庫包含了處理 goroutine 相關的邏輯,像 gonet 之類的直接操作 epoll 的會少一些這方面的消耗,Java 的 nio 也是直接操作的 epoll 。
net拿了 gnet 的測試源碼跑了下壓測,看到 pprof 如下,其實這裡 gnet 還有更進一步的性能優化空間:time.Time.AppendFormat 佔用 30% CPU。
gnet可以使用如下提前 Format ,允許減少獲取當前時間精度的情況下大幅減少這部分的消耗。
var timetick atomic.Value
func NowTimeFormat() []byte {
return timetick.Load().([]byte)
}
func tickloop() {
timetick.Store(nowFormat())
for range time.Tick(time.Second) {
timetick.Store(nowFormat())
}
}
func nowFormat() []byte {
return []byte(time.Now().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
}
func init() {
timetick.Store(nowFormat())
go tickloop()
}這樣優化後接下來的瓶頸在於 runtime 的內存分配,是由於這個壓測代碼中還存在下面的部分沒有復用內存:
runtimepipeline code其實 gnet 本身的消耗已經做到非常小了,而 c++ 的 ulib 也是類似這樣使用的非常簡單的 HTTP 編解碼操作來壓測。
分析對於這裡面測試的框架,影響因素主要如下:
1、直接基於 epoll 的簡單 http: 沒有完整的 http decoder 和 route (如 gnet, ulib 直接簡單的字節拼接,固定的路由 handler 回調)
2、zero copy 和內存復用: 內部處理字節的 0 拷貝(go 官方 http 庫為了減少開發者的出錯概率,沒有使用 zero copy,否則開發者可能在無意中引用了已經放回 buff 池內的的數據造成沒有意識到的並發問題等等),而內存復用,大部分框架或多或少都已經做了。
3、prefork:注意到 go 框架中有使用了 prefork 進程的方式(比如 fasthttp-prefork),這是 fork 出多個子進程,共享同一個 listen fd,且每個進程使用單核但並發(1 個 P)處理的邏輯可以避免 go runtime 內部的鎖競爭和 goroutine 調度的消耗(但是 go runtime 中為了並發和 goroutine 調度而存在的相關「無用」代碼的消耗還是會有一些)
4、語言本身的性能差異對於第一點,其實簡化了各種編解碼和路由之後,雖然提高了性能,但是往往會降低框架的易用性,對於一般的業務而言,不會出現如此高的 QPS,同時選擇框架的時候往往還需要考慮易用性和可擴展性等,同時還需要考慮到公司內部原有中間件或者 SDK 所使用的框架集成複雜度。
對於第二點,如果是作為一個網絡代理而言,沒有業務方的開發,往往可以使用真正的完全 zero copy,但是作為業務開發框架提供出去的話是需要考慮一定的業務出錯概率,往往犧牲一部分性能是划算的。
第三點 prefork , java netty 等是直接對於線程操作,可以更加定製化的優化性能,而 go 的 goroutine 需要的是一個通用協程,目的是降低編寫並發程序的難度,在這個層次上難免性能比不上一個優化的非常出色的 Java 基於線程操作的框架;但是直接操作線程的話需要合理控制好線程數,這是個比較頭疼的調優問題(特別是對於新手來說),而 goroutine 則可以不關心池子的大小,使得代碼更加優雅和簡潔,這對於工程質量保障其實是一個提升。另外這裡存在 prefork 是由於 go 沒法直接操作線程,而 fasthttp 提供了 prefork 的能力,使用多進程方式來對標 Java 的多線程來進一步提高性能。
第四點,語言本身來說 Java 還是更加的成熟,包括 JVM 的 Jit 能力也使得在熱代碼中和 Go 編譯型語言的差異不大,何況 Go 本身的編譯器還不是特別成熟,比如逃逸分析等方面的問題, Go 本身的內存模型和 GC 的成熟度也比不上 Java。還有很重要的一點,Go 的框架成熟度和 Java 也不在一個級別,但相信這些都會隨著時間逐步成熟。
總之,對於這個框架壓測數據意義在於了解性能天花板,判斷繼續優化的空間和 ROI (投入產出比)。具體選擇框架還是要根據使用場景,性能,易用性,可擴展性,穩定性以及公司內部的生態等作出選擇,語言和性能分別只是其中一個因素。
各種框架的應用場景不同導致其優化側重點不同,如 spring web 為了易用性,可擴展性和穩定性而犧牲了性能,但它同樣擁有龐大的社區和用戶。再比如 Service Mesh Sidecar 場景下 Go 的天然並發編程上的優勢,以及小內存佔用,快速啟動,編譯型語言等特點使得比 Java 更加適合。
(附:其實我使用上述代碼和 dockerfile 構建,並且使用同樣的壓測腳本[6],在阿里雲 4 核獨享機器測試下 go fasthttp-easyjson-prefork 框架 Json serialization 的性能要高於 Java wizzardo-http 和 firenio-http-lite 30% 以上且延遲更低的,這可能和內核有關)。
以上為淘系架構團隊風弈的個人見解,歡迎前來討論。
同時也歡迎帶上簡歷發送到郵箱:fengyi.shy@alibaba-inc.com,加入淘系架構團隊,一起並肩作戰或前來指導工作,為上層淘系各業務提升基礎性能和研發效率。
總結我認為,大部分時候,框架的性能不是關鍵,更何況 Go、Java 等語言相比 PHP 等動態語言性能相對更好。何況 PHP 做 Web 開發,語言本身在大部分時候都不是問題呢。
你怎麼看?歡迎留言。
參考資料[1]壓測框架源碼: https://github.com/TechEmpower/FrameworkBenchmarks
[2]Round 19: https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=update
[3]Round 19: https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=update
[4]Round 19: https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=update
[5]一次測試: https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext
[6]同樣的壓測腳本: https://tfb-status.techempower.com/unzip/results.2020-05-13-21-08-06-543.zip/results/20200509183726/fasthttp-easyjson-prefork/json/raw.txt