故事
岭南有个盐场,分作七灶,每灶管着一口盐井、一堆盐工。七灶各自熬盐,各自记账,月底把盐包和账册一起运到总场,由总场核对数目、统一定价。这法子稳妥,却慢——月底七天,总场灯火通明,七灶的账册堆成小山,算盘声能传到海里去。
后来总场派了个巡盐官,姓周,常驻第三灶。周官人有个怪脾气:每到月初,他亲自骑马跑遍七灶,在每灶的账房门口钉一块木牌,上写"本月第三灶主计"。钉完牌子,他回到第三灶,把自家灶里的账册一锁,对外宣称:本月我只认第三灶的数,其余六灶的账册我不看,月底也不对。
盐商们慌了。第三灶的盐价万一报低了,周官人又不核对,岂不是要亏?周官人说不妨——木牌钉下的那一刻,七灶都看见了,这个月第三灶的价就是官价,谁改谁犯法。他手里握着总场的印,印在他手里,第三灶的账就是总场的账;印若被抢走,他自然钉不住那块牌子。
这规矩实行了三年,月底对账的七天省掉了。盐商们月初跑一趟第三灶,问个价,整月不变。有人偷偷去第五灶询价,第五灶的掌柜指着门口的木牌说:"周官人的牌子还在,我这本账写了也不算数,你看了白看。"
第三年冬天,周官人骑马去第四灶钉牌子,路上遇了山匪,马惊了,把他摔进溪涧,冻了半宿才爬上来。他拖着湿身子赶到第四灶,比往年晚了两个时辰。就这两个时辰里,第四灶的掌柜以为周官人死了,自己翻出旧账册,按上月的价卖了一船盐给过路商队。周官人赶到时,木牌一钉,宣布第四灶本月主计,可那一船盐已经离岸,价是旧价,收不回来。
总场为此吵了半个月。有人主张废掉周官人的规矩,恢复月底对账;有人主张给周官人多配两匹马、两个护卫。最后定下的补丁很细:周官人每月钉牌子的同时,要在总场的钟楼里挂一盏灯笼,灯笼亮着,牌子才作数;灯笼若灭,各灶自动恢复月底对账,直到新灯笼亮起。
灯笼是周官人亲手点的,每月初一点,月底灭。若他中途死了、病了、被匪劫了,灯笼无人续油,自然熄灭,各灶不会无限期地等下去。那盏灯笼,后来被人叫做"独占期"——亮着的时候,第三灶的账就是总场的账,不必再问其余六灶;灭了,一切回到老规矩。
—
周官人的木牌和灯笼,在分布式系统里叫做领导者租约(Leader Lease)。
Raft([✓#6 传灯人])和 Paxos 选出一个领导者之后,理论上每次读操作都要走一轮 Quorum——去问多数节点"现在是什么值",才能保证读到最新的。这在工程上代价极高:七灶的盐价,每次查询都要跑遍七灶,月底对账变成日日对账。领导者租约的思路是:让领导者"独占"一段明确的时间窗口,窗口期内它的本地状态就是权威状态,不必再征集多数确认。
租约的核心不是"信任领导者",而是信任时间——或者更准确地说,是信任一个各方都能观察到的、单调递增的时钟。周官人的灯笼就是这样一个时钟:所有节点(七灶)都看得见灯笼亮灭,亮的时候承认独占,灭的时候自动失效。这比"领导者活着吗"这种模糊判断要硬得多——节点不需要猜周官人的心跳,只需要看灯笼还剩多少油。
实现上,领导者向所有节点申请租约时,会带一个过期时间戳。节点收到后,在自己本地的时钟上记下"到 T 时刻为止,我认你是领导者"。如果领导者在 T 之前完成写操作并广播,所有节点按正常流程确认;读操作则可以直接向领导者发起,领导者不必再征集 Quorum——因为所有节点都已经预先承诺了在 T 之前不选新主。这和周官人的木牌是一个道理:牌子钉下,第五灶就算想改价,也得等灯笼灭了、新牌子钉了才行。
第四灶那船错卖的盐,对应的是租约边界上的 race condition。周官人摔进溪涧的两个时辰,就是领导者申请租约和网络延迟之间的空隙——第四灶以为旧租约已死、新租约未至,擅自做了决定。补丁是"灯笼自动熄灭",对应工程上的租约过期时间必须保守:宁可早灭,不可晚灭。早灭意味着偶尔多跑一轮 Quorum,晚灭意味着读到陈旧数据。
Spanner 的 TrueTime([✓#11 时辰簿] 的延伸)把租约的精度推到毫秒级:领导者申请的租约带上 TrueTime 的置信区间,所有节点用同样的置信区间判断过期。时钟误差被显式地算进租约长度里,周官人不再需要猜测"我的灯笼和第四灶的灯笼谁快谁慢"。
但租约不能无限长。灯笼总要灭,因为分布式系统里没有完美的心跳检测——周官人可能死了,马可能惊了,溪涧可能太深。租约的真正价值在于把"领导者是否还活着"这个模糊问题,转换成"租约是否过期"这个确定性问题。模糊问题需要猜测和超时,确定性问题只需要比较本地时钟。
有些系统把租约和心跳叠在一起:领导者定期续租,像周官人每月初一钉牌子;若连续两期不续,灯笼自动熄灭。这降低了第四灶那种"空窗期误判"的概率,但增加了复杂度——续租消息本身也可能丢失,节点需要处理"领导者续租了我没收到"的情况。最干净的实现是单轮广播租约+保守过期时间:领导者申请时一次性拿到足够长的独占期,期内不续租,到期自动失效,像钟楼的灯笼按月燃烧,不添油。
租约到期后的切换代价是可用性换延迟。灯笼灭了,各灶恢复月底对账,读操作变慢;灯笼亮着,读操作快,但领导者若真死了,系统要空等到灯笼燃尽才能选新主。这和 CAP([✓#4 两座钟楼])的取舍是同构的:分区时,租约系统要么拒绝读(等灯笼灭)、要么冒着读到旧数据的风险(假灯笼亮着)。没有免费午餐,只有明确的账单。
周官人后来老了,把规矩传给儿子。儿子比他谨慎,每月灯笼只点十五日,到期必灭,再亮再申请。盐商们抱怨读价太频繁,儿子说:十五日是我能担保的最长独占期,再长我就不敢担保自己不死。这话说穿了租约的本质——它不是权力的扩张,而是责任的边界。领导者租约期内做的承诺,系统必须兑现;租约期外的承诺,系统概不负责。灯笼的亮度,就是承诺的半径。
概念解析
领导者租约(Leader Lease) 是共识协议中让领导者在一段时间内独占权威、从而避免每次读操作都走 Quorum 确认的优化机制。
与心跳的区别。 心跳(heartbeat)是领导者周期性宣告"我还活着",节点被动接收后更新认知;租约是领导者一次性获得"到 T 时刻为止我有效"的集体承诺,节点主动在本地时钟上维护过期判断。心跳模糊、租约确定——节点不需要猜领导者死活,只需要比较本地时间。
实现要点。 领导者向所有节点广播租约请求,带过期时间戳 T;节点在 T 之前不发起新选举、不承认新写。读操作可直接向领导者发起,领导者本地返回即可。写操作仍需正常共识流程——租约优化的是读,不是写。
时钟依赖。 租约的正确性依赖各节点时钟的相对速率,不要求绝对同步,但要求时钟漂移有界。若节点时钟跑得太快,会提前认为租约过期;跑得太慢,会延迟发现领导者已死。TrueTime 这类区间时钟把漂移显式纳入租约长度计算,让边界可量化。
与 Fencing Token([✓#14 旧符新令])的关系。 Fencing 解决的是"旧领导者没死透、新领导者已上任"时的写冲突,靠单调递增的令牌让存储层拒绝过期写;租约解决的是"领导者活着时能不能少跑 Quorum"的读优化。两者常一起用:租约期内读免 Quorum,写带 Fencing Token 防边界冲突。
Spanner 的实践。 Spanner 的读写事务中,读操作在领导者租约期内直接本地执行, Paxos 的 Quorum 只用于写和租约续期。这让它在跨洲际部署中仍能保持较低的读延迟——代价是 TrueTime 的硬件投入和保守的租约长度。
"灯笼"的隐喻。 租约不是权力的授予,而是责任的边界。亮着的时候,领导者的本地状态就是系统状态;灭了,一切回到集体确认。亮度本身比领导者的生死更容易被各方独立验证——这是把模糊问题(活着吗)转换成确定性问题(过期了吗)的经典手法。