Skip to content
Go
服务注册与发现

服务注册与发现

go-zero 的服务注册发现抽象层支持多种后端(etcd、Consul、Nacos、K8s),框架默认为 etcd 提供最佳支持。


架构模型

┌──────────┐  注册  ┌──────────┐  发现  ┌──────────┐
│ RPC 服务  │ ────▶ │ 注册中心  │ ────▶ │ API 服务  │
│ Provider │       │ (etcd等)  │       │ Consumer │
└──────────┘       └──────────┘       └──────────┘
       │                                    │
       └──────────── gRPC 调用 ─────────────┘

每个 RPC 服务启动时向注册中心注册自己的地址,API 服务通过注册中心发现 RPC 服务的所有实例,并使用 P2C 负载均衡算法选择最优实例。


etcd 模式(推荐生产使用)

依赖

# 安装 etcd
# Docker
docker run -d --name etcd \
  -p 2379:2379 -p 2380:2380 \
  quay.io/coreos/etcd:v3.5.9 \
  /usr/local/bin/etcd \
  --listen-client-urls http://0.0.0.0:2379 \
  --advertise-client-urls http://127.0.0.1:2379

# 或直接下载二进制
# https://github.com/etcd-io/etcd/releases

RPC 服务配置(Provider)

# etc/user.yaml
Name: user.rpc
ListenOn: 0.0.0.0:8080
Etcd:
  Hosts:
    - 127.0.0.1:2379
    - 127.0.0.1:2380     # etcd 集群多节点
    - 127.0.0.1:2381
  Key: user.rpc            # 注册 Key

💡 Key 命名规范:标准格式 <服务名>.rpc,多环境用 /prod/order.rpc,业务域用 payment.order.rpc

API 服务配置(Consumer)

# etc/user-api.yaml
Name: user-api
Host: 0.0.0.0
Port: 8888

UserRpcConf:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: user.rpc
  Timeout: 2000
// 注入 RPC 客户端
type ServiceContext struct {
    Config  config.Config
    UserRpc user.User
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:  c,
        UserRpc: user.NewUser(zrpc.MustNewClient(c.UserRpcConf)),
    }
}

验证注册

etcdctl get --prefix user.rpc

# 输出:
# user.rpc/7587870460981677828
# 192.168.1.10:8080

etcd 生产部署建议

  • 使用 3 或 5 个节点的 etcd 集群
  • 配置 TLS 加密通信
  • Key 命名带环境前缀:/prod/user.rpc/staging/user.rpc
  • 启用磁盘告警,定期备份 etcd 数据

直连模式(开发/测试)

不想启动 etcd 时,使用直连模式:

RPC 服务端 — 去掉 Etcd 配置

Name: user.rpc
ListenOn: 0.0.0.0:8080
# 不配置 Etcd = 不注册到注册中心

API 客户端 — 使用 Endpoints

UserRpcConf:
  Endpoints:
    - 127.0.0.1:8080

或使用配置结构体的直连字段:

UserRpc: zrpc.RpcClientConf{
    Endpoints: []string{"127.0.0.1:8080"},
}

💡 直连模式适合本地开发和简单测试,不适合生产环境(无故障转移和负载均衡)。


K8s 模式(云原生)

在 K8s 环境中,可直接使用 Headless Service DNS 替代注册中心:

# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: user-rpc
spec:
  clusterIP: None          # Headless Service
  selector:
    app: user-rpc
  ports:
    - port: 8080
      name: grpc
# RPC 服务端配置 — 不配置 Etcd
Name: user.rpc
ListenOn: 0.0.0.0:8080
# 在 K8s 中注册由 Pod 生命周期自动管理
# API 客户端配置 — 使用 K8s DNS
UserRpcConf:
  Target: dns:///user-rpc.default.svc.cluster.local:8080

💡 K8s 模式的优势:无需独立维护 etcd 集群,与 K8s 原生服务发现机制结合。


Consul / Nacos 集成

go-zero 社区提供了 Consul 和 Nacos 的适配支持。

Consul

