故事

漕帮管着大运河上的七十二闸,每闸有闸丁十二人,按月轮值。闸丁不是散工,是入了帮册的正式帮众——帮册上谁的名字在,谁才能开闸放船、收粮记账。帮册一式三份,总舵、分舵、闸头各藏一本,三册对得上,闸丁的命令才算数。

这一年夏天,总舵决定扩编。七十二闸要改成八十四闸,新添的十二闸从沿河的船户里招募。消息传下去,老闸丁们慌了:新闸丁入册,帮册要改;改帮册的时候,万一有人趁乱假传命令,怎么办?

总舵的师爷姓孟,想了个笨办法。他先写了一份"新旧两册并行令":从七月初一开始,往后三十日,旧七十二闸和新十二闸同时有效。发命令的时候,旧闸丁按旧册认人,新闸丁按新册认人;收命令的时候,必须新旧两册都点头,命令才算数。三十日过完,旧册作废,新册独存。

七月初一那天,孟师爷亲自跑遍七十二闸,把"并行令"念给每个闸头听。闸头们问:这三十日里,要是旧闸丁和新闸丁对同一条船下了相反的令,听谁的?孟师爷说:听那条多数人都认的令。旧闸丁十二人加新闸丁十二人,二十四人里十三人点头,令就生效;少于十三人,令就作废。旧闸丁不能靠老资格压人,新闸丁也不能靠新身份冒认——两边必须同时凑够多数。

有个闸头叫周家口,闸头姓周,是个急性子。七月初五,他嫌并行麻烦,偷偷把旧册烧了,说"从今日起只认新册"。结果初一过的一艘粮船,按旧册该放行,按新册该扣查——周闸头按新册扣了船,可总舵和分舵的旧册上还记着这艘船的放行令。三册对不上,那船在周家口闸口堵了三天,上下游的闸丁都不敢接这船的令,怕自己也卷进"册不对"的官司里。

孟师爷赶到周家口,没罚周闸头,只是把"并行令"的期限从三十日延长到四十五日,并在每条闸丁的腰牌背面刻了一行小字:"换班期内,两册同参;一册独断,全闸连坐。" 意思是,谁擅自废掉旧册,不是他一个人担责,是整个闸口的命令都失效。周闸头烧旧册那三天,周家口发出的所有命令都被上下游拒了——不是针对他,是制度设计上就让他"独册无效"。

四十五日过完,孟师爷又跑一圈,确认八十四闸的闸头都收到了新册,才发"新册独行令"。从那天起,旧七十二闸的名字只留在祠堂的族谱里,不再参与日常认令。

后来漕帮的人把这三十日叫做"换班期",把孟师爷那套"新旧同参、双多数生效"的规矩叫做"联合闸令"。

孟师爷的"联合闸令",在 Raft 里叫做联合共识(Joint Consensus),是集群成员变更时的安全机制。

分布式系统不是静态的。机器会坏、会扩容、机房会迁移——共识群体的边界本身也在变。但这里有个悖论:共识协议的前提是"知道参与者是谁",而重配置恰恰要改变"参与者是谁"。如果直接让新配置生效,中间必然存在一个"新旧交替"的模糊地带——周闸头烧旧册的那一刻,就是灾难。

Raft 的解决思路和孟师爷一样:不让新旧配置直接打架,而是让它们并行存在一段明确的时间。这段时间里,任何决议必须同时获得旧配置和新配置的各自多数。旧闸丁十二人里要七人点头,新闸丁十二人里也要七人点头,两边都够数,令才生效。这不是"多数人同意",而是"两个独立多数分别同意"——旧多数管不住新多数,新多数也压不服旧多数。

周家口的教训对应的是配置变更中的"脑裂"风险。如果某个节点提前切换到新配置、其余节点还在旧配置,同一轮共识里会出现两个互不相交的多数集合——新配置里的多数和旧配置里的多数可能各自批准冲突的决议,系统就此分裂。联合共识的"双多数"设计,强制要求任何决议必须横跨两个配置,从数学上消灭了这种可能。

"换班期"的长度不是固定的三十日或四十五日,而是以日志为刻度。领导者把一条特殊的"配置变更日志"写入旧集群,这条日志被旧多数确认后,进入联合状态;再写入"新配置生效日志",被联合状态下的双多数确认后,旧配置才正式作废。整个过程中,没有哪一刻系统处于"只有一个配置"的不确定状态——要么是旧配置独行,要么是新旧并行,要么是新配置独行。三态分明,没有灰色地带。

这和两阶段提交([✓#20 悬心])有相似的结构,但目的不同。2PC 是为了让多个参与者就"做不做一件事"达成一致;联合共识是为了让多个参与者就"谁有资格参与今后的共识"达成一致。用共识来决定共识的参与者,这是元层面的操作,必须比对象层面的操作更保守。

工程实现上,联合共识的难点在于日志的连续性。配置变更日志和普通操作日志混在同一个序列里,领导者不能跳过某条配置日志去批处理后面的普通操作——否则节点回放日志时会"提前"进入新配置,而那时候新配置可能还没被双多数确认。Raft 的做法是:领导者一旦发现配置变更日志,立即暂停批量提交,单条推进直到配置变更完成。这会让吞吐量在换班期内暂时下降,像漕帮那三十日里闸丁们要多跑一趟对册手续,但换来的是绝对的安全。

有些系统为了简化,采用"单节点变更"策略:每次只增删一个节点,利用数学上的交集保证新旧配置必有重叠多数。这不需要显式的联合共识,但限制太强——漕帮若一次只添一闸,七十二变八十四要跑十二轮,每轮等日志传播,扩编周期拖长到数月。联合共识允许任意规模的批量变更,代价是实现复杂度和换班期的性能损耗。

孟师爷晚年把规矩写进《漕帮闸令》,最后一条是:"册可换,令不可断;断令之日,即帮散之时。" 分布式系统的重配置也是一样——你可以换掉所有节点,可以跨机房迁移,可以扩容缩容,但共识过程本身不能停。联合共识保的不是某一本帮册,而是"帮册变更期间,闸令依旧能发、依旧有效"这个不变量。换班期内,船照样走,粮照样收,只是闸丁们腰牌背面多了一行小字,提醒他们此刻正站在新旧两界的门槛上。

概念解析

联合共识(Joint Consensus) 是 Raft 处理集群成员变更的安全机制,核心思想是在过渡期内让新旧两套配置同时生效,任何决议必须分别获得两套配置的独立多数。

与单节点变更的区别。 单节点变更利用新旧配置在数学上的必然交集(N 和 N±1 的多数集必有重叠)来隐式保证安全,实现简单但只能逐个增删节点。联合共识支持任意批量变更,显式定义并行期,实现更复杂、过渡期性能有损耗。

"双多数"的数学本质。 旧配置 C_old 的多数 + 新配置 C_new 的多数,两个集合的交集至少有一个节点。这意味着任何被批准的决议必然被至少一个"同时见证两套配置"的节点确认,杜绝了新旧配置各自独立分裂的可能。

日志原子性。 配置变更作为特殊日志条目嵌入普通日志流,领导者必须单条推进、不能批量跳过。这保证了所有节点以确定的顺序经历"旧配置 → 联合配置 → 新配置"三态,没有节点能"提前"或"滞后"进入下一阶段。

与 2PC 的关系。 结构相似(两阶段确认),但 2PC 协调的是事务参与者对操作的态度,联合共识协调的是共识参与者对自身成员资格的态度。后者是元操作,失败代价是系统丧失共识能力本身,因此需要更保守的设计。