必须用带超时的 http.client 实例,如 &http.client{timeout: 3 * time.second},禁用重定向并避免复用 http.defaultclient;健康判断需分层验证 http 状态码、业务依赖(db/redis)及连续失败阈值,确保真实可用而非假活。

健康检查该用哪个 HTTP 客户端?别直接用 http.Get
Go 自带的 http.Get 默认没有超时控制,一旦目标服务卡住或网络不通,调用会阻塞几秒甚至更久,导致健康检查线程堆积、误判。必须用带超时的 http.Client 实例。
- 用
&http.Client{Timeout: 3 * time.Second}显式设置超时,3 秒足够探测多数内网服务 - 禁用重定向:
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },避免健康检查被 302 重定向干扰 - 不要复用全局
http.DefaultClient,它默认无超时且共享 Transport,容易被其他模块污染配置
如何判断“不健康”才算可靠?别只看 HTTP 状态码
返回 200 OK 不代表服务真可用——可能只是健康检查接口硬编码返回 200,但后端 DB 已断开、缓存雪崩、goroutine 泄漏。需要分层验证。
- HTTP 层:检查状态码是否为
200,且响应体 JSON 中status字段为"up"(如果服务支持) - 业务层:在健康检查响应里加入轻量 probe,比如
{"db": "ok", "redis": "ok"},客户端解析并校验字段值 - 时间层:连续失败 3 次才标记为不健康,避免瞬时抖动误剔;恢复需连续成功 2 次才重新上线
自动剔除节点时,为什么 etcd 的 lease 比 Redis 更稳?
用 Redis 的 EXPIRE 做服务注册过期,容易因网络分区或客户端崩溃导致“幽灵节点”残留;etcd 的 lease 机制能由服务端主动回收,可靠性更高。
- 注册时绑定 lease:
client.Put(ctx, key, value, clientv3.WithLease(leaseID)) - 定期续租:
client.KeepAlive(ctx, leaseID)返回的 channel 持续监听 keepalive 响应,断连自动关闭 - 剔除逻辑不依赖客户端上报,而是监听 etcd 的
Watch事件:/services/xxx节点消失即触发下线 - 注意:etcd lease TTL 最小建议设为 10 秒,太短会导致频繁 renew 增加压力,太长则故障发现延迟高
多实例并发健康检查怎么避免互相干扰?
每个 worker goroutine 都独立执行检查,但共享同一个服务列表和剔除逻辑,容易出现“竞态剔除”——A 实例刚标记某节点不健康,B 实例还没来得及刷新缓存又把它加回来。
- 用
sync.Map存储节点状态,key 是服务地址,value 是最后成功时间戳,读写原子 - 剔除操作必须加锁或走单点协调器(如通过 etcd 的
CompareAndSwap),不能直接删本地 map 后广播 - 推荐方案:所有 worker 只写状态到 etcd 的临时 key(如
/health/{ip}:{port}),由单独的 reconciler goroutine 统一扫描、聚合、更新服务发现列表
分布式健康检查真正难的不是发请求,而是状态一致性和边界条件——比如网络分区时 lease 是否真的过期、etcd watch 是否丢事件、服务重启瞬间的窗口期。这些地方没做幂等和补偿,自动剔除就会变成自动误杀。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











