Java真的要沒落了?

2021-03-04 12:01:22

最近也收到很多後端同學的提問,為什麼Go的web框架速度還不如Java?為什麼許多原本的 Java 專案都試圖用 go 進行重寫開源?Java會不會因為容器的興起而沒落?Java這個20多年的後端常青樹難道真的要走下坡路了?橙子邀請了淘系技術部的同學對以上問題進行解答,也歡迎大家一起交流。



Q:為什麼Go的web框架速度還不如Java?


風弈:華山論劍,讓我們索性把各框架的效能分析跑一下再說話。

各種框架的應用場景不同導致其優化側重點不同,下面我們展開詳細分析。


  http server 概述


首先描述一下一個簡單的 web server 的請求處理過程:

Net 層讀取封包後經過 HTTP Decoder 解析協定,再由 Route 找到對應的 Handler 回撥,處理業務邏輯後設定相應 Response 的狀態碼等,然後由 HTTP Encoder 編碼相應的 Response,最後由 Net 寫出資料。

而 Net 之下的一層由核心控制,雖然也有很多優化策略,但這裡主要比較 web 框架本身,那麼暫時不考慮 Net 之下的優化。

看了下 techempower 提供的壓測框架原始碼,各類框架基本上都是基於 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 中物理機下 Plaintext 下的 QPS 在七百萬級,而 Data updates 在萬級別,相差百倍以上),所以這邊主要分析 Json serialization 和 Plaintext兩種相對能比較體現出框架本身 http 效能的排名。

在 Round 19 Json serialization 中 Java 效能最高的框架是 firenio-http-lite (QPS: 1,587,639),而 Go 最高的是 fasthttp-easyjson-prefork(QPS: 1,336,333),按照這裡面的資料是Java效能高。

從 fasthttp-easyjson-prefork 的 pprof 看除了 read 和 write 外, json (相當於 Business logic) 佔了 4.5%,fasthttp 自身(HTTP Decoder, HTTP Encoder, Router)佔了 15%,僅看 Json serialization 似乎會有一種 Java 比 Go 效能高的感覺。

那我們繼續把業務邏輯簡化,看一下 Plaintext 的排名,Plaintext 模式其實是在使用 HTTP pipeline 模式下壓測的,在 Round 19 中 Java 和 Go 已經幾乎一樣的 QPS 了,在 Round 19 之後的一次測試中 gnet 已經排在所有語言的第二,但是前幾個框架QPS其實差別很微小。

這時候其實主要瓶頸都在 net 層,而 go 官方的 net 庫包含了處理 goroutine 相關的邏輯,像 gonet 之類的直接操作 epoll 的會少一些這方面的消耗,Java 的 nio 也是直接操作的 epoll 。

拿了 gnet 的測試原始碼跑了下壓測,看到 pprof 如下,其實這裡 gnet 還有更進一步的效能優化空間:time.Time.AppendFormat 佔用 30% CPU。


可以使用如下提前 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 的記憶體分配,是由於這個壓測程式碼中還存在下面的部分沒有複用記憶體:

其實 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 構建,並且使用同樣的壓測指令碼,在阿里雲4核獨享機器測試下 go fasthttp-easyjson-prefork 框架 Json serialization 的效能要高於 Java wizzardo-http 和 firenio-http-lite 30% 以上且延遲更低的,這可能和核心有關)。

Q:為什麼許多原本的 Java 專案都試圖用 go 進行重寫開源?


空濛:Java還是go核心是生態問題。

生態發展會經歷起步、發展、繁榮、停滯、消亡幾個階段,Java目前至少還在繁榮階段,go還是發展階段,不同階段在開發人員的數量與品質、開源能力豐富性、工程配套上是有巨大差異的,go是在狂補這三塊。另外不同公司還有個公司內部小生態的所處階段問題,也會影響技術的選型判斷。

現階段go的火熱,很大因素是雲原生裹挾著大家往前,k8s operator go語言實現的自帶光環,各種中介軟體能力在下沉與k8s融合,帶動著一波基礎中介軟體能力的go實現潮頭,但基礎的中介軟體能力相對是有限集合,如RPC、config、messagequeue等,這些中介軟體能力,以及雲原生k8s對上層業務而言應該做的是開發語言的中立性,讓業務基於公司的小生態和整個語言技術的大生態去抉擇,如果硬逼著業務也用go語言開發那就是耍流氓了。

總結來說,基礎中介軟體能力需要與k8s的融合需要會有go語言的動力,但整個開源生態其他能力並不見得是必須;業務開發依據公司生態和技術大生態選擇最合適的開發語言,不要盲目的追從而導致在人、開源能力、工程配套上的尷尬。go語言能否在業務研發上發力,還有待其生態的進一步發展。


Q:Java會不會因為容器的興起而沒落?


玄力:近年來以容器為核心的雲原生技術,讓伺服器端部署的伸縮性、可共同作業性,得到巨大的提升。使得原本開發語言本身選取的重要性,有一定程度的減弱。但不妨礙Java語言本身繼續保持活力。

畢竟,作為研發而言,研發輸出效率也是蠻關鍵的一個考量點,得益於Java完善而有龐大的開發者生態,提供了比大多數語言都要豐富的類庫/框架,也得益於Java強大的IDE工具,開發起來往往事半功倍。

而且,Java自身也有一些變種語言(如Scala),也是在朝更靈活更好用的方向發展;

另一方面,在巨量資料領域,Java仍在大放異彩,我們所熟知的 ES、Kafka、Spark、Hadoop。

我們評估和預測一個技術的生命力的時候,往往不會孤立地只看技術本身,同時也會結合它背後的整個生態。一個具有頑強生命力的技術的背後往往都有一個成熟的生態體系支撐,上面也提到Java在多個領域都有完善而龐大的生態,因此,我們認為Java的生命力仍然是頑強的。

但由於眾所周知的原因,客觀來講,Java本身在使用上,也會有一定的限制性。並且,在容器場景中,Java程序的記憶體設定,是需要小心謹慎的。

總的來說,Java的地位仍難撼動,而且在雲原生場景中,也仍綻放著生命力。

今日話題:

大家還有什麼話題想要了解,歡迎評論區留言,我們下期見~

✿  拓展閱讀

作者|風弈、空濛、玄力

編輯|橙子君

出品|阿里巴巴新零售淘系技術