菜鸟笔记
提升您的技术认知

混沌工程介绍

混沌工程基础介绍

在一个由很多微服务组成的分布式系统中,我们永远难以全面掌握发生什么事件会导致系统局部不可用,甚至全面崩溃。但我们却可以尽可能地在这些不可用的情况发生之前找出系统中的脆弱点。Netflix的工程师团队是根据多年实践经验主动发现系统中脆弱点的一整套方法。这套方法现在已经逐渐演变成计算机科学的一门新兴学科,即“混沌工程”。通过一系列可控的实验和执行实验的原则,混沌工程将揭示出分布式系统中随时发生的各类事件是如何逐步导致系统整体不可用的。

混沌工程是什么

混沌工程是一门新兴的技术学科,它的初衷是通过实验性的方法,让人们建立复杂分布式系统能够在生产中抵御突发事件能力的信心。

只要你有过在生产环境中实际运行一个分布式系统的经历,你就应该清楚,各种不可预期的突发事件是一定会发生的。分布式系统天生包含大量的交互、依赖点,可能出错的地方数不胜数。硬盘故障,网络不通,流量激增压垮某些组件……我们可以不停地列举下去。这都是每天要面临的常事,任何一次处理不好就有可能导致业务停滞、性能低下,或者其他各种无法预料的异常行为。

在一个复杂的分布式系统中,我们单靠人力并不能够完全阻止这些故障的发生,而应该致力于在这些异常行为被触发之前,尽可能多地识别出会导致这些异常的、在系统中脆弱的、易出故障的环节。当我们识别出这些风险时,就可以有针对性地对系统进行加固、防范,从而避免故障发生时所带来的严重后果。我们能够在不断打造更具弹性[1]系统的同时,建立对运行高可用分布式系统的信心。

混沌工程正是这样一套通过在系统基础设施上进行实验,主动找出系统中的脆弱环节的方法学。这种通过实验验证的方法显然可以为我们打造更具弹性的系统,同时让我们更透彻地掌握系统运行时的各种行为规律。

混沌工程,是一种提高技术架构弹性能力的复杂技术手段。Chaos工程经过实验可以确保系统的可用性。混沌工程旨在将故障扼杀在襁褓之中,也就是在故障造成中断之前将它们识别出来。通过主动制造故障,测试系统在各种压力下的行为,识别并修复故障问题,避免造成严重后果。

它被描述为“在分布式系统上进行实验的学科,目的是建立对系统承受生产环境中湍流条件能力的信心。

Chaos Engineering is the discipline of experimenting on a systemin
order to build confidence in the system’s capabilityto withstand
turbulent conditions in production.

它也可以视为流感疫苗,故意将有害物质注入体内以防止未来疾病,这似乎很疯狂,但这种方法也适用于分布式云系统。混沌工程会将故障注入系统以测试系统对其的响应。这使公司能够为宕机做准备,并在宕机发生之前将其影响降至最低。

如何知道系统是否处于稳定状态呢?通常,团队可以通过单元测试、集成测试和性能测试等手段进行验证。但是,无论这些测试写的多好,我们认为都远远不够,因为错误可以在任何时间发生,尤其是对分布式系统而言,此时就需要引入混沌工程(Chaos Engineering)。

故障演练:目标是沉淀通用的故障模式,以可控成本在线上重放,以持续性的演练和回归方式运营来暴露问题,推动系统、工具、流程、人员能力的不断前进。

为什么需要混沌工程

  1. 混沌工程与测试的区别

混沌工程、故障注入和故障测试在侧重点和工具集的使用上有一些重叠。举个例子,Netflix的很多混沌工程实验的研究对象都是基于故障注入来引入的。混沌工程和其他测试方法的主要区别在于,混沌工程是发现新信息的实践过程,而故障注入则是基于一个特定的条件、变量的验证方法。

例如,当你希望探究复杂系统会如何应对异常时,会对系统中的服务注入通信故障,如超时、错误等,这是一个典型的故障注入场景。但有时我们希望探究更多其他非故障类的场景,如流量激增、资源竞争条件、拜占庭故障(例如性能差或有异常的节点发出错误的响应、异常的行为、对调用者随机返回不同的响应等)、非计划中的或消息内容非正常组合的处理等。如果一个面向公众用户的网站突然出现流量激增的情况,从而产生了更多的收入,那么我们很难将这种情况称为故障,但我们仍然需要探究清楚系统在这种情况下会如何变现。和故障注入类似,故障测试是通过对预先设想到的可以破坏系统的点进行测试,但是并不能去探究上述这类更广阔领域里的、不可预知的、但很可能发生的事情。

