一、线上服务突然无法接单

某天早上10点,运营同学传来消息:用户反馈电商平台的订单查询功能全失败,前端显示503错误。开发同学第一时间远程连接服务器,用curl命令请求服务接口,确实返回503状态码——说明服务进程还在运行,但已经无法处理任何新请求,就像你开的外卖餐馆,所有服务员都在忙, queue里的位置也满了,新客人根本没人接,只能直接拒绝接单。

二、揪出“没工人干活”的元凶

1.1 先看日志里的关键线索

开发同学立刻去应用日志里搜关键词,果然在error日志里看到这样的记录:QueuedThreadPool exhausted, maxThreads=50, activeThreads=50, queueSize=50——直白说就是Jetty的线程池已经耗尽了:设定的最大线程数只有50,而且全被占满,排队缓冲的队列也满了,新任务根本没法进,只能被拒绝。

1.2 用命令工具验证线程池状态

为了确认不是日志误报,同学用Java自带的命令行工具查线程池状态:

# 先找到Java进程ID,替换成实际的服务进程
jps -l | grep order-service
# 假设输出结果:18765 com.example.order.OrderApplication

# 查看该进程中Jetty线程池的运行情况
jstack 18765 | grep -E "jetty|QueuedThreadPool"

从输出里能看到,所有Jetty线程都处于RUNNABLE或WAITING状态,没有空闲线程,和日志的结论一致——确实是线程池耗尽导致的服务不可用。

1.3 根因分析:配置太“抠门”扛不住高并发

这个订单服务是用Spring Boot集成Jetty做的,原来的线程池配置是:核心线程20,最大线程50,队列50,空闲线程存活时间60秒。双11期间,服务的QPS(每秒请求量)冲到200,每个请求平均处理时间是1.5秒,按线程数的计算逻辑:需要的最小线程数是QPS * 平均处理时间 = 200 *1.5=300,远超过配置的最大线程50,队列的50个位置也根本缓冲不了积压的请求,最终导致所有请求被拒绝。

三、给线程池“加人手”修复故障

2.1 调整线程池核心配置

针对这个问题,开发同学修改了Jetty线程池的配置,用Spring Boot的Java配置类来定义更合理的参数,代码如下(技术栈统一用Java+Spring Boot 2.x集成Jetty):

import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.eclipse.jetty.util.thread.QueuedThreadPool;

@Configuration
public class JettyThreadPoolFix {
    @Bean
    public JettyServletWebServerFactory jettyFactory() {
        JettyServletWebServerFactory factory = new JettyServletWebServerFactory();
        // 调整后配置:核心线程50(日常够用),最大线程200(双11峰值扛住),队列100(缓冲请求),空闲线程30秒自动回收
        QueuedThreadPool threadPool = new QueuedThreadPool(
            200,    // 最大线程数
            50,     // 核心线程数
            30000   // 空闲线程存活时间,单位毫秒
        );
        factory.setThreadPool(threadPool);
        return factory;
    }
}

这个配置的逻辑很简单:平时用核心线程,不用多开浪费资源;峰值时最多扩容到200个线程,处理高并发;队列存100个请求,避免瞬间流量直接被拒绝;空闲线程30秒后自动销毁,减少资源占用。

2.2 加超时限制,避免线程被“卡死”

还有个隐形问题:订单查询请求里的数据库查询,原来没有设置超时,慢查询会一直占着线程,导致线程无法释放。同学给Jetty和数据库都加了超时配置,在application.yml里添加:

# Spring Boot配置,Jetty和数据库超时设置
server:
  jetty:
    connector:
      idle-timeout: 30000  # Jetty连接空闲30秒自动断开,避免无效连接占线程
  datasource:
    hikari:
      connection-timeout: 5000  # 数据库连接超时5秒
      leak-detection-threshold: 60000  # 线程泄漏检测,超过60秒未释放连接就报警

这样就算有慢查询,最多也只占线程5秒,超时后会自动释放回线程池,不会导致线程池被永久占满。

2.3 加监控告警,提前发现异常

修复完成后,同学还搭了Prometheus+Grafana监控Jetty线程池的指标,比如活跃线程数、队列大小、拒绝请求数,设置告警规则:当活跃线程数达到最大线程数的80%时,就给运维发告警,这样不用等服务挂了才发现问题。

四、这些细节你得留意

3.1 应用场景

这种线程池耗尽的问题,大多出现在高并发场景(比如电商大促、秒杀)、任务耗时差异大的服务(比如需要调用多个下游接口的服务)、依赖不稳定的服务(比如数据库、第三方接口响应慢)。只要是用线程池处理请求的Web服务,都可能遇到。

3.2 技术优缺点

调大线程池的好处是能提升服务的吞吐量,扛住高并发;但坏处也很明显:线程太多会导致Java的上下文切换频繁,反而降低性能,就像一个餐馆突然招了1000个服务员,但厨房和出餐口根本跟不上,反而会更乱。队列的作用是平滑流量,但队列太大也会导致请求积压,内存占用变高,还要平衡线程数和下游服务的处理能力(比如数据库最多能扛100个连接,线程数就不能超过100)。

3.3 注意事项

  1. 不能只凭感觉设线程数,要根据QPS * 平均任务耗时的公式估算,再结合下游服务的能力调整;
  2. 必须给依赖的下游服务(数据库、缓存、接口)设置超时,避免线程被永久阻塞;
  3. 定期监控线程池指标,设置告警阈值,提前预判问题;
  4. 不要把队列设得太大,避免请求积压过多导致内存溢出。

五、总结

这次故障的核心是Jetty线程池配置不合理,无法应对双11的高并发,加上没有超时设置导致线程被慢查询占满,最终服务不可用。修复时调整了线程池的核心和最大线程数,优化了队列策略,添加了超时和监控告警,后续再也没出现类似问题。这个案例告诉我们:Web容器的线程池配置不是拍脑袋的参数,要结合业务的并发量、任务耗时、依赖服务的能力来调整,同时要有监控告警做保障,才能让服务稳定运行。