RPC场景下理解一致性哈希负载均衡原理

总结摘要
简单来说就是把客户端、服务端的名字各哈希成一个32位整数,然后把他们都放到[0, 2^32-1]这个圆环上,然后客户端从他们各自的位置顺时针行走,他们各自碰到的第一个服务端,就是他们的请求处理方

用RPC框架的实际场景,从头到尾给你讲清楚一致性哈希的原理。

先看传统方式的问题

假设你有4台服务器(Server1、Server2、Server3、Server4),处理用户请求:

传统做法:取模算法

1
2
3
4
5
6
7
// 比如有100个用户请求
用户1: hash("用户1") = 100  100 % 4 = 0  Server1
用户2: hash("用户2") = 101  101 % 4 = 1  Server2
用户3: hash("用户3") = 102  102 % 4 = 2  Server3
用户4: hash("用户4") = 103  103 % 4 = 3  Server4
用户5: hash("用户5") = 104  104 % 4 = 0  Server1
...以此类推

看起来没问题对吧?

但是,当服务器发生变化时:

场景:Server4宕机了

1
2
3
4
5
6
7
还剩3台服务器:Server1、Server2、Server3

用户1: 100 % 3 = 1 → Server2 (原来在Server1) ❌ 变了
用户2: 101 % 3 = 2 → Server3 (原来在Server2) ❌ 变了
用户3: 102 % 3 = 0 → Server1 (原来在Server3) ❌ 变了
用户4: 103 % 3 = 1 → Server2 (原来在Server4) ❌ 变了
用户5: 104 % 3 = 2 → Server3 (原来在Server1) ❌ 变了

结果: 所有用户的请求目标都变了!这意味着:

  • 原来的连接都要重新建立
  • 缓存全部失效(如果服务器有本地缓存)
  • 大量的数据需要迁移

一致性哈希的核心思想

想象一个圆环(哈希环)

1
2
3
4
5
        0
   ↑     ↓
  ← 圆环  →
   ↓     ↑
       2^32-1

这个环是一个首尾相连的圆,取值范围从0到2^32-1(约42亿)

步骤1:把服务器放到环上

把4台服务器通过哈希计算,放到环的各个位置:

1
2
3
4
Server1的哈希值 = 1000
Server2的哈希值 = 3000  
Server3的哈希值 = 6000
Server4的哈希值 = 9000

在环上的位置:

1
2
3
位置:0    1000    3000    6000    9000    2^32-1
      |-----|-------|-------|-------|-------|
服务器:   S1      S2      S3      S4

步骤2:把用户请求也放到环上

1
2
3
用户A的哈希值 = 2000
用户B的哈希值 = 5000  
用户C的哈希值 = 8000

步骤3:寻找服务器(关键!)

规则:沿着环顺时针走,遇到的第一个服务器就是目标

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
       0
   S1(1000) ← 用户D(500) 顺时针找,遇到S1
     ↙↘
    ↙  ↘
   ↙    ↘
S4(9000)  S2(3000) ← 用户A(2000) 顺时针找,遇到S2
    ↖    ↗
     ↖  ↗
      ↖↙
   S3(6000) ← 用户B(5000) 顺时针找,遇到S3
          ← 用户C(8000) 顺时针找,遇到S4

所以:

  • 用户A(2000) → 顺时针第一个服务器是S2(3000)
  • 用户B(5000) → 顺时针第一个服务器是S3(6000)
  • 用户C(8000) → 顺时针第一个服务器是S4(9000)
  • 用户D(500) → 顺时针找,从500到1000遇到S1

现在看服务器变化时的效果

场景:Server4宕机(移除S4)

原来:

1
2
3
4
S1(1000) ← 用户D(500)
S2(3000) ← 用户A(2000)
S3(6000) ← 用户B(5000)
S4(9000) ← 用户C(8000)

移除S4后:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
       0
   S1(1000) ← 用户D(500)
     ↙↘
    ↙  ↘
   ↙    ↘
 环底    S2(3000) ← 用户A(2000)
    ↖    ↗
     ↖  ↗
      ↖↙
   S3(6000) ← 用户B(5000)
          ← 用户C(8000) → 现在顺时针找,S4没了,继续走到S1

变化:

  • 用户A、B、D:没变(仍然路由到原来的服务器)
  • 用户C:从S4变成了S1(只有这一个用户变了!

关键优势:只有1/4的用户受到影响,其他3/4完全不变!

RPC框架中的实际应用

场景1:有状态的用户会话

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 假设每个服务器内存中保存了用户会话
Server1内存{用户D: 会话信息}
Server2内存{用户A: 会话信息}
Server3内存{用户B: 会话信息}
Server4内存{用户C: 会话信息}

// 当Server4宕机后
用户C的请求路由到了Server1

// 问题:Server1没有用户C的会话信息!
// 解决方案需要从共享存储加载或者让用户重新登录

场景2:本地缓存

1
2
3
4
5
6
7
8
9
// 每个服务器有本地缓存
Server1缓存{商品1: 数据, 商品5: 数据}
Server2缓存{商品2: 数据, 商品6: 数据}
Server3缓存{商品3: 数据, 商品7: 数据}
Server4缓存{商品4: 数据, 商品8: 数据}

// 当Server4宕机后
商品4的请求到了Server1  缓存未命中查数据库
但商品5的请求还在Server1  缓存命中 

虚拟节点解决什么问题

问题:服务器少时分布不均

假设只有2台服务器:

1
2
3
4
5
S1(1000)     S2(5000)
|------------|--------|
范围:S1负责0-1000和5000-2^32-1(很大)
      S2只负责1000-5000(很小)
负载严重不均!

解决方案:虚拟节点

把每台物理服务器变成多个虚拟节点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
物理服务器Server1拆成
- 虚拟节点1-1: hash("Server1#1") = 1000
- 虚拟节点1-2: hash("Server1#2") = 3500
- 虚拟节点1-3: hash("Server1#3") = 6000
- 虚拟节点1-4: hash("Server1#4") = 8500

物理服务器Server2拆成
- 虚拟节点2-1: hash("Server2#1") = 2000
- 虚拟节点2-2: hash("Server2#2") = 4500
- 虚拟节点2-3: hash("Server2#3") = 7000
- 虚拟节点2-4: hash("Server2#4") = 9500

环上分布:

1
2
位置: 1000 2000 3500 4500 6000 7000 8500 9500
节点: S1-1 S2-1 S1-2 S2-2 S1-3 S2-3 S1-4 S2-4

这样负载就均匀了,因为环上到处都是S1和S2的虚拟节点。

总结一句话

一致性哈希就是:把服务器和数据都放到一个环上,数据总是找顺时针碰到的第一个服务器。当服务器变化时,只影响它和上一个服务器之间的那部分数据。

用RPC的例子说:

  • 好处:服务器宕机时,只有1/N的请求需要重新路由
  • 妙处:用虚拟节点解决分布不均的问题
  • 本质:在负载均衡和状态保持之间找到了平衡