RPC 核心概念 04:服务发现与负载均衡
单机时代我们靠 IP+Port 直连;微服务时代,服务实例数动辄几十上百,IP 还会频繁变化。服务发现 + 负载均衡就是解决"我该把请求发到哪里"的核心机制。
一、问题的提出
调用方:
client:=NewUserClient("user.example.com:8000")resp,_:=client.GetUser(ctx,req)直连有以下问题:
- 实例 IP 变化(容器重启、扩容);
- 单点故障;
- 流量无法均衡到多个实例;
- 实例上下线无法感知。
解决思路:在客户端和服务实例之间引入服务注册中心和负载均衡器。
二、服务发现的两种模式
2.1 客户端发现(Client-side Discovery)
[ Registry ] ↑ | | 注册 | 查询 | ↓ [ Service A实例 ] [ Client ] ──直连──► [ Service A 实例 N ]特点:
- Client 直接查注册中心拿到实例列表;
- LB 算法在 Client 侧;
- 性能好(少一跳),但所有客户端都要集成 SDK。
代表:Netflix Ribbon、gRPC、tRPC、Dubbo。
2.2 服务端发现(Server-side Discovery)
[ Service A实例 ] → [ Registry ] ↑ [ Client ] → [ LB / Sidecar ] → [ Service A 实例 N ]特点:
- LB 是独立组件(可能是 nginx、Envoy、k8s Service);
- Client 只知道 LB 的入口;
- 多语言成本低,但多一跳。
代表:k8s Service + kube-proxy / Istio Sidecar、AWS ALB。
2.3 现代趋势:Service Mesh
把"客户端发现"改造为"Sidecar 拦截 + 客户端发现",业务代码无感知。
三、注册中心的角色
注册中心负责:
- 服务注册:实例启动后上报 IP、端口、元数据;
- 健康检查:周期 ping,剔除不健康节点;
- 服务订阅:客户端订阅变化,实时更新本地缓存;
- 元数据管理:版本、环境、机房等。
3.1 主流注册中心
| 名称 | 特点 |
|---|---|
| Consul | 多数据中心、KV、健康检查丰富 |
| etcd | k8s 自带、强一致(Raft) |
| ZooKeeper | 老牌、CP 系统、运维较重 |
| Nacos | 阿里开源,配置 + 注册一体 |
| Eureka | Netflix,AP 系统(已停止维护) |
| Polaris(北极星) | 腾讯开源,tRPC 默认搭档 |
3.2 CAP 取舍
- CP(强一致,可能不可用):ZooKeeper、etcd;
- AP(高可用,可能不一致):Eureka、Nacos、Polaris。
服务发现场景优先 AP——短暂列表不一致比注册中心宕机带来的损失小得多。
3.3 Polaris 简介
Polaris(北极星)是腾讯开源的服务治理平台,集注册发现 + 配置 + 路由 + 限流熔断 + 可观测于一体,是 tRPC 体系的默认服务治理中枢。其核心 API:
import"github.com/polarismesh/polaris-go/api"provider,_:=api.NewProviderAPI()provider.Register(&api.InstanceRegisterRequest{Service:"trpc.app.user",Namespace:"Production",Host:"10.0.0.1",Port:8000,})consumer,_:=api.NewConsumerAPI()resp,_:=consumer.GetOneInstance(&api.GetOneInstanceRequest{Service:"trpc.app.user",})四、负载均衡算法
4.1 静态算法
轮询(Round Robin)
请求1 → A 请求2 → B 请求3 → C 请求4 → A简单公平,但忽略实例性能差异。
加权轮询(Weighted Round Robin)
为每个实例配置权重(如 CPU 多的权重大),按比例分配。Nginx 的默认算法。
随机(Random)
请求量大时表现接近轮询,实现简单,无状态。
哈希(Hash)
按某个 key(如用户 ID)哈希到实例,保证同一用户落到同一实例,常用于带本地缓存或会话粘性场景。
4.2 动态算法
最少连接(Least Connections)
把请求发给当前连接数最少的实例,自动避开慢节点。
P2C / EWMA(指数加权移动平均)
随机选两个实例,比较各自负载(连接数、最近平均延迟),选好的那个。简单高效,Google SRE 推荐。
一致性哈希(Consistent Hashing)
哈希环 + 虚拟节点,实例增减时只影响相邻段。常用于:
- 缓存系统(Redis Cluster);
- 大对象存储路由。
4.3 算法选择
| 场景 | 推荐 |
|---|---|
| 实例性能均匀 | 轮询/随机 |
| 实例性能不均匀 | 加权轮询 / P2C |
| 会话粘性 | 哈希 / 一致性哈希 |
| 带状态缓存 | 一致性哈希 |
| 长连接服务 | 最少连接 |
五、健康检查
5.1 客户端主动探测
healthCheck:type:tcp / http / grpcinterval:5stimeout:2sthreshold:3# 连续失败次数5.2 服务端被动上报
实例自己定期向注册中心发心跳:
实例 ──5s 一次心跳──► 注册中心超过 N 个周期未收到则剔除。
5.3 熔断式被动探测
调用失败率达到阈值,本地标记节点"不健康",停发流量一段时间,过会儿再试探。
六、动态路由
除了"哪个实例可用",还有"该把请求送到哪个实例"。
6.1 元数据路由
每个实例打标签(version、env、region),按标签匹配:
routes:-match:header:x-canary:"true"route:version:"v2"# 灰度6.2 就近路由
按机房/区域优先:
Beijing-Client ─prefer→ Beijing-实例 ─fallback→ Shanghai-实例6.3 流量染色
用户/请求带"染色 tag",整个调用链都按染色规则路由,是全链路灰度的基石。
七、tRPC-Go 中的实现
tRPC 通过插件实现服务发现 + 负载均衡 + 路由:
client:service:-name:trpc.app.usertarget:polaris://trpc.app.userprotocol:trpcloadbalance:weighted_randomdiscovery:polaris代码层面:
proxy:=pb.NewUserClientProxy(client.WithTarget("polaris://trpc.app.user"),client.WithBalancerName("p2c"),)resp,err:=proxy.GetUser(ctx,req)整个发现 + 选路 + 调用过程对业务完全透明。
八、缓存与同步
客户端不可能每次调用都查注册中心,必须本地缓存:
启动时 → 拉取全量 之后 → 监听增量变更(watch / push) 失败 → 降级到上次的缓存降级缓存至关重要——注册中心宕机时业务还能继续跑。
九、灰度与金丝雀
借助元数据路由实现:
- 部署 v2 版本,权重设为 5%;
- 监控 v2 错误率/延迟;
- 没问题逐步放量;
- 出问题立即把 v2 权重设为 0。
十、踩坑总结
- 依赖回环:注册中心也需要服务发现,要避免循环依赖;
- 网络分区:注册中心心跳超时不一定真挂了;
- 缓存雪崩:注册中心抖动导致全量重连;
- 慢启动:新实例上线先少量流量预热;
- 权重突变:避免直接 0 → 100,渐进调权重。
十一、小结
- 服务发现解决"调谁",负载均衡解决"调多个中的哪一个";
- 客户端发现性能好但有 SDK 依赖,服务端发现简单但多一跳;
- 注册中心选型:服务发现场景偏好 AP;
- LB 算法没有银弹,按场景选;
- tRPC + Polaris 是国产工业级实践代表。
下一篇我们将探讨 RPC 服务治理三大件:超时、重试与熔断。