第十四章 系统配置最佳实践

系统配置是SRE经常要面对的问题。这是一项繁琐的工作,有时还会让人沮丧,特别是当工程师不熟悉系统,或是系统配置目标不够清晰时这种情况更为明显。系统配置常见的场景如下:在系统初始阶段的配置设计,出现事故时的紧急配置设计。

本章基于经验和策略,以基础架构系统工程师的角度来进行系统配置,从而达到“安全和可持续”的目标。

什么是配置?

系统并不是永恒不变的。不断变化的业务需求、基础架构的需求和其他因素都会导致系统发生变化。当需要对系统快速迭代时(这个过程是昂贵和冗长的,不仅仅包含系统的重构、部署和代码的更改,系统配置的更新也是重要的一部分。由此,我们需要提供一种低开销的和人机界面的方式进行系统配置。SRE会利用此系统进行系统部署、性能调整和事件响应期间的配置变更。

我们将系统分为三个关键组件: . 应用程序 . 数据集 . 系统配置

实际上,我们是无法将以上三个部分清楚的区分。例如,许多系统使用编程语言进行配置。同样,数据集可能包含代码,例如存储SQL的过程,这些代码已经构成了“应用程序”。

而良好的配置界面可以完成快速、可信和可测试的配置更改。减少错误的发生,降低工程师的学习曲线。

配置的可靠性

系统最终由人来进行管理和配置。系统配置人机界面的质量会影响团队运行该系统的能力和可靠性。精心设计的配置界面对使用者的影响类似于代码质量对系统可维护性的影响。

但在一些方面,配置往往与代码有不同的意义。通过代码更改系统功能是一个冗长且复杂的过程,涉及的范围往往是小增量的更改、代码审查和测试。相比之下,更改单个配置选项可能会对功能产生重大影响。例如,一个错误的防火墙配置规则可能会将自己锁到系统之外。而且,配置通常存在于未经测试(甚至是不可测试)的环境中。

而且,系统配置更改可能需要工程师处于很大的压力下。在故障期间,可以简单安全地调整配置是必不可少的过程。举个航空的例子,早期,飞机的控制界面和指示灯混乱导致了许多安全事故。研究表明,无论飞行员技能或经验如何,操作故障都是频繁的。所以,可靠的配置是至关重要的。

原理和机制

在设计新软件或使用现有软件组装新系统时我们需要讨论配置:如何配置它?配置如何加载?我们将配置设为两部分:配置理论和配置机制。

配置理论适用于完全独立于所选语言和其他机制的配置方面。我们对原理的讨论包括如何构造配置,如何实现抽象,以及如何无缝地支持不同的用例。

我们对配置机制讨论涵盖了语言设计,部署策略以及与其他系统的交互等方面。本章重点关注机制,部分原因是语言选择已经在整个行业中进行了广泛的讨论。此外,一些特定的组织可能已经具有强大特色化的要求,例如预先存在的配置基础架构,因此配置机制不容易推广。Jsonnet的以下章节给出Jsonnet现有软件中配置机制的案例——特别是语言设计方面的。

分别讨论理论和机制使我们能够更清楚地进行配置。实际上,如果配置需要大量难以理解的用户输入,那么配置语言(无论是XML还是Lua)等实现的细节也无关紧要。相反,如果必须将它们输入到非常麻烦的界面中,即使最简单的配置输入也会导致问题。比如旧的Linux内核配置的过程是——必须通过一系列命令进行配置设置每个参数。为了进行最简单的校正,用户必须从头开始配置过程。

配置理论

本节讨论内容基于完整配置实现,因此下文一些观点是对所有配置实现的概况。

在以下理论观点中,我们的理想配置是不需要任何配置。在理想场景下,新系统部署前,能根据部署信息、负载或其他配置自动生成正确配置。当然,这些实际都不太可能实现。需要指出的是,这个想法指出了配置的目标:避免复杂、多样的配置,向简单化努力。

历史上,NASA的核心系统提供了大量的控制操作(相当于配置),这需要对操作者进行高强度的训练才能掌握。图14-1,展现了NASA的一名操作者,通过复杂的操作排列进行飞行器控制。在现代工业中,这种培训已不再可行。

图14-1 NASA航天器控制中心的控制面板,说明了配置复杂性

尽管这些理想的配置可以减少对操作员的训练,但也让操作员对问题的深入理解降低。然而,随着系统复杂性增加,操作员对系统的认知理解却越来越重要。

当我们把这些原则应用到Google的系统时,目标是让内部用户使用简单、全面和低成本。

与用户交互的配置问题

无论你的配置是什么,如何配置,最终都体现在与计算机交互的一个界面,来询问用户接下来如何操作,让用户来选择。无论是XML编辑,还是GUI向导,这种模型都适用。

在现代软件系统中,我们可以从两个不同的角度来看待这个模型:

以基础设施为主:

提供尽可能多的配置。这样做使用户能够根据他们的确切需求调整系统。配置越多越好,因为系统可以调整到完美。

以用户为主:

首先会询问用户一部分关于基础设施的问题,之后才能回归到实际业务上。这样的配置,越少越好,因为回答问题是很繁琐、麻烦的事情。

在最初的理论模型中,我们推崇以用户为中心的理念。

这样的软件设计脱离了实际。以用户为中心的配置,要求你的软件设计需要真正考虑到用户的需求,专注于用户,这就需要对用户的需求进行深入挖掘。相比而言,以基础设施为中心的配置,需要你为用户提供丰富的配置,来达到系统操作的目的。这些模型互相不冲突,但调试他们比较困难。在配置中,选项可枚举是比较好的,而不是设计出一个极其通用的软件,真正做到“开箱即用”。

实际中,可以通过各种方式(后续章节会介绍)来删除一些配置,让系统的配置从以基础设施为主,向以用户为主进行转变。

交互性问题更应接近用户目标

当我们以用户为中心进行配置时,提出的交互式问题,需要确保用户能够准确理解。我们可以思考用户输入的本质:一方面,针对用户的每一项提问,要有更少的配置选项;另一方面,用户又想了解系统如何实现他们的需求,这就需要更多的选项。

让我们以沏茶的过程来比喻如何配置的过程。在配置项比较少的情况下,用户可以要求“热绿茶”,并能基本满足用户需求。相反,配置项很多的情况下,用户可以要求:水量、水温、茶叶品牌、茶叶分量、浸泡时间、茶杯类型。为了获取接近完美的茶,而使用更多配置项,坚持这些细节所付出的成本和代价,可能太大。

这个比喻对用户和配置系统开发人员都很有帮助。当用户确定操作步骤时,系统应按照他们要求进行。但是当用户只清楚自己目标时,我们的系统可以改进其中的配置步骤,最终实现用户目标即可。因此,预先了解用户目标是很有必要的。

此理论在实际应用中发挥的价值,以任务调度系统来补充说明。假如你需要运行一个分析进程,通过Kubernetes或Mesos可以实现你的目标,而不需要你花费很长时间去配置细节参数,比如选择哪台物理机运行等。

配置的必选项和可选项

配置分为两类:必选项和可选项。必选项的配置回答的问题针对核心功能。例如谁为一项手术收费。可选项一般不代表核心功能,但配置这些选项可以提高功能的质量——例如,设置一些工作进程。

为了保持以用户为中心并确保易用性,你的系统应尽量减少必选配置的数量。这不是一件容易的事,但这很重要。虽然人们可能会争辩说,增加一个或两个小步骤只会增加很少的成本,但工程师的生活往往是一个无穷无尽的单独步骤链。这些小步骤的减少可以显着提高工作效率。

最初的一组必选配置通常包括您在设计系统时考虑的问题。减少必选项的最简单方法是将它们转换为可选项,这意味着这些默认配置可以安全有效地应用于大多数(如果不是全部)用户。例如,我们可以简单地执行运行,而不是要求用户定义执行是否应该运行。

虽然默认值通常是保存在代码中的静态值,但并非必须如此。它可以基于系统的其他属性动态确定。利用动态确定可以进一步简化配置。

对于上下文,请参考以下动态默认示例。计算密集型系统通常可以通过配置控制使用多少计算线程。它的动态缺省配置了与系统(或容器)具有执行核数一样多的线程。在这种情况下,单个静态默认值没有用。动态默认值意味着我们不要求用户给定平台上部署的正确线程数。同样,单独部署在容器中的Java二进制文件可以根据容器中的可用内存自动调整其堆限制。这是两个常见的动态默认值示例。如果您需要限制资源使用,则对能够覆盖配置中的动态默认值非常有用。

使用动态默认值可能不适用于所有人。随着时间的推移,用户可能更喜欢不同的方法并要求更好地控制动态默认值。如果很多用户反映动态配置有问题,一般来说这表示你的动态配置逻辑可能不再符合当前用户群的要求。需要考虑实施改进,使您的动态默认值无需额外的配置即可运行。如果只有一小部分用户不满意,他们最好手动设置配置选项。更复杂的系统会增加用户的工作量(例如,增加了文档的阅读难度)。

在为可选项选择默认选项时,无论您选择静态还是动态默认值,请仔细考虑您选择的影响。经验表明,大多数用户会使用默认值,因此默认值的配置既是机会也是责任。你可以巧妙地向人们推进正确的方向,但指定错误的默认值会造成很大的伤害。例如,考虑配置默认值及其在计算机科学之外的影响。器官默认捐献的国家的器官捐献都比例明显高于器官默认不捐献的国家。简单地选择特定的默认值会对整个系统中的医疗选择产生深远的影响。

一些可选项在没有明确用例的情况下开始。您可能想要完全删除这些问题。大量可选项可能会使用户感到困惑,因此您应该仅在真正需要的情况下添加配置选项。最后,如果您的配置恰好使用了继承的概念,那么能够恢复配置中任何可选项的默认值是很有用的。

避免简单

到目前为止,我们已经讨论过将系统配置简化为最简单的形式。但是,配置系统也可能需要考虑高级用户。回到我们的茶类比,如果我们真的需要在特定时间内浸泡茶怎么办?

适应高级用户的一种策略是找到常规用户和高级用户所需要的最低公分母,并将此复杂性作为默认值。缺点是这个决定会影响每个人; 即使是最简单的用例现在也需要以低级别的角度来考虑。

通过根据默认行为的可选覆盖考虑配置,用户配置“绿茶”,然后添加“将茶浸泡五分钟。”在此模型中,默认配置仍然是高级别并且接近用户的目标,但用户可以微调低级别配置。这种方法并不新颖。我们可以用高级编程语言(如C ++或Java)做类比,程序员能够将代码中的机器(或VM)指令包含在以高级语言编写的代码中。在某些消费者软件中,我们看到具有高级选项的屏幕可以提供比典型视图更精细的控制。

优化整个配置的总时间是有用的。不仅要考虑配置本身的行为,还要考虑用户在提供许多选项时可能遇到的决策困难,在纠正错误配置所需的时间,由于信心较低而导致的修改配置速度较慢等等。在考虑配置设计备选方案时,如果能够更轻松地支持最常见的用例,则可以选择以较少但难度较大的步骤完成复杂配置的选项。

如果您发现超过一小部分用户需要复杂配置,则可能错误地识别了常见用例。如果是这样,请重新审视系统的初始产品假设并其他用户进行研究。

配置机制

到目前为止,我们的讨论涵盖了配置哲学。本节将重点转移到用户如何与配置交互的机制。

单独的配置和产生的数据

存储配置的语言是一个不可避免的问题。您可以选择使用INI、YAML或XML文件中的纯数据。或者,配置可以存储在更高级语言中,以允许更灵活的配置。

从根本上说,向用户提出的所有问题都可以归结为静态信息)。这显然对“应该使用多少线程”等问题的静态回答)“但是,即使”每一个请求都应该使用什么功能?“也只是功能的静态引用。

