心跳检查必须用context.WithTimeout而非time.After,因后者会泄漏goroutine;需包裹HTTP/TCP全链路操作,禁用重定向与连接复用,TCP直连需设WriteDeadline并立即关闭,失败判定与恢复策略应分离。

心跳检查函数必须用 context.WithTimeout 而不是 time.After
直接用 time.After 做超时控制会导致 goroutine 泄漏——每次调用都会 spawn 一个永不结束的 timer goroutine,服务跑几天就 OOM。正确做法是把超时绑定到请求生命周期里。
- 用
context.WithTimeout(ctx, 3*time.Second)包裹整个 HTTP 请求或 TCP 探测 - 所有阻塞操作(
http.Do、net.Dial、conn.Write)都传入该 context - 不要在函数内新建独立 timer 或 select + time.After
示例片段:
func checkHeartbeat(ctx context.Context, endpoint string) error {
req, _ := http.NewRequestWithContext(ctx, "HEAD", endpoint+"/health", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("health check failed: %w", err)
}
resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("unexpected status %d", resp.StatusCode)
}
return nil
}HTTP 心跳必须禁用重定向和长连接复用
默认 http.Client 会自动跟随 3xx 重定向,并复用底层连接。这两者在心跳场景下都是隐患:重定向可能绕过真实服务端点;连接复用会让失败连接滞留,掩盖真实故障。
- 设置
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse } - 设置
Transport: &http.Transport{DisableKeepAlives: true} - 避免使用全局共享的
http.DefaultClient,每个心跳检查应持有独立 client 实例或至少隔离 transport
TCP 层直连比 HTTP 更低延迟,但需手动处理 EOF 和 timeout
当服务间走内网直连且无反向代理时,用 TCP 握手代替 HTTP 更快(省去 header 解析、TLS 握手等)。但 net.Dial 默认不设 deadline,一次卡住就拖垮整个健康检查周期。
立即学习“go语言免费学习笔记(深入)”;
- 必须对 conn 显式调用
conn.SetDeadline或conn.SetReadDeadline,不能只靠 context - 写完探测数据后立即
conn.Close(),不要等读响应——很多服务心跳端点不返回任何 body - 常见错误是只 dial 不 write,导致服务端收不到探测,误判为宕机
典型 TCP 心跳写法:
conn, err := net.DialTimeout("tcp", addr, 500*time.Millisecond)
if err != nil {
return err
}
defer conn.Close()
conn.SetWriteDeadline(time.Now().Add(200 * time.Millisecond))
_, _ = conn.Write([]byte("PING\n"))
// 不读响应,直接 close高内聚的关键是把“失败判定逻辑”和“恢复策略”分离
心跳函数本身只负责“这一次是否通”,不决定重试几次、是否触发告警、要不要切流量。否则函数职责膨胀,测试难、复用差、升级时容易牵一发而动全身。
- 返回明确错误类型:
ErrTimeout、ErrConnectionRefused、ErrUnexpectedStatus,而非笼统的error - 让调用方决定:连续 3 次
ErrTimeout才标记为不可用;连续 5 次ErrConnectionRefused才触发熔断 - 避免在心跳函数里打日志、发 metrics、调 webhook——这些属于编排层职责
真正容易被忽略的是:不同环境的心跳阈值必须可配置。开发环境 1s 超时合理,生产 K8s Pod 网络抖动频繁,得设成 2.5s;而 Service Mesh 侧车(如 Istio)注入后,TCP 探测延迟可能翻倍,硬编码会批量误杀实例。


















