为什么需要故障演练
应用高可用建设往往是基于先验设计的具体实施,描绘一幅看似全面但静态的蓝图,而问题在于,随着部署环境、流量模式和调用依赖的日益复杂,应用系统的运行时处在一个信息过载的状态,没有人能知道将会发现什么,起码不能全部知道。
每一次故障都是独特的,或许是因为某次调用在某个时间刚好访问了某些数据,时间是无限的,数据也是无限的,所以故障也是无限的。但故障的成因是可枚举的,从基础设施到上层服务,功能趋同形成内聚的节点,无限的数据流被切割成可数的块,这有限的故障归因,是高可用建设的结果能够预期的基础。
面向失败设计(Failover Design)是构建高可用和弹性系统的关键,如何确保在部分组件失效的时候,系统也能正常运转或快速恢复,在业务功能之外,对每一处可能引发系统故障的节点,额外地要实现冗余、转移、隔离和降级等手段,这是可先验设计和实现的。
每一种容灾手段就像针对某类疾病的靶向药,从实验室开发到药品上市,要经过一期又一期的临床实验,才能勉力对抗人类基因的无限性,确保药品在广泛的病患群体中取得符合预期的疗效。
异常是不可避免的,高可用建设不是消灭异常,而是消化异常,故障演练就是应用高可用的临床实验,在生产的可控范围内进行可预期的异常暴露和处理,随着不断迭代改进,演练形成的风险处置预案就像给系统注入的疫苗,随时应对真实的生产故障。
混沌工程告诉我们什么
混沌工程(Chaos Engineering)是指导故障演练进行系统性实验的学科,萌发于2008年Netflix业务的上云,随后被诸多大型互联网企业采纳实践,伴随着系统复杂度和不可预知性而发展,逐渐形成体系的方法论。
混沌工程的核心思想是在真实环境中引入故障,从而评估和提升系统的稳定性。如《混沌工程原则》一文中所说,“我们需要在异常行为出现之前,在整个系统内找出这些弱点”,找到弱点的前提是知道什么是弱点,从类比的角度,混沌工程更像是白盒测试,实施人员了解待测试功能的内部结构和逻辑,只不过用例的输入过于庞杂,无法在测试环境人为地构造,待测试的功能也不仅仅是代码实现,而是应用异常的监控机制、响应机制和处理机制,包含了组织、流程和工具等多方面。
实验是一种科学研究方法,通过有控制的测试和观察来验证假设、探索现象或建立因果关系,通常遵循问题定义、假设形成、实验设计、实验操作、数据分析和结果解释等步骤。在《混沌工程原则》中,为了具体地解决分布式系统在规模上的不确定性,把混沌工程看作是为了揭示系统弱点而进行的实验,这些实验遵循几个步骤:
- 首先,用系统在正常行为下的一些可测量的输出来定义“稳定状态”。
- 其次,假设控制组和实验组都能保持这种稳定状态。
- 然后,在实验组中引入反映真实世界事件的变量,如服务器崩溃、硬盘故障、网络连接断开等。
- 最后,通过控制组和实验组之间的状态差异来反驳稳定状态的假说。
除此之外,文中还声明了一些高级原则,作为实施混沌工程的最佳实践:
- 建立一个围绕稳定状态行为的假说,用可测量的输出来定义系统的运行状态,验证系统是否正常工作,而不是试图验证它如何工作。
- 多样化真实世界的事件,关注任何能够破坏稳态的事件,既有状态引发的异常(如硬件故障、软件故障),也有行为引发的异常(如错误配置、流量过载),引入的故障成因要尽可能贴近现实,按潜在影响和发生概率进行优先级排序。
- 在生产环境中运行实验,系统的行为会依据环境和流量模式而有所不同,最佳的拟真环境就是真实的环境,但稳态破坏的风险暗含故障影响的不可逆性,渐进式多环境实验更为恰当。
- 持续自动化运行实验,故障演练是循环改进的过程,避免人工依赖的不可持续性,要在系统中构建实验的自动化编排和分析。
- 最小化爆炸半径,引入故障是为了暴露问题,不是创造问题,演练应该尽量避免对业务造成不可接受的实质伤害,要确保负面影响最小化且都被考虑到。
由此,提炼混沌工程指导原则中最重要的关键字,那就是可控制和可观测。
如何实现核心要素
可控制是故障演练的大脑,是业务影响面的底线,由组织流程和技术流程保障。
组织流程指用户实施一次故障演练的一般性步骤,包括计划、执行、观察、记录、还原、分析六个阶段。
- 计划:制定目标、圈定范围、生成步骤、周知人员等。
- 执行:编排、配置、安装、运行等。
- 观察:日志、监控、功能体验等。
- 记录:体验是否正常、指标是否及时、容灾是否生效等。
- 还原:清除故障、还原现场、重启应用等。
- 分析:效果核查、问题总结、改进计划等。
技术流程指实现一套平台管理体系,将组织流程固化,提供标准化引导、正确性约束和自动化运行。技术流程将人与机器的角色进行分离,规定了哪些是用户介入的,哪些是系统实现的。按生命周期划分故障演练的流程,可以分为演练准备、演练运行、演练恢复和演练复盘,每个阶段有其特定的交互,其中用户行为是驱动,系统功能是支撑,如下图。
监控的指标与演练场景密切相关,不同场景关注点可能不一样。根据混沌工程的稳态行为假说原则,一方面要关注指标的含意,如系统整体的吞吐量、错误率、延迟等,如何表示系统当前的运行状态,界定系统运行是否正常;另一方面要关注指标的变化,无论是演练前后还是演练过程中的变化,着重明确定义预期内变化和预期外变化。
预期内变化不是指系统在实验过程中一直保持“正常状态”,而是代表系统状态的行为指标与预案设想一致。以双活容灾切换为例,假设定义可测量输出指标为“请求接口成功率”,正常情况下该指标测量值约为99.99%,单边机房断网故障注入后,成功率应降至50%左右,发起双活流量切换后,成功率在数分钟内应回升到99.99%附近。
预期外变化则刚好相反,关注的指标变化不符合预先设想,这里要进一步区分无损意外和有损意外。
无损意外指故障引入后,系统虽然不按计划表现,但没有变得更“坏”。还是以双活容灾切换为例,注入单边机房断网故障后,接口成功率还是有80%,这可能仅代表业务对故障影响面判断过于悲观,不影响故障处理方案——也就是流量切换的实施和观察。
有损意外指故障引入后,系统出现没预料到的异常表现,或异常表现超出了既定范围,这种情况可能是因为业务对故障影响面判断失误,也可能是故障处理方案没达到期望效果,又或者是故障的目标范围等配置出错。为及时止损应当立刻中止演练,给予人工自由裁量的手动中止操作显得十分必要,再进一步减少人工依赖,演练系统也应具备锚定特定指标的自动熔断保护机制,监控和告警起到了“观察者”和“吹哨人”的作用。
除此之外,整个故障演练流程应提前设置存续周期,超期撤回将作为业务保护的最后兜底。
构建故障演练平台
故障演练只有零次和无数次,在空间上,同一时间应用存在不同的故障场景,或是基础设施,或是上层服务,在时间上,应用的运行环境可能发生变化,同一种故障场景的处理预案需要不断验证成效,这是演练的“保鲜”。
综合来看,故障演练平台要兼顾多样性、重复性和安全性。多样性是指原子故障类型的丰富程度,以尽可能模拟生产运行可能发生的情况;重复性是指演练过程的多次运行,体现对单次接入的易用和多次循环的便捷;安全性是指业务影响的底线,演练的过程和结果都要在可控范围内。
多样性
故障类型可以按层级分类,分为硬件层和软件层,自底向上大致可以对应IaaS、PaaS和SaaS,而且硬件异常(IaaS)往往也会在软件异常(PaaS、SaaS)上体现,层级分类侧重于描绘故障发生的“主体”,表现故障发生的对象及其产生原理,例如机器掉电和数据库下线。
故障演练关注的主体是应用,以影响面评估,可以按依赖分类,分为外部依赖和内部依赖。外部依赖指应用正常运行时为应用提供服务的组件,可以是部署环境,如主机和容器,也可以是数据来源,如数据库和其他微服务;内部依赖指应用自身的运行情况与使用外部依赖的情况,显然,外部异常也会在内部异常上体现,应用自身异常也会在上游调用中体现。
故障场景从依赖中盘点,在外部层级和内部进程中实现,如下图。
重复性
流程的重复性要求功能的易用性,平台功能的易用性体现在一站式、轻接入和重交互。
一站式指故障演练平台能够支持各类场景、各种依赖的集中式编排演练,包括基础设施、容器平台和业务应用,既考究平台的集成程度,也考究平台的开放程度。集成程度也就是前面提到的“多样性”,开放程度则表明对新场景、新依赖的加入要友好,要做到统一的故障定义、通用的流程调度和模版的场景编排。
从故障到流程再到场景,也是故障演练平台的迭代建设过程。首先是实现原子的故障注入能力,能够在业务应用中触发特定异常并进行回收;其次是支持故障的编排能力,分批递进地运行,代表故障发生的组合与时序,模拟复杂系统的故障场景;最后是场景归纳,针对应用构架范式提供对应的演练计划,将专家经验功能化。
轻接入指应用接入故障演练的改造成本,最好是零改造或少改造,结合故障类型考虑,常见的无侵入的故障注入方式有:
- Java应用:JavaAgent attach
- K8s集群:Operator、CRD
- 主机:本地进程(agent)
- API:云产品OpenAPI、K8s管理面API
多样的故障注入方式对平台有两方面的考验,一是通用的流程要适配不同的触发方式和结果获取方式,实现上可以用行为(action)和回调(callback)对两者进行分离和扩展,二是在流程中要先埋好平台配套工具,那么对一次故障演练需要定义[预检查]-[预安装]-[故障注入]-[资源回收]等基础流程节点。
重交互指用户在故障演练平台可以直接获取到的信息,尤其指应用在演练过程中的运行数据,以及基于这些信息能做的反应,并将其表现为可视化的交互。故障注入只是手段,是拟真环境的准备,观察应用才是目的,所以故障演练最好与可观测平台打通,在实验中绑定应用运行指标,充分利用可观测的完整性,又或者依赖无侵入的工具进行采集,提供实时看板、异常识别和异常告警,指引用户对实验进行推进和回撤。
安全性
故障演练的防护手段,主要围绕四个方面做文章,分别是权限控制、环境隔离、爆炸半径和熔断制动。
权限控制目的在于尽量避免人为的误操作,在不恰当的地方注入不恰当的故障,造成不可挽回的影响。人的行为是最难控制的,除了非功能性的组织流程约束外,要在系统中限定人的可操作范围,构建多维度的授权体系,例如环境权限、应用权限、资源权限,以及故障类型权限。
环境隔离如前面混沌工程的原则中所说,最佳的拟真环境就是真实的环境,但稳态破坏的风险暗含故障影响的不可逆性,渐进式多环境实验更为恰当。应用的运行环境不是由故障演练定义的,而是由应用的发布机制决定的,开发、测试、预发、灰度、生产等等,故障演练平台需要做的是能够基于这些描述定义应用的元数据,并实现相应的纳管和授权。
爆炸半径有两方面的含义,一是故障的发生圈定在一定范围内,二是故障的影响圈定在一定范围内。前者的实践经验在于定义清晰的故障行为边界,常用[Target]-[Scope]-[Matcher]-[Action]分层模型,也是个漏斗模型,从给定的资源范围内按条件筛选出执行特定行为的目标,如下图。
以机器下线举例,Target是云主机,描述故障发生的主体类型,Scope是生产环境A应用B集群,描述故障发生的目标范围,Matcher是随机10%,描述范围内的命中实例,Action是关机,描述引发故障的具体行为。
严格意义上讲,故障的影响是无法被准确圈定的,甚至可以说故障演练的目的也只是为了检查故障的影响范围,再以此倒推预定处理方案的完整性,或发现其他问题,尝试应对,并周而复始地验证。就像在高速上开车,交通意外的风险总是存在的,我们要把好方向(控制流程),看清道路(观测指标),并随时准备刹车(中止演练)。
熔断制动的策略是故障演练的AEB(自动紧急制动系统),如何在人工干涉外避免演练冲出轨道,自动中止演练需要触发机制和执行机制配合。
触发机制指“When”,系统什么时候判断可以中止了,分为主动触发和被动触发。主动触发指系统一开始就知道该在什么时候结束演练,一般是给实验赋予一个存续周期,时间到了无论如何都回滚故障,也作为故障影响的最后兜底。被动触发指由外部系统告诉演练系统该结束了,一般是绑定应用特定的运行指标,并配置一个阈值作为超出影响的界限,这里需要构建的是基于应用可观测数据的监听与响应机制。
执行机制指“How”,如何撤回已注入的故障,这里也有两方面的要求,一是每一种实验行为(Action)都要有配套的正操作和反操作,如有内存抢占就要有内存释放,二是要保证反操作最少被执行一次,重点不在反操作的运行原理,而在正操作不能被“遗忘”,构建实验行为的状态管理机制,并确保操作的可重入和幂等性。