天天看點

Java真的要沒落了?Q:為什麼Go的web架構速度還不如Java?Q:為什麼許多原本的 Java 項目都試圖用 go 進行重寫開源?Q:Java會不會因為容器的興起而沒落?

原創 風弈、空蒙、玄力 淘系技術  2月26日

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

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

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

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

▐  http server 概述

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

Java真的要沒落了?Q:為什麼Go的web架構速度還不如Java?Q:為什麼許多原本的 Java 項目都試圖用 go 進行重寫開源?Q:Java會不會因為容器的興起而沒落?

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性能高。

Java真的要沒落了?Q:為什麼Go的web架構速度還不如Java?Q:為什麼許多原本的 Java 項目都試圖用 go 進行重寫開源?Q:Java會不會因為容器的興起而沒落?

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

Java真的要沒落了?Q:為什麼Go的web架構速度還不如Java?Q:為什麼許多原本的 Java 項目都試圖用 go 進行重寫開源?Q:Java會不會因為容器的興起而沒落?

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

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

Java真的要沒落了?Q:為什麼Go的web架構速度還不如Java?Q:為什麼許多原本的 Java 項目都試圖用 go 進行重寫開源?Q:Java會不會因為容器的興起而沒落?

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

Java真的要沒落了?Q:為什麼Go的web架構速度還不如Java?Q:為什麼許多原本的 Java 項目都試圖用 go 進行重寫開源?Q:Java會不會因為容器的興起而沒落?

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

Java真的要沒落了?Q:為什麼Go的web架構速度還不如Java?Q:為什麼許多原本的 Java 項目都試圖用 go 進行重寫開源?Q:Java會不會因為容器的興起而沒落?
Java真的要沒落了?Q:為什麼Go的web架構速度還不如Java?Q:為什麼許多原本的 Java 項目都試圖用 go 進行重寫開源?Q:Java會不會因為容器的興起而沒落?

其實 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的地位仍難撼動,而且在雲原生場景中,也仍綻放着生命力。