系统学习 故障排查学习路径:从方法论到系统、网络、数据库与 K8s

context-deadline-exceeded

Go 程序在 context 截止时间到达时返回 context deadline exceeded 的定位与修复

现象

调用方在设定的截止时间后收到 context deadline exceeded,gRPC 可能同时映射为 DEADLINE_EXCEEDED,HTTP 客户端可能显示 context deadline exceeded (Client.Timeout exceeded while awaiting headers)。错误通常在客户端本地生成,服务端可能仍在执行,也可能根本没收到请求。

这个报错和 i/o timeout 不同:前者来自 Go context 的计时器,后者来自套接字操作自身超时。两者也可能同时出现,例如拨号阶段先触发 i/o timeout,上层再包装成 deadline 错误。定位时先确认超时发生在哪一层,再判断是谁的时间预算先用完。

快速定位

先看客户端与服务端的耗时分布,确认超时发生在建连、等待响应头、读取响应体还是下游调用:

GODEBUG=http2debug=2 ./<app> 2>&1 | grep -E 'deadline|timeout|CANCEL|GOAWAY'
curl -v --max-time 3 http://<host>:<port>/healthz
curl -s 'http://127.0.0.1:6060/debug/pprof/goroutine?debug=2' | head -100
ss -tin '( dport = :<port> or sport = :<port> )'
nstat -az TcpExtTCPTimeouts TCPRetransSegs
journalctl -u <service> --since '30 min ago'
kubectl logs <pod> -n <ns> --since=30m

在代码里同时记录 err 和 ctx.Err(),不要只打印外层错误:

ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()

var total int
if err := db.QueryRowContext(ctx, q, id).Scan(&total); err != nil {
    log.Printf("query failed: err=%v ctx_err=%v", err, ctx.Err())
    if errors.Is(err, context.DeadlineExceeded) {
        http.Error(w, "upstream timeout", http.StatusGatewayTimeout)
        return
    }
}

服务端指标要按 handler、下游依赖和分位数拆开看。客户端错误率升高但服务端处理耗时正常,问题可能在连接建立、排队、重试放大或客户端事件循环被阻塞。反过来,服务端处理时延的 p99 已经接近 deadline,说明预算本身设得太紧。

排查时先确认是整个请求超时,还是某个下游调用单独超时。记录每次调用的开始时间、截止时间和剩余预算,才能看出是预算分配不合理,还是某一段异常变慢。若同一请求在重试后仍然超时,重点检查总预算是否被多段串行调用和重试共同耗尽,而不是继续机械地缩短单次超时。

常见根因

  1. deadline 按平均延迟设置,没有覆盖下游 p99,也没有给重试和网络抖动留余量。
  2. 下游调用没有从入站 context 派生,而是新建 context.Background(),超时无法沿链路传播。
  3. 连接池等待、数据库锁等待、队列排队占用了大部分预算,真正执行只剩很少时间。
  4. 一次请求内串行调用多个下游,各段超时相加超过总 deadline。
  5. 重试次数没有上限,失败后反复重试,把剩余预算迅速耗尽。
  6. 建连、DNS、TLS 握手被计入同一个 deadline,跨地域或高丢包时先超时。
  7. HTTP 客户端的 Timeout 与单请求 context 叠加,实际生效的是更短的那个。
  8. 服务端 CPU 饱和、goroutine 调度延迟或事件循环阻塞,导致响应在本地已超时后才发出。
  9. 服务端忽略 context 取消继续处理,客户端已经放弃,后端负载反而被放大。
  10. gRPC 开启 WaitForReady 后,连接不可用时请求会一直等到 deadline,容易被误判为服务端慢。

修复

相关命令

参考链接

读完自测

合上页面,先回答,再展开对照。

  1. 客户端报 context deadline exceeded,能否直接断定服务端处理慢?
  2. 在 Go 中如何区分 deadline 超时和底层套接字的 i/o timeout?
  3. 为什么下游调用不能各自新建 context.Background() 并配一个固定超时?
答案
  1. 不能。该错误由客户端本地计时器产生,服务端可能仍在处理或根本没收到请求;要对比服务端 handler 耗时、排队时间和下游指标才能定位。
  2. deadline 超时用 errors.Is(err, context.DeadlineExceeded) 或 ctx.Err() 判断;套接字超时是 net.Error 且 Timeout() 为真,二者应同时记录并区分发生阶段。
  3. 新建 Background context 会切断入站 deadline 的传播,各段超时互不知情,剩余预算无法共享,父请求取消也无法传递,容易产生孤儿请求和重试放大。
微信小程序码:扫码打开命令速查小程序

小程序版

微信扫码,随时查命令