如何设计高可用系统之故障隔离

简介: 简单来说,当功能或性能不符合预期,就是故障。减少故障的方式有多种,包括系统优化、监控、风险扫描、链路分析、变更管控、故障注入演练、故障隔离等。故障隔离是其中一种手段,并且要求在系统设计时就需要考虑清楚。

image.png

作者:大谷

什么是故障

简单来说,当功能或性能不符合预期,就是故障。

故障有两个比较重要的衡量指标:

RPO(Recovery Point Objective):主要指的是业务系统能容忍的最大数据丢失量,针对的是数据丢失。对于资金业务来说,一般 RPO 不能大于 0 的。

RTO(Recovery Time Objective): 主要指的是所能容忍的所业务停止服务的最长时间,针对的是服务丢失。

从单系统的角度看故障

image.png

一个系统,从头到脚,有非常多的故障点,所以,对于一个分布式系统来说,一定要假定故障是随时、而且一定会发生的。

故障隔离的目的

减少故障的方式有多种,包括系统优化、监控、风险扫描、链路分析、变更管控、故障注入演练、故障隔离等。故障隔离是其中一种手段,并且要求在系统设计时就需要考虑清楚。

从系统的角度看

故障隔离是指在系统设计的时候,要尽可能考虑故障的情况,当存在依赖关系的系统、系统内部组件或系统依赖的底层资源发生故障后,采取故障隔离措施可以将故障范围控制在局部,防止故障范围扩大,增加对上层系统可用性带来的影响。

并且当故障发生时,我们能够快速定位故障源,为后续的故障恢复提供必要条件。

从业务的角度看

故障隔离是为保障重点业务和保障重点客户,本质上弃卒保车的做法。

所以,各个业务域需要定义出哪一些是重点业务和客户。区分办法视各个业务而定。

有一种区分是按核心功能、重要功能、非关键功能三个等级来区分。比如,对于支付业务来说,支付肯定是一级业务,像营销、限额等是二级业务,而运营管理功能是三级业务。

故障隔离的基本原理

  • 故障时能够切断依赖
  • 服务或资源隔离,避免共享
  • 避免同步调用

故障隔离的常用模式

第一、依赖降级

默认降级

默认降级就是当依赖方出现问题的时候,采用默认的策略进行处理,而不是直接系统异常。

它的另一个变种是需要通过开关才能打开降级能力,一般用于一些比较谨慎的场景,比如对整个营销系统依赖进行降级。

例子 1

当依赖的缓存系统故障时,业务处理不是直接失败,而是捕捉到异常后,直接降级调用数据库

例子 2

在支付的场景中,会有每日提现额度的限制(是一个二级业务)。当依赖的限额系统出现故障时,针对小额提现的场景,可以默认降级,不依赖于限额系统。并且,同时,会记录下哪些使用额度没有同步到限额系统,在限额系统恢复后,将故障期间的使用额度同步到限额系统。

动态切换(Failover)
当故障发生时,动态地切换到备用方案

例子 1

数据库的主备切换机制,独立的 HA 心跳机制检查主节点状态,一旦主节点不可用,则切换到备节点。类似的,缓存系统的备节点也是类似原理。

当然,这种方式常常是异步复制,RPO>0

例子 2

如下图,是针对流水型数据 master 节点故障的 FO 方案,可以做到 RPO=0。什么是流水型数据,就是比较支付订单,贷款请求等这种一次请求生成一条的业务数据。

当 master 库故障时,因为是异步复制的,如果直接切到 slave 库,可能会导致数据丢失。那么 FO 的方案,就是直接切到一个空的 FO 库,对于新数据就读写到新的 FO 库。这个时候的影响,是老数据无法访问,但是新业务还能继续运行,比如支付业务还是继续跑。

这个时候再人工恢复 master,保证其与 slave 数据完成同步。再把调用连接切回到 master。

一般来说,已经写到 FO 库的数据可以回迁,也可以不回迁,应用通用流水号的 FO 标记则能识别应该调用哪一个库。

image.png

例子 3

如下图,也是针对于消息型数据 master 故障时的 markdown 方案。比如消息中心的存储就是用这种方案。

消息型跟流水型数据的区分是,消息型通用路由规则是随机的,放在哪都无所谓,而流水型如何使用分库分表方案,通用路由规则需要固定。比如基于 user_id 的后 2 位路由到库表,这个不能随意变更,不然会导致数据大量错乱,可能会导致大量的跨库事务问题和恢复后数据无法回查的问题。

这个方案类似于例子 2 的思路。不同点在于它是多个节点都是活跃的,每一次请求可以随机写到其中某一个库,当其中一个库挂了,那只会影响 50% 的老数据,对于另外 50% 的老数据和新增的数据是不会有影响的。而且这个方案还可以继续扩展,可以搞成 N 组数据库。

image.png

动态调整请求
当依赖方某些节点出现故障时,自动调整调用频率,或直接剔除

