初识spring native

官方介绍

Spring Native提供了使用GraalVM 本机图像编译器将Spring应用程序编译为本机可执行文件的支持。
与Java虚拟机相比,本机映像可以为许多类型的工作负载提供更便宜,更可持续的托管。这些包括微服务,功能工作负载,非常适合容器和Kubernetes
使用本机映像具有关键优势,例如即时启动,即时峰值性能和减少的内存消耗。
GraalVM本机项目希望随着时间的推移会改善一些缺点和折衷方案。构建本机映像是一个繁重的过程,比常规应用程序要慢。预热后,本机映像具有较少的运行时优化。最后,它比具有某些不同行为的JVM还不成熟。
常规JVM和此本机映像平台之间的主要区别是:

- 在构建时将未使用的零件删除。
- 反射,资源和动态代理需要配置。
- 类路径在构建时是固定的。
- 没有类延迟加载:可执行文件中附带的所有内容都将在启动时加载到内存中。
- 一些代码将在构建时运行。
- 围绕Java应用程序的某些方面存在一些局限性,这些局限性未得到完全支持。

简单来说,就是更快.更短,更小

  • 更快的启动速度
  • 更短的响应时间
  • 更小的内存消耗

十分适用于目前互联网环境的快捷开发和微服务架构的项目

而Spring Native的基础则是Graalvm,一个由oracle开发维护的多语言编译/运行时平台.
它的官方说法是高性能JDK发行版,目前已支持到7种语言,包括不仅限于java.ruby.node等
基于graalvm开发的java框架还有一个国内目前还不算太火的Quarkus,在一些油管up的测评视频中,证明quarkus(1.13)要比spring native(0.7x)更快更小,我也写了quarkus快一年了,即使抛弃graalvm本身,也确实比springboot要更快,更短

代码部分

我用的是0.92,仅支持springboot2.4.5
还有,机器的内存最好备到8个G,因为我测试时候memory in use一度飙升到5个多G,这可能也是graalvm为了效率更高付出的代价吧

pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.xiaowu</groupId>
<artifactId>behappy-springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>behappy-springboot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-native.version>0.9.2</spring-native.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>${spring-native.version}</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>${spring-native.version}</version>
<executions>
<execution>
<id>test-generate</id>
<goals>
<goal>test-generate</goal>
</goals>
</execution>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>native-image</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>native-image-maven-plugin</artifactId>
<version>21.0.0.2</version>
<configuration>
<!-- The native image build needs to know the entry point to your application -->
<mainClass>org.xiaowu.behappy.BehappySpringbootApplication</mainClass>
<!--https://blog.csdn.net/u013794093/article/details/100094871?utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.baidujs&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.baidujs-->
<buildArgs>
<!--https://blog.csdn.net/thomasyuang/article/details/84318184-->
<arg>--static</arg><!--解决standard_init_linux.go:178: exec user process caused "no such file or directory"-->
<!--<arg>libc=musl</arg>gcc编译过程中会有些许问题,这个参数仅jdk11支持-->
</buildArgs>
</configuration>
<executions>
<execution>
<goals>
<goal>native-image</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<repositories>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>

测试Controller

1
2
3
4
5
6
7
8
9
10
11
/**
* 小五
*/
@RestController
public class TestController {

@GetMapping("/")
public String test() {
return "TestController";
}
}

linux环境下安装各种环境(windows需要安装各种c的运行库,安完还是缺...linux要好很多)

graalvm安装

1
2
3
4
5
使用SDKMan安装适当的SDK和GraalVM插件,SDKMan是Java SDK管理工具,可让轻松安装和配置GraalVM所需的依赖项(截至当日,支持8和11)
curl -s "https://get.sdkman.io" | bash
sdk install java 21.0.0.r8-grl
sdk use java 21.0.0.r8-grl
gu install native-image

gcc,unzip,zip,glibc-static,zlib

1
2
3
4
yum install -y glibc-static zlib zlib-devel zip unzip
sudo yum group install "Development Tools"
该命令安装了一堆新的软件包,包括gcc,g++和make。
gcc --version

构建

然后spring native提供了两种构建方式

将Spring Native应用程序构建到Docker映像中

1
2
3
4
运行以下命令以构建Docker映像文件,并将编译后的代码包装在构建包中。
mvn spring-boot:build-image
使用Docker运行映像:
docker run -p 8080:8080 docker.io/xxx

将Spring Native应用程序构建到可执行文件中

1
2
3
4
运行以下命令编译Spring Boot应用程序的本机二进制可执行文件:
mvn clean -Pnative-image package
通过运行以下命令来执行本机应用程序:
target/spring-native-example

横向对比

这里我启动的是可执行文件

速度可以说是相当快了
接下来给他做成docker容器再看下

1
2
3
4
5
6
FROM scratch
COPY target/org.xiaowu.behappy.behappyspringbootapplication /app
ENTRYPOINT ["/app"]

docker build -f Dockerfile -t behappy:1.0 .
docker run -p 8080:8080 --name springnative -d behappy:1.0

再贴两张youtube上某up做的对比图

项目仅加了一个webflux
前者是native的,后者是非native的,
可以看到size是有明显缩小的

上图的启动时间是3秒
下图可以看到0.1秒多,spring native官方介绍过,正常的一般启动都不会超过100毫秒,可以看出启动时间上的差距是巨大的
右面是两者的cpu消耗,io占用等信息,也能看出两者有着明显的差距

更新:SpingBoot3(时隔两年的第二次测试)

随着springboot3的发布,原spring native项目已被其包含在内(取代),现在可以直接使用

