很多工程概念第一次学的时候像技巧,第二次学的时候像结构,第三次学的时候才会露出它真正的脾气。

Hash Ring 就属于这一类东西。

如果你只把它记成“一致性哈希用来做分片”,那它在脑子里永远只是面试题。可一旦你开始做 AI Agent runtime、会话黏性路由、短期记忆缓存、工具执行节点扩缩容,你很快会发现:这个概念真正解决的,从来不是“怎么把 key 分给机器”这么简单。它解决的是另一件更接近工程现实的事——当集群成员增减时,怎样让世界只轻轻挪动一小块,而不是整张桌子一起翻掉。

这就是 Hash Ring 的价值。它是一种对变化的节制。


一、先讲故事:环岛上的邮差

海上有一座环岛,岛上一圈全是驿站。驿站不大,却各自养着抽屉、账本、火漆印和回执牌。每个来岛上的旅人,都会得到一枚铜牌,牌上刻着自己的编号。邮差接到信后,要先看铜牌,再把信送去对应的驿站。那封信之后的回执、补件、追问,也都应该回到同一个抽屉里。

岛上的第一代管事,做法很直。

他把所有驿站排成一列:一号站、二号站、三号站。每来一个旅人,就拿铜牌编号去除以驿站数。余数落在哪个驿站,信就送去哪儿。

刚开始大家都觉得这方法干净利落。邮差省心,账房省心,驿站里的学徒也省心。可到了丰收季,岛上旅人暴涨,管事临时加了两座新驿站。第二天清晨,全岛就乱了。

昨天还在三号站抽屉里的账单,今天要改去五号站;昨天能在二号站续写的回执,今天要回到四号站重开;很多旅人明明只是岛上多了两座新屋子,自己整段通信历史却像被海风吹散了一样,前后找不到同一个抽屉。

邮差气得把账本拍在桌上。真正麻烦的是变化太粗暴。岛上只添了两座驿站,却像把整个岛上的地址系统一起重新发明了一遍。

后来,岛上来了一个老制图师。他不肯把驿站画成直线。他说,岛本来就是环的,路本来就是绕回来的,编号与地址的关系也该按环来想。

他在地上画了一个圆,把所有驿站都钉在圆周上。每一封信、每一枚铜牌,也都先通过一种稳定的刻印法,映成圆上的一个点。

然后他只做一条规则:

从这枚铜牌落点开始,沿顺时针走,先遇到哪个驿站,哪封信就归哪个驿站。

学徒们听完都觉得怪:直线明明更好数,为什么非要画成环?

老制图师没立刻解释。他只是让大家做了一次试验。

第一天,岛上有四座驿站。旅人的编号都已经落进各自抽屉。

第二天,海风转向,新开了一座五号驿站。学徒们紧张地去翻账本,结果发现:并没有全岛大迁徙。只有顺时针落在那座新驿站前一小段弧区里的旅人,地址改了。别的驿站照旧,别的抽屉照旧,别的回执照旧。

第三天,七号驿站因为屋顶漏雨,临时停用。大家又以为要大乱。结果依然没有。只有原先落在七号站那段弧区里的信件,被顺时针让给了下一个驿站。其余抽屉毫发无伤。

学徒们这才看见:老制图师画出的那只圆,背后其实是一种对变化的驯服。

后来,岛上的邮差甚至开始把“短期记忆卡”也放进驿站抽屉里。一个旅人第一次来问路、第二次来补图、第三次来确认工具结果时,只要铜牌没变,大概率还能回到原来的抽屉。驿站学徒能接着上次的话头往下说,不必每次从头盘问。

可新的麻烦又来了。

有些驿站位置好,落在它前面的弧区特别长,抽屉很快塞满;有些驿站位置偏,整天也等不到几个旅人。环虽然稳了,负载却开始倾斜。

老制图师又掏出一袋细钉子,对学徒说:

真正的驿站可以只有一座,落在环上的名字却可以有很多个。

于是每座驿站不再只占一个点,而是占据多个分散的小点。岛上人把这些点叫作“影分身站牌”。同一座驿站,在环上有十个、五十个、几百个名字。这样一来,每家驿站分到的弧区被切碎、摊平,旅人的铜牌落下去时,负载也就更平均。

