最近的 CrowdStrike 中断事件提醒我们,持续评审并维护高标准的评审流程对于提交和推出生产变更来说有多么重要。这并不是对 CrowdStrike 中断事件及其流程的批评,而是一个重新审视最佳实践和总结中断分析方法(基于我对数百万 TPS 复杂系统的回顾经验)的契机。
一个潜伏的 Bug(显然是由于缺少空值检查)导致了 CrowdStrike 中断。社交媒体上出现了一些帖子,对可能做出不当更改的开发者进行了批评。然而,在深入探讨其他细节之前,我遵循了一个原则:在回顾影响客户的事件时,我们关注的焦点不在于“谁”出了问题,而在于“为什么”会出现这样的问题。
首先,我们需要认识到,开发或运维人员不应该受到指责,指责他们并不能对系统当前的状态带来实质性的改善。如果问题的根源在于人为失误,那么这可能意味着我们的系统缺少必要的检查或故障安全机制,我们首先需要关注的是为什么他们的错误会进在系统里。
我们应持不断努力构建系统、流程和工具来减少运维人员犯错的可能性,特别是那些可能对生产系统造成广泛影响的错误。目标是创造一个环境,使得运维人员几乎不可能犯下这样的错误。
一个重要的提醒:永远不要让你的工作流程变得过于复杂——保持简洁,同时增加做错事的难度(例如,防止意外删除数据表)。以下是一些思考最佳实践的指导原则,它们可以帮助我们在 Bug 到达生产环境之前就将其拦截,或者如果 Bug 不可避免地进入了生产环境,至少可以将其影响降到最低。当你分析中断事件并考虑如何改进系统时,这些流程同样适用。
通常可以将最佳实践和中断事后分析分为三个类别:
完全预防;
最小化影响范围;
快速检测和快速恢复。
我们将在后续部分详细讨论这三个类别。
1. 完全预防
我们先问一个问题:如何能在 Bug 或问题影响到生产系统之前就捕捉到它们?解决方案包括:在本地环境中进行简单的测试、对代码评审设定高标准、单元和集成测试覆盖率、部署管道测试自动化,以及生产部署之前的警报。
建立一个完备的沙盒(开发)环境:开发者应当拥有一个完备且隔离的环境,这样他们就可以在不影响真实用户的前提下使用真实数据进行快速的实验和测试。尽管这是我们追求的目标,但这些环境往往不够稳定。这是因为许多开发者同时在一个环境中测试他们的变更。解决这一问题的一个方法是确保值班人员或支持团队在处理高优先级生产问题的同时,也关注并解决这些环境的不稳定性问题。虽然这是一个持续存在的挑战,但它对于及早发现并解决 Bug/ 问题来说至关重要。
对代码评审流程设定高标准:通过设定严格的代码评审标准,我们可以确保大多数,甚至全部的 Bug 在进入生产环境之前被识别和修复。我曾目睹敏锐的评审人员在评审过程中发现并解决了一些在代码变更期间未被触及的问题。虽然在理想情况下这些情况不应该发生,但现实往往复杂多变。一种流行的实践是在代码提交前要求至少两个评审人员的批准。但需要注意的是,过多的评审人员可能会拖慢评审流程,影响开发敏捷性。因此,我们需要找到一个平衡点。
如果代码评审超过几个星期还没有结束,很可能评审人员试图解决的问题超出了代码变更的原始意图。代码评审的目标不是为了追求完美无暇的代码,而在于提高代码的整体可读性、可理解性和可维护性。客户更看重交付给他们的新变更 / 功能。交付能力是衡量成功的标准。这里有一份很好的阅读材料,可以帮助你在这方面建立正确的思维模式。
确保高单元测试和集成测试覆盖率:我曾目睹多起事件,由于单元或集成测试覆盖率不足,导致 Bug 进入了生产环境。虽然许多开发者私下里承认编写单元测试和集成测试是一项乏味的工作,但这些测试在防止生产系统出现更广泛的中断方面发挥着至关重要的作用,尽管这一点往往没有得到足够的认可。强制规定只有在新代码变更达到足够的单元和集成测试覆盖率时才能通过代码评审,这是团队可以采纳的另一个重要流程。
全面的预生产环境测试:与之前讨论的可能存在稳定性问题的开发者环境不同,预生产环境最接近生产环境。在这个环境中,变更已经通过了代码评审,完成了单元测试、集成测试和手动测试,已经准备好部署到生产环境。一些工具,如 SonarQube,可以在部署管道中帮助我们自动化这些检查。在这个环境中采用外部测试方法,可以模拟外部客户在使用应用程序时可能遇到的情况。这种测试方法要求你为服务设置一个外部流程,定期发送请求,并验证结果是否与预期响应的准确性一致。
预生产环境的一个不足之处在于它通常不会经历与生产系统相似的流量,这可能无法揭示代码或服务在规模扩展时可能出现的问题。因此,许多团队会定期在预生产环境中进行负载测试,模拟生产流量,并作为部署管道的一部分,确保所有服务指标在进入生产环境前都保持健康。虽然这种测试可能会带来较高的成本,但可以通过使用按需资源配置(在不使用时释放资源)来控制成本。这通常是在真实客户体验到新代码变更之前的最后一道防线。在这个环境中,任何与可用性或延迟相关的警报都应被赋予高优先级,并阻止部署,直到问题被彻底解决。理想情况下,系统应该能够在关键指标开始报警时自动暂停部署到生产环境。
上述步骤旨在确保在部署到生产环境之前捕捉到 Bug。然而,我也观察到,即使采取了这些措施,由于各种原因,包括未被测试到的边缘情况,有时候这些步骤也可能失效。CrowdStrike 中断事件就是一个例子。此外,1990 年 AT&T 的崩溃事件和由于闰年导致的 Zune Bug 也是这方面的有趣案例。接下来,我们将讨论如果一个 Bug 逃过了预生产环境的检测,应该如何处理。
2. 最小化影响范围
如果一个 Bug 还是不可避免地进入了生产环境,我们该如何确保其影响范围尽可能小?
请注意:上一节讨论了不会导致生产中断的可能性。
在单盒环境中测试变更:在代码被部署到生产环境之前,会先被部署到单盒容器环境中进行测试。这种环境通常由一个或几个服务器组成,它们处理的生产流量比例较小,大约是 1% 到 5%。一个关键点是,即便单盒容器环境处理的是生产流量,其性能指标也应与生产环境分开记录。这样做是有必要的,因为它能帮助我们及时发现指标异常,并在必要时迅速回滚。
理想情况下,回滚操作应实现自动化,一旦监测到警报触发,便立即执行。我们还希望对生产环境的任何更改都是可逆的。然而,正如我们在 CrowdStrike 中断事件中所看到的,并非所有更改都能轻易回滚。在这种情况下,采用单盒测试或后续介绍的分阶段部署策略更为明智,这有助于我们有效控制影响范围。影子模式测试是另一种在不可逆更改推出前验证代码的有效方法。在影子模式下,你可以收集并分析新代码的性能指标或日志数据,同时又不会改变现有系统的行为。一旦这些指标或日志经过彻底验证,就可以放心地替换现有行为。
针对单盒环境执行外部测试流程:如前所述,外部测试方法需要构建一个独立的外部流程,持续模拟客户流量并验证系统响应。这一点至关重要,因为仅依赖服务器端指标可能无法完全揭示客户实际可能遇到的问题,例如网络延迟等。通过测量和监控这些指标,我们可以在客户向支持团队反馈问题之前发现并解决问题。毕竟,如果客户向我们反馈问题,而我们却一无所知,那将是一种失职。我们的目标是主动识别并预警系统问题,并在客户开始寻求支持之前积极地进行修复。
分阶段推出生产变更:在单盒环境测试之后,代码变更应该分阶段推出,仅影响一小部分流量。实现这一目标的方法有很多。例如,我们可以采用部署策略,如可用区域感知部署,即逐个可用区域部署。如果服务在多个区域运行,这种方法是确保高可用性的最佳实践之一。此外,我们需要按区域收集指标,以便通过警报系统捕捉到问题并触发自动化回滚,正如上一节所讨论的。如果变更无法回滚,最好是能够找到一种方法将变更应用于只有少数客户的场景,你可能在与这些客户直接合作或之前已经达成共识,以便有更好的事件响应准备。这应该是一种特殊情况,但好过触发可能影响所有客户的中断,尤其是当你对变更可能造成的影响信心不足时。
正如上一节所讨论的,如果能够在生产环境中进行影子测试,这是一种强大的方法。另一种方法类似于 A/B 测试,系统逐步以 1%、5%、10%、25%、50% 和 100% 的增量向客户推出新变更。这种方法为控制变更影响提供了更好的手段。对于处理数百万每秒事务(TPS)的服务,就像我有幸参与的一些服务一样,可以考虑采用蜂窝架构。
我们不在这里深入探讨蜂窝架构的细节,但可以简单地理解为,蜂窝架构不是在单个区域运行单个生产环境,而是运行多个环境,每个环境处理来自不同预分配用户群流量,这些用户群被分配给不同的单元。你也可以根据系统的其他关键属性来预分配流量,例如客户的账户 ID 等。除非系统需要处理数十万以上的 TPS,否则我不建议采用蜂窝架构。蜂窝架构引入了不同类型的复杂性,而且额外的运维开销对于一些系统来说可能并不划算。蜂窝架构的一个缺点是,它会增加总体的部署时间,因为原本单个部署服务区域被划分成多个单元,部署通常需要按照逐个单元的顺序进行。
3. 快速检测和快速恢复
一旦意外的错误部署到了生产环境,我们希望能够尽早发现问题,并采取行动将系统恢复到稳定状态。
构建细粒度的指标和报警,从监控总体服务健康指标(如可用性、延迟等)开始。但随着系统的不断演进,定期评估是否需要其他可以准确捕捉客户体验的细粒度指标就变得尤为重要。例如,如果一些大型客户主导了系统的整体流量,而有些客户只贡献了整体流量的一小部分,那么 P99、P99.9 这些可用性指标可能不一定能捕捉到贡献较小比例整体流量的小型客户的体验。
在这种情况下,你应该构建一个自定义指标,比如单个客户级别的可用性(PCA)指标。这种指标可以衡量所有客户的体验,无论他们产生的流量是多是少。你需要一种方法来捕捉每个客户的体验,然后对所有客户的数据进行聚合,以确定在可接受的服务故障范围内有多少客户能够成功地获得服务。这并不意味着你可以忽略整体的 P99 和更高百分位的可用性或延迟指标。相反,你应该将 PCA 指标和服务级别的指标结合起来,以便更全面地理解客户体验。
我记得有一次,尽管总体指标(例如 P99、P99.9)看起来是可接受的,但一小部分使用不太常见功能的客户却持续面临可用性问题。在这种情况下,针对这些特定客户的可用性指标远远低于我们设定的服务水平协议(SLA)。为了捕捉这些问题,我们可以考虑主动为这些特性添加单独的指标,以确保能够覆盖到他们。我们已经讨论了在单盒环境中进行外部测试,但这些测试流程也应该在生产环境中执行,以确保能够捕捉到最终客户体验。记得有一次,尽管服务指标看起来很健康,但团队还是发现了一个只能从客户端看见的问题,这个问题是由于网络相关问题引起的,仅通过查看服务指标是无法发现的。正如之前讨论的,我们永远不希望处于这样一种境地:我们的客户向我们报告问题,而我们却一无所知,也没有采取任何措施去修复。
优化故障原因分析流程:除了指标之外,另一个需要关注的领域是确保我们的开发人员能够在系统报警触发后迅速定位问题,这通常从分析日志和关键指标开始。如果团队最近经历了一次中断,那就应该深入探讨如何进一步缩短找到根本原因所需的时间,以便在未来遇到类似问题时能够更迅速做出响应。这可能涉及改进日志记录策略或推动更好的指标,让值班人员能够精确地定位问题。此外,建立团队级别的知识库,包括运行手册或“急救箱”,不仅可以促进团队内部的知识共享,还可以帮助组织中的其他团队。
自动化回滚与回滚速度:我们的部署流程应该设计成能够在报警触发时自动执行回滚操作。同样重要的是,整个可用区域应该能够在几个小时内(大约 3 到 6 小时)将部署的变更回滚到之前的版本。回滚所需的时间越短,客户受到的影响就越小。那些无法回滚的情况应该是非常罕见的,在这种情况下,应该评估并遵循高级领导制定的操作响应计划,确保在生产环境中遇到问题时能够妥善处理。这种情况应该是一次性的,而不是常态。
提升系统在中断后的恢复能力:回滚并不总是能够完全恢复系统状态。在这种情况下,需要问一下开发团队是否有改进恢复系统到安全状态的方法或流程,以及可以采取哪些行动来加快系统状态恢复的速度。
结束语
总的来说,最重要的是培养一种持续评估、学习和改进的文化。这种持续的评估过程至关重要,因为在快速交付客户价值、确保系统安全和有限的资源之间总会存在冲突。拉斯穆森在其关于动态社会风险管理的论文中对这一点进行了很好的阐释。论文的一个核心观点是,操作一个系统需要有一个安全区域。
这个安全区域不断地受到管理压力(经济失败的边界)、有限的资源(不可接受的工作量边界)以及维护系统安全(失败的边界)的推动。一旦安全区域向失败边界倾斜,我们就会遭遇一次中断,并采取行动将那个“安全操作区域”泡沫重新拉回到中心位置。因此,虽然我们倾向于取笑经历过中断的团队或公司,但也要懂得进行自我反思,看看自己的“安全操作区域”泡沫是否过于靠近失败的边界。
这张图来自拉斯穆森关于动态社会风险管理的论文
查看英文原文链接:
https://www.infoq.com/articles/analysis-optimization-change-release-process/
评论