前言:多「活」、多「备」是两个相对的概念,设计和实现的难度相差很大,不要搞混了
1.为什么要做多活
在一些极端场景下,有可能所有服务器都出现故障,例如机房断电、机房火灾、地震等这些不卡抗拒因素会导致系统所有服务器都故障从而导致业务整体瘫痪,而且即使有其他地区的备份,把备份业务系统全部恢复到能够正常提供业务,花费的时间也比较长。为了满足中心业务连续性,增强抗风险能力,多活作为一种可靠的高可用部署架构,成为各大互联网公司的首要选择。
1.1.多活场景
- 多活架构的关键点就是指不同地理位置上的系统都能够提供业务服务,这里的“活”是指实时提供服务的意思。
- 与“活”对应的是字是“备”,备是备份,正常情况下对外是不提供服务的,如果需要提供服务,则需要大量的人工干预和操作,花费大量的时间才能让“备”变成“活。
单纯从描述来看多活很强大,能够保证在灾难的情况下业务都不受影响,是不是意味着不管什么业务,我们都要去实现多活架构呢?其实不是,实现多活架构都要付出一定的代价,具体表现为:
- 不同多活方案实现复杂度不一样,随着业务规模和容灾级别的提升,多活方案会给业务系统设计带来更大复杂度
- 不管采用哪种多活方案都难以完全避免跨机房甚至是跨地区服务调用带来的耗时增加
- 多活会带来成本会上升,毕竟要多在一个或者多个机房搭建独立的一套业务系统
因此,多活虽然功能很强大,但也不是每个业务都要上多活。例如,企业内部的 IT 系统、管理系统、博客站点等,如果无法承受异地多活带来的复杂度和成本,是可以不做异地多活的,而对于重要的业务例如核心金融、支付、交易等有必要做多活。
1.2.多活方案
常见的多活方案有同城双活、两地三中心、三地五中心、异地多活等多种技术方案,不同多活方案技术要求、建设成本、运维成本都不一样
2.架构演变
2.1.单机架构
2.1.1.最简单的单机:单库
先从简单的讲起,假设业务处于起步阶段,体量非常小,那架构是这样的:
这个架构模型非常简单,客户端请求进来,业务应用读写数据库,返回结果,非常好理解。
但需要注意的是,这里的数据库是「单机」部署的,所以它有一个致命的缺点:一旦遭遇意外,例如磁盘损坏、操作系统异常、误删数据,那这意味着所有数据就全部「丢失」了,这个损失是巨大的。
2.1.2.存储「冷备份」
如何避免这个问题呢?我们很容易想到一个方案:备份。
你可以对数据做备份,把数据库文件「定期」cp 到另一台机器上,这样,即使原机器丢失数据,你依旧可以通过备份把数据「恢复」回来,以此保证数据安全。
这个方案实施起来虽然比较简单,但存在 2 个问题:
- 恢复需要时间:业务需先停机,再恢复数据,停机时间取决于恢复的速度,恢复期间服务「不可用」
- 数据不完整:因为是定期备份,数据肯定不是「最新」的,数据完整程度取决于备份的周期
很明显,你的数据库越大,意味故障恢复时间越久。那按照前面我们提到的「高可用」标准,这个方案可能连 1 个 9 都达不到,远远无法满足我们对可用性的要求。
那有什么更好的方案,既可以快速恢复业务?还能尽可能保证数据完整性呢?
这时你可以采用这个方案:主从副本。
2.2.主从副本
2.2.1.主从复制:存储「热备份」
你可以在另一台机器上,再部署一个数据库实例,让这个新实例成为原实例的「副本」,让两者保持「实时同步」,就像这样:
我们一般把原实例叫作主库(master),新实例叫作从库(slave)。这个方案的优点在于:
- 数据完整性高:主从副本实时同步,数据「差异」很小
- 抗故障能力提升:主库有任何异常,从库可随时「切换」为主库,继续提供服务
- 读性能提升:业务应用可直接读从库,分担主库「压力」读压力
这个方案不错,不仅大大提高了数据库的可用性,还提升了系统的读性能。
2.2.2.应用部署在其他机器
同样的思路,你的「业务应用」也可以在其它机器部署一份,避免单点。因为业务应用通常是「无状态」的(不像数据库那样存储数据),所以直接部署即可,非常简单。
注意:主库提供读/写,从库只读
2.2.2.部署接入层,实现负载均衡
因为业务应用部署了多个,所以你现在还需要部署一个「接入层」,来做请求的「负载均衡」(一般会使用 nginx 或 LVS),这样当一台机器宕机后,另一台机器也可以「接管」所有流量,持续提供服务。
上面的方案,还是存在风险:对于机房级别的故障是无法避免的
2.3.同城灾备
同城灾备分为「冷备」和「热备」,冷备只备份数据,不提供服务,热备实时同步数据,并做好随时切换的准备
想要抵御「机房」级别的风险,那应对方案就不能局限在一个机房内了。
机房级别的冗余方案
- A、B两个机房采用「同城专线」连接
- 存储「热备份」
- 备份B机房,提前部署好接入层、应用层,等待随时切换
补充说明:
- 两个机房唯一的区别是,A 机房的存储都是主库,而 B 机房都是从库
- 热的意思是指,B 机房处于「待命」状态,A 故障后 B 可以随时「接管」流量,继续提供服务。热备相比于冷备最大的优点是:随时可切换。
这样的话,A 机房整个挂掉,只需要做 2 件事即可:
- B 机房所有从库提升为主库
- DNS 指向 B 机房接入层,接入流量,业务恢复
同城灾备的最大优势在于,再也不用担心「机房」级别的故障了,一个机房发生风险,只需把流量切换到另一个机房即可,可用性再次提高。
2.3.同城双活
同城双活比灾备的优势在于,两个机房都可以接入「读写」流量,提高可用性的同时,还提升了系统性能。虽然物理上是两个机房,但「逻辑」上还是当做一个机房来用。
同城双活与上面同城灾备的不同点是:同城双活的备份B机房,接入了外部的流量,提供服务。
补充说明:
因为 A 机房的存储都是主库,所以我们把 A 机房叫做「主机房」,B 机房叫「从机房」。B机房是「从机房」,只对外提供读服务。B机房的写操作要转到A机房(因此,需要在业务上进行改造,对于存储的操作,要区分读写请求)
虽然把 2 个机房当做一个整体来规划,但这 2 个机房在物理层面上,还是处于「一个城市」内,如果是整个城市发生自然灾害,例如地震、水灾(河南水灾刚过去不久),那 2 个机房依旧存在「全局覆没」的风险。
但这次冗余机房,就不能部署在同一个城市了,你需要把它放到距离更远的地方,部署在「异地」。
2.4.两地三中心
两地三中心是在同城双活的基础上,额外部署一个异地机房做「灾备」,用来抵御「城市」级别的灾害,但启用灾备机房需要时间。
两地三中心 = 同城双活、异地灾备
- 两地:2个城市
- 三中心:3个机房
- 2个机房在同一个城市,同时提供服务
- 第3个机房部署在异地,只做数据灾备
补充说明:“两地”带来最大的难题是「网络延迟」,因此,异地的应用不要“跨网络”去访问异地的存储
2.5.异地双活
异地双活才是抵御「城市」级别灾害的更好方案,两个机房同时提供服务,故障随时可切换,可用性高。但实现也最复杂,理解了异地双活,才能彻底理解异地多活
Q:先提个问题,异地双活是不是直接「照搬」同城双活的模式去部署就可以了呢?
A:不行的,“异地”会带来最大难题「网络延迟」。一次跨机房访问延迟就达到了 30ms,这大致是机房内网网络(0.5 ms)访问速度的 60 倍(30ms / 0.5ms),一次请求慢 60 倍,来回往返就要慢 100 倍以上
- 同城双活可以采用「同城专线」,B机房写A机房的存储是很快的
- 异城双活即使采用「跨城专线」,B机房写A机房的存储还是要跨越地区,延时是无法被业务接收的
因此,引出一个原则:在A城市的机房,不允许读写B城市的机房的存储
2.5.1.初始架构
架构介绍
- 两个机房,都部署了接入层、应用层、存储层
- 由DNS实现流量的转发,两个机房的应用可以同时对外提供服务
- 两个机房的存储必须都是「主库」,而且两个机房的数据还要「互相同步」数据,即客户端无论写哪一个机房,都能把这条数据同步到另一个机房。
难点1:怎么实现这种「双主」架构呢?它们之间如何互相同步数据?
A:对于存储中间件,要开发对应的「数据同步中间件」来实现双向同步的功能。业界也开源出了很多数据同步中间件,例如阿里的 Canal、RedisShake、MongoShake,可分别在两个机房同步 MySQL、Redis、MongoDB 数据。
难点2:两个机房都可以写,如果修改的是同一条的数据,发生冲突怎么办?这是一个很严重的问题,系统发生故障并不可怕,可怕的是数据发生「错误」,因为修正数据的成本太高了。我们一定要避免这种情况的发生。
A:解决这个问题,有 2 个方案
方案1:数据同步中间件要有自动「合并」数据、解决「冲突」的能力
这个方案实现起来比较复杂,要想合并数据,就必须要区分出「先后」顺序。我们很容易想到的方案,就是以「时间」为标尺,以「后到达」的请求为准。
但这种方案需要两个机房的「时钟」严格保持一致才行,否则很容易出现问题。完全「依赖」时钟的冲突解决方案,不太严谨。
方案2:从「源头」就避免数据冲突的发生,即「在最上层接入流量时,就不要让冲突的情况发生」
2.5.2.路由层「分片」
避免数据冲突的解决方案:「在最上层接入流量时,就不要让冲突的情况发生」
具体来讲就是,要在最上层就把用户「区分」开,部分用户请求固定打到北京机房,其它用户请求固定打到上海机房,进入某个机房的用户请求,之后的所有业务操作,都在这一个机房内完成,从根源上避免「跨机房」。
所以这时,你需要在接入层之上,再部署一个「路由层」(通常部署在云服务器上),自己可以配置路由规则,把用户「分流」到不同的机房内。
但这个路由规则,具体怎么定呢?有很多种实现方式,最常见的总结了 3 类:
1.按地理位置分片
非常适合与地理位置密切相关的业务,例如打车、外卖服务就非常适合这种方案。
拿外卖服务举例,你要点外卖肯定是「就近」点餐,整个业务范围相关的有商家、用户、骑手,它们都是在相同的地理位置内的。举例:北京、河北地区的用户点餐,请求只会打到北京机房,而上海、浙江地区的用户,请求则只会打到上海机房。这样的分片规则,也能避免数据冲突。
2.按业务类型分片
假设一共有 4 个应用,北京和上海机房都部署这些应用。但应用 1、2 只在北京机房接入流量,在上海机房只是热备。应用 3、4 只在上海机房接入流量,在北京机房是热备。
这样一来,应用 1、2 的所有业务请求,只读写北京机房存储,应用 3、4 的所有请求,只会读写上海机房存储。
2.直接哈希分片
会根据用户 ID 计算「哈希」取模,然后从路由表中找到对应的机房,之后把请求转发到指定机房内。
举例:一共 200 个用户,根据用户 ID 计算哈希值,然后根据路由规则,把用户 1 - 100 路由到北京机房,101 - 200 用户路由到上海机房,这样,就避免了同一个用户修改同一条数据的情况发生。
分片的核心思路在于,让同一个用户的相关请求,只在一个机房内完成所有业务「闭环」,不再出现「跨机房」访问。阿里在实施这种方案时,给它起了个名字,叫做「单元化」。
2.5.3.全局数据
这里还有一种情况,是无法做数据分片的:全局数据。例如系统配置、商品库存这类需要强一致的数据,这类服务依旧只能采用写主机房,读从机房的方案,不做双活。
双活的重点,是要优先保证「核心」业务先实现双活,并不是「全部」业务实现双活。
总结:到这里你可以看出,完成这样一套架构,需要投入的成本是巨大的。
路由规则、路由转发、数据同步中间件、数据校验兜底策略,不仅需要开发强大的中间件,同时还要业务配合改造(业务边界划分、依赖拆分)等一些列工作,没有足够的人力物力,这套架构很难实施。
参考:
搞懂异地多活,看这篇就够了 | Kaito's Blogmysql - 同城双活与异地多活架构分析 - vivo 互联网技术 - SegmentFault 思否