<tr id="ieeco"><small id="ieeco"></small></tr>
<acronym id="ieeco"></acronym>

Java真的要沒落了?

最近也收到很多后端同學的提問,為什么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的地位仍難撼動,而且在云原生場景中,也仍綻放著生命力。

今日話題:

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

???拓展閱讀

作者|風弈、空蒙、玄力

編輯|橙子君

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

已標記關鍵詞 清除標記
相關推薦
彩票送彩金