站在业务开发的角度,平时写代码时,代码的可靠性是需要我们考虑的东西,例如对于一个简单的业务场景,我们或许会使用幂等的Retry操作来尽量保证在一些些突发情况如MySQL抖动时能尽量处理成功当前业务;对于存储层来说,我们或许会使用本地缓存、Redis、MySQL三层存储结构保证系统读性能的可靠性,或是配置MySQL的读写分离来保证MySQL的读写性能;

站在架构设计的角度,本篇从宏观的角度来介绍保证系统可用性的架构方案——异地多活,内容包括:异地多活到底是什么?为什么需要异地多活?它到底解决了什么问题?究竟是怎么解决的?

1. 系统可用性

要想理解异地多活,我们需要从「架构设计」的原则说起;一个好的软件架构应该遵循以下3个原则:

  • 高性能
  • 高可用
  • 易扩展

高性能,意味着系统拥有更大流量的处理能力,更低的响应延迟;例如1秒可处理10W并发请求,接口响应时间5ms等等;

易扩展,表示系统在迭代新功能时,能以最小的代价去扩展,系统遇到流量压力时,可以在不改动代码的前提下,去扩容系统;

而「高可用」这个概念,看起来很抽象,怎么理解它呢?通常用2个指标来衡量:

(1)平均故障间隔 MTBF(Mean Time Between Failure):表示两次故障的间隔时间,也就是系统「正常运行」的平均时间,这个时间越长,说明系统稳定性越高;

(2)故障恢复时间 MTTR(Mean Time To Repair):表示系统发生故障后「恢复的时间」,这个值越小,故障对用户的影响越小;

可用性与这两者的关系如下,这个公式得出的结果是一个「比例」:


衡量系统的高可用性,一般通过SLA,全称Service Level Agrement,通常我们会用「N个9」来描述一个系统的可用性;

对于SLA指标来说,9的数字越多可用性越高,宕机时间越少,系统就可以在给定的时刻内高比例地正常工作,从而对系统的挑战就越大,投入的成本也会越高;比如5个9要求系统每年只宕机5分钟左右,而4个9要求每年宕机时间不超过一个小时;这就使得系统需要在架构设计、基础设施、数据备份等不同层面采取多种方式来保证可用性;

描述

可用性级别

日故障时间

年故障时间

可用

90%

2.4小时

36.5天

基本可用

99%

14分钟

87.6小时

较高可用性

99.9%

86秒

8.76小时

具备自恢复能力的可用性

99.99%

8.6秒

52.4分钟

极高可用性

99.999%

0.86秒

5.24分钟

系统发生故障其实是不可避免的,尤其是规模越大的系统,发生问题的概率也越大;这些故障一般体现在3个方面:

硬件故障:CPU、内存、磁盘、网卡、交换机、路由器
软件问题:代码Bug、版本迭代
不可抗力:地震、水灾、火灾、战争

这些风险其实都有可能发生,代码BUG是最常见的,磁盘损坏在我的工作期间也遇到过,新闻上也曾发布过"xx市挖地铁时挖断光纤导致xx购物APP无法使用";所以,在面对故障时,我们的系统能否以「最快」的速度恢复,就成为了可用性的关键;

可如何做到快速恢复呢?

这篇文章要讲的「异地多活」架构,就是为了解决这个问题,而提出的高效解决方案;接下来会分析一个系统会遇到哪些可用性问题,以及为什么架构要这样演进,从而理解异地多活架构的意义;

2. 保证可用性的方案

2.1 数据备份

业务处于起步阶段,体量非常小,架构模型可以非常简单:客户端请求进来,业务应用读写数据库,返回结果,非常好理解;

这里的数据库「单机」部署的,所以它有一个致命的缺点:一旦遭遇意外,例如MySQL部署节点磁盘损坏、操作系统异常、运维误删数据,那这意味着所有业务数据就全部「丢失」了,这个损失是巨大的;解决这个问题,最容易想到一个方案——备份

mysql mgr 异地双活 mysql异地双活架构_java

你可以对数据做备份,把数据库文件「定期」copy 到另一台机器上,这样,即使原机器丢失数据,你依旧可以通过备份把数据「恢复」回来,以此保证数据安全。

这个方案实施起来虽然比较简单,但存在 2 个问题:

(1)恢复需要时间:业务需先停机,再恢复数据,停机时间取决于恢复的速度,恢复期间服务「不可用」
(2)数据不完整:因为是定期备份,数据肯定不是「最新」的,数据完整程度取决于备份的周期;很明显,你的数据库越大,意味故障恢复时间越久,那按照前面我们提到的「高可用」标准,这个方案可能连1个9都达不到,远远无法满足我们对可用性的要求;

