一、事故发生与应急触发
2025 年某月某日,正值下午 14:00 左右,我们监控系统突然跳出大量告警:下单服务(Java 微服务)响应延迟从常态的 200 ms 左右,飙升至近 5 秒,有部分请求多达 10 秒以上,而且请求失败率也迅速从 0.2% 攀升至约 7%。
业务同事立刻在群里报出:“用户下单卡住了,支付按钮一直转圈。”我和值班的运维、开发立刻赶到了线上。
初步观察
-
服务部署在 6 台实例上,CPU 和内存使用率都并未达到阈值(平均 CPU ~60%)。
-
然而线程池活跃数迅速上涨:activeThreads 从 ~45 增至 ~98(线程池 max=100),队列长度从 ~30 飙至 ~210。
-
JVM GC 日志中出现多次 Full GC 停顿约 0.6–0.9 秒,虽然未报 OutOfMemoryError。
-
日志里有大量
TimeoutException抛出,下游库存服务调用响应时间从 ~100 ms 提升至 > 900 ms。
鉴于影响严重,我们迅速启动「生产事故流程」:
-
临时增加服务实例数从 6 → 10。
-
下单通路限流:设置最大并发数为 500(比平时 1200 少很多)。
-
非核心模块(比如推荐、统计延迟)暂时关闭或降为后台异步。
这些操作在约 20 分钟内,使得成功率回升至 ~96%,响应时间降至 ~800 ms,但还远未恢复至正常水平。
二、现场数据收集与问题链拆解
恢复可用只是第一步,接下来必须定位根因,防止同类事故复发。我们按下列步骤开展。
2.1 构建时间线
-
13:55:库存服务日志中已有多次响应 > 500 ms,提示“锁等待 > 2 s”。
-
13:57:我们系统中的统计任务触发,全表扫描一批旧订单状态,更新量约 80 万条。
-
14:00:下单服务调用库存服务开始积压,线程池队列从 ~30 快速攀升。
-
14:02:监控显示,HTTP 504(网关超时)错误频次顶峰。
2.2 关键诊断数据
-
线程 Dump:在问题高峰期截取。大约 70% 线程在
LinkedBlockingQueue.take()或者ThreadPoolExecutor.getTask()阻塞状态。说明线程拿不到任务,是因为队列一直在等待或积压。 -
GC 日志:频繁的 Full GC,且 young 区 eden 淘汰变慢。说明虽然内存未爆满,但“对象积压 +线程等待”拖慢了释放速度。
-
下游服务慢日志:库存服务某批量更新 UPDATE 语句执行时间超 2 秒,并锁住了数千 ROW,导致后续 SELECT/UPDATE 进入等待。
-
请求链路分析:大部分下单请求的耗时集中在调用库存服务那一步,而非本地校验或支付接口。
通过以上,我们初步判定问题链为:“统计任务 → 库存服务锁等待 → 下单服务调用变慢 → 线程池积压 → 失败率飙升”。
三、根因深入与系统配置翻查
3.1 批量任务执行时机问题
统计任务本计划在夜间低峰运行,但因月末结算一天提前,错部署至高峰期运行。任务遍历旧订单表约 80 万条,并同步更新库存状态,该操作未加分片控制,锁等待严重。
3.2 下游服务缺乏保护机制
库存服务在处理该 UPDATE 操作时使用默认隔离级别,无限等待锁。调用方(下单服务)对库存服务响应未设超时或熔断,导致每个调用都卡着,线程资源逐渐耗尽。
3.3 上游服务线程/队列配置不适应突发变慢状况
下单服务配置:
threadPool:
corePoolSize: 50
maxPoolSize: 50
queueCapacity: 100
在系统正常时足够,但当下游变慢、流量偏高时,这个配置成为瓶颈。线程饱和后,队列达到 100,使得新请求延迟严重或被拒绝。
3.4 监控告警缺口
我们发现监控虽然有响应延迟、错误率阈值,但缺乏“线程池饱和比例”“队列长度趋势”“下游服务响应时间突变”这些告警维度。导致线程积压已严重但真正开始报障前我们没意识到。
四、修复措施与防再发策略
4.1 当天修补动作
-
将统计任务暂停,安排至夜间执行,并加分片(每批 5 万条)执行。
-
在库存服务层面,新增 UPDATE 语句批量控制,每次最多处理 1 万条,且设置锁等待超时 5 秒,超时则跳过。
-
下单服务改配置为:corePoolSize: 70,maxPoolSize: 100,queueCapacity: 300;同时增加 CallerRunsPolicy,以防队列满后直接新线程执行。
-
在调用库存服务处加超时:连接超时 300 ms、响应超时 800 ms;调度服务降级路径:若超时,则返回“库存状态暂不可用,请稍后”提示而非长期等待。
-
在监控系统中新增告警:线程池活跃 > 80%,队列长度 > 150,库存服务平均响应 > 500 ms。
这些调整当天就执行完毕,次日系统恢复正常,没有重现 5 秒以上响应或 7%错误率情况。
4.2 长期改进清单
-
定时任务迁移:将所有大规模更新任务全部迁移至夜间 00:00-04:00 高峰低期,并进行流控、分片。
-
服务弹性设计:库存服务与关键调用方引入熔断器(如 Resilience4j)和限流保护,避免单点延迟传导。
-
线程池配置复审:设定默认线程数较为保守,同时对“下游慢+流量增高”的组合场景做 压力测试。
-
监控告警完善:加入更丰富维度,如“调用者等待时间”“队列深度”“锁等待时间”“线程平均等待时间”等。
-
演练机制:每季度模拟“下游服务响应慢”场景,观察主服务是否会积压、延迟是否会上升,确保配置不会触边界。
五、几点现场心得
-
在生产环境中,往往不是一个单独问题触发,而是多个因素叠加:流量 ↑、下游 ↓、任务错峰 ↓。因此,单看“线程满”很可能误判。类似文章也指出,“下游系统延迟”是 Java 性能问题里的前几位。(yCrash)
-
线程池饱和或队列阻塞,是 症状,真正要问的是「为什么线程没办法释放/为什么队列一直在积压」。
-
降级及扩容虽然有效,但不能当作最终方案,它只是赢得时间。真正改造的是系统的弹性与监控体系。
-
监控系统必须「从链路思维」看问题,而不仅仅「单服务指标」。即:服务 A ↔ 服务 B ↔ 数据库请求,任何环节慢,都可能传播。
-
事故不仅是技术问题,也是沟通问题:现场及时发布「我们知道问题在进行中」「预计修复时间」「用户影响情况」,减轻业务焦虑。
六、结语
那天从早期响应到根因定位,用时近 4 小时才让系统恢复正常并稳定下来。虽然我们避免了重大损失,但也意识到系统还有隐患。我们用实际行动把一次“隐形故障”变成了改造契机。
希望这篇还原真实情景的技术笔记,能在你遇到类似 Java 服务响应慢、线程积压、下游服务变慢时,提供一点思路:别只看“谁出问题”,而是“为什么链条断了”。若你也有类似生产事故经验,欢迎分享、交流。愿你的系统稳定、夜间告警少。
浙公网安备 33010602011771号