升级 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 milestone 5 EnableAutoConfiguration spring.factories · Issue #32566 · spring-projects/spring-boot

从 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

从两次测试结果可以看出,在不做任何优化的前提下:

 
  • 低负载(-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

分别使用

压 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 的优化。

 
Rust 编写的 Zed 编辑器正式开源 原魅族副总嘲讽华为花上万亿建设鸿蒙生态 Oracle 的 2024 年 Java 工作规划 逆天神机 —— 17 寸的 64 核 AMD EPYC 工作站 德国程序员因报告漏洞被判罚 2.4 万元 ioGame17 文档或将强制收费,netty java 游戏服务器 Docker 25.0.0 发布 网易云音乐第三方开源 API 因侵权被要求删除 FreeBSD 也要“锈化”?团队称考虑在基础系统采用 Rust AI 工具正在导致代码质量的下降
{{o.name}}
{{m.name}}

猜你喜欢

转载自my.oschina.net/klblog/blog/10946547