# 启动 Consul
docker run -d --name consul -p 8500:8500 consul:latest
# 使用 Consul 做注册发现
Consul:
  Host: 127.0.0.1:8500
  Key: user.rpc

Nacos

Nacos 在 go-zero 中通过配置中心集成,支持配置热更新和服务发现:

Nacos:
  Host: 127.0.0.1:8848
  Namespace: public
  Group: DEFAULT_GROUP
  DataId: user.rpc

💡 大多数 go-zero 项目使用 etcd 或 K8s DNS 即可满足需求,Consul/Nacos 适用于已有这些基础设施的团队。


P2C 负载均衡算法

go-zero 的 zrpc 客户端使用 P2C(Pick Two Choices)算法

每次从所有可用实例中随机选两个
  → 比较两者的负载状态
  → 选择负载更低的那个
  → EWMA(指数加权移动平均)统计延迟

为什么用 P2C 而不是轮询?

算法 优点 缺点
轮询 简单 不均(长连接 + 慢请求)
最少连接 精确 需要全局锁
随机 无状态 可能不均
P2C 近似最优,无需全局锁 稍有偏差

P2C 是 Google 大规模使用的负载均衡算法,在性能和公平性之间取得了很好的平衡。


健康检查与优雅下线

自动健康检查

go-zero RPC 服务自动实现 gRPC Health Checking Protocol:

# 使用 grpc_health_probe 检查
grpc_health_probe -addr=localhost:8080

优雅停止

// go-zero 自动处理 SIGTERM/SIGINT
// 收到信号后:
// 1. 从 etcd 注销自身
// 2. 停止接受新连接
// 3. 等待现有请求处理完毕
// 4. 退出进程

使用 go-zero/core/proc 可以自定义优雅停止行为:

proc.AddShutdownListener(func() {
    // 自定义清理逻辑
    // 关闭数据库连接、刷新缓冲区等
})

单点故障与高可用

服务端多实例

# 同一 RPC 服务启动多个实例
# 实例1
LISTEN_ON=0.0.0.0:8080 go run user.go
# 实例2
LISTEN_ON=0.0.0.0:8081 go run user.go

它们以不同实例 ID 注册到 etcd 的同一 Key 下:

etcdctl get --prefix user.rpc
# user.rpc/7587870460981677828 → 192.168.1.10:8080
# user.rpc/7587870460981677829 → 192.168.1.10:8081

客户端自动故障转移

当一个实例不可用时:

  1. etcd Watch 机制通知客户端该实例下线
  2. P2C 算法自动排除不可用实例
  3. 熔断器对故障实例快速失败

服务发现流程详解

RPC 启动
  → 连接 etcd
  → 注册 Key: user.rpc/instance-id → 192.168.1.10:8080
  → 设置 TTL 租约 + 心跳续约

API 启动
  → 连接 etcd
  → Watch Key: user.rpc
  → 获取所有实例列表 [8080, 8081]
  → 对每个 endpoint 创建 gRPC 连接 + 熔断器

API 调用 RPC
  → P2C 选实例
  → 熔断器检查(打开 → 跳过)
  → gRPC 调用
  → 更新延迟统计

RPC 实例宕机
  → etcd 租约过期 → Key 删除
  → API Watch 收到通知 → 更新实例列表
  → 新请求不再路由到宕机实例

常见陷阱

陷阱 说明
🚨 etcd Key 不一致 Provider 注册的 Key 和 Consumer 查找的 Key 必须完全一致
🚨 etcd 连接泄漏 频繁创建 zrpc.MustNewClient 会导致 etcd Watch 连接不释放
🚨 K8s 环境中还依赖 etcd K8s 有原生服务发现,再引入 etcd 增加运维复杂度
🚨 单点 etcd etcd 单节点挂掉后整个服务发现体系瘫痪,生产至少 3 节点
🚨 Network partition 下多注册 网络分区时 etcd 节点各执一词,可能导致流量分发错乱
🚨 忘记清除下线实例 非正常退出的实例可能在 etcd 中残留,需依赖 TTL 自动清理