我们可以描述一下测试和实验最重要的区别。在测试中,我们要进行断言:给定一个特定的条件,系统会输出一个特定的结果。一般来说,测试只会产生二元的结果,即验证一个结果是真还是假,从而判定测试是否通过。严格意义上来说,这个实践过程并不能让我们发掘出系统未知的或尚不明确的认知,它仅仅是对已知的系统属性可能的取值进行测验。而实验可以产生新的认知,而且通常还能开辟出一个更广袤的对复杂系统的认知空间。整本书都在探讨这个主题——混沌工程是一种帮助我们获得更多的关于系统的新认知的实验方法。它和已有的功能测试、集成测试等测试已知属性的方法有本质上的区别。

一些混沌工程实验的输入样例:

  • 模拟整个云服务区域或整个数据中心的故障。
  • 跨多实例删除部分Kafka主题(Topic)来重现生产环境中发生过的问题。
  • 挑选一个时间段,针对一部分流量,对其涉及的服务之间的调用注入一些特定的延时。
  • 方法级别的混乱(运行时注入):让方法随机抛出各种异常。
  • 代码插入:在目标程序中插入一些指令,使得故障注入在这些指令之前先运行。
  • 强迫系统节点间的时间彼此不同步。
  • 在驱动程序中执行模拟I/O错误的程序。
  • 让一个Elasticsearch集群的CPU超负荷。

混沌工程实验的机会是无限的,可能会根据您的分布式系统的架构和您组织的核心业务价值而有所不同。

  1. 实施混沌工程的先决条件

要确定您的组织是否已准备好开始采用Chaos Engineering,您需要回答一个问题:您的系统是否能够适应现实世界中的事件,例如服务故障和网络延迟峰值?
如果您知道答案是“否”,您还有一些工作要做。Chaos Engineering非常适合揭露生产系统中未知的弱点,但如果您确定混沌工程实验会导致系统出现严重问题,那么运行该实验就没有任何意义。先解决这个弱点。然后回到Chaos Engineering,它将发现你不了解的其他弱点,或者它会让你更有信心你的系统实际上是有弹性的。混沌工程的另一个基本要素是可用于确定系统当前状态的监控系统。如果不了解系统的行为,您将无法从实验中得出结论。

混沌工程原则

“混乱”一词让我们想起随机性和无序性。然而,这并不意味着混沌工程的实施也是随机和随意的,也不意味着混沌工程师的工作就是引发混乱。相反的是,我们把混沌工程视为一门原则性很强的学科,特别是一门实验性的学科。

在上面的引用中,Dekker观测了分布式系统的整体行为,他也主张从整体上了解复杂系统是如何失效的。我们不应该仅仅着眼于发生故障的组件,而是应该尝试去理解,像组件交互中一些偶发的意外行为,最终是如何导致系统整体滑向一个不安全、不稳定的状态的。

你可以将混沌工程视为一种解决“我们的系统离混乱边缘有多少距离”的经验方法。从另一个角度去思考,“如果我们把混乱注入系统,它会怎么样?”
在这一部分,我们会介绍混沌工程实验的基本设计方法,之后会讨论一些更高级的原则。这些原则建立在真正实施混沌工程的大规模系统之上。在实施混沌工程的过程中,并不是所有高级原则都必须用到。但我们发现,运用的原则越多,你对系统弹性的信心就越充足。

软件系统里并没有类似的传递函数。像很多复杂系统一样,我们无法为软件系统表现出的各种行为建立一个预测模型。如果我们有这样一个模型,可以推导出一次网络延迟骤升会给系统带来什么影响,那就太完美了。但不幸的是,迄今为止我们并没有发现这样一个模型。

因为我们缺乏这样一个理论的预测模型,因此就不得不通过经验方法来了解在不同的情况下我们的系统会如何表现。我们通过在系统上运行各种各样的实验来了解系统的表现。我们尝试给系统制造各种麻烦,看它会发生什么状况。

但是,我们肯定不会给系统不同的随机输入。我们在系统分析之后,期望能够最大化每个实验可以获得的信息。正如科学家通过实验来研究自然现象一样,我们也通过实验来揭示系统的行为。
在开发混沌工程实验时,请牢记以下原则,它们将有助于实验的设计。
 建立稳定状态的假设。
 用多样的现实世界事件做验证。
 在生产环境中进行实验。
 自动化实验以持续运行。
 最小化爆炸半径。

建立稳定状态假设

任何复杂系统都会有许多可变动的部件、许多信号,以及许多形式的输出。我们需要用一个通用的方式来区分系统行为是在预料之内的,还是在预料之外的。我们可以将系统正常运行时的状态定义为系统的“稳定状态”。