那有什么更好的方案,既可以快速恢复业务?还能尽可能保证数据完整性呢?

这时你可以采用MySQL自带的实时同步数据的方案——主从同步;

2.2 主从同步

mysql mgr 异地双活 mysql异地双活架构_java_02

可以在另一台机器上再部署一个数据库实例,让这个新实例成为原实例的「副本」,让两者保持「实时同步」;我们一般把原实例叫作主库(master),新实例叫作从库(slave),这个方案的优点在于:

数据完整性高:主从副本实时同步,数据「差异」很小;
抗故障能力提升:主库有任何异常,从库可随时「切换」为主库,继续提供服务;
读性能提升:业务应用可直接读从库,分担主库「压力」读压力;

这个方案不仅可以提高了数据库的可用性,还可以通过配置「读写分离」提升MySQL的读写性能;想想也是,既然都挂载了一个从库,只用来备份数据也太浪费了,完全可以将读流量接入进来,写主库读从库;

2.3 服务多节点部署

同样的思路,你的「业务应用」也可以在其它机器部署一份,避免单点;因为业务应用通常是「无状态」的(不像数据库那样存储数据),所以直接部署即可,非常简单;

因为业务应用部署了多个,所以你现在还需要部署一个「接入层」,来做请求的「负载均衡」(一般会使用nginx或LVS),这样当一台机器宕机后,另一台机器也可以「接管」所有流量,持续提供服务;甚至,接入层也可以是集群部署来保证可用性;

最终的架构示例如下:

mysql mgr 异地双活 mysql异地双活架构_mysql mgr 异地双活_03

从这个方案你可以看出,提升可用性的关键思路就是——「冗余」;

无论是服务层还是存储层,担心一个实例故障,那就部署多个实例,担心一个机器宕机,那就部署多台机器;这种架构基本就是现在的互联网服务的主流方案;


除了上面提到的多节点部署,还有要注意的吗?

现在让我们把视角下放,把焦点放到具体的「部署细节」上来。

按照前面的分析,为了避免单点故障,你的应用虽然部署了多台机器,但这些机器的分布情况,我们并没有去深究;而一个机房有很多服务器,这些服务器通常会分布在一个个「机柜」上,如果你使用的这些机器,刚好在一个机柜,还是存在风险;如果恰好连接这个机柜的交换机/路由器发生故障,那么你的应用依旧有「不可用」的风险;

部署在一个机柜有风险,那把这些机器打散,分散到不同机柜上,是不是就没问题了?

这样确实会大大降低出问题的概率,但我们依旧不能掉以轻心,因为无论怎么分散,它们总归还是在一个相同的环境下:机房

那继续追问,机房会不会发生故障呢?

一般来讲,建设一个机房的要求其实是很高的,地理位置、温湿度控制、备用电源等等,机房厂商会在各方面做好防护;但即使这样,我们每隔一段时间还会看到这样的新闻:

2015年5月27日,杭州市某地光纤被挖断,近3亿用户长达5小时无法访问支付宝;
2021年7月13日,B站部分服务器机房发生故障,造成整站持续3个小时无法访问;
2021年10月9日,富途证券服务器机房发生电力闪断故障,造成用户2个小时无法登陆、交易;
...

可见,即使机房级别的防护已经做得足够好,但只要有「概率」出问题,那现实情况就有可能发生;虽然概率很小,但一旦真的发生,影响之大可见一斑;

看到这里你可能会想,机房出现问题的概率也太小了吧,有必要考虑得这么复杂吗?但你有没有思考这样一个问题:不同体量的系统,它们各自关注的重点是什么?

体量很小的系统,它会重点关注「用户」规模、增长,这个阶段获取用户是一切;等用户体量上来了,这个阶段会重点关注「性能」,优化接口响应时间、页面打开速度等等,这个阶段更多是关注用户体验;等体量再大到一定规模后你会发现,「可用性」就变得尤为重要,像微信、支付宝这种全民级的应用,如果机房发生一次故障,那整个影响范围可以说是非常巨大的;

所以,再小概率的风险,我们在提高系统可用性时,也不能忽视;那到底该怎么应对机房级别的故障呢?——还是冗余

想要抵御「机房」级别的风险,那应对方案就不能局限在一个机房内了;

2.4 同城灾备