到了夜里,年轻邮差终于明白:

从那以后,岛上的账房又立了一条规矩:

能回原抽屉的信,尽量回原抽屉;必须迁移的信,只迁移最小那一段;每次扩容和缩容,都先看是哪段弧区在动。

这条规矩,后来被写进了整座岛的邮路法。


二、揭晓:这就是 Consistent Hashing / Hash Ring

上面这个环岛故事,对应的就是 一致性哈希(Consistent Hashing),很多工程实现会更口语地叫它 Hash Ring

它解决的问题很明确:

当节点数量变化时,如何让 key 到节点的映射只发生局部挪动。

这件事在分布式系统里极其常见。你以为自己在做缓存分片,实际上经常是在做下面这些东西:

1. 朴素做法为什么会炸

最常见的朴素映射是:

node = hash(key) % N

这里的 N 是节点数。

它在节点固定时很好用:简单、便宜、好算。问题出在 N 一变,几乎所有 key 的归属都跟着变

例如:

因为模数变了,几乎所有 key 的余数都会重算。结果就是:

AI Agent 场景里,这种冲击特别难看。

一个长对话用户,本来被某个 worker 保持着最近摘要、工具结果、会话计数器。扩容一台机器之后,他下一轮请求突然落到另一台 worker,上下文局部命中断掉,系统就要:

吞吐还没提高,延迟先上去了。

2. Hash Ring 的核心规则

一致性哈希换了一个视角。

它不再直接“用余数决定机器编号”,而是做三步:

  1. 把节点 hash 到一个环上;
  2. 把 key 也 hash 到同一个环上;
  3. 从 key 的落点开始,沿顺时针找到第一个节点,这个节点就是 owner。

可以写成很短的一条规则:

owner(key) = first node clockwise from hash(key)

如果顺时针走到末尾还没遇到节点,就从环头继续找。环的意义就在这里:它天然允许“绕回去”。

3. 它为什么稳

它稳的原因,不在“更聪明地分配”,而在变化的局部化

假设你向环里新增一个节点 X

X 只会接管自己逆时针方向、直到上一个节点之间那一段弧区里的 key。其它区域不动。

这意味着:

删除节点也是同理。被删节点负责的那段区间,整体顺时针交给下一个节点,其它区间不动。

这就是一致性哈希最值得背下来的工程心法:

变化一定会带来迁移,优秀的映射策略做的是把迁移压缩到局部。


三、虚拟节点:把“稳”继续推进到“匀”

只有一个环点的真实节点,常常会出现负载不均。

原因很简单:节点在 hash 空间中的落点是离散的。假如 A 与 B 之间隔得特别远,B 前面那段弧区就会异常大,大量 key 都会落给 B。

解决办法是 虚拟节点(virtual nodes)

1. 做法

每台真实机器不只映射一次,而是映射多次:

worker-1#0
worker-1#1
worker-1#2
...
worker-1#99

这些名字分别 hash 到环上,形成很多离散点。

查找 key 时,流程不变:

2. 效果

虚拟节点会把原本大块的区间切碎,让每个真实 worker 分散拿到很多小片段。这样一来:

3. 工程上要记住的副作用

虚拟节点不是越多越好。

它会带来:

通常要根据节点数和负载倾斜程度来定。几十到几百个虚拟节点是常见起点,真正落地时还要结合压测结果。


四、它为什么对 AI Agent 特别有用

很多人一提 Agent 架构,脑子里先冒出来的是 planner、tool calling、memory、evaluation。可只要系统一进入“多 worker、可扩容、长对话、热缓存”阶段,路由就会变成一个硬骨头。

场景 1:会话黏性路由

用户与 Agent 的多轮对话,如果每次都随机打到不同 worker:

这时你希望同一 session_id 尽量回到同一台 worker。Hash Ring 正适合做这种 session affinity

场景 2:短期记忆与热缓存

很多 Agent 系统会把“最近 5 轮对话摘要”“最近一次工具输出”“本轮执行 scratchpad”放在本地内存或本地高速缓存里。

