後端程式設計師一定要看的語言大比拼:Java vs. Go vs. Rus

local0發表於2021-09-09

圖片描述

這是Java,Go和Rust之間的比較。這不是基準測試,更多是對可執行檔案大小、記憶體使用率、CPU使用率、執行時要求等的比較,當然還有一個小的基準測試,可以看到每秒處理的請求數量,我將嘗試對這些數字進行有意義的解讀。

為了嘗試將蘋果與蘋果進行比較(也許是?),我在此比較中使用每種語言編寫了一個Web服務。Web服務非常簡單,它提供了三個REST服務端點(endpoint)。

圖片描述

Web服務提供的服務端點

這三個Web服務的程式碼倉庫託管在。

編譯後的二進位制檔案尺寸

有關如何構建二進位制檔案的一些資訊。對於Java,我使用和mvn package命令將所有內容構建到一個大的jar中。對於Go,我使用go build。最後,我使用了cargo build --release構建Rust服務的二進位制檔案。

圖片描述

每個程式的大小(以兆位元組為單位)

編譯後的檔案大小還取決於所選的庫/依賴項,因此,如果依賴項的身軀臃腫,則編譯後的程式也將難以倖免。在我的特定情況下,針對我選擇的特定庫,以上是程式編譯後的大小。

在後續的一個單獨小節中,我會把這三個程式都構建並打包為docker映象,並列出它們的大小,以顯示每種語言所需的執行時開銷。下面有更多詳細資訊。

記憶體使用情況

空閒狀態

圖片描述

每個應用程式在記憶體空閒時的記憶體使用情況

什麼?Go和Rust版本顯示空閒時記憶體佔用量的條形圖在哪裡?好了,它們在那裡,只有JVM啟動的程式在空閒狀態時消耗160 MB以上的記憶體,它什麼也沒做。Go應用程式僅使用0.86 MB,Rust應用也僅使用了0.36 MB。這是一個巨大的差異!在這裡,Java使用的記憶體比Go和Rust應用使用的記憶體高出兩個數量級,只是空佔著記憶體卻什麼都不做。那是巨大的資源浪費。

服務REST請求

讓我們使用發起訪問API的請求,並觀察記憶體和CPU使用情況,以及在我的計算機上三個版本程式的每個端點每秒處理的請求數。

wrk -t2 -c400 -d30s  
wrk -t2 -c400 -d30s 
wrk -t2 -c400 -d30s 

上面的wrk命令使用兩個執行緒並在連線池中保持400個開啟的連線,並重復呼叫GET端點,持續30秒。這裡我僅使用兩個執行緒,因為wrk和被測程式都在同一臺計算機上執行,所以我不希望它們在可用資源(尤其是CPU)上相互競爭(太多)。

每個Web服務都經過單獨測試,並且在每次執行之間都重新啟動了Web服務。

以下是該程式的每個版本的三個執行中的最佳結果。

  • /hello

該端點返回Hello,World!資訊。它分配字串“ Hello,World!” 並將其序列化並以JSON格式返回。

圖片描述

/hello端點的CPU使用率

圖片描述

/hello端點的記憶體使用情況

圖片描述

/hello端點處理的每秒請求數
  • /greeting/{name}

該端點接受一個段路徑引數{name},然後格式化字串“Hello,{name}!”,序列化並以JSON格式的問候訊息返回。

圖片描述

/greeting端點的CPU使用率

圖片描述

/greeting端點的記憶體使用情況

圖片描述

/greeting端點處理的每秒請求數
  • /fibonacci/{number}

該端點接受一個段路徑引數{number},並返回序列化為JSON格式的斐波納契數和輸入數。

對於這個特定的端點,我選擇以遞迴形式實現它。我毫不懷疑,迭代實現會產生更好的效能結果,並且出於生產目的,應該選擇一種迭代形式,但是在生產程式碼中,有些情況下必須使用遞迴(並非專門用於計算第n個斐波那契數 )。為此,我希望該實現涉及大量CPU棧分配。

圖片描述

/fibonacci端點的CPU使用率

圖片描述

/fibonacci端點的記憶體使用情況

圖片描述

/fibonacci端點處理的每秒請求數

在Fibonacci端點測試期間,Java是唯一一個有150個請求超時的實現,如下面wrk的輸出所示。

圖片描述

超時時間

圖片描述

/fibonacci端點的延遲

執行時大小

為了模擬現實世界中的雲原生應用程式,並避免“它僅可以在我的機器上執行!”,我分別為這三個應用程式建立了一個docker映象。

Docker檔案的原始碼包含在程式碼庫相應程式資料夾下。

作為我使用過的Java應用程式的基礎映象,openjdk:8-jre-alpine是已知大小最小的映象之一,但是,這附帶了一些警告,這些警告可能適用於您的應用程式,也可能不適用於您的應用程式,主要是alpine映象在處理環境變數名稱方面不是posix相容的,因此您不能在Dockerfile中使用ENV中的(點)字元(不過這沒什麼大不了的),另一個是alpine Linux映象是使用musl libc而不是glibc編譯的,這意味著如果您的應用程式依賴於需要glibc,它可能無法正常工作。不過,在這裡,alpine映象工作是正常的。

