--- 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`。 3. **RoundRobinLocator**:默认轮询策略,内部用 AtomicLong 做递增索引取模。 ```java // Spring Cloud LoadBalancer 核心接口 public interface LoadBalancerClient { Mono execute(String serviceId, LoadBalancerRequest, InstanceChooser); ServiceInstance choose(String serviceId); Flux 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+)才能维持稳定性,这增加了内存开销。 ## 关联笔记 - [[服务注册与发现]] - [[限流熔断降级]]