生产环境应选 etcd 作为注册中心,因其轻量、Go 原生支持 client/v3、无 C 依赖;须配租约与心跳续期,注册键格式为 /services/{name}/{host}:{port},客户端宜轮询+缓存而非依赖 Watch。

注册中心选型直接影响服务发现可靠性
用 etcd 比 consul 更轻,Go 原生支持 clientv3,没有额外 C 依赖;zookeeper 在 Go 生态里维护成本高,不推荐。如果只是本地测试或单机部署,registry memory(内存注册)够用,但上线必须切到 etcd——否则服务重启后注册信息全丢。
常见错误:直接用 github.com/coreos/etcd/client(v2 API),它已废弃,新项目必须用 go.etcd.io/etcd/client/v3,否则连不上 etcd v3.4+。
注册逻辑必须带租约(lease)和心跳续期
没租约的服务注册等于“僵尸注册”:进程挂了但 etcd 里还留着,下游调用会失败。正确做法是创建一个 lease,把服务注册键绑定到该 lease,并启动 goroutine 定期调用 KeepAlive。
-
lease.TTL设为 10–30 秒,太短增加 etcd 压力,太长故障发现延迟高 - 注册键建议格式:
/services/{service_name}/{host}:{port},避免用 IP 自动发现时冲突 - 务必在
defer里调用lease.Revoke,否则进程退出不释放 lease,etcd 租约堆积
服务注销要区分正常退出和崩溃场景
正常退出可主动调用 lease.Revoke 清理;但进程 panic 或 kill -9 时无法执行 defer,只能靠 lease 过期自动清理——所以 TTL 不能设成几分钟级别。
立即学习“go语言免费学习笔记(深入)”;
折中方案是加一层健康检查:注册时写入一个带时间戳的 health key,再起一个独立 goroutine 每 5 秒更新一次;消费者拉取服务列表时,过滤掉 health 时间超过 2 倍 TTL 的节点。
示例片段:
// 注册 health key
cli.Put(ctx, "/services/api/10.0.0.1:8080/health", strconv.FormatInt(time.Now().Unix(), 10))
// 单独 goroutine 刷健康时间
go func() {
for range time.Tick(5 * time.Second) {
cli.Put(ctx, "/services/api/10.0.0.1:8080/health", strconv.FormatInt(time.Now().Unix(), 10))
}
}()客户端发现需缓存 + 轮询,别依赖长连接
etcd 的 Watch 接口理论上能实时推送变更,但微服务规模稍大就容易断连、重复事件、序列丢失——生产环境更稳的方式是:每 3–5 秒轮询一次 Get,结果做内存缓存,配合版本号比对判断是否更新。
关键点:
- 用
WithPrefix一次性拉取所有实例,别逐个查 - 缓存结构用
map[string]*ServiceInstance,key 是{host}:{port},避免重复解析 - 轮询间隔别低于 2 秒,否则 etcd QPS 压力陡增;也别高于 10 秒,否则故障感知太慢
复杂点在于多实例负载均衡策略要和服务发现解耦——注册模块只管提供“活着的地址列表”,路由逻辑交给上层框架或 SDK 处理。


















