直接用DNS或配置文件做服务发现行不通,因微服务IP端口频繁变动会导致调用方连接失败;Consul等注册中心是解决“谁在哪儿”问题的必要组件。
为什么直接用 DNS 或配置文件做服务发现行不通
微服务数量一多,IP 和端口频繁变动,硬编码 host:port 或靠 DNS 解析静态记录,会导致调用方始终连不到新实例,甚至持续重试已下线节点。Consul、Nacos 这类注册中心不是“锦上添花”,而是解决“谁在哪儿”这个基础问题的必要组件。
Go 服务向 Consul 注册的最小可行代码
用 hashicorp/consul/api 客户端注册,关键不是连上 Consul,而是确保健康检查能真实反映服务状态——否则注册了也等于没注册。
- 服务注册必须带
Check字段,推荐用 HTTP 健康接口(如/health),避免用 TCP 检查:后者只看端口通不通,不校验服务是否真能处理请求 -
TTL模式需要服务主动续租,容易因 GC、goroutine 泄漏或网络抖动导致误注销;HTTP 检查更稳 - 注册时
ID必须全局唯一,建议拼接主机名+端口+启动时间戳,避免多实例注册覆盖
config := api.DefaultConfig()
config.Address = "127.0.0.1:8500"
client, _ := api.NewClient(config)
registration := &api.AgentServiceRegistration{
ID: 
fmt.Sprintf("order-service-%s-%d-%d", hostname, port, time.Now().Unix()),
Name: "order-service",
Address: hostname,
Port: port,
Check: &api.AgentServiceCheck{
HTTP: fmt.Sprintf("http://%s:%d/health", hostname, port),
Timeout: "2s",
Interval: "5s",
},
}
client.Agent().ServiceRegister(registration)
客户端如何从 Consul 拉取可用实例并负载均衡
别依赖 Consul 的 DNS 接口(如 order-service.service.consul)做客户端负载均衡——它返回的是随机 A 记录,无法感知实例健康变化延迟,且 Go 的 net/http 默认不刷新 DNS 缓存。
- 应使用 Consul API 的
Health().Service()主动查询,过滤出Passing状态的节点 - 每次请求前都查一次?太重。合理做法是:后台 goroutine 每 3–5 秒轮询,缓存结果到内存 map,并加读写锁保护
- 负载均衡策略自己实现最可控:轮询、随机、加权最少连接——不要依赖第三方库自动选,否则出问题时连日志都难定位
services, _, _ := client.Health().Service("order-service", "", false, nil)
var endpoints []string
for _, s := range services {
if s.Checks.AggregatedStatus() == api.HealthPassing {
endpoints = append(endpoints, fmt.Sprintf("%s:%d", s.Service.Address, s.Service.Port))
}
}
// endpoints 就是当前可用的 order-service 实例列表
服务注销时机不对会导致“幽灵实例”残留
进程收到 SIGTERM 后立刻退出,没来得及调用 ServiceDeregister,Consul 会等 TTL 超时(默认 30 秒)才清理,这期间所有请求仍会被路由过去,造成 502/timeout。
- 必须在
os.Signal监听中阻塞执行注销,且加超时控制(比如 3 秒内没响应就强制退出) - 注销失败不能静默吞掉:要打 error 日志,方便排查网络分区或 Consul 不可用问题
- Kubernetes 环境下,还要配合
preStophook 提前发信号,避免 Pod 被强制 kill
真正难的不是写注册逻辑,而是让整个生命周期——启动注册、运行中保活、关闭前注销——在各种异常(OOM kill、节点宕机、网络中断)下都保持行为可预测。这点很容易被忽略,直到线上出现偶发性超时才去翻日志。

