進(jìn)擊的 Java ,云原生時(shí)代的蛻變

2021-01-31    分類: 網(wǎng)站建設(shè)

云原生時(shí)代的來臨,與Java 開發(fā)者到底有什么聯(lián)系?有人說,云原生壓根不是為了 Java 存在的。然而,本文的作者卻認(rèn)為云原生時(shí)代,Java 依然可以勝任“巨人”的角色。作者希望通過一系列實(shí)驗(yàn),開拓同學(xué)視野,提供有益思考。

在企業(yè)軟件領(lǐng)域,Java 依然是絕對王者,但它讓開發(fā)者既愛又恨。一方面因?yàn)槠湄S富的生態(tài)和完善的工具支持,可以極大提升了應(yīng)用開發(fā)效率;但在運(yùn)行時(shí)效率方面,Java 也背負(fù)著”內(nèi)存吞噬者“,“CPU 撕裂者“的惡名,持續(xù)受到 NodeJS、Python、Golang 等新老語言的挑戰(zhàn)。

在技術(shù)社區(qū),我們經(jīng)??吹接腥嗽诔?Java 技術(shù),認(rèn)為其不再符合云原生計(jì)算發(fā)展的趨勢。先拋開上面這些觀點(diǎn),我們首先思考一下云原生對應(yīng)用運(yùn)行時(shí)的不同需求:

體積更小:對于微服務(wù)分布式架構(gòu)而言,更小的體積意味著更少的下載帶寬,更快的分發(fā)下載速度。

啟動速度更快:對于傳統(tǒng)單體應(yīng)用,啟動速度與運(yùn)行效率相比不是一個(gè)關(guān)鍵的指標(biāo)。原因是,這些應(yīng)用重啟和發(fā)布頻率相對較低。然而對于需要快速迭代、水平擴(kuò)展的微服務(wù)應(yīng)用而言,更快的的啟動速度就意味著更高的交付效率,和更加快速的回滾。尤其當(dāng)你需要發(fā)布一個(gè)有數(shù)百個(gè)副本的應(yīng)用時(shí),緩慢的啟動速度就是時(shí)間殺手。對于Serverless 應(yīng)用而言,端到端的冷啟動速度則更為關(guān)鍵,即使底層容器技術(shù)可以實(shí)現(xiàn)百毫秒資源就緒,如果應(yīng)用無法在 500ms 內(nèi)完成啟動,用戶就會感知到訪問延遲。

占用資源更少:運(yùn)行時(shí)更低的資源占用,意味著更高的部署密度和更低的計(jì)算成本。同時(shí),在 JVM 啟動時(shí)需要消耗大量 CPU資源對字節(jié)碼進(jìn)行編譯,降低啟動時(shí)資源消耗,可以減少資源爭搶,更好保障其他應(yīng)用 SLA。

支持水平擴(kuò)展:JVM 的內(nèi)存管理方式導(dǎo)致其對大內(nèi)存管理的相對低效,一般應(yīng)用無法通過配置更大的 heap size 實(shí)現(xiàn)性能提升,很少有 Java 應(yīng)用能夠有效使用 16G 內(nèi)存或者更高。另一方面,隨著內(nèi)存成本的下降和虛擬化的流行,大內(nèi)存配比已經(jīng)成為趨勢。所以我們一般是采用水平擴(kuò)展的方式,同時(shí)部署多個(gè)應(yīng)用副本,在一個(gè)計(jì)算節(jié)點(diǎn)中可能運(yùn)行一個(gè)應(yīng)用的多個(gè)副本來提升資源利用率。

熱身準(zhǔn)備

熟悉 Spring 框架的開發(fā)者大多對 Spring Petclinic 不會陌生。本文將借助這個(gè)著名示例應(yīng)用來演示如何讓我們的 Java 應(yīng)用變得更小、更快、更輕、更強(qiáng)大!

我們 fork 了 IBM 的 Michael Thompson 的示例,并做了一些調(diào)整。

  1. $ git clone https://github.com/denverdino/adopt-openj9-spring-boot 
  2. $ cd adopt-openj9-spring-boot 

首先,我們會為 PetClinic 應(yīng)用構(gòu)建一個(gè) Docker 鏡像。在 Dockerfile 中,我們利用 OpenJDK 作為基礎(chǔ)鏡像,安裝 Maven,下載、編譯、打包 Spring PetClinic 應(yīng)用,最后設(shè)置鏡像的啟動參數(shù)完成鏡像構(gòu)建。

  1. $ cat Dockerfile.openjdk 
  2. FROM adoptopenjdk/openjdk8 
  3. RUN sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/' /etc/apt/sources.list 
  4. RUN apt-get update 
  5. RUN apt-get install -y \ 
  6.     git \ 
  7.     maven 
  8. WORKDIR /tmp 
  9. RUN git clone https://github.com/spring-projects/spring-petclinic.git 
  10. WORKDIR /tmp/spring-petclinic 
  11. RUN mvn install 
  12. WORKDIR /tmp/spring-petclinic/target 
  13. CMD ["java","-jar","spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar"] 

