服务注册与发现
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/releasesRPC 服务配置(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:8080etcd 生产部署建议
- 使用 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.rpcNacos
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客户端自动故障转移
当一个实例不可用时:
- etcd Watch 机制通知客户端该实例下线
- P2C 算法自动排除不可用实例
- 熔断器对故障实例快速失败
服务发现流程详解
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 自动清理 |