高密度Java應用部署的一些實踐

infoq發表於2014-08-21

傳統的Java應用部署模式,一般遵循“硬體->作業系統->JVM->Java應用”這種自底向上的部署結構,其中JEE應用可以細化為“硬體->作業系統->JVM->JEE容器->JEE應用”的部署結構。這種部署結構往往比較重,作業系統、JVM和JEE容器造成的overhead很高,而很多時候一個Java應用並不需要跑滿整個硬體的資源,導致這種模式的資源利用率是比較低的。

而另一方面,硬體虛擬化技術逐漸成熟:VMware Hypervisor、Xen、KVM、Power LPAR等技術能夠幫助我們在同一個硬體上部署多個作業系統例項,而時下流行的OS Container技術如LXC、Docker等,則是把作業系統虛擬化為多個例項,實現更輕量級的虛擬化。無論哪個層面的虛擬化,其目的都是對資源利用率更加高效的追求,從而成為如今構建雲端計算平臺底層架構的基礎技術。

Java應用也可以通過同樣的思路來實現高密度的部署。JVM虛擬化是比OS虛擬化更高一層的做法,可以更大程度的提高資源利用率,降低平均應用的部署成本。本文將介紹Multi-tenant JVM這一方案實現高密度Java應用部署的一些特點和思路。

背景介紹

早在2004年,Sun公司就提出過Java應用虛擬化這方面的想法。當時Grzegorz Czajkowski領導了一個叫做巴塞羅那的研究專案,該專案基於Java HotSpot虛擬1.5版本開發了Multi-Tasking Virtual Machine(MVM)。MVM的目的旨在提高Java程式的啟動速度,節省記憶體開銷。不過自從Sun被甲骨文收購後,我們沒有聽到關於該專案的任何新的進展。

儘管我們沒有看到MVM成功產品化,不過它卻留下兩個JSR規範:JSR121和JSR284。對於JSR284,目前在java.net上有一個實現它的孵化專案。

從2009年開始,我所在的IBM Java團隊開始研究Java應用的SaaS化方案,即讓一個應用例項服務於多個租戶。為了保證多個租戶在使用同一個應用例項時候資料的隔離,該方案在應用這個層面做了一些Bytecode Instrument(BCI)的工作,主要通過改寫getstatic/putstatic使每個租戶有獨立的類的靜態資料拷貝而沒有相互影響。但是,該方案在Bytecode層面更改帶來的額外效能開銷, 以及Java Reflection等訪問帶來的安全性/正確性的問題。 而且,除了資料上的隔離,也需要針對關鍵性的資源譬如CPU、Heap、IO等資源的使用進行管理,於是該方案下沉到了JVM層面,形成現在的多租戶JVM(Multi-tenant JVM)方案。

Multi-tenant JVM是JVM層面的虛擬化,其思路是把多個Java應用部署在同一個JVM上,讓這些應用共享底層的GC、JIT、Java執行時庫等基礎元件。除了IBM的團隊之外,愛爾蘭的Waratek公司也實現了多租戶的JVM。和IBM Multi-tenant JVM類似,Waratek允許多個應用執行在同一個CloudVM上,每一個應用執行在一個叫Java Virtual Container(JVC)的容器裡。從現有公開的資料開看,IBM Multi-tenant JVM是基於Java 7的,而Waratek是基於Java 6的,兩者支援的CPU架構和平臺也有所不同。

此外,JEE方面在兩年前也有討論計劃增加對PaaS和多租戶的支援,這項提議旨在定義PaaS環境下如何使得JEE應用支援多租戶,保證不同租戶在使用這些應用時相互隔離,以及資源方面的管理(如JMS資源),不過該項提議已經推遲到JEE 8。

除了提升部署密度之外,多租戶的另一項好處在於應用啟動的加速。快速的程式啟動受益於不同的應用共享同一個JVM,我們稱之為javad。Java核心的類庫在javad執行後,不再需要被重新裝載和定義。你也許可以用Nailgun來加速你的啟動時間,但Nailgun的問題是沒有安全的資料隔離,這包括類的靜態資料以及Java屬性值,而且Nailgun在易用性等方面也不如Multi-tenent JVM。

多租戶JVM的實現思路

跟傳統JVM相比,多租戶JVM的主要工作圍繞隔離而進行,其針對JVM/JDK的改動主要實現三個方面的目標:

  1. 租戶之間的資料隔離
  2. Java類庫支援多租戶語境
  3. 資源管理隔離

租戶之間的資料隔離

讓每個租戶應用擁有獨立的類靜態資料拷貝,這個目標主要通過修改getstatic/putstatic位元組碼指令實現。下面是一個簡單的例子:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

每一個執行在Multi-tenant JVM上的程式都有不同System.out例項。就java.lnag.System內部實現來說,out是其類靜態變數:

public final class System {

    // The standard input, output, and error streams.
    // Typically, these are connected to the shell which
    // ran the Java program.
    /**
     * Default input stream
     */
    public static final InputStream in = null;
    /**
     * Default output stream
     */
    public static final PrintStream out = null;
    /**
     * Default error output stream
     */
    public static final PrintStream err = null;
……
}