要做机房级别的冗余方案,也就是说,你需要再搭建一个机房来部署服务;简单起见,你可以在「同一个城市」再搭建一个机房,原机房我们叫作A机房,新机房叫B机房;

注意:这两个机房的网络用一条「同城专线」连通,几乎没有延迟,因此可用来做数据的实时同步;

这时考虑一个问题:如果A机房真挂掉了,如何保证服务不中断呢?

业务不中断是不可能的,我们能做的只能是尽可能缩短业务恢复的时间;为此,需要把在A机房首次部署的事宜在B机房「提前」做好,也就是说,你需要在B机房提前部署好接入层、业务应用,等待异常时随时切换,架构就变成了这样:

mysql mgr 异地双活 mysql异地双活架构_数据_04

这样的话,A机房整个挂掉,我们只需要做2件事即可:

(1)B机房所有从库提升为主库
(2)DNS指向B机房,接入流量,业务恢复

两个机房唯一的区别是,A机房的存储都是主库,而B机房都是从库;

这种方案,我们把它叫做「热备」;

热的意思是指,B机房处于「待命」状态,A故障后B可以随时「接管」流量,继续提供服务;热备相比于冷备最大的优点是:随时可切换;而「冷备」一般是仅做存储层的实时备份,跨机房切换流量时,需要在B机房部署接入层、配置转发规则、部署应用启动服务,这些操作都是有时间开销的;

2.5 同城双活

虽然我们有了应对机房故障的解决方案,但还是心存疑问:如果A机房真的挂掉,把全部流量切到B机房,B机房能否真的如我们所愿,正常提供服务?

这就好比有两支军队A和B,A军队历经沙场,作战经验丰富,而B军队只是后备军只学过理论知识,并没有实战经验;如果A军队丧失战斗能力,需要B军队立即顶上时,作为指挥官的你,肯定也会担心B军队能否真的担此重任吧?

我们的架构也是如此,此时的B机房虽然是随时「待命」状态,但A机房真的发生故障,我们要把全部流量切到B机房,其实是不敢百分百保证它可以「如期」工作的!

另外,从「成本」的角度来看,我们新部署一个机房,需要购买服务器、内存、硬盘、带宽资源,花费成本也是非常高昂的,只让它当一个后备军,未免也太「大材小用」了;

因此,可以让B机房也接入流量,实时提供服务,这样做是有好处的:

(1)可以实时训练这支后备军,让它达到与A机房相同的作战水平,随时可切换;
(2)B机房接入流量后,可以分担A机房的流量压力;这才是把B机房资源优势,发挥最大化的最好方案;

那怎么让B机房也接入流量呢?很简单,就是把B机房的接入层IP地址,加入到DNS中,这样,B机房从上层就可以有流量进来了;如下图所示:

mysql mgr 异地双活 mysql异地双活架构_数据_05


注意:上面举的A、B两个机房的例子里,对于同一个机房,部署了NGINX、应用服务、MySQL节点,仅仅是为了方便来说明问题;事实上,为了尽量避免「把鸡蛋放进同一个篮子」,一般我们是不会把接入层、存储层和服务层都部署在同一个机房的;实际上,图里的NGINX、应用服务、MYSQL节点都各自部署在不同的机房,他们之间通过「同城专线」连通,几乎没有什么延迟损耗

实际的部署架构示意图如下:

mysql mgr 异地双活 mysql异地双活架构_mysql_06

接下来,为方便表述,继续以A、B两个机房为例来说明问题;


现在,因为B机房实时接入了流量,此时如果A机房挂了,那我们就可以「大胆」地把A的流量,全部切换到B机房,完成快速切换!

到这里你可以看到,我们部署的B机房,在物理上虽然与A有一定距离,但整个系统从「逻辑」上来看,我们是把这两个机房看做一个「整体」来规划的,也就是说,相当于把2个机房当作1个机房来用;

这种架构方案,比前面的「同城灾备」更进了一步,B机房实时接入了流量,还能应对随时的故障切换,这种方案我们把它叫做「同城双活」;

因为两个机房都能处理业务请求,这对我们系统的内部维护、改造、升级提供了更多的可实施空间(流量随时切换),现在,整个系统的弹性也变大了,可用性明显提升;一般的业务做到这个程度也就差不多了

2.6 两地三中心

还是回到风险上来说;A、B这2个机房其实还是处于「一个城市」内,如果是整个城市发生自然灾害,例如地震、水灾,那2个机房依旧存在"全军覆没"的风险;

