Consul服务发现需Go代码显式注册、健康检查和查询逻辑协同驱动;漏Check字段、用localhost地址、查服务不加PassingOnly:true即失效,客户端初始化必须设HTTP超时与Transport配置。
直接说结论:Consul 不是“配置”出来的,而是靠 Go 代码显式注册 + 健康检查 + 查询逻辑共同驱动的;漏掉 Check 字段、用 localhost 当地址、查服务不加 PassingOnly: true,服务发现就等于没做。
默认的 consul.NewClient 没有 HTTP 超时,网络抖动或 Consul 不可用时,service.Register 或 api.Health.ServiceNodes 会卡死数秒甚至更久——这不是延迟,是阻塞。
http.Transport,设 Timeout(推荐 3 * time.Second)KeepAlive(如 30 * time.Second),避免 Consul 重启后长连接僵死consul.DefaultConfig(),它的 HttpClient 是 nil,会 fallback 到全局 http.DefaultClient,没超时也没重试示例:
cfg := &consul.Config{ Address: "127.0.0.1:8500", HttpTransport: &http.Transport{ Timeout: 3 * time.Second, KeepAlive: 30 * time.Second, MaxIdleConns: 10, MaxIdleConnsPerHost: 10, }, WaitTime: 5 * time.Second,}client, _ := consul.NewClient(cfg)
Address 写 localhost 是最常见错误——容器内、跨主机部署时,其他服务根本连不上这个地址;Check 字段为空,服务注册成功但状态永远是 critical,查询时默认被过滤掉。
立即学习“go语言免费学习笔记(深入)”;
Address 必须填容器网络可路由的真实 IP(如 net.InterfaceAddrs() 扫出来的 IPv4)或 DNS 名,不是 localhost 也不是 0.0.0.0
Check.HTTP 要用完整 URL(如 "http://10.244.1.5:8080/health"),且后端必须返回 2xx 状态码;重定向(302)或超时都会导致状态变 failing
Check.Timeout 必须显式设(如 "3s"),否则默认 10s,若你的 /health 响应慢,Consul 就判你挂了serviceID 必须全局唯一,重启服务时若 ID 不变,旧实例注销失败,会产生“幽灵节点”api.Health().Service() 默认返回所有状态节点,包括 critical、warning、已下线但未清理的残留条目——直接取第一个或随机选,大概率调到不可用实例。
&api.QueryOptions{AllowStale: false, PassingOnly: true}
AllowStale: false 避免读到过期缓存(Consul 默认允许 stale read 提升吞吐)ServiceNodes 结果做长期决策——网络抖动可能让这次查询失败或超时,应结合本地缓存 + 定期刷新kv.Watch 是 HTTP 轮询封装,不是 WebSocket;裸写 for 循环调用等于高频刷接口,还容易漏事件、goroutine 泄漏。
watch.NewWatcher 替代裸 kv.Watch,它内置指数退避、重连和事件去重Watcher 的 ctx 必须可控(比如随服务生命周期 cancel),否则 goroutine 泄漏kv.Get("missing-key", nil) 的 error 是 nil,真正该判空的是返回的 *api.KVPair;pair == nil 可能是 key 不存在、ACL 拒绝、路径是目录,三者表现一致最常被忽略的点:健康检查不是“配完就完事”,HTTP 检查路径必须真实响应 200,且不能重定向;服务发现不是“查一次就行”,必须持续 Watch 或定期刷新节点列表,并配合本地缓存降级。这些细节不处理,服务发现就只是个摆设。