很多现有的数据采集框架已经默认采集大量的系统级别指标,所以通常来说,让你的系统有能力抓取业务级别的指标比抓取系统级别的指标更难。然而花精力来采集业务级别的指标是值得的,因为它们才能真实地反映系统的健康状况。这些指标获取的延迟越低越好:那些在月底算出来的业务指标和系统今天的健康状况毫无关系。

在选择指标时,你需要平衡以下几点:
 指标和底层架构的关系。
 收集相关数据需要的工作量。
 指标和系统接下来的行为之间的时间延迟。

如果你还不能直接获得和业务直接相关的指标,则也可以先暂时利用一些系统指标,比如系统吞吐率、错误率、99%以上的延迟等。你选择的指标和自己的业务关系越强,得到的可以采取可执行策略的信号就越强。你可以把这些指标想象成系统的生命特征指标,如脉搏、血压、体温等。同样重要的是,在客户端验证一个服务产生的警报可以提高整体效率,并可以作为对服务器端指标的补充,以构成某一时刻用户体验的完整画面。

用多样的现实世界事件做验证

每个系统,从简单到复杂,只要运行时间足够长,都会受到不可预测的事件和条件的影响。例如,负载的增加、硬件故障、软件缺陷,以及非法数据(有时称为脏数据)的引入。我们无法穷举所有可能的事件或条件,但常见的有以下几类:
 硬件故障。
 功能缺陷。
 状态转换异常(例如发送方和接收方的状态不一致)。
 网络延迟和分区。
 上行或下行输入的大幅波动以及重试风暴。
 资源耗尽。
 服务之间不正常的或者预料之外的组合调用。
 拜占庭故障(例如性能差或有异常的节点发出有错误的响应、异常的行为、对调用者随机地返回不同的响应等)。
 资源竞争条件。
 下游依赖故障。
也许最复杂的情况是上述事件的各类组合导致系统发生异常行为。

要彻底阻止对可用性的各种威胁是不可能的,但是我们可以尽可能地减轻这些威胁。在决定引入哪些事件的时候,我们应当估算这些事件发生的频率和影响范围,权衡引入它们的成本和复杂度。在Netflix,我们选择关闭节点的一方面原因是,节点中断在现实中发生的频率很高,而引入关闭节点事件的成本和难度很低。对于区域故障来说,即使引入一些事件的成本高昂且引入流程复杂,我们还是必须要做,因为区域性故障对用户的影响是巨大的,除非我们有足够的弹性应对它。

文化因素也是一种成本。例如在传统数据中心的文化中,基础设施和系统的健壮性、稳定性高于一切,所以传统数据中心通常会对变更进行严格的流程控制。这种流程控制,和频繁关闭节点的操作是一对天然的矛盾体。随机关闭节点的实验对传统数据中心的文化是一种挑战。随着服务从数据中心迁移到云上,管理基础设施的职责被转移给了云服务提供商,硬件的各类故障都由云服务平台管理,工程部门对硬件故障就越来越习以为常。这种认知实际上在鼓励一种将故障当作可预料的态度,这种态度可以进一步推动混沌工程的引入和实施。虽然硬件故障并不是导致线上事故最常见的原因,但是注入硬件故障是在组织中引入混沌工程并获益的一个较简单的途径。

和硬件故障一样,一些现实世界的事件也可以被直接注入,例如每台机器的负载增加、通信延迟、网络分区、证书失效、时间偏差、数据膨胀等。除此之外的一些事件的注入可能会具有技术或文化上的障碍,所以我们需要寻找其他方法来看一看它们会如何影响生产环境。[1]例如,发布有缺陷的代码。金丝雀发布可以阻止许多显而易见的简单软件缺陷被大规模发布到生产环境中,但其并不能阻止全部的缺陷被发布出去。故意发布有缺陷的代码风险太大,可能会给用户带来严重的影响(参见第7章)。要模拟这类发布所带来的缺陷问题,一种办法是对相应的服务调用注入异常。

在生产环境中进行实验

在我们这个行业里,在生产环境中进行软件验证的想法通常都会被嘲笑。“我们要在生产环境中验证”这句话更像是黑色幽默,它可以被翻译成“我们在发布之前不打算完整地验证这些代码”。

经典测试的一般信条是,寻找软件缺陷要离生产环境越远越好。例如,在单元测试中发现缺陷比在集成测试中发现更好。这里的逻辑是,离生产环境的整个部署越远,就越容易找到缺陷的根本原因并将其彻底修复。如果你曾经分别在单元测试、集成测试和生产环境中调试过问题,上述逻辑的好处就不言而喻了。

但是在混沌工程领域,整个策略却要反过来。在离生产环境越近的地方进行实验越好。理想的实践就是直接在生产环境中进行实验。