没办法,继续冗余;但这次冗余机房部署在「异地」;通常建议两个机房的距离要在1000公里以上,这样才能应对城市级别的灾难;

假设之前的A、B机房在北京,那这次新部署的C机房可以放在上海;按照前面的思路,把C机房用起来,最简单粗暴的方案还就是做「冷备」,即定时把A、B机房的数据,在C机房做备份,防止数据丢失;

这种方案,就是我们经常听到的「两地三中心」:两地是指2个城市,三中心是指有3个机房,其中2个机房在同一个城市,并且同时提供服务,第3个机房部署在异地,只做数据灾备;

这种架构方案,通常用在银行、金融、政企相关的项目中;它的问题还是前面所说的,启用灾备机房需要时间,而且启用后的服务,不确定能否如期工作;所以,要想真正的抵御城市级别的故障,越来越多的互联网公司,开始实施「异地双活」;

3. 异地双活

3.1 不能忽视的网络延迟和网络质量

根据上面的「同城双活」方案,那异地双活是不是直接"照搬"同城双活的模式去部署就可以了呢?我们不再把A、B机房部署在同一个城市,而是异地部署,例如A机房放在北京,B机房放在上海;

那么跨地机房之间如何通信呢?也有「同城专线」类似的通信方式,叫「跨城专线」;但是,因为两个机房物理距离距离较远,现在两地之间的网络延迟就变成了「不可忽视」的因素了!

例如,北京到上海的距离大约1300公里,即使架设一条高速的「网络专线」,光纤以光速传输,一个来回也需要近10ms的延迟;况且,网络线路之间还会经历各种路由器、交换机等网络设备,实际延迟可能会达到30ms~100ms,如果网络发生抖动,延迟甚至会达到1秒;

不止是延迟,远距离的网络专线质量,是远远达不到机房内网络质量的,专线网络经常会发生延迟、丢包、甚至中断的情况;总之,不能过度信任和依赖「跨城专线」;

你可能会问,这点延迟对业务影响很大吗?影响非常大!

试想,一个客户端请求打到上海机房,上海机房要去读写北京机房的存储,一次跨机房访问延迟就达到了30ms,这大致是机房内网网络(0.5ms)访问速度的60倍(30ms/0.5ms),一次请求慢60倍,来回往返就要慢100倍以上;

而我们在App打开一个页面,可能会访问后端几十个API,每次都跨机房访问,整个页面的响应延迟有可能就达到了秒级,这个性能简直惨不忍睹,难以接受;

可见,虽然我们只是简单的把机房部署在了「异地」,但「同城双活」的架构模型,在这里就不适用了!

3.2 数据如何访问

这里再思考一个问题,跨地的存储层数据访问如何解决?如果A、B机房写入了不同的数据,通过中间件双向同步数据,整合后不会有什么问题(此时肯定就不能使用「自增主键」了,而是「分布式主键」,保证整合数据时不会报主键冲突);

但是如果A、B机房在很短的时间内更新了相同行的数据,数据发生了「冲突」;这是一个很严重的问题,系统发生故障并不可怕,可怕的是数据发生「错误」,因为修正数据的成本太高了;

方案1:数据同步中间件要有自动「合并」数据、解决「冲突」的能力;

这个方案实现起来比较复杂,要想合并数据,就必须要区分出「先后」顺序;很容易想到的方案就是以「时间」为标尺,以「最新」的请求为准;但这种方案需要两个机房的「时钟」严格保持一致才行,否则很容易出现问题;例如:

第1个请求先落到北京机房,北京机房时钟是10:01,修改X=1;
第2个请求再落到上海机房,上海机房时钟是10:00,修改X=2;

因为北京机房的时间「更晚」,那最终结果就会是X=1;但这里两地的机房时钟没有对齐,其实应该以第2个请求为准,X=2才对;可见,完全「依赖」时钟的冲突解决方案,不太严谨;

所以,通常会采用第二种方案,从「源头」就避免数据冲突的发生;

方案2:数据独立存储,从源头避免发生读写冲突的情况;

有点类似按照用户id分表的思想,某个指定用户只会路由到固定的分表;同理,从源头避免数据冲突的思路是:在最上层接入流量时,就不要让冲突的情况发生

具体来讲就是,要在最上层就把请求「区分」开,部分用户请求固定打到北京机房,其它用户请求固定打到上海机房,进入某个机房的用户请求,之后的所有业务操作,都在这一个机房内完成,从根源上避免「跨机房」;

