在定义什么是高可用,可以先定义下什么是不可用,一个网站的内容最终呈现在用户面前需要经过若干个环节,而其中只要任何一个环节出现了故障,都可能导致网站页面不可访问,这个也就是网站不可用的情况。
参考维基百科,看看维基怎么定义高可用
系统无中断地执行其功能的能力,代表系统的可用性成都,是进行系统设计时的准则之一。
这个难点或是重点在于“无中断”,要做到 7 x 24 小时任何人任何时间任何地点去访问任务服务都能得到预期的结果。
刚说到要提供一套无中断提供预计服务的系统,需要硬件,软件相结合,但是我们的硬件总是会出故障,软件也总会有bug,硬件会慢慢老化,软件会越来越复杂和庞大,除了硬件软件在本质上无法做到“无中断”,外部环境也可能导致服务的中断,例如断电,地震,火灾,光纤被挖掘机挖断,这些影响的程度可能更大。
在业界有一套比较出名的评定网站可用性的指标,常用N个9来量化可用性,可以直接映射到网站正常运行时间的百分比上
描述 | N个9 | 可用性级别 | 年度停机时间 |
---|---|---|---|
基本可用 | 2个9 | 99% | 87.6小时 |
较高可用 | 3个9 | 99% | 8.8小时 |
具备故障自动恢复能力可用 | 4个9 | 99.99% | 53分钟 |
极高可用 | 5个9 | 99.999% | 5分钟 |
之前就职的一家大型互联网公司也是按照这个指标去界定可用性,不过在执行的过程中也碰到了一些问题,例如,有一些服务的升级或数据迁移明明可以在深夜停机或停服务进行,然而考虑到以后的报告要显示出我们的系统达到了多少个9的高可用,而放弃停服务这种简单的解决方案,例如停机2个小时,就永远也达不到4个9。然而在一些高并发的场合,例如在秒杀或拼团,虽然服务停止了几分钟,但是这个对整个公司业务的影响可能是非常重大的,分分钟丢失的订单可能是一个庞大的数量。
很明显如果只是单单靠停机或不可用的时间来作为高可用评价纬度也是非常的不靠谱,设置会让技术人员为了追求四个或五个9而采用一些更复杂但没必要的方式。
我们需要一种对高可用更为科学的评估方式
我们在深夜停机一个小时和白天高峰期停机一个小时出现的影响是完全不一样的,这个也是需要根据业务而定,一般来说 To C 业务领域造成不可用是对于用户请求数的影响,换算成公式
一段时间的停机影响请求量占比 = 停机时间影响请求量 / 总的请求量 复制代码
这个纬度是从用户影响的纬度去进行评估,可以更加结合业务的纬度对业务造成多大影响去进行评估。当然做好这个评估的前提还需要对我们的服务进行分级,只有结合服务分级才能更好的进行评估。
高可用是一个比较复杂的命题,基本上在所有的处理中都会涉及到高可用,所有在设计高可用方案也涉及到了方方面面,这中间将会出现的细节是多种多样的,所以我们需要对这样一个微服务高可用方案进行一个顶层的设计。
每一个访问可能都会有多个服务组成而成,每个机器每个服务都可能出现问题,所以第一个考虑到的就是每个服务必须不止一份可以是多份,所谓多份一致的服务就是服务的冗余,这里说的服务泛指了机器的服务,容器的服务,还有微服务本身的服务。
在机器服务层面需要考虑,各个机器间的冗余是否有在物理空间进行隔离冗余 ,例如是否所有机器是否有分别部署在不同机房,如果在同一个机房是否做到了部署在不同的机柜,如果是docker容器是否部署在分别不同的物理机上面。 采取的策略其实也还是根据服务的业务而定,所以需要对服务进行分级评分,从而采取不同的策略,不同的策略安全程度不同,伴随这的成本也是不同,安全等级更高的服务可能还不止考虑不同机房,还需要把各个机房所处的区域考虑进行,例如,两个机房不要处在同一个地震带上等等
服务的冗余会要求我们可以随时对服务进行扩容或者缩容,有可能我们会从2台机器变成3台机器,想要对服务进行随时随地的扩缩容,就要求我们的服务是一个无状态化,所谓无状态化就是每个服务的服务内容和数据都是一致的。
例如,从我们的微服务架构来看,我们总共分水平划分了好几个层,正因为我们每个层都做到了无状态,所以在这个水平架构的扩张是非常的简单。
假设,我们需要对网关进行扩容,我们只需要增加服务就可以,怎么做到服务无状态呢,举个例子,在网关这一层,我们需要做鉴权的操作,需要把用户的session进行存储,这时候其实有几种方案:
每个网关单独存一份,在上层通过用户的IP一致性哈希来选择网关,这样一个用户就只会落到一台网关上。
这实际上不Ok,如果这台网关宕机了,那么这个用户的session也会丢失
每个网关都存所有的session数据,当一个网关接收到一个session,就会把这个session同步到所有的网关,那么无论访问到那一台网关都可以获取到session
这实际上也不Ok,同步session是需要时间,如果我们追求的是CAP中的AP模型,那么就会可能出现,网关还未完成同步,用户就已经请求到无同步的网关。
网关不保存任何的session数据,不提供会造成一致性的服务,将不一致的数据进行几种存储,借助更加擅长数据同步的中间件来完成。
这个是目前主流的方案,服务本身尽可能提供逻辑的服务,将数据的一致性保证集中式处理,这样就可以把“状态”抽取出来,让网关保持一个“无状态”
这里仅仅是举了网关的例子,在微服务只基本所有的服务,都应该按照这种思路去做,如果服务中有状态,就应该把状态抽取出来,让更加擅长处理数据的组件来处理,而不是在微服务中去兼容有数据的状态。
相信负载均衡这个话题基本已经深入每个做微服务开发或设计者的人心,从狭义上来说,负载均衡的实现有硬件和软件,硬件有F5,A10等机器,软件有LVS,nginx,HAProxy等等,负载均衡的算法有 random , RoundRobin , ConsistentHash等等。
上面说的基本可以属于狭义上的负载均衡,这里更想说的是广义上的负载均衡
广义上的负载均衡包含了
接下来看看一个网关对业务逻辑层的调用怎么去实现负载均衡
在网关这一种的负载均衡大部分是由注册中心进行实现
在使用ZK作为注册中心时,故障的发现是由Zk去进行发现,业务逻辑层通过watch的心跳机制将自己注册到zk上,网关对zk进行订阅就可以知道有多少可以调用的列表。当业务逻辑层在重启或者被关闭时就会跟zk断了心跳,zk会更新可调用列表。
使用zk作为负载均衡的协调器,最大的问题是zk对于服务是否可用是基于pingpong的方式,只要服务心跳存在,zk就认为服务是处在于可用状态,但是服务如果处在于假死的状态,zk是无从得知的。这个时候,业务逻辑服务是否真正可用只能够由网关知道。
为何会牵出幂等设计的问题,主要是因为负载均衡的failover策略,就是对失败的服务会进行重试,一般来说,如果是读操作的服务,重复执行也不会出问题,但想象一下,如果是一个创建订单减库存的操作,第一次调用也【业务逻辑1】超时,再重新调用了【业务逻辑2】,这个时候我们都不能确认超时调用的【业务逻辑1】是否真的被调用,有可能根本就调用不成功,有可能已经调用成功但是因为某些原因返回超时而已,所以,很大程度这个接口会被调用2次。如果我们没有保证幂等性,就有可能一个订单导致了减少2次的库存。
所谓的幂等性,就是得保证在同一个业务中,一个接口被调用了多次,其导致的结果都是一样的。
怎么做幂等性设计 : 看下 《「如何设计」具备幂等性的服务》
在每一次调用,时间越长存在超时的风险就越大,逻辑越复杂执行的步骤越多存在失败的风险也就越大,如果在业务允许的情况下,用户调用只给用户必须要的结果,而不是需要同步的结果可以放在另外的地方异步去操作,这就减少了超时的风险也把复杂业务进行拆分减低复杂度。
当然异步化的好处是非常多,例如削封解耦等等,这里只是从可用的角度出发。
异步化大致有这三种的实现方式:
什么是柔性化,想象一个场景,我们的系统会给每个下单的用户增加他们下单金额对应的积分,当一个用户下单完毕后,我们给他增加积分的服务出现了问题,这个时候,我们是要取消掉这个订单还是先让订单通过,积分的问题通过重新或者报警来处理呢?
所谓的柔性化,就是在我们业务中允许的情况下,做不到给予用户百分百可用的通过降级的手段给到用户尽可能多的服务,而不是非得每次都交出去要么100分或0分的答卷。
怎么去做柔性化,更多其实是对业务的理解和判断,柔性化更多是一种思维,需要对业务场景有深入的了解。
在电商订单的场景中,下单,扣库存,支付是一定要执行的步骤,如果失败则订单失败,但是加积分,发货,售后是可以柔性处理,就算出错也可以通过日志报警让人工去检查,没必要为加积分损失整个下单的可用性
先来讲讲微服务中限流/熔断的目的是什么,微服务后,系统分布式部署,系统之间通过rpc框架通信,整个系统发生故障的概率随着系统规模的增长而增长,一个小的故障经过链路的传递放大,有可能会造成更大的故障。
限流跟高可用的关系是什么,假定我们的系统最多只能承受500个人的并发访问,但整个时候突然增加到1000个人进来,一下子就把整个系统给压垮了,本来还有500个人能享受到我们系统的服务,突然间变成了所有人都无法得到服务,与其让1000人都不法得到服务,不如就让500个人得到服务,拒绝掉另外500个人。限流是对访问的隔离,是保证了部门系统承受范围内用户的可用性。
熔断跟高可用的关系是什么,上面说了微服务是一个错综复杂的调用链关系,假设 模块A 调用 模块B , 模块B 又调用了 模块C , 模块C 调用了 模块D,这个时候,模块D 出了问题出现严重的时延,这个时候,整个调用链就会被 模块D 给拖垮,A 等B,B等C,C等D,而且A B C D的资源被锁死得不到释放,如果流量大的话还容易引起雪崩。熔断,主动丢弃 模块D 的调用,并在功能上作出一些降级才能保证到我们系统的健壮性。 熔断是对模块的隔离,是保证了最大功能的可用性。
之前上面说的服务冗余,可以简单的理解为计算的高可用,计算高可用只需要做到无状态既可简单的扩容缩容,但是对于需要存储数据的系统来说,数据本身就是有状态。
跟存储与计算相比,有一个本质的差别:
将数据从一台机器搬到另一台机器,需要经过线路进行传输
网络是不稳定的,特别是跨机房的网络,ping的延时可能是几十几百毫秒,虽然毫秒对于人来说几乎没有什么感觉,但是对于高可用系统来说,就是本质上的不同,这意味着整个系统在某个时间点上,数据肯定是不一致的。按照“数据+逻辑=业务”的公式来看,数据不一致,逻辑一致,最后的业务表现也会不一致。举个例子
无论是正常情况下的传输延时,还是异常情况下的传输中断,都会导致系统的数据在某个时间点出现不一致,而数据的不一致又会导致业务出现问题,但是如果数据不做冗余,系统的高可用无法保证
所以,存储高可用的难点不在于怎么备份数据,而在于如何减少或者规避数据不一致对业务造成的影响
分布式领域中有一个著名的CAP定理,从理论上论证了存储高可用的复杂度,也就是说,存储高可用不可能同时满足“一致性,可用性,分区容错性”,最多只能满足2个,其中分区容错在分布式中是必须的,就意味着,我们在做架构设计时必须结合业务对一致性和可用性进行取舍。
存储高可用方案的本质是将数据复制到多个存储设备中,通过数据冗余的方式来现实高可用,其复杂度主要呈现在数据复制的延迟或中断导致数据的不一致性,我们在设计存储架构时必须考虑到一下几个方面:
主从复制是最常见的也是最简单的存储高可用方案,例如Mysql,redis等等
其架构的优点就是简单,主机复制写和读,而从机只负责读操作,在读并发高时候可用扩张从库的数量减低压力,主机出现故障,读操作也可以保证读业务的顺利进行。
缺点就是客户端必须感知主从关系的存在,将不同的操作发送给不同的机器进行处理,而且主从复制中,从机器负责读操作,可能因为主从复制时延大,出现数据不一致性的问题。
刚说了主从切换存在两个问题: 1.主机故障写操作无法进行 2.需要人工将其中一台从机器升级为主机
为了解决这个两个问题,我们可以设计一套主从自动切换的方案,其中设计到对主机的状态检测,切换的决策,数据丢失和冲突的问题。
1.主机状态检测
需要多个检查点来检测主机的机器是否正常,进程是否存在,是否出现超时,是否写操作不可执行,读操作是否不可执行,将其进行汇总,交给切换决策
2.切换决策
确定切换的时间决策,什么情况下从机就应该升级为主机,是进程不存在,是写操作不可这行,连续检测多少失败次就进行切换。应该选择哪一个从节点升级为主节点,一般来说或应该选同步步骤最大的从节点来进行升级。切换是自动切换还是半自动切换,通过报警方式,让人工做一次确认。
3.数据丢失和数据冲突 数据写到主机,还没有复制到从机主机就挂了,这个时候怎么处理,这个也得考虑业务的方式,是要确保CP或AP
还要考虑一个数据冲突的问题,这个问题在mysql中大部分是由自增主键引起,就算不考虑自增主键会引起数据冲突的问题,其实自增主键还要引起很多的问题,这里不细说,避免使用自增主键。
上述的数据冗余可以通过数据的复制来进行解决,但是数据的扩张需要通过数据的分片来进行解决(如果在关系型数据库是分表)。
何为数据分片(segment,fragment, shard, partition),就是按照一定的规则,将数据集划分成相互独立、正交的数据子集,然后将数据子集分布到不同的节点上。
HDFS , mongoDB 的sharding 模式也基本是基于这种分片的模式去实现,我们在设计分片主要考虑到的点是: