负载均衡算法:从轮询到一致性哈希

高性能网络 2026-07-22 · 阅读约 22 分钟

负载均衡器回答的问题看似简单——把请求分给哪台后端?但这个问题在不同场景下的正确答案天差地别:无状态短请求要的是均匀与低开销,有状态的缓存与分片数据要的是节点变化时尽可能少的数据迁移。本文按复杂度递增梳理从轮询、加权、随机、最少连接,到取模哈希、一致性哈希的完整谱系,并用一段零依赖 Python 实验把『扩容时 key 迁移率』与『分配均匀度』两个关键指标量化出来。

一、无状态场景:均匀是唯一的追求

对无状态的短请求(典型如 Web 请求),负载均衡的目标是让各后端的负载尽可能相等,同时调度开销尽可能小:

  • 轮询(Round Robin):依次轮转,绝对的均匀(后文实测标准差几乎为 0),但完全不知道后端的真实负载——某台机器变慢了照样分同样多请求。加权轮询(WRR)允许按机器能力分配比例,Nginx 默认策略就是带权轮询的平滑变体。
  • 随机:每次随机挑一台。概率均匀,实测标准差约 3%,优势是无共享状态、天然无锁,适合超高并发调度器。
  • 最少连接(Least Connections):把新请求发给当前活跃连接最少的后端。能感知真实负载,适合长连接与请求耗时不均的场景;代价是需要维护计数并考虑慢启动(新上线节点瞬间被打爆,需要 slow-start 过渡)。
  • 两随机选一(Power of Two Choices):随机挑两台比较负载取较轻者,用 O(1) 的代价逼近全局最少连接的效果,是理论优雅与工程实用的典范结合。

二、有状态场景:一致性哈希解决扩缩容地震

当后端是有状态的(缓存、数据分片、会话保持),请求与后端的映射就不能只看负载,还必须稳定:同一个 key 要落到同一台机器。最直接的做法是取模哈希 node = hash(key) % N——简单、均匀,但有一个致命伤:N 变化时几乎全部 key 的归属都要重算。从 3 台扩到 4 台,模数从 3 变 4,理论上有 3/4 的 key 要搬家。对缓存系统这意味着 75% 的缓存瞬间失效、回源流量雪崩——这就是著名的『扩容地震』。

一致性哈希(Karger 等,1997)的解法是改变映射的几何结构:把哈希输出空间视为一个环(0 到 2^32-1 首尾相接),节点和 key 都映射到环上,key 归属于顺时针方向的第一个节点。增删一个节点只影响环上它与相邻节点之间那段弧上的 key,理论迁移率从 75% 降到 1/N。

但朴素一致性哈希有个均匀性问题:节点在环上的位置由一次哈希决定,3 个节点随机落位很容易挤成一团,导致负载严重偏斜。工程解法是虚拟节点:每个物理节点派生 100~200 个虚拟节点撒到环上,统计规律让分布趋近均匀,同时保留『扩缩容只迁移 1/N』的性质。后文实验会同时量化这两个效果。

三、动手实验:迁移率与均匀度的量化对比

下面代码对 10000 个 key 分别用取模哈希与带 150 个虚拟节点的一致性哈希做映射,模拟从 3 节点扩容到 4 节点,统计需要迁移的 key 比例;同时对比轮询、随机、带虚拟节点与不带虚拟节点的一致性哈希四种分配方式的均匀度。零依赖,Python 3 直接运行:

import hashlib, bisect, random
random.seed(3)

def h(s):
    return int(hashlib.md5(s.encode()).hexdigest(), 16)

KEYS = [f'user-{i}' for i in range(10000)]
NODES3 = ['node-A', 'node-B', 'node-C']
NODES4 = NODES3 + ['node-D']

# 取模哈希: N 变化 -> 大面积迁移
mod3 = {k: NODES3[h(k) % 3] for k in KEYS}
mod4 = {k: NODES4[h(k) % 4] for k in KEYS}
mod_moved = sum(1 for k in KEYS if mod3[k] != mod4[k])

# 一致性哈希: 环上顺时针找第一个虚拟节点
VNODE = 150
class ConsistentHash:
    def __init__(self, nodes):
        self.points = sorted((h(f'{n}#{v}'), n)
                             for n in nodes for v in range(VNODE))
        self.keys = [p[0] for p in self.points]
    def get(self, key):
        i = bisect.bisect(self.keys, h(key)) % len(self.points)
        return self.points[i][1]

ch3, ch4 = ConsistentHash(NODES3), ConsistentHash(NODES4)
ch_moved = sum(1 for k in KEYS if ch3.get(k) != ch4.get(k))

print(f'取模哈希扩容迁移率: {mod_moved/len(KEYS)*100:.1f}%')
print(f'一致性哈希扩容迁移率: {ch_moved/len(KEYS)*100:.1f}%')

在沙箱(Python 3,无第三方库)上的真实运行输出:

== 节点扩容(3 -> 4)的 key 迁移率 ==
  取模哈希      : 7506/10000 迁移, 迁移率 75.1% (理论 75%)
  一致性哈希    : 2097/10000 迁移, 迁移率 21.0% (理论约 25%)

