Java 微服務能像 Go 一樣快嗎?

作者 | Mark Nelson、Peter Nagy

策劃 | 田曉旭

Peter Nagy 和我在 2020 年 8 月的甲骨文 Groundbreakers Tour 2020 LATAM 大會上發表一篇論文,題爲《Go Java, Go!》。我們在本文中提出一個問題:“Java 微服務能像 Go 一樣快嗎?” 爲此,我們創建了一系列微服務並進行了基準測試,並在會議上展示了我們的成果。但其中還有不少可以探索的空間,因此我們決定將在本文中進一步探討。

1 背景介紹

我們希望通過實驗瞭解 Java 微服務在運行速度上能否達到 Go 微服務的水平。目前,軟件行業普遍認爲 Java 已經過於陳舊、緩慢且無聊。而 Go 則成了快速、嶄新以及酷炫的代名詞。真是這樣嗎?我們想從數據的角度看看這樣的印象是否站得住腳。

我們希望建立一個公平的測試,因此創建了一項非常簡單的微服務,其中不含外部依賴項(例如數據庫),而且代碼路徑非常短(僅處理字符串)。我們在其中包含有指標及日誌記錄,因爲似乎一切微服務都或多或少包含這些內容。另外,我們使用了小型、輕量化的框架(Helidon for Java 以及 Go-Kit for Go),兩袖清風嘗試了 Java 的純 JAX-RS。我們也嘗試了不同版本的 Java 與不同 JVM。我們對堆大小及垃圾收集機制做出基本調整,並在測試運行前對微服務進行了預熱。

2Java 的發展歷史

Java 由 Sun Microsystems 公司開發,後被甲骨文所收購。其 1.0 版本發佈於 1996 年,目前的最新版本是 2020 年的 Java 15。Java 當前的主要設計目標,在於實現 Java 虛擬機及字節碼的可移植性,外加帶有垃圾回收的內存管理機制。時至今日,Java 作爲一種開源語言仍是全球最受歡迎的語言選項之一(根據 StackOverflow 及 TIOBE 等來源)。

下面來聊聊 “Java 問題”。人們對於它速度緩慢的印象其實更多是種固有觀念,而不再適應當下的事實。如今的 Java 甚至擁有不少性能敏感區,包括存儲對象數據堆、用於管理堆的垃圾收集器,外加準時化(JIT)編譯器。

多年以來,Java 曾先後使用多種不同的垃圾收集算法,包括串行、並行、併發標記 / 清除、G1 以及最新的 ZGC 垃圾收集器。現代垃圾收集器旨在儘可能減少垃圾收集造成的暫停時長。

甲骨文實驗室開發出一款名爲 GraalVM 的 Java 虛擬機,其使用 Java 編寫而成,具有新的編譯器外加一系列令人興奮的新功能,包括可以將 Java 字節碼轉換爲無需 Java 虛擬機即可運行的原生鏡像等。

3Go 的發展歷史

Go 語言由谷歌的 Robert Griesemer、Rob Pike 以及 Ken Thomson 開發而成。他們幾位也是 UNIX、B、C、Plan9 以及 UNIX 視窗系統等項目的主要貢獻者。作爲一種開源語言,Go 的 1.0 版本發佈於 2012 年,2020 年最新版本爲 1.15。Go 語言的本體、採用速度以及工具生態系統的發展都相當迅猛。

Go 語言受到 C、Python、JavaScript 以及 C++ 的影響,已經成爲一種理想的高性能網絡與多處理語言。

截至我們發佈主題演講時,StackOverflow 上共有 27872 個帶有 “Go” 標籤的問題,Java 則爲 1702730 個。

Go 是一種靜態類型的編譯語言,其語法類似於 C,且擁有內存安全、垃圾回收、結構化類型以及 CSP 樣式併發(通信順序過程)等功能特性。Go 還使用名爲 goroutine 的輕量級進程(並非操作系統線程),外加各進程間用於通信的通道(類型化,FIFO)。Go 語言不提供競態條件保護。

Go 是衆多 CNCF 項目的首選語言,例如 Kubernetes、Istio、Prometheus 以及 Grafana 等皆是由 Go 語言編寫而成(或者大部分是)。

Go 語言在設計上強調快速構建與快速執行。到底是兩個空格還是四個空格?Go 語言表示不用麻煩,無所謂。

與 Java 相比,我將個人體會到的 Go 語言優勢整理如下:

但 Go 當然也不完美。與 Java 相比,我認爲 Go 存在以下問題:

4 負載測試方法

我們使用 JMeter 進行負載測試。測試多次調用服務,並收集關於響應時間、吞吐量(每秒事務)以及內存使用情況的數據。在 Go 方面,我們主要收集常駐集大小,Java 方面則主要跟蹤原生內存。

