跨地域强一致并不遥远:Megastore如何改造Paxos?
大家好我是作者。这个系列写到第三篇前两篇我们把 Megastore 的整体架构和 Paxos 基础都过了一遍。今天这篇只聚焦一个问题Paxos 怎么从单数据中心内部跨到真正的“国界”之外去工作。先说个反直觉的事实。大部分开发者听到“跨地域强一致”第一反应都是“不可能延迟没法接受”。但 Megastore 在 2005 年前后就在全球多个数据中心之间提供了同步复制、故障自动切换的强一致数据访问而且写延迟没有高到不可用。它做到这件事靠的不是把 Paxos 原封不动地扩大规模而是做了一整套针对跨地域场景的协议改造和部署设计。这篇文章我会从三个层面拆解跨地域共识到底难在哪、Entity Group 为什么是划分共识边界的核心、以及真正跨机房跑 Paxos 时协议和部署上的那些细节改动。会涉及一些论文里没有展开讲的工程坑也有我自己实践后的理解希望对正在做异地多活或者研究分布式一致性的人有点帮助。1. 为什么非要把 Paxos 搬到“国界”之外1.1 “国界”的本质是延迟、分区和故障域先澄清一下标题里的“国界”。Megastore 面对的场景是数据中心的分布式部署这里说的“国界”就是机房的边界、地域的边界。不是真正的国家国界而是把数据从一个物理位置搬到多个地理位置之后所有技术约束都变了。单数据中心内部的 Paxos节点间延迟通常只有 0.5 到 2 毫秒网络分区是极端情况。可一旦把副本放到跨地域的多个机房哪怕走的是高质量专线基本延迟也是 30 到 100 毫秒起步而且网络跳变、运营商路由问题、光纤中断这些突发情况全都变成了常态。你不再只是处理“一台机器挂了”这种小概率事件而是要处理“一整个机房不可达”这种更粗粒度的故障。更麻烦的是故障域。“国界”之外每个机房都是一个独立的故障域它可能断电、可能断网、可能是整个区域的网络被切断。单机房 Paxos 可以在机架故障时继续工作但如果你把所有副本放在一个机房这个机房整体故障时系统就彻底不可用了。所以想提高可用性就必然要把副本放到多个地域。这就是跨地域强一致的根本矛盾不跨地域故障域太大可用性上不去跨了地域延迟和分区问题又让一致性实现变得极其困难。1.2 三种“土办法”的真实代价在 Megastore 之前业内应对跨地域数据需求通常有三种方案各有各的痛。第一种是异步主从复制。主库在一个地域备库在另一个地域数据通过 binlog 或者 WAL 异步同步过去。主库挂了之后备库提升为新的主库。代价很明显主库宕机那一瞬间异步队列里还没同步过去的数据直接丢失而且往往丢得悄无声息。金融类、交易类业务根本不敢用这种方案因为一致性不达标。第二种是同步两阶段提交到所有副本。写操作必须等到所有副本都确认才能返回。这个方案在理论上可以做到强一致但可用性被最慢最远的节点绑架了任何地域的一个抖动都会拖垮所有写操作。两个异地机房之间延迟本来就高再加上两阶段提交的多轮交互写延迟能被拉到几百毫秒业务基本扛不住。第三种是每个数据中心都有完整副本但只做本地读写操作全局同步。这算是前面两者的折衷但问题是它的同步粒度是整个数据库。对数据库任何一个键的写操作都要跑一遍跨机房共识协议所有热点写入都被迫承担跨地域延迟系统的吞吐量会被压得很低。三种方案的痛点我用一个表格对比一下方案一致性可用性写延迟适用场景异步主从复制弱丢数据高低可容忍数据丢失的场景同步两阶段提交强依赖最慢节点极高数据量小、写并发低全库同步复制强中等高技术可行但工程代价大Megastore 的答案不一样。它既不做全库同步也不做纯异步而是把同步复制的粒度缩小到“实体组”这个级别。这个思路直接改变了跨地域一致性的可行性。接下来进入正题。2. Entity Groups给 Paxos 划出一条合理的“国境线”2.1 复制粒度的选择决定了跨地域的写放大倍数如果我们把整个数据库看成一个巨大的 Paxos 组那么任意一个键的写入都需要所有副本参与共识。这不是协议的问题而是粒度的问题跨地域同步的代价与粒度成正比。Megastore 引入了 Entity Group实体组的概念。它把数据按照业务实体的维度进行分组每个实体组拥有自己独立的 Paxos 日志和复制状态。也就是说不同实体组的 Paxos 实例是完全独立运转的。写入某个实体组的数据只会在该实体组自己的 Paxos 副本之间同步不会影响其他实体组。为什么要这样设计因为绝大多数跨地域业务场景下数据的局部性是很强的。比如一个用户的行为数据用户的个人资料、偏好设置、最近操作这些数据的读写都集中在“这个用户”身上。把“每个用户”作为实体组的边界那么大多数写操作只会落在某一个实体组内部。我不是在说每个用户单独占一个 Paxos 组就一定合适而是说这是一种通过业务语义来划分共识边界的方式。论文里的经典示例是一个企业邮箱系统每个用户是一个实体组组内包含该用户的邮件、联系人、设置等数据。这样用户在手机端发一封邮件写操作只涉及这个用户的实体组而不需要跟全局其他用户的数据做同步。这个设计的价值是把跨地域一致性从“系统级问题”降级成了“单个分组问题”。写放大系数从“全库副本数”降为“单个实体组副本数”故障影响范围也小了一个实体组的 Paxos 状态卡住不影响其他实体组继续提交。2.2 实体组内的原子性规则实体组不仅决定了数据的放置还决定了事务的边界。Megastore 的事务模型是一个事务可以修改一个实体组内的多个键并且保证原子性但跨实体组的事务只提供快照隔离或者非强一致语义。在实现上每个实体组的 Paxos 日志记录了本组内所有写操作的日志条目。客户端发起事务时先把变更写入本地事务缓冲区然后通过 Paxos 向整个实体组的副本提交日志。一旦日志条目在法定人数副本上提交成功再按顺序应用到实体组的数据存储中。原子性的来源就是 Paxos 日志日志条目要么被法定人数接受并提交要么被放弃不存在中间状态。多键写入被捆绑在同一个日志条目里因此要么全部生效要么全部不生效。这里有一个容易混淆的点Paxos 日志本身的提交是共识层的操作而日志的应用是存储层的操作。Megastore 的做法是先提日志、后应用数据中间还有一层 catch-up 机制来处理副本落后的情况。这个属于典型的“预写日志 回放”的思路但关键是它和 Paxos 强一致绑定而不是事后异步回放。这也是为什么它能在跨地域场景下保持强一致的原因。2.3 分组的“用户可见性”设计Entity Group 的一个反直觉特点是分组不是系统自动做的而是应用开发者在 schema 里显式声明的。这意味着设计者必须提前想清楚哪些数据应该放在同一个实体组里哪些可以跨组。这个工作听着简单实际很容易翻车。我见过不少团队在建模时图省事把所有数据堆在少数几个大实体组里结果热点全部集中在那几个组上跨地域同步的瓶颈立刻暴露。比如你要做一个社交应用如果每个已被关注的大 V 是一个实体组他的动态更新会带来巨大的单点写流量这个实体组的 Paxos 日志会成为整个系统的瓶颈。正确做法是让应用的核心访问模式尽量落在单个实体组内部。聚合多的数据放一组跨组访问少的放另一组避免为了省事而做出一个全局热点组。这个数据模型设计经验今天看仍然是做跨地域系统最重要也最容易被低估的一步。3. 跨机房 Paxos 的关键改造协议细节与部署策略3.1 准备阶段与接受阶段谁在跨地域路上响应你理解了实体组之后再回到 Paxos 协议本身。经典 Paxos 两阶段——prepare 和 accept——在单机房内跑得很顺但放到跨地域场景就必须重新审视每一步的 RTT 代价。以一次写操作为例客户端把日志条目发给 leaderleader 如果要走完整基本 Paxos需要先广播 prepare 请求到所有 acceptor等多数派返回然后再广播 accept 请求等多数派确认。两次跨地域广播两次等待最远副本响应意味着传统 Paxos 的最坏路径延迟是两个跨地域 RTT。Megastore 对 prepare 阶段做了大幅精简。对于 leader 正常工作的稳态场景多数条目的提交并不需要完整的 prepare/promise 流程。leader 在租约期内是唯一的可以直接进入 accept 阶段向所有 acceptor 广播“接受这个条目和编号”。Fast Paxos 的线路在论文中也有讨论简单说当没有竞争者时一轮 RTT 就能完成提案提交。我的理解是Megastore 在工程实现上对于协调者coordinator这套机制做了大量优化由协调者判定当前可以由谁发起提案并将这种判断批量化和缓存化。所有站点通过租约机制认可协调者的领导地位避免每次写操作都要进行一轮 prepare。这么做的效果是跨地域写路径从两轮往返变为一轮往返延迟直接砍半。论文中给出的经验数据大约是几十毫秒量级的跨地域写延迟实际部署时加上网络抖动P95 表现仍可接受。这里需要特别说一个容易被忽略的点prepare 阶段省掉的前提是 leader 确实没有竞争者。如果两个临时 leader 同时出现Paxos 的安全性依然靠提案编号和法定人数保证但延迟会急剧恶化。因此协调者租约、Chubby 选主、以及“唯一 leader 失效后尽快恢复”这三件事是 Fast Paxos 路径能跑远的地基。3.2 副本身份分配主数据中心、本地读取与跨地域法定人数部署层面Megastore 通常把副本放在多个数据中心。核心问题是怎么分布副本、怎么选取法定人数quorum。先说副本位置。我见过最典型的部署是 5 个副本放在 3 个数据中心这种布局下Paxos 法定人数为 3。它能容忍任意 2 个副本故障但对“数据中心级别”的故障来说3 个数据中心中任意 1 个整体宕掉只要它上面的副本不超过 2 个系统仍然可用。也可以选 7 副本、5 法定人数容错更高但跨地域写路径的延迟也会相应提高因为每次写需要更多远程副本确认。法定人数的位置分布决定了跨地域延迟到底由谁买单。同样是 5 副本 3 法定人数如果 3 个法定成员都在同一个数据中心那跨地域写就退化成单机房写快是快但一个机房挂了法定人数就可能凑不齐反过来如果 3 个法定成员横跨三个数据中心那么每次写都必须等待至少两条远程 RTT。Megastore 的做法是通过管理节点把副本位置的布局信息暴露给 leaderleader 在选取法定人数时倾向于选择一个“本地 一个邻近区域副本”的组合尽量降低写路径最远节点的延迟。对延迟模型做个简单估算。假设本地机房内 RTT 为 1ms跨地域专线 RTT 为 70ms。如果法定人数是“本地副本 一个邻近地域副本 一个远地域副本”那么写到完成的延迟基本由最远的那个副本决定大约 70ms。这还是一个理性可接受的值。但如果法定人数里有两个跨地域副本而它们又不在同一个方向延迟就可能叠加到 100ms 以上。3.3 Witness 节点解决两个机房对半分裂的“三难”这里有一个非常具体、几乎每个跨地域 Paxos 系统都会踩的坑只有两个数据中心而且副本数刚好对半的时候网络分区会导致两边都无法选出多数派。假设你只有两个机房 A 和 BA 有 3 个副本B 有 3 个副本总共 6 个副本法定人数需要 4。A 和 B 之间的网络断了A 只能联系到自己的 3 个副本B 也只能联系到自己的 3 个副本两边都凑不齐 4整个系统就锁死了。这种“对半分裂”是跨地域 Paxos 最常见的可用性陷阱。解决思路很简单也很有启发性再加入一个 Witness 副本放在第三个故障域。Witness 节点不保存业务数据只参与投票用来凑法定人数。它存在的意义就是防止分裂的两种假的多数同时出现。具体部署时你要保证 Witness 放在独立的故障域不能和某个主副本放在同一个机架、同一个机房。我见过有人把 Witness 临时放在机房 A结果机房 A 故障时 Witness 也跟着不可用照样锁死。所以 Witness 的位置必须真正独立最好是一个单独的城市或者至少单独的可用区。这里还要澄清一个容易误解的点Witness 节点并不是“少数服从多数”的破解法——它只是让多数派更容易在同一时刻被某一个分区内凑齐。要保证系统在一个机房故障后仍然可用真实副本总数和 Witness 位置必须联合设计好。3.4 快速共识路径与协调者租约Paxos 在跨地域场景下做正确并不难难得是让它每次写入都足够快。Megastore 的协调者租约机制简单说就是给 leader 一份“当前无竞争者”的声明在租约有效期内其他副本不会尝试成为新的 leader因此 leader 可以跳过 prepare 阶段直接发起 accept。这个租约一方面降低了延迟另一方面也引入了新的可用性风险。如果 leader 所在的数据中心网络抖动导致租约续期失败其他地域可能要等租约超时才能接管。租约太短会频繁出现暂时性的无主状态租约太长故障切换就慢。Megastore 在设计上把租约和 Chubby 锁绑定本质上是在延迟、可用性、安全性之间找平衡。具体数值我没法给出通用推荐因为要和网络条件、业务容忍度绑定但有一点要提醒租约不是一个纯粹的“配置项”它是跨地域 Paxos 的另一个核心状态必须纳入监控和故障演练。4. 读路径与故障切换跨“国界”系统的日常运行4.1 三种读取级别强一致读没有想象中贵很多人在理解 Paxos 时只盯着写但跨地域系统里读的路径同样关键。Megastore 的数据副本在每个数据中心都有一份完整数据所以读操作允许“就近读取”客户端访问离自己最近的数据中心而不必每次都跨地域读远端。这里的关键区别是读的一致性级别。Megastore 提供了三种读取方式current 读、snapshot 读和 inconsistent 读。current 读必须读取到某个实体组的最新已提交日志位置。如果本地副本的日志应用位置落后需要先做一次 catch-up从其他副本拉取缺失的日志条目然后再读。snapshot 读读取某个已知时间点的快照不保证最新。inconsistent 读完全不做一致性校验直接读本地副本可能过期的数据。current 读的执行路径比我原先想得要轻量。它不需要发起全局共识只需要向该副本确认“本实体组的日志已经应用到了最近的 committed 位置”。这个位置信息由 Paxos 日志状态机维护并且各副本通过心跳等方式交换进度。所以一次强一致读代价只是本地读取加上可能的日志追赶而不是一轮跨地域表决。这个设计对读多写少的业务场景非常友好。我实际测试过在跨地域部署下本地 current 读的延迟基本在几毫秒以内只有写入才需要承担跨地域延迟。4.2 故障切换数据中心整体宕机时发生了什么跨地域系统最怕的故障就是整机房不可用。假设 leader 所在的数据中心整体宕机之后会发生什么从 Paxos 的状态机视角看不管 leader 是否还活着我们首先要等它的租约超时。为什么必须等因为如果旧 leader 的租约还没过期其他副本就不能安全地发起新的一轮选举否则可能出现“双主”。租约超时后其他数据中心的副本可以提议自己为新的 leader并通过 Paxos 在法定人数中达成一致完成 leader 切换。这里有一个大家容易误解的点整个切换过程不需要旧 leader 的数据先被复制到新 leader。Paxos 的法定人数机制保证了旧 leader 提交过的日志已经存在于至少一个法定人数的副本中因此新 leader 一定能从这些副本那里拿到完整的已提交日志继续应用。这个性质其实来自 Paxos 的基本安全性两个法定人数的交集不为空。实际工程中还有一个重要环节Chubby 或其他协调服务在选举和租约管理中的作用。Megastore 使用 Chubby 提供锁和选主服务同时在 Chubby 不可用时必须有降级方案。Chubby 和 Paxos 不是同义词Chubby 是更高层的协调者Paxos 是底层共识协议两者的故障域需要分开考虑。4.3 故障演练与概率分析跨地域系统的问题在于它没有太多线上试错的机会。一个发生概率极低的网络分区因为触发条件苛刻可能几年不显现一次但一旦出现就是灾难。Megastore 的做法在论文和后续分享中都有提到——用确定性的模拟器注入各种故障验证 Paxos 协议实现在随机网络分区、消息延迟、进程崩溃情况下的正确性。我自己非常认同这种思路。做分布式系统不是“协议对就万事大吉”而是要对“协议实现”做覆盖式的测试。常见的测试场景包括一个副本收到消息后停滞、两个副本同时拥有不同日志、网络分区重组、leader 和租约同时失效等。在这些场景下跑测试才能确认 Paxos 状态机的实现没有 hidden bug。关于概率Megastore 论文里的一个分析结论是在 5 副本 3 法定人数的部署下两个不同副本同时发生故障的概率低于三个数据中心中两个同时不可用的概率。这个结论意味着只要副本分布合理跨地域 Paxos 的可用性通常能维持在四个九甚至更高。真正的风险反而是人为误配置比如把 Witness 放错位置、法定人数成员全部落在同一个故障域这类问题不是概率问题而是架构问题。5. 对普通团队选型的启发跨地域一致不是免费的午餐5.1 跨域强一致的适用条件看了 Megastore 的做法可能有人会想我们团队是不是也能做到跨地域强一致我的回答是技术上完全可以复刻思路但它有非常清晰的适用边界。第一个边界是数据模型必须能拆分成实体组。如果你的业务天然是全局聚合、全局热点的比如全局计数器、排行榜那这些数据无论如何都不适合放进跨地域强一致系统。你要么接受它只存在于单地域要么接受它偶尔不一致。第二个边界是跨组事务的弱化是否可接受。Megastore 的跨组事务只提供快照隔离或非强一致如果你的业务要求跨多个用户的数据必须强一致比如 A 给 B 转账但 A、B 分属两个实体组那 Megastore 模型就不合适。第三个边界是写延迟的承受能力。跨地域同步复制的最小写延迟不会低于最远法定副本的 RTT。如果你的业务需要亚毫秒级写延迟那跨地域强一致这条路不要走至少不能全量走。5.2 如果不想上 Paxos异步复制加补偿的替代路径很多现实业务其实用不到这么强的保证。我见过不少“异地多活”架构做法是每个地域有一个主副本业务按地域或按用户分片跨地域数据通过异步消息或事件流同步配合幂等消费和冲突检测机制。这个方案的最大优点是不会把全局写延迟拉高因为跨地域数据同步发生在后台。代价是故障切换时可能出现短暂不一致需要业务侧做补偿、对账甚至人工介入。这里的关键是先想清楚业务能不能容忍最终一致再决定上不上异步方案。如果答案是“可以”那你完全不需要复制一套潘多拉级别的跨地域 Paxos一个可靠的异步队列加去重机制往往就够了。5.3 从 Megastore 到 Spanner思路的延续最后说说后续演进。Spanner 很大程度上继承了 Megastore 的实体组思想并引入了 TrueTime 和两阶段提交使跨实体组事务成为可能。但它并没有打破跨地域同步复制的物理规律——写路径依然需要等待跨地域的共识确认只是通过更精细的时间戳和并发控制让事务的吞吐和隔离级别更强大。对普通团队来说我的建议是不要一上来就上 Spanner 级别的复杂度。先理清你的业务数据哪些真的需要跨地域强一致、哪些可以最终一致、哪些可以降级为单地域强一致。把 Megastore 的 Entity Group 思想当成一个建模工具而不是一套必须完全复刻的协议实现你会发现它的价值远超论文本身。我在实际项目中把实体组的概念用在了一个多机房的用户数据系统上虽然没有跑完整的 Paxos但把数据按用户维度分片、让热操作只在分片内同步确实把跨地域写冲突降低了几个量级。这个思路本身就是 Megastore 送给后来者最有价值的礼物。