← 中文资源

简体中文版本

Go HTTP 客户端超时 — context deadline exceeded 解析

分别测量 DNS、连接、TLS 与首字节延迟,核对 Client、Transport 与 context 三层超时预算,解决 Go HTTP 请求超时。

阅读英文原文

现象

请求报 context deadline exceeded (Client.Timeout exceeded while awaiting headers);同一接口用 curl 正常,从 Go 服务调用却超时;流量高峰或 DNS 变更后集中出现;延迟升高的同时 goroutine 与连接数不断累积。

诊断

先在 Go 之外分段测量:

curl -sS -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} first_byte=%{time_starttransfer} total=%{time_total}\n' https://api.example.com/health
GODEBUG=http2debug=1 go test ./internal/client -run TestFetch -v -count=1
go tool pprof -top http://localhost:6060/debug/pprof/goroutine

首字节时间接近 Go 超时值,说明上游确实慢;连接时间长则查网络或 DNS。HTTP/2 调试日志能看出连接是否复用、是否收到 GOAWAY。goroutine profile 中大量阻塞在读取响应的协程,通常意味着 body 未关闭,连接无法复用。

修复

复用单个 http.Client,同时设置客户端总超时和 Transport 的分段超时:

client := &http.Client{
    Timeout: 10 * time.Second,
    Transport: &http.Transport{
        DialContext:           (&net.Dialer{Timeout: 2 * time.Second}).DialContext,
        TLSHandshakeTimeout:   2 * time.Second,
        ResponseHeaderTimeout: 5 * time.Second,
        IdleConnTimeout:       90 * time.Second,
        MaxIdleConnsPerHost:   32,
    },
}

每次请求都 defer resp.Body.Close() 并读完 body,否则连接无法复用;调用方预算更紧时传入带 deadline 的 context。不要每次请求新建 client,那会失去 keep-alive 并放大延迟。

验证

重新压测,p99 应低于客户端超时且不再出现 deadline 错误;观察空闲连接被复用,压测结束后 goroutine 数回落到基线。再注入慢上游,确认超时在预算内触发、body 被关闭、没有连接泄漏;重试仅限幂等请求,避免重复写入。