RPC 核心概念 04:服务发现与负载均衡

单机时代我们靠 IP+Port 直连;微服务时代,服务实例数动辄几十上百,IP 还会频繁变化。服务发现 + 负载均衡就是解决"我该把请求发到哪里"的核心机制。
在这里插入图片描述

一、问题的提出

调用方:

client := NewUserClient("user.example.com:8000")
resp, _ := client.GetUser(ctx, req)

直连有以下问题:

  1. 实例 IP 变化(容器重启、扩容);
  2. 单点故障;
  3. 流量无法均衡到多个实例;
  4. 实例上下线无法感知。

解决思路:在客户端和服务实例之间引入服务注册中心负载均衡器

二、服务发现的两种模式

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 拦截 + 客户端发现",业务代码无感知。

三、注册中心的角色

注册中心负责:

  1. 服务注册:实例启动后上报 IP、端口、元数据;
  2. 健康检查:周期 ping,剔除不健康节点;
  3. 服务订阅:客户端订阅变化,实时更新本地缓存;
  4. 元数据管理:版本、环境、机房等。

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 / grpc
  interval: 5s
  timeout: 2s
  threshold: 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.user
      target: polaris://trpc.app.user
      protocol: trpc
      loadbalance: weighted_random
      discovery: polaris

代码层面:

proxy := pb.NewUserClientProxy(
    client.WithTarget("polaris://trpc.app.user"),
    client.WithBalancerName("p2c"),
)
resp, err := proxy.GetUser(ctx, req)

整个发现 + 选路 + 调用过程对业务完全透明

八、缓存与同步

客户端不可能每次调用都查注册中心,必须本地缓存:

启动时 → 拉取全量
之后  → 监听增量变更(watch / push)
失败  → 降级到上次的缓存

降级缓存至关重要——注册中心宕机时业务还能继续跑。

九、灰度与金丝雀

借助元数据路由实现:

  1. 部署 v2 版本,权重设为 5%;
  2. 监控 v2 错误率/延迟;
  3. 没问题逐步放量;
  4. 出问题立即把 v2 权重设为 0。

十、踩坑总结

  1. 依赖回环:注册中心也需要服务发现,要避免循环依赖;
  2. 网络分区:注册中心心跳超时不一定真挂了;
  3. 缓存雪崩:注册中心抖动导致全量重连;
  4. 慢启动:新实例上线先少量流量预热;
  5. 权重突变:避免直接 0 → 100,渐进调权重。

十一、小结

  • 服务发现解决"调谁",负载均衡解决"调多个中的哪一个";
  • 客户端发现性能好但有 SDK 依赖,服务端发现简单但多一跳;
  • 注册中心选型:服务发现场景偏好 AP;
  • LB 算法没有银弹,按场景选;
  • tRPC + Polaris 是国产工业级实践代表。

下一篇我们将探讨 RPC 服务治理三大件:超时、重试与熔断

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