博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

一、事故发生与应急触发

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 服务响应慢、线程积压、下游服务变慢时,提供一点思路:别只看“谁出问题”,而是“为什么链条断了”。若你也有类似生产事故经验,欢迎分享、交流。愿你的系统稳定、夜间告警少。