长连接健康检查不能只靠tcp keepalive,因其仅探测链路层连通性,无法发现进程崩溃、服务无响应或代理丢包;必须采用应用层心跳(如/healthz或grpc healthcheck),配合短超时、连续失败判定与连接池清理机制。

长连接健康检查为什么不能只靠 TCP Keepalive
TCP Keepalive 只能探测链路层是否断开,对后端进程崩溃、服务未响应 HTTP/GRPC 请求、中间代理静默丢包等情况完全无感。Golang 的 http.Transport 默认不启用 Keepalive,即使开启,超时时间(默认 15s)和重试逻辑也远不足以支撑业务级可用性判断。
真实场景中,你看到 net/http: request canceled (Client.Timeout exceeded while awaiting headers),往往不是网络问题,而是后端已卡死但 TCP 连接仍“活着”。这时候需要应用层主动发心跳请求并校验响应语义。
- 必须用业务可识别的 endpoint(如
/healthz或 GRPCHealthCheck方法),不能只依赖 TCP 层 - 心跳间隔要短于后端就绪检测窗口(例如后端 k8s readiness probe 是 5s,则客户端哨兵应 ≤3s 发一次)
- 失败判定需连续失败 N 次(推荐 2–3 次),避免偶发抖动误杀连接
用 http.Client + goroutine 实现轻量哨兵(适用于 REST 后端)
不要在每个请求前手动调 GET /healthz —— 那会放大延迟、破坏并发模型。正确做法是启动独立 goroutine,定期轮询,并在连接失效时触发 http.Transport.CloseIdleConnections() 清理旧连接池。
关键点在于:哨兵不管理连接本身,只负责“标记”连接池是否可信。真正的连接重建由 http.Client 在下次请求时自动完成(前提是 Transport 配置了合理的 MaxIdleConns 和 IdleConnTimeout)。
- 用
time.Ticker控制心跳节奏,避免time.Sleep导致 drift 累积 - 健康检查请求必须设短超时(
context.WithTimeout(ctx, 1*time.Second)),且禁用重定向(CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }) - 失败时仅记录日志 + 调用
transport.CloseIdleConnections(),不要尝试手动关闭单个连接(http.Transport内部连接复用逻辑复杂,手动干预易出错)
go func() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for range ticker.C {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
_, err := http.DefaultClient.GetContext(ctx, "https://backend/healthz")
cancel()
if err != nil {
log.Printf("health check failed: %v", err)
http.DefaultTransport.(*http.Transport).CloseIdleConnections()
}
}
}()
gRPC 场景下直接复用 grpc-go 的 health check client
别自己封装 HTTP 健康探针去打 gRPC 服务的 /healthz —— 大多数 gRPC 服务(尤其用 grpc-health-probe 或 google.golang.org/grpc/health)暴露的是标准 grpc.health.v1.Health service。直接用官方 client 更可靠。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
注意:healthcheck client 本身也依赖底层连接,所以它的 dial 必须配置 WithBlock() + 合理的 WithTimeout(),否则第一次连不上会阻塞整个哨兵 goroutine。
- 初始化时用
grpc.DialContext(ctx, addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock(), grpc.WithTimeout(3*time.Second)) - 每次检查用
client.Check(ctx, &healthpb.HealthCheckRequest{Service: ""}),检查response.Status == healthpb.HealthCheckResponse_SERVING - 如果
Check()返回status.Code() == codes.Unavailable,说明后端不可达,立刻调conn.Close()并重建 dial(不要复用旧 conn)
连接重建后如何避免请求雪崩
哨兵发现故障并清空连接池后,下一秒所有并发请求都会触发新连接建立 —— 如果后端正在重启,这会瞬间压垮它。必须加一层轻量级熔断或退避。
最简单有效的方式是在 http.Transport 层加 RoundTrip 拦截,对连续失败的 host 记录一个“冷却时间”,在此期间直接返回错误,跳过真实请求。
- 用
sync.Map存 host →time.Time(冷却截止时间),key 是req.URL.Host - 在自定义
RoundTripper的RoundTrip()开头检查:若当前时间 &url.Error{Op: "roundtrip", URL: req.URL.String(), Err: errors.New("backend cooling down")} - 冷却时长建议从 100ms 起步,每次失败 ×2(最大 2s),成功则清除该 host 记录
这个逻辑比全量引入 gobreaker 更轻、更贴近连接生命周期,也避免了在业务 handler 里做重试决策带来的耦合。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










