前言

在云原生的时代Java备受新老语言的挑战,在云原生时代对应用的需求有几大类

  • 体积更小:对于分布式架构而言,更小的体积意味着更少的下载宽带,更快的分发速度。
  • 启动速度更快,对于传统单体应用,启动速度与运行效率相比不是一个关键的指标。原因是,这些应用重启和发布频率相对较低。然而对于需要快速迭代、水平扩展的微服务应用而言,更快的的启动速度就意味着更高的交付效率,和更加快速的回滚。尤其当你需要发布一个有数百个副本的应用时,缓慢的启动速度就是时间杀手。对于Serverless 应用而言,端到端的冷启动速度则更为关键。
  • 占用资源更少 运行时更低的资源占用,意味着更高的部署密度和更低的计算成本。同时,在 JVM 启动时需要消耗大量 CPU资源对字节码进行编译,降低启动时资源消耗,可以减少资源争抢,更好保障其他应用 SLA。
  • 支持水平扩展:JVM 的内存管理方式导致其对大内存管理的相对低效,一般应用无法通过配置更大的 heap size 实现性能提升,很少有 Java 应用能够有效使用 16G 内存或者更高。另一方面,随着内存成本的下降和虚拟化的流行,大内存配比已经成为趋势。所以我们一般是采用水平扩展的方式,同时部署多个应用副本,在一个计算节点中可能运行一个应用的多个副本来提升资源利用率。

Docker

我们使用openjdk作为基础镜像,安装maven,下载,编译,打包,启动

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
RUN ls -l /tmp/spring-petclinic/target/
FROM adoptopenjdk/openjdk8:jre8u222-b10-alpine-jre
COPY --from=build /tmp/spring-petclinic/target/spring-petclinic-2.4.2.jar spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar
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
  • 资源

AOT

为了实现“一次编写,随处运行”的能力,Java 程序会被编译成实现架构无关的字节码。JVM 在运行时将字节码转换成本地机器码执行。这个转换过程决定了 Java 应用的启动和运行速度。为了提升执行效率,JVM 引入了 JIT compiler(Just in Time Compiler,即时编译器),其中 Sun/Oracle 公司的 HotSpot 是最著名 JIT 编译器实现。

HotSpot 提供了自适应优化器,可以动态分析、发现代码执行过程中的关键路径,并进行编译优化。HotSpot 的出现极大提升了Java 应用的执行效率,在 Java 1.4 以后成为了缺省的 VM 实现。但是 HotSpot VM 在启动时才对字节码进行编译,一方面导致启动时执行效率不高,一方面编译和优化需要很多的 CPU 资源,拖慢了启动速度。我们是否可以优化这个过程,提升启动速度呢?

openj9

OpenJ9 提供了 Shared Class Cache (SCC 共享类缓存) 和 Ahead-of-Time (AOT 提前编译) 技术,显著减少了 Java 应用启动时间。
SCC 是一个内存映射文件,包含了J9 VM 对字节码的执行分析信息和已经编译生成的本地代码。开启 AOT 编译后,会将 JVM 编译结果保存在 SCC 中,在后续 JVM 启动中可以直接重用。与启动时进行的 JIT 编译相比,从 SCC 加载预编译的实现要快得多,而且消耗的资源要更少。启动时间可以得到明显改善。

openj9的Docker实践
  • 官方介绍有两种方式,使用Docker卷,这种方式我在启动Docker的时候,cpu暴增,所以我放弃了这种方式
  • 预热
    由于Docker是分层的,我们可以使用个技巧来预热SCC,在构建过程中启动JVM加载应用,并开启SCC 和AOT,在应用启动后停止掉JVM,这样Docker的镜像中包含了生产的SCC文件
    这样做的好处,现在国内很多项目没有测试用例,在CI的时候只会进行build,没有跑测试用例,build好的运行文件,直接CD部署到生成环境有一定的风险性。我们可以在预热的时候,启动可执行文件,来把风险拦截在CI。
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

COPY --from=build /tmp/spring-petclinic/target/spring-petclinic-2.4.2.jar spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar

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 $2}' | xargs kill -1

CMD ["java","-Xscmx50M","-Xshareclasses","-Xquickstart","-jar","spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar"]
  • 构建执行
docker build -t petclinic-openjdk-openj9 -f Dockerfile.openjdk .
docker run --name hotspot -p 8081:8080 --rm petclinic-openjdk-openj9
  • 资源

    我们可以看到openj9占用的内存只有1%,而hotspot占用了百分之8