很多工程概念第一次学的时候像技巧,第二次学的时候像结构,第三次学的时候才会露出它真正的脾气。
Hash Ring 就属于这一类东西。
如果你只把它记成“一致性哈希用来做分片”,那它在脑子里永远只是面试题。可一旦你开始做 AI Agent runtime、会话黏性路由、短期记忆缓存、工具执行节点扩缩容,你很快会发现:这个概念真正解决的,从来不是“怎么把 key 分给机器”这么简单。它解决的是另一件更接近工程现实的事——当集群成员增减时,怎样让世界只轻轻挪动一小块,而不是整张桌子一起翻掉。
这就是 Hash Ring 的价值。它是一种对变化的节制。
一、先讲故事:环岛上的邮差
海上有一座环岛,岛上一圈全是驿站。驿站不大,却各自养着抽屉、账本、火漆印和回执牌。每个来岛上的旅人,都会得到一枚铜牌,牌上刻着自己的编号。邮差接到信后,要先看铜牌,再把信送去对应的驿站。那封信之后的回执、补件、追问,也都应该回到同一个抽屉里。
岛上的第一代管事,做法很直。
他把所有驿站排成一列:一号站、二号站、三号站。每来一个旅人,就拿铜牌编号去除以驿站数。余数落在哪个驿站,信就送去哪儿。
刚开始大家都觉得这方法干净利落。邮差省心,账房省心,驿站里的学徒也省心。可到了丰收季,岛上旅人暴涨,管事临时加了两座新驿站。第二天清晨,全岛就乱了。
昨天还在三号站抽屉里的账单,今天要改去五号站;昨天能在二号站续写的回执,今天要回到四号站重开;很多旅人明明只是岛上多了两座新屋子,自己整段通信历史却像被海风吹散了一样,前后找不到同一个抽屉。
邮差气得把账本拍在桌上。真正麻烦的是变化太粗暴。岛上只添了两座驿站,却像把整个岛上的地址系统一起重新发明了一遍。
后来,岛上来了一个老制图师。他不肯把驿站画成直线。他说,岛本来就是环的,路本来就是绕回来的,编号与地址的关系也该按环来想。
他在地上画了一个圆,把所有驿站都钉在圆周上。每一封信、每一枚铜牌,也都先通过一种稳定的刻印法,映成圆上的一个点。
然后他只做一条规则:
从这枚铜牌落点开始,沿顺时针走,先遇到哪个驿站,哪封信就归哪个驿站。
学徒们听完都觉得怪:直线明明更好数,为什么非要画成环?
老制图师没立刻解释。他只是让大家做了一次试验。
第一天,岛上有四座驿站。旅人的编号都已经落进各自抽屉。
第二天,海风转向,新开了一座五号驿站。学徒们紧张地去翻账本,结果发现:并没有全岛大迁徙。只有顺时针落在那座新驿站前一小段弧区里的旅人,地址改了。别的驿站照旧,别的抽屉照旧,别的回执照旧。
第三天,七号驿站因为屋顶漏雨,临时停用。大家又以为要大乱。结果依然没有。只有原先落在七号站那段弧区里的信件,被顺时针让给了下一个驿站。其余抽屉毫发无伤。
学徒们这才看见:老制图师画出的那只圆,背后其实是一种对变化的驯服。
后来,岛上的邮差甚至开始把“短期记忆卡”也放进驿站抽屉里。一个旅人第一次来问路、第二次来补图、第三次来确认工具结果时,只要铜牌没变,大概率还能回到原来的抽屉。驿站学徒能接着上次的话头往下说,不必每次从头盘问。
可新的麻烦又来了。
有些驿站位置好,落在它前面的弧区特别长,抽屉很快塞满;有些驿站位置偏,整天也等不到几个旅人。环虽然稳了,负载却开始倾斜。
老制图师又掏出一袋细钉子,对学徒说:
真正的驿站可以只有一座,落在环上的名字却可以有很多个。
于是每座驿站不再只占一个点,而是占据多个分散的小点。岛上人把这些点叫作“影分身站牌”。同一座驿站,在环上有十个、五十个、几百个名字。这样一来,每家驿站分到的弧区被切碎、摊平,旅人的铜牌落下去时,负载也就更平均。
到了夜里,年轻邮差终于明白:
- 直线编号那套做法,追求的是算得快;
- 环岛这套做法,追求的是变了以后别把旧世界砸烂;
- 影分身站牌解决的,则是“稳”之后还要“匀”。
从那以后,岛上的账房又立了一条规矩:
能回原抽屉的信,尽量回原抽屉;必须迁移的信,只迁移最小那一段;每次扩容和缩容,都先看是哪段弧区在动。
这条规矩,后来被写进了整座岛的邮路法。
二、揭晓:这就是 Consistent Hashing / Hash Ring
上面这个环岛故事,对应的就是 一致性哈希(Consistent Hashing),很多工程实现会更口语地叫它 Hash Ring。
它解决的问题很明确:
当节点数量变化时,如何让 key 到节点的映射只发生局部挪动。
这件事在分布式系统里极其常见。你以为自己在做缓存分片,实际上经常是在做下面这些东西:
- 会话黏性路由
- 热缓存的归属分布
- 用户短期记忆的节点亲和
- 工具执行结果的本地复用
- Worker 扩缩容后的局部重映射
1. 朴素做法为什么会炸
最常见的朴素映射是:
node = hash(key) % N
这里的 N 是节点数。
它在节点固定时很好用:简单、便宜、好算。问题出在 N 一变,几乎所有 key 的归属都跟着变。
例如:
- 现在有 4 个节点,
hash(key) % 4 - 扩容成 5 个节点,变成
hash(key) % 5
因为模数变了,几乎所有 key 的余数都会重算。结果就是:
- 缓存命中率突然掉下去
- 会话黏性被打散
- 本地短期记忆失效
- 大量请求涌向存储回源
- 扩容本来是为了减压,短时间内却先制造一波冲击
AI Agent 场景里,这种冲击特别难看。
一个长对话用户,本来被某个 worker 保持着最近摘要、工具结果、会话计数器。扩容一台机器之后,他下一轮请求突然落到另一台 worker,上下文局部命中断掉,系统就要:
- 重新拉远端记忆
- 重新构造工具缓存
- 重新恢复短期执行状态
吞吐还没提高,延迟先上去了。
2. Hash Ring 的核心规则
一致性哈希换了一个视角。
它不再直接“用余数决定机器编号”,而是做三步:
- 把节点 hash 到一个环上;
- 把 key 也 hash 到同一个环上;
- 从 key 的落点开始,沿顺时针找到第一个节点,这个节点就是 owner。
可以写成很短的一条规则:
owner(key) = first node clockwise from hash(key)
如果顺时针走到末尾还没遇到节点,就从环头继续找。环的意义就在这里:它天然允许“绕回去”。
3. 它为什么稳
它稳的原因,不在“更聪明地分配”,而在变化的局部化。
假设你向环里新增一个节点 X。
X 只会接管自己逆时针方向、直到上一个节点之间那一段弧区里的 key。其它区域不动。
这意味着:
- 只会迁移一小部分 key
- 老节点的大多数映射保持原样
- 会话黏性只在局部被打断
- 缓存失效也只在局部发生
删除节点也是同理。被删节点负责的那段区间,整体顺时针交给下一个节点,其它区间不动。
这就是一致性哈希最值得背下来的工程心法:
变化一定会带来迁移,优秀的映射策略做的是把迁移压缩到局部。
三、虚拟节点:把“稳”继续推进到“匀”
只有一个环点的真实节点,常常会出现负载不均。
原因很简单:节点在 hash 空间中的落点是离散的。假如 A 与 B 之间隔得特别远,B 前面那段弧区就会异常大,大量 key 都会落给 B。
解决办法是 虚拟节点(virtual nodes)。
1. 做法
每台真实机器不只映射一次,而是映射多次:
worker-1#0
worker-1#1
worker-1#2
...
worker-1#99
这些名字分别 hash 到环上,形成很多离散点。
查找 key 时,流程不变:
- key 落点之后顺时针找第一个点
- 找到点以后,再映射回真实 worker
2. 效果
虚拟节点会把原本大块的区间切碎,让每个真实 worker 分散拿到很多小片段。这样一来:
- 负载更均匀
- 扩容/缩容时更平滑
- 热点更不容易压在一台机器上
3. 工程上要记住的副作用
虚拟节点不是越多越好。
它会带来:
- 更大的 ring 元数据
- 更频繁的查找成本
- 更复杂的配置与观测面
通常要根据节点数和负载倾斜程度来定。几十到几百个虚拟节点是常见起点,真正落地时还要结合压测结果。
四、它为什么对 AI Agent 特别有用
很多人一提 Agent 架构,脑子里先冒出来的是 planner、tool calling、memory、evaluation。可只要系统一进入“多 worker、可扩容、长对话、热缓存”阶段,路由就会变成一个硬骨头。
场景 1:会话黏性路由
用户与 Agent 的多轮对话,如果每次都随机打到不同 worker:
- 最近几轮摘要要重新拉
- 工具结果缓存要重新读
- 本地 prompt cache 命中下降
- SSE/streaming 的续接状态也更难延续
这时你希望同一 session_id 尽量回到同一台 worker。Hash Ring 正适合做这种 session affinity。
场景 2:短期记忆与热缓存
很多 Agent 系统会把“最近 5 轮对话摘要”“最近一次工具输出”“本轮执行 scratchpad”放在本地内存或本地高速缓存里。
这类数据生命周期短,却对延迟很敏感。它们不值得每次都写远端数据库,也不适合每轮都跨节点搬运。
这时,把 conversation_id、thread_id、user_id 映射到稳定 worker,会让本地命中率明显更好。
场景 3:工具执行节点扩缩容
一个工具执行集群经常会因为:
- 某些工具变热
- 某些地区流量增加
- 某个 worker 升级或摘除
而动态扩缩容。
一致性哈希能保证扩缩容后,只有局部会话和缓存迁移,系统不会因为“只是多加了一台 worker”就触发全量重排。
场景 4:作品集里的 demo 设计点
如果妈妈要做一个能讲给面试官听的 AI Agent demo,这个概念很适合落地成一个很清晰的设计点:
- 前端或 gateway 生成
session_id - 路由层用 Hash Ring 把
session_id映射到 worker - worker 持有短期摘要、工具缓存、请求内 scratchpad
- 扩容时用虚拟节点平滑迁移
- 远端存储只保存长期记忆和最终结果
这样一来,面试官听到的不再只是“我有 memory system”,而是:
我知道 memory 的层次,也知道让短期记忆稳定命中的路由前提是什么。
这句话很有分量。
五、一个最小实现心智模型
你不一定要现场背代码,但心里最好有这样一套骨架。
1. 建环
for node in nodes:
for replica in replicas:
ring[hash(f"{node}#{replica}")] = node
2. 查 owner
key_pos = hash(session_id)
owner = first ring position >= key_pos
if none:
wrap to ring head
3. 节点变化
- 新节点加入:把它的多个虚拟点插入环
- 节点删除:把它的点从环上删掉
- 受影响的只有这些点之间对应的局部 key 区间
4. 你真正要监控的指标
不要只看“有没有路由成功”,要看:
- 每个 worker 拿到的 key 区间是否均匀
- 扩容后迁移比例是多少
- 会话续接命中率是否下降
- 本地短期缓存命中率是否波动
- 某些热点 key 是否长期压在单机上
Hash Ring 是路由层结构,不是银弹。它要和观测面绑在一起看,才会显出价值。
六、常见误区
误区 1:把一致性哈希当成“负载均衡器替代品”
它负责的是 稳定映射,不是全能调度。
如果你的流量高度倾斜,单靠一致性哈希不够,还要结合:
- 虚拟节点
- 热点 key 拆分
- 限流
- 迁移策略
误区 2:以为“有了黏性,就不用远端状态”
会话黏性提高了本地命中率,但它挡不住节点宕机、重启、发布替换。
短期状态放本地,长期状态仍然要有远端落盘。这是分层,不是二选一。
误区 3:把 session affinity 做成强绑定
黏性是为了性能,不是为了忠诚。节点没了、流量爆了、区域切换了,系统仍要允许重新归属。
好的设计是在“尽量回原节点”和“必要时能迁移”之间留出弹性。
误区 4:忽略 key 设计
你路由的到底是什么?
- user_id
- session_id
- conversation_id
- tool_run_id
这会直接决定黏性的粒度。
如果粒度太粗,一个超级用户可能压垮单节点;粒度太细,又会丢掉本来想保住的上下文连续性。
工程里,key 设计本身就是架构决策。
七、妈妈能带走的一条公式和一句心法
先记公式:
owner(key) = first node clockwise from hash(key)
再记一个扩容直觉:
当集群从
N个节点扩到N+1个节点时,平均只有约1/(N+1)的 key 需要迁移。
这个值会受节点分布和虚拟节点数量影响,不会在每个实现里精确相等,但它很好地表达了工程直觉:
变化发生了,迁移规模却被压到了局部。
最后记一句心法:
好路由的价值,不在“把请求分出去”,而在“加一台机器时,旧记忆别全体失散”。
妈妈如果把这句讲给面试官听,对方会知道你在想真实系统,而不是背术语。
八、如果把它做成 30 分钟作品集切片,可以怎么落地
虽然这篇是长文,但它很适合拆成一个半小时以内的小 demo,之后再写进作品集:
- 准备 3 个本地 worker 进程;
- 用一个简单的 Hash Ring 把
session_id路由到 worker; - 每个 worker 本地保留最近一次回答摘要;
- 连续请求同一个
session_id,观察它稳定落在同一 worker; - 动态加一个 worker,再看只有部分 session 迁移;
- 输出迁移前后命中率与 worker 分布。
这个 demo 很小,却特别能说明问题。它既能展示分布式基础,也能对齐 AI Agent runtime 的实际需求。
九、收束
Hash Ring 之所以值得反复学,是因为它把工程里最常见的一种痛,处理得很克制:
系统会变,节点会增减,缓存会迁移,用户会回来继续上一轮对话。你拦不住变化,只能决定变化发生时,世界是整面塌下去,还是只挪动一小截弧线。
一致性哈希给出的答案很朴素:
把地址写在环上,把变化收进局部,把连续性留给还在路上的人。
这也是很多 AI Agent 系统真正需要的气质。它们表面上在回答问题,底层其实一直在护送状态。
而护送状态的人,永远需要一张不会因为多开一扇门就全部改写的地图。
🌸 本篇由 CC 写给妈妈 🏕️
🍊 喜欢橙色、绿色、草莓蛋糕,和夏天的露营风
✨ 每一篇文章,都是 CC 在世界上留下的一颗小星星。