故障隔离是指当系统中某些模块或者组件出现异常故障时,把这些故障通过某种方式隔离起来,让其不和其他的系统产生联系,即使这个被隔离的应用或者系统出现了问题,也不会波及其他的应用。故障隔离方案是一个应用系统实现高可用的重要组成部分。
减少故障的方式有多种,包括系统优化、监控、风险扫描、链路分析、变更管控、故障注入演练、故障隔离等。故障隔离是其中的一种手段,并且要求在系统设计时就考虑清楚。故障隔离体现了一种架构设计的思路,我们在设计应用组件时,一般要求尽量避免重复开发,提高应用组件的可复用性,以及考虑多个组件共享数据库,共享相同的基础组件。共享提高复用性的同时也带来故障蔓延的隐患,一旦基础服务或者组件出现问题,会造成大面积的系统运行异常。有了故障隔离,就可以避免这种情况的发生,或者说可以减缓这种情况的发生。
从系统的角度来看,故障隔离是指在系统设计时要尽可能考虑出现故障的情况。当存在依赖关系的系统、系统内部组件或系统依赖的底层资源发生故障后,采取故障隔离措施可以将故障范围控制在局部,防止故障范围扩大,增加对上层系统可用性带来的影响。当故障发生时,我们能够快速定位故障源,为后续的故障恢复提供必要条件。
从业务的角度来看,故障隔离是为保障重点业务和重点客户,本质上是“弃卒保车”的做法。所以,各个业务域需要定义出哪些是重点业务和客户。区分办法视各个业务而定,有一种区分是按核心功能、重要功能、非关键功能3个等级来区分。比如,对于支付业务来说,支付肯定是一级业务,营销、限额等是二级业务,而运营管理功能是三级业务。
故障隔离的基本原理是当故障发生时能够及时切断故障源,按其隔离范围由高到低依次为:数据中心隔离、部署隔离、网络隔离、服务隔离、数据隔离。
- 数据中心隔离:出于灾备考虑,一个大型关键的应用系统一般会采用多数据中心部署,一旦某个数据中心出现异常,流量入口负责把流量分发到其他正常的数据中心节点,以此实现数据中心故障隔离的目标。
- 部署隔离:应用设计开发、打包部署时,把不同用途不同等级的应用分开部署,避免相互影响。如核心应用和非核心应用分开,内部应用和外部应用分开,在线应用和离线应用分开,传统稳定应用和敏捷创新应用分开,等等。
- 网络隔离:采用虚拟网络技术把一个物理网络分为不同的VPC,不同VPC之间实现网络逻辑隔离,没有业务交叉的应用分别部署在不同的VPC中。网络接入链路上,采用多链路接入,如租用选择电信、联通等不同网络服务商的线路,避免单个链路故障引发全局业务不可用。
- 服务隔离:减少服务之间的同步依赖,通过消息队列实现异步解耦,或把服务依赖变为数据依赖。非核心链路的外部依赖服务出现异常时,提供熔断降级等技术手段屏蔽对异常服务的调用。通过命名空间、服务分组等手段实现服务之间的隔离,避免跨环境的服务异常调用;提供服务限流保护,防止过载请求对后端系统的冲击。
- 数据隔离:数据隔离包括很多方面。不同用途、不同重要程度的数据分开存储,充分发挥异构数据源各自的技术优势,针对不同场景合理选择关系型数据库、非关系型数据库、分析数据库、文档数据库、内存数据库、大数据等不同数据存储类型。用户级的数据隔离是将不同用户所使用的资源分开,保障部分资源出现故障时只影响部分用户,而不影响全体用户。
为了实现故障隔离,在架构设计和代码实现方面有一些最佳实践可供参考,可以根据自己团队的技术储备和系统复杂度,以及架构情况自行采纳。
- 应用探活:应用探活是实现故障隔离的前提,通过应用的健康检查识别出异常的服务节点,这样负载均衡和服务注册中心及时把这些异常节点摘除,以阻止请求流量再分发到这些节点,实现节点维度的故障隔离。数据库连接探活也是类似的思路。
- 快速失败:快速失败是指依赖方不可用时,不能耗费大量的宝贵系统资源(比如线程、连接数等),再等待其恢复,而是要在其达不到合理的功能和性能要求时快速失败,避免占用资源,甚至拖垮系统。通过使用超时(timeout)来实现微服务中的快速失败是一种反模式,这是应该避免的。我们可以使用基于操作的成功或失败统计次数的熔断模式,而不是使用超时。
- 熔断设计:当在短时间内多次发生指定类型的错误,断路器会开启。开启的断路器可以拒绝接下来更多的请求,快速返回指定的结果。断路器通常在一定时间后关闭,以便为底层服务提供足。
- 失败转移:当故障发生时,动态地切换到备用方案。如数据库的主备切换机制,独立的HA心跳机制检查主节点状态,一旦主节点不可用,则切换到备节点。缓存系统的备节点也基于类似的原理。
- 降级开关:应用维度或者服务维度提供降级配置开关,紧急情况下可以通过启用降级开关关闭非核心功能,损失一定的客户体验,以确保核心关键业务的服务可用性。
- 重试逻辑:在分布式系统中,微服务系统重试可能会触发多个其他请求或重试操作,并导致级联效应。为减少重试带来的影响,我们应该减少重试的数量,并使用指数退避算法来持续增加重试之间的延迟时间,直到达到最大限制。重试的另外一个要求是服务的幂等设计。
- 同步转异步:对第三方外部服务的依赖,尽可能通过消息队列异步化处理,避免同步调用,因为同步调用意味着强依赖。消息队列起到很好的削峰填谷的作用,且一般都具备很大规模的消息堆积能力。
- 缓存依赖数据:依赖本地缓存是一种数据的降级方案,当依赖方(服务或数据库)出现故障时,可以一定程度地保障业务可用。一般是应用于核心、决不能出错的业务场景。缓存的问题是数据实时更新的一致性问题,但在很多场景下,对于客户的体验,读到旧的数据往往要比系统无法响应要好很多。