阿里技术专家:进击的 Java ,云原生时代的蜕变
導(dǎo)讀:云原生時(shí)代的來(lái)臨,與Java 開(kāi)發(fā)者到底有什么聯(lián)系?有人說(shuō),云原生壓根不是為了 Java 存在的。然而,本文的作者卻認(rèn)為云原生時(shí)代,Java 依然可以勝任“巨人”的角色。作者希望通過(guò)一系列實(shí)驗(yàn),開(kāi)拓同學(xué)視野,提供有益思考。
在企業(yè)軟件領(lǐng)域,Java 依然是絕對(duì)王者,但它讓開(kāi)發(fā)者既愛(ài)又恨。一方面因?yàn)槠湄S富的生態(tài)和完善的工具支持,可以極大提升了應(yīng)用開(kāi)發(fā)效率;但在運(yùn)行時(shí)效率方面,Java 也背負(fù)著”內(nèi)存吞噬者“,“CPU 撕裂者“的惡名,持續(xù)受到 NodeJS、Python、Golang 等新老語(yǔ)言的挑戰(zhàn)。
在技術(shù)社區(qū),我們經(jīng)常看到有人在唱衰 Java 技術(shù),認(rèn)為其不再符合云原生計(jì)算發(fā)展的趨勢(shì)。先拋開(kāi)上面這些觀點(diǎn),我們首先思考一下云原生對(duì)應(yīng)用運(yùn)行時(shí)的不同需求:- 體積更小:對(duì)于微服務(wù)分布式架構(gòu)而言,更小的體積意味著更少的下載帶寬,更快的分發(fā)下載速度。
- 啟動(dòng)速度更快:對(duì)于傳統(tǒng)單體應(yīng)用,啟動(dòng)速度與運(yùn)行效率相比不是一個(gè)關(guān)鍵的指標(biāo)。原因是,這些應(yīng)用重啟和發(fā)布頻率相對(duì)較低。然而對(duì)于需要快速迭代、水平擴(kuò)展的微服務(wù)應(yīng)用而言,更快的的啟動(dòng)速度就意味著更高的交付效率,和更加快速的回滾。尤其當(dāng)你需要發(fā)布一個(gè)有數(shù)百個(gè)副本的應(yīng)用時(shí),緩慢的啟動(dòng)速度就是時(shí)間殺手。對(duì)于Serverless 應(yīng)用而言,端到端的冷啟動(dòng)速度則更為關(guān)鍵,即使底層容器技術(shù)可以實(shí)現(xiàn)百毫秒資源就緒,如果應(yīng)用無(wú)法在 500ms 內(nèi)完成啟動(dòng),用戶就會(huì)感知到訪問(wèn)延遲。
- 占用資源更少:運(yùn)行時(shí)更低的資源占用,意味著更高的部署密度和更低的計(jì)算成本。同時(shí),在 JVM 啟動(dòng)時(shí)需要消耗大量? CPU資源對(duì)字節(jié)碼進(jìn)行編譯,降低啟動(dòng)時(shí)資源消耗,可以減少資源爭(zhēng)搶,更好保障其他應(yīng)用 SLA。
- 支持水平擴(kuò)展:JVM 的內(nèi)存管理方式導(dǎo)致其對(duì)大內(nèi)存管理的相對(duì)低效,一般應(yīng)用無(wú)法通過(guò)配置更大的 heap size 實(shí)現(xiàn)性能提升,很少有 Java 應(yīng)用能夠有效使用 16G 內(nèi)存或者更高。另一方面,隨著內(nèi)存成本的下降和虛擬化的流行,大內(nèi)存配比已經(jīng)成為趨勢(shì)。所以我們一般是采用水平擴(kuò)展的方式,同時(shí)部署多個(gè)應(yīng)用副本,在一個(gè)計(jì)算節(jié)點(diǎn)中可能運(yùn)行一個(gè)應(yīng)用的多個(gè)副本來(lái)提升資源利用率。
熱身準(zhǔn)備
熟悉 Spring 框架的開(kāi)發(fā)者大多對(duì) Spring Petclinic?不會(huì)陌生。本文將借助這個(gè)著名示例應(yīng)用來(lái)演示如何讓我們的 Java 應(yīng)用變得更小、更快、更輕、更強(qiáng)大!
我們 fork 了 IBM 的 Michael Thompson 的示例,并做了一些調(diào)整。
$?git?clone?https://github.com/denverdino/adopt-openj9-spring-boot $?cd?adopt-openj9-spring-boot 首先,我們會(huì)為 PetClinic 應(yīng)用構(gòu)建一個(gè) Docker 鏡像。在 Dockerfile 中,我們利用 OpenJDK 作為基礎(chǔ)鏡像,安裝 Maven,下載、編譯、打包 Spring PetClinic 應(yīng)用,最后設(shè)置鏡像的啟動(dòng)參數(shù)完成鏡像構(gòu)建。$?cat?Dockerfile.openjdk FROM?adoptopenjdk/openjdk8 RUN?sed?-i?'s/archive.ubuntu.com/mirrors.aliyun.com/'?/etc/apt/sources.list RUN?apt-get?update RUN?apt-get?install?-y?git?maven WORKDIR?/tmp RUN?git?clone?https://github.com/spring-projects/spring-petclinic.git WORKDIR?/tmp/spring-petclinic RUN?mvn?install WORKDIR?/tmp/spring-petclinic/target CMD?["java","-jar","spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar"] 構(gòu)建鏡像并執(zhí)行$?docker?build?-t?petclinic-openjdk-hotspot?-f?Dockerfile.openjdk?. $?docker?run?--name?hotspot?-p?8080:8080?--rm?petclinic-openjdk-hotspot|??????_,,,--,,_/,`.-'`'???._??-;;,________?__|,4-??)?)_???.;.(__`'-'__?????___?__????_?___?_______|???????|?'---''(_/._)-'(__)???|???|???|???|??|??|?|???|???????||????_??|????___|_?????_|???????|???|???|???|???|_|?|???|???????|?__?_?_|???|_|?|???|___??|???|?|???????|???|???|???|???????|???|???????|????|????___|????___|?|???|?|??????_|???|___|???|??_????|???|??????_|?????|???|???|???|___??|???|?|?????|_|???????|???|?|?|???|???|?????|_????)?)?)?)|___|???|_______|?|___|?|_______|_______|___|_|??|__|___|_______|??/?/?/?/==================================================================/_/_/_/ ... 2019-09-11?01:58:23.156??INFO?1?---?[???????????main]?o.s.b.w.embedded.tomcat.TomcatWebServer??:?Tomcat?started?on?port(s):?8080?(http)?with?context?path?'' 2019-09-11?01:58:23.158??INFO?1?---?[???????????main]?o.s.s.petclinic.PetClinicApplication?????:?Started?PetClinicApplication?in?7.458?seconds?(JVM?running?for?8.187) 可以通過(guò) http://localhost:8080/ 訪問(wèn)應(yīng)用界面。檢查一下構(gòu)建出的 Docker 鏡像, ”petclinic-openjdk-openj9“ 的大小為 871MB,而基礎(chǔ)鏡像 ”adoptopenjdk/openjdk8“ 僅有 300MB!這貨也太膨脹了!$?docker?images?petclinic-openjdk-hotspot REPOSITORY??????????????????TAG?????????????????IMAGE?ID????????????CREATED?????????????SIZE petclinic-openjdk-hotspot???latest??????????????469f73967d03????????26?hours?ago????????871MB 原因是:為了構(gòu)建 Spring 應(yīng)用,我們?cè)阽R像中引入了一系列編譯時(shí)依賴,如 Git,Maven 等,并產(chǎn)生了大量臨時(shí)的文件。然而這些內(nèi)容在運(yùn)行時(shí)是不需要的。在著名的軟件12要素第五條明確指出了,”Strictly separate build and run stages.“ 嚴(yán)格分離構(gòu)建和運(yùn)行階段,不但可以幫助我們提升應(yīng)用的可追溯性,保障應(yīng)用交付的一致性,同時(shí)也可以減少應(yīng)用分發(fā)的體積,減少安全風(fēng)險(xiǎn)。
鏡像瘦身
Docker 提供了 Multi-stage Build(多階段構(gòu)建),可以實(shí)現(xiàn)鏡像瘦身。原圖
我們將鏡像構(gòu)建分成兩個(gè)階段:
- 在 ”build“ 階段依然采用 JDK 作為基礎(chǔ)鏡像,并利用 Maven 進(jìn)行應(yīng)用構(gòu)建;
- 在最終發(fā)布的鏡像中,我們會(huì)采用 JRE 版本作為基礎(chǔ)鏡像,并從”build“ 鏡像中直接拷貝出生成的 jar 文件。這意味著在最終發(fā)布的鏡像中,只包含運(yùn)行時(shí)所需必要內(nèi)容,不包含任何編譯時(shí)依賴,大大減少了鏡像體積。
查看一下新鏡像大小,從 871MB 減少到 167MB!
$?docker?build?-t?petclinic-openjdk-hotspot-slim?-f?Dockerfile.openjdk-slim?. ... $?docker?images?petclinic-openjdk-hotspot-slim REPOSITORY???????????????????????TAG?????????????????IMAGE?ID????????????CREATED?????????????SIZE petclinic-openjdk-hotspot-slim???latest??????????????d1f1ca316ec0????????26?hours?ago????????167MB 鏡像瘦身之后將大大加速應(yīng)用分發(fā)速度,我們是否有辦法優(yōu)化應(yīng)用的啟動(dòng)速度呢?從 JIT 到 AOT —啟動(dòng)提速
為了解決 Java 啟動(dòng)的性能瓶頸,我們首先需要理解 JVM 的實(shí)現(xiàn)原理。為了實(shí)現(xiàn)“一次編寫(xiě),隨處運(yùn)行”的能力,Java 程序會(huì)被編譯成實(shí)現(xiàn)架構(gòu)無(wú)關(guān)的字節(jié)碼。JVM 在運(yùn)行時(shí)將字節(jié)碼轉(zhuǎn)換成本地機(jī)器碼執(zhí)行。這個(gè)轉(zhuǎn)換過(guò)程決定了 Java 應(yīng)用的啟動(dòng)和運(yùn)行速度。為了提升執(zhí)行效率,JVM 引入了 JIT compiler(Just in Time Compiler,即時(shí)編譯器),其中 Sun/Oracle 公司的 HotSpot 是最著名 JIT 編譯器實(shí)現(xiàn)。
HotSpot 提供了自適應(yīng)優(yōu)化器,可以動(dòng)態(tài)分析、發(fā)現(xiàn)代碼執(zhí)行過(guò)程中的關(guān)鍵路徑,并進(jìn)行編譯優(yōu)化。HotSpot 的出現(xiàn)極大提升了Java 應(yīng)用的執(zhí)行效率,在 Java 1.4 以后成為了缺省的 VM 實(shí)現(xiàn)。但是 HotSpot VM 在啟動(dòng)時(shí)才對(duì)字節(jié)碼進(jìn)行編譯,一方面導(dǎo)致啟動(dòng)時(shí)執(zhí)行效率不高,一方面編譯和優(yōu)化需要很多的 CPU 資源,拖慢了啟動(dòng)速度。我們是否可以優(yōu)化這個(gè)過(guò)程,提升啟動(dòng)速度呢?
熟悉 Java 江湖歷史的同學(xué)應(yīng)該會(huì)知道 IBM J9 VM,它是用于 IBM 企業(yè)級(jí)軟件產(chǎn)品的一款高性能的 JVM,幫助 IBM 奠定了商業(yè)應(yīng)用平臺(tái)中間件的霸主地位。2017 年 9 月,IBM 將 J9 捐獻(xiàn)給 Eclipse 基金會(huì),并更名 Eclipse OpenJ9,開(kāi)啟開(kāi)源之旅。OpenJ9 提供了 Shared Class Cache (SCC 共享類緩存) 和 Ahead-of-Time (AOT 提前編譯) 技術(shù),顯著減少了 Java 應(yīng)用啟動(dòng)時(shí)間。SCC 是一個(gè)內(nèi)存映射文件,包含了J9 VM 對(duì)字節(jié)碼的執(zhí)行分析信息和已經(jīng)編譯生成的本地代碼。開(kāi)啟 AOT 編譯后,會(huì)將 JVM 編譯結(jié)果保存在 SCC 中,在后續(xù) JVM 啟動(dòng)中可以直接重用。與啟動(dòng)時(shí)進(jìn)行的 JIT 編譯相比,從 SCC 加載預(yù)編譯的實(shí)現(xiàn)要快得多,而且消耗的資源要更少。啟動(dòng)時(shí)間可以得到明顯改善。
我們開(kāi)始構(gòu)建一個(gè)包含 AOT 優(yōu)化的 Docker 應(yīng)用鏡像:$cat?Dockerfile.openj9.warmed FROM?adoptopenjdk/openjdk8-openj9?AS?build RUN?sed?-i?'s/archive.ubuntu.com/mirrors.aliyun.com/'?/etc/apt/sources.list RUN?apt-get?update RUN?apt-get?install?-y?git?maven WORKDIR?/tmp RUN?git?clone?https://github.com/spring-projects/spring-petclinic.git WORKDIR?/tmp/spring-petclinic RUN?mvn?install FROM?adoptopenjdk/openjdk8-openj9:jre8u222-b10_openj9-0.15.1-alpine COPY?--from=build?/tmp/spring-petclinic/target/spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar?spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar #?Start?and?stop?the?JVM?to?pre-warm?the?class?cache RUN?/bin/sh?-c?'java?-Xscmx50M?-Xshareclasses?-Xquickstart?-jar?spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar?&'?;?sleep?20?;?ps?aux?|?grep?java?|?grep?petclinic?|?awk?'{print?$1}'?|?xargs?kill?-1 CMD?["java","-Xscmx50M","-Xshareclasses","-Xquickstart",?"-jar","spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar"] 其中 Java 參數(shù) -Xshareclasses開(kāi)啟SCC,-Xquickstart開(kāi)啟AOT。在 Dockerfile 中,我們運(yùn)用了一個(gè)技巧來(lái)預(yù)熱 SCC。在構(gòu)建過(guò)程中啟動(dòng) JVM 加載應(yīng)用,并開(kāi)啟 SCC 和 AOT,在應(yīng)用啟動(dòng)后停止 JVM。這樣就在 Docker 鏡像中包含了生成的 SCC 文件。
然后,我們來(lái)構(gòu)建 Docker 鏡像并啟動(dòng)測(cè)試應(yīng)用,$?docker?build?-t?petclinic-openjdk-openj9-warmed-slim?-f?Dockerfile.openj9.warmed-slim?. $?docker?run?--name?hotspot?-p?8080:8080?--rm?petclinic-openjdk-openj9-warmed-slim ... 2019-09-11?03:35:20.192??INFO?1?---?[???????????main]?o.s.b.w.embedded.tomcat.TomcatWebServer??:?Tomcat?started?on?port(s):?8080?(http)?with?context?path?'' 2019-09-11?03:35:20.193??INFO?1?---?[???????????main]?o.s.s.petclinic.PetClinicApplication?????:?Started?PetClinicApplication?in?3.691?seconds?(JVM?running?for?3.952) ...
可以看到,啟動(dòng)時(shí)間已經(jīng)從之前的 8.2s 減少到 4s,提升近50%。
docker stats查看資源消耗。
$?./run-hotspot-4.sh ... Wait?a?while?... CONTAINER?ID????????NAME????????????????CPU?%???????????????MEM?USAGE?/?LIMIT?????MEM?%???????????????NET?I/O?????????????BLOCK?I/O???????????PIDS 0fa58df1a291????????instance4???????????0.15%???????????????597.1MiB?/?5.811GiB???10.03%??????????????726B?/?0B???????????0B?/?0B?????????????33 48f021d728bb????????instance3???????????0.13%???????????????648.6MiB?/?5.811GiB???10.90%??????????????726B?/?0B???????????0B?/?0B?????????????33 a3abb10078ef????????instance2???????????0.26%???????????????549MiB?/?5.811GiB?????9.23%???????????????726B?/?0B???????????0B?/?0B?????????????33 6a65cb1e0fe5????????instance1???????????0.15%???????????????641.6MiB?/?5.811GiB???10.78%??????????????906B?/?0B???????????0B?/?0B?????????????33 ... 然后使用基于 OpenJ9 VM 的鏡像,同時(shí)啟動(dòng) 4 個(gè) Docker 應(yīng)用實(shí)例,并查看資源消耗。$?./run-openj9-warmed-4.sh ... Wait?a?while?... CONTAINER?ID????????NAME????????????????CPU?%???????????????MEM?USAGE?/?LIMIT?????MEM?%???????????????NET?I/O?????????????BLOCK?I/O???????????PIDS 3a0ba6103425????????instance4???????????0.09%???????????????119.5MiB?/?5.811GiB???2.01%???????????????1.19kB?/?0B?????????0B?/?446MB??????????39 c07ca769c3e7????????instance3???????????0.19%???????????????119.7MiB?/?5.811GiB???2.01%???????????????1.19kB?/?0B?????????16.4kB?/?120MB??????39 0c19b0cf9fc2????????instance2???????????0.15%???????????????112.1MiB?/?5.811GiB???1.88%???????????????1.2kB?/?0B??????????22.8MB?/?23.8MB?????39 95a9c4dec3d6????????instance1???????????0.15%???????????????108.6MiB?/?5.811GiB???1.83%???????????????1.45kB?/?0B?????????102MB?/?414MB???????39 ... 與 HotSpot VM 相比,OpenJ9 的場(chǎng)景下應(yīng)用內(nèi)存占用從平均 600MB 下降到 120MB。驚喜不驚喜?通常而言,HotSpot JIT 比 AOT 可以進(jìn)行更加全面和深入的執(zhí)行路徑優(yōu)化,從而有更高的運(yùn)行效率。為了解決這個(gè)矛盾,OpenJ9 的 AOT SCC 只在啟動(dòng)階段生效,在后續(xù)運(yùn)行中會(huì)繼續(xù)利用JIT進(jìn)行分支預(yù)測(cè)、代碼內(nèi)聯(lián)等深度編譯優(yōu)化。更多關(guān)于 OpenJ9 SCC 和 AOT 的技術(shù)介紹,請(qǐng)參考
- https://www.ibm.com/developerworks/cn/java/j-class...
- https://www.ibm.com/developerworks/cn/java/j-optim...
- HotSpot 在 Class Data Sharing (CDS) 和 AOT 方面也有了很大進(jìn)展,但是 IBM J9 在這方面更加成熟。期待阿里的
原生代碼編譯
為了將 Java 應(yīng)用編譯成本地可執(zhí)行代碼,我們首先要解決 JVM 和應(yīng)用框架在運(yùn)行時(shí)的動(dòng)態(tài)性挑戰(zhàn)。JVM 提供了靈活的類加載機(jī)制,Spring 的依賴注入(DI,Dependency-injection)可以實(shí)現(xiàn)運(yùn)行時(shí)動(dòng)態(tài)類加載和綁定。在 Spring 框架中,反射,Annotation 運(yùn)行時(shí)處理器等技術(shù)也被廣泛應(yīng)用。這些動(dòng)態(tài)性一方面提升了應(yīng)用架構(gòu)的靈活性和易用性,另一方面也降低了應(yīng)用的啟動(dòng)速度,使得 AOT 原生編譯和優(yōu)化變得非常復(fù)雜。為了解決這些挑戰(zhàn),社區(qū)有很多有趣的探索,Micronaut 是其中一個(gè)優(yōu)秀代表。與 Spring 框架序不同,Micronaut 提供了編譯時(shí)的依賴注入和AOP處理能力,并最小化反射和動(dòng)態(tài)代理的使用。Micronaut 應(yīng)用有著更快的啟動(dòng)速度和更低的內(nèi)存占用。更加讓我們更感興趣的是 Micronaut 支持與 GraalVM?配合,可以將 Java 應(yīng)用編譯成為本地執(zhí)行代碼全速運(yùn)行。注:GraalVM 是 Oracle 推出的一種新型通用虛擬機(jī),支持多種語(yǔ)言,可以將Java應(yīng)用程序編譯為本地原生應(yīng)用。原圖下面開(kāi)始我們的探險(xiǎn),我們利用 Mitz 提供的 Micronaut 版本 PetClinic 示例工程并做了一點(diǎn)點(diǎn)調(diào)整。(使用 Graal VM 19.2)$?git?clone?https://github.com/denverdino/micronaut-petclinic $?cd?micronaut-petclinic 其中 Docker 鏡像的內(nèi)容如下:$?cat?Dockerfile FROM?maven:3.6.1-jdk-8?as?build COPY?./?/micronaut-petclinic/ WORKDIR?/micronaut-petclinic RUN?mvn?package FROM?oracle/graalvm-ce:19.2.0?as?graalvm RUN?gu?install?native-image WORKDIR?/work COPY?--from=build?/micronaut-petclinic/target/micronaut-petclinic-*.jar?. RUN?native-image?--no-server?-cp?micronaut-petclinic-*.jar FROM?frolvlad/alpine-glibc EXPOSE?8080 WORKDIR?/app COPY?--from=graalvm?/work/petclinic?. CMD?["/app/petclinic"] 其中
- 在 "build" 階段,利用Maven構(gòu)建 Micronaut 版本的 PetClinic 應(yīng)用,
- 在 "graalvm" 階段,我們通過(guò) native-image將 PetClinic jar 文件轉(zhuǎn)化成可執(zhí)行文件。
- 在最終階段,將本地可執(zhí)行文件加入一個(gè) Alpine Linux 基礎(chǔ)鏡像
Micronaut 和 Graal VM 還在快速發(fā)展中,遷移一個(gè) Spring 應(yīng)用還有不少工作需要考慮。此外 Graal VM 的調(diào)試、監(jiān)控等工具鏈還不夠完善。但是這已經(jīng)讓我們看到了曙光,Java 應(yīng)用和 Serverless 的世界不再遙遠(yuǎn)。由于篇幅有限,對(duì) Graal VM 和Micronaut 有興趣的同學(xué)可以參考
https://docs.micronaut.io/latest/guide/index.html#...
https://www.exoscale.com/syslog/how-to-integrate-s...
總結(jié)與后記
作為進(jìn)擊的巨人,Java ?技術(shù)在云原生時(shí)代也在不停地進(jìn)化。在JDK 8u191 和 JDK 10 之后,JVM 增強(qiáng)了在?在 Docker 容器中對(duì)資源的感知。同時(shí)社區(qū)也在多個(gè)不同方向探索 Java 技術(shù)棧的邊界。JVM OpenJ9 作為傳統(tǒng)VM的一員,在對(duì)現(xiàn)有 Java 應(yīng)用保持高度兼容的同時(shí),對(duì)啟動(dòng)速度和內(nèi)存占用做了細(xì)致的優(yōu)化,比較適于與現(xiàn)有 Spring 等微服務(wù)架構(gòu)配合使用。而 Micronaut/Graal VM 則另辟蹊徑,通過(guò)改變編程模型和編譯過(guò)程,將應(yīng)用的動(dòng)態(tài)性盡可能提前到編譯時(shí)期處理,極大優(yōu)化了應(yīng)用啟動(dòng)時(shí)間,在 Serverless 領(lǐng)域前景可期。這些設(shè)計(jì)思路都值得我們借鑒。在云原生時(shí)代,我們要能夠在橫向的應(yīng)用開(kāi)發(fā)生命周期中,將開(kāi)發(fā)、交付、運(yùn)維過(guò)程進(jìn)行有效的分割和重組,提升研發(fā)協(xié)同效率;并且要能在整個(gè)縱向軟件技術(shù)棧中,在編程模型、應(yīng)用運(yùn)行時(shí)和基礎(chǔ)設(shè)施等多層面進(jìn)行系統(tǒng)優(yōu)化,實(shí)現(xiàn) radical simplification,提升系統(tǒng)效率。
感謝這個(gè)時(shí)代,感謝所有幫助和支持我們的小伙伴,感謝所有追夢(mèng)的技術(shù)人,我們一起開(kāi)拓云原生的未來(lái)。
掃描下方二維碼添加小助手,與 8000 位云原生愛(ài)好者討論技術(shù)趨勢(shì),實(shí)戰(zhàn)進(jìn)階!
進(jìn)群暗號(hào):公司-崗位-城市RECOMMEND云原生推薦
總結(jié)
以上是生活随笔為你收集整理的阿里技术专家:进击的 Java ,云原生时代的蜕变的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: js笔记(10)之无缝滚动
- 下一篇: php连接MYSQL(1)