例子 1

消息中心在推送消息时,会根据消费节点的响应时间,调整推送的消息数量和间隔,避免将消费节点打挂。比如:推送时发生消费节点最近 5 秒平均响应时间超过 1000ms,则减少消息推送量,加大推送间隔。如果下次推送时间恢复正常,则逐步加大推送量,减小间隔,多次之后,如果消费节点没有问题,则恢复到正常的节奏。

例子 2

rpc 调用时,当调用节点出现调用异常时,有多种动态调节策略。比如节点不可用,直从消费节点中剔除。如果是节点响应明显变慢,则会降低路由的权重,减少调用次数。还有就是节点冷启动的情况,当节点第一次调用时,会灰度先调用少量数量,使系统先完成初始化,再逐步加大权重,直到跟其他节点权重一样。

快速失败(Fail Fast)
快速失败是指依赖方不可用时,不能耗费大量的宝贵系统资源(比如线程,比如连接数)等,在等待其恢复,而是要在其超出合理的功能和性能要求时,快速失败,避免占用资源,反而拖垮系统

例子 1

rpc 调用时,如果不设置超时时间,当调用方不可用时,同时会耗尽调用方的线程池,从而拖垮调用方。如果调用方的上游也是这样,那就会导致整体雪崩。

例子 2

数据库执行 SQL,如果不设置超时时间,当出现不合理的慢 SQL 时,很快会耗尽连接池,新的业务请求拿不到连接,就会一直挂起等待,很快你的线程池和请求队列会被占满,系统严重不可用。如果请求队列连是无界队列,则有可能直接 OOM。

缓存依赖数据
依赖数据本地缓存是一种数据的降级方案,当依赖方故障时,可以一定程度地保障业务可用。一般是应用于核心,决不能挂的业务场景。

例子 1

客户系统是一个全部系统都会依赖的非常核心的系统,如果其不可用,则会导致全局性的业务不可用,非常凶险。其中一种处理办法,就是客户信息的本地缓存,一方面可以降级被客户系统的调用压力,另一方面,当客户系统异常时,只要是已经缓存客户,还是可以继续使用业务,可以最大限制的保障业务可用。

这里的技术难点是在于缓存的一致性更新。通常有几种策略,一种是定时更新(客户系统正常时),另一种是本地缓存由客户系统提供一个统一的 sdk 管理,提供一种机制,当某一个用户变更时,通用各种 sdk 方更新数据。

另一个变种是,是将缓存放在 Tair,而不是调用方本地。这样,数据一致性的维护会更简单一些。目前内部用的最多是这种方案。

例子 2

归因系统会依赖于 Tair 中的配置信息,来判断请求中的参数(pub 值)是否合法。可以将配置信息(不大),缓存到调用方本地,这样,当 Tair 故障时,只会影响少量新增的配置,对于大部分业务功能是可用的。

减少或不要对低级别系统的依赖
这个是一种依赖原则,因为高级别系统的可用性标准(可用率、性能等)一般是高用于低级别系统的,如果依赖于低级别系统,当它发生故障时,高级别系统也会故障。这样本本质上是将高级将系统的可用性拉低到低级别系统的水平。

例子 1

比如,在支付系统去依赖于运营后台的话,假设原来支付系统的可用率是 99.99%,而运营后台的可用率是 99%,一旦这么依赖且无降级方案,则支付系统的可用率也会降低为 99%。

第二、功能降级

限流
限流的目的,是通过牺牲超过系统处理能力的部分业务,来保全系统整体的可用。

例子 1

所有 API 网关都会有限流的能力,常用的算法有计数器、漏桶算法,令牌桶算法等,总之就是会保护在一定时间内的并发数或者请求数。避免突发流量或者恶意的攻击,将系统打垮。

例子 2

归因系统中,收到归因请求,不会直接处理,而是先落到队列中(用数据库实现),然后按一定的时候频率,每次捞取一定量的数据处理,这样可以匀速地处理系统请求,削峰填谷,避免突发的请求数将系统打挂。

关闭非关键业务
就是关闭非关键的业务,不让其影响关键业务

例子 1

支付账单在双 11 高峰的时候,关闭实时更新账单的能力,改为双 11 过后,批量处理。

例子 2

双11高峰的时最为关键的业务,就是担保交易的付款,其它业务的重要性都会相对较低,比如收费业务,完全可以晚一点再向商户收取,以及淘宝的一些通过 CAE 代扣进来的返佣等业务,也可以一定程度上的滞后。

例子 3

消息中心提供了积压查询的功能,但是该功能是一个非关键功能,并且非常的耗费 DB 性能。大促时或系统处理能力紧张时,可以关闭掉,等恢复正常再打开。

第三、日志降级

顾名思义,就是将日志从 INFO,甚至 DEBUG 级别降为 WARN 或 ERROR 级别。

例子 1