要回到配置是代码还是数据这个古老的问题,我们的经验表明,代码和数据都有,但将两者分离是最优的。系统基础结构应该对纯静态数据进行操作,这些数据可以是协议缓冲区、YAML或JSON等格式。这种选择并不意味着用户需要与纯数据进行实际交互。用户可以与生成此数据的高级接口进行交互。然而,这种数据格式可以被API使用,从而允许系统和自动化的进一步堆叠。

这个高级接口几乎可以是任何东西。它可以是一种高级语言,如基于Python的域特定语言(DSL)、Lua或专用构建语言,如Jsonnet(我们将在第15章中详细讨论)。我们可以把这样的接口看作一个编译,类似于我们如何对待C++代码。高级接口也可能根本不是语言其配置由web UI消化。

从有意与静态数据表示分离的配置UI开始,意味着系统具有部署的灵活性。不同的组织可能具有不同的文化规范或产品需求(例如使用公司内的特定语言或需要将配置外部化到最终用户),并且这种通用的系统可以适用于支持不同的配置需求。毫不费力地支持多种语言。参见图14-2。

图14-2。配置流具有单独的配置界面和配置数据基础结构。请注意,Web UI通常还会显示当前配置,从而使关系成为双向关系。

这种分离对于用户来说是完全不可见的。用户的共同路径可能是在配置语言中编辑文件,而所有其他的事情都发生在幕后。例如,一旦用户向系统提交更改,新存储的配置就自动编译成原始数据

