2021-01-31 分類: 網站建設
云原生時代的來臨,與Java 開發者到底有什么聯系?有人說,云原生壓根不是為了 Java 存在的。然而,本文的作者卻認為云原生時代,Java 依然可以勝任“巨人”的角色。作者希望通過一系列實驗,開拓同學視野,提供有益思考。
在企業軟件領域,Java 依然是絕對王者,但它讓開發者既愛又恨。一方面因為其豐富的生態和完善的工具支持,可以極大提升了應用開發效率;但在運行時效率方面,Java 也背負著”內存吞噬者“,“CPU 撕裂者“的惡名,持續受到 NodeJS、Python、Golang 等新老語言的挑戰。
在技術社區,我們經常看到有人在唱衰 Java 技術,認為其不再符合云原生計算發展的趨勢。先拋開上面這些觀點,我們首先思考一下云原生對應用運行時的不同需求:
體積更小:對于微服務分布式架構而言,更小的體積意味著更少的下載帶寬,更快的分發下載速度。
啟動速度更快:對于傳統單體應用,啟動速度與運行效率相比不是一個關鍵的指標。原因是,這些應用重啟和發布頻率相對較低。然而對于需要快速迭代、水平擴展的微服務應用而言,更快的的啟動速度就意味著更高的交付效率,和更加快速的回滾。尤其當你需要發布一個有數百個副本的應用時,緩慢的啟動速度就是時間殺手。對于Serverless 應用而言,端到端的冷啟動速度則更為關鍵,即使底層容器技術可以實現百毫秒資源就緒,如果應用無法在 500ms 內完成啟動,用戶就會感知到訪問延遲。
占用資源更少:運行時更低的資源占用,意味著更高的部署密度和更低的計算成本。同時,在 JVM 啟動時需要消耗大量 CPU資源對字節碼進行編譯,降低啟動時資源消耗,可以減少資源爭搶,更好保障其他應用 SLA。
支持水平擴展:JVM 的內存管理方式導致其對大內存管理的相對低效,一般應用無法通過配置更大的 heap size 實現性能提升,很少有 Java 應用能夠有效使用 16G 內存或者更高。另一方面,隨著內存成本的下降和虛擬化的流行,大內存配比已經成為趨勢。所以我們一般是采用水平擴展的方式,同時部署多個應用副本,在一個計算節點中可能運行一個應用的多個副本來提升資源利用率。
熟悉 Spring 框架的開發者大多對 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"]
構建鏡像并執行:
- $ 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.“ 嚴格分離構建和運行階段,不但可以幫助我們提升應用的可追溯性,保障應用交付的一致性,同時也可以減少應用分發的體積,減少安全風險。
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
鏡像瘦身之后將大大加速應用分發速度,我們是否有辦法優化應用的啟動速度呢?
為了解決 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進行分支預測、代碼內聯等深度編譯優化。
HotSpot 在 Class Data Sharing (CDS) 和 AOT 方面也有了很大進展,但是 IBM J9 在這方面更加成熟。期待阿里的 Dragonwell 也提供相應的優化支持。
思考:與 C/C++,Golang, Rust 等靜態編譯語言不同,Java 采用 VM 方式運行,提升了應用可移植性的同時犧牲了部分性能。我們是否可以將 AOT 做到極致?完全移除字節碼到本地代碼的編譯過程?
為了將 Java 應用編譯成本地可執行代碼,我們首先要解決 JVM 和應用框架在運行時的動態性挑戰。JVM 提供了靈活的類加載機制,Sprin
網頁名稱:進擊的 Java ,云原生時代的蛻變
地址分享:http://m.newbst.com/news28/98428.html
成都網站建設公司_創新互聯,為您提供電子商務、網站內鏈、ChatGPT、外貿網站建設、網站改版、App開發
聲明:本網站發布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯系客服。電話:028-86922220;郵箱:631063699@qq.com。內容未經允許不得轉載,或轉載時需注明來源: 創新互聯
猜你還喜歡下面的內容