必须显式加 PassingOnly: true,否则查到的可能是已下线但未清理的实例;Consul 默认返回所有注册实例(含 critical/warning),需主动过滤 passing 状态,否则易导致 503 错误。

必须显式加 PassingOnly: true,否则查到的可能是已下线但未清理的实例。
Consul 查询时默认不过滤健康状态
用 api.Health().Service 查服务,默认返回所有注册过的服务实例,包括 critical、warning 甚至已断连但未超时剔除的脏数据。这不是 bug,是 Consul 的设计:它把“注册”和“健康”拆成两个维度管理。
- 不传
PassingOnly: true→ 返回全部状态实例,len(results)可能 > 0,但实际调用会失败 - 漏掉这个参数是线上最常见的“服务发现成功但请求 503”的原因
- 即使你注册时配了 HTTP 健康检查,查询端也必须自己声明只取
passing状态
consulapi.ServiceEntry 结构体里真正可用的字段
查到的每个 ServiceEntry 包含 Service 和 Checks 两层嵌套。别直接用 entry.Service.Address —— 它可能为空,Consul 默认回退到节点地址(Node.Address)。
- 优先取
entry.Service.Address,非空就用;为空则 fallback 到entry.Node.Address -
entry.Service.Port是注册时填的端口,不是健康检查端口 -
entry.Checks是切片,需遍历找Status == "passing"的项,但一般只关心整体是否 passing,不用细看单个 check
查询超时与重试要独立控制
Consul 客户端的全局 config.Timeout 控制连接+请求总耗时,但服务发现本身应容忍短暂抖动。生产环境建议:
- 单独设查询上下文:
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - 不要复用 gin 的
c.Request.Context(),避免因 HTTP 请求提前结束导致服务列表没刷出来 - 如果查不到 passing 实例,别立刻 panic 或 return error;可降级走本地缓存或预设兜底地址(如
localhost:8081)
Gin 中做服务发现的典型位置
不能在每个 HTTP handler 里实时查 Consul —— 那会成为性能瓶颈和单点故障源。正确做法是启动时初始化 + 定期刷新。
- 在
main()启动后,起一个 goroutine 调用watchService(),监听变更并更新内存 map - handler 内直接读本地缓存:
svc := serviceCache["user-srv"],而不是每次调client.Health().Service() - 注意
consulapi.Watcher在连接断开后会自动重连并补发事件,但首次 watch 可能错过中间变更,需配合一次全量拉取
最容易被忽略的是:Consul DNS 接口(如 user-srv.service.consul)在容器里默认不可用,net.LookupHost 会失败——除非你部署了 consul-template 或配置了 dnsmasq。别指望 DNS 替代 API 查询。











