第15章 goroutine、阻塞与锁等待诊断
CPU 很低也可能已经过载
服务响应慢但 CPU 不高,往往意味着请求在等待。可能等连接、等锁、等 channel、等慢客户端,也可能等一个永远不会到来的结果。此时继续分析 CPU 热点容易偏离方向。goroutine profile 能展示当前执行者停在哪里,配合业务计数与阶段信息,可以把等待归到具体资源。
随书现场采样在两个 worker 与八个等待槽都占满时,/stats 观察到 accepted 为十、started 为二、active 为二、queued 为八。这个稳定状态由故障负载维持,goroutine 栈可以与工作池实现逐项对照。我们不只数 goroutine 总量,而是找哪个栈负责哪一种等待。
若大量请求卡在 Transport 获取连接,先检查活跃响应体与连接上限;若大量 worker 卡在同一锁,检查持锁操作;若生成端卡在 channel 发送,检查下游是否已经离开。每种堆栈都应引出一个可以证伪的假设,而不是直接贴上“Go 调度器问题”的标签。
一张堆栈只是一个时刻
goroutine dump 是快照。正常系统也会有许多等待网络的 goroutine,单张图不能证明泄漏。应在相同负载阶段采集多次,看数量是否持续增长、相同任务是否长期不消失、解除故障后是否恢复。与任务 ID、在途计数和接纳速率关联,比孤立查看栈更可靠。
泄漏也不一定表现为 goroutine 增长。连接、文件、缓存引用或定时任务可能占用资源而没有新增 goroutine。反过来,连接池扩张到稳定规模会增加 goroutine,却未必是泄漏。判断依据是是否违反资源上限与生命周期,而不是增长本身令人不安。
现场采样结束后发送 SIGTERM,并给很短排空预算,本机记录十个已接纳任务全部进入终结,其中取消为十,active 和 queued 都归零,进程以失败状态退出。这里 completed 表示终结总数,包含失败与取消,不能把它解读成十个业务成功。准确理解字段比漂亮的归零截图更重要。
阻塞采样和锁采样
block profile 可用于观察某些同步阻塞,mutex profile 关注锁竞争。这些采样通常需要显式启用并选择采样率,默认行为与成本要查实际 Go 版本文档。启用很高采样率会改变程序运行,不应在未测量开销时长期全量运行。
锁竞争高时,先看锁保护了什么。若临界区包含网络、磁盘、日志写入或大对象编码,减少锁内工作可能比替换锁类型更有效。拆锁之前还要检查跨字段不变量,否则吞吐提高却引入状态不一致。优化结束后必须重跑关闭与接纳竞争测试。
channel 阻塞也可能是正确背压。一个有界 worker 等待任务,或者生产者在受控容量内等待消费者,不一定有问题。关键是等待是否符合预算、是否能取消、是否有明确拥有者。诊断不是消除所有等待,而是识别无界、无价值或违反契约的等待。
trace 与事件顺序
运行时 trace 能帮助理解调度、系统调用、GC 与任务时序,但采集窗口和数据量需要控制。本书没有声称完成所有内核或调度器实验;如果读者进一步使用 trace,应先设计问题,例如某次延迟是否与 GC 或调度等待重叠,再选择短窗口验证。
应用级阶段事件也很重要。记录接纳、开始、外呼头到达、读取完成和终结,可以区分排队时间与执行时间。事件必须拥有稳定 ID 和一致时间基准,避免把不同请求的日志拼成一条虚假链路。高频路径宜聚合或采样,不能让诊断本身成为新的瓶颈。
现场不能随意暴露完整堆栈。栈、请求元数据和 profile 可能携带内部地址、路径与业务线索。受控采样与必要脱敏是工程操作要求,不妨碍本书本地实验。公开复现包应只保留教学负载,不包含真实用户数据。
练习:给三种等待分别命名
依次制造连接槽被占、worker 被可取消任务占用、客户端不读取响应。每次采集 goroutine 栈并写出一个判断:等待的是哪项资源,谁拥有它,解除条件是什么。然后解除故障,检查对应的恢复信号:连接实验要求下一次外呼完成,worker 实验可以检查 Pool 的 active,流实验则等待 handler 退出通知并核对 stream finished。/stats 不提供普通请求或流的在途计数,不能跨场景套用它。
最后选择一个等待,移除其取消路径,在独立子进程中验证总截止能够结束实验。不要把未退出 goroutine 遗留在整个测试进程。报告应同时包含故障状态与恢复状态,因为只有恢复才能证明你的解释触及了实际资源关系。
本章实测证据
以下摘自本次真实运行服务及管理端口采样记录。故障解除采用取消,终结计数包含失败与取消。
LIVE STATS {accepted:10 started:2 completed:0 failed:0 cancelled:0 active:2 queued:8}
shutdown complete {accepted:10 started:10 completed:10 failed:10 cancelled:10 active:0 queued:0}
process exit: 1, error: context deadline exceeded