静态配置数据一旦获得,也可以在数据分析中使用。例如,如果生成的配置数据是JSON格式的,那么可以将其加载到PostgreSQL中,并用数据库查询进行分析。作为基础架构所有者,您可以快速且容易地查询正在使用哪些配置参数以及由谁使用这些配置参数。此查询对于识别您可以删除的特性或测量bugy选项的影响非常有用。

当使用最终的配置数据时,您会发现存储有关配置如何被摄取的元数据也很有用。例如,如果您知道数据来自Jsonnet中的配置文件,或者您在将数据编译成数据之前已经拥有了原始数据的完整路径,那么您可以跟踪配置作者。

配置语言是静态数据也是可以接受的。例如,您的基础结构和接口都可能使用普通JSON。但是,要避免接口使用的数据格式和内部使用的数据格式之间的紧密耦合。例如,您可以在内部使用包含配置所消耗的数据结构的数据结构。内部数据结构还可能包含完全特定于实现的数据,这些数据永远不需要在系统外部出现。

工具的重要性

工具可以区分混乱的噩梦和可持续的可伸缩的系统,但在设计配置系统时经常会被忽略。本节讨论优化配置系统应该有的关键工具。

语义验证

虽然大多数语言提供现成的语法验证,但不要忽视语义验证。即使您的配置在语法上有效,它是否可能做有用的事情?或者用户是否引用了一个不存在的目录(由于打字错误),或者需要比实际数据库多一千倍的RAM(因为单位不是用户所期望的)?