== 分配均匀度(3 节点, 10000 个 key) ==
  轮询                  : A:3334 B:3333 C:3333  均值 3333 标准差 0.5 (0.0%)
  随机                  : A:3311 B:3464 C:3225  均值 3333 标准差 98.8 (3.0%)
  一致性哈希(150虚节点) : A:3572 B:3102 C:3326  均值 3333 标准差 191.9 (5.8%)
  一致性哈希(无虚节点)  : A:5772 B:2163 C:2065  均值 3333 标准差 1724.9 (51.7%)

校验通过: 一致性哈希迁移率远低于取模哈希, 虚拟节点显著改善均匀度

四、结果解读:四个数字各自说明什么

  • 取模哈希迁移率 75.1%:实测与理论值 75% 分毫不差。换成缓存场景的语言:扩一台机器,四分之三的缓存键瞬间失效。这就是缓存系统绝对不能用取模哈希的原因。
  • 一致性哈希迁移率 21.0%:接近理论值 1/4(新增节点应从环上『接管』约四分之一的弧长)。同样扩一台机器,需要迁移的数据量只有取模哈希的三分之一不到,且随节点数增多优势更大(N 越大,1/N 越小)。
  • 虚拟节点的价值用标准差说话:不带虚拟节点时标准差高达 51.7%(A 节点背了 5772 个 key,C 只有 2065),带 150 个虚拟节点后压到 5.8%。虚拟节点的本质是把『一次随机落点』变成『150 次随机落点求和』,用大数定律抹平偏斜——代价只是环上查找结构变大一点,二分查找毫秒级毫无压力。
  • 轮询的 0.0% 与随机的 3.0%:轮询均匀度完美但无负载感知;随机以极低成本拿到 3% 的均匀度,解释了为什么大型调度器(如 Google Maglev 之前的某些系统)偏爱随机及其变体。一致性哈希的 5.8% 均匀度完全够用——记住它的目标是稳定而非完美均匀,两个目标不可兼得时按业务取舍。

五、谱系之外:生产级算法一览

  • Maglev(Google):查表式一致性哈希——预计算一张大查找表,节点变化时重新填充但保持已有映射尽量不动,O(1) 查表,支撑 Google 的 L4 负载均衡。
  • Rendezvous Hashing(HRW):对每个 key 计算它与所有节点的组合哈希、取最大者。无需维护环结构,节点列表一致即结果一致,适合客户端侧分片。
  • Jump Hash(Google):数学上直接计算 key 归属的桶编号,O(ln N) 无存储,桶数变化只影响最后一个桶区间——适用于桶编号连续的场景(如存储分片)。
  • 微服务层的『负载均衡』:Envoy 的 least_request、p2c + EWMA(指数加权移动平均响应时间),把延迟感知引入调度,比纯连接数更贴近真实负载。

六、工程应用

  • 分层选型:L4(四层)负载均衡要的是吞吐与稳定映射(一致性哈希/Maglev),L7(七层)要的是负载感知(least_conn/p2c)。典型架构是 L4 把流量分给的 L7 集群,L7 再做应用层调度。
  • Nginx 配置:upstream { ip_hash; } 提供会话保持的简易方案,但扩缩容照样触发取模哈希式地震;有状态上游应使用 hash $key consistent;(consistent 参数即一致性哈希)。
  • 缓存分片:Redis Cluster 用 16384 个哈希槽(slot)而非一致性哈希——槽位表显式管理、迁移以槽为单位,运维可控性更强;Memcached 客户端传统上用带虚拟节点的一致性哈希。
  • 会话保持的取舍:能用无状态 + 共享会话存储(Redis)就别用 sticky session——粘性会话把可用性绑死在单机寿命上,任何重启都会踢掉一批用户。
  • 灰度与预热:新节点上线用权重渐进提升(Nginx 权重或 least_conn 的 slow_start),避免冷缓存节点瞬间承接全量比例的流量而超时。

七、常见误区

  1. 给缓存用取模哈希:本文实测 75% 迁移率就是答案。任何『键到节点』的映射在节点数会变化的前提下都应该用一致性哈希或其变体。
  2. 一致性哈希不加虚拟节点:实测均匀度 51.7% 的偏斜足以让热点节点先挂、然后雪崩。虚拟节点数量是均匀度与内存的权衡,100~200 是经验甜区。
  3. 把负载均衡当万能容错:算法解决的是『分给谁』,健康检查解决的是『谁还活着』——没有快速准确的健康检查与熔断,再聪明的算法也会把流量送进故障节点。
  4. 忽视权重与真实容量脱节:异构集群里权重拍脑袋设置,不如用实际压测的容量比例;容器混部时还要考虑宿主机的噪声邻居。
  5. 把会话保持当架构依赖:依赖 ip_hash 维持登录态的系统无法优雅扩缩容,会话外置是有状态服务改造的第一步。

八、参考文献

  • Karger, D. et al. Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web. STOC 1997.
  • Eisenbud, D. et al. Maglev: A Fast and Reliable Software Network Load Balancer. NSDI 2016.
  • Lamping, J., Veach, E. A Fast, Minimal Memory, Consistent Hash Algorithm (Jump Hash). arXiv:1406.2294, 2014.
  • Mitzenmacher, M. The Power of Two Choices in Randomized Load Balancing. IEEE TPDS 12(10), 2001.
  • Nginx 官方文档: Module ngx_http_upstream_module (load balancing methods). https://nginx.org/en/docs/http/ngx_http_upstream_module.html