直接用DNS或配置文件做服务发现行不通,因微服务IP端口频繁变动会导致调用方连接失败;Consul等注册中心是解决“谁在哪儿”问题的必要组件。
微服务数量一多,IP 和端口频繁变动,硬编码 host:port 或靠 DNS 解析静态记录,会导致调用方始终连不到新实例,甚至持续重试已下线节点。Consul、Nacos 这类注册中心不是“锦上添花”,而是解决“谁在哪儿”这个基础问题的必要组件。
用 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 的 DNS 接口(如 order-service.service.consul)做客户端负载均衡——它返回的是随机 A 记录,无法感知实例健康变化延迟,且 Go 的 net/http 默认不刷新 DNS 缓存。
Health().Service() 主动查询,过滤出 Passing 状态的节点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 秒内没响应就强制退出)
preStop hook 提前发信号,避免 Pod 被强制 kill真正难的不是写注册逻辑,而是让整个生命周期——启动注册、运行中保活、关闭前注销——在各种异常(OOM kill、节点宕机、网络中断)下都保持行为可预测。这点很容易被忽略,直到线上出现偶发性超时才去翻日志。