尽可能验证配置在语义上是否有意义,有助于防止中断并降低运营成本。对于每个可能的错误配置,我们应该问自己,在用户提交配置时是否可以阻止它,而不是在提交更改之后。

配置语法

虽然能够确保配置满足用户需求,但消除机械障碍也很重要。从语法角度来看,配置语言应该具备一下特点:

在编辑器中高亮显示语法(在公司内使用)

通常,您已经通过重用现有语言解决了这个问题。但是,特定语言可能具有额外的语法方式,从特定的方面突出显示 。

短绒

使用linter来识别语言使用中的常见不一致。 Pylint是一种流行的语言示例。

自动语法格式化

内置标准化可最大限度地减少关于格式化的讨论,并在贡献者切换项目时减少认知负荷。标准格式化还可以实现更轻松的自动编辑,这在大型组织中很有用。现有语言中的autoformatters示例包括clang-format和autopep8。

这些工具使用户能够轻松地编写和编辑配置并确保其语法正确。在面向空白的配置中进行正确的缩进会产生很大的优势。

权限和变更跟踪

由于配置可能会影响公司和机构的关键系统,因此确保良好的用户隔离以及了解系统中发生的变化非常重要。如第10章所述,有效的死后文化可以避免责怪个人。但是,它既可以在事件期间进行,也可以在进行事后调查时知道谁更改了配置,并了解配置更改如何影响系统。无论事故是由于事故还是恶意行为都是如此。

