Spring Boot应用线程阻塞致请求挂起的排查与调优问询
结合你提供的jstack日志和应用场景,核心问题很明确:Jetty工作线程被阻塞的Mqtt同步发布调用占用,导致线程池耗尽,新请求无法处理;同时自定义线程池也处于等待状态,任务队列可能已经堆积。下面逐个解答你的三个问题:
1. 如何确定Spring应用/JVM/Jetty服务器可承载的线程数量?
确定合理的线程数需要结合应用类型、硬件资源和实际压测验证,分几个维度来看:
- 区分应用类型:你的应用以长等待API(12-60秒)为主,属于IO密集型(大部分时间在等待外部资源,比如Mqtt消息发送、远程调用),这类场景的线程数公式参考:
线程数 = CPU核心数 × (1 + 等待时间/CPU处理时间)。AWS t2.medium是2核CPU,假设你的API等待时间是30秒,CPU处理时间是0.1秒,那么线程数可以设置在2×(1+30/0.1)=602左右,但实际要考虑资源上限,不能盲目设置过高。 - 考虑JVM资源限制:每个线程默认栈大小是1MB左右,250个线程就占用约250MB内存,你的JVM堆是2G,t2.medium总内存4G,内存层面还有余量,但线程数过多会导致CPU上下文切换开销剧增,反而降低性能。
- Jetty线程池的参考值:Jetty的
QueuedThreadPool默认最大线程数是200,你当前线程数到250+说明已经超过默认值,需要结合压测调整。可以先从核心线程数=CPU核心数×2,最大线程数=CPU核心数×10开始测试,再根据监控数据调整。 - 实际压测验证:用JMeter、Gatling等工具模拟并发请求,监控CPU使用率、线程池活跃数、响应时间、错误率等指标,找到性能拐点(比如CPU使用率超过80%、响应时间急剧上升),此时的并发数就是你的合理承载阈值。
2. 如何调优该应用以避免此类服务不可用的情况?
针对你的场景,核心优化方向是减少Jetty工作线程的阻塞,同时优化线程池配置:
- 异步化阻塞操作:从jstack日志看到,Jetty工作线程卡在
MqttTopic.publish的同步调用上,这是关键瓶颈。把Mqtt消息发布操作放到自定义的threadPoolTaskExecutor中异步执行,不要占用Jetty的工作线程。示例代码:@Autowired private ThreadPoolTaskExecutor mqttTaskExecutor; public void publishMqttMessage(String topic, String payload) { mqttTaskExecutor.submit(() -> { // 原Mqtt同步发布逻辑 mqttTopic.publish(payload.getBytes()); }); } - 优化Mqtt客户端配置:改用
MqttAsyncClient的异步发布API,避免同步阻塞;检查Mqtt的QoS级别,如果不需要消息可靠送达,降低到QoS 0(无需确认),减少等待时间;同时配置Mqtt客户端的超时时间,避免无限等待。 - 调整线程池参数:
- Jetty线程池:设置合理的
minThreads(核心线程数)、maxThreads(最大线程数)和idleTimeout(空闲线程超时时间),比如minThreads=4、maxThreads=100,避免线程数过度膨胀; - 自定义
threadPoolTaskExecutor:根据异步任务的数量调整核心/最大线程数,设置合适的队列容量(比如LinkedBlockingQueue设置固定容量),并配置拒绝策略(比如CallerRunsPolicy让调用线程处理,或者自定义返回503错误),防止任务无限堆积。
- Jetty线程池:设置合理的
- 添加请求超时控制:针对长等待API,在Spring中通过
@RequestMapping(timeout = ...)设置请求超时,或者在Jetty配置中设置全局请求超时,避免线程被长时间占用。 - 监控线程状态:用JMX、Prometheus+Grafana监控线程池的活跃线程数、队列长度、任务拒绝次数,当指标异常时及时告警,提前发现问题。
3. 如何在应用挂起前对API进行限流?
限流的核心是在资源耗尽前拒绝部分请求,保护应用可用,推荐几种适合你场景的方案:
- 基于线程池的限流:利用线程池的最大线程数+队列容量实现限流,当线程池满、队列也满时,触发拒绝策略,直接返回503 Service Unavailable。这种方式简单有效,适合控制并发请求数。
- 使用Guava RateLimiter实现请求数限流:针对单个API或者全局设置每秒允许的请求数,比如限制长等待API每秒处理10个请求,避免过多请求占用资源。示例代码:
private final RateLimiter rateLimiter = RateLimiter.create(10.0); // 每秒10个请求 @GetMapping("/long-wait-api") public ResponseEntity<?> longWaitApi() { if (!rateLimiter.tryAcquire()) { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁,请稍后再试"); } // 业务逻辑 return ResponseEntity.ok("success"); } - Jetty内置限流Filter:使用Jetty的
QoSFilter或者自定义Filter,在请求进入Jetty工作线程前进行限流,比如限制并发连接数或者请求速率。 - 熔断降级:结合Resilience4j的CircuitBreaker,当Mqtt服务响应慢或者不可用时,触发熔断,直接返回降级响应,避免线程被阻塞在Mqtt调用上。
- 基于资源指标的动态限流:监控CPU使用率、线程池活跃数等指标,当达到阈值(比如CPU超过70%、线程池活跃数达到80%最大线程数)时,触发限流,动态调整允许的请求数。
内容的提问来源于stack exchange,提问作者Sakshi
相关产品推荐
相关产品推荐

