Spring Boot 3.0 手上过 - GraalVM原生镜像
“手上过”是方言,跟那句“Talk is cheap. Show me the code.”的意思差不多。
GraalVM Native Image,原生镜像是独立的可执行镜像,这个独立是指不需要JVM。原生镜像通常具有镜像文件小,启动速度快,内存占用少等特点。这几个特点刚好解决了大规模容器化部署和Serverless应用场景的分发效率、(冷启动)响应速度和资源消耗等问题。
本文使用
start.spring.io
创建了一个Spring Boot 3.0的项目,并使用GraalVM编译为原生镜像,从构建速度、镜像大小、启动速度和内存占用等方面,跟传统镜像进行了比较。Spring官方文档: GraalVM Native Image Support
Initializr
在 start.spring.io 上初始化一个全新的Maven项目,Spring Boot 3.0.2,Java 17,增加Spring Web和GraalVM Native Support依赖项。
pom.xml中关于GraalVM原生镜像构建的配置:
1 | <build> |
安装GraalVM
用GraalVM来构建原生镜像,因为网络的原因,在国内的主机上操作,真的的是徒增烦恼。给人的感受,就跟当初学习Kubernetes一样,很容易被劝退,所以笔者按量付费在阿里云上买的海外主机(ECS),操作系统Ubuntu。
安装 GraalVM跟安装JDK一样。
1 | $ wget https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-22.3.1/graalvm-ce-java17-linux-amd64-22.3.1.tar.gz |
1 | #修改.bashrc文件 |
1 | $ gu install native-image |
构建原生镜像
使用buildpacks构建原生镜像
buildpacks需要系统已经安装好了Docker,但本身我们这里一直在讲的原生镜像,就是Docker镜像啊,所以系统上安装Docker是个隐含条件。
1 | $ mvn -Pnative spring-boot:build-image |
使用Native Image构建
Native Image不需要本机安装Docker,但是需要本机安装GraalVM。
1 | $ mvn -Pnative package |
资源消耗
编译为原生镜像,非常耗时耗资源,阿里云1核(vCPU) 2GiB内存的ECS,直接把CPU和内存打满,跑了30分钟无果,直接卡死了。强制重启后调整为4核(vCPU) 8GiB内存,耗时5分钟完成。
两种原生镜像构建方式的比较
构建方式 | 先决条件(本机安装的软件) | 编译速度 | 结果镜像 |
---|---|---|---|
buildpacks | Docker | 5 Min | 93.6MB |
native-image | GraalVM和native-image | 5 Min | 93.6MB |
可见buildpacks和native-imange这两种方式,只是对本机上的软件安装要求不同,编译速度差不多,最终得到的原生镜像相同。
原生镜像跟传统镜像的比较
传统镜像即mvn spring-boot:build-image
生成的镜像。
镜像类型 | 是否包含JVM | 构建时间 | 镜像大小 | 启动时间 | 内存占用 |
---|---|---|---|---|---|
传统镜像 | 是 | 01:33 min | 279MB | 2.358 seconds | 131.8MiB |
原生镜像 | 否 | 06:32 min | 93.6MB | 0.061 seconds | 23.39MiB |
5倍 | 三分之一 | 39倍 | 五分之一 |
可见除了构建速度,原生镜像在镜像大小、启动时间和内存占用上,都明显占优。Serverless的冷启动如果能做到几十毫秒话,已经够了。当然实际的应用比这个要复杂很多。比如加上数据库连接的话,时间会更长。现在数据库的发展趋势,云原生和Serverless,也是一个主要的方向,除了解决缩放的问题,连接应该也是一个要优化的点。
跟Golang和Node.js比较,笔者这里没有详细的数据,大概是Golang还是全面占优,但是不是数量级上的差距。Node.js的镜像文件应该更大,它内置了一个V8,启动后内存占优,启动速度差不多,但都不是数量级的差距。就是说Java应用构建为原生镜像后,镜像大小、启动速度、内存占用这几项就不是劣势了。