高可用计算架构和高可用存储架构的设计目的都是为了解决部分服务器故障时,如何保证系统能够继续提供服务。
在一些极端场景下,可能所有服务器都故障(例如,机房断电、机房火灾、地震、水灾等)导致业务整体瘫痪,即使有其他地区的备份,把备份业务系统全部恢复到能够正常提供业务,花费的时间也比较长。因为备份系统平时不对外提供服务,可能会存在很多隐藏的问题没有发现。如果期望达到即使在此类灾难性故障时,业务也不受影响,或者能够很快恢复,就需要设计异地多活架构。
判断一个系统是否符合异地多活,需要满足两个标准:
实现异地多活架构代价很高,具体表现为:
常见的新闻网站、企业内部的 IT 系统、游戏、博客站点等,可以不做异地多活的;因为这类业务系统即使中断,对用户的影响并不会很大。
共享单车、滴滴出行、支付宝、微信这类业务,就需要做异地多活;这类业务系统中断后,对用户的影响很大。
如果业务规模很大,尽量做异地多活:
根据地理位置上的距离来划分,异地多活架构可以分为同城异区、跨城异地、跨国异地。
将业务部署在同一个城市不同区的多个机房,然后将两个机房用专用的高速网络连接在一起。如果考虑一些极端场景(例如,停电、水灾),同城异地似乎没什么作用。
但同城的两个机房,距离上一般大约是几十千米,通过搭建高速的网络,两个机房能够实现和同一个机房内几乎一样的网络传输速度。这意味着虽然是两个不同地理位置上的机房,但逻辑上可以看作同一个机房,这样的设计大大降低了复杂度,减少了异地多活的设计和实现复杂度及成本。
极端灾难发生概率是比较低的,但机房火灾、机房停电、机房空调故障这类问题发生的概率更高,而且破坏力一样很大。而这些故障场景,同城异地架构都可以很好地解决。
因此,结合复杂度、成本、故障发生概率来综合考虑,同城异地是应对机房级别故障的最优架构。
将业务部署在不同城市的多个机房,而且距离最好要远一些,这样才能有效应对某一地区的极端灾难事件。但“距离较远”这点并不只是一个距离数字上的变化,而是量变引起了质变,导致了跨城异地的架构复杂度大大上升。
距离增加带来的最主要问题是两个机房的网络传输速度会降低,这是物理定律的限制:
除了距离上的限制,中间传输各种不可控的因素也非常多。
以上描述的问题,虽然同城异地理论上也会遇到,但由于距离较短,中间经过的线路和设备较少,问题发生的概率会低很多。
而且同城异地距离短,即使是搭建多条互联通道,成本也不会太高,而跨城异地距离太远,搭建或者使用多通道的成本会高不少。
跨城异地距离较远带来的网络传输延迟问题,给异地多活架构设计带来了复杂性,如果要做到真正意义上的多活,业务系统需要考虑部署在不同地点的两个机房,在数据短时间不一致的情况下,还能够正常提供业务。
这就引入了一个看似矛盾的地方:数据不一致业务肯定不会正常,但跨城异地肯定会导致数据不一致。
如何解决这个问题呢?重点还是在“数据”上,即根据数据的特性来做不同的架构。
将业务部署在不同国家的多个机房。相比跨城异地,跨国异地的距离更远,数据同步延迟更长,正常情况下可能就有几秒钟。这种程度的延迟已经无法满足异地多活标准的第一条:“正常情况下,用户无论访问哪一个地点的业务系统,都能够得到正确的业务服务”。
虽然跨城异地也会有此类同步延迟问题,但几十毫秒的延迟对用户来说基本无感知的;而延迟达到几秒钟就感觉比较明显了。
跨国异地多活的主要应用场景一般有这几种情况:
因此,跨城异地多活是架构设计复杂度最高的一种,下面是跨城异地多活架构设计的一些技巧和步骤。
“异地多活”是为了保证业务的高可用,但并不是要保证所有业务都能“异地多活”!
为了架构设计去改变业务规则(特别是核心的业务规则)是得不偿失的。优先实现核心业务的异地多活架构!
异地多活本质上是通过异地的数据冗余,来保证在极端异常的情况下业务也能够正常提供给用户,因此数据同步是异地多活架构设计的核心。但并不是要所有数据都实时同步!
异地多活架构面临一个无法彻底解决的矛盾:业务上要求数据快速同步,物理上正好做不到数据快速同步,因此所有数据都实时同步,实际上是一个无法达到的目标。
既然是无法彻底解决的矛盾,那就只能想办法尽量减少影响。有几种方法可以参考:
数据同步是异地多活架构设计的核心,幸运的是基本上存储系统本身都会有同步的功能。虽然绝大部分场景下,存储系统本身的同步功能基本上也够用了,但在某些比较极端的情况下,存储系统本身的同步功能可能难以满足业务需求。
尤其是异地多机房这种部署,各种各样的异常情况都可能出现,只考虑存储系统本身的同步功能时,就会发现无法做到真正的异地多活。
解决的方案是将多种手段配合存储系统的同步来使用,甚至可以不采用存储系统的同步方案,改用自己的同步方案:
某些场景下无法保证 100% 的业务可用性,总是会有一定的损失。因此,异地多活也无法保证 100% 的业务可用,例如:
这是由物理规律决定的,光速和网络的传播速度、硬盘的读写速度、极端异常情况的不可控等,都是无法 100% 解决的。
所以要忍受这一小部分用户或者业务上的损失,否则本来想为了保证最后的 0.01% 的用户的可用性,做一个完美方案,结果却发现 99.99% 的用户都保证不了了。
对于某些实时强一致性的业务,实际上受影响的用户会更多,甚至可能达到 1/3 的用户。
以银行转账这个业务为例,假设小明在北京 XX 银行开了账号,如果小明要转账,一定要北京的银行业务中心才可用,否则就不允许小明自己转账。
针对银行转账这个业务,虽然无法做到“实时转账”的异地多活,但可以通过特殊的业务手段让转账业务也能实现异地多活。例如,转账业务除了“实时转账”外,还提供“转账申请”业务,即小明在上海业务中心提交转账请求,但上海的业务中心并不立即转账,而是记录这个转账请求,然后后台异步发起真正的转账操作,如果此时北京业务中心不可用,转账请求就可以继续等待重试;假设等待 2 个小时后北京业务中心恢复了,此时上海业务中心去请求转账,发现余额不够,这个转账请求就失败了。
“转账申请”的这种方式虽然有助于实现异地多活,但其实还是牺牲了用户体验的,本来一次操作的事情,分为两次:一次提交转账申请,另外一次是要确认是否转账成功。
虽然无法做到 100% 可用性,为了让用户心里更好受一些,可以采取一些措施进行安抚或者补偿,例如:
采用多种手段,保证绝大部分用户的核心业务异地多活!
按照一定的标准将业务进行分级,挑选出核心的业务,只为核心业务设计异地多活,降低方案整体复杂度和实现成本。
常见的分级标准有下面几种:
以用户管理系统为例,“登录”业务符合“访问量大的业务”和“核心业务”这两条标准,因此登录是核心业务。
挑选出核心业务后,需要对核心业务相关的数据进一步分析,目的在于识别所有的数据及数据特征,这些数据特征会影响后面的方案设计。
常见的数据特征分析维度有:
确定数据的特点后,可以根据不同的数据设计不同的同步方案。常见的数据同步方案有:
无论数据同步方案如何设计,一旦出现极端异常的情况,总是会有部分数据出现异常的。例如,同步延迟、数据丢失、数据不一致等。
异常处理就是假设在出现这些问题时,系统采取的应对措施。异常处理主要有以下几个目的:
常见的异常处理措施有这几类:
异地多活方案主要应对系统级的故障,例如,机器宕机、机房故障、网络故障等问题,虽然影响很大,但发生概率较小,而接口级别的故障虽然影响没有那么大,但是发生的概率比较高。
接口级故障的典型表现就是系统并没有宕机,网络也没有中断,但业务却出现问题了:
导致接口级故障的原因一般有下面几种:
解决接口级故障的核心思想和异地多活基本类似:优先保证核心业务和优先保证绝大部分用户。
系统将某些业务或者接口的功能降低,只提供部分功能或完全停止所有功能。降级的核心思想就是丢车保帅,优先保证核心业务。
常见的实现降级的方式有:
熔断机制实现的关键是需要有一个统一的 API 调用层,由 API 调用层来进行采样或者统计,如果接口调用散落在代码各处就没法进行统一处理了。
熔断机制实现的另外一个关键是阈值的设计,例如 1 分钟内 30% 的请求响应时间超过 1 秒就熔断,这个策略中的“1 分钟”“30%”“1 秒”都对最终的熔断效果有影响。实践中一般都是先根据分析确定阈值,然后上线观察效果,再进行调优。
限流只允许系统能够承受的访问量进来,超出系统访问能力的请求将被丢弃,保证一部分请求能够正常响应。限流一般都是系统内实现的,常见的限流方式可以分为两类:基于请求限流和基于资源限流。
从外部访问的请求角度考虑限流,常见的方式有:限制总量、限制时间量。
无论是限制总量还是限制时间量,共同的特点都是实现简单,但在实践中面临的主要问题是比较难以找到合适的阈值。
例如系统设定了 1 分钟 10000 个用户,但实际上 6000 个用户的时候系统就扛不住了;也可能达到 1 分钟 10000 用户后,其实系统压力还不大,但此时已经开始丢弃用户访问了。
即使找到了合适的阈值,基于请求限流还面临硬件相关的问题。
例如一台 32 核的机器和 64 核的机器处理能力差别很大,阈值是不同的,可能有的技术人员以为简单根据硬件指标进行数学运算就可以得出来,实际上这样是不可行的,64 核的机器比 32 核的机器,业务处理性能并不是 2 倍的关系,可能是 1.5 倍,甚至可能是 1.1 倍。
为了找到合理的阈值,通常情况下可以采用性能压测来确定阈值,但性能压测也存在覆盖场景有限的问题,可能出现某个性能压测没有覆盖的功能导致系统压力很大;另外一种方式是逐步优化,即:先设定一个阈值然后上线观察运行情况,发现不合理就调整阈值。
基于上述的分析,根据阈值来限制访问量的方式更多的适应于业务功能比较简单的系统,例如负载均衡系统、网关系统、抢购系统等。
常见的内部资源有:连接数、文件句柄、线程数、请求队列等。
例如,采用 Netty 来实现服务器,每个请求都先放入一个队列,业务线程再从队列读取请求进行处理,队列长度最大值为 10000,队列满了就拒绝后面的请求;也可以根据 CPU 的负载或者占用率进行限流,当 CPU 的占用率超过 80% 的时候就开始拒绝新的请求。
基于资源限流相比基于请求限流能够更加有效地反映当前系统的压力,但实践中设计也面临两个主要的难点:
通常情况下,这也是一个逐步调优的过程,即:设计的时候先根据推断选择某个关键资源和阈值,然后测试验证,再上线观察,如果发现不合理,再进行优化。
排队实际上是限流的一个变种,限流是直接拒绝用户,排队是让用户等待一段时间。排队虽然没有直接拒绝用户,但用户等了很长时间后进入系统,体验并不一定比限流好。
由于排队需要临时缓存大量的业务请求,单个系统内部无法缓存这么多数据,一般情况下,排队需要用独立的系统去实现,例如使用 Kafka 这类消息队列来缓存用户请求。