在多項測試中,我們都將 JMeter 與被測應用程序放置在同一臺計算機上運行。經過對比,我們發現在其他機器上運行 JMeter 幾乎不會對結果造成任何影響。後續在將應用程序部署到 Kubernetes 中時,我們會考慮將 JMeter 運行在集羣之外的遠程計算機之上。

在進行測試之前,我們使用 1000 項服務調用對應用程序進行了預熱。

應用程序本體的源代碼以及負載測試定義請參見 GitHub repo:

https://github.com/markxnelson/go-java-go

5 首輪測試

在第一輪測試中,我們在小型機器上運行測試,搭載了 2.5 GHz 雙核英特爾酷睿 i7 的筆記本電腦,具有 16 GB 內存並運行 MacOS。我們運行了 100 個線程,每個線程 10000 個循環,再額外加個 10 秒的啓動時間。Java 應用程序運行在 JDK 11 與 Helidon 2.0.1 之上。Go 應用程序則使用 Go 1.13.3 進行編譯。

測試結果如下:

我們宣佈,Go 成爲首輪測試的獲勝者!

以下爲根據這些結果得出的觀察結論:

6GraalVM 原生鏡像

GraalVM 提供原生鏡像功能,使您能夠使用 Java 應用程序並在實質上將其編譯爲原生可執行代碼。根據 GraalVM 項目網站的介紹:

該可執行文件包含應用程序類、依賴項中的類、運行時庫類以及 JDK 中的靜態鏈接原生代碼。其並非運行在 Java 虛擬機之上,而是包含必要組件,例如來自不同運行時系統(也被稱爲「基層虛擬機」)的內存管理、線程調度等功能。基層虛擬機代表的是各運行時組件(例如反優化器、垃圾收集器、線程調度等)。

在添加 GraalVM 原生鏡像(原生鏡像由 GraalVM EE 20.1.1——JDK 11 構建而成)之後,首輪測試結果如下:

在這種情況下,與運行在 JVM 上的應用程序相比,我們發現使用 GraalVM 原生鏡像並不會在吞吐量或者響應時間等層面帶來任何實質性的改善,但內存佔用量確實有所減少。

以下是測試期間的響應時間圖表:

首輪響應時間圖

請注意,在所有三種 Java 變體當中,第一批請求的響應時間要長得多(藍線相較於左軸的高度)而且在各項測試中,我們還看到一些峯值,其可能是由垃圾收集或優化所引起。

7 第二輪測試

接下來,我們決定在更大的計算機上運行測試。在本輪中,我們使用臺具有 36 個核心(每核心雙線程)、256 GB 內存的計算機,並配合 Oracle Linux 7.8 操作系統。

與第一輪一樣,我們仍然使用 100 個線程、每線程 10000 個循環,10 秒啓動時間以及相同版本的 Go、Java、Helidon 以及 GraalVM。

下面來看結果:

我們宣佈,GraalVM 原生鏡像成爲第二輪測試的贏家!

下面來看本輪測試的響應時間圖:

啓用日誌記錄,但未經預熱的測試運行響應時間

不使用日誌記錄也未經預熱的測試運行響應時間

經過預熱,但未使用日誌記錄的測試運行響應時間

第二輪的觀察結果:

8 第三輪測試:Kubernetes

在第三輪中,我們決定在 Kubernetes 集羣上運行應用程序,藉此模擬更爲自然的微服務運行時環境。

在本輪中,我們使用包含三個工作節點的 Kubernets 1.16.8 集羣,每個工作節點中包含兩個核心(各對應兩個線程)、14 GB 內存以及 Oracle Linux 7.8。在某些測試中,我們在變體上運行一個 Pod;在其他一些測試中,我們則運行一百個 Pod。

應用程序訪問通過 Traefik 入口控制器實現,其中 JMeter 運行在 Kubernetes 集羣之外。在某些測試中,我們也會嘗試使用 ClusterIP 並在集羣內運行 JMeter。

與之前的測試一樣,我們使用 100 個線程、每線程 10000 個循環,外加 10 秒啓動時間。

以下是各個變體的容器大小:

以下爲本輪測試結果:

響應時間圖表:

Kubernetes 測試中的響應時間

在本輪中,可以看到 Go 有時更快,而 GraalVM 原生鏡像也經常取得領先,但二者的差異很小(一般低於 5%)。

9 測試結論

縱觀幾輪測試與結果,我們得出了以下結論:

10 未來展望

經過這輪有趣的測試,我們打算繼續探索,特別是:

原文鏈接:

https://medium.com/helidon/can-java-microservices-be-as-fast-as-go-5ceb9a45d673


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qbXQanD1NuL45_IofWjJ7g