如何避免带Netty服务端与客户端的Spring Boot应用出现CheckpointOpenResourceException
基于Azul 21.0.2,在集成Spring WebFlux(含基于Netty的响应式Web客户端)的Spring Boot 3.2.2应用中测试Coordinated Restore at Checkpoint(CRac)功能时,执行启动命令:
java -Dspring.context.checkpoint=onRefresh -XX:CRaCCheckpointTo=./crac -jar my-app.jar
出现原生代码层面的打开文件描述符问题(timerfd、eventfd、eventpoll),导致检查点生成失败,抛出如下异常:
17:01:11.144 7742 ERROR main o.s.boot.SpringApplication - Application run failed org.springframework.context.ApplicationContextException: Failed to take CRaC checkpoint on refresh at org.springframework.context.support.DefaultLifecycleProcessor$CracDelegate.checkpointRestore(DefaultLifecycleProcessor.java:534) ~[spring-context-6.1.3.jar!/:6.1.3] ... Caused by: org.crac.CheckpointException: null at org.crac.Core$Compat.checkpointRestore(Core.java:144) ~[crac-1.4.0.jar!/:na] at org.crac.Core.checkpointRestore(Core.java:237) ~[crac-1.4.0.jar!/:na] at org.springframework.context.support.DefaultLifecycleProcessor$CracDelegate.checkpointRestore(DefaultLifecycleProcessor.java:528) ~[spring-context-6.1.3.jar!/:6.1.3] ... 15 common frames omitted Suppressed: jdk.internal.crac.mirror.impl.CheckpointOpenResourceException: FD fd=6 type=unknown path=anon_inode:[eventpoll] ... 17 common frames omitted Suppressed: jdk.internal.crac.mirror.impl.CheckpointOpenResourceException: FD fd=7 type=unknown path=anon_inode:[eventfd] ... 17 common frames omitted Suppressed: jdk.internal.crac.mirror.impl.CheckpointOpenResourceException: FD fd=10 type=unknown path=anon_inode:[timerfd] ... 17 common frames omitted ...
补充信息:
- 已确认问题与Netty强相关,移除所有Netty依赖/代码后检查点可正常生成。
- 同时使用WebFlux服务端和WebClient时会触发该问题,应用还集成了Spring Data Redis Reactive(Lettuce)、Spring Kafka、Spring GraphQL,采用Kotlin协程开发。
问题本质
这不是Spring框架的Bug,而是Netty在CRac场景下的资源生命周期管理问题:Netty的EventLoopGroup(包括服务端WebFlux的EventLoop和WebClient的EventLoop)会提前初始化并持有原生文件描述符(eventpoll、eventfd、timerfd属于Netty NIO事件循环的核心资源),而Spring在onRefresh阶段触发检查点时,这些资源还未被正确关闭/暂停,导致CRac检测到打开的原生资源并抛出异常。
分步解决方法
1. 延迟WebClient的EventLoop初始化
WebClient默认会提前创建自己的EventLoopGroup,在检查点生成前就持有了打开的文件描述符。可以通过配置让WebClient的EventLoopGroup在检查点之后再初始化:
@Bean fun webClient(): WebClient { return WebClient.builder() .clientConnector(ReactorClientHttpConnector { httpClient -> httpClient.run { tcpConfiguration { tcpClient -> tcpClient.bootstrap { b -> b.group(NioEventLoopGroup(0)) // 0表示使用默认线程数,但延迟初始化 } } } }) .build() }
2. 控制WebFlux服务端EventLoop的生命周期
Spring WebFlux的Netty服务端默认会在上下文刷新阶段启动EventLoop,需要自定义配置让其在检查点完成后再启动:
@Bean fun nettyServerCustomizer(): NettyServerCustomizer { return NettyServerCustomizer { httpServer -> httpServer.run { tcpConfiguration { tcpServer -> tcpServer.bootstrap { b -> b.group(NioEventLoopGroup(0)) } } } } }
3. 处理Lettuce Redis客户端的Netty资源
Lettuce也依赖Netty,需要配置Lettuce客户端在检查点前关闭连接池,检查点恢复后重新初始化:
@Bean fun redisConnectionFactory(): ReactiveRedisConnectionFactory { val clientConfig = LettuceClientConfiguration.builder() .clientOptions(ClientOptions.builder() .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) .build()) .build() val redisConfig = RedisStandaloneConfiguration("localhost", 6379) return LettuceConnectionFactory(redisConfig, clientConfig).apply { // 注册CRac钩子,检查点前关闭连接 addLifecycleProcessor(object : Lifecycle { override fun isRunning(): Boolean = false override fun start() {} override fun stop() { this@apply.shutdown() } }) } }
4. 调整CRac检查点触发时机
如果上述配置仍有问题,可以将检查点触发时机从onRefresh改为onApplicationReady,此时应用已完成初始化,部分资源可以被正确暂停:
java -Dspring.context.checkpoint=onApplicationReady -XX:CRaCCheckpointTo=./crac -jar my-app.jar
注意:此方法需要确保所有Netty资源在onApplicationReady前已正确注册CRac钩子,能够在检查点前暂停资源。
5. 使用Netty的CRac适配模块
Netty从4.1.94版本开始提供了CRac的初步支持,Spring Boot 3.2.2默认依赖Netty 4.1.100.Final,可手动添加Netty的CRac模块,它会自动注册CRac钩子,在检查点前关闭Netty的EventLoop资源,恢复时重新初始化:
<!-- Maven示例,Gradle同理 --> <dependency> <groupId>io.netty</groupId> <artifactId>netty-crac</artifactId> <version>4.1.100.Final</version> </dependency>
总结
核心思路是确保所有基于Netty的组件(WebFlux服务端、WebClient、Lettuce)在CRac检查点生成前,暂停或关闭持有原生文件描述符的资源,并在恢复时重新初始化。优先升级Netty版本并使用其CRac适配模块,其次调整检查点触发时机或自定义组件的初始化逻辑。
内容的提问来源于stack exchange,提问作者Brian Preuß

