HOOOS

单元化架构机房级切流:如何优雅搞定防脑裂与数据对齐?

0 27 架构老兵 单元化架构容灾切换防脑裂
Apple

在分布式单元化(Set化)架构中,机房级容灾切换(俗称“切流”)是检验架构韧性的最高标准。切流过程中,最核心的两个硬骨头就是防脑裂(Split-Brain)数据对齐(Data Alignment)

一旦发生脑裂,双机房同时对同一批用户(同一单元)开放写入口,数据双向分叉,后续的数据清洗和对账将是灾难性的;而如果数据对齐没做好,切流后用户轻则看到数据回滚,重则产生脏写覆盖。

本文结合一线互联网大厂的生产实践,聊聊在单元化架构下,机房级切流时的防脑裂设计与数据对齐方案。


一、 防脑裂的核心设计:“有且仅有一个机房在写”

防脑裂的本质,是确保在任何极端网络分区或故障场景下,针对某一个路由维度(如 User ID 所在的单元 Block),全球有且仅有一个中心可以执行写操作

在实践中,我们通常采用“多层防御”的立体闭环设计。

1. 路由中心的租约机制(Lease Mechanism)

传统的“强一致性注册中心(如 ZooKeeper/Etcd)+ 推送机制”在极端网络分区时可能失效(例如,A机房与全局配置中心失联,但A机房内部依然存活且有流量灌入)。

引入租约机制可以有效解决这个问题:

  • 下发租约:路由中心(通常部署在第三方仲裁机房或采用 Raft 多数派机制)向各机房的网关/业务层授信。例如:“A机房对 Unit_01 拥有 10 秒的写入租约”。
  • 自动降级:如果 A 机房在 10 秒内无法与路由中心完成续约,A 机房的网关必须主动且无条件将 Unit_01 的写入请求截断或报错,强制降级为只读。
  • 安全接管:B 机房在申请接管 Unit_01 的写入权时,路由中心必须确保 A 机房的租约已绝对过期(即 $T_{A_expire} + \Delta t$ 之后),才允许向 B 机房颁发新的写入租约。

2. 数据库底座的“全局锁/隔离标记”

网关层的路由可能存在微小的时差或残留请求。因此,必须在存储层(DB/KV)进行最终的“兜底围栏”(Fencing)。

  • 存储级 Fencing Token:在切换时,运维控制台向 B 机房的数据库发送指令,提升其为 Master,并生成一个递增的 Epoch(纪元版本号)。
  • 写入校验:数据库或数据中间件在执行写操作时,校验当前的 Epoch。任何携带旧 Epoch(来自老机房滞后请求)的写入请求都会被直接拒绝。
  • 物理隔离:在切换的一瞬间,通过中间件或底层脚本,将老机房对应单元的数据库物理账号一键置为 Read-Only,从底层杜绝脏写。

3. 应用层双向拦截(南北向与东西向)

  • 南北向(网关层):外部流量进入时,网关根据最新路由表拦截。若路由表版本滞后,网关比对本地缓存与请求携带的路由版本,发现冲突直接拒绝。
  • 东西向(RPC层):单元间调用(RPC)时,服务框架底层会拦截并校验:当前服务单元是否为该分片(Unit)的合法服务提供者。非本单元的跨机房写入请求,直接抛出 UnitRouteException

二、 流量切换过程中的“数据对齐”与“防脏写”

在日常演练(软切)与真实灾难(硬切)下,数据对齐的策略完全不同。

方案 A:优雅切流(计划内演练/软切)—— 零数据丢失的“五步法”

在系统健康的演练场景下,目标是 RPO = 0,RTO < 秒级。我们采用“状态机渐进式切换”:

[原机房: 读写] -> [1. 原机房: 禁写读存活] -> [2. 追平延迟(Binlog/MQ)] -> [3. 路由表变更(指向新机房)] -> [4. 新机房: 读写]
  1. 禁写(Disable Write)
    控制台向原机房(A机房)下发指令,将 Unit_01 的状态置为 Read-Only。此时,A机房停止接受该单元的新写请求,网关返回排队(Queue)或友好提示,但允许未完成的事务执行完毕。
  2. 追平延迟(Sync Drain)
    通过数据复制通道(如 Canal、Otter 或商业同步工具),监控 A 机房到 B 机房的数据延迟(Replication Lag)。当检测到同步位点完全对齐,即 Lag ≈ 0 时,触发下一步。
  3. 切换路由(Switch Route)
    路由中心更新路由表,将 Unit_01 的 Master 标记指向 B 机房,并广播至全网网关。
  4. 状态激活(Activate)
    B 机房接收到新流量,验证路由无误后,解除 Read-Only 限制,正式开放读写。
  5. 回放残余数据
    为了防止极少数在通道中滞留的异步消息或离线任务,启动增量对齐校验,对两边进行最后的静态审计。

