总结摘要
简单来说就是把客户端、服务端的名字各哈希成一个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的请求需要重新路由
- 妙处:用虚拟节点解决分布不均的问题
- 本质:在负载均衡和状态保持之间找到了平衡