Multi-tenant JVM對於標準的JVM行為進行的更改如下:

  • 每一個租戶第一次使用java/lang/System時,都會觸發它的初始化,也就是<clinit>。而一般的JVM,java/lang/System只會被初始化一次。
  • <clinit>的執行,對於每一個靜態成員變數存取,都被重新定向到了具體的租戶存貯空間。比如對於out = null賦值,putstatic執行時實際上會找到當前的租戶,然後把值存到該租戶的空間去 ,getstatic有著類似的道理。

Java類庫支援多租戶語境

這部分主要通過改造類庫實現,具體的功能包括:

  • System.exit(code) 呼叫只會使當前租戶退出,而不會令整個JVM退出。而租戶申請的一些諸如File/Socket控制程式碼之類系統資源,會隨著租戶的推出而被釋放。
  • 租戶A不可能通過類似如下
ThreadGroup group = Thread.currentThread().getThreadGroup();
ThreadGroup parent = group.getParent();

列舉執行緒的辦法獲得租戶B的執行緒。不同租戶的執行緒分屬於不同的執行緒組。

  • Java屬性值的隔離,比如同樣的語句System.getProperty(“name”)對於不同的租戶可能是不同的值。

資源管理隔離

這是Multi-tenant JVM很重要的功能。在Multi-tenant JVM上,Heap/CPU/Disk IO/Net IO這些資源的使用是受資源策略保護的,比如你可以去限制某個租戶它的CPU最少可以使用20%,而在系統空閒時,最大可以用到100%。

Multi-tenant JVM通過Token Bucket來對IO(Disk/Net)和CPU資源進行管理。對於IO而言,Multi-tenant JVM截獲IO有關的OS API呼叫,使得IO發生之前受制於我們預先規定的資源策略。我們舉個網路IO的例子,例如Java程式從Socket的讀取操作,JDK內部的實現通過JNI實際上會對應到系統的API呼叫

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

在recv呼叫發生之前,Multi-tenant JVM通過資源策略保證租戶的IO使用頻寬不會超過給它設定的限制。關於網路IO,我們這裡有一個很好的演示:

用簡單的-Xlimit:netIO=6M引數限制執行在Multi-tenant JVM上的Ftp Server頻寬上限讀寫各為6Mib/s。

關於對CPU管理,Multi-tenant JVM實現的基本的思路是,把租戶執行緒所花費的CPU時間量化為Tokens,執行時每一個租戶執行緒都會被週期性檢查是否其當前CPU時間的使用超過了給它設定的限制。如果超過,當前執行緒會被掛起,直到滿足限制為止。週期性檢查的程式碼是由Multi-tenant JVM插入到租戶執行緒裡去的,對於使用者程式而言完全是透明的。

Multi-tenant JVM對於Heap的管理建立在Balanced GC Policy基礎之上。同一般的Java程式類似,你可以使用-Xms/-Xmx為租戶程式設定最大/最小的堆記憶體值。Balanced GC Policy基於Region對Heap進行管理,每個租戶程式根據-Xms/-Xmx的設定來為其分配Region,而租戶物件的分配也必然只能發生在它自己擁有的區域內。

多租戶JVM的用法與限制

IBM釋出的Java 7 R1預設支援多租戶JVM,在命令列上新增-Xmt引數即可啟用。由於多租戶JVM對JVM的變更,JNI Native Libraries、JVMTI以及GUI programs在多租戶狀態下的使用是受限制的。Multi-tenant JVM並未實現對JNI的隔離,所以不同的租戶應用不能裝載依賴同樣的JNI Native Lib,所有發生在JNI Native Lib裡的IO,不會受限於該租戶資源消費策略。同樣的情況適用於CPU以及Memory。

Multi-tenant JVM目前沒有實現對JVMTI Agent的改造用以支援我們前面所描述的靜態資料的隔離,這可能會對使用者如果想除錯Java核心類庫程式碼(不是使用者程式碼)造成困擾。

關於GUI,Multi-tenant JVM沒有實現底層對於UI程式訊息佇列的隔離,所以不支援在同一個Multi-tenant JVM執行大於1個的GUI程式。

還有一點,不要在非Daemon執行緒裡寫 “暴力”的死迴圈程式碼,例如:

while(true)
{  
try () {
    ....
} catch(Throwable t) {
{
}

}

最後需要注意的是,當開啟IO資源控制時,儘量一次寫出更多的位元組,避免影響程式的IO效能。

總結

Multi-tenant JVM目前在應用啟動時間和更小的記憶體佔用開銷方面已經被證實有效。根據目前的一些基準測試結果來看,對於簡單的應用,相較於一般JVM,Multi-tenant JVM可以獲得5~6倍的執行個數。後續計劃釋出的版本仍然會集中在提高啟動時間、更小的記憶體開銷這兩個方面,也會陸續有一些效能的報告發布。

長遠來看,Multi-tenant JVM會基於使用者、IBM產品線以及技術社群等的反饋,做進一步的提高,以及解決一些目前所存在的侷限性,比如對於JNI隔離的支援,JVMTi的多租戶支援等等。

相關文章