IT 论文精读 · PAPER 74
Karger, Lehman, Leighton, Levine, Lewin & Panigrahy · MIT · STOC 1997
1997 年,MIT 一群做理论的人(Karger 等)为了治「网页突然爆红、服务器被挤垮」这个毛病,提出了一致性哈希——一套决定「这份东西该交给哪台机器」的规矩,好处是机器增减时几乎不用重新洗牌。第二年其中两位作者拿它创办了 Akamai;今天你刷到的视频、Amazon 的购物车、Cassandra 这类数据库,在决定「数据放哪台」时用的仍是同一个点子。
要把很多东西分给几台机器,最自然的办法是按余数分:把名字换算成一个数,除以机器台数,余几就归几号机器。十台机器时人人有份、也挺均匀。
麻烦出在机器数一变。加第十一台,除数从 10 变成 11,几乎每份数据的归属都跟着变了——全集群互相搬家、网卡打满,谁也没空干正事。可你明明只想多加一台机器而已。
一致性哈希换了个摆法:想象一个钟面,每台机器按自己的名字算出一个位置钉上去,每份数据也按自己的名字算出一个位置。规矩只有一句:这份数据归「从它的位置顺时针走、碰到的第一台机器」。
关键在于位置是各算各的:一份数据落在钟面哪儿只跟它自己的名字有关,跟一共有几台机器毫无关系。于是加一台机器等于往钟面上插进一个新钉子:它只从顺时针那位邻居手里接走「紧挨在自己前面那一小段」的数据,钟面其它地方一个字都不用改;掉一台机器同理,它那段顺延给下一位。所有变动都被关在一小段弧里。
还有个当年更要紧的好处:那时不同浏览器知道的缓存名单并不一样。按余数分,名单差一台答案就全变;钟面规矩下名单略有出入,绝大多数数据的落点仍然相同——大家不必先商量,就能分到同一台机器上。
只钉一个钉子有个副作用:钟面切出的弧有长有短,运气差的机器管一大段、运气好的只管一丁点,忙闲不均。
办法很土:让每台机器在钟面上撒几十上百个钉子(化出许多「分身」)。段数一多,长短互相抵消,各家管的总量就接近了;而且一台机器下线时,它那几十段活是被许多不同机器分头接走的,不会整个砸到某个倒霉邻居头上。
诚实一句:它摆平的是「数据份数」,不是「访问热度」——一条被全网疯抢的记录仍旧只归一台机器,照样能压垮它。
别按「除以机器台数取余数」分配——把机器和数据都按名字钉到同一个钟面上,数据归顺时针遇到的第一台机器。这样加减机器只波及一小段弧,搬家量从「几乎全部」降到「约等于一台机器的份」;再让每台机器多撒些钉子,忙闲就均了。
想看环的示意图、四条性质的定义和定理保证? → 切到精读版
这篇论文先把「怎样才算分配得好」形式化成四条性质(均衡、单调、分散度、负载),再给出满足它们的极简构造:把机器和数据用哈希映到同一个环上,每份数据归顺时针遇到的第一台机器——于是增删一台机器只需迁移约 1/n 的数据,且各客户端手上的机器名单不完全一致时,分配结果依然高度一致。另一半用「随机树」把单个热点的访问层层摊开。它后来成了 Akamai、Chord、Dynamo、Cassandra 乃至现代负载均衡器的共同零件。
hash mod n):最常见的分配法——把名字哈希成数,再对机器台数取余,余几归几号机器。O(log n)):只看增长快慢、忽略常数的粗略度量,O(log n) 即随规模涨得很慢。作者是 David Karger、Eric Lehman、Tom Leighton、Matthew Levine、Daniel Lewin、Rina Panigrahy,来自 MIT 理论组,论文题为《Consistent Hashing and Random Trees: Distributed Caching Protocols for Relieving Hot Spots on the World Wide Web》,发表于 STOC 1997。它接续 1990 年代中期「Web 缓存该怎么组织」的一串工作,回答的是万维网刚爆发时最现实的工程灾难——热点;1998 年 Leighton 与 Lewin 把这套理论创业落地成 Akamai(Lewin 于 2001 年 9 月 11 日在美国航空 11 号航班上遇难)。下游则几乎覆盖了后二十年的分布式存储(见「影响」一节)。
1990 年代中期的万维网有个要命现象:一个页面被门户首页一挂,请求量能在几分钟内涨几个数量级,源站直接被打死。自然的解法是在网络里摆一堆缓存分担,问题随即变成:一个页面该由哪台缓存负责?
最直接的答案是取模分桶:cache = hash(URL) mod n,即「把 URL 哈希成数、除以缓存台数取余」。它有两个硬伤。
第一,机器集合一变、全盘重排。n 从 10 变 11,绝大多数 URL 的归属都改了——全体缓存瞬间失效、请求一起回源,恰恰在扩容那一刻把源站打死。
第二,也是当年更棘手的:大家的名单对不上。缓存由不同机构运营、随时上下线,各浏览器知道的列表天然不一致;若各按自己名单的 n 取模,同一页面在不同客户端会算到不同缓存上——同一份内容在很多缓存里各存一份、每台都得回源取一次,空间与带宽双浪费。
还有第三件事取模根本没碰:页面本身就热。分配再均匀,一个疯传页面也只有一台缓存负责,照样被压垮——这是后半「随机树」的活。
key mod 4 改成 key mod 5,归属几乎全部重排——扩容一台就要全集群搬家。论文真正的贡献有一半在于它先把问题定义清楚了。它引入 ranged hash function(带视图的哈希函数):普通哈希只有一个固定映射,它却对每一种可能的视图 V(某客户端知道的那批桶)各给出一套「项 → 桶」的分配,并要求这族函数同时满足四条:
后两条正是冲着「视图不一致」去的:即使大家名单不同,结论也要大体一致。这就是「一致性哈希」里「一致」的真正含义——不是数据一致性,而是不同视图下分配结果的一致,这个常年被误解的点值得单独记住。
满足这四条的构造朴素得让人意外:用哈希把桶映到单位区间 [0,1)(首尾相接看成一个环),再把项也映到同一个环上。规则一句话:bucket(item) = 环上第一个 hash 值 ≥ hash(item) 的桶(到头则绕回开头)——就是「顺时针最近」。
单调性几乎是白送的:项的位置只由它自己的名字决定,与有几个桶无关;插入一个新桶只会把「紧邻它逆时针那一小段」里的项从原来的后继手上接过来,环上其余弧段完全不动,删桶同理。期望迁移量因此是:加入第 n+1 个桶时只有约 1/(n+1) 的项换手,且全部搬进新桶。
但只这样还不够均衡:n 个随机点把环切出的 n 段弧长短差得很多,最长那段可以是平均值的好几倍。论文的做法是把每个桶复制成 Θ(log C) 个点撒到环上(C 是可能出现的桶总数),一台机器于是在环上有许多「分身」,长弧短弧互相抵消,各机器分到的总弧长便集中到平均值附近。这个技巧后来被工业界叫做虚拟节点(virtual node / vnode),还带来两个红利:下线时它的多段弧被不同的机器分头接管(不砸单个邻居);容量不同的机器可多撒或少撒分身来加权。
在此之上论文证明(高概率成立,即出错概率随规模迅速趋近于零):均衡——每个桶拿到 O(1/|V|) 比例的项;单调——无条件成立(这是构造自带的性质,不靠概率);在「每个客户端至少看到全部桶的 1/t」的假设下,分散度与负载都被压在 O(t·log C) 量级。分量在于:一条查一次哈希就能算出答案、不需要任何协调的规则,同时拿到了这四条。
一致性哈希解决「谁负责」,没解决「负责不过来」。论文后半给每个页面配一棵随机树:把一批缓存当节点,用「页面名字 + 节点编号」哈希出一棵 d 叉树——页面不同,树的形状与成员就不同。请求这样走:客户端不直奔「那台负责的机器」,而是随机挑这棵树的一片叶子去问;叶子没有就问父节点,一路往上,只有树根也没有才回源站。妙处在两点:其一,每个节点只被自己的 d 个孩子问,扇入是常数,没有哪台机器直面全网;其二,节点第一次取到页面后就缓存住,后续再问直接命中,往上传的请求数被卡死。热点压力于是沿树迅速衰减,源站只挨到很少几次回源;又因为树按页面名字随机生成,不同热点的树根落在不同机器上,不会合力压垮同一台。论文证明:在其请求模型下任何机器承担的请求量高概率不超过其「公平份额」的对数级倍数。
两半是同一个信念的两面:不靠中心协调、不靠全局状态,只靠「大家算同一个哈希」就把分工定下来——环定空间上的分工,树定压力上的分摊。这也是它能在 1997 年那种「谁也管不了谁」的互联网上落地的原因。
先说清楚:这是一篇理论论文(STOC),结论是定理而非实验数字——它没跑系统评测,所以没有可引用的吞吐 / 延迟数据。它给出的保证是:
1/(n+1) 的项换手(取模分桶几乎要重排全部);单调性无条件成立。Θ(log C) 份后,高概率下各桶项数落在平均值的常数倍内。O(t·log C) 量级——同一页面在全网只被少数几台缓存各存一份。真正的「实验」是后来的工业验证:Akamai 用它撑起全球 CDN,Dynamo、Cassandra 把它当默认放置方式沿用至今。
它把一件人人都在做、却做得很糙的事(数据分片)变成了有定义、有保证的原语,于是成了一整代系统的共同零件——Chord 等 DHT 的环、Dynamo 的数据放置、Cassandra 的 token 环、memcached 的 ketama、七层负载均衡器挑后端的逻辑,底下都是这个环。更深一层的贡献是「视图不一致也能一致分配」——这正是无中心系统能工作的前提,也是后续二十年「如何更均衡、更省内存地分片」这条研究线的起点。
① 一句话:把机器和数据哈希到同一个环上、数据归顺时针遇到的第一台机器,增删机器只波及一段弧。
② 痛点:hash(key) mod n 在 n 变化时几乎全部重排;且各客户端机器名单不一致时,算出的归属互相冲突。
③ 真贡献是先把「分得好」定义出来:均衡、单调、分散度、负载——后两条专治「视图不一致」,这才是「一致性」三字的原意(不是数据一致性)。
④ 构造极简:桶与项各自哈希到 [0,1) 环上,顺时针最近即归属;单调性是构造自带的,加入第 n+1 个桶期望只搬 1/(n+1) 的数据、且全部搬进新桶。
⑤ 虚拟节点:每台机器撒 Θ(log C) 个分身才谈得上均衡,副产品是下线时由多台分头接管、可按容量加权。
⑥ 随机树:按页面名字随机生成一棵缓存树,扇入恒定、命中即止,把单个热点层层摊薄——这半篇基本被遗忘。
⑦ 影响:Akamai(两位作者创办)、Chord、Dynamo、Cassandra、ketama、七层负载均衡的共同地基。
⑧ 局限:只均键数不均热度;虚拟节点的元数据成本;不表达机架 / 可用区约束;rendezvous、jump、Maglev 等替代各有取舍。