负载均衡
问题
Go 微服务中的负载均衡策略有哪些?gRPC 的客户端负载均衡是怎么工作的?
答案
负载均衡类型
Go 微服务主流选择
Go 微服务通常使用客户端负载均衡:调用方从注册中心获取实例列表,自己选择目标实例。gRPC 原生支持这种模式。
gRPC 客户端负载均衡
// 使用 round_robin 策略
conn, _ := grpc.Dial(
"etcd:///user-service",
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
)
常见策略
| 策略 | 原理 | 适用场景 |
|---|---|---|
| Round Robin | 轮流分配 | 实例性能相近 |
| Weighted Round Robin | 按权重分配 | 实例性能不同 |
| Random | 随机选择 | 简单场景 |
| Least Connections | 选连接最少的 | 长连接服务 |
| 一致性哈希 | 相同 key 固定实例 | 有状态/缓存场景 |
自定义负载均衡器
import (
"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/base"
)
// 加权轮询 Picker
type weightedPicker struct {
subConns []balancer.SubConn
weights []int
current int
}
func (p *weightedPicker) Pick(balancer.PickInfo) (balancer.PickResult, error) {
// 加权轮询选择
sc := p.selectByWeight()
return balancer.PickResult{SubConn: sc}, nil
}
// 注册自定义策略
func init() {
balancer.Register(
base.NewBalancerBuilder("weighted_round_robin", &weightedPickerBuilder{}, base.Config{}),
)
}
// 使用
conn, _ := grpc.Dial(target,
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"weighted_round_robin"}`),
)
一致性哈希
适用于需要将相同用户/key 路由到固定实例的场景(如缓存):
type consistentHashPicker struct {
ring *hashring.HashRing // 第三方一致性哈希库
}
func (p *consistentHashPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
// 从 Context 中取 hash key
key, _ := info.Ctx.Value(hashKeyCtx{}).(string)
node := p.ring.GetNode(key)
return balancer.PickResult{SubConn: node.SubConn}, nil
}
常见面试问题
Q1: 客户端负载均衡 vs 服务端负载均衡?
答案:
| 维度 | 客户端 LB | 服务端 LB |
|---|---|---|
| 调用链路 | Client → Server | Client → LB → Server |
| 性能 | 无额外跳转 | 多一跳 |
| 复杂度 | 客户端需集成 SDK | 客户端无感知 |
| 代表 | gRPC、Dubbo | Nginx、Kong |
| Go 推荐 | ✅ 内部 gRPC 调用 | 外部 HTTP 入口 |
Q2: gRPC 的 round_robin 有什么局限?
答案:
- 不感知实例负载,高负载和低负载实例均分流量
- 不支持权重,无法区分不同规格的实例
- 不处理慢实例,不会自动避开响应慢的节点
生产环境通常需要 P2C (Power of 2 Choices) 策略:随机选 2 个实例,选负载更低的那个。go-zero 和 Kratos 都内置了 P2C。