Files

7.1 KiB
Raw Permalink Blame History

tags, create time, update time
tags create time update time
arch/microservice
load-balancing
consistent-hashing
spring-cloud-lb
round-robin
2026-08-08 18:00 2026-08-08 18:00

负载均衡算法

概述

负载均衡将客户端请求分发到多个服务实例,是微服务架构中不可或缺的一环。本文对比客户端 vs 服务端两种负载均衡架构,详细讲解轮询、随机、一致性哈希、最少连接等核心算法,以及 Spring Cloud LoadBalancer 的实现原理。

核心原理

客户端负载均衡 vs 服务端负载均衡

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),使实例分布更均匀。

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
// 一致性哈希简化实现
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 做递增索引取模。
// 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 中的加权轮询实现:

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+)才能维持稳定性,这增加了内存开销。

关联笔记