Java致命傷:持續交付使JVM JIT成為反模式 - astradot

發表於2021-02-02

Java的JVM JIT編譯器存在一個假設前提:JVM是長時間執行的程式,基於這種假設才有JIT,但是持續交付以及由此導致的JVM頻繁重啟意味著這種假設前提卻不存在了。

在Astradot,我們相信JVM的時代即將結束。我們正在用AOT編譯語言編寫後端,以便100%的時間為您提供出色的體驗。我們最近將微服務從Kotlin轉換為Go,發現這是一個受歡迎的變化。

 每個Java基準測試都會告訴您,在評估其效能之前,首先要多次執行程式碼以進行“預熱”,以便通過JVM的C2編譯器對其進行JIT和優化。

但是,在現實世界中,在“預熱”程式碼之前對應用程式進行的呼叫在很大程度上是應用程式體驗的一部分。

 

JVM JIT遭遇“註冊”頁面

Astradot擁有Kotlin微服務,可處理諸如註冊和登入之類的身份驗證活動。如果您在重新部署服務後立即嘗試註冊,則單擊“註冊”按鈕後感覺註冊頁面已凍結了一樣。該頁面可能需要幾秒鐘才能響應。這是因為JVM第一次載入大量的Kotlin / Spring類的程式碼,並在沒有優化的情況下通過直譯器執行它。

當然,單擊登入按鈕越多,響應時間就會越長,但是第一次進行註冊的使用者可能會認為我們的系統已凍結並消失了。

由於每個微服務都有多個例項正在執行,因此您可能第二次嘗試註冊請求時,請求將轉到另一個JVM例項。對於該JVM,這是第一次載入註冊程式碼,因此您再次遇到凍結行為。從終端使用者的角度來看,他現在嘗試多次註冊,並且每次遇到緩慢的行為。因此,這就是他現在對我們產品的印象。

 

持續交付破壞了JIT編譯器的核心假設

Astradot的一項指標收集器服務每個JVM每秒獲取500個請求。全新部署後,即使達到如此高的吞吐量,也要花費整整2個小時,JVM C2編譯器才能完全優化該程式碼路徑,以使響應時間降至最低。

為了說明這2個小時,以下是Sysdig最新容器使用情況調查的結果:

Java致命傷:持續交付使JVM JIT成為反模式 - astradot

74%的容器壽命≤1小時。這改變了JIT編譯器背後的核心假設,即JVM是一個長期執行的過程。在通過JVM C2編譯器優化容器之前,將對其進行重新部署。因此,您的使用者將永遠無法體驗到所有這些JVM基準測試所承諾的驚人效能。

對於低吞吐量的部分程式碼,情況會更糟。想想我之前提到的“註冊”頁面。即使我們的auth微服務已部署了好幾天,signup()函式仍將無法獲得足夠的呼叫來觸發C2編譯器對其進行完全優化。因此,使用者將始終體驗該程式碼的未優化版本。

 

現代編譯語言的興起

JVM JIT編譯器的賣點之一是它具有執行時資訊,因此可以進行更好的優化。20年前可能是這樣。但是從那以後,Ahead(AOT)編譯語言得到了發展。Go(類似於Java一樣被垃圾收集,但是經過AOT編譯)可以實現類似或更好的效能。Rust能夠在基準測試中始終擊敗Java。

這是由於Java的基本設計。它鼓勵在堆上使用虛擬方法和分配。JIT優化的很大一部分圍繞嘗試將這些虛擬呼叫轉換為靜態呼叫,內聯它們,執行轉義分析以將那些堆分配轉換為堆疊分配。

Go和Rust預設情況下鼓勵在任何地方使用靜態方法呼叫和堆疊分配,因此它們不需要大型JIT的所有複雜性和開銷即可在執行時對其進行優化。

 

AOT編譯的Java

有跡象表明Java人員正在意識到JIT的危險。GraalVM具有AOT編譯器,並且Quarkus和Micronaut之類的框架以使用它們。Java的動態特性意味著動態類載入,反射,代理等功能在AOT中不可用或受限制。生產Java應用程式通常還與依賴於執行時位元組碼檢測的APM跟蹤代理一起執行。整個JVM生態系統根本不是圍繞AOT編譯設計的。

具有25年曆史的執行時生態系統以適應AOT編譯的感覺就像在豬上塗上口紅一樣。使用Go和Rust等現代編譯語言重新開始比較容易。

 

相關文章