本文翻译自 Netflix 工程师合著的 *Chaos Engineering*一书。这本书介绍了混沌工程的主要概念,以及如何在组织中实践这些概念和经验。也许我们开发的相关工具只适用于 Netflix 自身的业务和系统环境,但我们相信工具背后的原则可以更广泛地应用于其他领域。
InfoQ 将就这一专题持续出稿,感兴趣的同学可以持续关注。
本文略长,共计 1.3 万字,预计阅读时间 35 分钟。
混沌工程原则
优化复杂系统的性能通常需要在混乱的边缘进行,即系统行为将要变得混乱且无迹可寻之前。
Sydney Dekker,《陷入失败》
“混乱”一词让我们想起随机性和无序。然而,这不意味着混沌工程的实施也是随机和随意的,也不意味着混沌工程师的工作就是引发混乱。相反,我们把混沌工程视为一种学科,一种实验学科。
在上面的引用中,Dekker 观测了分布式系统的整体行为,他也主张从整体上了解复杂系统是如何失效的。我们不应该仅仅着眼于发生故障的组件,而是应该尝试去理解,例如组件交互中的一些偶发意外行为,最终如何导致系统整体滑向不安全,不稳定的状态。
你可以将混沌工程视为一种解决“系统离混乱的边界有多远” 的经验方法。从另一个角度去思考,“如果我们把混乱注入到系统里,它会怎么样?”
在这一部分,我们会介绍混沌工程实验的基本设计,之后我们会讨论一些更高级的原则。这些原则建立在真实实践混沌工程的大规模系统之上。在实践混沌工程的过程中,并不是必须遵照所有高级原则,但我们发现,运用的原则越多,你对系统弹性的信心就越充足。
实验
在大学里,电气工程专业的学生必须学习一门“信号和系统”的课程,在这个课程中他们学习如何使用数学模型来推理电气系统的行为。其中一门需要掌握的技术被称为拉普拉斯变换。你可以用拉普拉斯变换,将整个电路的行为用一个数学函数表达,我们称之为传递函数。传递函数描述的是系统在受到脉冲时如何响应,输入信号包含所有可能的输入频率的总和。一旦你有了一个电路的传递函数,就可以预测它在受到所有可能的输入信号时会如何响应。
软件系统里并没有类似的传递函数。像很多复杂系统一样,我们无法为软件系统表现出的各种行为建立一个预测模型。如果我们有这样一个模型,可以推导出一次网络延迟骤升会给系统带来什么影响,那样就太完美了。但不幸的是,迄今为止我们并没有发现这样一个模型。
因为我们缺乏这样的理论预测模型,所以就不得不通过经验方法来理解,在各种不同情况下系统会如何表现。我们通过在系统上运行各种各样的实验,尝试给系统制造各种麻烦,看它会发生什么状况。
但是,我们肯定不会给系统随机的不同的输入。我们在系统性分析之后,期望可以最大化每个实验可以获得的信息。正如科学家通过实验来研究自然现象一样,我们通过实验来揭示系统的行为。
FIT 故障注入测试
分布式系统的经验告诉我们,各种系统问题基本都是由预期外的事件或不良的延迟导致的。2014 年初,Netflix 开发了一个名为 FIT 的工具,意思是故障注入测试(Failure Injection Testing)。这个工具能让工程师在访问服务的一类请求的请求头中注入一些失败场景,当这些注入了失败场景的请求在系统中流转时,微服务中被注入的故障锚点会根据不同的失败场景触发相应的逻辑。
例如,我们要测试系统在某个保存用户数据的微服务中断时的弹性能力。我们预计系统中某些服务不会如预期运行,但诸如重放等基本功能仍适用于已登录用户。使用 FIT,我们指定进入服务的所有请求中,有 5%会在请求头中包含失败场景。当这些请求在系统中传播时,只要发到客户数据微服务的请求都会自动收到故障响应。
高级原则
在开发混沌工程实验时,牢记以下原则将有助于实验设计。在接下来的章节里将会深入探讨以下每个原则:
建立稳定状态的假设;
多样化现实世界事件;
在生产环境运行实验;
持续自动化运行实验;
最小化“爆炸半径”。
预测和预防故障
在 2017 年美洲 SRECon 大会上,Preetha Appan 介绍了她和她的团队在 indeed.com 开发的一个引入网络故障的工具。在演讲中,她阐述了预防故障的切实需求,而不是仅仅在故障发生时做出响应。他们的工具 Sloth,作为一个守护进程,运行在基础设施的每一个节点上,包括数据库和索引服务器。参见 https://www.usenix.org/conference/srecon17americas/program/presentation/appan
3. 建立稳定状态的假设
对于任何复杂系统,都会有许多可变动的部件,有许多形式的输入输出。我们需要有一个通用的方式来区分系统行为是预期中的,还是预期之外的。我们将系统正常运行时的状态定义为系统的“稳定状态”。
如果你在开发或运行一个软件服务,你如何清楚地了解它是否在正常工作?你如何认定它的稳定状态?你应该从哪里着眼来回答上面的问题?
稳定状态
在系统思维社区中使用“稳定状态”这个术语,来指代系统维持在一定范围内或一定模式的属性,诸如人体维持体温在一定范围内一样。我们期望通过一个模型,基于所期望的业务指标,来描述系统的稳定状态。这是我们在识别稳定状态方面的一个目标。要牢记稳定状态一定要和客户接受程度一致。在定义稳定状态时,要把客户和服务之间的服务水平协议(SLA)纳入考量。
如果你的服务是一个新服务,想知道它是否正常工作的唯一途径可能只有自己去运行一下。例如服务可以通过网站访问,那你可能需要打开网页并尝试触发一个任务或事务来检查这个服务。
这种快速检查系统健康的方法显然并不理想:他是劳动密集型的,也就是说我们基本不会或不常会做这件事。我们可以自动化运行类似测试,但这还不够。如果这些测试不能找到系统中的问题,该怎么办?
更好的方法是先搜集和系统健康有关的数据。如果你在阅读本书,我们相信你已经在使用某种指标收集系统来监控你的系统。有大量的开源和商业工具可以采集系统方方面面的数据:CPU 负载,内存使用情况,网络 I/O,以及各类时序信息,例如需要多长时间响应一个 Web 请求,或者查询各种数据库的耗时。
系统的指标有助于帮助我们诊断性能问题,有时也能帮助我们发现功能缺陷。业务指标与系统指标形成对比,业务指标通常回答这样的问题:
我们正在流失用户吗?
用户目前可以操作网站的关键功能吗?例如在电子商务网站里为订单付款,添加购物车等。
目前存在较高的延迟致使用户不能正常使用我们的服务吗?
某些组织有非常明确的,和收入直接相关的实时指标。例如像 Amazon 和 eBay 会跟踪销售量,Google 和 Facebook 会跟踪广告曝光次数。
由于 Netflix 使用的是按月订阅模式,所以我们没有这类指标。我们也会测量注册率,这是一个重要的指标,但是只看注册率不能反映整体系统的健康状况。
我们真正想要的是一个可以反映当前活跃用户的满意状况的指标,因为满意的用户才有可能连续订阅。可以这么说,如果当前和系统做交互的用户是满意的,那么我们基本可以确定系统目前是健康的。
遗憾的是,我们目前还没找到一个直接、实时的可以反映用户满意度的指标。我们会监控客服电话的呼叫量,这或许是一个可以间接反映客户满意度的指标,但是从运营角度出发,我们需要更快、更细粒度的反馈。Netflix 有一个还不错的可以间接反映用户满意度的指标——播放按钮的点击率。我们管这个指标叫做视频每秒开始播放数,简称为 SPS(Starts per sencond)。
SPS 很容易测量,而且因为用户付费订阅服务的直接目的就是看视频,所以 SPS 应该和用户满意度密切相关。这个指标在东海岸下午 6 点会明显高于早上 6 点。我们就可以据此来定义我们系统的稳定状态了。
相比某个服务的 CPU 负载来说,Netflix 的可靠性工程师(SREs)更关注 SPS 的下降:SPS 下降会立刻向他们发送警报。CPU 负载的尖刺有时重要有时不重要,而像 SPS 这样的业务指标才是系统边界的表述。这才是我们要关注并验证的地方,而不是那些像 CPU 负载类的内部指标。
很多现有的数据采集框架默认采集大量的系统级别指标,所以通常来说,让系统有能力抓取到业务级别的指标比系统级别更难。然而花精力采集业务级别指标是值得的,因为它们才是系统健康的真实反映。
这些指标获取的延迟越低越好:那些在月底算出来的业务指标和系统今天的健康状况毫无关系。
对于选择的任何指标,需要平衡以下几点:
指标和底层架构的关系;
收集相关数据需要的工作量;
指标和系统后续行为间的时间延迟。
如果你还不能直接获取和业务直接相关的指标,可以暂时先利用一些系统指标,比如系统吞吐率,错误率,99%以上的延迟等。你选择的指标和业务关系越强,得到的可以采取可执行策略就越强。你可以把这些指标想象成系统的生命特征指标,像脉搏、呼吸、血压、体温等。同样重要的是,在客户端验证服务产生的警报可以提高整体效率,而且可以作为对服务端指标的补充,以构成某一时刻用户体验的完整画面。
3.1 如何描述稳定状态
与人体的生命体征一样,你需要清楚“健康”的数值范围。例如我们知道 37 摄氏度以下是人体的健康温度。
请牢记目标:我们期望通过一个模型,基于所期望的业务指标,来描述系统的稳定状态。
很多业务指标并不像我们的体温那样稳定,他们也许会经常剧烈波动。我们再举一个医学中的例子,心电图测量心脏附近人体表面的电压差,这个信号是用来观测心脏行为的。但心电图采集的信号会随着心脏跳动而变化,因此医生不能将这个信号与单个阈值进行比较来判断患者是否健康。医生需要比较的是信号波动的模式和患者健康时的模式是否一致。
在 Netflix,SPS 也不是一个和人体体温一样的稳定指标,它也随着时间波动。下图描绘的就是 SPS 随时间变化的波动情况,可以看出,它有一个稳定的模式。这是因为人们习惯于在晚餐时间看电视节目。因为 SPS 随时间的变化可以预期,所以我们就可以用一周前的 SPS 波动图作为稳定状态的模型。Netflix 的可靠性工程师们总是将过去一周的波动图放在当前的波动图之上,以发现差异。就像下图中当前的图线是红色,上一周的图线是黑色。
图 1 SPS 随时间变化
你所处的行业决定了你的指标是否以一种可以预期的方式随时间波动。例如,如果你负责一个新闻网站,流量的尖刺可能来源于一个大众关注度高的新闻事件。某些事件的尖刺可以预期,比如选举、重大赛事,但是其他类型的事件不太可能被预测到。在这一类场景中,准确描述系统的稳定状态将会非常复杂。无论哪种情况,描述稳定状态都是建立有意义假设的必要前提。
3.2 建立假设
每当你进行混沌工程实验的时候,你应该首先在心里对实验结果有一个假设。将你的系统直接置于各种事件里看看系统会怎么样的想法可能比较诱人,然而,没有一个预先的假设,你就不清楚应该从数据里找什么,最终也难以得出有效结论。
定义好指标并理解其稳定状态的行为之后,你就可以使用它们来建立实验的假设。思考一下当你向系统注入不同类型的事件时,稳定状态行为会发生什么变化。例如你向中间层的服务增加请求数量,稳定状态会被破坏还是保持不变?如果被破坏了,你期待系统如何表现?
在 Netflix,我们使用混沌工程来提高系统弹性,因此我们实验的假设一般是这样的形式:向系统注入的事件不会导致系统稳定状态发生明显的变化。
例如,我们在弹性实验里故意让一个非关键服务中断,以验证系统是否可以优雅降级。我们可能会中断基于用户浏览历史展示的个性化影片列表服务,这时系统应该返回默认的影片列表。
每当我们执行非关键服务中断实验时,我们的假设都是注入的故障不会对 SPS 产生影响。换句话说,我们的假设是实验措施不会使系统行为偏离稳定状态。
我们还会定期执行演习,例如将一个 AWS 区域的流量全部转移到另外两个区域。目的是验证我们在进行这类故障恢复时,SPS 不会偏离稳定状态。这可以让我们对出现一个区域中断性故障时执行故障恢复充满信心。
最后,让我们思考一下,如何衡量稳定状态行为的变化。即便你已经建立了稳定状态行为模型,你也需要定义清楚,当偏离稳定状态行为发生时你要如何测量这个偏差。如果你曾调节过报警系统的阈值,就应该清楚,定义偏离稳定状态的偏差是否在合理的范围是具有挑战性的。只有定义清楚“正常”的偏差范围,才可以获得一套验证假设的完善的测试集。
金丝雀分析
在 Netflix,我们使用金丝雀发布:我们首先把新代码发布到一个只接受一小部分生产流量的小型集群,然后进行验证以确保新发布的健康性,最后再进行全面发布。
我们使用名叫自动金丝雀分析 ACA(Automated Canary Analysis)的内部工具,通过稳定状态的指标,来验证金丝雀集群是否健康。ACA 拿金丝雀集群与一个大小相同,并运行旧代码的基准集群,比较一系列系统指标。金丝雀集群的得分必须足够高才能通过金丝雀发布阶段。服务拥有者还可以向 ACA 中添加应用的自定义指标。
通过 ACA,工程师可以清晰地观察描述稳定状态的重要参数,同时可以对基于稳定状态建立的假设,在不同集群间的表现进行比较验证。我们其他的一些混沌工程工具也会使用 ACA 提供的服务来测试稳定状态变化的各类假设。
4. 多样化现实世界事件
每个系统,从简单到复杂,只要运行时间足够长,都会受到不可预测的事件和条件的影响。例如负载的增加、硬件故障、软件缺陷、还有非法数据(有时称为脏数据)的引入。我们无法穷举所有可能的事件或条件,但常见的有以下几类:
硬件故障;
功能缺陷;
状态转换异常(例如发送方和接收方的状态不一致);
网络延迟或隔离;
上行或下行输入的大幅波动以及重试风暴;
资源耗尽;
服务之间的不正常的或者预料之外的组合调用;
拜占庭故障(例如性能差或有异常的节点发出有错误的响应、异常的行为、对调用者随机返回不同的响应,等等);
资源竞争条件;
下游依赖故障。
也许最复杂的情况是上述事件的各类组合导致系统发生异常行为。
要彻底阻止对可用性的各种威胁是不可能的,但是我们可以尽可能减轻这些威胁。在决定引入哪些事件时,我们应当估算这些事件发生的频率和影响范围,然后权衡引入他们的成本和复杂度。在 Netflix,我们选择关闭节点的一方面原因就是,节点中断在现实中发生频率很高,同时引入关闭事件的成本和难度很低。对于区域故障来说,即使引入的成本高昂且复杂,我们还是必须要做,因为区域性故障对用户的影响是巨大的,除非有足够的弹性应对它。
文化因素也是一种成本。例如对于传统数据中心来说,健壮性、稳定性、变更的严格控制,在文化上要优先于敏捷性——随机关闭节点类型的实验对传统数据中心文化上是一种挑战。随着迁移到云上带来的硬件职责外部化,工程部门对硬件故障越来越习以为常。这种认知实际上在鼓励一种对待故障可预期性的态度,这种态度可以进一步推动混沌工程的采用和被支持。虽然硬件故障并不是线上事故的最常见原因,但是它相对容易理解,同时也是在组织里引入混沌工程并获益的一个较简单的途径。
和硬件故障一样,一些现实世界的事件也可以直接注入:例如每台机器的负责增加、通信延迟、网络分区、证书失效、时间偏差、数据膨胀等等。除此之外,其他的一些事件的注入可能会具有技术或文化的障碍,所以我们需要找寻其他方法来看看它们会如何影响生产环境。例如发布有缺陷的代码。金丝雀发布可以阻止许多简单和明显的软件缺陷被大规模发布到生产环境,但并不能阻挡全部的缺陷被发布出去。故意发布有缺陷的代码风险太大,可能会造成对用户过度的影响(参见最小化“爆炸半径”一节)。要模拟这类发布带来的缺陷问题,一种办法是对相应的服务调用注入异常。
Blockade
戴尔云管理团队开发了一个开源的,基于 Docker 的,用来测试分布式应用的网络故障或隔离的混沌工程工具,名叫 Blockade。它通过在 Docker 的宿主机网络管理中创建各种异常场景,来影响在 Docker 容器中运行的应用。这个工具的一些功能包括:在容器间创建任意分区、容器数据丢包,容器网络延迟注入,以及在故障注入时便捷的系统监控能力。
我们了解了通过对相应服务调用注入异常,来模拟一次失败的部署,这是因为错误代码带来的影响已经被隔离在仅仅是运行他们的几台服务器中。通常来说,故障隔离既可以是物理隔离也可以是逻辑隔离。隔离是容错的必要但不充分条件。要获得一个可以接受的结果,还需要某种形式的冗余或者优雅降级。只要子部件的故障能引发整个系统不可用,那么这个故障就没有被隔离。故障的影响范围和隔离范围被称为故障的故障域。
提供产品的组织设定产品可用性的预期,并负责制定 SLA ——哪些东西一定不能失败或者失败是否具备预案。发现和验证故障域以确保满足产品的可用性预期是工程师团队的责任。
故障域还为混沌工程提供了一个乘数效应。回到刚才的例子,如果对服务调用失败的模拟是成功的,那么这不仅可以验证该服务对部署缺陷代码的弹性,同时也可以验证它在高负载、错误配置、其他异常终止等场景引发失败时的弹性。此外,在故障域中你可以向系统注入各类故障来观察故障的症状。例如你在真实环境中看到了一些症状,你可以逆向找出症状的根源故障及其发生的几率。在故障域的级别进行实验还有个好处是,可以提前对不可预见的导致故障的原因做好准备。
我们不应该给系统注入引发故障的根因事件。每一个资源都会形成一个故障域,这个故障域包括所有对它有强依赖的部件(当该资源不可用时,所有依赖也都不再可用)。向系统注入故障根因的事件会暴露出这些因为资源共享形成的故障域。团队也经常会为这类资源共享的情况而感到惊讶。
我们不需要穷举所有可能对系统造成改变的事件,只需要注入那些频繁发生且影响重大的事件,同时要足够理解会被影响的故障域。工程师在设计系统架构的时候也许已经考虑了故障域。例如在微服务架构中,服务组是最重要的故障域之一。有时团队认为他们的服务不是关键服务,但最后却在故障时因为没有做合理的隔离导致整个系统宕机。所以,在系统中用实验验证这些预先定好的边界非常关键。
再强调一次,注入的事件一定是你认为系统能处理的。同时,注入的事件应该是所有可能的真实世界的事件,而不仅仅是故障或延迟。我们上面举的例子更多关注软件部分,但真实世界里人的因素对系统弹性和可用性也起到了至关重要的作用。例如对故障处理中人工控制的流程和工具进行实验或演习也会提高可用性。
Netflix 的混沌工程的规范化
在 Netflix,混乱猴子和混乱金刚是由一个集中的团队进行开发,发布,维护和制定规则和执行的。而 FIT 是一个自助服务的工具。这也是第一次我们的工具需要微服务工程师们的时间和接受才能运行。当然,他们不得不对工具发现的对弹性的威胁做出回应。即便许多团队在开发阶段确实尝试到了 FIT 的好处,但还是难以广泛和高频率的使用。我们在 FIT 有这么强大的工具来提高弹性,但是我们却遇到了普及问题。
Netflix 的混沌工程团队认为现在我们分别有了小尺度和大尺度(如关闭节点和关闭整个区域)上的实践,但我们还缺少中间环节的最佳实践:持续提高我们对微服务各类故障的弹性。FIT 对这方面的探索提供了一个基础,但是执行这样一个实验的负担,使得我们并没能像混乱猴子和混乱金刚那样,在工程师团队中形成一致。
我们重新回过头从头到尾仔细思考了如何将混沌工程作为一个落地的实践。我们走访了一些工程师,询问他们混沌工程对他们意味着什么。多数的回复都是混沌工程就是在生产环境中搞破坏。听起来很有意思,很多人都在生产环境中搞破坏,但这些破坏都毫无价值。我们要想办法让混沌工程规范化。
2015 年中,我们发表了混沌工程原则(Principles of Chaos Engineering),定义了混沌工程作为计算机科学中的一门新的学科。
通过这个新的规范形式,我们在 Netflix 正式推动了混沌工程。我们为混沌工程的组成绘制了蓝图:我们清楚目标是什么,而且我们清楚如何评估做的够不够好。原则为我们奠定了基础,让我们可以把混沌工程提升到新的高度。
5. 在生产环境运行实验
在我们这个行业里,在生产环境中进行软件验证的想法通常都会被嘲笑。“我们要在生产环境中验证”这句话更像是黑色幽默,它被翻译成“我们在发布之前不打算完善地验证这些代码”。
经典测试的一个普遍信条是,寻找软件缺陷要离生产环境越远越好。例如,在单元测试中发现缺陷比在集成测试中发现更好。这里的逻辑是,离生产环境越远,或者是离发布越远的时候,发现的缺陷就越容易被找到根本原因并彻底修复。如果你曾经分别在单元测试,集成测试,和生产环境中 debug 过问题,上述逻辑的智慧就不言而喻了。
但是在混沌工程领域里,整个策略却要反过来了。在离生产环境的越近的地方进行实验越好。理想的实践就是直接在生产环境中执行实验。
传统软件测试中,我们是在验证代码的逻辑正确性。是我们对函数和方法的行为有很好理解的情况下,写测试来验证它们对不对,换句话说,是验证代码写得对不对。
而当我们执行混沌工程实验时,我们所感兴趣的是整个系统作为一个整体的行为。代码只是整个系统的重要组成部分,而除了代码之外,整个系统还有很多其他方面。特别是,状态、输入、以及第三方系统导致的难易预见的系统行为。
下面我们深入了解一下为什么在生产环境中执行对混沌工程来说是至关重要的。我们要建立的是对系统在生产环境的信心,我们当然就需要在生产环境中进行实验。否则,我们就仅仅是在其他我们并不太关心的环境中建立信心,这会大大削弱这些实践的价值。
5.1 状态和服务
我们之前简要讨论过系统的“状态”。在这一节我们仔细讨论一下有状态服务。如果我们不需要在系统中维护任何状态,那么软件工程将会简单很多。然而状态却在我们建造的这类系统中无处不在。
在微服务架构中,当我们提到状态的时候,通常我们说的是有状态服务,例如数据库服务。数据库仅仅保存一些测试设置开关的系统,与保存所有生产数据的系统,在行为上是不同的。其他一些有状态服务包括缓存服务,对象存储服务,以及可持久化的消息服务。
配置数据是另一种影响系统行为的状态。无论使用静态配置文件,动态配置服务(像 etcd),还是两者的组合(像我们在 Netflix 一样),这些配置信息本身也是一种状态,而且它们可以严重影响系统行为。
即使在无状态的服务中,状态仍然以内存中的数据结构的形式存在于请求之间,并因此影响到后续请求。
还有无数的角落里隐藏着许许多多的状态。在云服务中,自动伸缩组中虚拟机或者容器的个数也是一个系统的状态,这个状态会随时间、随外部需求或集群变化而不断改变。网络硬件如交换机和路由器也有状态。
总有一些意想不到的状态会伤害到你。如果你是我们的目标读者,你应该已经踩过一些坑了。要想解决混沌工程关注的对于系统弹性的威胁,你需要把生产环境中存在的各式各样的状态问题暴露给混沌工程实验。
5.2 生产环境中的输入
对于软件工程师来说,最难的一课莫过于,系统的用户永远不会如你预期的那样与你的系统进行交互。这一课在设计系统的 UI 的时候尤为典型,在设计混沌工程实验的时候也需要牢记。
设想一下,你的系统提供了从用户接收不同类型请求的服务。你可以为用户输入设计一个组合数据模型,但因为用户永远不会如你预期般行动,生产系统总是会收到测试覆盖之外的输入数据组合。
真正对系统的建立信心的唯一方法就是在生产环境中针对真实的输入数据验证实验。
5.3 第三方系统
分布式系统就是,其中有台你根本不知道的机器故障了,有可能会让你自己的服务也故障。
Leslie Lamport
即使可以预见所有在控制范围内系统的状态,我们也总是会依赖于外部系统,它们的行为我们不可能全都知道。2012 年的平安夜,在 Netflix 全体员工的记忆上烙上了深深的印记。AWS 一个区域的 ELB(Elastic Load Balancing)服务故障,导致了 Netflix 全部服务严重中断。
如果系统部署在像 AWS 或 Azure 这样的云服务中,那么存在大量的你所依赖,而又不完全了解的外部服务是显而易见的。但即使你的系统全都运行在自己的数据中心,但在生产环境中,系统还是会依赖其他的外部服务,例如 DNS,SMTP,NTP,等等。就算这些服务你都自己部署,他们也经常会需要和外部的不受你控制的服务进行交互。
如果你的服务提供了一个 Web UI,那么你的用户所使用的浏览器就变成了你系统的一部分,但它不受你控制。即使你对客户端拥有全面的控制,例如 IoT 设备,你仍然逃不开用户所连接的网络环境和设备的影响。
第三方系统的行为在他自己的生产环境,与他和其他环境的集成的大环境中的行为总是有所不同。这进一步强调了你需要在生产环境运行实验的事实,生产环境才是你的系统和第三方系统进行真实交互的唯一场所。
面向云服务的混沌工程工具
只要系统部署在云上,那么混乱就是不可避免的。所幸我们的社区已经注意到并且开发了一些优秀的基于云的混沌工程工具,并且可以和不同的云提供商整合使用。除了混乱猴子之外,值得一提的工具有 Chaos Lambda,它可以让我们在生产时间随机关闭 AWS 的 Auto Scaling Group(ASG)实例。还有 Microsoft Azure 的 Fault Analysis Service,它是专为测试在 Microsoft Azure Service Fabric 所构建服务的工具。
5.4 生产环境变更
在 Netflix,我们的系统一直在不断更新。每天工程师和自动脚本都在通过不同的方式更新着系统,例如发布新代码,更改动态配置,添加持久化的数据,等等。
如果我们扩展我们系统的概念来包括这些生产环境中的变更,那么很明显在测试环境中想要模拟这些系统行为有多困难。
5.5 外部有效性
当心理学家或教育研究人员等社会科学家进行实验时,他们的主要关注点之一就是“外部有效性”:这个实验的结果能否概括我们真正感兴趣的现象?或者测量结果的产品运行环境是否是专门为了测试而准备的?
如果你不直接在生产环境中运行混沌工程实验,那么本章所讨论的问题(状态、输入、第三方系统,生产环境变更)就都是混沌工程实验的外部有效性的潜在威胁。
5.6 不愿意实践混沌工程的借口
我们认识到,在有些环境中,直接在生产环境中进行实验可能非常困难甚至不可能。我们并不期待工程师将干扰注入到行驶中的自动驾驶汽车的传感器里。但是,多数用户应该都不是在操作这类安全悠关的系统。
5.6.1 我很确定它会宕机!
如果你不愿在生产环境执行混沌工程实验的原因是,你对系统在你注入事件时会如何反应缺乏信心,那么这可能是你的系统还不够成熟来应对混沌工程实验的信号。你应该在对系统的弹性具备一定信心的时候再进行混沌工程实验。混沌工程的一个主要目的是识别系统中的薄弱环节。如果已经看到明显的薄弱环节,那你应该首先专注于提高系统在这一点上的弹性。当你确信系统有足够的弹性时,就可以开始进行混沌工程实验了。
5.6.2 如果真的宕机了,麻烦就大了!
即使你对系统弹性有很大的信心,你也可能会犹豫不决要不要执行混沌工程实验,因为担心如果实验揭示出系统薄弱环节的同时造成过多的破坏。
这是一个非常合理的顾虑,这也是我们正在致力解决的问题。我们采用的方法是通过下面两个途径来最小化潜在的影响范围:
支持快速终止实验;
最小化实验造成的“爆炸半径”。
当你执行任何混沌工程实验之前,应该先有一个用来立即终止实验的“大红色按钮”(我们真的有一个大红色按钮,虽然是虚拟的)。更好的方法是自动化这个功能,当它监测到对稳定状态有潜在危害的时候立即自动终止实验。
第二个策略涉及在设计实验时,要考虑到如何既能从实验中获得有意义的结论,同时兼顾最小化实验可能造成的潜在危害。这一点在后面的最小化“爆炸半径”一节中讨论。
5.7 离生产环境越近越好
即便你不能在生产环境中执行实验,你也要尽可能的在离生产环境最接近的环境中运行。越接近生产环境,对实验外部有效性的威胁就越少,对实验结果的信心就越足。
不能在生产环境中做实验?
在 2015 年纽约 Velocity Conference 上,来自 Fidelity Investment 的 Kyle Parrish 和 David Halsey 做了题为“太大而无法测试:如何破坏一个在线交易平台而不造成金融灾难”的演讲。他们在一个包括大型机的金融交易系统上运行了混沌工程实验。用他们的话来说,“我们意识到,用我们的灾备环境,配合生产环境的前端,就可以用现有的一些部件,再构造出一套生产环境。我们越深入,这个想法就越可行。我们发现大型机灾备系统非常理想,因为它与生产环境保持实时同步,包含所有生产环境的代码,数据,处理能力和存储能力,支持团队也完全了解它是如何运行的。我们也清楚的了解到,可以在这个系统上执行 2 倍或 3 倍于实际生产峰值时的流量。我们可以再造一套生产环境!”这是一个有创造力的绕过制度规范来模拟生产环境的实例。
记住:为了保障系统在未来不会遭受大规模中断事故,冒一点可控的风险是值得的。
6. 持续自动化运行实验
自动化是最长的杠杆。在混沌工程的实践中,我们自动执行实验,自动分析实验结果,并希望可以自动创建新的实验。
6.1 自动执行实验
手动执行一次性的实验是非常好的第一步。当我们想出寻找故障空间的新方法时,我们经常从手动的方法开始,小心谨慎地处理每一件事以期建立对实验和对系统的信心。所有当事人都聚集在一起,并向 CORE(Critical Operations Response Engineering,Netflix 的 SRE 团队的名称)发出一个警示信息,说明一个新的实验即将开始。
用这种较恐惧和极端级别的态度有利于:1)正确运行实验 2)确保实验有最小的“爆炸半径”。当我们成功执行了实验之后,下一步就是自动化这个实验以让其持续运行。
如果一个实验不是自动化的,那他就是作废了。
当今系统的复杂性意味着我们无法先验的知道,生产环境的哪些变动会改变混沌工程实验的结果。基于这个原因,我们必须假设所有变动都会改变实验结果。在共享状态、缓存、动态配置管理、持续交付、自动伸缩、时间敏感的代码等等的作用之下,生产环境实际上处在一个无时不在变化的状态。导致的结果就是,对实验结果的信心是随着时间而衰减的。
理想情况下,实验应该随着每次变化而执行,这有点儿像是混沌金丝雀。当发现新的风险时,操作人员如果相当确定根源是即将发布的新代码,那他就可以选择是否应该阻止发布新版本并优先修复缺陷。这种方法可以更深入了解生产中的可用性风险发生和持续的时间。在另一个极端,每年一次的演习中的问题调查难度更高,需要完全从零开始检查,而且很难确定这个潜在的问题存在于生产环境多久了。
如果实验不是自动化的,那么它就不会被执行。
在 Netflix,服务的可用性由该服务的开发和维护团队全权负责。我们的混沌工程团队通过培训,工具,鼓励和压力来帮助服务所有者提高服务的可用性。我们不能也不应该让工程师牺牲开发速度,专门花时间来手动定期执行混沌工程实验。相反的,我们自己投入精力来开发混沌工程的工具和平台,以期不断降低创建新实验的门槛,并能够完全自动运行这些实验。
混沌工程自动化平台 Chaos Automation Platform(ChAP)
我们的混沌工程团队在 2015 年的大部分时间里都在以咨询的形式,在关键微服务上运行混沌工程实验。这对于真正掌握 FIT 的能力和局限非常必要,但是我们也知道这样手把手的咨询方式没办法规模化。我们需要一个可以在整个组织规模化这个实践机制。
到 2016 年初,我们有了一个计划将混沌工程的原则引入到微服务层。我们注意到 FIT 有一些问题妨碍了自动化和广泛应用。其中一部分可以在 FIT 本身解决,另外一部分则需要较大的工程量,比请求头修改和 FIT 提供的代码故障模拟点注入要复杂得多。
2016 年底我们发布了混沌工程自动化平台,简称 ChAP,旨在解决这些不足之处。
FIT 中大多数问题都是由于缺乏自动化导致的。过多的对人工参与,例如设置故障场景,观测运行时关键指标的变化等,这些被证明是大规模应用的障碍。我们倾向于依靠现有的金丝雀分析(参见前面对金丝雀分析的介绍)来自动判断实验是否在可接受的范围内执行。
随后我们使用一个自动化模板开始进行真正的实验。在上面讨论过的 FIT 例子里,我们影响了 5%的流量,观察它对 SPS 是否有影响。如果我们没有观察到任何影响,我们会将受影响的流量提高到 25%。任何影响都有可能被其他和 SPS 相关的噪音所掩盖。像这样用大量流量做实验是有风险的,它只能给我们较低的信心我们可以隔离一些小的影响,它也可以防止多个故障同时出现。
为了最小化爆炸半径,ChAP 为每个被实验的微服务创建一个控制节点和一个实验集群。例如我们像上述的实例中说的,测试一个用户信息的微服务,ChAP 会询问我们的持续集成工具 Spinnaker 关于这个集群的信息。ChAP 用这个信息创建这个服务的两个节点,一个作为控制节点,另一个作为实验节点。然后它会分流一小部分流量,并平均分配在控制节点和实验节点上。只有实验节点里注入了故障场景。当流量在系统中流转时,我们可以实时比对控制节点和实验节点上的成功率和操作问题。
有了自动化的实验,我们就有了较高的信心,我们可以通过一对一比对控制节点和实验节点来监测到即使是很小的影响。我们只影响了流量中的很小部分,并且已经隔离了实验,因此我们就可以并行运行大量的实验了。
在 2016 年底,我们将 ChAP 与持续交付工具 Spinnaker 集成在一起,这样微服务就可以在每次新发布时运行混沌工程实验了。这个新功能有些类似金丝雀,但是在这个场景下我们需要它不间断运行,不会立即自动优雅降级,因为这里我们的目的是要尽可能发现所有未来潜在的系统问题。服务所有者基于在他们的微服务中发现的薄弱环节,我们给予了他们防止降级发生的机会。
6.2 自动创建实验
如果你已经可以配置定期自动运行实验,你就处在一个非常好的状态了。然而,我们认为还可以追求一个更好的自动化水平:自动设计实验。
设计混沌工程实验的挑战并非来自于定位导致生产环境崩溃的原因,这些信息在我们的故障跟踪中有。我们真正想要做的是找到那些本不应该让系统崩溃的事件的原因,包括那些还从未发生过的事件,然后持续不断的设计实验来验证,保证这些事件永远不会导致系统崩溃。
然而这是非常困难的。导致系统波动的原因空间是非常巨大的,我们不可能有足够的时间和资源穷举所有可能导致问题的事件及其组合。
Lineage-Driven Fault Injection (LDFI)
一个值得关注的关于自动创建实验的研究是一项叫做 Lineage-Driven Fault Injection (LDFI)的技术,由加州大学圣克鲁兹分校的 Peter Alvaro 教授开发。LDFI 可以识别出可能导致分布式系统故障的错误事件组合。LDFI 的工作原理是通过推断系统正常情况下的行为来判断需要注入的候选错误事件。
2015 年,Peter Alvaro 与 Netflix 的工程师合作来研究是否可以把 LDFI 应用在我们的系统上。他们成功地在 Netflix FIT 框架的基础上开发了一个版本的 LDFI,并且识别出了可能导致严重故障的一些错误事件组合。
有关如何在 Netflix 应用这项工作的更多信息,请参阅“第七届 ACM 云计算研讨会论文集”(SoCC '16)上发表的论文“Internet 规模的自动化故障测试研究”http://dx.doi.org/10.1145/2987550.2987555 。
7. 最小化“爆炸半径”
1986 年 4 月 26 日,人类历史上最严重的核事故之一发生在乌克兰的切尔诺贝利核电站。具有讽刺意味的是,灾难是由于一次弹性演习导致的:一次验证冷却剂泵冗余电源的演习。虽然我们大多数人并不从事像核电厂冷却系统这样高危的项目工作,但每一次混沌工程实验的确具备导致生产环境崩溃的风险。混沌工程师的一项专业职责就是要理解和降低生产风险,可以为实验而具备良好设计的系统可以阻止大规模的生产事故,仅仅影响到少量的用户。
不幸的是,我们经常运行本来只会影响一小部分用户的测试,却由于级联故障无意中影响到了更多的用户。在这些情况下,我们不得不立即中断实验。虽然我们绝不想发生这种情况,但随时遏制和停止实验的能力是必备的,可以避免造成更大的危机。我们的实验通过很多方法来探寻故障会造成的未知的和不可预见的影响,所以关键在于如何让这些薄弱环节曝光出来而不会意外造成更大规模的故障。我们称之为最小化“爆炸半径”。
能带来最大信心的实验也是风险最大的,是对所有生产流量都影响的实验。而混沌工程实验应该只承受谨慎的,可以衡量的风险,并采用渐进的方式,每一步都基于前一步的基础之上。这种递进的方式不断增加对系统的信心,而不会对用户造成过多不必要的影响。
最小风险的实验只作用于很少的用户之上。为此我们验证客户端功能时只向一小部分终端注入故障。这些实验仅限于影响一小部分用户或一小部分流程。他们不能代表全部生产流量,但他们是很好的早期指标。例如,如果一个网站无法通过早期实验,就没必要影响大量其余的真实用户。
当自动化实验成功之后(或者小量设备验证没有涵盖要测试的功能时),下一步就是运行小规模的扩散实验。这种实验会影响一小部分百分比的用户,因为我们允许这些流量都遵循正常的路由规则,所以最终它们会在生产服务器上均匀分布。对于此类实验,你需要用你定义好的成功指标来过滤所有被影响的用户,以防实验的影响被生产环境的噪音掩盖。小规模扩散实验的优势在于它不会触动到生产环境的例如断路器阈值,所以你可以验证每一个单一请求的超时和预案。这可以验证系统对瞬时异常的弹性。
下一步是进行小规模的集中实验,通过修改路由策略让所有实验覆盖的用户流量导向到特定的节点。这些节点会经历高度集中的故障、延迟等测试。这里我们会允许断路器打开,同时会暴露隐藏的资源限制。如果我们发现有无效的预案或者奇怪的锁竞争等情况导致服务中断,那么只有实验覆盖的用户会受到影响。这个实验模拟大规模生产环境故障,但同时可以把负面影响控制在最小,然而结果却能带来高度的信心。
风险最大但最准确的实验是大规模无自定义路由的实验。在这个实验级别,实验结果应该在你的主控控制台显示,同时因为断路器和共享资源的限制,实验可能会影响到不在实验覆盖范围内的用户。当然,没有什么比让所有生产用户都参与实验,能给你更多关于系统可以抵御特定故障场景的确定性了。
除了不断扩大实验范围,在实验造成过多危害时及时终止实验也是必不可少的。有些系统设计会使用降级模式来给用户带来稍小的影响,这还好,但是当系统完全中断服务的时候,就应该立即终止实验。这可以由之前讨论过的“大红色按钮”来处理。
我们强烈建议实施自动终止实验,尤其是在定期自动执行实验的情况下。关于弄清楚如何构建一个可以实时监控到我们感兴趣的指标的系统,并可以随时实施混沌工程实验,这完全依赖于你手上的独特的系统构造,我们留给读者当做一个课后练习。
为了让尽可能高效地应对实验发生不可预期的情况,我们要避免在高风险的时间段运行实验。例如我们只在所有人都在办公室工作的时间段运行实验。
如果实验的工具和仪器本身就会对系统和指标产生影响,那么整个混沌工程的目的就被破坏了。我们要的是建立对系统弹性的信心,记住每次只检验一个可控的故障。
译者简介
侯杰,TGO鲲鹏会会员,美利金融技术副总裁,整体负责美利金融技术研发工作。曾在爱点击,IBM 中国,IBM 澳大利亚担任研发管理,咨询管理等职位,带领团队负责过多个大规模金融行业信息化项目,和互联网转型实践。毕业于南京大学。
下期预告
接下来我们会讨论实践中的混沌工程以及混沌工程的成熟度模型。
评论 1 条评论