做者| 易立 阿里云资深技术专家html
导读:云原生时代的来临,与Java 开发者到底有什么联系?有人说,云原生压根不是为了 Java 存在的。然而,本文的做者却认为云原生时代,Java 依然能够胜任“巨人”的角色。做者但愿经过一系列实验,开拓同窗视野,提供有益思考。java
在企业软件领域,Java 依然是绝对王者,但它让开发者既爱又恨。一方面由于其丰富的生态和完善的工具支持,能够极大提高了应用开发效率;但在运行时效率方面,Java 也背负着”内存吞噬者“,“CPU 撕裂者“的恶名,持续受到 NodeJS、Python、Golang 等新老语言的挑战。git
在技术社区,咱们常常看到有人在唱衰 Java 技术,认为其再也不符合云原生计算发展的趋势。咱们先抛开这些观点,首先思考一下云原生对应用运行时的不一样需求。github
熟悉Spring框架的开发者大多对 Spring Petclinic 不会陌生。本文将借助这个著名示例应用来演示如何让咱们的Java应用变得更小,更快,更轻,更强大!spring
咱们fork了IBM的Michael Thompson的示例,并作了一些调整。docker
$ 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/ 访问应用界面。ubuntu
检查一下构建出的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的出现极大提高了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%。<br />在这个方案中,咱们一方面将耗时耗能的编译优化过程转移到构建时完成,一方面采用以空间换时间的方法,将预编译的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支持与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"]
其中
native-image
将PetClinic jar文件转化成可执行文件。构建应用
$ 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,提高系统效率。
本文完成于在参加阿里集团20周年的火车旅途上,9/10阿里年会是很是难忘的经历。感谢马老师,感谢阿里,感谢这个时代,感谢全部帮助和支持咱们的小伙伴,感谢全部追梦的技术人,咱们一块儿开拓云原生的将来。
“阿里巴巴云原生微信公众号(ID:Alicloudnative)关注微服务、Serverless、容器、Service Mesh等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,作最懂云原生开发者的技术公众号。”