雲時代,JAVA何去何從?

在雲原生的世界裏,Go語言憑藉語法簡單、啓動速度快、依賴少、Goroutine併發等特點,成爲了一等公民。而Java作爲20年前的編程語言,那個時代注重的是複雜的OOP設計、企業級規範,長期運行下的穩定性和性能。Java語言似乎與當前雲原生環境下的快速交付、微服務等需求格格不入。

阿里巴巴是世界上最大的Java用戶之一,在擁抱雲原生的同時,也要保持現有業務的迭代演進。因此Alibaba JVM團隊一直致力於讓Java語言與時俱進,適應雲上場景。今天我們就來聊一聊Java在雲上遇到的挑戰以及如何通過Dragonwell JDK來克服這些困難。

Java語言 & 雲原生

Java是一門企業級,高性能,高穩定性的編程語言。企業級簡單來說就是適合開發長期運行的大型應用,比如Linux + OpenJDK的開發的服務如果沒有發佈和升級的需求,一般情況下可以保證運行一年以上不用重啓。

Java擁有豐富的生態,大量的高質量第三方類庫、框架(比如Spring、Netty)被維護在maven等中心倉庫,用戶只需聲明式地引入包依賴,即可調用實現。舉例來說,node的npm生態雖然很完善,但是想要找到一個分佈式事務框架就很困難;反觀Java,幾乎所有開源軟件與工具都會考慮對Java平臺的支持,我們可以找到多種分佈式事務框架的Java客戶端。因此只要選擇了Java,就是選擇了一個資源寶庫。

作爲Java的開發者肯定聽說過Java EE(目前捐給了Eclipse社區,更名爲Jakarta EE),Java EE很大程度上成就了Java語言。編程語言本身只是提供控制流、數據結構定義、垃圾回收、併發基礎設施、抽象手段等基礎能力。而這個編程語言的殺手級場景究竟什麼,是取決於語言之上的標準庫、規範的。Jakarta EE定義的JDBC規範就引領各大廠商爲Java提供了接口一致的數據庫驅動; Tomcat、JBoss實現的Servlet容器讓開發者有機會選擇不同的Servlet實現。

Java的跨平臺性向開發者屏蔽了底層的硬件和操作系統細節。開發者們可以在Mac、Windows的開發環境開發、調試應用,最後到Linux的生產環境去部署,這大大降低了研發、調試、運維的工作量。

萬物皆有TradeOff,我們上述的一些設計取向給我們帶來的一些麻煩。

代碼加載開銷高

爲了實現跨平臺性,Java定義了自己的字節碼,通過字節碼描述計算,最後各個平臺實現的字節碼引擎來執行字節碼。我們來看一段程序想要被加載需要經過的流程:

  • new字節碼或者static相關字節碼觸發類加載
  • 從一系列jar包中找到感興趣的class文件
  • 將class文件的讀取到內存裏的byte數組
  • defineClass,包括了class文件的解析、校驗、鏈接
  • 類初始化(static塊,或者靜態變量初始化)
  • 開始解釋執行
  • 2000次解釋後被client compiler JIT編譯,隨後15000次執行後被server compiler JIT編譯

簡單的代碼執行卻涉及了大量的額外操作,一次類加載基本上是在毫秒級的,因此大型Java應用(數萬個class)的啓動耗時很長,並需要一段時間來進行JIT預熱,無法滿足快速交付的需求。

內存管理造成內存佔用大

我們經常收到到諸如 "明明heap只用了幾百M內存,爲啥監控提示內存水位高,進程佔用了5G的內存"這樣的答疑需求。我們結合一個實例來更好地瞭解JVM的內存管理。

  1. 開始heap沒有對象,只分配了虛擬地址空間
  2. 隨着對象分配,發生page fault,內存被實際分配出來,當heap塞滿,無法分配對象時,下一次對象分配將觸發gc
  3. gc只保留了活對象,而死對象回收所空出來的空間可以被後續分配

GC結束後,雖然有很多空閒內存,但是因爲heap是jvm管理的,jvm瞭解這些空間是空閒的,但是操作系統不知情,因此無法把這些空間分配給其他進程使用。JVM之所以不把內存歸還給操作系統的主要原因是這些內存很快就需要被應用使用,如果頻繁進行歸還,再而觸發page fault反而帶來性能下降。

使用每個請求一個線程的模型

基於上述的Jakarta EE規範,大部分Java通信組件或者中間件都是基於線程模型的,比如Servlet容器使用線程池來處理併發請求。JDBC訪問過程中需要阻塞線程,這也是規範,因此在線程模型下這些組件協作的很好。

但是多線程的抽象下編程簡單了,對操作系統的負擔卻增加了。右圖上每個豎塊表示一個線程,他們分別處理一個請求,帶顏色的區域說明這些線程實際在CPU上運行。雖然從線程視角任務是一直在運行的,而實際上是操作系統通過分時機制交替執行他們製造的假象。在高負載下操作系統調度任務的開銷不容小視。

Dragonwell 助力雲上轉型

Dragonwell產品介紹

要了解Dragonwell首先要了解OpenJDK,OpenJDK是由Oracle開源的JDK實現,是目前最廣泛使用的實現,類似的實現還有OpenJ9等。

