系统性能三指标
要想理解异地多活,我们需要从架构设计的原则说起
现如今,我们开发一个软件系统,对其要求越来越高,一个好的软件架构应该遵循以下3个原则:
- 高性能
- 高可用
- 易扩展
- 高性能:系统拥有更大流量的处理能力,同时接口返回的速度应该尽可能的快。
- 易扩展:系统在迭代新功能的时候,能够以最小的代价去扩展,系统遇到流量压力的时候,可以在不改动代码的前提下去扩容系统。
- 高可用:通常由两个指标来衡量,分别是平均故障时间和故障恢复时间
不同的软件,不同阶段的公司,产品开发的不同阶段,对这些指标的要求是不一样的:比如一个初创公司,这个时候用户、流量最重要,开发功能,让页面尽可能美观,做产品比其他指标更重要;当流量上来了之后,怎么尽可能缩短响应时间,让系统能处理的QPS更大,就成了至关重要的问题;当产品使用的人数足够多,影响力足够大,可用性的重要就凸现出来了,怎么保证系统尽可能稳定,不出问题,对于公司来说才是最重要的。
系统发生故障是不可避免的,尤其是规模越大的系统,互相之间的调用也更加复杂,对于硬件的要求也越高,从理论上来说发生问题的概率也越大。这些故障一般提现在3个方面:
- 硬件故障:CPU 内存 磁盘 网卡 交换机 路由器
- 软件问题:代码BUG 版本迭代 线上故障等等
- 不可抗力:地震 水灾 火灾 战争
我们通常用N个9来表示系统的可用性高不高,从数据上能看出来,越到后面系统可用性带来的平均受益是越小的,但是难度是指数级别上升的。我们平常写的小玩具和企业级的应用差别也无非是这几个方面:高并发、高性能、高可用。
这些风险随时都有可能发生,所以在面对故障的时候,系统能否以【最快】的速度恢复,就成了可用性的关键。
如何做到快速恢复呢?
异地多活就是为了解决这个问题,而提出的高效解决方案
单机房
单机
客户端请求先进来,业务应用读写数据库,返回结果
这里的数据库是单机部署的,所以有一个致命的缺点:一旦遭遇意外,例如磁盘损坏、操作系统异常、误删数据,这意味着所有数据就全部【丢失】了,这个损失是巨大的
备份
我们可以对数据做备份,将数据库文件【定期】copy到另一台机器上,这样即使原机器丢失数据,依旧可以通过备份把数据【恢复】回来,以此保证数据安全
这个方案实施起来虽然比较简单,但存在两个问题
- 恢复需要时间:业务需要先停机,在恢复数据,这段时间服务是不可用的,对于一个系统来说这显然不能忍受
- 数据不完整:因为是定期备份,数据肯定不是最新的,会有丢数据的风险。这里补充一句,其实现代系统想要做到数据实时强一致性,几乎是不可能的
主从
可以在另一台机器上,再部署一个数据库实例,成为这个原实例的副本,让两者保持【实时同步】,这里的实时同步要打上引号,因为两台机器有网络隔离,永远不可能真正的实时同步,比如当主库写一条数据,还没同步的时候就宕机了,这个时候从库就会有丢数据的可能。
我们一般把原实例称为主库(master),新实例称作从库(slave),这个方案的优点在于
- 数据完整性高:主从副本实时同步,数据差异很小
- 抗故障能力提升:主库有任何异常,从库可以随时切换为主库,继续提供服务
- 读性能提示:从库可以直接用来读
主从+多机器
同样的,业务应用也也可以在其他机器部署一份,避免单点。因为业务应用通常是【无状态】的,这里的无状态很好理解,业务代码的逻辑部分(除去数据库的部分),在什么机器上都能跑,且不会对机器造成持久化的影响,不像数据库一样存储数据,所以直接部署即可,非常简单。
因为业务应用部署了多个,所以现在还需要一个接入层,来做请求的负载均衡,一般是用nginx或者是lvs,这样当一台机器宕机之后,另一台机器也可以【接管】所有流量,持续提供服务。
提高可用性的核心思想
从这些方案可以看出,提升可用性的关键思路就是:冗余
担心一个实例故障,那就部署多个实例;担心一个机器宕机,那就部署多台机器;担心一个数据库可能会崩然后丢数据,那就多整几个数据库;这种冗余的思想放在机房层面,就产生了同城灾备、同城双活等方案;放在城市层面,就产生了两地三中心、异地双活、异地多活等方案
以上说的方案还是有缺点的,因为应用虽然部署了多台机器,但是这些机器的分布情况,我们并没有去深究。
而一个机房有很多服务器,这些服务器通常会分布在一个个【机柜】上,如果使用的机器刚好在一个机柜,还是存在风险。
如果恰好链接这个机柜的交换机/路由器发生故障,那么你的应用依旧有【不可用】的风险
哪怕是在不同机柜上,依旧会有风险,因为它们始终还是属于一个机房。
机房的故障率从现实角度来分析其实真的很低,建设一个机房的要求是很高的,地理位置、温湿度控制、备用电源等等。机房厂商会在各方面做好防护,但即使这样,还是有以下事故
- 15年支付宝因为光纤被挖断,5小时无法访问支付宝
- 21年b站服务器着火,3小时无法访问
- 21年富途证券服务器断电,2小时无法访问
可见,哪怕机房级别的防护已经做的足够好,但只要有概率出现问题,那现实情况就有可能发生。虽然概率很小,但一旦发生,就会造成重大损失。
像前文所说的一样,不同体量的系统,关注的重点是不一样的。小系统关注的重点是用户,这个阶段用户的规模、增长就是一切。在用户体量上来之后,会重点关注性能,优化接口响应时间,接口打开速度等等。这个阶段更多的是关注用户体验,而体量再大下去,可用性就会变得尤为重要。像微信、支付宝这种全民级别的应用,如果机房发生一次故障,那么影响和损失都是巨大的
我们该如何应对机房级别的故障呢?没错,还是冗余
多机房
同城灾备
简单起见,可以在同一个城市再搭建一个机房,原机房为A,新机房为B,这两个机房的网络用一条【专线】连通。
为了避免A机房故障导致数据丢失,所以我们需要把数据在B机房也做【定时备份】。这种方案,我们成为【冷备】。因为B机房只做备份,不提供服务,只有在A机房故障的时候才会弃用。
或者可以把AB之间的关系换成主从的关系,这样不仅能提高系统吞吐量,也能够更加保证数据的完整性
在这个方案的设想中,如果A机房真挂掉了,要想保证服务不中断,还需要做这些事情
- B机房所有从库升级成主库
- 在B机房部署应用,启动服务
- 部署接入层,配置转发规则
- DNS指向B机房接入层,接入流量,业务恢复
整个过程的每一步需要人为介入,且需要花费大量时间,回复之前整个服务还是不可用的,如果想要做到故障之后立即【切换】,就需要考虑下面这种架构
这样的话,A机房整个挂掉,我们只需要做两件事
- B机房所有从库提升为主库
- DNS指向B机房接入层,接入流量,业务回复
这种方案我们叫【热备】,热备相比于冷备的最大优点是随时可切换,不同点有需要多加一层应用层和接入层,同时数据库层面的定时备份变成了实时备份,这些都是需要额外开销的。我们把这两个方案统称为:同城灾备
同城灾备的最大优势在于,我们不用担心【机房】级别的故障了,一个机房发生风险,我们只需要把流量切换到另一个机房,当然这不一定会没有问题,比如冷备的问题是之前的备用系统没有经过流量的测试,不一定能扛得住;热备也是,瘫了一个主系统,那么备用系统的压力范围,也不一定能抗住。
同城双活
虽然有了应对机房故障的解决方案,但是有个问题是不能忽略的:A机房挂掉,全部流量切到B机房,B机房是否真的能如我们所愿,正常提供服务?
另外从成本的角度上看,我们新部署一个机房,需要购买很多硬件资源,花费成本也是非常高昂的,如果只是放在那里不去使用,是很浪费资源的一种表现。
因此我们需要让B机房也接入流量,实时提供服务
只需要把B机房的接入层IP地址,加入到dns服务中,这样B机房从上层就可以有流量进来了
这里有一个新问题:B机房的存储都是从库,而从库默认都是不可写的,也就是说B机房是处理不了写请求的。这个问题就应该在业务应用层解决,需要区分读写分离,一般是通过中间件实现,写流量给A机房,读流量可以给两个机房
这种架构有什么问题呢?
两地三中心
因为把两个机房当成一个整体来规划,如果是一个城市的话,当整个城市发生自然灾害的时候,例如地震、水灾,那么依旧可能有【全局覆没】的风险
这个时候就可以将备份机房放在另一个城市
两地三中心就是指两个城市,三个机房,其中2个机房在同一个城市,并且同时提供服务,第三个机房部署在异地,制作数据灾备。
这种架构方案,通常用在银行、金融、政企相关的项目中,问题还是启用后的服务,不确定能否如期工作。
所以想要真正抵御城市级别的故障,越来越多的互联网公司,开始实施【异地双活】
异地双活
主要问题是跨机房的延迟调用,当B地的应用去跨区域读写A地的存储,网络延迟就会让整个请求变得非常慢。而要解决这个问题,就必须在存储层做改造了。
B机房的存储不再是从库,而也要变为主库,同时两个机房的数据还要【互相同步】,无论客户端写哪一个机房,都要把数据同步到另一个机房。因为只有两个机房都拥有全量数据,才能支持任意切换机房,持续提供服务。MySQL本身是提供了双主架构的,支持双向数据复制,但平时用的不多。而且Redis、mongoDB等数据库是没有这个功能的,所以必须开发对应的【数据同步中间件】来实现双向同步的功能。
除了数据库这种有状态的软件之外,通常还会用到消息队列,例如rabbitMQ,kafka等,这些也是有状态的服务,所以它们也需要开发双向同步的中间件,支持任意机房写入数据,同步至另一个机房
业界开源出了很多数据同步中间件,例如阿里的canal、redisshake、mongoshake,可分别在两个机房同步MySQL、REDIS、MONGODB数据
这样的话有一个新的问题,两个机房都可以写,如果操作的是同一条数据,就很容易发生竞态的问题
分别有两个方案
- 消息同步中间件要有自动解决数据的能力,区分出操作的先后顺序
- 从源头避免数据冲突的发生
一般都是采用第二种方案:在最上层接入流量的时候,就不要让冲突的情况发生。
具体来讲就是将用户区分开,部分用户请求固定达到北京机房,其他用户请求固定打到上海机房。进入某个机房的用户请求,之后的所有业务操作,都在这一个机房内完成,从根源上避免【跨机房】。
这时候需要在接入层之上,再部署一个路由层,自己配置路由规则,把用户分流到不同的机房内。
一般来说有三种方式
- 按业务类型分片,比如某个子域的请求固定全打在某个机房
- 直接哈希分片,先对请求进行哈希,再对机房的数量进行取模,这样可以保证流量均匀分布到某个机房,但是对于某些请求来说可能速度会慢,比如一个新疆的请求,打到了广州机房,网络延迟就会比打在西安机房大
- 按地理位置分片,请求只会打在距离自己最近的机房,处理请求的速度快,但是流量不均匀
异地多活
把异地双活的思想推到多个城市,部署多个机房