新聞中心
進擊的 Java ,云原生時代的蛻變
作者:易立 2019-09-20 13:37:50
開發(fā)
后端
云計算
云原生 云原生時代的來臨,與Java 開發(fā)者到底有什么聯系?有人說,云原生壓根不是為了 Java 存在的。然而,本文的作者卻認為云原生時代,Java 依然可以勝任“巨人”的角色。作者希望通過一系列實驗,開拓同學視野,提供有益思考。

站在用戶的角度思考問題,與客戶深入溝通,找到昌吉網站設計與昌吉網站推廣的解決方案,憑借多年的經驗,讓設計與互聯網技術結合,創(chuàng)造個性化、用戶體驗好的作品,建站類型包括:成都網站設計、成都網站建設、外貿網站建設、企業(yè)官網、英文網站、手機端網站、網站推廣、空間域名、網絡空間、企業(yè)郵箱。業(yè)務覆蓋昌吉地區(qū)。
【編者的話】云原生時代的來臨,與Java 開發(fā)者到底有什么聯系?有人說,云原生壓根不是為了 Java 存在的。然而,本文的作者卻認為云原生時代,Java 依然可以勝任“巨人”的角色。作者希望通過一系列實驗,開拓同學視野,提供有益思考。
在企業(yè)軟件領域,Java 依然是絕對王者,但它讓開發(fā)者既愛又恨。一方面因為其豐富的生態(tài)和完善的工具支持,可以極大提升了應用開發(fā)效率;但在運行時效率方面,Java 也背負著”內存吞噬者“,“CPU 撕裂者“的惡名,持續(xù)受到 NodeJS、Python、Golang 等新老語言的挑戰(zhàn)。
在技術社區(qū),我們經常看到有人在唱衰 Java 技術,認為其不再符合云原生計算發(fā)展的趨勢。先拋開上面這些觀點,我們首先思考一下云原生對應用運行時的不同需求:
體積更?。?/strong>對于微服務分布式架構而言,更小的體積意味著更少的下載帶寬,更快的分發(fā)下載速度。
啟動速度更快:對于傳統(tǒng)單體應用,啟動速度與運行效率相比不是一個關鍵的指標。原因是,這些應用重啟和發(fā)布頻率相對較低。然而對于需要快速迭代、水平擴展的微服務應用而言,更快的的啟動速度就意味著更高的交付效率,和更加快速的回滾。尤其當你需要發(fā)布一個有數百個副本的應用時,緩慢的啟動速度就是時間殺手。對于Serverless 應用而言,端到端的冷啟動速度則更為關鍵,即使底層容器技術可以實現百毫秒資源就緒,如果應用無法在 500ms 內完成啟動,用戶就會感知到訪問延遲。
占用資源更少:運行時更低的資源占用,意味著更高的部署密度和更低的計算成本。同時,在 JVM 啟動時需要消耗大量 CPU資源對字節(jié)碼進行編譯,降低啟動時資源消耗,可以減少資源爭搶,更好保障其他應用 SLA。
支持水平擴展:JVM 的內存管理方式導致其對大內存管理的相對低效,一般應用無法通過配置更大的 heap size 實現性能提升,很少有 Java 應用能夠有效使用 16G 內存或者更高。另一方面,隨著內存成本的下降和虛擬化的流行,大內存配比已經成為趨勢。所以我們一般是采用水平擴展的方式,同時部署多個應用副本,在一個計算節(jié)點中可能運行一個應用的多個副本來提升資源利用率。
熱身準備
熟悉 Spring 框架的開發(fā)者大多對 Spring Petclinic 不會陌生。本文將借助這個著名示例應用來演示如何讓我們的 Java 應用變得更小、更快、更輕、更強大!
我們 fork 了 IBM 的 Michael Thompson 的示例,并做了一些調整。
- $ git clone https://github.com/denverdino/adopt-openj9-spring-boot
- $ cd adopt-openj9-spring-boot
首先,我們會為 PetClinic 應用構建一個 docker 鏡像。在 Dockerfile 中,我們利用 OpenJDK 作為基礎鏡像,安裝 Maven,下載、編譯、打包 Spring PetClinic 應用,最后設置鏡像的啟動參數完成鏡像構建。
- $ 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"]
構建鏡像并執(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)
可以通過 http://localhost:8080/ 訪問應用界面。
檢查一下構建出的 Docker 鏡像, ”petclinic-openjdk-openj9“ 的大小為 871MB,而基礎鏡像 ”adoptopenjdk/openjdk8“ 僅有 300MB!這貨也太膨脹了!
- $ docker images petclinic-openjdk-hotspot
- REPOSITORY TAG IMAGE ID CREATED SIZE
- petclinic-openjdk-hotspot latest 469f73967d03 26 hours ago 871MB
原因是:為了構建 Spring 應用,我們在鏡像中引入了一系列編譯時依賴,如 Git,Maven 等,并產生了大量臨時的文件。然而這些內容在運行時是不需要的。
在著名的軟件12要素第五條明確指出了,”Strictly separate build and run stages.“ 嚴格分離構建和運行階段,不但可以幫助我們提升應用的可追溯性,保障應用交付的一致性,同時也可以減少應用分發(fā)的體積,減少安全風險。
鏡像瘦身
Docker 提供了 Multi-stage Build(多階段構建),可以實現鏡像瘦身。
我們將鏡像構建分成兩個階段:
- 在 ”build“ 階段依然采用 JDK 作為基礎鏡像,并利用 Maven 進行應用構建;
- 在最終發(fā)布的鏡像中,我們會采用 JRE 版本作為基礎鏡像,并從”build“ 鏡像中直接拷貝出生成的 jar 文件。這意味著在最終發(fā)布的鏡像中,只包含運行時所需必要內容,不包含任何編譯時依賴,大大減少了鏡像體積。
- $ cat Dockerfile.openjdk-slim
- FROM adoptopenjdk/openjdk8 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:jre8u222-b10-alpine-jre
- COPY --from=build /tmp/spring-petclinic/target/spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar
- CMD ["java","-jar","spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar"]
查看一下新鏡像大小,從 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
鏡像瘦身之后將大大加速應用分發(fā)速度,我們是否有辦法優(yōu)化應用的啟動速度呢?
從 JIT 到 AOT —啟動提速
為了解決 Java 啟動的性能瓶頸,我們首先需要理解 JVM 的實現原理。
為了實現“一次編寫,隨處運行”的能力,Java 程序會被編譯成實現架構無關的字節(jié)碼。JVM 在運行時將字節(jié)碼轉換成本地機器碼執(zhí)行。這個轉換過程決定了 Java 應用的啟動和運行速度。為了提升執(zhí)行效率,JVM 引入了 JIT compiler(Just in Time Compiler,即時編譯器),其中 Sun/Oracle 公司的 HotSpot 是最著名 JIT 編譯器實現。
HotSpot 提供了自適應優(yōu)化器,可以動態(tài)分析、發(fā)現代碼執(zhí)行過程中的關鍵路徑,并進行編譯優(yōu)化。HotSpot 的出現極大提升了Java 應用的執(zhí)行效率,在 Java 1.4 以后成為了缺省的 VM 實現。但是 HotSpot VM 在啟動時才對字節(jié)碼進行編譯,一方面導致啟動時執(zhí)行效率不高,一方面編譯和優(yōu)化需要很多的 CPU 資源,拖慢了啟動速度。我們是否可以優(yōu)化這個過程,提升啟動速度呢?
熟悉 Java 江湖歷史的同學應該會知道 IBM J9 VM,它是用于 IBM 企業(yè)級軟件產品的一款高性能的 JVM,幫助 IBM 奠定了商業(yè)應用平臺中間件的霸主地位。2017 年 9 月,IBM 將 J9 捐獻給 Eclipse 基金會,并更名 Eclipse OpenJ9,開啟開源之旅。
OpenJ9 提供了 Shared Class Cache(SCC 共享類緩存)和 Ahead-of-Time(AOT 提前編譯)技術,顯著減少了 Java 應用啟動時間。
SCC 是一個內存映射文件,包含了J9 VM 對字節(jié)碼的執(zhí)行分析信息和已經編譯生成的本地代碼。開啟 AOT 編譯后,會將 JVM 編譯結果保存在 SCC 中,在后續(xù) JVM 啟動中可以直接重用。與啟動時進行的 JIT 編譯相比,從 SCC 加載預編譯的實現要快得多,而且消耗的資源要更少。啟動時間可以得到明顯改善。
我們開始構建一個包含 AOT 優(yōu)化的 Docker 應用鏡像:
- $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 參數 -Xshareclasses 開啟SCC,-Xquickstart 開啟AOT。
在 Dockerfile 中,我們運用了一個技巧來預熱 SCC。在構建過程中啟動 JVM 加載應用,并開啟 SCC 和 AOT,在應用啟動后停止 JVM。這樣就在 Docker 鏡像中包含了生成的 SCC 文件。
然后,我們來構建 Docker 鏡像并啟動測試應用:
- $ 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)
- ...
可以看到,啟動時間已經從之前的 8.2s 減少到 4s,提升近50%。
在這個方案中,我們一方面將耗時耗能的編譯優(yōu)化過程轉移到構建時完成,一方面采用以空間換時間的方法,將預編譯的 SCC 緩存保存到 Docker 鏡像中。在容器啟動時,JVM 可以直接使用內存映射文件來加載 SCC,優(yōu)化了啟動速度和資源占用。
這個方法另外一個優(yōu)勢是:由于 Docker 鏡像采用分層存儲,同一個宿主機上的多個 Docker 應用實例會共享同一份 SCC 內存映射,可以大大減少在單機高密度部署時的內存消耗。
下面我們做一下資源消耗的比較,我們首先利用基于 HotSpot VM 的鏡像,同時啟動 4 個 Docker 應用實例,30s 后利用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 的鏡像,同時啟動 4 個 Docker 應用實例,并查看資源消耗。
- $ ./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 的場景下應用內存占用從平均 600MB 下降到 120MB。驚喜不驚喜?
通常而言,HotSpot JIT 比 AOT 可以進行更加全面和深入的執(zhí)行路徑優(yōu)化,從而有更高的運行效率。為了解決這個矛盾,OpenJ9 的 AOT SCC 只在啟動階段生效,在后續(xù)運行中會繼續(xù)利用JIT進行分支預測、代碼內聯等深度編譯優(yōu)化。
HotSpot 在 Class Data Sharing (CDS) 和 AOT 方面也有了很大進展,但是 IBM J9 在這方面更加成熟。期待阿里的 Dragonwell 也提供相應的優(yōu)化支持。
思考:與 C/C++,Golang, Rust 等靜態(tài)編譯語言不同,Java 采用 VM 方式運行,提升了應用可移植性的同時犧牲了部分性能。我們是否可以將 AOT 做到極致?完全移除字節(jié)碼到本地代碼的編譯過程?
原生代碼編譯
為了將 Java 應用編譯成本地可執(zhí)行代碼,我們首先要解決 JVM 和應用框架在運行時的動態(tài)性挑戰(zhàn)。JVM 提供了靈活的類加載機制,Spring 的依賴注入(DI,Dependency-injection)可以實現運行時動態(tài)類加載和綁定。在 Spring 框架中,反射,Annotation 運行時處理器等技術也被廣泛應用。這些動態(tài)性一方面提升了應用架構的靈活性和易用性,另一方面也降低了應用的啟動速度,使得 AOT 原生編譯和優(yōu)化變得非常復雜。
為了解決這些挑戰(zhàn),社區(qū)有很多有趣的探索,Micronaut 是其中一個優(yōu)秀代表。與 Spring 框架序不同,Micronaut 提供了編譯時的依賴注入和AOP處理能力,并最小化反射和動態(tài)代理的使用。Micronaut 應用有著更快的啟動速度和更低的內存占用。更加讓我們更感興趣的是 Micronaut 支持與 GraalVM 配合,可以將 Java 應用編譯成為本地執(zhí)行代碼全速運行。
注:GraalVM 是 Oracle 推出的一種新型通用虛擬機,支持多種語言,可以將Java應用程序編譯為本地原生應用。
下面開始我們的探險,我們利用 Mitz 提供的 Micronaut 版本 PetClinic 示例工程并做了一點點調整。(使用 Graal VM 19.2)
- $ git clone https://github.com/denverdino/micronaut-petclinic
- $ cd micronaut-petclinic
其中 Docker 鏡像的內容如下:
- $ 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構建 Micronaut 版本的 PetClinic 應用
- 在 "graalvm" 階段,我們通過 native-image 將 PetClinic jar 文件轉化成可執(zhí)行文件
- 在最終階段,將本地可執(zhí)行文件加入一個 Alpine Linux 基礎鏡像
構建應用:
- $ docker-compose build
啟動測試數據庫:
- $ docker-compose up db
啟動測試應用:
- $ docker-compose up app
- micronaut-petclinic_db_1 is up-to-date
- Starting micronaut-petclinic_app_1 ... done
- Attaching to micronaut-petclinic_app_1
- app_1 | 04:57:47.571 [main] INFO org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.PostgreSQL95Dialect
- app_1 | 04:57:47.649 [main] INFO org.hibernate.type.BasicTypeRegistry - HHH000270: Type registration [java.util.UUID] overrides previous : org.hibernate.type.UUIDBinaryType@5f4e0f0
- app_1 | 04:57:47.653 [main] INFO o.h.tuple.entity.EntityMetamodel - HHH000157: Lazy property fetching available for: com.example.micronaut.petclinic.owner.Owner
- app_1 | 04:57:47.656 [main] INFO o.h.e.t.j.p.i.JtaPlatformInitiator - HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
- app_1 | 04:57:47.672 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 159ms. Server Running: http://1285c42bfcd5:8080
應用啟動速度如閃電般提升至 159ms,僅有 HotSpot VM 的1/50!
Micronaut 和 Graal VM 還在快速發(fā)展中,遷移一個 Spring 應用還有不少工作需要考慮。此外 Graal VM 的調試、監(jiān)控等工具鏈還不夠完善。但是這已經讓我們看到了曙光,Java 應用和 Serverless 的世界不再遙遠。
總結與后記
作為進擊的巨人,Java 技術在云原生時代也在不停地進化。在JDK 8u191 和 JDK 10 之后,JVM 增強了在 在 Docker 容器中對資源的感知。同時社區(qū)也在多個不同方向探索 Java 技術棧的邊界。JVM OpenJ9 作為傳統(tǒng)VM的一員,在對現有 Java 應用保持高度兼容的同時,對啟動速度和內存占用做了細致的優(yōu)化,比較適于與現有 Spring 等微服務架構配合使用。
而 Micronaut/Graal VM 則另辟蹊徑,通過改變編程模型和編譯過程,將應用的動態(tài)性盡可能提前到編譯時期處理,極大優(yōu)化了應用啟動時間,在 Serverless 領域前景可期。這些設計思路都值得我們借鑒。
在云原生時代,我們要能夠在橫向的應用開發(fā)生命周期中,將開發(fā)、交付、運維過程進行有效的分割和重組,提升研發(fā)協(xié)同效率;并且要能在整個縱向軟件技術棧中,在編程模型、應用運行時和基礎設施等多層面進行系統(tǒng)優(yōu)化,實現 radical simplification,提升系統(tǒng)效率。
感謝這個時代,感謝所有幫助和支持我們的小伙伴,感謝所有追夢的技術人,我們一起開拓云原生的未來。
網站標題:進擊的Java,云原生時代的蛻變
文章起源:http://fisionsoft.com.cn/article/cdeooes.html


咨詢
建站咨詢
