在Salt堆中寻沙
2025-11-13 作者:Opeyemi Onikute, Menno Bezema, Nick Rhodes 阅读时长:12 分钟 本文也提供日本語和한국어版本。
当数千台服务器在15分钟内出现数百次配置变更高峰时,你如何找到配置管理故障的根本原因?
这就是我们在构建基础设施以减少因Salt(一种配置管理工具)故障而导致的发布延迟时所面临的挑战。(正如下文将解释的,我们最终将此类故障在边缘的占比降低了超过5%。)我们将探讨Salt的基本原理,以及它在Cloudflare中的应用。然后,我们将描述常见的故障模式,以及它们如何延迟我们向客户发布有价值变更的能力。
通过首先解决一个架构问题,我们为自助服务机制奠定了基础,用于查找服务器、数据中心和数据中心组上Salt故障的根本原因。该系统能够将故障与git提交、外部服务故障和临时发布关联起来。其结果便是减少了软件发布延迟的持续时间,并整体降低了SRE(站点可靠性工程师)繁琐、重复的分类工作。
首先,我们将介绍Cloudflare网络的基础知识以及Salt在其内部的运作方式。然后,我们将探讨我们如何解决这个类似于“在Salt堆中寻沙”的挑战。
Salt的工作原理
配置管理确保系统与其配置信息保持一致,并随着时间的推移维护这些信息的完整性和可追溯性。一个好的配置管理系统确保系统不会“漂移”——即偏离期望状态。现代CM系统包括基础设施的详细描述、这些描述的版本控制以及其他机制,以在不同环境中强制执行期望状态。没有CM,管理员必须手动配置系统,这个过程容易出错且难以复现。
Salt就是这种CM工具的一个例子。它专为高速远程执行和配置管理而设计,使用简单、可扩展的模型来管理大规模设备群。作为一个成熟的CM工具,它在团队和组织边界之间提供了一致性、可复现性、变更控制、可审计性和协作能力。
Salt的设计围绕着主/从架构、基于ZeroMQ构建的消息总线和声明式状态系统。(在Cloudflare,我们通常避免使用“master”和“minion”这两个词。但本文将使用它们,因为Salt就是用这些词来描述其架构的。)Salt master是一个中央控制器,负责分发作业和配置数据。它监听消息总线上的请求,并向目标minion分派命令。它还存储状态文件、支柱数据和缓存文件。Salt minion是安装在每个受管主机/服务器上的轻量级代理。每个minion通过ZeroMQ与master保持连接,并订阅发布的作业。当某个作业与minion匹配时,它会执行请求的函数并返回结果。
下图展示了本文为了说明目的而对文档中描述的Salt架构进行的简化。
状态系统提供声明式配置管理。状态通常用YAML编写,描述一个资源(包、文件、服务、用户等)及其期望的属性。一个常见的例子是包状态,它确保包以指定版本安装。
|
|
状态可以调用执行模块,这些是执行系统操作的Python函数。在应用状态时,Salt会返回一个结构化结果,包含状态是否成功(result: True/False)、注释、所做的更改以及持续时间。
Salt在Cloudflare的应用
我们使用Salt来管理我们不断增长的机器群,并且之前曾写过关于我们广泛使用的文章。上述的主-从架构允许我们以状态的形式将配置推送到数千台服务器,这对于维护我们的网络至关重要。我们设计的变更传播方式包含了爆炸半径保护。有了这些保护措施,一次高级状态失败就变成了一个信号,而不是一个影响客户的事件。
这种发布设计是有意为之——我们决定“安全失败”而不是“硬失败”。通过进一步添加护栏,在功能到达所有用户之前安全地发布新代码,我们能够自信地传播变更,默认情况下故障会停止Salt部署管道。然而,每次停止都会阻塞其他配置部署,并需要人工干预来确定根本原因。由于步骤是重复的且不带来持久价值,这可能很快变成一个繁琐的过程。
我们Salt变更部署管道的一部分使用了Apt。每X分钟就有一个提交合并到master分支,每Y分钟这些合并被打包并部署到APT服务器。从该APT服务器检索Salt Master配置的关键文件是APT源文件:
|
|
该文件将master引导到其特定环境的正确套件。使用该套件,master检索包含最新更改的相关Salt Debian包的最新包。它安装该包并开始部署包含的配置。当它在机器上部署配置时,机器使用Prometheus报告其健康状态。如果一个版本是健康的,它将被推进到下一个环境。在能够推进之前,一个版本必须通过一定的“浸润”阈值,以便让一个版本“暴露”其错误,使更复杂的问题显现出来。这是理想情况。
不理想的情况则带来无数麻烦:当我们进行渐进式部署时,如果一个版本损坏了,那么任何后续版本也会损坏。并且因为损坏的版本不断被新版本超越,我们需要完全停止部署。在版本损坏的场景下,尽快发布修复至关重要。这触及了本文的核心问题:如果一个损坏的Salt版本在环境中传播,我们正在放弃部署,并且需要尽快发布修复,该怎么办?
痛点:Salt如何中断和报告错误(以及它如何影响Cloudflare)
尽管Salt力求幂等和可预测的配置,但故障可能发生在渲染、编译或运行时阶段。这些故障通常是由于配置错误造成的。Jinja模板中的错误或无效的YAML可能导致渲染阶段失败。例如缺少冒号、缩进不正确或未定义的变量。通常会引发语法错误,并附带指向问题行的堆栈跟踪。
另一个常见的故障原因是缺少支柱或grain数据。由于支柱数据在master上编译,忘记更新支柱顶层文件或刷新支柱数据可能导致KeyError异常。作为一个使用依赖关系来维持顺序的系统,配置错误的依赖关系可能导致状态执行顺序错误或被跳过。当minion无法向master认证,或由于网络或防火墙问题无法连接到master时,也可能发生故障。
Salt通过几种方式报告错误。默认情况下,当任何状态失败时,salt 和 salt-call 命令以返回码1退出。Salt还为特定情况设置了内部返回码:1表示编译错误,2表示状态返回False,5表示支柱编译错误。测试模式显示将进行哪些更改而无需实际执行,但有助于捕获语法或排序问题。可以使用 -l debug CLI选项(salt <minion> state.highstate -l debug)切换调试日志。
状态返回还包括各个状态失败的详细信息——持续时间、时间戳、函数和结果。如果我们通过引用Salt文件服务器中不存在的文件来为file.managed状态引入一个故障,我们会看到这个失败:
|
|
返回结果也可以以JSON格式显示:
|
|
输出格式的灵活性意味着人们可以在自定义脚本中解析它们。但更重要的是,它也可以被更复杂、相互关联的自动化系统使用。我们知道可以轻松解析这些输出,将Salt故障的原因归因于某个输入——例如源代码控制的更改、外部服务故障或软件发布。但当时缺少了某些东西。
解决方案
配置错误是大规模系统中常见的故障原因。有些甚至可能导致全系统中断,我们通过发布架构来防止这种情况。当新的发布或配置在生产环境中出现问题时,我们的SRE团队需要找到并修复根本原因,以避免发布延迟。正如我们之前提到的,由于系统复杂性,这种分类工作既繁琐又日益困难。
虽然有些组织使用形式化技术,如自动化根本原因分析,但大多数分类工作仍然是令人沮丧的手动过程。在评估了问题的范围后,我们决定采用自动化方法。本节描述了解决生产环境中这个广泛而复杂问题的分步方法。
第一阶段:可检索的CM输入
当Salt minion上的高级状态失败时,SRE团队面临着一个繁琐的调查过程:手动SSH到minion,在日志中搜索错误消息,追踪作业ID(JID),并在多个关联的master中的一个上定位与该JID关联的作业。这一切都需要在一个4小时的master日志保留窗口内争分夺秒地进行。根本问题是架构性的:作业结果位于Salt Master上,而不是在执行它们的minion上,这迫使操作员猜测哪个master处理了他们的作业(SSH到每个master上),并且限制了没有master访问权限的用户的可见性。
我们构建了一个解决方案,直接在minion上缓存作业结果,类似于master上存在的local_cache返回器。这使得本地作业检索和延长保留期成为可能。这将一个多步骤、有时限的调查转变为单一查询——操作员可以从minion本身检索作业详细信息,自动提取错误上下文,并将故障追溯到特定的文件更改和提交作者。自定义返回器智能地过滤和管理缓存大小,消除了“哪个master?”的问题,同时也实现了自动错误归因,减少了解决时间,并从常规故障排除中移除了人力负担。
通过去中心化作业历史记录并使其在源头上可查询,我们显著地向自助调试体验迈进了一步,在这种体验中,故障被自动上下文化和归因,让SRE团队专注于修复而不是调查取证。
第二阶段:使用Salt Blame模块进行自助服务
一旦作业信息在minion上可用,我们就不再需要解析哪个master触发了失败的作业。下一步是编写一个Salt执行模块,允许外部服务查询作业信息,更具体地说是失败的作业信息,而无需了解Salt内部机制。这促使我们编写了一个名为Salt Blame的模块。Cloudflare以其无责文化而自豪,而我们的软件则不然……
该“blame”模块负责整合三件事:
- 本地作业历史信息
- CM输入(作业期间存在的最新提交)
- Git仓库提交历史
我们选择编写执行模块是为了简单性,将外部自动化与理解Salt内部机制的需求解耦,并有可能被操作员用于进一步的故障排除。编写执行模块在运维团队中已经非常成熟,并且遵循明确定义的最佳实践,如单元测试、代码检查和广泛的同行评审。
该模块非常简单易懂。它按时间倒序迭代本地缓存中的作业,按时间顺序查找第一个失败的作业,然后是紧接在它之前的成功作业。这仅仅是为了缩小真正首次故障的范围,并为我们提供前后状态结果。在这个阶段,我们有几种途径向调用者提供上下文:为了找到可能的提交责任人,我们会查看最后一次成功作业ID和失败作业ID之间的所有提交,以确定是否有任何提交更改了与失败相关的文件。我们还提供了失败状态及其输出的列表,作为发现根本原因的另一条途径。我们已经了解到,这种灵活性对于涵盖广泛的故障可能性非常重要。
我们还区分了正常的失败状态和编译错误。如Salt文档所述,每个作业根据结果返回不同的返回码。
- 编译错误:当状态编译器遇到任何错误时,返回码设为 1。
- 失败状态:当任何状态返回 False 结果时,返回码设为 2。
我们的大多数故障都表现为失败状态,这是源代码控制变更的结果。为客户构建新功能的工程师可能会无意中引入一个未被我们的CI和Salt Master测试捕获的故障。在该模块的第一个迭代中,列出所有失败状态就足以精确定位高级状态故障的根本原因。
然而,我们注意到有一个盲点。编译错误不会导致状态失败,因为没有状态运行。由于这些错误返回的返回码与我们检查的不同,该模块对它们完全视而不见。大多数编译错误发生在Salt服务依赖项在状态编译阶段失败时。它们也可能因源代码控制的更改而发生,尽管这种情况很少见。
在同时考虑了状态失败和编译错误后,我们极大地提高了定位问题的能力。我们将该模块发布给SRE,他们立即意识到了更快进行Salt分类的好处。
|
|
第三阶段:自动化,自动化,再自动化!
更快的分类总是受欢迎的进步,工程师们也习惯于在minion上运行本地命令来分类Salt故障。但在繁忙的轮班期间,时间至关重要。当故障跨越多个数据中心或机器时,在这些minion上运行命令很容易变得繁琐。这个解决方案还需要在多个节点和数据中心之间进行上下文切换。我们需要一种方法来使用单个命令聚合常见的故障类型——单个minion、预生产数据中心和生产数据中心。
我们实施了多种机制来简化分类工作并消除手动触发。我们的目标是让这些工具尽可能接近分类工作发生的位置,这通常是聊天工具。通过三个不同的命令,工程师现在可以直接从聊天线程中对Salt故障进行分类。
通过分层方法,我们使得对minion、数据中心和数据中心组进行个别分类成为可能。这种层级结构使该架构完全可扩展、灵活且自组织。工程师可以根据需要对一个minion进行故障分类,同时也可以对整个数据中心进行分类。
能够同时对多个数据中心进行分类的功能,对于跟踪预生产数据中心故障的根本原因立即变得有用。这些故障延迟了变更向其他数据中心的传播,并阻碍了我们为客户功能、bug修复或事件补救发布变更的能力。这个分类选项的添加,将调试和修复Salt故障的时间减少了超过5%,使我们能够持续为客户发布重要的变更。
虽然5%看起来不是立竿见影的巨大改进,但其魔力在于累积效应。我们不会公布发布延迟的实际时间数据,但我们可以做一个简单的思维实验。如果平均每天花费的时间即使只有60分钟,那么减少5%每月就能为我们节省90分钟(1小时30分钟)。
另一个间接的好处在于更高效的反馈循环。由于工程师花费更少的时间摆弄复杂的配置,这些精力被转移到防止故障复发上,从而进一步以不可估量的方式减少了整体时间。我们未来的计划包括通过测量和数据分析来理解这些直接和间接反馈循环的结果。
下图展示了一个预生产分类输出的例子。我们能够将故障与git提交、发布和外部服务故障关联起来。在繁忙的轮班期间,这些信息对于快速修复中断至关重要。平均而言,每个minion的“blame”查询耗时不到30秒,而多个数据中心能够在不到一分钟内返回结果。
下图描述了分层模型。层次结构中的每一步都是并行执行的,使我们能够获得极快的结果。
有了这些可用的机制,我们通过在已知条件下(尤其是那些对发布管道有影响的条件)触发分类自动化,进一步缩短了分类时间。这直接提高了对边缘变更的交付速度,因为找到根本原因并进行前向修复或回滚所需的时间更少了。
第四阶段:测量,测量,再测量
在我们获得了极快的Salt分类能力之后,我们需要一种方法来测量根本原因。虽然个别的根本原因并非立即可见其价值,但历史分析被认为是重要的。我们想了解故障的常见原因,特别是当它们阻碍我们为客户提供价值时。这些知识创造了一个反馈循环,可用于将故障数量保持在较低水平。
我们使用Prometheus和Grafana来跟踪故障的主要原因:git提交、发布、外部服务故障和未归因的失败状态。失败状态列表特别有用,因为我们想知道那些重复出现的问题,并推动更好地采用稳定的发布实践。我们也特别关注根本原因——因git提交导致的故障数量激增表明需要采用更好的编码实践和代码检查;因外部服务故障导致的激增表明内部系统存在需要调查的回归;因发布导致的故障激增表明需要更好的门控和发布引导。
我们每月分析一次这些指标,通过内部工单和升级提供反馈机制。虽然这些工作的直接影响还不明显,因为这些努力才刚刚开始,但我们期望通过减少我们所见的故障数量,来改善我们Saltstack基础设施和发布过程的整体健康状况。
更广阔的图景
许多运维工作通常被视为“必要的恶”。当故障发生时,运维人员习惯于介入并修复它们。这种警报-响应的循环对于保持基础设施运行是必要的,但它常常导致繁琐的工作。我们在之前的博客文章中讨论过繁琐工作的影响。
这项工作代表了朝着正确方向迈出的又一步——为我们的on-call SRE减少了更多繁琐工作,并腾出宝贵的时间来处理新出现的问题。我们希望这能鼓励其他运维工程师分享他们正在为减少组织中整体繁琐工作所做的进展。我们也希望这类工作可以在Saltstack本身内部得到采用,尽管不同公司生产系统间缺乏同质性使得这不太可能。
未来,我们计划提高检测的准确性,并减少依赖外部输入关联来确定失败结果的根源。我们将研究如何将更多此类逻辑迁移到我们原生的Saltstack模块中,进一步简化流程并避免外部系统漂移导致的回归。
如果这类工作让你感到兴奋,我们鼓励你看看我们的招聘页面。
Cloudflare的连接云保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用,抵御DDoS攻击,阻止黑客入侵,并可以帮助您踏上零信任之旅。
从任何设备访问 1.1.1.1 开始使用我们的免费应用程序,让您的互联网更快更安全。
要了解我们帮助构建更好的互联网的使命,请从这里开始。如果您正在寻找新的职业方向,请查看我们的空缺职位。