高可用并不是一套整体解决方案,而是由诸多环节组成,一环扣一环,鬼知道为了这些串联起来的环节,我得出多少张牌去应对,才能最终组成一个整个系统的高可用落地方案。
![为了做到微服务的高可用,鬼知道我出了多少张牌](https://i-blog.csdnimg.cn/blog_migrate/f711c3eac8b659bcc0333062eb6d9217.png)
什么是高可用
在定义什么是高可用,可以先定义下什么是不可用,一个网站的内容最终呈现在用户面前需要经过若干个环节,而其中只要任何一个环节出现了故障,都可能导致网站页面不可访问,这个也就是网站不可用的情况。
参考维基百科,看看维基怎么定义高可用:
系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。
这个难点或是重点在于“无中断”,要做到 7x24 小时无中断无异常的服务提供。
为什么需要高可用
一套对外提供服务的系统是需要硬件,软件相结合,但是我们的硬件总是会出故障,软件会有 Bug,硬件会慢慢老化,网络总是不稳定,软件会越来越复杂和庞大。
除了硬件软件在本质上无法做到“无中断”,外部环境也可能导致服务的中断,例如断电,地震,火灾,光纤被挖掘机挖断,这些影响的程度可能更大。
高可用的评价纬度
在业界有一套比较出名的评定网站可用性的指标,常用 N 个 9 来量化可用性,可以直接映射到网站正常运行时间的百分比上:
之前就职的一家互联网公司也是按照这个指标去界定可用性,不过在执行的过程中也碰到了一些问题。
例如,有一些服务的升级或数据迁移明明可以在深夜停机或停服务进行,然而考虑到以后的报告要显示出我们的系统达到了多少个 9 的高可用,而放弃停服务这种简单的解决方案,例如停机 2 个小时,就永远也达不到 4 个 9。
然而在一些高并发的场合,例如在秒杀或拼团,虽然服务停止了几分钟,但是这个对整个公司业务的影响可能是非常重大的,分分钟丢失的订单可能是一个庞大的数量。
所以 N 个 9 来量化可用性其实也得考虑业务的情况。
微服务高可用设计手段
高可用是一个比较复杂的命题,基本上在所有的处理中都会涉及到高可用,所有在设计高可用方案也涉及到了方方面面。
这中间将会出现的细节是多种多样的,所以我们需要对这样一个微服务高可用方案进行一个顶层的设计,围绕服务高可用,先检查下我们手里有多少张牌。
服务冗余
①冗余策略
每一个访问可能都会有多个服务组合而成,每个机器每个服务都可能出现问题,所以第一个考虑到的就是每个服务必须不止一份可以是多份。
所谓多份一致的服务就是服务的冗余,这里说的服务泛指了机器的服务,容器的服务,还有微服务本身的服务。
在机器服务层面需要考虑,各个机器间的冗余是否有在物理空间进行隔离冗余。
例如是否所有机器分别部署在不同机房,如果在同一个机房是否做到了部署在不同的机柜,如果是 Docker 容器是否部署在分别不同的物理机上面。
采取的策略其实也还是根据服务的业务而定,所以需要对服务进行分级评分,从而采取不同的策略。
不同的策略安全程度不同,伴随着的成本也是不同,安全等级更高的服务可能还不止考虑不同机房,还需要把各个机房所处的区域考虑进行。
例如,两个机房不要处在同一个地震带上等等。
②无状态化
服务的冗余会要求我们可以随时对服务进行扩容或者缩容,有可能我们会从 2 台机器变成 3 台机器。
想要对服务进行随时随地的扩缩容,就要求我们的服务是一个无状态化,所谓无状态化就是每个服务的服务内容和数据都是一致的。
例如,从我们的微服务架构来看,我们总共分水平划分了好几个层,正因为我们每个层都做到了无状态,所以在这个水平架构的扩张是非常的简单。
假设,我们需要对网关进行扩容,我们只需要增加服务就可以,而不需要去考虑网关是否存储了一个额外的数据。
网关不保存任何的 Session 数据,不提供会造成一致性的服务,将不一致的数据进行几种存储,借助更加擅长数据同步的中间件来完成。
这个是目前主流的方案,服务本身尽可能提供逻辑的服务,将数据的一致性保证集中式处理,这样就可以把“状态”抽取出来,让网关保持一个“无状态”。
这里仅仅是举了网关的例子,在微服务基本所有的服务,都应该按照这种思路去做。
如果服务中有状态,就应该把状态抽取出来,让更加擅长处理数据的组件来处理,而不是在微服务中去兼容有数据的状态。
数据存储高可用
之前上面说的服务冗余,可以简单的理解为计算的高可用,计算高可用只需要做到无状态既可简单的扩容缩容,但是对于需要存储数据的系统来说,数据本身就是有状态。
跟存储与计算相比,有一个本质的差别:将数据从一台机器搬到另一台机器,需要经过线路进行传输。
网络是不稳定的,特别是跨机房的网络,Ping 的延时可能是几十几百毫秒,虽然毫秒对于人来说几乎没有什么感觉,但是对于高可用系统来说,就是本质上的不同,这意味着整个系统在某个时间点上,数据肯定是不一致的。
按照“数据+逻辑=业务”的公式来看,数据不一致,逻辑一致,最后的业务表现也会不一致。
举个例子:
无论是正常情况下的传输延时,还是异常情况下的传输中断,都会导致系统的数据在某个时间点出现不一致。
而数据的不一致又会导致业务出现问题,但是如果数据不做冗余,系统的高可用无法保证。
所以,存储高可用的难点不在于怎么备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。
分布式领域中有一个著名的 CAP 定理,从理论上论证了存储高可用的复杂度,也就是说,存储高可用不可能同时满足“一致性,可用性,分区容错性”。
最多只能满足 2 个,其中分区容错在分布式中是必须的,就意味着,我们在做架构设计时必须结合业务对一致性和可用性进行取舍。
存储高可用方案的本质是将数据复制到多个存储设备中,通过数据冗余的方式来实现高可用,其复杂度主要呈现在数据复制的延迟或中断导致数据的不一致性。
我们在设计存储架构时必须考虑到以下几个方面:
- 数据怎么进行复制
- 架构中每个节点的职责是什么
- 数据复制出现延迟怎么处理
- 当架构中节点出现错误怎么保证高可用
①数据主从复制
主从复制是最常见的也是最简单的存储高可用方案,例如 MySQL,Redis 等等。
其架构的优点就是简单,主机复制写和读,而从机只负责读操作,在读并发高时候可用扩张从库的数量减低压力,主机出现故障,读操作也可以保证读业务的顺利进行。
缺点就是客户端必须感知主从关系的存在,将不同的操作发送给不同的机器进行处理。
而且主从复制中,从机器负责读操作,可能因为主从复制时延大,出现数据不一致性的问题。
②数据主从切换
刚说了主从切换存在两个问题:
主机故障写操作无法进行。
需要人工将其中一台从机器升级为主机。
为了解决这个两个问题,我们可以设计一套主从自动切换的方案,其中涉及到对主机的状态检测,切换的决策,数据丢失和冲突的问题。
主机状态检测:需要多个检查点来检测主机的机器是否正常,进程是否存在,是否出现超时,是否写操作不可执行,是否读操作不可执行,将其进行汇总,交给切换决策。
切换决策:确定切换的时间决策,什么情况下从机就应该升级为主机,是进程不存在,是写操作不可行,连续检测多少失败次就进行切换。
应该选择哪一个从节点升级为主节点,一般来说或应该选同步步骤最大的从节点来进行升级。切换是自动切换还是半自动切换,通过报警方式,让人工做一次确认。
数据丢失和数据冲突:数据写到主机,还没有复制到从机,主机就挂了,这个时候怎么处理,这个也得考虑业务的方式,是要确保 CP 或 AP。
还要考虑一个数据冲突的问题,这个问题在 MySQL 中大部分是由自增主键引起。
就算不考虑自增主键会引起数据冲突的问题,其实自增主键还要引起很多的问题,这里不细说,避免使用自增主键。
③数据分片
上述的数据冗余可以通过数据的复制来进行解决,但是数据的扩张需要通过数据的分片来进行解决(如果在关系型数据库是分表)。
何为数据分片(Segment,Fragment, Shard, Partition),就是按照一定的规则,将数据集划分成相互独立、正交的数据子集,然后将数据子集分布到不同的节点上。
HDFS , MongoDB 的 Sharding 模式也基本是基于这种分片的模式去实现。
我们在设计分片主要考虑到的点是:
- 做数据分片,如何将数据映射到节点。
- 数据分片的特征值,即按照数据中的哪一个属性(字段)来分片。
- 数据分片的元数据的管理,如何保证元数据服务器的高性能、高可用,如果是一组服务器,如何保证强一致性。
柔性化/异步化
①异步化
在每一次调用,时间越长存在超时的风险就越大,逻辑越复杂执行的步骤越多,存在失败的风险也就越大。
如果在业务允许的情况下,用户调用只给用户必须要的结果,而不是需要同步的结果可以放在另外的地方异步去操作,这就减少了超时的风险也把复杂业务进行拆分减低复杂度。
当然异步化的好处是非常多,例如削峰解耦等等,这里只是从可用的角度出发。
异步化大致有这三种的实现方式:
- 服务端接收到请求后,创建新的线程处理业务逻辑,服务端先回应答给客户端。
- 服务端接收到请求后,服务端先回应答给客户端,再继续处理业务逻辑。
- 服务端接收到请求后,服务端把信息保存在消息队列或者数据库,回应答给客户端,服务端业务处理进程再从消息队列或者数据库上读取信息处理业务逻辑。
②柔性化
什么是柔性化?想象一个场景,我们的系统会给每个下单的用户增加他们下单金额对应的积分,当一个用户下单完毕后,我们给他增加积分的服务出现了问题。
这个时候,我们是要取消掉这个订单还是先让订单通过,积分的问题通过重新或者报警来处理呢?
所谓的柔性化,就是在我们业务中允许的情况下,做不到给予用户百分百可用的,通过降级的手段给到用户尽可能多的服务,而不是非得每次都交出去要么 100 分或 0 分的答卷。
怎么去做柔性化,更多其实是对业务的理解和判断,柔性化更多是一种思维,需要对业务场景有深入的了解。
在电商订单的场景中,下单,扣库存,支付是一定要执行的步骤,如果失败则订单失败。
但是加积分,发货,售后是可以柔性处理,就算出错也可以通过日志报警让人工去检查,没必要为加积分损失整个下单的可用性。
兜底/容错
兜底可能是我们经常谈论的一种降级的方案,方案是用来实施,但是这里兜底可能更多是一种思想,更多的是一种预案,每个操作都可以犯错,我们也可以接受犯错。
但是每个犯错我们都必须有一个兜底的预案,这个兜底的预案其实就是我们的容错或者说最大程度避免更大伤害的措施,实际上也是一个不断降级的过程。
举个例子:
例如我们首页请求的用户个性化推荐商品的接口,发现推荐系统出错,我们不应该去扩大(直接把异常抛给用户)或保持调用接口的错误,而是应该兼容调用接口的错误,做到更加柔性化。
这时候可以选择获取之前没有失败接口的缓存数据,如果没有则可以获取通用商品不用个性化推荐,如果也没有可以读取一些静态文字进行展示。
由于我们架构进行了分层,分层 App,网关,业务逻辑层,数据访问层等等,在组织结构也进行了划分,与之对应的是前端组,后端业务逻辑组,甚至有中台组等等。
既然有代码和人员架构的层级划分,那么每一层都必须有这样的思想:包容下一层的错误,为上一层提供尽可能无错的服务。
举个例子:
商品的美元售价假设要用商品人民币售价/汇率,这个时候错误发生在低层的数据层,上一层如果直接进行除,肯定就抛出 java.lang.ArithmeticException: / by zero。
本着我们对任何一层调用服务都不可信的原则,应该对其进行容错处理,不能让异常扩散,更要保证我们这一层对上一次尽可能的作出最大努力确定的服务。
负载均衡
相信负载均衡这个话题基本已经深入每个做微服务开发或设计者的人心,负载均衡的实现有硬件和软件。
硬件有 F5,A10 等机器;软件有 LVS,Nginx,HAProxy 等等,负载均衡的算法有 Random,RoundRobin,ConsistentHash 等等。
①Nginx 负载均衡故障转移
转移流程:Nginx 根据给定好的负载均衡算法进行调度,当请求到 Tomcat1,Nginx 发现 Tomcat1 出现连接错误(节点失效),Nginx 会根据一定的机制将 Tomcat1 从调用的负载列表中清除。
在下一次请求,Nginx 不会分配请求到有问题的 Tomcat1 上面,会将请求转移到其他的 Tomcat 之上。
节点失效:Nginx 默认判断节点失效是以 connect refuse 和 timeout 为标准,在对某个节点进行 fails 累加,当 fails 大于 max_fails 时,该节点失效。
节点恢复:当某个节点失败的次数大于 max_fails 时,但不超过 fail_timeout,Nginx 将不再对该节点进行探测,直到超过失效时间或者所有的节点都失效,Nginx 会对节点进行重新探测。
②ZK 负载均衡故障转移
在使用 ZK 作为注册中心时,故障的发现是由 ZK 去进行发现,业务逻辑层通过 Watch 的心跳机制将自己注册到 ZK 上,网关对 ZK 进行订阅就可以知道有多少可以调用的列表。
当业务逻辑层在重启或者被关闭时就会跟 ZK 断了心跳,ZK 会更新可调用列表。
使用 ZK 作为负载均衡的协调器,最大的问题是 ZK 对于服务是否可用是基于 Pingpong 的方式。
只要服务心跳存在,ZK 就认为服务是处在可用状态,但是服务如果处在假死的状态,ZK 是无从得知的。这个时候,业务逻辑服务是否真正可用只能够由网关知道。
幂等设计:为何会牵出幂等设计的问题,主要是因为负载均衡的 Failover 策略,就是对失败的服务会进行重试。
一般来说,如果是读操作的服务,重复执行也不会出问题,但想象一下,如果是一个创建订单减库存的操作,第一次调用也 Tomcat1 超时,再重新调用了 Tomcat2。
这个时候我们都不能确认超时调用的 Tomcat1 是否真的被调用,有可能根本就调用不成功,有可能已经调用成功但是因为某些原因返回超时而已。
所以,很大程度这个接口会被调用 2 次。如果我们没有保证幂等性,就有可能一个订单导致了减少 2 次的库存。
所谓的幂等性,就是得保证在同一个业务中,一个接口被调用了多次,其导致的结果都是一样的。
服务限流降级熔断
先来讲讲微服务中限流/熔断的目的是什么,微服务后,系统分布式部署,系统之间通过 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 的调用,并在功能上作出一些降级才能保证到我们系统的健壮性。熔断是对模块的隔离,是保证了最大功能的可用性。
服务治理
①服务模块划分
服务模块与服务模块之间有着千丝万缕的关系,但服务模块在业务中各有权重。
例如订单模块可能是一家电商公司的重中之重,如果出问题将会直接影响整个公司的营收。
而一个后台的查询服务模块可能也重要,但它的重要等级绝对是没有像订单这么重要。
所以,在做服务治理时,必须明确各个服务模块的重要等级,这样才能更好的做好监控,分配好资源。
这个在各个公司有各个公司的一个标准,例如在电商公司,确定服务的级别可能会更加倾向对用户请求数和营收相关的作为指标。
可能真正的划分要比这个更为复杂,必须根据具体业务去定,这个可以从平时服务模块的访问量和流量去预估。
往往更重要的模块也会提供更多的资源,所以不仅要对技术架构了如指掌,还要对公司各种业务形态了然于心才可以。
服务分级不仅仅在故障界定起到重要主要,而且决定了服务监控的力度,服务监控在高可用中起到了一个保障的作用。
它不仅可以保留服务崩溃的现场以等待日后复盘,更重要的是它可以起到一个先知,先行判断的角色,很多时候可以预先判断危险,防范于未然。
②服务监控
服务监控是微服务治理的一个重要环节,监控系统的完善程度直接影响到我们微服务质量的好坏。
我们的微服务在线上运行的时候有没有一套完善的监控体系能去了解到它的健康情况,对整个系统的可靠性和稳定性是非常重要,可靠性和稳定性是高可用的一个前提保证。
服务的监控更多是对于风险的预判,在出现不可用之间就提前的发现问题,如果系统获取监控报警系统能自我修复则可以将错误消灭在无形,如果系统发现报警无法自我修复则可以通知人员提早进行接入。
一个比较完善的微服务监控体系需要涉及到哪些层次?如下图,大致可以划分为五个层次的监控:
基础设施监控:例如网络,交换机,路由器等低层设备,这些设备的可靠性稳定性就直接影响到上层服务应用的稳定性。
所以需要对网络的流量,丢包情况,错包情况,连接数等等这些基础设施的核心指标进行监控。
系统层监控:涵盖了物理机,虚拟机,操作系统这些都是属于系统级别监控的方面,对几个核心指标监控,如 CPU 使用率,内存占用率,磁盘 IO 和网络带宽情况。
应用层监控:例如对 URL 访问的性能,访问的调用数,访问的延迟,还有对服务提供性能进行监控,服务的错误率。
对 SQL 也需要进行监控,查看是否有慢 SQL,对于 Cache 来说,需要监控缓存的命中率和性能,每个服务的响应时间和 QPS 等等。
业务监控:比方说一个电商网站,需要关注它的用户登录情况,注册情况,下单情况,支付情况。
这些直接影响到实际触发的业务交易情况,这个监控可以提供给运营和公司高管他们需要关注的数据,直接可能对公司战略产生影响。
端用户监控:用户通过浏览器,客户端打开连到到我们的服务,那么在用户端用户的体验是怎么样,用户端的性能是怎么样,有没有产生错误,这些信息也是需要进行监控并记录下来。
如果没有监控,有可能用户因为某些原因出错或者性能问题造成体验非常的差,而我们并没有感知。
这里面包括了,监控用户端的使用性能,返回码,在哪些城市地区他们的使用情况是怎么样,还有运营商的情况,包括电信,联通用户的连接情况。
我们需要进一步去知道是否有哪些渠道哪些用户接入的时候存在着问题,包括我们还需要知道客户端使用的操作系统浏览器的版本。
总结
出了那么多张牌,出牌只是术,真正的道还是得静下心来看看整个服务高可用的本质是什么。
随着微服务架构的相互调用越来越复杂,环节只会越来越多,只有建立清晰的架构和层次才能理清楚每个环节高可用的保障,保持简单。
从手段看高可用
主要使用的技术手段是服务和数据的冗余备份和失效转移,一组服务或一组数据都能在多节点上,之间相互备份。
当一台机器宕机或出现问题的时候,可以从当前的服务切换到其他可用的服务,不影响系统的可用性,也不会导致数据丢失。
从架构看高可用
保持简单的架构,目前多数网站采用的是比较经典的分层架构,应用层,服务层,数据层。
应用层是处理一些业务逻辑,服务层提供一些数据和业务紧密相关服务,数据层负责对数据进行读写。
简单的架构可以使应用层,服务层可以保持无状态化进行水平扩展,这个属于计算高可用。
相比计算高可用,在数据层思考的高可用则属于数据高可用,数据高可用相比计算高可用需要考虑到数据的一致性问题会更加的复杂。
这个时候 CAP 理论在里面会发挥关键的作用,究竟是选择 AP 或 CP,这个得根据业务去选择模型。
从硬件看高可用
首先得确认硬件总是可能坏的,网络总是不稳定的。解决它的方法也是一个服务器不够就来多几个,一个机柜不够就来几个,一个机房不够就来几个。
从软件看高可用
软件的开发不严谨,发布不规范也是导致各种不可用出现,通过控制软件开发过程质量监控,通过测试,预发布,灰度发布等手段也是减少不可用的措施。
从治理看高可用
一个系统在线上跑的好好的,但我们也不能确保它在下一秒会不会出现不可用状态。
将服务规范化,事前做好服务分割,做好服务监控,预判不可用的出现,在不可用出现之前发现问题,解决问题。
【注:文章部分内容参考 李云华《从 0 开始学架构》杨波老师《微服务》】
作者:陈于喆
简介:十余年的开发和架构经验,国内较早一批微服务开发实施者。曾任职国内互联网公司网易和唯品会高级研发工程师,后在创业公司担任技术总监/架构师,目前在洋葱集团任职技术研发副总监。
【51CTO原创稿件,合作站点转载请注明原文作者和出处为51CTO.com】