針對我們常用的JDK8、JDK11 LTS版本,OpenJDK本身一直維持着活躍的開發,但是社區本身沒有持續地提供帶有最新更新的發行版本。想要用使用最新的安全的JDK版本有兩種途徑

  1. 使用Oracle JDK:Oracle會提供專門支持,但是這是要收費的
  2. 使用三方廠商自己維護的JDK: 通常雲廠商爲了讓客戶們使用到最新的安全的JDK,都會以OpenJDK爲上游,推出自己的JDK發行版,Amazon Corretto、Alibaba Dragonwell都是這樣一類發行版。這些版本由雲廠商維護支持。

Dragonwell就屬於上述的第二種。與Corretto等發行版不同的是阿里巴巴針對自己的實踐,加入了大量優化特性,特別是針對雲場景。我們可以選擇性地打開這些優化,如果關閉則表現與OpenJDK一致。下面我們針對Dragonwell的特性,以及這些特性如何助力Java應用上雲進行剖析。

Elastic Heap

基於上面描述的GC導致JVM會佔用大量內存這一問題,Elastic Heap功能會估算應用實際需要使用的內存大小。將內存定期歸還給操作系統。

在阿里巴巴的場景下,每個裸金屬服務器會部署大量在線業務,當在線業務處於低谷期時,elastic heap功能會自動地釋放內存。此時調度系統就可以在裸金屬服務器上創建離線任務,將省出來的內存利用起來。下面看spring boot demo一個的例子:

我們使用wrk對應用進行壓測,不久內存使用(RES)就到達了配置的1G。且內存不會降下來,即便壓測停止也會一直保持在這個水位。

隨後我們使用Elastic Heap來改善這個狀況。使用Elastic Heap,隨着壓測停止,內存使用逐漸降低到了 700M。這緩解了Java應用佔用內存過多的問題。

JWarmup

Java代碼需要經過足夠的解釋執行次數後纔會被JIT編譯器編譯,通過上圖的命令可以看到不同編譯級別的執行次數閾值。在Web Server領域,應用剛剛發佈完成時解釋代碼版本執行就慢,同時隨着閾值到達,會觸發編譯,編譯線程本身也會消耗CPU,這就導致了Java服務剛發佈完成時性能差。

那麼能否讓JIT編譯提前完成呢?JWarmup就是用來達成這個目的的。如圖所示:

  • 在beta環境收集代碼的profiling信息
  • 將收集到的profiling數據分pai到生產服務器後,進行發佈啓動
  • Warmup會提前編譯熱點方法,保證線上請求開始處理時已經到達一個較高的編譯級別了

多租戶

多租戶是JVM層面上提供的虛擬化技術,在JVM中引入了一個租戶的概念,每個租戶的最大資源(通常是CPU、內存、文件fd)使用是可以獨立控制的。

一種用法是將多個Java的微服務部署到同一個Java虛擬機中,每個應用可以的資源是隔離的。相比容器隔離的好處是底層數據可以共享,並且應用之間的RPC可以被轉換成方法調用,大大減少開銷。

協程

上文中提到了Java使用了線程阻塞模型來處理請求,導致總體效率不夠高。

這一點也是業界共識,近年來出現了大量異步處理的框架,在異步的加持下就可以用少量線程來併發處理大量請求了。node.js是異步編程的典型框架,vertex和node兩個單詞都是"節點"的意思,Java的生態中Vert.X的流行正是表明了廣大開發者對於目前的阻塞模型的現狀並不滿意。我們來看上圖中的Vert.X的JDBC client的用法,包含了大量的回調函數使用,這對於複雜的應用的接入是一個大的挑戰。但是如果我們結合kotin協程的支持就可以顯著地簡化這類異步框架的使用。

右圖中對異步的 getConnection 進行了封裝,調用後立即切換出協程。當操作完成,回調中會恢復協程執行。這樣封裝後,上層代碼就可以簡化了。右圖的

conn = client.aGetConnection();

本質上是事件驅動的異步操作,但寫法上是順序的。這就是協程帶給我們的性能紅利。

Dragonwell的Wisp特性就是在JVM層面支持了協程,並在所有阻塞調用(如 Socket.connect() )做出了類似 aGetConnection 的封裝。因此現有的同步寫法的代碼都可以被異步調度執行,得到性能提升。

我們繼續看一個案例:

我們使用了一個Spring Boot 以 undertow作爲Servlet容器的Http Server hello world程序作爲實例:

在關閉協程時可以看到有大量的worker線程在處理請求,通過多次執行、預熱,throughput最終鎖定在37193。

添加一個參數 -XX:+UseWisp2 後繼續測試:

添加一個參數 -XX:+UseWisp2 後繼續測試:

可以看到2個內核級pthread處理了所有請求。默認名字是Wisp-Root-Worker

吞吐量最終鎖定在了51129,我們免費獲得了(51129 - 37193) / 37193 = 37%的性能提升。

總結

  • Dragonwell是OpenJDK在生產環境的可靠免費替代品
  • Dragonwell已經推出了JDK11的版本,阿里雲是國內第一個官方支持JDK11的廠商

Alibaba Dragonwell有大量適合應用上雲的特性:

  • Elastic Heap:減少微服務的內存消耗,內存分時複用
  • JWarmup: 減少預熱過程,提高交付效率
  • 多租戶: 支撐微服務合併部署,提高部署密度和RPC效率
  • Wisp 協程: 提高微服務的吞吐量

更多Alibaba Dragonwell的信息可以在Dragonwell官網 http://dragonwell-jdk.io/ 看到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章