Administrator
发布于 2023-10-31 / 4223 阅读
88

Spring Boot 3 虚拟线程集成实践

等到了 Spring Boot 3.2 的虚拟线程开关

Spring Boot 3.2 把虚拟线程集成做进了自动配置,一个属性就能让 Tomcat 用虚拟线程处理请求。我在 3.2 RC 阶段就拉下来试了,把订单查询接口从平台线程池切过去,吞吐数据很说明问题。不过 RC 阶段 API 还有微调,正式版出来后我对照验证了一遍,结论基本一致。

配置方式

3.2 提供了 spring.threads.virtual.enabled=true,底层会给 Tomcat、MVC 异步、Scheduled 都换上虚拟线程执行器:

# application.yml
spring:
  threads:
    virtual:
      enabled: true

如果想只给 Tomcat 单独配,也可以自己声明执行器,粒度更细:

@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerCustomizer() {
    return handler -> {
        handler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
    };
}

我们最终选了全局开关,简单。但要注意:全局开启后,连 @Scheduled 也走虚拟线程,如果定时任务里有 synchronized 同样会 pin,得一并排查。

实测吞吐对比

压测环境:4 核 8G,JDK 21,接口内部模拟 80ms 的数据库等待(贴近真实订单查询)。用 JMeter 打 5000 并发,每轮跑 5 分钟取稳定值:

配置吞吐量P99错误率
平台线程池(200)2400 req/s380ms0%
虚拟线程9100 req/s96ms0%

吞吐翻了 3.8 倍,因为请求在等 DB 时载体线程立刻去服务别的虚拟线程,200 个平台线程的瓶颈被彻底打破。CPU 方面,平台线程池跑满时 95%,虚拟线程下 62%,更从容。

但别高兴太早

实测里踩了两个点,都是迁移虚拟线程的老生常谈但必须亲自验:

  • 我们代码里有段 synchronized 做本地缓存更新,虚拟线程被 pin 住,吞吐反而掉了 15%。改成 ReentrantLock 后恢复,并回到 9100 的水平;
  • ThreadLocal 存的用户上下文在虚拟线程下数量爆炸,原来线程池 200 个,现在几万个,内存涨了 400MB。改用 ScopedValue 后回落到正常水位。

适用边界

虚拟线程对 IO 等待型接口收益最大。我拿一个纯计算的报表接口测,吞吐几乎没变——计算不阻塞,虚拟线程的优势无从发挥,反而因为调度层多了点开销略慢一点点。所以别盲目全开,先挑 IO 密集的接口试水,观察载体线程 CPU 和内存再推广。

下篇预告

这篇先把《Spring Boot 3 虚拟线程集成实践》里的坑列了,下一篇写我们当时是怎么在线上工程里真正落地的——包括那次让领导拍桌的故障复盘。

参考