所以,需要在接入层之上,再部署一个「路由层」(通常部署在云服务器上),自己可以配置路由规则,根据规则把用户「分流」到不同的机房内;

mysql mgr 异地双活 mysql异地双活架构_mysql_07

常见的路由规则有:

  1. 按业务类型分片
  2. 用户ID哈希分片
  3. 按地理位置分片

1. 按业务类型分片

按业务类型在不同机房接入流量,需要考虑多个应用之间的依赖关系,要尽可能的把完成「相关」业务的应用部署在同一个机房,避免跨机房调用;例如,订单、支付服务有依赖关系,会产生互相调用,那这 2 个服务在 A 机房接入流量;社区、发帖服务有依赖关系,那这 2 个服务在 B 机房接入流量;

2. 用户ID哈希分片

这种方案就是,最上层的路由层,会根据用户ID计算「哈希」取模,然后从路由表中找到对应的机房,之后把请求转发到指定机房内;

举例:一共200个用户,根据用户ID计算哈希值,然后根据路由规则,把用户1-100路由到北京机房,101-200用户路由到上海机房,这样,就避免了同一个用户修改同一条数据的情况发生;

3. 按地理位置分片

这种方案,非常适合与地理位置密切相关的业务,例如打车、外卖服务就非常适合这种方案;

拿外卖服务举例,你要点外卖肯定是「就近」点餐,整个业务范围相关的有商家、用户、骑手,它们都是在相同的地理位置内的;针对这种特征,就可以在最上层,按用户的「地理位置」来做分片,分散到不同的机房;

举例:北京、河北地区的用户点餐,请求只会打到北京机房,而上海、浙江地区的用户,请求则只会打到上海机房;这样的分片规则,也能避免数据冲突;

总之,分片的核心思路在于,让同一个用户的相关请求,只在一个机房内完成所有业务「闭环」,不再出现「跨机房」访问;

当然,最上层的路由层把用户分片后,理论来说同一个用户只会落在同一个机房内,但不排除程序Bug导致用户会在两个机房「漂移」;安全起见,每个机房在写存储时,还需要有一套机制,能够检测「数据归属」,避免不该写本机房的情况发生;

现在,两个机房就可以都接收「读写」流量(做好分片的请求),底层存储保持「双向」同步,两个机房都拥有全量数据,当任意机房故障时,另一个机房就可以「接管」全部流量,实现快速切换;此外,我们还可以更细化地优化路由规则,让用户访问就近的机房,这样整个系统的性能也会大大提升;

注意这里还有一种情况,是无法做数据分片的:全局数据;例如系统配置、商品库存这类需要强一致的数据,这类服务依旧只能采用写主机房,读从机房的方案,不做双活;双活的重点,是要优先保证「核心」业务先实现双活,并不是「全部」业务实现双活;

3.3 异地多活

理解了异地双活,那「异地多活」顾名思义,就是在异地双活的基础上,部署多个机房即可;

这些服务按照「单元化」的部署方式,可以让每个机房部署在任意地区,随时扩展新机房,只需要在最上层定义好分片规则就好了;

mysql mgr 异地双活 mysql异地双活架构_数据_08

为了方便同步数据,将各个节点双向同步的「网状」架构升级为「星状」,及即设立一个「中心机房」,任意机房写入数据后,都只同步到中心机房,再由中心机房同步至其它机房;但与此同时,这个中心机房的「稳定性」要求会比较高;不过也还好,即使中心机房发生故障,我们也可以把任意一个机房,提升为中心机房,继续按照之前的架构提供服务;

至此,我们的系统彻底实现了「异地多活」!

3.4 小结

总结一下:

  1. 一个好的软件架构,应该遵循高性能、高可用、易扩展3大原则,其中「高可用」在系统规模变得越来越大时,变得尤为重要,能以「最快」的速度恢复故障,才是高可用追求的目标;
  2. 提升高可用的核心思想是「冗余」,主从副本、同城灾备/双活、异地双活/多活都是在做冗余;
  3. 灾备分为冷备和热备,冷备只备份数据,不提供服务;热备实时同步数据,并做好随时切换的准备;
  4. 双活比灾备的优势在于,两个机房都可以接入「读写」流量,提高可用性的同时,还提升了系统性能;
  5. 异地双活是抵御「城市」级别灾害的更好方案,两个机房同时提供服务,故障随时可切换,可用性高;但同时实现也最复杂;异地多活是实现高可用的最终方案;

参考:

nginx高可用方案 - 知乎

搞懂异地多活,看这篇就够了