健康检查函数必须支持超时控制和可取消上下文:所有探测操作须接收context.context,每次探测前新建独立子上下文(如ctx, cancel := context.withtimeout(parentctx, 800ms)),避免共用已取消上下文;http.client.timeout建议设为0,统一由上下文控制超时;主备切换需基于滑动窗口计数器(如5次中至少4次成功)判定状态,配合draining机制优雅终止旧连接,地址应动态获取而非硬编码。

健康检查函数必须支持超时控制和可取消上下文
直接用 http.Get 或 net.Dial 做探测,一旦目标服务卡住或网络不通,协程就永久阻塞。Go 的健康检测模块第一道防线是上下文超时——所有探测操作必须接收 context.Context,且超时时间要远小于主调逻辑的容忍阈值(比如主服务心跳周期为 5s,健康检查超时应设为 ≤1s)。
常见错误是把 context.WithTimeout 放在循环外,导致多次探测共用同一个已取消的上下文;正确做法是每次探测前新建带独立超时的子上下文:
ctx, cancel := context.WithTimeout(parentCtx, 800*time.Millisecond) defer cancel() resp, err := http.DefaultClient.Do(req.WithContext(ctx))
另外,不要忽略 http.Client 自身的 Timeout 字段——它和上下文超时是叠加生效的,建议统一用上下文控制,http.Client.Timeout 设为 0 避免冲突。
主备切换逻辑不能只靠单次探测结果
一次 HTTP 状态码 200 不代表服务真正可用,一次 503 也不代表彻底宕机。真实场景中网络抖动、瞬时过载很常见,直接“一票否决”会导致频繁误切。
推荐用滑动窗口计数器实现状态判定:
- 维护一个长度为
N(如 5)的布尔切片,记录最近 N 次探测是否成功 - 每次探测后更新最老位置,用
sum(successes) >= threshold判定节点健康(例如 5 次里至少 4 次成功才认为健康) - 主节点连续失败达到阈值才触发切换,备节点需连续成功达到阈值才被提升为主
注意:计数器状态必须线程安全,用 sync.RWMutex 保护读写,避免多个 goroutine 同时更新导致计数错乱。
切换过程要避免脑裂和请求中断
主备切换不是简单改个指针变量。如果正在处理的请求还在用旧主节点连接,立刻切换会导致部分请求失败或数据不一致。
关键动作有三个:
- 先标记旧主为“下线中”,拒绝新请求路由过去(可通过原子布尔变量
atomic.CompareAndSwapUint32(&state, StateUp, StateDraining)) - 等待正在使用的连接自然完成或超时关闭(设置合理的
http.Transport.IdleConnTimeout和http.Transport.MaxIdleConnsPerHost) - 确认无活跃连接后,再将流量完全切到新主,并重置旧主的状态计数器以便后续恢复
漏掉“ draining ”阶段是生产环境最常踩的坑——看似切换快,实则引发大量 5xx 错误。
探测目标地址必须支持动态更新
硬编码 "http://10.0.1.10:8080/health" 违背主备设计初衷。实际部署中 IP 可能漂移、DNS 记录会变更、K8s Service Endpoint 动态调整。
模块初始化时应接受一个地址提供者接口:
type EndpointProvider func() ([]string, error)
典型实现包括:
- 从 Consul/Etcd 拉取服务列表
- 解析 DNS SRV 记录(如
_health._tcp.service.example.com) - 读取配置中心 JSON,配合 Watch 机制监听变更
每次探测前调用该函数获取最新地址列表,再按优先级(如标签 role=primary)排序。别忘了加缓存和刷新间隔,避免高频轮询拖垮注册中心。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