構(gòu)建鏡像并執(zhí)行:

  1. $ docker build -t petclinic-openjdk-hotspot -f Dockerfile.openjdk . 
  2. $ docker run --name hotspot -p 8080:8080 --rm petclinic-openjdk-hotspot 
  3.               |\      _,,,--,,_ 
  4.              /,`.-'`'   ._  \-;;,_ 
  5.   _______ __|,4-  ) )_   .;.(__`'-'__     ___ __    _ ___ _______ 
  6.  |       | '---''(_/._)-'(_\_)   |   |   |   |  |  | |   |       | 
  7.  |    _  |    ___|_     _|       |   |   |   |   |_| |   |       | __ _ _ 
  8.  |   |_| |   |___  |   | |       |   |   |   |       |   |       | \ \ \ \ 
  9.  |    ___|    ___| |   | |      _|   |___|   |  _    |   |      _|  \ \ \ \ 
  10.  |   |   |   |___  |   | |     |_|       |   | | |   |   |     |_    ) ) ) ) 
  11.  |___|   |_______| |___| |_______|_______|___|_|  |__|___|_______|  / / / / 
  12.  ==================================================================/_/_/_/ 
  13. ... 
  14. 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 '' 
  15. 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) 

可以通過 http://localhost:8080/ 訪問應(yīng)用界面。

檢查一下構(gòu)建出的 Docker 鏡像, ”petclinic-openjdk-openj9“ 的大小為 871MB,而基礎(chǔ)鏡像 ”adoptopenjdk/openjdk8“ 僅有 300MB!這貨也太膨脹了!

  1. $ docker images petclinic-openjdk-hotspot 
  2. REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE 
  3. petclinic-openjdk-hotspot   latest              469f73967d03        26 hours ago        871MB 

原因是:為了構(gòu)建 Spring 應(yīng)用,我們在鏡像中引入了一系列編譯時(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)鏡像瘦身。

進(jìn)擊的 Java ,云原生時(shí)代的蛻變

我們將鏡像構(gòu)建分成兩個(gè)階段:

  • 在 ”build“ 階段依然采用 JDK 作為基礎(chǔ)鏡像,并利用 Maven 進(jìn)行應(yīng)用構(gòu)建;
  • 在最終發(fā)布的鏡像中,我們會采用 JRE 版本作為基礎(chǔ)鏡像,并從”build“ 鏡像中直接拷貝出生成的 jar 文件。這意味著在最終發(fā)布的鏡像中,只包含運(yùn)行時(shí)所需必要內(nèi)容,不包含任何編譯時(shí)依賴,大大減少了鏡像體積。
  1. $ cat Dockerfile.openjdk-slim 
  2. FROM adoptopenjdk/openjdk8 AS build 
  3. RUN sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/' /etc/apt/sources.list 
  4. RUN apt-get update 
  5. RUN apt-get install -y \ 
  6.     git \ 
  7.     maven 
  8. WORKDIR /tmp 
  9. RUN git clone https://github.com/spring-projects/spring-petclinic.git 
  10. WORKDIR /tmp/spring-petclinic 
  11. RUN mvn install 
  12. FROM adoptopenjdk/openjdk8:jre8u222-b10-alpine-jre 
  13. COPY --from=build /tmp/spring-petclinic/target/spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar 
  14. CMD ["java","-jar","spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar"] 

查看一下新鏡像大小,從 871MB 減少到 167MB!

  1. $ docker build -t petclinic-openjdk-hotspot-slim -f Dockerfile.openjdk-slim . 
  2. ... 
  3. $ docker images petclinic-openjdk-hotspot-slim 
  4. REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE 
  5. petclinic-openjdk-hotspot-slim   latest              d1f1ca316ec0        26 hours ago        167MB 

鏡像瘦身之后將大大加速應(yīng)用分發(fā)速度,我們是否有辦法優(yōu)化應(yīng)用的啟動速度呢?

從 JIT 到 AOT —啟動提速

為了解決 Java 啟動的性能瓶頸,我們首先需要理解 JVM 的實(shí)現(xiàn)原理。

為了實(shí)現(xiàn)“一次編寫,隨處運(yùn)行”的能力,Java 程序會被編譯成實(shí)現(xiàn)架構(gòu)無關(guān)的字節(jié)碼。JVM 在運(yùn)行時(shí)將字節(jié)碼轉(zhuǎn)換成本地機(jī)器碼執(zhí)行。這個(gè)轉(zhuǎn)換過程決定了 Java 應(yīng)用的啟動和運(yùn)行速度。為了提升執(zhí)行效率,JVM 引入了 JIT compiler(Just in Time Compiler,即時(shí)編譯器),其中 Sun/Oracle 公司的 HotSpot 是最著名 JIT 編譯器實(shí)現(xiàn)。

HotSpot 提供了自適應(yīng)優(yōu)化器,可以動態(tài)分析、發(fā)現(xiàn)代碼執(zhí)行過程中的關(guān)鍵路徑,并進(jìn)行編譯優(yōu)化。HotSpot 的出現(xiàn)極大提升了Java 應(yīng)用的執(zhí)行效率,在 Java 1.4 以后成為了缺省的 VM 實(shí)現(xiàn)。但是 HotSpot VM 在啟動時(shí)才對字節(jié)碼進(jìn)行編譯,一方面導(dǎo)致啟動時(shí)執(zhí)行效率不高,一方面編譯和優(yōu)化需要很多的 CPU 資源,拖慢了啟動速度。我們是否可以優(yōu)化這個(gè)過程,提升啟動速度呢?

熟悉 Java 江湖歷史的同學(xué)應(yīng)該會知道 IBM J9 VM,它是用于 IBM 企業(yè)級軟件產(chǎn)品的一款高性能的 JVM,幫助 IBM 奠定了商業(yè)應(yīng)用平臺中間件的霸主地位。2017 年 9 月,IBM 將 J9 捐獻(xiàn)給 Eclipse 基金會,并更名 Eclipse OpenJ9,開啟開源之旅。

OpenJ9 提供了 Shared Class Cache(SCC 共享類緩存)和 Ahead-of-Time(AOT 提前編譯)技術(shù),顯著減少了 Java 應(yīng)用啟動時(shí)間。

SCC 是一個(gè)內(nèi)存映射文件,包含了J9 VM 對字節(jié)碼的執(zhí)行分析信息和已經(jīng)編譯生成的本地代碼。開啟 AOT 編譯后,會將 JVM 編譯結(jié)果保存在 SCC 中,在后續(xù) JVM 啟動中可以直接重用。與啟動時(shí)進(jìn)行的 JIT 編譯相比,從 SCC 加載預(yù)編譯的實(shí)現(xiàn)要快得多,而且消耗的資源要更少。啟動時(shí)間可以得到明顯改善。

我們開始構(gòu)建一個(gè)包含 AOT 優(yōu)化的 Docker 應(yīng)用鏡像:

  1. $cat Dockerfile.openj9.warmed 
  2. FROM adoptopenjdk/openjdk8-openj9 AS build 
  3. RUN sed -i 's/archive.ubuntu.com/mirrors.aliyun.com/' /etc/apt/sources.list 
  4. RUN apt-get update 
  5. RUN apt-get install -y \ 
  6.     git \ 
  7.     maven 
  8. WORKDIR /tmp 
  9. RUN git clone https://github.com/spring-projects/spring-petclinic.git 
  10. WORKDIR /tmp/spring-petclinic 
  11. RUN mvn install 
  12. FROM adoptopenjdk/openjdk8-openj9:jre8u222-b10_openj9-0.15.1-alpine 
  13. COPY --from=build /tmp/spring-petclinic/target/spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar 
  14. # Start and stop the JVM to pre-warm the class cache 
  15. 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 
  16. CMD ["java","-Xscmx50M","-Xshareclasses","-Xquickstart", "-jar","spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar"] 

其中 Java 參數(shù) -Xshareclasses 開啟SCC,-Xquickstart 開啟AOT。

在 Dockerfile 中,我們運(yùn)用了一個(gè)技巧來預(yù)熱 SCC。在構(gòu)建過程中啟動 JVM 加載應(yīng)用,并開啟 SCC 和 AOT,在應(yīng)用啟動后停止 JVM。這樣就在 Docker 鏡像中包含了生成的 SCC 文件。

然后,我們來構(gòu)建 Docker 鏡像并啟動測試應(yīng)用:

  1. $ docker build -t petclinic-openjdk-openj9-warmed-slim -f Dockerfile.openj9.warmed-slim . 
  2. $ docker run --name hotspot -p 8080:8080 --rm petclinic-openjdk-openj9-warmed-slim 
  3. ... 
  4. 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 '' 
  5. 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) 
  6. ... 

可以看到,啟動時(shí)間已經(jīng)從之前的 8.2s 減少到 4s,提升近50%。

在這個(gè)方案中,我們一方面將耗時(shí)耗能的編譯優(yōu)化過程轉(zhuǎn)移到構(gòu)建時(shí)完成,一方面采用以空間換時(shí)間的方法,將預(yù)編譯的 SCC 緩存保存到 Docker 鏡像中。在容器啟動時(shí),JVM 可以直接使用內(nèi)存映射文件來加載 SCC,優(yōu)化了啟動速度和資源占用。

這個(gè)方法另外一個(gè)優(yōu)勢是:由于 Docker 鏡像采用分層存儲,同一個(gè)宿主機(jī)上的多個(gè) Docker 應(yīng)用實(shí)例會共享同一份 SCC 內(nèi)存映射,可以大大減少在單機(jī)高密度部署時(shí)的內(nèi)存消耗。

下面我們做一下資源消耗的比較,我們首先利用基于 HotSpot VM 的鏡像,同時(shí)啟動 4 個(gè) Docker 應(yīng)用實(shí)例,30s 后利用docker stats查看資源消耗。

  1. $ ./run-hotspot-4.sh 
  2. ... 
  3. Wait a while ... 
  4. CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS 
  5. 0fa58df1a291        instance4           0.15%               597.1MiB / 5.811GiB   10.03%              726B / 0B           0B / 0B             33 
  6. 48f021d728bb        instance3           0.13%               648.6MiB / 5.811GiB   10.90%              726B / 0B           0B / 0B             33 
  7. a3abb10078ef        instance2           0.26%               549MiB / 5.811GiB     9.23%               726B / 0B           0B / 0B             33 
  8. 6a65cb1e0fe5        instance1           0.15%               641.6MiB / 5.811GiB   10.78%              906B / 0B           0B / 0B             33 
  9. ... 

然后使用基于 OpenJ9 VM 的鏡像,同時(shí)啟動 4 個(gè) Docker 應(yīng)用實(shí)例,并查看資源消耗。

  1. $ ./run-openj9-warmed-4.sh 
  2. ... 
  3. Wait a while ... 
  4. CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS 
  5. 3a0ba6103425        instance4           0.09%               119.5MiB / 5.811GiB   2.01%               1.19kB / 0B         0B / 446MB          39 
  6. c07ca769c3e7        instance3           0.19%               119.7MiB / 5.811GiB   2.01%               1.19kB / 0B         16.4kB / 120MB      39 
  7. 0c19b0cf9fc2        instance2           0.15%               112.1MiB / 5.811GiB   1.88%               1.2kB / 0B          22.8MB / 23.8MB     39 
  8. 95a9c4dec3d6        instance1           0.15%               108.6MiB / 5.811GiB   1.83%               1.45kB / 0B         102MB / 414MB       39 
  9. ... 

與 HotSpot VM 相比,OpenJ9 的場景下應(yīng)用內(nèi)存占用從平均 600MB 下降到 120MB。驚喜不驚喜?

通常而言,HotSpot JIT 比 AOT 可以進(jìn)行更加全面和深入的執(zhí)行路徑優(yōu)化,從而有更高的運(yùn)行效率。為了解決這個(gè)矛盾,OpenJ9 的 AOT SCC 只在啟動階段生效,在后續(xù)運(yùn)行中會繼續(xù)利用JIT進(jìn)行分支預(yù)測、代碼內(nèi)聯(lián)等深度編譯優(yōu)化。

HotSpot 在 Class Data Sharing (CDS) 和 AOT 方面也有了很大進(jìn)展,但是 IBM J9 在這方面更加成熟。期待阿里的 Dragonwell 也提供相應(yīng)的優(yōu)化支持。

思考:與 C/C++,Golang, Rust 等靜態(tài)編譯語言不同,Java 采用 VM 方式運(yùn)行,提升了應(yīng)用可移植性的同時(shí)犧牲了部分性能。我們是否可以將 AOT 做到極致?完全移除字節(jié)碼到本地代碼的編譯過程?

原生代碼編譯

為了將 Java 應(yīng)用編譯成本地可執(zhí)行代碼,我們首先要解決 JVM 和應(yīng)用框架在運(yùn)行時(shí)的動態(tài)性挑戰(zhàn)。JVM 提供了靈活的類加載機(jī)制,Sprin

網(wǎng)頁名稱:進(jìn)擊的 Java ,云原生時(shí)代的蛻變
地址分享:http://www.muchs.cn/news28/98428.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供電子商務(wù)、網(wǎng)站內(nèi)鏈、ChatGPT外貿(mào)網(wǎng)站建設(shè)、網(wǎng)站改版、App開發(fā)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

h5響應(yīng)式網(wǎng)站建設(shè)