Java、Go 和 Rust 的比較

這是一篇 Java、Go 和 Rust 之間的比較。不僅僅在基準指標的意義上,更多是在輸出可執行文件大小、內存使用、CPU 使用、運行時要求之間的比較,當然還有一些簡單性能測試。

爲了更貼近現實,我已經用這種比較中的每種語言編寫了一個 Web 服務。Web 服務非常簡單,它爲三個 REST http 服務。

Web 服務,使用 Java、Go 和 Rust。

github:https://github.com/dexterdarwich/ws-compare

文件大小

在 Java 打包構建的場景下下,我使用 maven-shade-plugin 將所有內容構建到一個 jar 中,並使用了 mvn package 打成 jar 包。在 Go 的情況下,我使用了 go build. 最後,對於 Rust,我使用了 cargo build --release.

每個程序的編譯大小(以兆字節爲單位)。

工程編譯的大小還取決於所選的庫 / 依賴項,在我的具體情況下,以上是編譯後的程序大小。

在單獨的部分中,我將構建所有三個程序並將其打包爲 docker 映像,並將列出它們的大小以及顯示每種語言所需的運行時開銷。更多詳情如下。

內存使用情況

沒有任何請求的情況下

每個應用程序在內存中空閒時的內存使用情況。

Go 和 Rust 版本在空閒時顯示內存佔用幾乎看不到,只是當 JVM 啓動程序並閒置不做任何事情時,Java 消耗了 160 MB 以上的空間。在 Go 的情況下,程序使用 0.86 MB,在 Rust 的情況下使用 0.36 MB。這是一個很大的不同!因爲這是在內存中什麼也不做情況下,Java 內存佔用比 Go 和 Rust 對應物多兩個數量級,所以這是對資源的巨大浪費。

 REST 請求

讓我們使用 wrk 通過請求訪問 API 並觀察內存和 CPU 使用情況,以及我的機器上針對程序的三個版本的每個請求地址請求的 qps。

wrk -t2 -c400 -d30s http://127.0.0.1:8080/hello 
wrk -t2 -c400 -d30s http://127.0.0.1:8080/greeting/Jane
wrk -t2 -c400 -d30s http://127.0.0.1:8080/fibonacci/35

關於上面的 wrk 命令說如下,使用兩個線程(用於 wrk)並在池中保持 400 個打開的連接,並在 30 秒的持續時間內重複調用 GET 請求。這裏我只使用了兩個線程,因爲 wrk 和被測程序都在同一臺機器上運行,所以我不希望它們在可用資源上(尤其是 CPU)相互競爭。

每個 Web 服務都分別進行了測試,並且在每次運行之間重新啓動了 Web 服務。下面這個請求是該程序每個版本的三個運行中最好的一個。

/hello

此請求返回 Hello, World! 信息。它分配字符串 “Hello, World!” 並將其序列化並以 JSON 格式返回。

請求 /hello 時的 CPU 使用率

訪問 /hello 時的內存使用情況

訪問 /hello qps

/greeting/{name}

此請求接受段路徑參數 {name},然後格式化字符串 “ Hello, {name}!” , 將其序列化並作爲 JSON 格式的問候消息返回。

請求 /greeting/{name} 時的 CPU 使用率

訪問 /greeting/{name} 時的內存使用情況

訪問 /greeting/{name} qps

/fibonacci/{number}

此端點接受段路徑參數 {number} 並返回斐波那契數和序列化爲 JSON 格式的輸入數字。

這個特定接口我選擇以遞歸形式實現它。毫無疑問通過迭代實現會產生更好的性能結果,並且出於生產目的,應該選擇迭代形式,但是在生產代碼中存在必須使用遞歸的情況(不一定是專門用於計算第 n 個斐波那契數)。另外爲了測試性能對比,通過該實現能夠使大量 CPU 參與堆棧分配。

請求 /fibonacci/{number} 時的 CPU 使用率

訪問 /fibonacci/{number} 時的內存使用情況

訪問 /fibonacci/{number} qps

在 Fibonacci 接口期間,Java 實現是唯一一個在 150 個請求上出現超時的編程語言,如下面 wrk 的輸出所示。

/fibonacci 請求的延遲

運行時大小

爲了模擬真實世界的雲原生應用程序,並消除 “它可以在我的機器上運行!” 的問題,我爲這三個應用程序中的每一個都創建了一個 docker 鏡像。

作爲 java 應用程序的基礎運行時鏡像,我使用了 openjdk:8-jre-alpine,這是已知的尺寸最小的鏡像之一,然而,這有一些注意事項,可能適用於也可能不適用於你的應用程序,主要是 alpine 鏡像在處理環境變量名方面不符合 posix 標準,所以你不能在 docker 文件中使用. 字符在 docker 文件中的 ENV(不是什麼大問題),另一個問題是,alpine Linux 鏡像是用 musl libc 而不是 glibc 編譯的,這意味着如果你的應用程序依賴於需要 glibc(或朋友)存在的東西,它根本無法工作。在我的例子中,alpine 工作就很好。

