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