如何在Golang中实现服务发现_服务注册中心使用方式

服务注册必须调用Register并持续心跳续期,否则注册信息会因TTL过期而消失;应使用Txn事务保证注册原子性,服务发现需通过Watch监听而非轮询,并与gRPC等框架生命周期对齐。

服务注册必须调用 Register 且保持心跳续期

Go 服务要被其他节点发现,不能只“启动就完事”。以 etcdconsul 为例,注册本质是往 KV 存储写一条带 TTL 的键值对(如 /services/myapp/10.0.1.2:8080),但该键会自动过期。因此必须持续发送心跳(TTL 续期),否则注册信息几秒后就消失。

常见错误是:调用一次 Register 后就不管了,结果服务在注册中心里“闪现”几秒就下线。

  • 使用 clientv3.NewLease 创建租约,Grant 获取 lease ID,再用该 ID 注册服务键
  • KeepAlive 流持续刷新租约——这是必须的 goroutine,不能漏
  • 注册键建议包含 IP、端口、元数据(如 version=1.2.0),方便后续路由或灰度
  • 服务退出前

    务必调用 Revoke 主动清理,避免僵尸节点

etcd 客户端需区分 PutTxn 注册逻辑

直接 Put 键值无法保证“先检查未注册再写入”,并发注册时可能覆盖或重复。生产环境应使用事务(Txn)做原子性校验。

resp, err := cli.Txn(context.TODO()).If(
    clientv3.Compare(clientv3.Version("/services/myapp/"+addr), "=", 0),
).Then(
    clientv3.OpPut("/services/myapp/"+addr, payload, clientv3.WithLease(leaseID)),
).Commit()

这段代码确保:只有该地址此前没注册过,才允许写入。否则 resp.Succeeded 为 false,可返回冲突错误给调用方。

  • Version 比较比 Value 比较更轻量,适合判断是否存在
  • 不要用 Get + Put 两步操作,中间存在竞态窗口
  • 注册失败时别盲目重试,先查是否已有同名实例在运行

服务发现应监听 Watch 而非轮询 Get

客户端如果每隔几秒 Get 一遍 /services/myapp/ 下所有服务,不仅增加 etcd 压力,还会错过瞬时上下线事件。正确做法是用 Watch 监听前缀变更。

watchCh := cli.Watch(context.TODO(), "/services/myapp/", clientv3.WithPrefix())
for wresp := range watchCh {
    for _, ev := range wresp.Events {
        switch ev.Type {
        case mvccpb.PUT:
            // 新实例上线,解析 ev.Kv.Key 和 ev.Kv.Value
        case mvccpb.DELETE:
            // 实例下线,从本地缓存移除对应 addr
        }
    }
}

Watch 是长连接,服务端推送变更,延迟通常在百毫秒级。但要注意:

  • Watch 可能断连,需在 for 循环外捕获 channel 关闭并重连
  • 首次 Watch 后应补一次 Get 全量,避免错过连接建立前的变更
  • 不要在 Watch 回调里做耗时操作(如 HTTP 请求),应投递到 worker 队列

gRPC 服务集成时注意 resolver.Builder 的生命周期

如果你用 gRPC,想让 ClientConn 自动感知后端实例变化,得实现 resolver.Builder 并注册。关键点在于:Build 返回的 Resolver 必须自己维护 Watch,并把地址更新通过 cc.UpdateState 推给 gRPC 内部负载均衡器。

容易忽略的是:当 Resolver 被 gRPC 销毁(如 ClientConn.Close)时,你自己的 Watch goroutine 必须退出,否则造成 goroutine 泄漏。

  • Close 方法里关闭 Watch channel、取消 context、等待 goroutine 结束
  • 不要复用全局 etcd client 的 Watch —— 每个 Resolver 应管理自己的 Watch 实例
  • gRPC 默认使用 round_robin 策略,但前提是你的 Resolver 正确传入 resolver.State{Addresses: [...]}
服务发现不是“配个地址就能用”的功能,注册端的心跳续期、发现端的 Watch 稳定性、与框架(如 gRPC)的生命周期对齐,三者缺一不可。最常出问题的环节,往往卡在租约没续上,或者 Watch 断连后没重试。