至於應用程式的Go版本和Rust版本,我已經對其進行了靜態編譯,這意味著它們不希望在執行時映象中存在libc(glibc,musl…等),這也意味著它們不需要執行OS的基本映象。因此,我使用了scratch docker映象,這是一個no-op映象,以零開銷託管已編譯的可執行檔案。

我使用的Docker映象的命名約定為{lang}/webservice。該應用程式的Java,Go和Rust版本的映象大小分別為113、8.68和4.24 MB。

圖片描述

最終Docker映象大小

結論

圖片描述

三種語言的比較

在得出任何結論之前,我想指出這三種語言之間的關係。Java和Go都是支援垃圾回收的語言,但是Java會提前編譯為在JVM上執行的位元組碼。啟動Java應用程式時,JIT編譯器會被呼叫以透過將位元組碼編譯為原生程式碼來最佳化位元組碼,以提高應用程式的效能。

Go和Rust都提前編譯為原生程式碼,並且在執行時不會進行進一步的最佳化。

Java和Go都是支援垃圾收集的語言,具有**STW(停止世界)**的副作用。這意味著,每當垃圾收集器執行時,它將停止應用程式,進行垃圾收集,並在完成後從停止的地方恢復應用程式。大多數垃圾收集器需要停止執行,但是有些實現似乎不需要這樣做。

當Java語言在90年代建立時,其最大的賣點之一是一次編寫,可在任何地方執行。當時這非常好,因為市場上沒有很多虛擬化解決方案。如今,大多數CPU支援虛擬化,這種虛擬化抵消了使用某種語言進行開發的誘惑(該語言承諾可以執行在任何平臺上)。Docker和其他解決方案以更為低廉的代價提供虛擬化。

在整個測試中,應用程式的Java版本比Go或Rust對應版本消耗了更多的記憶體,在前兩個測試中,Java使用的記憶體大約增加了8000%。這意味著對於實際應用程式,Java應用程式的執行成本會更高。

對於前兩個測試,Go應用程式使用的CPU比Java少20%,同時處理比java版多出38%的請求。另一方面,Rust版本使用的CPU比Go減少了57%,而處理的請求卻增加了13%。

第三次測試在設計上是佔用大量CPU的資源,因此我想從中擠出CPU的每一分。Go和Rust都比Java多使用了1%的CPU。而且我認為,如果wrk不是在同一臺計算機上執行,那麼這三個版本都會使CPU達到100%的上限值。在記憶體方面,Java使用的記憶體比Go和Rust多2000%。Java可以處理的請求比Go多出20%,而Rust可以處理的請求比Java多出15%。

在撰寫本文時,Java程式語言已經存在了將近30年,這使得在市場上尋找Java開發人員變得相對容易。另一方面,Go和Rust都是相對較新的語言,因此與Java相比,自然而然的開發人員的數量更少些。不過,Go和Rust都擁有很大的吸引力,許多開發人員正在將它們用於新專案,並且有許多使用Go和Rust的生產中正在執行的專案,因為簡單地說,就資源而言,它們比Java更有效。

在編寫本文的程式時,我同時學習了Go和Rust。就我而言,Go的學習曲線很短,因為它是一種相對容易掌握的語言,並且與其他語言相比語法很小。我只用了幾天就用Go編寫了程式。關於Go需要注意的一件事是編譯速度,我不得不承認,與Java/C/C++/Rust等其他語言相比,它的速度非常快。該程式的Rust版本花了我大約一個星期的時間來完成,我不得不說,大部分時間都花在弄清borrow checker向我要什麼上。Rust具有嚴格的所有權規則,但是一旦掌握了Rust的所有權和借用概念,編譯器錯誤訊息就會突然變得更加有意義。違反借閱檢查規則時,Rust編譯器對您大吼的原因是因為編譯器希望在編譯時證明已分配記憶體的壽命和所有權。這樣做可以保證程式的安全性(例如:沒有懸掛的指標,除非使用了不安全(unsafe)的程式碼逃離檢查),並且在編譯時確定了釋放位置,從而消除了垃圾收集器的需求和執行時成本。當然,這是以學習Rust的所有權系統為代價的。

在競爭方面,我認為Go是Java(通常是JVM語言)的直接競爭對手,但不是Rust的競爭對手。另一方面,Rust是Java,Go,C和C ++的重要競爭對手。

由於他們的效率,我看到了自己將會在Go和Rust中編寫更多的程式,但是很可能在Rust中編寫更多的程式。兩者都非常適合Web服務,CLI,系統程式(…etc)開發。但是,Rust比Go具有根本優勢。它不是垃圾收集的語言,與C和C++相比,它可以安全地編寫程式碼。例如,Go並不是特別適合用於編寫OS核心,而這裡又是Rust的亮點,並與C/C ++競爭,因為它們是使用OS編寫的長期存在和事實上的語言。Rust與C/C++競爭的另一種方式在嵌入式世界中,我將繼續進行討論。

感謝您的閱讀!

本文翻譯自


講師主頁:
講師部落格:
實戰課:
免費課:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/132/viewspace-2825347/,如需轉載,請註明出處,否則將追究法律責任。

相關文章