對於 Go 和 Rust 版本的應用程序,我對它們進行了靜態編譯,這意味着它們不需要 libc(glibc,musl... 等)存在於運行時鏡像中,這也意味着它們不需要一個帶有操作系統的基礎鏡像來運行。所以我使用了 scratch docker 鏡像,它是一個以零開銷的方式承載編譯後的可執行文件。

我使用的 docker 鏡像的命名規則是 {lang}/webservice。該應用程序的 Java、Go 和 Rust 版本的鏡像大小分別爲 113、8.68 和 4.24 MB。

最終的 Docker 鏡像大小

結論

三種語言的比較

在得出任何結論之前,我想指出這三種語言之間的關係(或沒有關係)。Java 和 Go 都是垃圾收集語言,然而,Java 被提前編譯爲字節碼,在 JVM 上運行。當 Java 應用程序啓動時,即時編譯器(JIT)被調用,通過隨時隨地將其編譯爲本地代碼來優化字節碼,以提高應用程序的性能。

Go 和 Rust 都提前編譯爲本機代碼,並且在運行時不會發生進一步的優化。

Java 和 Go 都是垃圾收集語言,有一個 STW 的副作用。這意味着每當垃圾收集器運行時,它將停止應用程序,進行垃圾收集,當完成後,它將從它離開的地方恢復應用程序。大多數垃圾收集器需要 STW,但也有一些實現可以減緩這種情況的發生。

當 Java 在 90 年代創建時,它最大的賣點之一就是 "一次編譯,到處運行"。這在當時是很接地氣的需求,因爲當時市場上還沒有很多虛擬化解決方案。如今,大多數 CPU 都支持虛擬化,這使得使用 Java 語言開發的誘惑力消失了,因爲 Docker 和其他解決方案提供了廉價的虛擬化,它可以在任何地方運行(在任何支持的平臺上)。

在整個測試過程中,Java 版本的應用程序比 Go 或 Rust 版本的應用程序消耗了多個數量級的內存,在前兩個測試中,Java 使用的內存大約多出 8000%。這意味着對於現實世界的應用程序,Java 應用程序的運營成本更高。

對於前兩個測試,Go 應用程序使用的 CPU 比 Java 少約 20%,同時處理的請求多 38%。另一方面,Rust 版本使用的 CPU 比 Go 少 57%,同時處理的請求多 13%。

第三項測試在設計上是 CPU 密集型的,我想通過它來榨取 CPU 的每一個指令集。Go 和 Rust 的 CPU 使用率都比 Java 高 1%。而我認爲如果 wrk 不在同一臺機器上運行,這三個版本的 CPU 都會達到 100% 的上限。在內存方面,Java 比 Go 和 Rust 多用了 2000% 以上的內存。Java 能夠比 Go 多提供約 20% 的請求,而 Rust 比 Java 多提供約 15% 的請求。

在寫這篇文章的時候,Java 編程語言已經存在了近三十年,這使得市場上相對更容易找到 Java 開發者。另一方面,Go 和 Rust 都是相對較新的語言,所以與 Java 相比,市場上的開發者數量自然較少。不過 Go 和 Rust 都獲得了很大的發展,許多開發者在新項目中採用它們,而且有許多項目在生產中使用 Go 和 Rust,因爲簡單地說,它們在資源需求方面比 Java 更有效。

我同時學習了 Go 和 Rust。就我而言,Go 的學習曲線相對簡單,因爲它是一種比較容易上手的語言,而且與其他語言相比,其語法很小。我只花了幾天時間就用 Go 寫好了程序。關於 Go 有一點需要注意的是它的編譯速度,我不得不承認,與其他語言如 Java/C/C++/Rust 相比,它的編譯速度非常快。Rust 版本的程序花了我一週左右的時間來學習,我不得不說,其中大部分時間是在弄清楚借用檢查器要我做什麼。Rust 有嚴格的所有權規則,但一旦掌握了 Rust 中所有權和借用的概念,編譯器的錯誤信息就會突然變得更有意義。Rust 編譯器之所以在違反借用檢查規則時對你大喊大叫,是因爲編譯器想在編譯時證明分配內存的生命週期和所有權。通過這樣做,它保證了程序的安全性(例如:沒有野指針,除非使用了不安全的代碼轉義),並且在編譯時確定了取消分配,從而消除了對垃圾收集器的需求和運行時成本。當然,這是以學習 Rust 的所有權系統爲代價的。

就競爭而言,在我看來,Go 是 Java(一般的 JVM 語言)的直接競爭對手,但不是 Rust 的競爭對手。另一方面,Rust 是 Java、Go、C 和 C++ 的一個嚴重競爭對手。

由於它們的效率,我將會用 Go 和 Rust 寫更多的程序,但最有可能的是用 Rust 寫更多的程序。這兩種語言對於網絡服務、cli、系統程序(...... 等)的開發都很好。然而,Rust 不是一種垃圾收集語言,這是它跟 Go 相比的天然優勢。與 C 和 C++ 相比,它被設計爲可以安全地編寫代碼。例如,Go 並不特別適合用來編寫操作系統內核,而這又是 Rust 的優勢所在,它可以與 C/C++ 競爭,因爲它們是長期存在的、事實上可以用來編寫操作系統的語言。Rust 與 C/C++ 競爭的另一個方面是在嵌入式領域。

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