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,说明预算本身设得太紧。
排查时先确认是整个请求超时,还是某个下游调用单独超时。记录每次调用的开始时间、截止时间和剩余预算,才能看出是预算分配不合理,还是某一段异常变慢。若同一请求在重试后仍然超时,重点检查总预算是否被多段串行调用和重试共同耗尽,而不是继续机械地缩短单次超时。
context.Background(),超时无法沿链路传播。Timeout 与单请求 context 叠加,实际生效的是更短的那个。WaitForReady 后,连接不可用时请求会一直等到 deadline,容易被误判为服务端慢。ctx, cancel := context.WithTimeout(r.Context(), d),并立即 defer cancel()。errors.Is(err, context.DeadlineExceeded) 判断,同时记录 ctx.Err(),区分超时与主动取消。ctx.Err(),对已取消的请求尽快返回,避免继续消耗 CPU 和下游资源。DEADLINE_EXCEEDED 数量、重试率和下游延迟分别告警,避免只看总错误率。合上页面,先回答,再展开对照。
context deadline exceeded,能否直接断定服务端处理慢?context.Background() 并配一个固定超时?errors.Is(err, context.DeadlineExceeded) 或 ctx.Err() 判断;套接字超时是 net.Error 且 Timeout() 为真,二者应同时记录并区分发生阶段。
小程序版
微信扫码,随时查命令