ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Megastore架构拆解:实体组与Paxos如何实现多地写入

Megastore架构拆解:实体组与Paxos如何实现多地写入 先从一个经常被问烦了的问题说起为什么市面上很多号称分布式的数据库写入请求最终还是要乖乖落到某一个固定节点上无论你怎么扩展副本、加只读从库写操作依然只有一个总开关。但用户根本不在乎你的架构他们只知道一个事实——离得最近的那个机房写入最快华南的请求绕到华北的主库多出来的几十毫秒就足够让客户体验崩一次。这就是全国各地都能写入这个需求的真实起点也是当年 Google 做 Megastore 时要解决的核心矛盾能不能让数据库在多地部署的同时每个地方都能处理写入而数据还能保持一致。这篇先不铺开讲算法细节而是把 Megastore 的设计骨架拆开看它到底怎么用实体组这种数据结构换来了多写能力为什么可以用 Paxos 而不是两阶段提交以及一个普通业务系统能从中借鉴什么。适合对分布式数据库有基础认知、正在折腾异地多活或者多机房同步方案的后端工程师也适合想搞懂Bigtable 之上为什么还要再包一层的存储爱好者。1. 为什么全国各地都能写入是个硬骨头1.1 先看单机房方案的隐形天花板传统关系型数据库的容灾做得再花哨本质还是一个主库 N 个从库。主库负责写入从库负责冗余和只读流量。两台机器放在同一个机房问题还不大一旦要跨城市部署同步延迟就从零变成几十毫秒甚至更高主库的写入吞吐会被复制链路死死拽住。更麻烦的是切换。主库所在城市一旦出问题比如光缆被挖断、机房断电你只能把某个从库提拔成新主库。这中间有多少坑我都不用多说未替换完全的 binlog、半同步复制丢的那条事务、应用连接池里还没刷新的旧主库地址……每次都像走钢丝。即便一切顺利新主库上线后它接收的写入只包含之前的同步进度刚刚在主库上提交、还没来得及复制过来的那批事务就永远找不回来了。对强一致要求不高的业务可能能忍但金融、支付、库存这类系统根本不敢冒这个险。所以单机房方案的天花板不是并发量而是写入点唯一这个硬约束。你可以在全国放几十个只读副本但写入请求永远绕回那个唯一的 master。对老板来说这叫稳妥对用户来说这叫跨城下单慢半拍。1.2 异地多活的诱惑与代价既然单一写入点是瓶颈那自然想到在华东放一个主库在华南也放一个主库两边都能写然后互相同步不就行了这个想法很直觉落地才发现掉进了分布式系统的经典困局——网络分区。想象两个机房之间的专线断了。华东用户下单成功华南用户也下单成功。等网络恢复两边开始同步数据这时候问题来了两边都觉得自己那笔订单才应该保留谁该让谁如果没有一个事先约定好的仲裁机制结果就是两边的数据互相覆盖用户看到订单被回滚运营后台看到库存对不上。这类事故在业内发生过不止一次每次都是大白天上了热搜的级别。CAP 定理说的就是这个事网络分区发生时你在一致性和可用性之间只能选一个。传统选一致性所以必须牺牲掉其中一个机房的写入异地多活想选可用性就必须接受数据在分区间暂时不一致。Megastore 的厉害之处在于它在中间找出了一条路把一致性拆成一个小粒度单位让每个写入只需要在多数派副本上达成一致而不是在全部机房达成一致。这样一来整个数据库不再是一个整体而是被切成了很多个可以独立运转、又能最终并轨的小块。这个小块就是实体组。2. 实体组把数据库拆成一个个可以单飞的小空间2.1 Megastore 到底是什么定位Megastore 不是凭空出现的数据库它跑在 Bigtable 之上底层还是那张巨大的、按 Key 排序的稀疏表。Bigtable 本身没有跨行事务也没有 SQL 语义Megastore 的活儿就是在上面补出事务、索引、复制这些数据库该有的东西。很多资料喜欢把它归类为NewSQL的早期探索但更准确地说它是在 NoSQL 存储和关系型数据库之间搭了一座桥。它有关系型数据库的表结构、索引和 ACID 事务但事务的范围不是整个库而是被限制在一个实体组内。这个限制不是妥协而是整个设计能成立的前提。理解了这一点就理解了一大半 Megastore。2.2 实体组到底长什么样实体组Entity Group这个概念用大白话讲就是把一批经常一起改、一起读的数据放进同一个快递箱。比如一个用户账号、他名下的收货地址、他的最近订单这些数据天然就应该放在一组。为什么因为你打开 App 的第一件事就是加载我的资料 我的订单 我的地址一次操作涉及的全部数据都在同一个组里事务和性能就都有了保证。技术实现上实体组由一个根实体Root和若干子实体Child组成所有实体通过主键上的某种前缀关系组织在一起。比如根实体是用户 12345那么他的每一个订单、每一条地址记录在底层存储的 Key 上都带着这个用户 ID 作为前缀。物理上它们紧挨着逻辑上它们共享同一条复制链、同一个事务边界。这里有个特别容易理解偏的地方实体组不是一张表它更像是一个聚合。同一个实体组里的数据可以来自不同的逻辑表——用户表、订单表、地址表——但在存储和复制层面它们是一体的。反过来如果两个表在业务上毫无关联那就别硬塞进同一个组否则所有写入都挤在一条复制链上相当于自己把自己的多写能力废掉。2.3 实体组带来的关键性质一旦数据被切分成实体组Megastore 就获得了两个非常重要、乍一看甚至有点矛盾的保证同一个实体组内支持完整的、强一致的 ACID 事务写入通过 Paxos 在多数派副本上达成一致读也能拿到最新已提交的数据。不同实体组之间不提供强一致事务数据最终会一致但中间存在一个异步同步的窗口。这个局部强一致、全局最终一致的模型就是 GeoNames 分布式多写能力的地基。每个实体组相当于一个独立的小数据库也都拥有自己的多数派因此任何有副本的机房都可以接受针对某个实体组的写入只要它能说服多数派副本。你不需要把整个数据库锁住只需要让这一个快递箱的副本们达成一致。这也是它和普通分库分表的本质区别分库分表只是把数据拆到不同 MySQL 实例上每个分片依然是单点主从而 Megastore 的每个实体组本身就是一个迷你分布式系统自带副本、共识和故障恢复能力。每个分片都能独立写入这才是全国各地都能写入的真相。3. 实体组内的写入靠 Paxos 而不是两阶段提交3.1 为什么两阶段提交在这里不合适看到多个副本要达成一致很多人的第一反应是两阶段提交2PC。但 2PC 有一个致命的角色叫协调者它一旦崩溃整个事务就卡死所有参与者都得等它恢复。把协调者放在华东华南的写入请求实际上还是在依赖华东的进程如果协调者自身没有做高可用或者网络分区把协调者和某个参与者隔开事务就只能超时回滚。换句话说2PC 换汤不换药多写点依然不成立。Paxos 不一样。它没有中心协调者靠的是多数派概念只要超过一半的副本活着且互相连通系统就能继续对外服务。华东挂了华南和华北的副本仍然可能组成一个多数派照样批准新的写入。这就把单一写入点换成了写入点集合每个机房都可能是那个发起提案的节点。两阶段提交和 Paxos 解决问题的层次也不同。2PC 解决的是多个参与者怎么原子地提交一个事务Paxos 解决的是多个副本怎么对同一个值达成一致。在 Megastore 里事务范围内的数据变更不是直接写到每个副本上而是先写成一条日志记录让所有副本对这个日志达成一致然后再各自应用日志、更新存储。3.2 写入过程其实就是状态机复制Megastore 的写入路径本质上是经典的状态机复制模型。每个实体组的副本都维护着一条按序排列的操作日志所有读请求只读本地已应用的日志状态写请求则把一个新的日志条目提交到多数派副本上提交成功后每个副本按同样的顺序应用这条日志最终所有副本达到相同状态。类比一下这就像多个城市的乐队成员用同一份乐谱演奏。乐谱本身的修改必须有顺序、有版本谁都不能凭自己的想法乱改但只要大家都照着最新版的乐谱演奏哪怕排练场次不同、进度不同最终合奏时也是同一首曲子。数据库里的乐谱就是日志排练就是异步应用日志。这里有个容易忽略的细节事务的原子性体现在日志条目上。一条事务无论改了多少行数据都会被打包成一条日志记录。副本要么完整应用它要么等下次再应用绝不会只应用一半。这也是为什么 Megastore 能直接建立在 Bigtable 这类不具备事务能力的存储之上——真正的事务能力不是由底层存储提供的而是由日志共识层提供的。3.3 副本角色与 leader 的真正含义Paxos 协议里经常听到 leader、proposer、acceptor很多人一看就觉得这和主从架构没区别leader 还是单点还是要挂。但在 Megastore 的语境里要区分两层Paxos 的 leader 只是提案的发起者不是写入的所有者。它负责推动某个写入提案但能否提交取决于多数派副本的批准。也就是说即使 leader 机器宕机其他副本也能推举出新的 leader继续处理写入。更关键的是对于不同实体组leader 副本完全可以落在不同的机房。华东机房可能是订单分组 A 的 leader华南机房可能是订单分组 B 的 leader。这样一来数据被切分成了无数个可以独立决策的小单元每个单元都就近找一个 leader天然实现各地写入。用大白话讲这就是把一个大仓库拆成了无数个小快递站每个站都有自己的站长同一个城市里的包裹不必都去总仓过一遍。用户在上海下单他的订单组 leader 在上海用户在北京下单他的订单组 leader 可能在北京。两边各写各的互不打架。方面传统主从复制Megastore 实体组 Paxos写入点全局唯一 master每个实体组可以在任意有副本的机房写入故障恢复手动/半自动主从切换多数派自动选新 leader无需全局协调事务范围通常整库或单分片实体组内强一致跨实体组弱一致一致性与可用性取舍分区时只保一致性分区时保局部可用性全局最终一致4. 一条跨城写入的真实路径从用户点击到落盘确认4.1 写入流程逐步拆解纸上谈兵说完了现在把一条用户请求掰开揉碎。假设用户在杭州下单订单实体组的 leader 恰好也在杭州这条路会走哪几步第一步客户端向杭州副本发起事务。实体组的本地副本会先检查事务涉及的数据是否都在本组内如果发现跨组操作会直接走一条代价更高的跨组协议如果只在组内就进入第二步。第二步本地副本把事务转换成一条待提交的日志记录并通过 Paxos 向所有副本提议。注意这里不需要所有副本都在线只需要多数派副本确认这条日志。多数派可能包含杭州、上海和北京三个副本中的两个哪怕广州副本暂时失联也不影响提交。第三步一旦多数派确认事务就算提交了。但提交不等于每个副本都已经应用了这条日志更不等于用户的读请求马上能看到最新值。提交只代表日志被持久化、序号被确定应用日志的工作可以在后台异步进行。第四步本地副本立即应用日志并更新存储然后向客户端返回成功。这个流程里最反直觉的一点是用户写入确认的速度取决于多数派副本的往返时间而不是所有副本的同步时间。如果多数派选得离用户近延迟就低如果多数派分布在跨几千公里的三个城市那单次写入延迟可能要到几十毫秒甚至更高。所以 Megastore 的实际部署通常会把副本放在尽可能接近用户流量、又不至于落在同一个故障域里的地方。4.2 读路径快照隔离与就近读写入可以被多数派批准后马上返回读请求的处理则分两种模式。一种是 current 读保证读到最新已提交的数据。实现方式也不复杂先问当前实体组的日志位置到哪了等本地副本把日志应用到这个位置再读存储。这个等待时间通常很短但本质上是一个读也要参与同步的过程适合对实时性要求高的读。另一种是 snapshot 读直接读本地副本当前已经应用到的状态完全不等待延迟极低但可能不是最新的。如果业务能容忍稍微旧一点点的数据比如动态详情、评论列表snapshot 读就能把延迟压到最低。这两种模式切换起来非常灵活关键点在于 leader 副本上维护了一个时间戳任何一次 successful commit 都会推进这个时间戳。读者只需要知道当前日志应用到了哪个时间戳就能决定走哪种读策略。这个设计放到业务系统里其实就是强一致读和弱一致读的取舍但因为它是按实体组粒度做的粒度细浪费就少。4.3 本地率为什么是绩效指标Megastore 论文里反复强调一个概念本地可用性或者叫 local rate。它统计的是所有读写请求中有多少比例能在发起请求的机房内被完整处理不需要跨机房协调。这个数字直接决定系统的真实性能。如果本地率是 99%意味着绝大多数请求只在本机房内完成共识体验就是本地数据库如果本地率只有 60%那剩下的 40% 请求都要跨城往返平均延迟直接翻倍系统的分布式优势就荡然无存。要做到高本地率唯一的路子就是数据设计把会被同一个用户高频访问的数据放在一个实体组里并保证同一个用户的所有流量基本都打到同一个机房。这也是为什么 Megastore 类的架构都强调就近接入和按用户维度切分而不是按订单号随机散列。随机散列会让一个用户的数据碎片化散落在各个机房重建成本极高这是很多团队复刻 Megastore 时最容易踩的坑。5. 跨实体组的操作能不用就不用5.1 跨组写为什么贵前面把实体组夸成这样也让很多人产生一个错觉以后所有事务都可以直接做不用再担心分布式了。实际情况是跨实体组的事务虽然支持存在但代价极高。跨组事务的原理是在多个实体组之间用两阶段提交做协调每个参与者组内部还要先通过 Paxos 达成一致。这两层协调叠加在一起事务延迟和失败概率都会明显上升。只要一个组拒绝提交整个跨组事务就要回滚一个组正在进行日志同步另一个组卡住协调者就得等。在多机房环境下跨组事务几乎必然成为性能黑洞。所以在 Megastore 的实践里跨组操作有一条铁律能避免就避免。设计表结构时你得先问自己一句——哪几条数据是同一个业务动作里必须一起修改的把他们塞进同一个实体组。哪几条只是常常一起读但不常一起写那就没必要放一起读操作用异步聚合就好。5.2 一条实体组设计的黄金原则我见过的实体组设计案例做得好的都有一个共同点以业务里的聚合根为圆心圈住所有和它生命周期紧密相关的子实体。举个电商例子。用户、收货地址、优惠券、最近订单列表明显应该跟着同一个用户 ID 走放进同一个实体组。为什么因为用户所有高频操作——下单、查订单、改地址——都只涉及自己这组数据跨组操作自然就消失了。这里的性能提升不是一点点而是量级上的。反过来有一个典型反面案例把商品库存设计成一个全局共享的实体组。所有门店、所有渠道的下单都要更新同一个库存组这个实体组瞬间变成全网的写入热点。它每次更新都要在多数派之间同步并发一高整个系统就卡死在库存组上。这不是 Megastore 不行而是实体组粒度设计错了。库存这类数据天生就是跨用户、跨地域共享的硬要套一个实体组等于把所有流量往一个快递站里塞。场景实体组设计建议原因用户中心按 user_id 聚合账号、资料、订单高频请求全部组内完成站内信/通知按收件人聚合读写都围绕单用户展开全局库存/热点商品不要单组做全局热点该数据本身是共享可变状态报表/汇总数据使用最终一致异步重算跨组强一致代价过高5.3 弱一致窗口怎么补即便实体组设计得再漂亮跨组最终一致产生的延迟窗口还是存在的。一个订单在华东提交统计报表在华北可能几分钟后才同步到。业务方如果无法接受这种延迟通常的兜底做法是为了展示而设计的读路径允许走最终一致为了钱和货而设计的写路径则严格保证组内强一致。把必须强一致和可以弱一致的数据分开比单纯纠结技术架构要重要得多。实际上很多从 Megastore 演化出来的架构最后都把跨组状态本身当成一种可重试的异步任务来处理。比如订单状态变更必须强一致但订单状态变更引发的一个积分变动、一条消息推送就可以放进消息队列异步完成。设计原则仍然是同一个能圈在一个组里的圈进来圈不进来的就用最终一致和补偿逻辑承接。这也是我经常挂在嘴边的一句话分布式不是把简单的事变复杂而是帮你把复杂的事画一个边界。6. 从 Megastore 能学到什么给普通后端开发者的 3 条实用启示6.1 多地写入的本质是一次取舍不是一次技术升级很多团队做异地多活第一反应是找一套能两地三中心双活的数据库产品装上去就万事大吉。但 Megastore 的整个设计告诉我们多地可写不是一种能力而是把一致性切成小颗粒后的副产品。如果没有事先梳理清楚哪些数据能接受最终一致、哪些不能再先进的存储也撑不住业务复杂度。我参与过的项目中最稳的做法是先做业务分级。想一想哪个操作失败会造成资金损失哪个操作晚十秒看到没有任何影响前者必须放进强一致边界后者可以放到异步链路。这个梳理工作不需要任何数据库知识但它决定了你要不要上 Megastore 这类方案以及上了之后会活得舒不舒服。6.2 实体组思想完全可以提前用起来就算你短期内不可能切换到 Megastore实体组的思想也值得先在设计业务表结构时实践起来。它和领域驱动设计里的聚合Aggregate是一回事把必须一起修改的数据圈成一个边界边界内用事务边界外用事件驱动。比如传统的订单表和订单明细表分开两张表靠外键关联实体组的思路会把它们放在同一个存储单元里主键加上订单号前缀。你会发现这样设计之后后续做分库分表、做数据迁移都因为一个聚合的所有数据都挨在一起而省掉大量麻烦。很多团队搞分库分表失败不是因为中间件不好而是因为他们分片的维度不是业务聚合维度而是随机取模。随机取模把聚合拆得稀碎跨分片查询自然躲不掉。6.3 复制协议不是银弹运维水平才是Megastore 的 Paxos 方案解决了共识问题但付出了巨大的运维代价。每一组副本都要维护日志、处理视图变更、跟进已提交日志的追赶。副本故障、日志落后、磁盘损坏这些场景在传统主从中已经够头疼在实体组架构里会乘以副本数。所以从工程角度看我的建议很朴素先评估自己的团队有没有能力运维这样的系统。一个能跑的 Postgres 主从 消息队列异步补偿很多时候比一个调不通的 Paxos 集群要可靠得多。技术选型不是越高级越好而是越匹配团队成熟度越好。Megastore 这类架构适合那些用户分布极广、业务天然可以按用户切分、且团队有能力吃透共识引擎的大规模系统对绝大多数中小团队来说借鉴它的实体组设计思路比直接复刻它的 Paxos 布局更实际。我自己看 PostgreSQL 生态里的 BDR、CockroachDB 这类产品时也会下意识先看它们是怎么处理热点实体组和跨组事务的——凡是没把这两件事讲清楚的基本都在回避 Megastore 当年踩过的坑。反过来只要你把数据的聚合边界画对多数高并发业务用更传统的方案就能扛住根本不需要走到 Paxos 那一步。这篇算 Megastore 系列的开篇先把为什么可以多地写的骨架搭清楚了。下一篇我打算直接展开 Paxos 协议在实体组层面的具体行为讲讲提案编号、quorum 计算和副本追赶这些真正磨人的工程细节。最后留一句个人的经验阅读这类系统时别急着背协议名字先画一张你业务里的实体组边界图你会在画的过程中真正想明白这套架构的取舍。
返回列表