ARTICLE DETAIL

资讯详情

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

异地多活架构实战:数据一致性、容灾与单元化设计要点

异地多活架构实战:数据一致性、容灾与单元化设计要点 我到现在都记得那个周六晚上刚躺下准备刷会儿手机群里就炸了机房某条核心网络链路抖动线上交易超时率肉眼可见地往上飙。那次我们费了很大力气才把流量切到备用机房好在部署了“备用”机器但整个过程还是心惊胆战——因为备用机房主要在“备”平时不承接流量真到切换的时候数据滞后、依赖缺失、配置不准各种问题全冒出来了。那之后我就在想能不能不只是“备”而是让多个机房同时干活这就是“异地多活”给我们出的题让多个数据中心同时对外提供服务任何一个中心出故障流量都能顺滑地切走用户几乎无感。这篇文章我想把异地多活架构从头到尾捋一遍讲清楚它解决什么问题、核心难点在哪里、有哪些常见架构模式、关键细节怎么落以及我实际操作中踩过的坑和排查经验。适合正在做高可用架构、微服务治理或者公司业务体量已经到了“单机房不敢死”的阶段、需要提前做技术规划的读者。不管你是一线开发还是架构决策者这篇文章的思路都能直接用上。1. 异地多活是什么到底在解哪道题1.1 高可用演进的三步走备份、容灾、多活很多人刚开始接触高可用时都是从备份起步的数据库做从库定时备份文件机器挂了就从备份恢复。这种方式能解决硬件损坏的问题但恢复时间很长而且在故障期间服务是中断的。之后再进一步是“主备容灾”本地机房挂了之后把服务切到灾备机房数据通过同步或半同步方式复制过去。这套模式比纯备份靠谱但灾备机房平时只承担极少的校验和备份流量大部分机器是闲置的资源浪费大而且切换一次非常痛苦。异地多活把思路彻底倒过来了没有明显的“主”和“备”多个机房都是“活”的都在承接真实用户流量都在写入数据。任何一个机房出问题其他机房能顶上来。从用户视角看整个系统好像一个永不宕机的整体。我用一个生活化的类比来帮助理解普通容灾相当于你家里备了一个手动发电机停电了掏出来拉响全家黑灯好久才来电异地多活相当于你直接接了两条不同的电网入户一条线断了另一条线自动顶上你在房间里可能只有空调抖了一下感觉不到切换。1.2 异地多活绕不开的两个指标RPO与RTO做异地多活必然要跟两个指标死磕RPO恢复点目标和RTO恢复时间目标。RPO关注的是“丢多少数据”。假设业务上允许在灾难切换时丢最近5秒的数据那么RPO就是5秒如果要求一毫秒都不能丢RPO就是0。RTO关注的是“多久能恢复”。故障之后从触发切换到系统完全对外提供服务花了多少时间那就是RTO。这两个指标直接决定了架构选型和成本投入。要实现RPO为0技术上意味着两个机房的数据必须完全同步也就是必须走同步复制而同步复制在跨地域的网络上会放大网络延迟写性能会很难看。RTO压得越短对自动切换、健康检查、预案完整度的要求就越高。很多团队嘴上说“我们要做到零丢失零感知”但实际上在RPO和RTO之间来回挣扎最后不得不向业务妥协选择“允许一定数据丢失、允许几分钟中断”的折中方案。1.3 先冷静评估你的业务真的需要异地多活吗先把冷水泼在前面不是所有业务都需要异地多活。如果业务月活几万、故障半小时用户也不会大面积流失那花大力气做异地多活性价比极低。异地多活的成本不仅仅是两套机房的机器和带宽费用更是整个研发链路、数据链路、运维体系的改造投入这里的成本往往比硬件成本高一个量级。判断标准其实挺朴素如果机房整体宕机你的业务是否会遭受无法接受的损失。比如像支付、社交、电商这类用户体量巨大的业务每宕机一分钟都是真金白银的损失那异地多活就是必须的。如果是内部管理系统、后台报表系统、实验性质的产品做好同城多机房备份、省下异地多活的复杂度可能是更明智的做法。我见过一个很典型的反面案例某创业公司体量不大听了几次技术分享会后头脑发热要搞“三地五中心多活”整了一堆人扑上去结果业务规模撑不起这个复杂度半年后系统越来越难维护代码里到处是单元化分支判断最后又一点点退回单机房。异地多活是手段不是目的这个顺序不能搞反。2. 核心难点做之前先把账算清楚2.1 数据一致性CAP的约束没法绕开异地多活最核心、也最无法绕开的难点就是数据一致性。分布式系统有一个著名的CAP理论一致性、可用性、分区容错性三者只能取其二。多机房之间必然存在网络分区而且网络分区是常态而不是异常所以P是必选项。剩下来你在C和A之间必须做一个取舍。大多数做异地多活的系统选择的都是“最终一致性”某个用户在主中心写入的数据通过异步复制同步到其他中心在这个同步窗口内用户在另一个中心可能读到旧数据。代价是短时间的读旧数据或写冲突换来的是多个中心都能写、都能扛流量。这里有一个容易产生误解的点很多业务同学问“我们能不能做到数据强一致”技术上其实可以比如引入跨机房的分布式事务、全局一致性协议像Paxos/Raft的多副本延伸但这类方案会显著放大跨机房延迟性能折损非常大。跨城网络延迟通常几十毫秒如果每次写入都要跨DC确认一次那这个系统基本没法承接高并发写入。所以现实的做法是核心账户类、资金类数据尽量保持强一致其他非关键数据接受最终一致。做架构的同学必须能跟业务方把账对齐你要强一致就得接受性能下降和成本上升。2.2 流量调度怎么把用户稳稳地送进对应的单元异地多活天然会把用户分流到不同机房那就要回答一个问题一个请求来了它应该进哪个机房最简单的思路是DNS解析把同一个域名解析到不同机房的IP让用户按区域就近接入。DNS调度粒度粗、生效慢受本地DNS缓存影响可能要几分钟甚至几十分钟而且只能按地理位置分没法精确控制到用户级别。线上最常用的方式是在应用层架设负载均衡或网关做“按用户维度”的路由用户请求进来后根据用户ID或手机号等业务标识映射到某个固定的单元。这里的关键概念叫“单元化”。我习惯把单元理解成一个小而全的“分身”一个单元里包含完整的接入层、应用层、数据层能够独立承接某一批用户的全部业务请求。用户的归属在路由层确定后所有请求都会打到同一个单元避免请求在多个机房间跳来跳去。这样调度的问题就从“把一个请求送到任意机房”变成“把一个用户的全部请求锁定到一个单元”。2.3 故障切换与脑裂最考验预案的环节故障切换是异地多活里最让人心提到嗓子眼的时刻。正常的业务请求分布在多个机房某个中心突然挂了我们需要把原本打到它的流量全部调度到其他健康中心。听起来很简单但实际操作中到处是坑。最大的风险是脑裂。假设S中心没有完全死掉只是和N中心的网络中断了两边其实都还活着。如果N中心检测到S中心“联系不上”就本能地把S中心的流量揽过来那就会出现一个很危险的状态两边同时认为自己是“活着的中心”同时承接写入流量。等网络恢复两边数据已经分了叉冲突在所难免。为了避免脑裂必须引入一个“第三方裁判”比如通过zk/etcd这类分布式协调服务做仲裁只有当确认某个机房确实处于故障状态时才允许其他机房接管它的流量。不能只靠机房之间的心跳判断因为心跳本身也可能被网络抖动欺骗。在实际操作中我们还会做一个叫“拒绝写流量”的保险丝在切换过程中先让故障单元进入只读状态或者直接把入口流量摘掉待数据全部追平后再切到健康单元。这样虽然短时间内会有一定影响但能确保不发生数据分叉。2.4 一个反常识的结论多活主要靠“砍功能”而不是“加功能”新手设计多活架构容易有一个惯性思维多活就是要让所有功能都变成多活可用的。但真正做过几个项目之后你会发现多活设计里很重要的一项工作其实是“减”把业务功能分门别类确定哪些功能必须多活、哪些功能可以降级、哪些功能干脆在多活期间先关掉。比如一个电商系统浏览商品、加购物车、下订单、支付都是核心链路需要做多活但像用户问卷、积分兑换、个性化推荐这类非实时功能完全可以设计成“允许在故障期间不可用或延迟生效”。这么做不仅仅是为了降低改造工作量更是为了降低故障切换时的复杂度非核心链路在多活架构里通常以异步、最终一致的方式附带运作一旦故障切换这些非核心链条可以被快速降级为核心链路腾出带宽和机器资源。把话说直白一点多活架构的设计过程首先是问“哪些东西可以不做”而不是“哪些东西都要做”。想清楚这一点项目推进路径会清晰很多。3. 常见的架构模式选对方案少走一半弯路3.1 同城双活先迈过这一坎很多人一提多活就想到跨城市双活但其实第一步应该做的是同城双活。同城双活在物理上部署在同一城市的两栋大楼里两者之间有专线连接网络延迟通常在1-2毫秒级别和单机房内部网络差异不大。同样的延迟量级可以让数据库层面做同步复制这意味着同城双活能实现接近强一致的数据同步。写操作在主库上执行的同时通过同步复制把binlog或redo日志实时复制到备库主备都确认写完才向客户端返回成功。这样任何一个机房挂了另一个机房都有完整的数据。同城双活的难点不在于数据复制技术而在于应用层“无状态化”改造之前应用都带本地会话、本地存储、本地缓存现在这些状态必须外置到Redis或数据库这类共享存储中保证用户从A机房切到B机房时会话和缓存仍然可用。很多老系统卡在这一步。不过相比异地双活同城双活改造量小、见效快值得所有想往多活方向走的团队先练手。3.2 异地双活与单元化架构异地双活是把机房分布在两个城市物理距离几百公里以上专线延迟至少几十毫秒。这个延迟决定了数据库同步复制方案行不通如果每次写入都要等对端机房确认写性能会被拉得很低。所以异地双活必须走异步复制加最终一致性的路线。单元化架构是异地双活最常见的落地方式。它的核心思路是把用户按维度比如用户ID哈希、手机号取模、城市维度划分成若干个“分片”每个分片完整映射到一个单元。单元之间在业务上尽量不互相调用数据也基本隔绝。举个例子假设有4个单元单元A负责25%的注册用户单元B负责另外25%以此类推。用户刘一的ID走哈希后总是落在单元A那么刘一的登录、下单、支付、查订单全部发生在单元A内部单元A是这一批用户的“家”。单元A挂了就把属于A的用户临时调度到备用单元或健康单元等数据追平后继续服务。单元化改造中最难受的部分不是路由而是斩断跨单元的依赖。比如一个下单流程涉及商品详情、库存、订单、支付、营销等多个服务如果商品信息是全局共享的、库存是全局一张表的那单元A的用户下单就和单元B的用户下单发生了耦合跨单元调用大量产生“多活”就变成了“多机房多跳”性能会非常难看。所以单元化要求核心域的数据和逻辑按用户维度垂直切分全局共享的数据比如商品描述、配置要么只在一个权威单元写入然后异步同步出去要么整套机制重新设计。3.3 两地三中心与多地多活不同名字背后的真实含义圈内经常听到几个名词容易混淆两地三中心、异地双活、三地五中心、多单元多活。两地三中心是指同城两个中心做双活再加异地一个中心做灾备。严格来说它不完全是“多活”因为异地的那个中心平时只承担备份角色不承接线上流量。它的优势是部署成本和复杂度低于真正的异地双活能应对同城整体灾难的极端场景劣势是异地中心机器利用率低切换时依然存在RTO时间。三地五中心、多地多活则是在更大的地理范围上铺开多个单元真正让流量分布在多个城市。这种模式对业务分片能力要求极高通常只有体量达到数亿用户的互联网大厂才真正具备这样的改造条件。对于绝大多数公司来说同城双活加异地灾备的“两地三中心”反而是成本与收益最均衡的方案。选型时最忌讳别人搞什么你也搞什么。我建议按这个优先级来评估如果单机房都还没做到高可用先做单机房高可用机房内有没有单点都查一遍同城双活是否能让业务容忍几秒钟或几十秒的切换抖动如果业务体量确实无法接受城市级故障再考虑异地双活和单元化。跳步进入异地双活大概率会掉进复杂度的大坑。3.4 选型对照不是越高级越好我把几种方案的关键差异整理成一个表格方便对照评估。方案机房距离网络延迟数据一致性资源利用率改造难度适用场景单机房主备内部1ms强一致依赖复制技术低低起步阶段同城双活同城1-2ms可接近强一致中高中城市级容灾绝大多数业务够用两地三中心同城异地异地域几十ms异地异步最终一致中中高城市级整体故障兜底异地双活单元化跨城几十ms最终一致高高大流量、强连续性业务你看这个表格就能发现没有一个方案是完美的。同城双活虽然数据一致性最好但它解决不了整个城市都出问题的极端场景异地双活能把资源用满但研发复杂度和业务容忍度门槛高。做决策时把这张表拿给业务方看让他们参与到取舍中来往往能避免后期无休止的扯皮。4. 关键设计细节从流量入口到数据闭环4.1 接入层调度DNS、GSLB到应用层路由用户请求从浏览器出发到最终落到某个单元的机架经过了层层调度每一层解决的问题不一样。第一层是DNS做的是“大区”级别的粗粒度调度按照用户地理位置解析到最近的机房IP。这一层的问题在于缓存生效慢、精度低但它有一个无法替代的价值作为最终的切流兜底手段可以整体把一个域的流量指向另一个区域。第二层是GSLB或者自研的全局负载均衡可以在DNS之上做更细粒度的流量分配和健康检测。比如某个机房负载过高GSLB可以按比例把部分流量切到其他区域。这一层能感知到机房级别的健康状况是切换动作的主要执行者。第三层是应用网关或服务路由层它拿着用户请求里的业务标识比如userId查表确认这个用户属于哪个单元然后把请求转发到对应单元的入口。这里要注意一个细节如果用户的主单元正处于故障状态网关不能把这个请求死等在那里而是要有“故障单元用户迁移”的预案临时把该单元的用户映射到备用单元直到故障恢复。我强调一点这三层调度并不是互相替代而是互补关系。DNS管大方向GSLB管比例和健康检测网关管精确到用户的单元归属。任何一层单独作战都做不到精细切换。4.2 单元化路由的设计与分片规则做单元化路由第一件事是定义分片键。分片键一般选用户ID、手机号这类稳定且业务核心的标识。选好后要做分片映射常见做法是哈希取模但对扩容不友好单元从4个变成8个时用户映射关系会大幅重排。更稳妥的做法是引入管理层比如用户ID先哈希到一个大的区间区间再映射到具体的单元调整单元数量时只需要移动区间映射用户归属不震荡。分片规则敲定后路由结果要尽量“前置”客户端或网关拿到userId后直接算出一个单元ID然后一路往下传。后续的所有内部调用都携带这个单元ID服务层靠它决定数据读写的位置。这里要特别注意一个边界问题如果用户的可变身份标识发生变化怎么办。比如用户的手机号换绑了而分片键恰好是手机号那么这个用户在路由表里的位置就变了他的历史订单数据还在老单元新请求进了新单元两边数据就对不上了。所以在设计分片规则时最好选用户侧的“不可变标识”或者增加一层“用户身份路由表”userId不变手机号换绑只更新路由表里绑定关系分片位置以userId计算。4.3 数据同步的三种复制方式与取舍数据同步是异地多活的重头戏主要三种方式我从实际操作角度解释。同步复制写操作必须在主备两个节点都落盘后才返回。能做到RPO为0但代价是写请求的时延等于本机写时延加网络往返时延。跨城几十毫秒的网络延迟每笔写都等一轮整个系统的写性能会非常差。所以同步复制基本只用于同城双活场景或用于关键元数据的跨机房同步。异步复制主节点写完本地立即返回复制动作在后台异步进行。这种方式性能好、吞吐高但故障切换时可能丢失最近一小段时间的写入数据。RPO不再是0而是“最近几秒或几分钟的数据”。半同步复制主节点写本地后至少等待一个备节点确认收到才返回成功。这是同步和异步之间的折中能在绝大多数情况下保证有一个备节点持有最新数据又不要求所有备节点都确认。除了数据库复制消息队列在数据同步中也扮演重要角色。单元A产生的业务变更比如订单状态变化可以打包发给MQ由MQ广播给其他单元消费。这样其他单元可以更新自己的只读副本实现最终一致。消息的幂等消费和顺序消费是这里最容易被忽视的环节处理不好会出现数据错乱而且很难排查。4.4 冲突处理与对账补偿只要走异步复制和最终一致性就一定会出现数据冲突。最典型的是用户在单元A和单元B都改了同一条业务数据的某个字段。冲突处理不能靠“谁后写谁赢”这种简单规则因为“后”这个判断在分布式环境下本身就不靠谱。常用的做法是版本号和全局时钟每条数据维护一个版本号或写入时间戳。由于各机器的本地时钟可能有偏差更可靠的做法是引入全局发号器生成全局唯一的递增版本号用版本号大小判断新旧。比较极端的场景会用到CRDT这类无冲突复制数据类型但业务模型适配门槛高实际项目中用得不多。无论冲突处理策略多好对账永远不能省。所谓对账就是周期性拿源单元的数据和目标单元的数据做比对发现不一致就触发补偿任务。我们团队在早期上线多活系统时就是靠一套每天凌晨跑的对账任务发现了好几个隐蔽的同步漏洞。在对账系统还没建设完成之前不要轻易把多活架构切到生产环境跑核心业务这是我踩过坑之后得出的结论。5. 落地路线按这个顺序推进少返工5.1 先做现状梳理与业务分级启动异地多活项目我建议不要一上来就画架构图先花两周时间做一件事把现网的系统资产和业务依赖彻底盘一遍。哪些服务是无状态的哪些服务依赖本地存储数据库有哪些核心表表与表之间的关联关系是什么服务之间的调用链是什么这些信息如果没摸清后面改造时会频繁踩到隐藏依赖的地雷。摸清现状后做业务分级把业务分成“核心强一致”“核心最终一致”“非核心可降级”三档。分级的判断标准主要看业务对一致性的敏感度资金账户余额是强一致敏感用户浏览记录是最终一致可容忍推荐位这类则完全可以降级。分级结果直接决定了每个系统的改造深度和改造顺序。先改核心强一致里那些阻塞性的依赖再改核心最终一致的链路最后处理非核心降级的逻辑。不要眉毛胡子一把抓一上来就想把所有系统一次性改造到位那会让项目周期遥遥无期。5.2 拆分单元和依赖改造现状梳理清楚后开始设计单元划分。划分的原则是“高内聚、低耦合”把某一批用户的完整业务闭环放在同一个单元内尽量减少跨单元调用。实际操作中这一步往往是整个项目最痛苦的部分。因为老系统常见的状态是在数据库层面高度耦合订单表和商品表在同一个库库存和订单在一个事务里更新。要在多活架构里把它们按用户维度拆开往往意味着领域模型的重新设计。比如库存本来是一张全局的表拆成单元后就要变成“单元库存”的模型每个单元只管自己单元用户的库存原本全局统一的库存数量现在出现了并行库存的语义这需要产品方深度参与确认规则。为了减少改造量实践中有一种常见妥协允许“跨单元读、禁止跨单元写”。强依赖的数据做异步广播副本让其他单元能读到本单元的只读数据但写入严格限制在本单元。这样改造量会小很多代价是数据可能存在短暂的读取滞后但对很多业务来说可以接受。5.3 系统架构改造清单多活改造落到具体系统上大概有这些硬性任务我列成清单供参考。应用层所有实例必须无状态化Session会话全部外置到集中式缓存。应用配置中心化做成环境隔离的配置中心故障单元可以独立调整配置。数据库层区分“中心库”和“单元库”中心库存放全局数据单元库存放分片数据两类库的同步策略各不同。分布式缓存按单元维度拆分同时为缓存做多级容灾避免缓存失效直接打垮数据库。MQ消息做好幂等消费每条消息带全局唯一ID消费者按ID去重。定时任务按单元维度隔离调度避免多个机房重复执行相同的定时任务产生脏数据。链路追踪必须强制上每个请求带单元标识这样可以快速定位一次请求跨了哪些单元。日志按单元归档故障排查时能快速定位某个单元的日志记录。这些改造任务不能只靠架构师拍脑袋要细化成具体系统的开发任务排期。不强求一口气做完但每一条都要有明确的责任人和完成时间线。5.4 演练与预案多活是练出来的多活架构不是你上线那天才验证的而是靠一次次演练练出来的。我们当时的做法是每季度做一次全量切换演练人为模拟某个机房完全故障把它的全部流量切走观察健康机房的承接能力监控数据同步追平时间记录业务影响范围。第一次演练时半模拟切换还没有触发就发现流量调度网关的缓存策略会把已经切走的用户又重新拉回故障机房第二次演练时发现某个单元的数据同步通道没有做消息积压告警数据落后了十几分钟才被察觉。这些问题都靠演练才能暴露出来。预案文档要写得极其细致具体到每一步操作由谁执行、确认的指标是什么、达到什么条件可以进入下一步。切换命令要有自动化的脚本承载不能靠人在窗口里手动敲命令。同时回切预案的重要性不亚于切换预案很多时候切过去容易切回来难回切时的数据追平和灰度验证往往比切换本身更复杂。这部分我会在最后一章展开。6. 常见问题与排查经验实录6.1 延迟抖动导致写冲突现象某次大促期间多地流量明显增加数据冲突率突然升高对账任务报出大量不一致数据。排查过程我们第一时间看数据同步通道的延迟指标发现跨城复制链路在大促流量高峰时出现了周期性的网络抖动同步消息堆积导致去重、幂等逻辑在追平数据时出现错乱。又查了冲突日志发现很多冲突集中在同一批高频更新的用户数据上。解决方式对高频更新数据引入“合并写”机制把单用户毫秒级的多次更新合并为一次写操作降低同步频率同时把同步通道的关键消息设置高优先级避免被其他业务消息挤占带宽。教训是异步复制的稳定性极度依赖链路质量和消息优先级大促前必须对同步链路做压测而不是只顾着压测应用层。6.2 切流后数据追平问题现象故障切换时某个单元的流量已经切到备用单元但备用单元的数据始终比源单元落后一段时间而且持续追不平。排查过程检查数据同步任务发现该单元生成同步消息的业务方在切流后自身也进入了降级状态部分关键的变更事件没有正常发送到MQ追平任务读的是数据库Binlog但Binlog里有些历史数据因为归档策略过期清理了一部分导致追平任务无从补拉。解决方式为关键数据表建立一份“追平专用”的变更明细存储保留时间拉长到一周以上确保任何时间点启动追平都有数据可用而不是只能看Binlog。从这次之后我们要求所有核心同步任务的链路里必须有中心化的“变更日志”兜底不能只依赖Binlog。6.3 DNS切流生效慢现象某机房故障后通过DNS把域名切到另一个区域但切换后半个多小时仍有不少用户访问故障机房报错率持续存在。排查过程这不是故障机房的进程问题而是大量用户手机或本地运营商DNS缓存了旧的解析结果。DNS解析的TTL设为300秒但由于部分各级缓存不遵守TTL实际生效时间被拖长。有些APN网络甚至会把解析结果缓存成一个小时。解决方式给DNS切换设置一个“双保险”一方面把故障机房的IP在负载均衡层直接摘掉使缓存IP的请求在到达时立即被拒绝并返回重试指令另一方面业务侧增加远程配置中心的开关当检测到API请求异常时客户端可以重新拉取域名解析。经过这次之后我们对DNS的定位是“兜底方案”精确到秒级的切流还是要依赖应用层动态配置。6.4 回切比切换更难受现象一个单元从故障中恢复后要把用户流量切回去结果回切过程中出现大量数据不一致告警业务短暂波动比当初切走时还要严重。排查过程根本原因是故障期间备用单元已经承接了该批用户的全部写流量积累了故障期间的新数据。回切时源单元虽然恢复了但它自己的数据还停留在故障前的某个时间点。如果简单把用户流量切回去用户会发现之前操作过的订单不见了数据像是“往回倒退”。解决方式回切流程必须设计成“先补数、再灰度、最后全量”。先把备用单元在故障期间产生的增量数据回传到源单元并完成校验期间源单元保持只读或不下发流量确认两边数据达到一致后先切一小部分比如5%用户验证核心链路没有问题再逐步放大流量比例。这个流程要在脚本里固化不能靠临时拍脑袋。我把这次经历写下来是因为它让我想明白了一个很实在的道理异地多活不是画一张漂亮的架构图就完事的它是一个系统工程链路里的每一处细节都可能是故障时的引爆点。而你在上线前能做的就是把能想到的意外都预先写进预案里然后反复演练。数据同步延迟是否在可接受范围、切流脚本是否经得起验证、回切方案是否考虑周全这些永远是做着比说着重要得多。
返回列表