写日志是比较消耗性能的操作,而且会占用大量磁盘,比如在大促时,或者因为日志过大导致磁盘打满时,可以通用日志降级来处理。

服务或资源隔离

概述一下,一般来说,隔离的级别可以有很多,如下图,不同的处理办法会不同在级别上进行。

image.png

用户级别隔离
将不同用户所使用的资源分开,保障部分资源故障时,只影响部分用户,而不是全体用户。

例子 1

支付宝按 RZONE 拆成逻辑机房,每个 RZONE 机房都会有完整的业务处理能力。用户请求的时候,会根据其 id 号,按配置的路由规则路由到某一个 RZONE 中。

这样,当部分 RZONE 异常时,只会影响跌幅到这部分机房的用户,而不是全局不可用。

例子 2

分库分表的时候,将用户相关的数据独立成一组分库。用户处理请求的时候,会根据其 id 号路由到其中一个分库上。这样,当用户数据相关的数据库的某一个实例宕机时,只会影响其中的部分用户,比如分了 10 个分库,则只会影响 10% 的用户,而不是全局用户。

业务功能隔离
将不同业务功能所使用的资源隔离,不互相影响

例子 1

贷款系统中,将还款和借款拆分不同系统。因为前者是一个非实时的批处理系统,而后者是对实时性和稳定性要求很高的联机系统,是用户可以直接感知到的。

拆分后,不会因为在批处理高峰期占用过多资源而影响借款系统的稳定性,而且两者在运维上,也可以更方便地根据各自的需要评估所需的机器数据。

例子 2

支付系统中,支付是关键业务,而账单是非关键业务,而且后者涉及到一些大数据量的查询(尤其是大商户),这些查询如果使用在线库查询,会影响支付的稳定性。所以,这两个业务进行了拆分。拆分后,支付业务会将数据异步同步给账单系统,账单系统会保存到更适用于账单查询的大数据存储中(HBase 或 ES)。

系统资源隔离
将不同的请求和所使用的资源隔离,不互相影响。

例子 1

API 请求网关,如果所有 API 请求都使用一个线程池,则当某一个 API 处理过慢的时候,会因为占满了线池,而导致其他 API 也不可用。所以,需要将不同的 API 分组,分配不同的线程池,避免互相影响。

例子 2

数据库集群管理的时候,将关键业务的实例和非关键业务的实例分开到不同集群,保障非关键的实例发生异常拖挂系统时,不会影响到关键业务的实例。

异步化处理

分阶段处理
分阶段就是主要逻辑就是避免同步调用,因为同步调用意味着强依赖,当依赖方故障时,调用方也会异常,而且这个异常可能会被用户感知。换句话讲,就是尽量异步化处理。

例子 1

消息的处理过程主要分为消息投递和消息接收两个阶段,如果两个处理阶段强耦合在一起,当某阶段出现故障,会影响另一阶段的处理。

例子 2

支付请求分至少分为支付受理、支付处理、支付结果回调三个阶段,如果这三个阶段分阶段异步处理,那么这其中某一个步骤异常了,都会导致支付失败,支付成功率低,用户体验差。而且,因为需要等待所有步骤都处理完,接口的性能也会很差。

例子 3

归因请求处理,分为接收处理(只有简单的归类逻辑)、归因处理(会有复杂的业务逻辑和外部依赖)两个阶段。同上,如果强耦合,当某阶段出现故障,会影响另一阶段的处理。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
5月前
常见系统及架构
常见系统及架构
27 0
|
8月前
|
存储 消息中间件 运维
高可用架构和系统设计思想
本文从研发规范层面、应用服务层面、存储层面、产品层面、运维部署层面、异常应急层面这六大层面去剖析一个高可用的系统需要有哪些关键的设计和考虑
|
6月前
|
存储 算法 安全
|
6月前
|
缓存 监控 容灾
0-1设计高可用、高并发、高伸缩的分布式项目架构
0-1设计高可用、高并发、高伸缩的分布式项目架构
|
8月前
|
存储 缓存 负载均衡
架构系列——架构师必备基础:单体、分布式、集群与冗余的区别
架构系列——架构师必备基础:单体、分布式、集群与冗余的区别
|
消息中间件 监控 容灾
容灾、扩展场景架构设计|学习笔记
快速学习容灾、扩展场景架构设计
85 0
容灾、扩展场景架构设计|学习笔记
【系统概念】容错、高可用和灾备
容错,高可用、灾备这三个词的使用环境极易被混淆。很多时候以为这三个词的意思是相同的。
198 0
【系统概念】容错、高可用和灾备
|
消息中间件 缓存 容灾
|
消息中间件 监控 算法
高可用怎么设计呢
《高可用》系列
132 0
高可用怎么设计呢
|
负载均衡 容灾 NoSQL
【服务器系列】高可用方案
高可用的一些解决方案冷备双机热备同城双活异地双活异地多活。
327 0
【服务器系列】高可用方案