
站点可靠性工程师SRE的一项核心职责是量化所维护系统的可靠性。可靠性测试是 SRE 提升系统稳定性、降低故障风险、优化 MTTR 与 MTBF 的重要手段。SRE 通常会把传统软件测试技术应用到大规模系统中以完成这项工作。可靠性既可以从历史角度衡量也可以从未来角度衡量前者通过分析监控系统记录的历史行为数据获得后者则基于既有系统行为数据进行预测。要使这类预测足够可靠并能够用于实际决策通常必须满足以下条件之一站点在一段时间内保持完全不变没有软件发布服务器集群也没有调整。这样未来行为大体会与过去行为一致。能够有把握地描述站点发生的所有变更并在分析中纳入每项变更所带来的不确定性。测试是一种机制用于在系统发生变更时证明某些特定领域仍然保持等价。87 如果一项测试在变更前后都能通过它就可以降低可靠性分析中需要考虑的不确定性。全面而有效的测试能够帮助我们以足够精细的粒度预测某个站点未来的可靠性从而满足实际应用需求。所需测试量取决于系统的可靠性要求。随着测试覆盖代码库的比例提高不确定性以及每次变更可能导致的可靠性下降都会降低。足够高的测试覆盖率意味着在可靠性跌破可接受水平之前系统可以承受更多变更。如果变更过多、过快预测可靠性就会逼近可接受下限。此时可能需要暂停变更同时积累新的监控数据。这些数据可以补充测试覆盖率用于验证修改后的执行路径是否仍然满足可靠性要求。假设服务请求来自随机分布的客户端 [Woo96]就可以通过监控指标中的抽样统计信息推断总体流量是否已经使用了新的执行路径。这些统计信息还可以帮助识别需要加强测试或进行其他改造的区域。可靠性测试与平均修复时间之间的关系通过一项或一组测试并不一定能够证明系统可靠但测试失败通常表明系统存在可靠性问题。监控系统可以发现缺陷但发现速度取决于报告流程的响应速度。平均修复时间MTTR衡量的是运维团队修复缺陷所需的时间修复方式可以是回滚也可以是其他补救措施。测试系统有可能识别出 MTTR 为零的缺陷。当系统级测试被应用于某个子系统并且该测试检测到的问题与监控系统在生产环境中会检测到的问题完全相同时就会出现 MTTR 为零的情况。这样的测试可以阻止代码推送从而防止缺陷进入生产环境尽管缺陷本身仍然需要在源代码中修复。通过阻止代码推送来处理这类缺陷既快速又有效。发现的 MTTR 为零的缺陷越多用户实际感受到的平均故障间隔时间MTBF就越长。随着测试不断改进MTBF 会随之延长这也会鼓励开发者加快功能发布速度。当然部分新功能难免会引入缺陷。一旦发现并修复这些缺陷发布节奏也会随之调整。关于软件测试相关领域的作者在测试覆盖范围方面基本已有共识。多数分歧来自术语使用差异、对软件生命周期不同阶段测试影响的关注重点不同以及被测试系统本身的具体特点不同。关于大型互联网场景下的测试实践可参见 [Whi12]。以下各节将说明本章对软件测试相关术语的使用方式。软件测试类型传统测试与生产环境测试软件测试大致可分为两类传统测试和生产环境测试。传统测试在软件开发过程中更常见用于离线评估软件的正确性。生产环境测试则在运行中的网络服务上执行用于评估已部署的软件系统是否正常工作。传统测试单元测试、集成测试与系统测试如图 17-1 所示传统软件测试通常从单元测试开始。更复杂功能的测试则建立在单元测试的基础之上。图 17-1.传统测试的层级结构单元测试单元测试是最小、最简单的软件测试形式。它用于评估软件中某个可独立分离的单元例如类或函数是否正确运行并使该单元独立于其所属的更大软件系统。单元测试也可以作为一种规范用来确保函数或模块准确执行系统所要求的行为。单元测试通常也是引入测试驱动开发TDD理念的起点。集成测试通过单元测试的软件组件会被组装成更大的组件。随后工程师会对这些组装后的组件运行集成测试以验证其功能是否正常。依赖注入是一种非常强大的技术可以为复杂依赖项创建模拟对象从而让工程师能够清晰地测试组件。依赖注入的一个常见示例是用具有确定行为的轻量级模拟对象替换有状态数据库。系统测试系统测试是工程师针对尚未部署的系统执行的最大规模测试。属于某个特定组件例如已经通过集成测试的服务器的所有模块都会被组装成完整系统。随后工程师会测试系统的端到端功能。系统测试有多种类型。冒烟测试冒烟测试是最简单的系统测试类型之一。工程师通过这类测试验证系统的基本功能是否可用其目的在于确认系统能够正常启动并执行基本操作。冒烟测试也称为健全性测试其作用是避免在明显不可用的系统上继续执行成本更高的测试。性能测试在通过冒烟测试确认系统基本正确之后通常下一步会编写另一类系统测试以确保系统在其整个生命周期中都能保持可接受的性能。由于依赖项响应时间或资源需求在开发过程中可能发生显著变化因此需要对系统进行测试确保其性能不会在用户察觉之前逐渐下降。例如一个程序可能从原本只需要 8 GB 内存变为需要 32 GB 内存又或者 10 毫秒的响应时间可能逐渐变成 50 毫秒再变成 100 毫秒。性能测试可以防止系统随着时间推移而性能下降或运行成本变得过高。回归测试另一类系统测试用于防止缺陷重新潜入代码库。回归测试可以看作一个缺陷库其中记录了历史上导致系统故障或产生错误结果的各种缺陷。通过将这些缺陷转化为系统级或集成级测试负责重构代码库的工程师可以确保不会意外重新引入那些已经投入时间和精力修复过的问题。需要注意的是测试是有成本的包括时间成本和计算资源成本。单元测试通常非常经济往往只需几毫秒即可在笔记本电脑的可用资源上完成。相比之下搭建一个包含所需依赖项或模拟等价物的完整服务器来运行相关测试可能需要更长时间从几分钟到几小时不等并且可能需要专用计算资源。充分考虑这些成本对于提高开发人员效率、有效利用测试资源至关重要。生产环境测试面向真实系统的可靠性验证生产环境测试与真实生产系统交互而不是与封闭测试环境中的系统交互。这类测试在许多方面类似于黑盒监控相关内容可参见“分布式系统监控”因此有时也称为黑盒测试。生产环境测试对于运行可靠的生产服务至关重要。解开测试纠缠人们常说测试是在或者应该在一个封闭环境中进行的 [Nar12]。这种说法暗示生产环境并不是封闭的。事实也的确如此生产环境通常并非封闭环境因为发布流程会以小而可控的方式实时改变生产环境。为了控制不确定性并尽量避免风险暴露给用户变更并不一定会按照进入源代码控制系统的顺序推送到生产环境。部署通常分阶段进行并使用某种机制逐步调整用户环境同时在每个阶段进行监控以确保新环境不会遇到意料之外的问题。因此整个生产环境并不会有意地代表源代码控制系统中某个已提交二进制文件的特定版本。源代码控制系统中可能同时存在多个二进制文件及其关联配置文件版本等待部署到生产环境。这种情况会给针对生产环境的测试带来问题。例如测试可能使用源代码控制系统中最新版本的配置文件却搭配生产环境中已经部署的旧版本二进制文件。或者测试可能使用旧版本配置文件并发现一个实际上已经在新版本中修复的错误。类似地系统测试可以在运行测试之前使用配置文件组装其模块。如果测试通过但其版本与配置测试即下一节将讨论的内容失败时使用的版本相同那么该测试结果在理论上有效在实际应用中却无效。这种情况非常棘手。配置测试在海外某些大型互联网公司的实践中Web 服务的配置信息通常会记录在版本控制系统中的文件里。针对每个配置文件都会有一个独立的配置测试用于检查生产环境中特定二进制文件的实际配置情况并报告与该配置文件不一致之处。这类测试本质上并非完全封闭因为它们运行在测试基础设施沙箱之外。配置测试基于某个已提交配置文件的特定版本来构建和执行。通过比较测试通过的版本与自动化系统的目标版本可以间接反映实际生产环境与正在进行的工程工作之间存在多大差距。这类非封闭式配置测试在分布式监控解决方案中尤为重要因为生产环境中的通过或失败模式能够识别服务栈中局部配置组合不合理的路径。监控系统的规则会尝试将真实用户请求路径即来自跟踪日志的路径与这些不理想路径进行匹配。一旦规则发现匹配项就会触发警报表明正在进行的发布或推送操作存在安全隐患需要采取补救措施。当生产环境部署使用真实文件内容并提供实时查询接口来检索内容副本时配置测试可以非常简单。在这种情况下测试代码只需发出查询并将响应与文件内容进行比较即可。若配置存在以下情况之一测试就会变得更加复杂隐式包含二进制文件内置的默认值这意味着测试版本需要单独划分。通过 bash 等预处理器转换为命令行标志使测试结果受扩展规则约束。为共享运行时指定行为上下文使测试依赖于该运行时的发布计划。压力测试为了安全地运行系统SRE 需要了解系统及其组件的极限。在许多情况下单个组件并不会在达到某个临界点后优雅降级而是会发生灾难性故障。工程师使用压力测试来探查 Web 服务的边界。压力测试可以回答以下问题数据库最多能占用多少内存超过后就会开始出现写入失败应用服务器每秒最多可以发送多少查询超过后服务器就会过载并导致请求失败金丝雀测试在这份生产环境测试清单中金丝雀测试似乎明显缺席。“金丝雀”一词来自“煤矿里的金丝雀”这一说法指过去矿工用活鸟检测有毒气体以避免人类中毒的做法。在金丝雀测试中一部分服务器会被升级到新版本或新配置然后进入一段观察期。如果没有出现意外情况就继续发布并逐步升级其余服务器。如果出现问题已修改的服务器可以迅速回滚到已知良好的状态。我们通常将升级服务器后的观察期称为“二进制烘焙”。严格来说金丝雀测试并不是真正意义上的测试而是一种结构化的用户验收过程。配置测试和压力测试旨在验证确定性软件在特定条件下是否满足要求而金丝雀测试更具临场性。它只是将待测代码暴露在更难预测的真实生产环境中因此并不完美也并非总能发现新引入的缺陷。为了更具体地说明金丝雀测试如何发挥作用假设存在一个潜在故障它对用户流量的影响相对较小而部署过程正以指数级增长的方式逐步扩大。我们预期累计报告的异常数量 CU 会不断增长其中 R 表示异常报告速率U 表示故障阶数K 表示流量增长 e 倍即约 172%所需的时间。90为了避免对用户造成影响一旦部署过程中出现不良偏差就需要快速回滚到之前的配置。自动化系统需要短时间来监测偏差并作出响应在此期间可能会额外生成若干报告。等系统准备就绪后这些报告可用于估算累计偏差数量 C 和偏差率 R。除以 K 并进行校正后即可估计 U也就是潜在故障的阶数。91 例如U 1用户请求遇到了无法正常运行的代码。U 2该用户请求会随机破坏未来用户请求可能看到的数据。U 3被随机破坏的数据同时也是先前某个请求的有效标识符。大多数缺陷都是一阶缺陷它们与用户流量呈线性关系增长 [Per07]。通常可以将所有响应异常的请求日志转化为新的回归测试从而追踪这些缺陷。但这种策略对高阶缺陷无效如果按顺序重放之前的所有请求某个请求会反复失败但如果省略某些请求该请求又会突然通过。在发布过程中捕获这些高阶缺陷至关重要否则运维工作量会迅速增加。考虑到高阶错误和低阶错误的动态差异在使用指数级发布策略时并不需要刻意追求用户流量分配的绝对公平。只要每一种确定用户流量分配比例的方法都使用相同的 K 区间即使无法确定究竟是哪种方法最终发现了故障U 的估计值仍然有效。通过顺序使用多种方法并允许一定程度的重叠可以保持较小的 K 值。这种策略既能最大限度减少用户可见的异常总数 C也能尽早估计 U 值。当然我们希望 U 值为 1。创建测试和构建环境在项目启动之初就考虑各种测试类型和故障场景当然是理想状态但现实中SRE 加入开发团队时项目往往已经进入相当成熟的阶段团队的项目可能已经验证了研究模型库已经证明底层算法具备可扩展性用户界面模型也基本符合要求。此时代码库可能仍带有原型性质而全面测试尚未设计或部署。在这种情况下测试工作应该从哪里开始如果当前测试覆盖率很低甚至完全没有测试那么为每个关键函数和类编写单元测试会是一项极其艰巨的任务。因此应该优先编写那些投入最小、收益最大的测试。可以先提出以下问题能否对代码库进行优先级排序借鉴功能开发和项目管理中的经验如果每个任务都是高优先级任务那么实际上就没有真正的高优先级任务。能否基于重要性指标对被测系统的组件进行排序是否存在对任务或业务至关重要的特定功能或类例如涉及计费的代码通常属于业务关键代码。计费代码通常也比较容易与系统其他部分完全分离。其他团队正在集成哪些 API即使某种故障从未通过发布测试直接暴露给用户只要它误导了其他开发团队使他们为你的 API 编写出错误甚至只是次优的客户端也可能造成严重危害。发布明显存在缺陷的软件是开发人员最不可原谅的错误之一。事实上只需投入少量精力就可以为每次发布编写一组冒烟测试。这是一个低投入、高回报的起点能够确保软件经过基本测试从而提高可靠性。建立强大测试文化的一种方式是将所有已报告缺陷都记录为测试用例。如果每个缺陷都转化为一个测试用例那么每个测试用例最初都应该失败因为缺陷尚未修复。随着工程师修复缺陷软件通过这些测试团队就朝着构建全面回归测试套件的目标迈进了一步。创建经过充分测试的软件另一项关键任务是搭建测试基础设施。强大测试基础设施的基础是版本化的源代码控制系统它能够跟踪代码库的每一次变更。对于希望把需求、开发、测试、发布和知识沉淀串联起来的研发团队也可以借助 PingCode这类智能化研发管理工具将测试结果、缺陷修复、发布记录和 Wiki 经验沉淀统一管理让可靠性改进从单点动作变成可追踪、可复盘的研发流程。一旦源代码控制系统到位就可以添加持续构建系统在每次代码提交时构建软件并运行测试。我们的经验是如果构建系统能在软件项目出现问题时立即通知工程师效果最好。虽然这听起来显而易见但确保源代码控制系统中软件项目的最新版本始终处于可用状态至关重要。当构建系统通知工程师代码存在问题时工程师应暂停其他任务优先修复问题。认真对待缺陷是合理的原因如下如果缺陷出现后代码库继续发生变化修复损坏部分通常会更加困难。软件故障会拖慢团队进度因为团队成员必须想办法绕过或处理故障。每日构建、每周构建等发布节奏会失去价值。团队对紧急发布请求例如安全漏洞披露的响应会变得更加复杂和困难。在 SRE 领域稳定性和敏捷性这两个概念历来存在张力。最后一点提供了一个有趣例子说明稳定性实际上如何促进敏捷性当构建过程稳定可靠时开发人员就能更快地迭代。一些构建系统具备非常有价值的能力可以更精确地控制测试。例如某些构建系统会为软件项目创建依赖关系图。当某个文件发生变化时它只会重新构建依赖于该文件的软件部分。这样的系统能够提供可复现的构建结果。测试不再需要在每次提交时运行全部测试而是只针对受变更影响的代码运行。因此测试执行成本更低速度也更快。有很多工具可以帮助量化和可视化所需的测试覆盖率 [Cra10]。应利用这些工具明确测试重点把编写高测试覆盖率代码当作一个工程项目而不是一种抽象的理念。与其反复强调“我们需要更多测试”不如设定明确目标和截止日期。还要记住并非所有软件都一样。关乎生命安全或收入的关键系统需要比生命周期短暂的非生产脚本具备更高的测试质量和测试覆盖率。大规模可靠性测试了解测试的基本概念之后接下来看看 SRE 如何从系统视角开展测试从而在大规模场景下提高可靠性。一个小型单元测试可能只有少量依赖项一个源文件、测试库、运行时库、编译器以及运行测试的本地硬件。健全的测试环境要求每个依赖项都有自己的测试覆盖范围并且这些测试必须专门覆盖环境中其他部分所预期的使用方式。如果某个单元测试的实现依赖运行时库中一条没有测试覆盖的代码路径那么环境中的一个无关变更就可能导致该单元测试始终通过无论被测代码本身是否存在缺陷。相比之下发布测试可能依赖大量组件以至于它对代码库中的每个对象都存在传递依赖。如果测试依赖生产环境的干净副本那么原则上每个小补丁都需要执行一次完整的灾难恢复演练。实际测试环境会尝试在版本和合并之间选择分支点。这样可以用最少的迭代次数解决最大的依赖不确定性。当然当某个不确定区域最终演变为故障时就需要选择额外的分支点。测试可扩展工具作为软件SRE 工具同样需要测试。95 SRE 开发的工具可能执行以下任务检索和传播数据库性能指标。预测使用指标以规划容量风险。重构用户不可访问的服务副本中的数据。更改服务器上的文件。SRE 工具有两个共同特点它们的副作用仍然存在于经过测试的主流 API 之中。它们通过既有验证和发布屏障与面向用户的生产环境隔离。面向高风险软件的屏障防御绕过常规且经过充分测试 API 的软件即使这样做有正当理由也可能对运行中的服务造成严重破坏。例如数据库引擎的实现可能允许管理员暂时关闭事务以缩短维护窗口。如果批量更新软件使用了这种能力而该工具意外地针对面向用户的副本启动就可能破坏面向用户服务的隔离性。可以通过精心设计来避免这类风险使用单独的工具在复制配置中设置屏障使副本无法通过健康检查。这样该副本就不会被提供给用户。配置存在风险的软件使其在启动时检查是否存在安全屏障。只允许此类高风险软件访问不健康的副本。使用与黑盒监控相同的副本健康验证工具来移除屏障。自动化工具也是软件。由于它们的风险影响看似与服务的其他层级无关因此它们的测试需求也更加精细。自动化工具可能执行以下任务数据库索引选择。数据中心之间的负载均衡。重新排列中继日志以实现快速重放。自动化工具有两个共同特点实际执行的操作会作用于一个强大、可预测且经过充分测试的 API。该操作的目的是产生一种副作用而这种副作用对另一个 API 客户端而言表现为不可见的中断。测试可以验证另一个服务层在变更前后是否仍然表现出预期行为。通常可以测试通过 API 观察到的内部状态在整个操作过程中是否保持一致。例如即使没有可用的合适索引数据库也应努力返回正确结果。另一方面某些已记录的 API 不变量例如 DNS 缓存在 TTL 到期前应继续保留可能在整个操作过程中不再成立。例如如果运行级别变更将本地名称服务器替换为缓存代理这两种方案都可以保证已完成的查询在数秒内仍然可用但缓存状态不太可能在两者之间传递。既然自动化工具意味着要为其他二进制文件增加发布测试以应对环境中的瞬时变化那么应如何定义这些自动化工具本身的运行环境毕竟一个用于优化容器利用率的自动化工具如果也运行在容器中就很可能也会尝试迁移自己。如果其内部算法的新版本迅速产生脏内存页导致相关镜像的网络带宽不足最终无法完成实时迁移那将十分尴尬。即使存在一个要求二进制文件有意迁移自身的集成测试该测试也未必会使用生产规模的容器集群模型。几乎可以肯定的是它也不会允许使用稀缺且高延迟的洲际带宽来测试这类资源竞争。更有趣的是一个自动化工具可能会改变另一个自动化工具的运行环境。或者两个工具可能同时改变彼此的运行环境。例如集群升级工具在推送升级时可能消耗最多资源。因此容器重平衡工具可能倾向于迁移该工具。反过来容器重平衡工具自身也偶尔需要升级。如果相关 API 具备重启语义并且有人记得为这些语义实现测试覆盖同时检查点健康状况也能得到独立保证那么这种循环依赖是可以接受的。灾难恢复测试许多灾难恢复工具都可以被精心设计为离线运行。这类工具可以执行以下操作计算一个检查点状态使其等价于服务被干净停止后的状态。将检查点状态推送到现有非灾难验证工具可以加载的位置。支持常用的发布屏障工具由这些工具触发干净启动流程。在许多情况下可以实现这些阶段使测试易于理解且具备较高覆盖率。但如果必须打破任何约束例如离线、检查点、可加载、屏障或干净启动就很难保证相关工具能够在短时间内正确运行。在线修复工具本质上独立于主流 API 运行因此测试起来更具挑战性。在分布式系统中一个难点是如何判断正常行为即可能本质上具有最终一致性的行为是否会与修复过程发生不良交互。例如考虑一个可以用离线工具分析的竞态条件。离线工具通常期望系统具备即时一致性而不是最终一致性因为即时一致性更容易测试。情况会进一步复杂化因为修复二进制文件通常与它所竞争的生产环境二进制文件是分开构建的。因此可能需要构建一个统一的、经过插桩的二进制文件来运行这些测试以便工具能够观察到事务过程。使用统计测试统计技术例如模糊测试技术以及分布式状态测试技术并不一定具有可重复性。仅仅在代码变更后重新运行这些测试不能最终证明观察到的故障已经被修复。98 然而这些技术仍然很有用它们可以记录某次运行中所有随机选择的操作。有时只需记录随机数生成器的种子即可。如果这份日志会立即被重构为发布测试那么在编写错误报告之前先多运行几次通常很有帮助。重放测试不失败的次数可以帮助判断之后要证明故障已修复会有多困难。故障表现形式的差异可以帮助更准确地定位代码中的可疑区域。后续运行中可能出现比初始运行更严重的故障情形。因此可能需要提高该缺陷的严重性和影响等级。对测试速度的要求对于代码库中的每个版本或补丁每个已定义测试都会给出通过或失败的结果。即使运行多次且看似完全相同该结果也可能发生变化。可以通过对多次运行结果取平均值并计算该概率的统计不确定性来估计测试实际通过或失败的概率。然而对每个版本点上的每个测试都执行这样的计算在计算上并不可行。因此必须针对众多感兴趣的场景提出假设并对每个测试和版本执行适当次数的重复运行从而得出合理推论。其中一些场景从代码质量角度看是良性的另一些则需要采取措施。这些场景会以不同程度影响所有测试尝试并且由于它们彼此相关要可靠且快速地得到可操作的假设列表也就是判断哪些组件确实存在问题就必须同时评估所有场景。使用测试基础设施的工程师希望知道他们自己的代码通常只占某次测试运行所依赖全部源代码的一小部分是否存在问题。通常如果他们的代码没有问题就意味着任何观察到的故障都可以归因于其他人的代码。换句话说工程师想知道他们的代码是否存在意料之外的竞争条件导致测试不稳定或者比其他因素导致的测试本身的不稳定性更加严重。测试截止时间大多数测试都很简单它们作为独立、封闭的二进制文件运行只占用少量计算资源运行时间仅几秒。这些测试能够为工程师提供交互式反馈帮助他们在将注意力转移到下一个缺陷或任务之前发现错误。需要跨多个二进制文件编排或者需要大量容器组成集群的测试其启动时间通常以秒为单位。这类测试通常无法提供交互式反馈因此可以归类为批处理测试。它们失败时并不是告诉工程师“不要关闭编辑器标签页”而是在告诉代码审查者“这段代码还没有准备好接受审查。”非正式的测试截止时间就是工程师下一次上下文切换的时间点。测试结果最好在工程师切换上下文之前交到他们手上否则下一个上下文可能已经切换到别的事情上。假设一位工程师正在开发一个包含超过 21,000 个简单测试的服务并且偶尔会针对该服务代码库提交补丁。为了测试这个补丁工程师需要比较补丁前后代码库的通过/失败结果向量。如果两个向量的比较结果良好就可以初步判断该代码库是否满足发布条件。这个判断会促使工程师运行大量发布测试、集成测试以及其他分布式二进制测试用以检验系统的扩展性和复杂性如果补丁显著增加了本地计算资源使用量就需要关注扩展性如果补丁在其他地方造成超线性工作负载就需要关注复杂性。误判环境不稳定性的概率有多大如果每 10 个补丁中就有 1 个被错误拒绝用户很可能会强烈抗议。但如果每 100 个完美补丁中只有 1 个被错误拒绝可能就不会引发太大争议。这意味着真正关心的是 0.99也就是补丁被接受比例的第 42,000 个根。这里的 42,000 来自补丁前后每个已定义测试各有一个结果。计算如下[\sqrt[42000]{0.99}]这意味着这些单独测试必须在超过 99.9999% 的情况下正确运行。嗯。推向生产环境尽管生产环境的配置管理通常保存在源代码控制仓库中但配置往往与开发人员的源代码分离。同样软件测试基础设施通常也无法访问生产环境配置。即使两者位于同一个仓库中配置管理变更也往往在不同分支或独立目录树中进行而测试自动化历来会忽略这些分支或目录树。在传统企业环境中软件工程师开发二进制文件后会将其交给管理员进行服务器更新。在这种情况下测试基础设施与生产配置的分离轻则令人烦恼重则损害可靠性和敏捷性。这种分离还可能导致工具重复。在名义上一体化的运维环境中这种分离会降低系统弹性因为它会在两套工具的行为之间造成细微不一致。此外由于版本控制系统之间存在提交竞争这种分离还会限制项目进度。在 SRE 模型中将测试基础设施与生产配置分离的影响更加严重因为它阻碍了将描述生产环境的模型与描述应用程序行为的模型关联起来。这种差异会影响希望在开发阶段发现预期结果统计差异的工程师。不过这种分离与其说是减缓开发速度不如说是阻碍系统架构变更因为迁移风险无法被充分消除。考虑一个统一版本控制和统一测试的场景以便 SRE 方法论能够充分发挥作用。分布式架构迁移失败会造成什么影响届时可能会运行相当多的测试。暂且假设软件工程师可以接受测试系统大约每 10 次测试中出现 1 次错误结果。如果已知测试可能会出现假阴性而且情况很快可能变得棘手那么团队愿意承担多大风险来推进迁移显然某些测试覆盖范围需要比其他范围更高的谨慎程度。这种区别可以概括为某些测试失败比其他测试失败预示着更大的风险。预期的测试失败不久以前一款软件产品可能一年才发布一次。它的二进制文件由编译器工具链耗时数小时甚至数天生成大部分测试也由人工根据手写指令完成。这种发布流程效率低下但在当时几乎没有自动化的必要。发布工作的主要成本集中在文档编写、数据迁移、用户培训以及其他事务上。无论进行了多少测试这些版本的平均故障间隔时间也只有一年。每次发布都会包含大量变更因此软件中必然隐藏着一些用户尚未察觉的缺陷。实际上前一个版本的可靠性数据对下一个版本几乎没有参考价值。高效的 API/ABI 管理工具以及能够扩展到大规模代码库的解释型语言如今已经支持每隔几分钟构建并执行一个新的软件版本。原则上一支足够庞大的测试团队例如 100 人可以使用前文描述的方法测试每个新版本并确保每个增量版本都达到相同质量标准。即使最终对同一份代码应用同一组测试每年发布的最终软件版本质量也会更高。这是因为除了年度版本之外代码的中间版本也经过了测试。通过测试中间版本可以明确地将测试中发现的问题追溯到根本原因并确保整个问题而不仅仅是暴露出来的有限症状得到修复。这种缩短反馈周期的原则同样适用于自动化测试覆盖率。如果允许用户在一年内试用更多软件版本MTBF 会受到影响因为用户可感知故障发生的机会会增加。然而也可以借此发现哪些方面需要增加测试覆盖率。如果实施这些测试每一项改进都能预防未来可能出现的故障。谨慎的可靠性管理会将测试覆盖率带来的不确定性限制与用户可感知故障限制结合起来从而调整发布节奏。这种组合能够最大限度利用来自运营和最终用户的知识。这些知识的积累会提高测试覆盖率进而加快产品发布速度。如果 SRE 修改的是配置文件或调整自动化工具的策略而不是实现用户功能其工程工作也符合相同的概念模型。在基于可靠性定义发布节奏时通常按功能或者更方便地按团队划分可靠性预算是合理的。在这种情况下功能工程团队的目标是满足一定的不确定性限值这会影响其目标发布节奏。SRE 团队拥有独立预算和自身的不确定性限制因此它们的发布频率也存在上限。为了保持可靠性并避免支持服务所需的 SRE 人数随规模线性增长生产环境必须在大多数时间无人值守地运行。要实现无人值守环境必须能够抵御轻微故障。当发生需要 SRE 人工干预的重大事件时SRE 使用的工具必须经过充分测试。否则这种干预会降低历史数据对近期预测的适用性。这种适用性的降低又需要等待监控数据分析结果来消除由此产生的不确定性。前文“测试可扩展工具”侧重讨论如何满足 SRE 工具的测试覆盖率要求本节则说明测试如何决定该工具在生产环境中的使用频率。配置文件之所以存在通常是因为修改配置比重新构建工具更快。这种低延迟通常有助于降低 MTTR。然而这些文件也经常因为一些并不需要低延迟的原因而被修改。从可靠性角度看如果某个配置文件旨在降低 MTTR且只在发生故障时才会修改那么它的发布频率低于 MTBF。因此在不影响站点整体可靠性的前提下手动修改是否真正达到最优状态存在相当大的不确定性。如果某个配置文件在每次面向用户的应用程序版本发布期间多次更改例如因为它保存了版本状态而这些更改没有得到与应用程序版本发布同等程度的对待就可能构成重大风险。如果该配置文件的测试和监控覆盖率没有显著优于用户应用程序那么该文件会对站点可靠性产生负面影响。处理配置文件的一种方法是确保每个配置文件都只属于上述两类之一并以某种方式强制执行这一规则。如果采用后一种策略应确保以下几点每个配置文件都具备足够测试覆盖率以支持常规编辑。在正式发布之前文件编辑会因等待发布测试而产生延迟。提供一种紧急机制允许在测试完成前将文件发布到生产环境。由于紧急发布会降低可靠性通常最好让用户能够注意到这一点例如提交一个缺陷报告要求下次采用更可靠的解决方案。应急机制与测试可以实现一种紧急机制用于禁用发布测试。但这意味着在监控报告真实用户影响之前任何匆忙进行手动编辑的人都不会收到错误提示。更好的做法是保持测试运行将提前推送事件与待处理测试事件关联起来并尽快用任何失败的测试对推送进行反向标注。这样一次有缺陷的手动推送可以很快被另一次希望缺陷更少的手动推送取代。理想情况下这种紧急机制会自动提高这些发布测试的优先级使其优先于测试基础设施正在处理的常规增量验证和覆盖率工作负载。整合除了对配置文件进行单元测试以降低其可靠性风险之外对配置文件进行集成测试也至关重要。出于测试目的配置文件的内容对读取配置的解释器而言可能构成潜在威胁。Python 等解释型语言常用于配置文件因为它们的解释器可以嵌入配置加载流程并且一些简单的沙箱机制可以防止非恶意编码错误造成严重后果。使用解释型语言编写配置文件风险较高因为这种做法容易带来难以彻底消除的潜在故障。由于加载内容实际上就是执行程序因此加载操作的低效程度没有理论上限。除其他测试外还应该对所有集成测试方法设置严格的截止时间检查将未能在合理时间内完成的测试标记为失败。如果配置信息以自定义语法的文本形式编写那么每一类测试都需要从头单独编写。使用现有语法例如 YAML并结合经过充分测试的解析器例如 Python 的 safe_load可以减轻配置文件带来的一部分工作量。精心选择语法和解析器可以确保加载操作耗时有严格上限。然而实现者仍需要处理模式错误而大多数简单处理策略都无法限制运行时间。更糟的是这些策略往往缺乏完善的单元测试。使用预定义模式的结构化序列化格式其好处是模式预先定义并且会在加载时自动检查从而进一步减少工作量同时仍然提供有界运行时间。SRE 的职责通常包括编写系统工程工具如果其他人尚未编写这些工具的话并通过测试覆盖率增强验证能力。所有工具都可能因为测试未发现的缺陷而出现异常行为因此建议采用纵深防御策略。当某个工具出现异常行为时工程师需要尽可能确保其他大多数工具仍能正常运行从而缓解或解决该异常行为带来的负面影响。确保站点可靠性的关键在于发现每一种预期异常行为并确保某些测试或其他工具的测试输入验证器能够报告这些异常行为。发现问题的工具可能无法修复甚至阻止问题但至少应该在发生灾难性故障之前报告问题。例如考虑一个配置好的用户列表例如非联网类 Unix 机器上的/etc/passwd文件。假设一次意外编辑导致解析器在只解析了文件一半后就停止。由于新创建的用户尚未加载机器很可能继续正常运行许多用户也可能不会注意到错误。维护用户主目录的工具可以轻易发现实际存在的目录与部分用户列表所隐含目录之间的不匹配并立即报告这种差异。该工具的价值在于报告问题而应避免自行尝试修复例如删除大量用户数据。生产探针测试旨在确定系统在已知数据下是否表现出可接受行为而监控旨在确认系统在未知用户数据下是否表现出可接受行为。因此测试与监控结合起来似乎可以覆盖主要风险来源包括已知风险和未知风险。然而实际风险远比这更复杂。已知良好的请求应该能够正常工作已知不良的请求则应该报错。在集成测试中同时覆盖这两类请求通常是一个好主意。可以像发布测试一样重放同一组测试请求。将已知良好的请求进一步划分为可以在生产环境中重放的请求和不能在生产环境中重放的请求就可以得到三组请求已知不良请求。已知良好且可以针对生产环境重放的请求。已知良好但无法在生产环境中重放的请求。每组测试用例都可以用作集成测试和发布测试。其中多数测试用例还可以作为监控探针使用。部署这类监控似乎是多余的原则上也似乎没有意义因为相同请求已经通过其他两种方式尝试过了。然而这几种方式之间存在差异原因如下发布测试很可能是在集成服务器基础上增加一个前端和一个模拟后端。探针测试可能覆盖的是发布后的二进制文件其中包括负载均衡前端以及独立、可扩展的持久化后端。前端和后端可能拥有独立发布周期。由于它们的发布节奏具有自适应性这些周期的进度很可能不同。因此在生产环境中运行的监控探针实际上是一种此前未被测试过的配置。这些探针操作不应该失败但如果失败了意味着什么要么是来自负载均衡器的前端 API要么是通往持久化存储的后端 API在生产环境与发布环境之间存在差异。除非已经知道生产环境和发布环境存在差异的原因否则站点很可能已经发生故障。同一个生产更新程序在逐步替换应用程序的同时也会逐步替换探针从而持续生成四种新旧组合旧探针到旧应用程序、旧探针到新应用程序、新探针到旧应用程序以及新探针到新应用程序。该更新程序可以检测这四种组合中哪一种发生错误并回滚到上一个已知良好状态。通常更新程序预期每个新启动的应用程序实例在准备接收大量用户流量前都会在短时间内处于不健康状态。如果探针已经被纳入就绪检查那么更新可以安全地无限期失败而不会将任何用户流量路由到新版本。更新会保持暂停直到工程师有时间和意愿诊断故障情况并促使生产更新程序干净回滚。这种基于探针的生产环境测试确实能够保护站点并为工程师提供清晰反馈。反馈越早提供给工程师就越有用。此外最好将测试自动化以便能够大规模向工程师发出警告。假设每个组件都有一个正在被替换的旧版本软件以及一个正在部署或即将部署的新版本。新版本可能正在与旧版本对等组件通信这会迫使它使用已弃用的 API。或者旧版本可能正在与对等组件的新版本通信并使用旧版本发布时尚未完全兼容的 API。但现在它确实能够正常工作。此时最好确保那些用于未来兼容性的测试作为监控探针运行时对 API 的覆盖范围足够全面。虚拟后端版本在实施发布测试时模拟后端通常由对等服务的工程团队维护并且仅作为构建依赖项被引用。测试基础设施执行的封闭式测试始终会将模拟后端和测试前端合并到版本控制历史记录中的同一构建点。该构建依赖项可能提供一个可运行的封闭式二进制文件。理想情况下维护该依赖项的工程团队会在发布主后端应用程序及其探针程序的同时发布这个虚拟后端二进制文件。如果后端版本可用那么将封闭式前端发布测试不包含虚拟后端二进制文件纳入前端发布包中可能是有价值的。监控系统应该能够感知两个对等节点之间某个服务接口两端的所有版本。这种设置可以确保在检索两个版本的所有组合并判断测试是否仍然通过时不需要太多额外配置。这类监控不必持续运行只需运行由任一团队发布新版本所产生的新组合即可。此类问题不一定要阻止新版本本身的发布。另一方面理想情况下部署自动化流程应该阻止相关生产环境部署直到问题组合不再可能出现。同样对等团队的自动化流程也可以考虑排空并升级那些尚未从问题组合中迁移出来的副本。结论可靠性测试是提升系统稳定性的长期工程可靠性测试是工程师提升产品可靠性最有效的投资之一。测试并不是项目生命周期中只做一两次的活动而是一个持续过程。编写高质量测试用例需要大量投入构建和维护能够促进强大测试文化的基础设施同样如此。只有理解问题才能解决问题而在工程领域只有通过测量才能真正理解问题。本章介绍的方法和技术为测量软件系统中的缺陷和不确定性奠定了坚实基础并帮助工程师在软件编写和发布过程中更好地理解其可靠性。对于 SRE 团队而言持续完善可靠性测试体系是提升系统稳定性、控制发布风险、降低 MTTR 并延长 MTBF 的关键路径。