第16章 从现象到根因的故障诊断流程
先保护证据和容量
线上出现超时,第一反应常是增加线程、连接或超时。这样有时暂时改善,也可能把下游压垮并销毁原本清晰的边界。更稳妥的起点是确认影响范围、接纳与完成速率、拒绝比例和资源趋势。必要时降低接纳,给系统恢复空间,同时保留能够解释等待的证据。
本书可以在本地构造四类症状:客户端超时但工作仍在、队列饱和但 CPU 不高、上游看似正常却拿不到连接、部署停止迟迟不退出。每一类先写观察,再提出原因,最后用一个最小干预验证。不要把熟悉的故障名称直接当成根因,诊断需要能被证伪。
观察窗口要覆盖故障形成与恢复。只有峰值截图,可能把正常突发当成泄漏;只有恢复后截图,又看不到触发关系。保留配置、版本、请求参数与时间线,能够让后续实验重建相同边界。涉及业务数据时保留摘要,不必为了复现传播完整敏感内容。
场景一:客户端走了,工作还在
对 /work,先比较客户端返回时间与 request finished 日志出现时间。该日志表示可取消等待已经结束;要确认整个 handler 返回,可在测试中等待 ServeHTTP 返回后发出的 finished 通知,或使用 DrainRequests。若客户端超时后等待仍未结束,追踪 context 从处理函数到实际阻塞点是否连续。常见断点包括 Background、无条件 Sleep、无取消的 channel 发送,以及没有读取截止的外部库。修复应发生在真正阻塞处,不能只在外层 select 增加超时。
/stats 中的 active 只反映工作池任务,用它观察 /work 会一直得到与该请求无关的数值。只有诊断已接纳的后台任务时,才把 active 归零作为工作池释放证据。首先确认指标属于哪个执行者,是排查的前提。
验证时重复同一故障,并要求工作退出、计时器释放、后续请求恢复。客户端错误文本可能完全不变,这是预期的:原先客户端就已及时返回,修复的是服务内部资源。若验收只看 HTTP 状态,就看不到这类修复的价值。
如果工作包含外部副作用,还要单独核查是否已经提交。取消后的资源释放不能推导出业务未发生。结果未知时应记录核查入口,避免把一次资源故障升级为重复副作用。诊断报告要同时解释执行状态与业务状态。
场景二:连接池等待不断增长
确认请求究竟停在 DNS、建连、获取连接、等响应头还是读 Body。若上游没有收到新请求,且连接预算已被占满,检查先前响应体是否关闭及是否读到 EOF。将每主机连接限制调得很大可能暂时缓解,但会让泄漏更晚、更昂贵地暴露。
用随书连接测试验证:保留一个未读 Body,下一请求受 context 限制失败;关闭前一 Body 后恢复。若恢复成立,说明问题与连接持有直接相关。接着检查所有状态码分支和超限分支,确保错误响应也释放资源。不要只修复成功路径。
若响应体本身持续慢读,应设置总预算与大小上限,必要时放弃连接复用。与其无限 drain 试图“清理干净”,不如在有限成本内关闭。清理路径同样需要预算,这是许多连接问题被反复引入的原因。
场景三:队列满,重试让情况更坏
区分服务过载拒绝与执行错误。队列满时快速拒绝是在保护已接纳工作,如果客户端立即重试,接纳路径也可能被打满。检查各层重试是否叠加,是否有抖动与总预算,是否把拒绝从监控分母中移除。漂亮的成功请求平均延迟可能只是隐藏了大量拒绝。
恢复策略可以降低入口速率、暂停非关键任务或缩小某类并发,而不是无限增加队列。容量调整应关联最稀缺资源:数据库连接、远端配额、CPU 或内存。排队只是时间上的缓冲,不会创造处理能力。故障解除后还需控制积压回放,防止恢复洪峰。
验收使用占满、拒绝、释放、恢复四阶段。只证明拒绝及时,不证明释放后能恢复,仍可能遗漏关闭标志或计数损坏。重复测试应覆盖相邻多轮饱和,而不是每次重建进程让状态自然清空。
场景四:SIGTERM 后进程不退出
先确认信号是否到达,再看当前停在 HTTP Shutdown、工作池等待还是其他资源清理。使用已经取消的 context、无限等待不合作任务、持锁等待 worker、未管理的后台 goroutine 都可能造成问题。不要在没有诊断时直接把宽限期从十秒调成十分钟。
随书子进程测试给出了两个基线:预算内全部排空时正常退出;预算不足时取消工作、等待实际清理并以失败状态退出。若你增加消息消费者、数据库或 WebSocket,要把它们放进同一责任表,并增加真实关闭验收。函数返回不一定意味着进程可以结束。
练习:写一页可以复现的事故记录
选一个场景,按触发条件、用户影响、观察证据、被排除原因、修复和回归验收写记录。每个因果判断至少对应一个可重复实验;不确定之处直接标为待验证。把报告交给没有参与排查的人,要求他仅凭记录复现并确认恢复。能做到这一点,说明你已经把个人经验转化成可协作的工程知识。
本章实测证据
这一故障用恢复验证原因:取消阻塞读取后下一次请求可完成。它不能代表真实上游的所有协议和容量。 以下摘自最终本机 go test -race -v ./...,完整命令及环境见附录B;括号中的耗时仅是当次运行记录。
--- PASS: TestFetcherBlockedBodyCancellationReleasesConnection (0.05s)