方案 B:强制切流(灾难发生/硬切)—— 容忍少量滞后的快速恢复

当 A 机房突然遭遇断电、光缆挖断等毁灭性打击时,我们无法进行上述优雅禁写,必须强切。此时的核心挑战是:如何处理 A 机房可能尚未同步到 B 机房的“飞地数据”(In-flight Data)?

1. RPO 评估与自动水位线拦截

B 机房在接管前,必须评估数据延迟。

  • 如果灾备同步通道显示,故障发生前的数据同步延迟在安全阈值内(例如 $< 2$ 秒),则自动/一键授权接管。
  • 如果延迟过大,宁可延长 RTO(进行人工确认或数据抢救),也不能盲目切流。

2. “最后写入者获胜”(LWW)与数据版本化

为了防止未来 A 机房恢复后,历史滞后数据同步到 B 机房从而覆盖 B 机房的新数据,所有底层存储必须具备多版本控制(MVCC)或带有物理时间戳/逻辑时钟

  • 基于 Row Version 的冲突解决
    数据同步工具在双向回放时,必须比对 modify_timeversion_on_node。如果 A 机房漂移过来的数据时间戳早于 B 机房当前记录的时间戳,则选择丢弃 A 机房的更新,或将其写入“冲突死信队列”待人工审计。

3. 异步消息与任务的“延迟对齐”(Reconciliation Pipeline)

  • MQ 幂等与去重:切流后,B 机房的消费端可能会重新消费在切换瞬间产生的重复消息。所有消费逻辑必须实现强幂等(Idempotent),基于唯一业务键(如 Order_SN)进行 Upsert 或防重表校验。
  • 旁路对账系统(T+0 / T+1)
    启动离线/近实时的对账任务。拉取两边机房在切换时间窗口内(如切换前后 10 分钟)的 Binlog,进行双向比对。发现 B 机房缺失的数据,通过特定的补偿服务(Compensation Service)进行业务层面的“补单”或“冲正”。

三、 实战避坑指南(经验教训)

在真实的机房级切换演练中,团队往往会踩中以下几个深坑:

  1. 分布式 ID 的双向碰撞
    如果采用发号器(Leaf / Snowflake),切流到 B 机房后,B 机房生成的 ID 绝对不能与 A 机房未同步完的数据 ID 冲突。
    • 解法:ID 生成器必须按机房/单元分段(例如:Snowflake 算法中的 WorkerID 必须包含机房或单元属性),或者使用全局强一致的分布式 ID 注册表。
  2. 连接池暴涨与雪崩
    在切流瞬间,成千上万的微服务实例会同时向新机房的数据库发起连接重建,极易瞬间冲垮 B 机房的数据库连接池。
    • 解法:网关层必须做漏斗形限流与预热(Warm-up)。切流时,流量应呈阶梯状(如 5% -> 20% -> 50% -> 100%)引入,给底层存储留出线程池和缓存初始化的时间。
  3. “幽灵流量”回溯
    有些客户端存在 DNS 缓存或 HTTP 连接复用(Keep-Alive),在切换发生 10 分钟后,依然有极少数老客户端往老机房发送写请求。
    • 解法:老机房的网关层必须保持“全局拦截态”,直到老机房彻底断网或下线,否则这些幽灵写请求会绕过路由中心直接造成脑裂。

四、 总结

单元化切流的防脑裂与数据对齐,不是某一个中间件或数据库的事情,而是一个从客户端、网关、RPC、中间件到数据库的“全链路立体防御体系”

  • 防脑裂靠“法制”:利用租约(Lease)和存储层 Fencing 机制,剥夺失联机房的写入权。
  • 数据对齐靠“规矩”:通过严格的状态机实现优雅切换;通过多版本冲突解决(LWW)和旁路对账实现硬切后的自我修复。

点评评价

captcha
健康