Files
autumn-recruitment/05.架构/service-governance/负载均衡算法.md
T

184 lines
7.1 KiB
Markdown
Raw Normal View History

2026-08-08 19:01:04 +08:00
---
tags: [arch/microservice, load-balancing, consistent-hashing, spring-cloud-lb, round-robin]
create time: 2026-08-08 18:00
update time: 2026-08-08 18:00
---
# 负载均衡算法
## 概述
负载均衡将客户端请求分发到多个服务实例,是微服务架构中不可或缺的一环。本文对比客户端 vs 服务端两种负载均衡架构,详细讲解轮询、随机、一致性哈希、最少连接等核心算法,以及 Spring Cloud LoadBalancer 的实现原理。
## 核心原理
### 客户端负载均衡 vs 服务端负载均衡
```mermaid
graph TD
subgraph ClientLB["客户端负载均衡"]
C1[客户端/消费者] --> LB1[客户端负载均衡器]
LB1 --> SI1["实例A: :8081"]
LB1 --> SI2["实例B: :8082"]
LB1 --> SI3["实例C: :8083"]
C1 -.->|自己选实例| LB1
end
subgraph ServerLB["服务端负载均衡"]
C2[客户端/消费者] --> NLB[Nginx/LVS]
NLB --> SNI1["实例A: :8081"]
NLB --> SNI2["实例B: :8082"]
NLB --> SNI3["实例C: :8083"]
end
```
| 对比维度 | 客户端负载均衡 | 服务端负载均衡 |
|---------|--------------|--------------|
| 部署位置 | 与消费者同进程(如 Spring Cloud LoadBalancer) | 独立中间件(Nginx、LVS、F5) |
| 延迟 | 低(多一次网络跳) | 高(所有请求必经 LB 节点) |
| 故障隔离 | 好(失败不影响其他调用链路) | 差(LB 单点故障影响全部) |
| 语言耦合 | 需要为每种语言实现 SDK | 协议无关,通用性强 |
| 适用场景 | JVM 微服务体系 | 跨语言 / 流量入口层 |
> [!TIP]
> 面试高频:Netflix 为什么选择客户端负载均衡?因为他们的服务都是 Java,可以用 Ribbon(现已被 Spring Cloud LoadBalancer 替代)嵌入每个微服务,避免了反向代理成为瓶颈和单点故障。
### 轮询法(Round Robin)
最简单的策略:按顺序依次分发请求。适用于各实例配置相近的场景。
- **简单轮询**:`index = (index + 1) % n`,第 i 个请求发给第 i mod n 个实例。
- **加权轮询**(Weighted Round Robin):为每个实例分配权重 wᵢ,权重高的获得更多请求。Nginx 的 smooth weighted round robin 用 `current_weight += weight`,每次选 current_weight 最大的,然后 `selected.weight -= total_weight`,保证长期均衡。
### 随机法
从健康实例列表中均匀随机选择一个。理论上 N 次请求后分布趋近均匀,但小样本下可能极端不均——例如连续两次命中同一个实例。加权随机在电商大促中常用:给大容量实例更高权重。
### 一致性哈希(Consistent Hashing)
传统哈希 `hash(instance) % n` 的问题在于实例增减时会导致大量请求重新路由(缓存穿透式抖动)。一致性哈希将其缓解到仅影响 `(1/n)` 的数据。
**核心思路**:将 hash 值空间映射到环上(0 ~ 2³²),服务和请求都 hash 到环上的同一点,请求顺时针找到最近的实例。
**虚拟节点解决数据倾斜**:一个物理实例对应环上的 k 个虚拟节点(k 通常取 100~200),使实例分布更均匀。
```mermaid
graph LR
H1[hash key: user_42] -->|"顺时针"| I3[实例C]
H2[hash key: user_99] -->|"顺时针"| I1[实例A]
H3[hash key: session_7] -->|"顺时针"| I2[实例B]
subgraph Ring["哈希环"]
VN1(VN-a1) --- VN2(VN-a2)
VN1 ---|"虚节" |VN2
VN3(VN-b1) --- VN4(VN-b2)
VN5(VN-c1) --- VN6(VN-c2)
end
```
```go
// 一致性哈希简化实现
type ConsistentHash struct {
hashes []uint64 // 虚拟节点的 hash 值排序
map map[uint64]int // hash -> 物理实例索引
k int // 每个实例的虚拟节点数
}
func NewConsistentHash(instances []string, k int) *ConsistentHash {
ch := &ConsistentHash{map: make(map[uint64]int), k: k}
for i, inst := range instances {
for v := 0; v < k; v++ {
h := murmur3.Sum64([]byte(fmt.Sprintf("%s-%d", inst, v)))
ch.hashes = append(ch.hashes, h)
ch.map[h] = i
}
}
sort.Slice(ch.hashes, func(i, j int) bool { return ch.hashes[i] < ch.hashes[j] })
return ch
}
func (ch *ConsistentHash) Get(key string) string {
h := murmur3.Sum64([]byte(key))
idx := sort.Search(len(ch.hashes), func(i int) bool {
return ch.hashes[i] >= h
})
if idx == len(ch.hashes) {
idx = 0 // 回到环首
}
return InstanceList[ch.map[ch.hashes[idx]]]
}
```
### 最少连接数法(Least Connections)
将请求发给当前活跃连接最少的实例,特别适合长连接场景(gRPC、WebSocket)。实现上需维护每个实例的活跃连接计数器,并发安全地读写。
### Spring Cloud LoadBalancer 实现原理
Spring Cloud LoadBalancer 取代了已停止维护的 Ribbon,核心机制:
1. **ServiceInstanceListSupplier**:从注册中心拉取实例列表并缓存。
2. **Reactively reactive**:基于 Reactor 响应式编程,返回 `Flux<ServiceInstance>`。
3. **RoundRobinLocator**:默认轮询策略,内部用 AtomicLong 做递增索引取模。
```java
// Spring Cloud LoadBalancer 核心接口
public interface LoadBalancerClient<S extends ServiceInstance> {
<T> Mono<T> execute(String serviceId, LoadBalancerRequest<T>, InstanceChooser);
ServiceInstance choose(String serviceId);
<T> Flux<T> getLazyLoadBalancerClient(String serviceId);
}
```
> [!NOTE]
> Spring Cloud LoadBalancer 不提供重试和熔断,这些能力由 Resilience4j 等库处理。它只管"选哪个实例"这一个动作。
## 代码示例
Go 中的加权轮询实现:
```go
type WeightedRR struct {
servers []*Server
totalW int
curIndex int
}
type Server struct {
Addr string
Weight int
CurWeight int
}
func (w *WeightedRR) Next() string {
maxW, best := -1, ""
for _, s := range w.servers {
s.CurWeight += s.Weight
if s.CurWeight > maxW {
maxW = s.CurWeight
best = s.Addr
}
}
for _, s := range w.servers {
s.CurWeight -= w.totalW
}
return best
}
```
## 实践场景
**秋招高频问题:**
- "一致性哈希为什么能减少数据迁移?" — 只有失效实例及其顺时针下一个实例之间的数据段会迁移。移除一个实例只影响其相邻的两个虚拟节点范围,而非全部数据。
- "什么时候不能用轮询?" — 实例规格差异大(如混部有强机和弱机),必须加权;或者存在长连接状态绑定(如 WebSocket 会话),需要一致性哈希来确保同一用户落到同一实例。
- "Spring Cloud 和 Nginx 的负载均衡有什么区别?" — Spring Cloud 在应用层做选择,感知注册中心变更(秒级);Nginx 依赖 upstream 配置或 DNS 解析更新,变更延迟较大。
> [!WARNING]
> 一致性哈希不适合频繁增删实例的场景。如果实例每几分钟就变一次,virtual nodes 数量要调得更大(500+)才能维持稳定性,这增加了内存开销。
## 关联笔记
- [[服务注册与发现]]
- [[限流熔断降级]]