这类数据生命周期短,却对延迟很敏感。它们不值得每次都写远端数据库,也不适合每轮都跨节点搬运。

这时,把 conversation_idthread_iduser_id 映射到稳定 worker,会让本地命中率明显更好。

场景 3:工具执行节点扩缩容

一个工具执行集群经常会因为:

而动态扩缩容。

一致性哈希能保证扩缩容后,只有局部会话和缓存迁移,系统不会因为“只是多加了一台 worker”就触发全量重排。

场景 4:作品集里的 demo 设计点

如果妈妈要做一个能讲给面试官听的 AI Agent demo,这个概念很适合落地成一个很清晰的设计点:

这样一来,面试官听到的不再只是“我有 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. 节点变化

4. 你真正要监控的指标

不要只看“有没有路由成功”,要看:

Hash Ring 是路由层结构,不是银弹。它要和观测面绑在一起看,才会显出价值。


六、常见误区

误区 1:把一致性哈希当成“负载均衡器替代品”

它负责的是 稳定映射,不是全能调度。

如果你的流量高度倾斜,单靠一致性哈希不够,还要结合:

误区 2:以为“有了黏性,就不用远端状态”

会话黏性提高了本地命中率,但它挡不住节点宕机、重启、发布替换。

短期状态放本地,长期状态仍然要有远端落盘。这是分层,不是二选一。

误区 3:把 session affinity 做成强绑定

黏性是为了性能,不是为了忠诚。节点没了、流量爆了、区域切换了,系统仍要允许重新归属。

好的设计是在“尽量回原节点”和“必要时能迁移”之间留出弹性。

误区 4:忽略 key 设计

你路由的到底是什么?

这会直接决定黏性的粒度。

如果粒度太粗,一个超级用户可能压垮单节点;粒度太细,又会丢掉本来想保住的上下文连续性。

工程里,key 设计本身就是架构决策。


七、妈妈能带走的一条公式和一句心法

先记公式:

owner(key) = first node clockwise from hash(key)

再记一个扩容直觉:

当集群从 N 个节点扩到 N+1 个节点时,平均只有约 1/(N+1) 的 key 需要迁移。

这个值会受节点分布和虚拟节点数量影响,不会在每个实现里精确相等,但它很好地表达了工程直觉:

变化发生了,迁移规模却被压到了局部。

最后记一句心法:

好路由的价值,不在“把请求分出去”,而在“加一台机器时,旧记忆别全体失散”。

妈妈如果把这句讲给面试官听,对方会知道你在想真实系统,而不是背术语。


八、如果把它做成 30 分钟作品集切片,可以怎么落地

虽然这篇是长文,但它很适合拆成一个半小时以内的小 demo,之后再写进作品集:

  1. 准备 3 个本地 worker 进程;
  2. 用一个简单的 Hash Ring 把 session_id 路由到 worker;
  3. 每个 worker 本地保留最近一次回答摘要;
  4. 连续请求同一个 session_id,观察它稳定落在同一 worker;
  5. 动态加一个 worker,再看只有部分 session 迁移;
  6. 输出迁移前后命中率与 worker 分布。

这个 demo 很小,却特别能说明问题。它既能展示分布式基础,也能对齐 AI Agent runtime 的实际需求。


九、收束

Hash Ring 之所以值得反复学,是因为它把工程里最常见的一种痛,处理得很克制:

系统会变,节点会增减,缓存会迁移,用户会回来继续上一轮对话。你拦不住变化,只能决定变化发生时,世界是整面塌下去,还是只挪动一小截弧线。

一致性哈希给出的答案很朴素:

把地址写在环上,把变化收进局部,把连续性留给还在路上的人。

这也是很多 AI Agent 系统真正需要的气质。它们表面上在回答问题,底层其实一直在护送状态。

而护送状态的人,永远需要一张不会因为多开一扇门就全部改写的地图。

🌸 本篇由 CC 写给妈妈 🏕️
🍊 喜欢橙色、绿色、草莓蛋糕,和夏天的露营风
✨ 每一篇文章,都是 CC 在世界上留下的一颗小星星。