在传统的软件测试中,我们是在验证代码逻辑的正确性,是在对函数和方法的行为有良好理解的情况下,写测试来验证它们对不对。换句话说,是在验证代码写得对不对。

而当进行混沌工程的实验时,我们所感兴趣的是整个系统作为一个整体的行为。代码只是整个系统中比较重要的一部分,而除了代码,整个系统还包含很多其他方面,特别是状态、输入,以及第三方系统导致的难以预见的系统行为。

下面来深入了解一下为什么在生产环境中进行实验对混沌工程来说是至关重要的。我们要在生产环境中建立对系统的信心,所以当然需要在生产环境中进行实验。否则,我们就仅仅是在其他并不太关心的环境中建立对系统的信心,这会大大削弱这些实践的价值。

即便你不能在生产环境中进行实验,也要尽可能地在离生产环境最近的环境中进行。越接近生产环境,对实验外部有效性的威胁就越少,对实验结果的信心就越足。

自动化实验以持续运行

手动执行一次性的实验是非常好的第一步。当我们想出寻找故障空间的新方法时,经常从手动的方法开始,小心谨慎地处理每一件事以期建立对实验和对系统的信心。所有当事人都聚集在一起,并向CORE(Critical Operations Response Engineering,Netflix的SRE团队的名称)发出一个警示信息,说明一个新的实验即将开始。

这种谨小慎微的态度有利于:a)正确运行实验,b)确保实验有最小的爆炸半径。在成功执行实验后,下一步就是将这个实验自动化,让其持续运行。

如果一个实验不是自动化的,那么就可以将这个实验废弃。

最小化爆炸半径

我们经常运行本来只会影响一小部分用户的测试,却由于级联故障无意中影响到了更多的用户。在这些情况下,我们不得不立即中断实验。虽然我们绝不想发生这种情况,但随时遏制和停止实验的能力是必备的,这可以避免造成更大的危机。我们的实验通过很多方法来探寻故障会造成的未知的和不可预见的影响,所以关键在于如何让这些薄弱环节曝光出来而不会因意外造成更大规模的故障。我们称之为“最小化爆炸半径”。
能带来最大信心的实验也是风险最大的,是对所有生产流量都有影响的实验。而混沌工程实验应该只承受可以衡量的风险,并采用递进的方式,进行的每一步实验都在前一步的基础之上。这种递进的方式不断增加我们对系统的信心,而不会对用户造成过多不必要的影响。

最小风险的实验只作用于很少的用户。为此在我们验证客户端功能时只向一小部分终端注入故障。这些实验仅限于影响一小部分用户或一小部分流程。它们不能代表全部生产流量,但却是很好的早期指标。例如,如果一个网站无法通过早期实验,那么就没有必要影响其余的大量真实用户。

在自动化实验成功之后(或者在少量的设备验证没有涵盖要测试的功能时),下一步就是运行小规模的扩散实验。这种实验会影响一小部分用户,因为我们允许这些流量遵循正常的路由规则,所以它们最终会在生产服务器上均匀分布。对于此类实验,你需要用定义好的成功指标[1]来过滤所有被影响的用户,以防实验的影响被生产环境的噪声掩盖。[2]小规模扩散实验的优势在于,它不会触及生产环境的阈值,例如断路器的阈值,这样你便可以验证每一个单一请求的超时和预案。这可以验证系统对瞬时异常的弹性。

接下来是进行小规模的集中实验,通过修改路由策略将所有实验覆盖的用户流量导向特定的节点。在这些节点上会做高度集中的故障、延迟等测试。在这里,我们会允许断路器打开,同时将隐藏的资源限制暴露出来。如果我们发现有无效的预案或者奇怪的锁竞争等情况导致服务中断,那么只有实验覆盖的用户会受到影响。这个实验模拟生产环境中的大规模故障,同时可以把负面影响控制到最小,结果却能使我们对系统建立高度的信心。

风险最大但最准确的实验是无自定义路由的大规模实验。在这个实验级别,实验结果应该在主控制台上显示,同时因为断路器和共享资源的限制,实验可能会影响不在实验覆盖范围内的用户。当然,没有什么比让所有生产环境中的用户都参与实验,能给你更多关于系统可以抵御特定故障场景的确定性了。

除了不断扩大实验范围,在实验造成过多危害时及时终止实验也是必不可少的。有些系统设计会使用降级模式来给用户带来较小的影响,这还好,但是在系统完全中断服务的时候,就应该立即终止实验。这可以由之前讨论过的“大红色按钮”来处理。

我们强烈建议实施自动终止实验,尤其是在定期自动执行实验的情况下。关于弄清楚如何构建一个可以实时监控我们感兴趣的指标,并可以随时实施混沌工程实验的系统,这完全依赖于你手上的独特的系统构造。