参考:

  1. https://news.sangniao.com/p/3379643754#Spring+Boot+3%E4%B8%AD%E7%9A%84Native+Image%E6%94%AF%E6%8C%81
  2. action workflow资源监控:https://github.com/runforesight/workflow-telemetry-action
  3. 问题解决:ERROR: Please rebuild the executable with an appropriate setting of the -march option.
  4. springboot - GraalVM 原生镜像(Image)支持

代码地址

因为构建需要太多资源,我这里直接使用github的actions

https://github.com/behappy-java-study/customerservice-spring-boot-3

  • orginal分支对应非native版本

  • native分支对应native版本

横向对比

构建消耗资源

CPU占用

  • 非native
  • native

内存占用

  • 非native
  • native

IO

  • 非native
  • native

最终构建完成时间

  • 非native(不到3分钟,当然这个时间不稳定,但也基本维持在3-5分钟之间)
  • native(维持在15分钟上下)

容器启动时间

启动容器

docker run --name spring-boot-3-native -d -p 8501:8500 wangxiaowu950330/customerservice-spring-boot-3:0.0.1-SNAPSHOT-native
docker run --name spring-boot-3-original -d -p 8502:8500 wangxiaowu950330/customerservice-spring-boot-3:0.0.1-SNAPSHOT-original

  • 非native(7秒左右启动成功)
1
2
3
4
5
6
7
8
9
10
11
[root@development1 ~]# docker logs spring-boot-3-original --tail 10 -f
2023-07-28T09:00:12.700Z WARN 1 --- [ main] org.hibernate.orm.deprecation : HHH90000021: Encountered deprecated setting [javax.persistence.sharedCache.mode], use [jakarta.persistence.sharedCache.mode] instead
2023-07-28T09:00:12.848Z INFO 1 --- [ main] SQL dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
Hibernate: drop table if exists customer cascade
Hibernate: create table customer (id uuid not null, inserted_at timestamp(6), inserted_by varchar(255), updated_at timestamp(6), updated_by varchar(255), version bigint, first_name varchar(255), last_name varchar(255), primary key (id))
2023-07-28T09:00:13.998Z INFO 1 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-07-28T09:00:14.010Z INFO 1 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-07-28T09:00:14.321Z WARN 1 --- [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-07-28T09:00:14.902Z INFO 1 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 13 endpoint(s) beneath base path '/monitoring'
2023-07-28T09:00:15.057Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8500 (http) with context path ''
2023-07-28T09:00:15.103Z INFO 1 --- [ main] c.g.w.s.c.CustomerserviceApplication : Started CustomerserviceApplication in 6.581 seconds (process running for 7.039)
  • native(依旧很快,毫秒级别,大概200毫秒启动完成)
1
2
3
4
5
6
7
8
9
10
11
[root@development1 ~]# docker logs spring-boot-3-native --tail 10 -f
2023-07-28T08:48:52.085Z INFO 1 --- [ main] SQL dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
Hibernate: drop table if exists customer cascade
Hibernate: create table customer (id uuid not null, inserted_at timestamp(6), inserted_by varchar(255), updated_at timestamp(6), updated_by varchar(255), version bigint, first_name varchar(255), last_name varchar(255), primary key (id))
2023-07-28T08:48:52.100Z INFO 1 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-07-28T08:48:52.101Z INFO 1 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-07-28T08:48:52.138Z WARN 1 --- [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-07-28T08:48:52.177Z INFO 1 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 13 endpoint(s) beneath base path '/monitoring'
2023-07-28T08:48:52.187Z INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8500 (http) with context path ''
2023-07-28T08:48:52.187Z INFO 1 --- [ main] c.g.w.s.c.CustomerserviceApplication : Started CustomerserviceApplication in 0.224 seconds (process running for 0.233)
2023-07-28T08:58:47.968Z WARN 1 --- [ice housekeeper] com.zaxxer.hikari.pool.HikariPool : customerservice - Thread starvation or clock leap detected (housekeeper delta=4m25s796ms902?s592ns).

压测

  1. 因为非native到达850-900线程数时,吞吐量已基本处于下跌状态且开始出现报错,所有姑且当最高承受线程数(压测线程数)为:800
  2. 压测接口行为为 插入数据,当表数量到达10万时截图并观察资源占用及吞吐量

吞吐量

  • 非native(吞吐大概在690/s上下)
  • native

cpu占用/内存占用/io

  • 非native
  • native

结论

  • 构建阶段:无论是所需资源还是时间,native方式无疑是被碾压的,相比原始方式

    • 前者构建时间是后者3倍之多
    • 前者所需内存最高点达到6500MB,而反观后者,几乎平均在1000MB上下
    • 前者cpu几乎是全程跑满的,而后者虽达到90%,但全程波浪线,并不会全程吃满
  • 运行阶段,native方式相比原始方式

    • 启动时间:因为aot的构建特点,所有代码都不会在运行阶段动态编译,相比后者,启动时间确实绝对的优势
    • 运行期间的资源占用:前者的资源占用确实没有后者吃的多,但相比之下还是内存占用的优化更为明显,内存顶峰值也才300多兆,而cpu其实在运行期间一直在反复横跳
    • 吞吐:这个是之前没发现的,同样压测800线程,native方式运行的程序吞吐量竟要比后者低了200多,这里需要后续再查阅下相关资料。
  • 目前来看,spring native最有优势的地方就是运行内存,但如果仅仅因为这点内存就耗费大量人力/精力去迁移到graalvm的话,有点不太划算。而且拿测试结果来说,吞吐下降这么多,其对于高并发项目似乎并不是太友好。

  • plus:后续待更新。。。