⭐ 推荐:社区规则条款 V1.0

Go 语言分布式系统:etcd / Consul 服务注册与发现

陈先生
陈先生 正式会员正式会员认证极客认证极客
发布于 2026-10-13 17:18 ·5 浏览 ·0 回复
内容摘要

围绕 Go 分布式系统中的服务注册与发现,说明注册中心需具备注册、保活、发现和变更通知能力,对比 etcd 以租约、前缀查询和 Watch 实现注册与下线通知,而 Consul 内置健康检查、DNS 与多数据中心支持,并给出 etcd 可复用代码、Key 命名规范及 10~30 秒 TTL 等续约和优雅下线细节。

照这篇做完,你能用 Go 写出一套能跑在生产的服务注册与发现:etcd 靠租约保活、Consul 靠自带健康检查,两条路线都有可直接抄的代码和下线细节。

第一步:先想清楚注册与发现要解决什么

分布式系统里,服务实例的 IP 和端口是动态的,写死在配置里必然出问题。注册中心要提供四件事:注册(实例启动后把自己写进去)、保活(实例挂了要能被摘掉)、发现(调用方拿到当前健康的实例列表)、变更通知(实例增减时调用方不用轮询)。

etcd 和 Consul 都能干这四件事,差别在于:etcd 是一个强一致的 KV 存储,健康检查语义要靠租约(Lease)自己实现;Consul 是专门的服务网格注册中心,健康检查、DNS 接口、多数据中心是内置的。

第二步:etcd 方案——租约 + 前缀 Watch

引入依赖:

go get go.etcd.io/etcd/client/v3

注册端核心就三步:申请租约 → 带租约写 Key → 后台续约。

cli, _ := clientv3.New(clientv3.Config{
    Endpoints:   []string{"127.0.0.1:2379"},
    DialTimeout: 5 * time.Second,
})
defer cli.Close()

ctx := context.Background()
lease, _ := cli.Grant(ctx, 10) // 10 秒 TTL
key := "/services/order/10.0.0.5:8080"
cli.Put(ctx, key, `{"weight":1}`, clientv3.WithLease(lease.ID))

// 自动续约,进程活着就一直续
ch, _ := cli.KeepAlive(ctx, lease.ID)
go func() {
    for range ch {
    }
}()

// 优雅下线:撤销租约,实例立刻消失
defer cli.Revoke(context.Background(), lease.ID)

发现端用前缀查询拿全量列表,再用 Watch 拿增量:

resp, _ := cli.Get(ctx, "/services/order/", clientv3.WithPrefix())
for _, kv := range resp.Kvs {
    fmt.Println(string(kv.Key), string(kv.Value))
}

watchCh := cli.Watch(ctx, "/services/order/", clientv3.WithPrefix())
for wresp := range watchCh {
    for _, ev := range wresp.Events {
        if len(ev.Kv.Value) == 0 { // 空值 = 删除事件
            fmt.Println("下线:", string(ev.Kv.Key))
        } else {
            fmt.Println("上线:", string(ev.Kv.Key))
        }
    }
}

注意:Key 的命名必须规整成 /services/{服务名}/{实例ID},前缀查询和 Watch 全靠这个约定。实例 ID 用 IP:Port 或 主机名-端口,保证全局唯一。

注意:TTL 不要设太短。10~30 秒比较稳妥,太短会因网络抖动误摘健康实例;同时 KeepAlive 的意义在于——进程假死(死锁、长 GC)时续约中断,实例自动被摘掉,这正是我们要的效果。

第三步:Consul 方案——ServiceRegister + 健康检查

Consul 把保活做进了 Agent,你不用自己维护租约。

go get github.com/hashicorp/consul/api
cfg := api.DefaultConfig()
cfg.Address = "127.0.0.1:8500"
c, _ := api.NewClient(cfg)

reg := &api.AgentServiceRegistration{
    ID:      "order-10.0.0.5-8080",
    Name:    "order",
    Address: "10.0.0.5",
    Port:    8080,
    Check: &api.AgentServiceCheck{
        HTTP:                           "http://10.0.0.5:8080/health",
        Interval:                       "10s",
        Timeout:                        "3s",
        DeregisterCriticalServiceAfter: "60s", // 关键
    },
}
c.Agent().ServiceRegister(reg)
defer c.Agent().ServiceDeregister(reg.ID)

发现端查健康实例,只返回 passing 状态的:

entries, _, _ := c.Health().Service("order", "", true, nil)
for _, e := range entries {
    addr := fmt.Sprintf("%s:%d", e.Service.Address, e.Service.Port)
    fmt.Println(addr)
}

如果你不想暴露 HTTP 健康端点,把 HTTP/Interval 换成 TTL: "15s",然后在业务里定时上报:

go func() {
    t := time.NewTicker(5 * time.Second)
    for range t.C {
        c.Agent().PassTTL("service:order-10.0.0.5-8080", "")
    }
}()

注意:DeregisterCriticalServiceAfter 不设置的话,服务彻底挂掉后实例会永远留在 Catalog 里,调用方一直拿到死地址。这是 Consul 最常见的坑。

注意:TTL 模式下的 CheckID 格式是 service:{注册时的ID},写错不会报错,但心跳等于白发。

第四步:调用方怎么做负载均衡

拿到实例列表后,别每个请求都去查一次注册中心。标准做法是本地缓存 + Watch/Blocking Query 更新:后台协程维护一份内存列表,请求时对它做轮询或随机挑选。etcd 用 Watch,Consul 用 Health().Service 的 QueryOptions{WaitIndex: lastIndex, WaitTime: 5 * time.Minute} 长轮询。

再进一步就是接入 gRPC 的 resolver.Builder,让 gRPC 自己按服务名拨号;或者用 go-kit、grpc-go 官方的 etcd resolver 插件,避免手写。

第五步:怎么选

  • 已经在用 Kubernetes、etcd 集群现成:选 etcd,少一套组件,Watch 语义干净。
  • 需要开箱即用的健康检查、DNS 查询、跨数据中心:选 Consul。
  • 注册中心本身必须集群部署——etcd 至少 3 节点,Consul 3 或 5 节点。单点注册中心挂了,整个系统就没法扩缩容。

注意:无论用哪个,服务关闭时一定要走 defer Revoke / Deregister,否则要等 TTL 过期才摘除,滚动发布期间会有一段时间请求打到已停的实例上。

小结

  1. 注册 = 写 Key(etcd 带 Lease / Consul 带 Service),保活 = KeepAlive 或 PassTTL。
  2. etcd 用前缀 + Watch 做发现;Consul 用 Health().Service(..., passingOnly=true)。
  3. Key/ID 命名要有约定且全局唯一,否则发现端拿不到正确列表。
  4. 调用方本地缓存实例列表,靠 Watch 或阻塞查询更新,不要每次请求查注册中心。
  5. 优雅下线(Revoke / Deregister)和 DeregisterCriticalServiceAfter 是两个必配项。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-817.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~