系统的每个配置代码段都应具有明确的所有者。例如,如果使用配置文件,则其目录可能由单个生产组拥有。如果目录中的文件只能有一个所有者,则跟踪谁进行更改会更容易。

版本控制配置,无论其执行方式如何,都允许您及时返回以查看在任何给定时间点配置的内容。将配置文件检入版本控制系统(如Subversion或Git)是当今常见的做法,但这种做法对于Web UI或远程API提取的配置同样重要。您可能还希望在配置和正在配置的软件之间实现更紧密的耦合。通过这样做,您可以避免无意中配置软件中尚不可用或不再支持的功能。

在相关的说明中,将配置和生成的应用程序的更改记录到系统是有用的(有时是必需的)。提交新版本配置的简单操作并不总是意味着直接应用配置(稍后将详细介绍)。如果在事件响应期间怀疑系统配置更改是罪魁祸首,则能够快速确定进行更改的完整配置编辑集非常有用。这样可以实现可靠的回滚,并能够通知其配置受到影响的各方。

安全配置变更申请

如前所述,配置是对系统功能进行大量更改的简单方法,但它通常不经过单元测试甚至不易测试。由于我们希望避免可靠性事件,因此我们应该检查配置更改的安全应用是什么。

要使配置更改安全,它必须具有三个主要属性:

  • 逐步部署,避免全有或全无变化的能力
  • 如果证明存在风险,则可以回滚更改
  • 如果更改导致失控,则自动回滚(或至少是停止进度的能力)

部署新配置时,避免全局一次性推送非常重要。相反,逐步推出新配置 - 这样做可以让您在导致100%中断之前检测问题并中止有问题的推送。这就是Kubernetes等工具使用滚动更新策略更新软件或配置而不是一次更新每个集群的原因之一。(有关相关讨论,请参阅第16章。)

回滚能力对于减少故障持续时间非常重要。回滚有问题的配置可以比临时修复程序更快地缓解故障-对修补程序改进的内在信心肯定较低。

为了能够向前滚动和回滚配置,它必须是密封的。需要可在其密封环境之外进行更改的外部资源的配置可能很难回滚。例如,存储在引用网络文件系统上的数据的版本控制系统中的配置不是密封的。

最后但同样重要的是,在处理可能导致操作员控制突然丧失的变更时系统应特别小心。在桌面系统上,如果用户未确认更改,屏幕分辨率更改通常会提示倒计时并重置。这是因为不正确的显示器设置可能会阻止用户恢复更改。同样,系统管理员通常会意外地将自己防火墙移出他们当前正在设置的系统。

这些原则并非配置所独有,适用于更改已部署系统的其他方法,例如升级二进制文件或推送新数据集。

结论

琐碎的配置更改可能会以极大的方式影响生产系统,因此我们需要刻意设计配置以减轻这些风险。配置设计包含API和UI设计的各个方面,应该是有目的的,而不仅仅是系统实现的副作用。将配置分为哲学和机制有助于我们在设计内部系统时获得清晰度,并使我们能够正确地进行讨论。

应用这些建议需要时间和勤勉。有关我们如何在实践中应用这些原则的示例,请参阅有关Canary Analysis Service的ACM Queue文章。在设计这个实用的内部系统时,我们花了大约一个月的时间来尝试减少强制性问题并为可选问题找到合适的答案。我们的努力创造了一个简单的配置系统。因为它易于使用,所以它在内部被广泛采用。我们已经看不到用户支持的需求 - 因为用户可以轻松了解系统,他们可以放心地进行更改。当然,我们还没有完全消除错误配置和用户支持,我们也没有想到。