導讀:雲原生時代的來臨,與Java 開發者到底有什麼聯繫?有人說,雲原生壓根不是爲了 Java
存在的。然而,本文的做者卻認爲雲原生時代,Java 依然能夠勝任「巨人」的角色。做者但願經過一系列實驗,開拓同窗視野,提供有益思考。
在企業軟件領域,Java 依然是絕對王者,但它讓開發者既愛又恨。一方面由於其豐富的生態和完善的工具支持,能夠極大提高了應用開發效率;但在運行時效率方面,Java 也揹負着」內存吞噬者「,「CPU 撕裂者「的惡名,持續受到 NodeJS、Python、Golang 等新老語言的挑戰。java
在技術社區,咱們常常看到有人在唱衰 Java 技術,認爲其再也不符合雲原生計算髮展的趨勢。咱們先拋開這些觀點,首先思考一下雲原生對應用運行時的不一樣需求。git
體積更小 - 對於微服務分佈式架構而言,更小的體積意味着更少的下載帶寬,更快的分發下載速度。
啓動速度更快 - 對於傳統單體應用,啓動速度與運行效率相比不是一個關鍵的指標。緣由是,這些應用重啓和發佈頻率相對較低。然而對於須要快速迭代、水平擴展的微服務應用而言,更快的的啓動速度就意味着更高的交付效率,和更加快速的回滾。尤爲當你須要發佈一個有數百個副本的應用時,緩慢的啓動速度就是時間殺手。對於Serverless 應用而言,端到端的冷啓動速度則更爲關鍵,即便底層容器技術能夠實現百毫秒資源就緒,若是應用沒法在500ms內完成啓動,用戶就會感知到訪問延遲。
佔用資源更少 - 運行時更低的資源佔用,意味着更高的部署密度和更低的計算成本。同時,在JVM啓動時須要消耗大量CPU資源對字節碼進行編譯,下降啓動時資源消耗,能夠減小資源爭搶,更好保障其餘應用SLA。
支持水平擴展 - JVM的內存管理方式致使其對大內存管理的相對低效,通常應用沒法經過配置更大的heap size實現性能提高,不多有Java應用可以有效使用16G內存或者更高。另外一方面,隨着內存成本的降低和虛擬化的流行,大內存配比已經成爲趨勢。因此咱們通常是採用水平擴展的方式,同時部署多個應用副本,在一個計算節點中可能運行一個應用的多個副原本提高資源利用率。
熱身準備
熟悉Spring框架的開發者大多對 Spring Petclinic 不會陌生。本文將藉助這個著名示例應用來演示如何讓咱們的Java應用變得更小,更快,更輕,更強大!github
咱們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!這貨也太膨脹了!編程
$ 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.「 嚴格分離構建和運行階段,不但能夠幫助咱們提高應用的可追溯性,保障應用交付的一致性,同時也能夠減小應用分發的體積,減小安全風險。ubuntu
鏡像瘦身
Docker提供了Multi-stage Build(多階段構建),能夠實現鏡像瘦身。緩存
咱們將鏡像構建分紅兩個階段:tomcat
在 」build「 階段依然採用JDK做爲基礎鏡像,並利用Maven進行應用構建;
在最終發佈的鏡像中,咱們會採用JRE版本做爲基礎鏡像,並從」build「 鏡像中直接拷貝出生成的jar文件。這意味着在最終發佈的鏡像中,只包含運行時所需必要內容,不包含任何編譯時依賴,大大減小了鏡像體積。
$ 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的出現極大提高了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進行分支預測、代碼內聯等深度編譯優化。
HotSpot在Class Data Sharing (CDS)和AOT方面也有了很大進展,可是IBM J9在這方面更加成熟。期待阿里的Dragonwell也提供相應的優化支持。
思考:與C/C++,Golang, Rust等靜態編譯語言不一樣,Java採用VM方式運行,提高了應用可移植性的同時犧牲了部分性能。咱們是否能夠將AOT作到極致?徹底移除字節碼到本地代碼的編譯過程?
原生代碼編譯
爲了將Java應用編譯成本地可執行代碼,咱們首先要解決JVM和應用框架在運行時的動態性挑戰。JVM提供了靈活的類加載機制,Spring的依賴注入(DI,Dependency-injection)能夠實現運行時動態類加載和綁定。在Spring框架中,反射,Annotation 運行時處理器等技術也被普遍應用。這些動態性一方面提高了應用架構的靈活性和易用性,另外一方面也下降了應用的啓動速度,使得AOT原生編譯和優化變得很是複雜。
爲了解決這些挑戰,社區有不少有趣的探索,Micronaut 是其中一個優秀表明。與Spring框架序不一樣,Micronaut提供了編譯時的依賴注入和AOP處理能力,並最小化反射和動態代理的使用。Micronaut 應用有着更快的啓動速度和更低的內存佔用。更加讓咱們更感興趣的是Micronaut支持與Graal VM配合,能夠將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"]
其中
在 "build" 階段,利用Maven構建 Micronaut 版本的 PetClinic 應用,
在 "graalvm" 階段,咱們經過 native-image 將PetClinic jar文件轉化成可執行文件。
在最終階段,將本地可執行文件加入一個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!
總結與後記
做爲進擊的巨人,Java技術在雲原生時代也在不停地進化。在JDK 8u191和JDK 10以後,JVM加強了在Docker容器中對資源的感知。同時社區也在多個不一樣方向探索Java技術棧的邊界。JVM OpenJ9做爲傳統VM的一員,在對現有Java應用保持高度兼容的同時,對啓動速度和內存佔用作了細緻的優化,比較適於與現有Spring等微服務架構配合使用。而Micronaut/Graal VM則另闢蹊徑,經過改變編程模型和編譯過程,將應用的動態性儘量提早到編譯時期處理,極大優化了應用啓動時間,在Serverless領域前景可期。這些設計思路都值得咱們借鑑。
在雲原生時代,咱們要可以在橫向的應用開發生命週期中,將開發、交付、運維過程進行有效的分割和重組,提高研發協同效率;而且要能在整個縱向軟件技術棧中,在編程模型、應用運行時和基礎設施等多層面進行系統優化,實現radical simplification,提高系統效率。
本文完成於在參加阿里集團20週年的火車旅途上,9/10阿里年會是很是難忘的經歷。感謝馬老師,感謝阿里,感謝這個時代,感謝全部幫助和支持咱們的小夥伴,感謝全部追夢的技術人,咱們一塊兒開拓雲原生的將來。
本文做者:易立 阿里雲資深技術專家
原文連接:https://yq.aliyun.com/article...
本文爲雲棲社區原創內容,未經容許不得轉載。