升级 JDK21、 Spring Boot 3.2并开启 Virtual Thread、CRaC
背景
-
JDK21 已发布一段时间,是 JDK17 后的的又一个长期维护版本,支持了 Virtual Thread、CRaC 特性,并带来了新的分代 ZGC 算法
-
Spring Boot 3.2.1 (
Runtime efficiency with Spring (today and tomorrow) )版本发布后,框架层面原生的支持了 Virtual Thread、CRaC 特性
同时在 ops-job 上应用积累经验,可在其他项目如 Apollo 、xxljob 上继续落地
ps:本次升级项目原依赖是 JDK17,Spring Boot 2.6.5
关键结果(收益)
-
在不影响程序逻辑情况下,大幅缩短启动时间
-
内存使用降低,性能更好
-
CPU 资源使用率降低(因 GC 导致的 CPU 使用降低)
升级改动
Maven 依赖调整
调整 Spring Boot 的依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.1</version> <relativePath/> <!-- lookup parent from repository --> </parent>
调整 Spring Cloud 、JDK 的依赖
<properties> <java.version>21</java.version> <spring-cloud.version>2021.0.1</spring-cloud.version> </properties>
代码兼容性改动
Spring Boot3 升级是个比较大的变动,有很多的不兼容性。这里只记录 ops-job 这个项目遇到的问题。更多问题参考:Spring Boot 3.0 Migration Guide (官网升级指南必看)
1、包名变动
javax 更名为 jakarta,相关的资源都需要改动。比如:
@PostConstruct 注解包名拜改变路径:javax.annotation.PostConstruct 变到 jakarta.annotation.PostConstruct
2、Spring 的 @Bean
注解只能用于有返回值的方法。
比如下面代码,想在 Bean 初始化生命周期中运行一些逻辑,升级后就不支持了。被 @Bean
注解的方法需要有返回值了
@Bean public void initSenTry() { String dsn = xx Sentry.init(options -> { options.setDsn(dsn); options.setEnvironment(env); }); SentryAppender sentryAppender = new SentryAppender(); sentryAppender.setContext(ctx); ThresholdFilter filter = new ThresholdFilter(); filter.setLevel(Level.ERROR.levelStr); filter.start(); sentryAppender.addFilter(filter); sentryAppender.start(); ctx.addTurboFilter(new TurboFilter() { @Override public FilterReply decide(Marker marker, ch.qos.logback.classic.Logger logger, Level level, String format, Object[] params, Throwable t) { logger.addAppender(sentryAppender); return FilterReply.NEUTRAL; } }); }
3、Spring 日志系统变化
slf4j 的 StaticLoggerBinder 类没有了,想要获取 LoggerContext 对象实现日志级别的动态调整,需要使用
LoggerFactory.getILoggerFactory 取而代之。比如:
private final LoggerContext ctx = (LoggerContext) LoggerFactory.getILoggerFactory();
4、Apollo 功能受限
Apollo 配置中心的 @ApolloConfig
注解失效了(内部配置加载逻辑是正常的,不影响应用启动)。比如:
@ApolloConfig private Config config;
从 Spring Boot 3 M5 开始,来自 spring.factories 文件的“org.springframework.boot.autoconfigure.EnableAutoConfiguration”自动配置注册不再起作用
基于此,预计有很多 start 都会受到影响,比如 mybatis-plus 、xxl-job
- 临时解决
在类路径下创建META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件,把 Apollo 的自动加载类放进去,如:
com.ctrip.framework.apollo.spring.boot.ApolloAutoConfiguration com.taptap.xxl.job.spring.XxlJobAutoConfiguration
其他有影响的框架,都可以复制下自动加载类,放到这个文件里
- 最终解决
可以尝试用最新版本,看官方是否解决了 Spring Boot3.x 的兼容性问题。
1、apollo-client : 升级到 2.1.0(apollo-client support spring boot 3.0 by nobodyiam · Pull Request #4 · apolloconfig/apollo-java )
2、xxl-job-spring-boot-starter : 更新到 2.3.2-183
问题记录
1、日志框架冲突
服务启动,会输出如下的日志
Standard Commons Logging discovery in action with spring-jcl: please remove commons-logging.jar from classpath in order to avoid potential conflicts
这个是 Spring Boot 的依赖冲突检查输出的,在 Maven 里排除 commons-logging.jar
就好了,如:
<dependency> <groupId>ru.yandex.clickhouse</groupId> <artifactId>clickhouse-jdbc</artifactId> <version>0.3.2</version> <exclusions> <exclusion> <artifactId>commons-logging</artifactId> <groupId>commons-logging</groupId> </exclusion> </exclusions> </dependency>
2、Server VM warning
当加载 agent 时,会出现如下的警告日志。
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended [otel.javaagent 2024-01-23 17:38:45:581 +0800] [main] INFO io.opentelemetry.javaagent.tooling.VersionLogger - opentelemetry-javaagent - version: 1.12.1 OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
可通过 JVM 参数 -Xshare:off 关闭 CDS 相关的逻辑
大功告成
如果一切顺利,启动成功后会在控制台看到如下输出
开启 Virtual Thread
启用并验证
通过如下配置,可以一键开启 spring 的 Virtual Thread 特性。
spring.threads.virtual.enabled=true
验证是否开启了
@RequestMapping("/") @RestController public class VirtualController { @GetMapping("/test") public String virtual() { System.out.println(Thread.currentThread()); return "test"; } }
上面代码将会在控制台输出
VirtualThread[#70,tomcat-handler-0]/runnable@ForkJoinPool-1-worker-1
性能测试
环境:Redis 为本地的实例
@RequestMapping("/") @RestController public class VirtualController { final StringRedisTemplate redisTemplate; public VirtualController(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } @GetMapping("/test") public String virtual() { redisTemplate.opsForValue().set("test", "test"); return redisTemplate.opsForValue().get("test"); } }
我在本地通过 wrk 压测上面的代码 (读写了下 Redis ),发现在不做任何参数调优的情况下,结果如下
tomcat 默认的线程池配置:
server.tomcat.threads.max=200 server.tomcat.threads.min-spare=10
-
wrk -t80 -c100 -d 10s --latency http://127.0.0.1:8040/test
-
wrk -t150 -c200 -d 10s --latency http://127.0.0.1:8040/test
从两次测试结果可以看出,在不做任何优化的前提下:
-
低负载(-t80 -c100):性能相当,差别不大
-
高负载(-t150 -c200):Virtual threads 依然保持高性能,Platform threads 出现了性能下降的问题。且两者 QPS 差距非常明显,Virtual threads 比 Platform threads 多 19%~29% 的性能。
在 Platform threads 模式下,尝试调大 tomcat 的线程参数
server.tomcat.threads.max=300 server.tomcat.threads.min-spare=50
分别使用
-
wrk -t150 -c200 -d 10s --latency http://127.0.0.1:8040/test
-
wrk -t80 -c100 -d 10s --latency http://127.0.0.1:8040/test
压 Platform threads 的服务。
从 tomcat 调参后的测试结果看,至少我本地这个环境,这个场景没法在通过加大 threads 数加大性能了。也在一次印证了 Platform threads 模式下,负载高过一个临界值后,性能会下降。
三方测试
-
三方测试参考:
虚拟线程原理及性能分析
开启 CRaC
说明(还不成熟)
CRaC 当前只是初步支持,有些场景:比如内存里维护了复杂状态的应用,可能会遇到问题,启用前请谨慎做好全场景的测试。
参考文档:Spring Boot Reference Documentation \
JVM Checkpoint Restore :: Spring Framework
启用并验证
集成步骤
-
1、JVM 层面(指定内存 dump 路径):启动时添加
-XX:CRaCCheckpointTo=PATH
参数,指定 CRaC 的输出加载路径 -
2、Spring 层面(找个合适的时机触发内存 dump):启动时添加
-Dspring.context.checkpoint=onRefresh
参数。该阶段启动时会自动创建检查点LifecycleProcessor.onRefresh
。此阶段完成后,所有非延迟初始化的单例都已实例化,并且InitializingBean#afterPropertiesSet
回调已被调用;但生命周期尚未开始,且ContextRefreshedEvent
尚未发布。 -
3、JVM层面(加载内存恢复状态):启动时添加
-XX:CRaCRestoreFrom=PATH
参数,指定加载的 CRaC 的路径
从集成步骤看,第 1、2 步应该都发生在 CI 阶段,且 Spring 的触发内存 dump 的意图很明显,等 onRefresh 完成后再出发,相当于初始 Bean 的时间就可以节省出来了。当然还可以使用 jcmd 指令触发 dump,比如:
jcmd target/example-spring-boot-0.0.1-SNAPSHOT.jar JDK.checkpoint
这个可以在任意时候触发,这就可以在 dump 前做完所有的预热逻辑,然后 dump 出来的状态就是性能峰值状态了
遇到的问题
1、JDK依赖问题
CRaC 特性依赖 JDK 特性支持,目前 openjdk 发行版只支持到 JDK17:Releases · CRaC/openjdk-builds 。如果在不支持的 JDK 下启用
CRaCCheckpointTo
,则会输出:
Unrecognized VM option 'CRaCCheckpointTo' Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit.
CRaC 最早是 Azul 发起的一个项目,可以用 Azul 的社区发行版来验证 CRaC 特性,如:
docker image:azul/zulu-openjdk:21-jdk-crac
2、GC 算法问题
ZGC 算法下,不支持 CRaC,在 ZGC 启用时,会输出:
Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit. -XX:+UseZGC is currently unsupported for -XX:CRaCCheckpointTo.
为了验证 CRaC 功能,只好先移除 -XX:+UseZGC
3、打开的 FD & Socket 问题
CRaC 要求应用程序关闭所有打开的文件、网络连接等。在 Linux 上,这些内容表示为文件描述符。但是,可能很难更改应用程序以与检查点正确协调,例如,由于无法修改库中的代码。在这些情况下,CRaC 通过配置提供有限的处理。
理论上所有的资源都需要向 JVM 注册资源的 Checkpoint 前后的资源状态,Spring 内置的依赖都处理好了这一步,但是三方依赖,比如 Opentelemetry 、Apollo 等没有做这一步,就会出现一些异常。
- Opentelemetry 的问题
Suppressed: jdk.internal.crac.mirror.impl.CheckpointOpenFileException: FD fd=35 type=regular path=/tmp/opentelemetry-temp-jars2649059754140254392/jartqV3k80l.jar (deleted) at java.base/jdk.internal.crac.mirror.Core.translateJVMExceptions(Core.java:114) ~[na:na] at java.base/jdk.internal.crac.mirror.Core.checkpointRestore1(Core.java:188) ~[na:na] at java.base/jdk.internal.crac.mirror.Core.checkpointRestore(Core.java:286) ~[na:na] at java.base/jdk.internal.crac.mirror.Core.checkpointRestore(Core.java:265) ~[na:na] at jdk.crac/jdk.crac.Core.checkpointRestore(Core.java:72) ~[jdk.crac:na] at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na] at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na] at org.crac.Core$Compat.checkpointRestore(Core.java:141) ~[crac-1.4.0.jar!/:na] ... 17 common frames omitted
- Apollo
Suppressed: java.nio.channels.IllegalSelectorException at java.base/sun.nio.ch.EPollSelectorImpl.beforeCheckpoint(EPollSelectorImpl.java:401) at java.base/jdk.internal.crac.mirror.impl.AbstractContext.invokeBeforeCheckpoint(AbstractContext.java:43) at java.base/jdk.internal.crac.mirror.impl.AbstractContext.beforeCheckpoint(AbstractContext.java:58) at java.base/jdk.internal.crac.mirror.impl.BlockingOrderedContext.beforeCheckpoint(BlockingOrderedContext.java:64) at java.base/jdk.internal.crac.mirror.impl.AbstractContext.invokeBeforeCheckpoint(AbstractContext.java:43) at java.base/jdk.internal.crac.mirror.impl.AbstractContext.beforeCheckpoint(AbstractContext.java:58) at java.base/jdk.internal.crac.mirror.Core.checkpointRestore1(Core.java:153) at java.base/jdk.internal.crac.mirror.Core.checkpointRestore(Core.java:286) at java.base/jdk.internal.crac.mirror.Core.checkpointRestoreInternal(Core.java:299) Suppressed: jdk.internal.crac.mirror.impl.CheckpointOpenSocketException: Socket[addr=apollo-config.dev.tapsvc.com/172.20.12.187,port=80,localport=47004] at java.base/jdk.internal.crac.JDKSocketResourceBase.lambda$beforeCheckpoint$0(JDKSocketResourceBase.java:68) at java.base/jdk.internal.crac.mirror.Core.checkpointRestore1(Core.java:169) at java.base/jdk.internal.crac.mirror.Core.checkpointRestore(Core.java:286) at java.base/jdk.internal.crac.mirror.Core.checkpointRestoreInternal(Core.java:299)
解决
可通过 File Descriptor Policies来有限的处理。
新建文件 crac.yaml ,配置内容如下:
type: socket localAddress: * action: ignore --- type: file path: /opt action: ignore --- type: file path: /tmp action: ignore --- type: pipe action: ignore
在 Java 应用启动系统参数里设置 -Djdk.crac.resource-policies = /{path}/crac.yaml
4、遗留的问题
遗留了一个问题,怎么都处理不了。刚好这个问题我认识的一个好友(Apollo 的作者)也遇到了,issue 如下:
Suppressed: jdk.internal.crac.mirror.impl.CheckpointOpenResourceException: FD fd=12 type=unknown path=anon_inode:[eventpoll] at java.base/jdk.internal.crac.mirror.Core.translateJVMExceptions(Core.java:117) at java.base/jdk.internal.crac.mirror.Core.checkpointRestore1(Core.java:188) at java.base/jdk.internal.crac.mirror.Core.checkpointRestore(Core.java:286) at java.base/jdk.internal.crac.mirror.Core.checkpointRestoreInternal(Core.java:299) Suppressed: jdk.internal.crac.mirror.impl.CheckpointOpenResourceException: FD fd=13 type=unknown path=anon_inode:[eventfd] at java.base/jdk.internal.crac.mirror.Core.translateJVMExceptions(Core.java:117) at java.base/jdk.internal.crac.mirror.Core.checkpointRestore1(Core.java:188) at java.base/jdk.internal.crac.mirror.Core.checkpointRestore(Core.java:286) at java.base/jdk.internal.crac.mirror.Core.checkpointRestoreInternal(Core.java:299)
关于CRaC 的结论
基于如下:
-
Openjdk 发行版并未全部覆盖支持,目前支持的最高版本是 JDK17。更高版本的支持只能用 Azul 的 JDK
-
当前还有非常多的集成问题,且大量第三方包并没有做适配,Azul 官方给的 resource-policies 解决方案还属于初步阶段
-
Spring Boot 3.2 也是初步支持,Issue 区有大量集成的问题
综上,CRaC 距离生产可用还有很长的路要长,至少,当前解决应用启动问题,预热峰值问题,GraalVM 的 Native 方案比 CRaC 要更成熟。当然,未来哪个方向会成为标准还不好说,CRaC 的优势是保留了 JIT 的优化。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
记一次 golang 的 zstd 压缩、解压缩优化
问题背景 1、开发反馈 trs 的 stg 环境开启 zstd 解压缩后,内存有明显持续上涨趋势,最终导致 OOM 如图,内存频繁申请释放,当时分析导致 OOM 的原因是因为 stg 的 CPU 不够,导致 GC 不及时,调整 CPU 资源后确实 OOM 没有了。并未怀疑程序本身的性能问题 2、infra 同学发现 adx 的服务存在 zstd 压缩导致 CPU 资源消耗异常的问题,发现是压缩对象的 init 操作非常重导致。 问题分析 结合上面两次问题,想到 Redis 压缩降本时提交的 go 的 zstd 代码有很大优化空间的。可将 zstd.NewWriter 、zstd.NewReader 等重对象使用 sync.Pool 缓存起来,每次使用时从池中取,用完在放回去,避免频繁 New 对象造成内存申请多从而造成 GC 压力大,CPU 资源消耗高的问题。 预期关键结果(收益) 开压缩相关的接口 RT 明显降低,压缩&解压缩申请的内存变少 CPU 资源显著降低,部分实例可减少申请 CPU 的 request 和 limit (减少实例数) 总的来说应该可以提高性能,降低资源...
- 下一篇
从 Greenplum 到 Databend,万全网络数据库平台架构演进
作者: 代城 万全网络高级工程师,负责万全网络数据平台整体架构研发工作,拥有超过 7 年的大数据相关技术研发经验,一直关注着开源和云技术的发展。 万全网络科技有限公司是一家专注于 B 端电商物流供应链的公司。致力于为客户提供全面的供应链解决方案,涵盖从产品采购到最终配送的全程服务。 公司的服务包括但不限于:供应链管理,仓储与配送,信息技术支持。 迁移背景 在不断发展的科技环境中,企业往往需要不断调整和优化其技术基础设施以适应变化的业务需求。 万全数据中台架构从 2020 年开始构建,历经 3 年多的时间打磨,已经实现了数仓开发的大部分基础功能,包括数据源管理、数据离线同步、数据实时同步、数据资源目录、数据开发、任务依赖管理以及数据服务接口。 此平台不仅支持数据的采集、清洗、加工,还涵盖了数据对外提供服务的完整链路。数据计算同步和计算引擎都是基于 Spark 构建的,确保了平台的稳定性和高效性。 在过去的几年中,它主要使用 Greenplum 作为大数据应用层数据库,该数据库主要用于支撑万全的业务线报表、大屏展示、帆软报表、SFA 运营报表等应用。然而,随着业务的不断发展和平台的逐步完善...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2全家桶,快速入门学习开发网站教程
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8编译安装MySQL8.0.19
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Red5直播服务器,属于Java语言的直播服务器