高密度Java應用部署的一些實踐
傳統的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的改動主要實現三個方面的目標:
- 租戶之間的資料隔離
- Java類庫支援多租戶語境
- 資源管理隔離
租戶之間的資料隔離
讓每個租戶應用擁有獨立的類靜態資料拷貝,這個目標主要通過修改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的多租戶支援等等。
相關文章
- 容器化部署實踐之Django應用部署(二)Django
- Kubernetes 部署 Laravel 應用的最佳實踐Laravel
- React/Vue 實現的前端應用, java/Go/Python 實現的後端應用,前後端分離的應用部署的最佳實踐ReactVue前端JavaGoPython後端
- 應用部署初探:6個保障安全的最佳實踐
- 使用Docker容器化部署實踐之Django應用部署(一)DockerDjango
- Java 反射機制應用實踐Java反射
- Java反射機制應用實踐Java反射
- Istio實踐(1)-環境搭建及應用部署
- Java RPC原理及Dubbo的實踐應用JavaRPC
- Docker+Jenkins+Gitlab+Django應用部署實踐DockerJenkinsGitlabDjango
- DevOps最佳實踐之應用開發和部署dev
- Java Web應用的程式碼分層最佳實踐。JavaWeb
- Java:應用Observer介面實踐Observer模式薦JavaServer模式
- Java 應用效能調優最強實踐指南!Java
- 不只是編排引擎,OpenStack Heat之應用部署實踐
- Websphere Application Server 環境配置與應用部署最佳實踐WebAPPServer
- Java併發:分散式應用限流 Redis + Lua 實踐Java分散式Redis
- 混合應用中的javascript實踐JavaScript
- SpringBoot魔法堂:應用熱部署實踐與原理淺析Spring Boot熱部署
- Java應用構建並部署ECSJava
- TiDB應用實踐TiDB
- Oracle Audit 應用實踐Oracle
- Java系列 | 遠端熱部署在美團的落地實踐Java熱部署
- 學用Java Web Start 部署應用程式 (轉)JavaWeb
- SpringCloud 應用在 Kubernetes 上的最佳實踐 — 部署篇(工具部署)SpringGCCloud
- 應用實踐——新東方實時數倉實踐
- Akka實踐一些總結(java專案)Java
- 深度學習的應用與實踐深度學習
- TiDB 在小米的應用實踐TiDB
- 策略模式在應用中的實踐模式
- 使用Docker部署Python應用的一些經驗總結DockerPython
- SpringCloud 應用在 Kubernetes 上的最佳實踐 — 部署篇(開發部署)SpringGCCloud
- Spring AOT應用實踐Spring
- Android快應用實踐Android
- 談談 django 應用實踐Django
- GitOps 應用實踐系列 - Argo CD 上手實踐GitGo
- LLM應用實戰: 文件問答系統Kotaemon-1. 簡介及部署實踐
- 關於Java健壯性的一些思考與實踐!Java