做者:易立 阿里雲資深技術專家 連接:https://mp.weixin.qq.com/s/x509eBHiDuasGmJupJKVow 複製代碼
雲原生時代的來臨,與Java 開發者到底有什麼聯繫?有人說,雲原生壓根不是爲了 Java 存在的。然而,本文的做者卻認爲雲原生時代,Java 依然能夠勝任「巨人」的角色。做者但願經過一系列實驗,開拓同窗視野,提供有益思考。html
在企業軟件領域,Java 依然是絕對王者,但它讓開發者既愛又恨。一方面由於其豐富的生態和完善的工具支持,能夠極大提高了應用開發效率;但在運行時效率方面,Java 也揹負着」內存吞噬者「,「CPU 撕裂者「的惡名,持續受到 NodeJS、Python、Golang 等新老語言的挑戰。java
在技術社區,咱們常常看到有人在唱衰 Java 技術,認爲其再也不符合雲原生計算髮展的趨勢。先拋開上面這些觀點,咱們首先思考一下雲原生對應用運行時的不一樣需求:git
熱身準備github
熟悉 Spring 框架的開發者大多對 Spring Petclinic 不會陌生。本文將藉助這個著名示例應用來演示如何讓咱們的 Java 應用變得更小、更快、更輕、更強大!面試
咱們 fork 了 IBM 的 Michael Thompson 的示例,並作了一些調整。spring
$ git clone https://github.com/denverdino/adopt-openj9-spring-boot $ cd adopt-openj9-spring-boot 複製代碼
首先,咱們會爲 PetClinic 應用構建一個 Docker 鏡像。在 Dockerfile 中,咱們利用 OpenJDK 做爲基礎鏡像,安裝 Maven,下載、編譯、打包 Spring PetClinic 應用,最後設置鏡像的啓動參數完成鏡像構建。docker
$ 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"] 複製代碼
構建鏡像並執行數據庫
$ 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!這貨也太膨脹了!ubuntu
$ 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.「 嚴格分離構建和運行階段,不但能夠幫助咱們提高應用的可追溯性,保障應用交付的一致性,同時也能夠減小應用分發的體積,減小安全風險。
鏡像瘦身
Docker 提供了 Multi-stage Build(多階段構建),能夠實現鏡像瘦身。
咱們將鏡像構建分紅兩個階段:
$ 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 複製代碼
鏡像瘦身以後將大大加速應用分發速度,咱們是否有辦法優化應用的啓動速度呢?
從 JIT 到 AOT —啓動提速
爲了解決 Java 啓動的性能瓶頸,咱們首先須要理解 JVM 的實現原理。
爲了實現「一次編寫,隨處運行」的能力,Java 程序會被編譯成實現架構無關的字節碼。JVM 在運行時將字節碼轉換成本地機器碼執行。這個轉換過程決定了 Java 應用的啓動和運行速度。爲了提高執行效率,JVM 引入了 JIT compiler(Just in Time Compiler,即時編譯器),其中 Sun/Oracle 公司的 HotSpot 是最著名 JIT 編譯器實現。
HotSpot 提供了自適應優化器,能夠動態分析、發現代碼執行過程當中的關鍵路徑,並進行編譯優化。HotSpot 的出現極大提高了Java 應用的執行效率,在 Java 1.4 之後成爲了缺省的 VM 實現。可是 HotSpot VM 在啓動時纔對字節碼進行編譯,一方面致使啓動時執行效率不高,一方面編譯和優化須要不少的 CPU 資源,拖慢了啓動速度。咱們是否能夠優化這個過程,提高啓動速度呢?
熟悉 Java 江湖歷史的同窗應該會知道 IBM J9 VM,它是用於 IBM 企業級軟件產品的一款高性能的 JVM,幫助 IBM 奠基了商業應用平臺中間件的霸主地位。2017 年 9 月,IBM 將 J9 捐獻給 Eclipse 基金會,並改名 Eclipse OpenJ9,開啓開源之旅。
OpenJ9 提供了 Shared Class Cache (SCC 共享類緩存) 和 Ahead-of-Time (AOT 提早編譯) 技術,顯著減小了 Java 應用啓動時間。
SCC 是一個內存映射文件,包含了J9 VM 對字節碼的執行分析信息和已經編譯生成的本地代碼。開啓 AOT 編譯後,會將 JVM 編譯結果保存在 SCC 中,在後續 JVM 啓動中能夠直接重用。與啓動時進行的 JIT 編譯相比,從 SCC 加載預編譯的實現要快得多,並且消耗的資源要更少。啓動時間能夠獲得明顯改善。
咱們開始構建一個包含 AOT 優化的 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%。
在這個方案中,咱們一方面將耗時耗能的編譯優化過程轉移到構建時完成,一方面採用以空間換時間的方法,將預編譯的 SCC 緩存保存到 Docker 鏡像中。在容器啓動時,JVM 能夠直接使用內存映射文件來加載 SCC,優化了啓動速度和資源佔用。
這個方法另一個優點是:因爲 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 能夠進行更加全面和深刻的執行路徑優化,從而有更高的運行效率。爲了解決這個矛盾,OpenJ9 的 AOT SCC 只在啓動階段生效,在後續運行中會繼續利用JIT進行分支預測、代碼內聯等深度編譯優化。
更多關於 OpenJ9 SCC 和 AOT 的技術介紹,請參考
思考:與 C/C++,Golang, Rust 等靜態編譯語言不一樣,Java 採用 VM 方式運行,提高了應用可移植性的同時犧牲了部分性能。咱們是否能夠將 AOT 作到極致?徹底移除字節碼到本地代碼的編譯過程?
原生代碼編譯
爲了將 Java 應用編譯成本地可執行代碼,咱們首先要解決 JVM 和應用框架在運行時的動態性挑戰。JVM 提供了靈活的類加載機制,Spring 的依賴注入(DI,Dependency-injection)能夠實現運行時動態類加載和綁定。在 Spring 框架中,反射,Annotation 運行時處理器等技術也被普遍應用。這些動態性一方面提高了應用架構的靈活性和易用性,另外一方面也下降了應用的啓動速度,使得 AOT 原生編譯和優化變得很是複雜。
爲了解決這些挑戰,社區有不少有趣的探索,Micronaut 是其中一個優秀表明。與 Spring 框架序不一樣,Micronaut 提供了編譯時的依賴注入和AOP處理能力,並最小化反射和動態代理的使用。Micronaut 應用有着更快的啓動速度和更低的內存佔用。更加讓咱們更感興趣的是 Micronaut 支持與 GraalVM 配合,能夠將 Java 應用編譯成爲本地執行代碼全速運行。
注: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"] 複製代碼
其中
構建應用
複製代碼
$ 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 還在快速發展中,遷移一個 Spring 應用還有很多工做須要考慮。此外 Graal VM 的調試、監控等工具鏈還不夠完善。可是這已經讓咱們看到了曙光,Java 應用和 Serverless 的世界再也不遙遠。因爲篇幅有限,對 Graal VM 和Micronaut 有興趣的同窗能夠參考
總結與後記
做爲進擊的巨人,Java 技術在雲原生時代也在不停地進化。在JDK 8u191 和 JDK 10 以後,JVM 加強了在 在 Docker 容器中對資源的感知。同時社區也在多個不一樣方向探索 Java 技術棧的邊界。JVM OpenJ9 做爲傳統VM的一員,在對現有 Java 應用保持高度兼容的同時,對啓動速度和內存佔用作了細緻的優化,比較適於與現有 Spring 等微服務架構配合使用。
而 Micronaut/Graal VM 則另闢蹊徑,經過改變編程模型和編譯過程,將應用的動態性儘量提早到編譯時期處理,極大優化了應用啓動時間,在 Serverless 領域前景可期。這些設計思路都值得咱們借鑑。
在雲原生時代,咱們要可以在橫向的應用開發生命週期中,將開發、交付、運維過程進行有效的分割和重組,提高研發協同效率;而且要能在整個縱向軟件技術棧中,在編程模型、應用運行時和基礎設施等多層面進行系統優化,實現 radical simplification,提高系統效率。