本文 的 原文 地址
原始的内容,请参考 本文 的 原文 地址
本文 的 原文 地址
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,尼恩一直在指导大家改造简历、指导面试。指导很多小伙伴拿到了一线互联网企业网易、美团、字节、如阿里、滴滴、极兔、有赞、希音、百度、美团的面试资格,拿到大厂offer。
刚好前面几天,有小伙伴面试,遇到一些异地多活的很重要的面试题:
高可用 异地多活,如何实现?
高可用 异地容灾,你们 的 线上的方案是啥?
没有 异地多活 , 你们 项目 怎么实现 高可用呢?
前段时间小伙伴面试美团,遇到这个问题。 小伙伴没有准备好, 面试挂了。
在这里,尼恩给自己的技术自由圈(未来 超级 架构师) 社区的小伙伴, 积累一些 异地多活的架构方案和素材。这些资料的主要的目标:方便在架构指导的时候,作为参考资料。
在尼恩 之前有过多篇大厂异地多活的方案文章:
《B站刚崩,唯品会又崩:亿级用户网站的架构硬伤与解决方案》
《100Wqps异地多活,得物是怎么架构的?》
《大家都崩,美团不崩:其高可用架构,巧夺天工!》
《美团面试:ES+Redis+MySQL高可用,如何实现?》
一、 高可用架构演进与异地多活
(1)什么是 高可用?
高可用性的核心在于保障系统服务的持续可用性。
可用性(Availability)是衡量架构设计的关键指标之一,通常结合高性能与可扩展性共同构成对核心架构的要求。
可用性(Availability) 公式如下:
可用性 (Availability) = MTBF / (MTBF + MTTR) * 100%
其中,两个关键指标量化:
- 平均故障间隔 (MTBF - Mean Time Between Failure):系统正常运行的持续时长平均值。
- 故障恢复时间 (MTTR - Mean Time To Repair):系统从故障中恢复所需的时间平均值。
高可用性追求的核心是:最大限度地降低MTTR(快速恢复),使故障对用户的影响最小化。
可用性 (Availability) 结果常以“N个9”表示(如99.9%、99.99%)。
各种规模的风险(硬件故障、软件缺陷、自然灾害)都可能引发故障。
高可用架构大致的演进路径如下:一般从单一机房开始,逐步发展到同城双机房,再到同城的多个机房。
随着业务的快速发展以及部署需求的调整,进一步演进到异地多活模式。
异地多活部署模式 已经是标配
尼恩一直以来都是辅导小伙伴 改造简历,辅导面试,辅导1000多人了。
可以说, 改遍了 各大厂的简历, 各个大厂的项目, 异地多活部署模式 已经是标配 。
异地多活 (Multi-Site High Availability, MSHA) 架构 正是为了应对大规模故障(尤其是机房或城市级灾难),实现快速恢复和业务连续性的有效方案。
(2)单机架构与风险
业务初期,系统架构通常十分简单:
应用服务器与单实例数据库(如MySQL)部署在同一环境中。
核心风险:单点故障。
一旦数据库服务器因磁盘损坏、操作系统错误或误操作出现故障,所有数据将丢失。
应对措施1 - 冗余备份:
缺陷:
- 恢复时间长: 需停机恢复数据,时长取决于数据量大小。
- 数据不完整: 备份周期内的新数据会丢失。
此方案难以满足基本可用性要求。
应对措施2 - DB 的主从副本架构
引入冗余机制是解决 DB 单点 故障的关键。
对数据库层,可以采用主从复制 (Master-Slave Replication):
优势:
- 数据完整性高: 主从副本(接近)实时同步,数据差异小。
- 容错能力强: 主库故障时,可手动或自动将从库提升(Promote)为主库接管服务。
- 读性能提升: 读请求可分流至从库,减轻主库压力。
应对措施2 - 多点部署 以避免 单点故障
由于业务应用通常是无状态的,亦可部署多个实例以避免单点故障。
此时需引入负载均衡器(如 Nginx/LVS):
此架构成为应对机器级故障的主流模式。
(3)如何 实现 机房级别的容灾?
体量很小的系统,它会重点关注「用户」规模、增长。
这个阶段, 获取用户是一切。
等用户体量上来了,这个阶段会重点关注「性能」。
这个阶段, 优化接口响应时间、页面打开速度等等,这个阶段更多是关注用户体验。
等体量再大到一定规模后你会发现,「可用性」就变得尤为重要。
像微信、支付宝这种全民级的应用,如果机房发生一次故障,那整个影响范围可以说是非常巨大的。
比如经常看到这样的案例:
- 2015 年 5 月 27 日,杭州市某地光纤被挖断,近 3 亿用户长达 5 小时无法访问支付宝
- 2021 年 7 月 13 日,B 站部分服务器机房发生故障,造成整站持续 3 个小时无法访问
- 2021 年 10 月 9 日,富途证券服务器机房发生电力闪断故障,造成用户 2 个小时无法登陆、交易
- ...
那到底该怎么应对机房级别的故障呢?
没错,思想还是冗余。
只不过这次要冗余机房
(4)应对机房风险:同城灾备
即使服务器分散在多个机柜,它们仍依赖同一个机房的基础设施(电力、网络、温控等)。
机房级别的故障虽然概率低,但影响巨大(如光纤挖断、电力故障、自然灾害),案例时有发生。
系统规模越大,此类风险的影响就越不可忽视。
核心思路:机房级冗余 - 同城灾备
在同一城市的不同区域建立第二个机房(灾备机房),通过专线与主机房连通。
这里有两种方案:冷备和热备
冷备VS热备:
特性冷备 (Cold Backup)热备 (Hot Backup)定义在系统停机状态下备份数据在系统运行期间实时备份数据数据状态备份时间点前的静态数据实时动态数据(含最新操作)恢复时间(RTO)小时级 (需手动恢复)分钟级 (自动切换)数据丢失(RPO)上次备份至故障时间的数据丢失秒级丢失(或零丢失)资源占用低(仅存储成本)高(需冗余服务器+实时同步)业务影响需停止服务服务无中断技术复杂度简单(定时脚本备份)复杂(需双活架构+心跳检测)典型场景历史数据归档、合规性备份核心交易系统、实时业务成本低(存储硬件为主)高(服务器+网络+软件许可)(1)冷备方案: 仅在灾备机房做数据定时备份。
(2)热备方案: 在灾备机房部署:
- 数据库从库:实时复制主库数据。
- 业务应用:提前部署好。
- 负载均衡器:提前配置好。
- 优点: 当主生产机房故障时,只需:
- 提升灾备机房的从库为主库。
- 将流量(通过DNS或其他服务发现)切换到灾备机房的负载均衡器。
- 业务恢复(MTTR大大缩短)。
总结:
同城灾备(冷备或热备)解决了机房级故障时的数据安全和服务恢复速度问题。
热备方案通过“预部署”进一步降低了MTTR。
但灾备机房平时不承担主要生产流量,资源利用率低。
(5)效能优化:同城双活
从「成本」的角度来看,我们新部署一个机房,需要购买服务器、内存、硬盘、带宽资源,花费成本也是非常高昂的,只让它当一个后备军,未免也太「大材小用」了!
如何 最大化 备机房 的价值?
如何 提升 备机房 的利用率低?
为了解决灾备机房利用率低的问题,并增强灾备系统的实战可靠性,让灾备机房也接入真实流量,如下图
但这里有一个问题:
B 机房的存储,现在可都是 A 机房的「从库」,从库默认可都是「不可写」的,B 机房的写请求打到本机房存储上,肯定会报错,这还是不符合我们预期。
怎么办?
这时,你就需要在「业务应用」层做改造了。
你的业务应用在操作数据库时,需要区分「读写分离」(一般用中间件实现),即两个机房的「读」流量,可以读任意机房的存储,但「写」流量,只允许写 A 机房,因为主库在 A 机房。
关键总结:
(1)读写分离: 应用层需要区分读写操作。所有写操作必须指向主库(在主生产机房1)。读操作可根据规则指向本机房的从库或其他可读节点。
(2)运作模式: 两个机房均处理业务流量(主要是读,但写统一到主机房),逻辑上被视为一个整体。任何一机房故障,另一机房可接管其全部流量。
(3)优点:
- 资源利用率提升: 灾备机房参与服务,分担压力。
- 灾备能力增强: 灾备机房一直在“实战”状态,切换更可靠。
- 维护灵活性: 流量可在机房间切换,便于维护升级。
(4)缺点
- 业务层需要改造读写分离模式
- 虽然提供了机房级别的容灾能力。然而,其致命弱点是无法抵御城市级灾难(如地震、洪水)
更高性能的方案是, 双主同步的改造:
可以不同的机房的DB,做成双主模式,实现本地写入、异地同步。
(6)如何 实现 城市级容灾?
为解决城市级灾难风险,需跨地域部署机房。
常见方案是在另一个相距较远的城市(通常建议1000公里以上)部署第三个机房。
这就是 两地三中心。
2个同城机房构成双活/主从集群,异地机房只做数据冷备或异步热备(不接入流量)。
这就称为“两地三中心”
三中心的主要用于,备份和灾难后恢复,旨在防止主城市毁灭性灾难导致的数据丢失。
缺点:
- 异地机房启用时间长,流程复杂。
- 启用后服务状态不确定(未经实战)。
- 资源利用率低(基本为后备)。
此方案是银行、金融系统常用的容灾模式,但互联网追求更高可用性的场景,会进一步向异地多活演进。
(7)效能优化:异地多活
从「成本」的角度来看,我们新部署一个机房,需要购买服务器、内存、硬盘、带宽资源,花费成本也是非常高昂的,只让它当一个后备军,未免也太「大材小用」了!
如何 最大化 备机房 的价值?
如何 提升 备机房 的利用率低?
办法是: 异地多活
异地双活旨在两个异地(城市)的机房都实时提供服务,并能在任一城市发生故障时,由另一城市迅速接管全部流量。
比如字节的异地多活架构规划如下:
(7)1 异地多活 主要挑战:网络延迟
物理距离(如北京到上海1300公里)带来显著的网络延迟(通常专线RTT 30ms~100ms以上)。
相比机房内网络延迟(Netherlands->CA .... 150,000,000 ns = 150 ms[/code]北京上海两地的网络延迟时间,大致是内网网络访问速度的 60 倍(30ms/0.5ms),如果不做任何改造,一方直接访问另外一方的服务,那么我们的APP的反应会比原来慢 60 倍,其实考虑上多次往返,可能会慢600倍。
如果机房都在上海,那么网络延迟只有内网速度的2倍,可以当成一个机房使用。
所有有些公司的多活方案,会选择同城机房,把同城的几个机房当成一个机房部署,可以在不影响服务架构的情况下扩展出多个机房,不失为一个快速见效的方法。
与同城多活的方案不同,异地多活的方案会限制机房间的相互调用,需要定义清晰的服务边界,减少相互依赖,让每个机房都成为独立的单元,不依赖于其他机房。
同城多活 和 异地多活的方案 对比:
关键结论:异地RT可能慢百倍(30ms/0.5ms = 60倍),考虑多次往返可能600倍
(7)2 解决方案:单元化架构
避免高延迟的关键在于避免跨机房调用。
需保证一个用户的请求链路上所有操作(包括读写数据库)在一个机房内完成(“业务闭环”)。
具体方案:单元化架构(Cell-based Architecture)。
单元化架构 核心思想:
(1)路由分片 (Routing/Sharding): 在流量入口层(路由层),根据预设规则将特定用户(或业务范围)的请求固定路由到指定城市的机房(单元)。
(2)常见分片规则:
- 用户属性分片 (如 UserID Hash): 简单有效,保证同一用户总在同一单元。适用于普通C端业务(如电商)。
- 地理分片 (Geographic Sharding): 根据用户地理位置将请求路由到最近的单元。天然适合LBS业务(如外卖、打车)。
- 业务分片: 按业务线维度划分单元(如应用A在北京,应用B在上海)。
(3)多机房主库 (Multi-Master): 每个机房的存储层(DB, Cache, MQ)都是主库/主节点。服务读写本机房存储。
(4)数据双向同步: 每个机房写入的数据,通过数据同步中间件(如Canal+MQ、自研同步工具)可靠地双向同步到其他机房,最终保证各机房拥有全量数据。
(5)单元内闭环: 进入北京单元用户的请求,只与北京的DB/Cache/MQ交互。进入上海单元用户的请求,只与上海的DB/Cache/MQ交互。避免跨单元远程调用。
(6)容灾切换: 任一城市故障时,路由层将故障单元的所有用户流量整体切换到健康单元。该单元已有全量数据和所有服务部署,可立即接管。
字节的单元化异地多活架构基本方案:
(7)3 单元化架构的分片路由
在异地双活架构中,相较于复杂的数据合并与冲突处理方案,更为有效的策略是从流量入口源头避免数据冲突的发生。
这种做法的核心在于: 确保同一个用户的所有操作都在同一单元内完成,从而杜绝跨单元写操作。
实现这一目标的关键是采用分片路由(Sharded Routing)的流量分发机制:
下面分析常见分片策略:
(7)3.1 业务类型分片(服务级隔离)
实现原理: 按应用服务划分单元边界
示例场景:
- 北京单元承载订单服务和支付服务
- 上海单元承载用户社区和内容服务
关键优势:
- 天然避免跨单元调用:关联服务部署在同一单元
- 维护清晰的服务边界
- 适用业务:功能模块边界清晰的中台系统
注意问题: 需确保业务单元内部服务的完整闭环,避免因功能耦合导致跨单元调用。
(7)3.2 用户哈希分片(用户级隔离)
实现原理: 对用户ID哈希取模映射单元
示例场景:
- 用户ID 1-1000 分配至北京单元
- 用户ID 1001-2000 分配至上海单元
架构示意:
核心价值:
- 保证同一用户请求始终路由至相同单元
- 用户数据变更自然隔离
- 适用场景:用户数据独立性强的业务(如社交平台、电商系统)
技术实现: 哈希算法通常结合一致性哈希,支持单元弹性扩展时最小化路由变动。
(7)3.3 地理分片(区域级隔离)
实现原理: 按用户地理位置就近分配单元
示例场景:
架构示意:
独特优势:
- 天然符合地理位置敏感型业务特征
- 结合网络拓扑实现最低访问延迟
- 适用业务:本地生活服务(外卖、打车)、区域化业务
实施要点: 需建设精准的IP地理定位库,同时考虑用户跨区域移动时的会话保持。
(7)4 关键技术与难点
(1)数据同步中间件: 开发高性能、高可靠、支持断点续传、冲突检测/处理的数据同步组件是关键(如阿里Canal、自研工具)。需支持多种存储(MySQL, Redis, Kafka等)。
(2)数据冲突处理:
- 被动策略 (Conflict Detection & Resolution): 中间件检测写入冲突(短时间内同一数据在两个单元被修改),通常以时间戳(需可靠时钟)或版本号裁定最新值。实现复杂,依赖全局时钟一致性。
- 主动策略:从源头避免 (Avoidance): 通过路由规则(Sharding Key)确保对特定数据的修改只发生在同一单元内(如同一个UserID或地理围栏内的骑手ID始终在同一单元路由)。这是推荐的主要方式。
(3)“数据归属”兜底: 为防止路由规则失效或Bug导致数据被跨单元修改,服务层或中间件需做校验,拦截“不合规”的跨单元数据操作。这是保障安全性的重要兜底措施。
(4)全局数据 (Global Data) 处理: 如全局配置、库存(要求强一致),无法分片路由。通常处理方式:
- 仅在一个“主中心单元”部署写服务(遵循写本单元原则)。
- 其他单元读请求可本地读(若有从库)或代理到主中心单元(牺牲延迟换取强一致)。
- 或采用分布式强一致协议(如Paxos/Raft),但延迟可能更高。
- 重点:优先保证核心业务双活,全局数据服务按需实现,不一定强求异地双活。
- 依赖治理: 明确单元边界,梳理服务间依赖,避免跨单元RPC调用。
(5)异地多活架构要解决的关键问题:
- 区域划分:首先要选择一个划分方法(Sharding Key),对服务进行分区,让服务内聚到同一个 ezone 中。分区方案是整个多活的基础,它决定了之后的所有逻辑。
- 比如淘宝使用用户ID取模,来划分单元
- 饿了么使用骑手,商家,用户的地理位置(地理围栏)划分区域单元
- 数据复制:为了实现可用优先原则,所有机房都会有全量数据,这样用户可以随时切换到其他机房,全量数据就需要对数据进行实时复制,使用相应的中间件,对 mysql,zookeeper ,消息队列和 redis 的数据进行复制。
- 区域优先原则:因为做了异地多活,会发现多个区域的服务,数据(mysql,reids)等。因为跨区域访问,会慢600倍(饿了么的数据),所以要保证优先访问当前区域的服务,数据。
- 故障转移原则:如果当前区域的服务、数据(mysql,redis)不能访问,能够进行故障转移,来访问其他zone的的服务和数据。
二、案例:饿了么异地多活架构实践
(1)业务分析
饿了么业务核心流程包含三个关键角色交互:
业务过程中包含3个最重要的角色,分别是用户、商家和骑手,一个订单包含3个步骤:
(1) 用户打开我们的APP,系统会推荐出用户位置附近的各种美食,推荐顺序中结合了用户习惯,推荐排序,商户的推广等。用户找到中意的食物 ,下单并支付,订单会流转到商家。
(2) 商家接单并开始制作食物,制作完成后,系统调度骑手赶到店面,取走食物
(3) 骑手按照配送地址,把食物送到客户手中。
核心特征:
(1) 强地域性:用户、商家、骑手需在相同地理区域
(2) 实时性要求:订单全流程需在30分钟内完成
(3) 数据一致性挑战:三方状态实时同步
(2)架构设计原则
- 业务内聚:每个订单的全流程(用户下单、商家接单、骑手配送)必须在同一个机房(称为“ezone”)内完成,不允许跨机房调用。这是为了避免跨城网络延迟影响实时性——同一ezone内的用户、商家、骑手数据本地化,订单流转速度最快。
- 可用性优先:故障切换时,优先保证用户能下单吃饭,允许短期数据不一致(事后修复)。每个ezone存储全量业务数据,某一ezone故障后,其他ezone可无缝接管用户。
- 数据正确性:切换过程中若发现订单状态不一致,会锁定该订单阻止修改,避免错误扩散,确保核心业务(如支付、配送)数据正确。
- 业务可感:基础设施无法完全屏蔽跨机房差异,需要业务代码配合——比如识别数据归属地、过滤非本ezone数据、通过状态机纠正不一致。
(3)分片策略:地理围栏划分
为了实现业务内聚,我们首先要选择一个划分方法(Sharding Key),对服务进行分区,让用户,商户,骑手能够正确的内聚到同一个 ezone 中。分区方案是整个多活的基础,它决定了之后的所有逻辑。
根据饿了么的业务特点,我们自然的选择地理位置作为划分业务的单元,把地理位置上接近的用户,商户,骑手划分到同一个ezone,这样一个订单的履单流程就会在一个机房完成,能够保证最小的延时,在某个机房出现问题的时候,也可以按照地理位置把用户,商户,骑手打包迁移到别的机房即可。
所以我们最终选择的方案如下图,自定义地理划分围栏,用围栏把全国分为多个 shard,围栏的边界尽量按照行政省界,必要的时候做一些调整,避免围栏穿过市区。一个ezone可以包含多个 shard,某个 ezone 的 shard ,可以随时切换到另外一个 ezone ,灵活的调度资源和failover。
这样的划分方案,基本解决了垮城市下单的问题,线上没有观察到有跨 ezone 下单的情况。围栏的划分是灵活的,可以随着以后业务的拓展进行修改,因为每个机房都是全量数据,所以调整围栏不会导致问题。
对这种划分方法,有一些常见的疑问,比如:
1.如果两个城市是接壤的,会出现商家和用户处于不同 ezone 的情况,岂不是破坏了内聚性原则?
这种情况确实会出现,为了尽量避免,我们在划分shard的时候没有简单的用城市名称,而是用了复杂的地理围栏实现,地理围栏主体按照省界划分,再加上局部微调,我们最大限度的避免了跨ezone下单的情况。但如果真的出现了,用户下单也不受影响,最多只是状态有1s左右的延迟。
2.用户是会动的,如果用户从北京到了上海,那么划分规则应该怎么应对?
用户在北京下单,数据落在北京shard,到上海下单,数据则落在上海的 shard,借助于底层的数据同步工具,用户无论在什么地方,都能看到自己的数据,但是有1s左右的延时,对于大部分的业务场景,这个延迟是可以承受的。当然也有些业务场景不能接受这 1s 的延时,我们也提供了另外的方案来应对,参考下文介绍Globa Zone的章节。
3.为什么不简单点,按照用户的ID来切分?
阿里是按照用户ID的取模来划分单元的,比较简洁。我们如果也用ID做切分,同一地方的用户,商户,骑手可能被划分到不同 ezone,就会出现比较多的跨机房调用,这样就更可能出现延迟,难以保证实时性。所以,我们本地配送的业务模式,决定了需要用地理位置来划分服务。
(4)流量路由体系
基于地理位置划分规则,我们开发了统一的流量路由层(API Router),这一层负责对客户端过来的 API 调用进行路由,把流量导向到正确的 ezone。API Router 部署在多个公有云机房中,用户就近接入到公有云的API Router,还可以提升接入质量。
前端 APP 做了改造,为每个请求都带上了分流标签,API Router 会检查流量上自带的分流标签,把分流标签转换为对应的 Shard ID,再查询 Shard ID 对应的 eZone,最终决定把流量路由到哪个 ezone。
最基础的分流标签是地理位置,有了地理位置,AR 就能计算出正确的 shard 归属。但业务是很复杂的,并不是所有的调用都能直接关联到某个地理位置上,我们使用了一种分层的路由方案,核心的路由逻辑是地理位置,但是也支持其他的一些 High Level Sharding Key,这些 Sharding Key 由 APIRouter 转换为核心的 Sharding Key,具体如下图。这样既减少了业务的改造工作量,也可以扩展出更多的分区方法。
除了入口处的路由,我们还开发了 SOA Proxy,用于路由SOA调用的,和API Router基于相同的路由规则。APIRouter 和 SOAProxy 构成了流量的路由通道,让我们可以灵活的控制各种调用在多活环境下的走向。
(5)数据同步机制
为了实现可用优先原则,所有机房都会有全量数据,这样用户可以随时切换到其他机房,全量数据就需要对数据进行实时复制,饿了么开发了相应的中间件,对 mysql,zookeeper ,消息队列和 redis 的数据进行复制。
(1)Mysql 数据复制工具 DRC:
Mysql 的数据量最大,每个机房产生的数据,都通过 DRC 复制到其他 ezone,每个ezone的主键取值空间是ezoneid + 固定步长,所以产生的 id 各不相同,数据复制到一起后不会发生主键冲突。
按照分区规则,正常情况下,每个 ezone 只会写入自己的数据,但万一出现异常,2个 ezone 同时更新了同一笔数据,就会产生冲突。DRC 支持基于时间戳的冲突解决方案,当一笔数据在两个机房同时被修改时,最后修改的数据会被保留,老的数据会被覆盖。
(2)ZooKeeper 复制:
有些全局的配置信息,需要在所有机房都完全一致,饿了么开发了 zookeeper 复制工具,用于在多个机房中同步 ZK 信息。
(3)消息队列和Redis复制:
MQ,Redis 的复制与 ZK 复制类似,也开发了 相应的复制工具。
强一致保证:对于个别一致性要求很高的应用,饿了么提供了一种强一致的方案(Global Zone),Globa Zone是一种跨机房的读写分离机制,所有的写操作被定向到一个 Master 机房进行,以保证一致性,读操作可以在每个机房的 Slave库执行,也可以 bind 到 Master 机房进行,这一切都基于我们的数据库访问层(DAL)完成,业务基本无感知。
(6)整体结构
以上介绍了各个考虑的方面,现在可以综合起来看,饿了么多活的整体结构如下图:
(7)常见问题
由于平台 篇幅限制, 此处省略1000字+
原始的内容,请参考 本文 的 原文 地址
本文 的 原文 地址
来源:豆瓜网用户自行投稿发布,如果侵权,请联系站长删除 |