舒缓疼痛:将自动化应用到集群上线中
十年前,集群基础设施SRE团队似乎每隔几个月都要雇用新人。事实上,大约与我们新集群上线的频率相同。因为在新集群中启动新的服务能够让新员工接触到服务内部的信息,这个任务似乎是一个自然的和有效的培训工具。
为使一个集群达到可用状态所需采取的步骤如下:
1.给数据中心大楼装配电源和冷却设备。
2.安装和配置核心交换机和与主干网的连接。
3.安装几个初始的服务器机架。
4.配置一些基本服务,例如DNS和安装器(installer),然后配置Chubby锁服务、存储服务和计算服务。
5.部署剩余的机架。
6.给面向用户的服务分配资源,使其团队可以建立服务。
第4步和第6步是极为复杂的。虽然基本的服务,如DNS是相对简单的,但存储和计算子系统当时正在全力开发中,所以每周都会增加新的功能开关、组件和优化。
一些服务有超过一百个不同的组件子系统,每一个都具有复杂的网状依赖。某个子系统的配置失败,或者是没有按照其他集群来配置一个系统或组件,将造成潜在的故障发生。
在一个案例中,一个存有数个PB的Bigtable集群因为延迟原因被配置为不使用12个磁盘系统中的第1个(日志型)磁盘。一年后,一些自动化程序假设如果一台机器的第一个磁盘没有被使用,这台机器就不被配置任何存储服务;因此,可以安全地清除数据。这导致所有的Bigtable数据立刻被清除了。幸好我们的数据有多个实时备份,但是这种意外也是很糟糕的。自动化应该对那些隐含的“安全”信号非常小心。
早期的自动化关注于加速集群交付。这种方法往往依靠“有创意”地使用SSH来应对繁琐的包分发和服务初始化问题。采用这种战略一开始很成功,但是这些格式自由的脚本逐渐堆积形成了技术债务。
使用Prodtest检测不一致情况
随着集群的数量增长,一些集群需要手工调整功能开关和配置。这样做的结果是,团队在追逐难以发现的错误上浪费了越来越多的时间。如果某个使GFS对日志处理响应更快的开关泄露到了默认模板中,文件很多的集群就可能在负载之下内存超标。每次大型配置改变时,令人恼怒和极为耗时的错误都会偷偷潜入。
我们用来配置集群的、有创造性的—但是脆弱的—shell脚本既无法适应需要修改该脚本的人的数量增加,也不能适应需要构建的集群的大量组合形式。这些shell脚本同时在宣布服务状态良好,可以承受用户的流量的问题上也未能解决一些显著问题,比如:
● 是否所有服务的依赖关系都可用,并且正确地配置了?
● 所有的配置和包都与其他部署一致吗?
● 团队是否能够确认配置中的每个例外都是合理的?
Prodtest(生产测试)是对这些问题的一个巧妙的解决方案。我们对Python单元测试框架进行了扩展,使其可以用来对实际服务进行单元测试。这些单元测试有依赖关系,允许进行链条式测试,一个测试中出现的故障可以很快中止整个测试。以图7-1所示的测试为例。
某个团队的Prodtest 执行时,会传入该集群的名字,这样它可以验证该集群中的服务。后来增加功能可以让我们生成一个有关单元测试和它们状态的图表。这个功能使工程师能够更快看到他们的服务是否在所有集群中都被正确配置了,如果没有,为什么没有。图中突出了出错的步骤,以及出现故障的单元测试会输出的更详细的错误信息。
图7-1:DNS服务的 ProdTest,展示了一个测试出现问题后如何中止之后的一系列测试。
每当一个团队遇到因另一个团队意外错误配置而导致的延迟时,可以提交一个Bug来扩展Prodtest。这确保了类似问题以后可以尽早发现。SRE能够非常自豪地向用户保证,他们的所有服务—不管是配置的新服务还是现有服务—都可以可靠地服务生产业务。
我们的项目经理第一次可以准确地预测一个集群的“上线的时间”了,同时可以对为什么每个集群从“网络就绪”到“服务线上流量”需要6个星期甚至更长时间有了一个完整的理解。毫无准备的,SRE突然从管理高层收到任务:在3个月内,有5个新的集群将在同一天进入网络就绪的状态。请在一周内完成上线。
幂等地解决不一致情况
“一周内上线”是一个很恐怖的任务。我们目前有十几个团队编写出的几万行的shell脚本。虽然我们能够快速分析任何一个集群的问题所在,但是修复它意味着十几个团队不得不提交数百个Bug,接着我们就只能希望这些Bug能被及时地解决。
我们意识到,从“Python单元测试发现错误配置”到“Python代码修复错误配置”的演进能够使我们更快解决这些问题。
单元测试已经能够指出被检查的集群中哪个测试失败,我们就给每个测试增加了一个修复程序。如果每一个修复程序都具有幂等性,并且假设所有的依赖关系都得到满足,那么修复问题就是简单而安全的。强调修复程序的幂等性意味着团队可以每15分钟运行一次他们自己的“修复脚本”,而不用担心对集群配置造成损害。如果DNS团队的测试在等待机器数据库团队对新集群的配置,只要该集群出现在数据库中,DNS团队的测试和修补程序就会立刻开始工作。
以图7-2 展示的测试为例。如果TestDNSMonitoringConfigExists失败,我们可以调用FixDNSMonitoringCreateConfig,这样会从一个数据库中将配置去掉,然后将一个骨架配置文件提交到版本控制系统中。接下来重试TestDNSMonitoringConfigExists成功,然后就可以进行TestDNSMonitoringConfigPushed测试。如果测试失败,就运行FixDNSMonitoringPushCon☆g。如果一个修复程序失败多次,自动化系统假设这个修补程序失败,并且会停止进行,通知用户修复。
图7-2:DNS 服务的ProdTest,体现了一个失败测试的后果仅仅是多运行一个修复程序。左侧是测试,右侧是对应的修复程序。
有了这些脚本后,一小组工程师可以保证我们能够在一个或两个星期内从“网络就绪以及机器在数据库中存在”发展到“服务网络搜索和广告流量的1%流量”。在当时,这似乎达到了自动化技术的巅峰。
然而,现在回头来看,这种方式本身是存在严重缺陷的;在测试、修复、再测试之间的延迟引入了“不稳定”的测试,时好时坏。并不是所有的修复程序都具有幂等性,所以一个“不稳定”的测试引起的一次修复可能造成系统处于不一致的状态。
专业化倾向
自动化程序的不同体现在三个方面:
● 能力,即准确性。
● 延迟,开始执行后,执行所有步骤需要多久。
● 相关性,自动化所涵盖的实际流程比例。
最开始,我们的流程有如下特点:能力强(由服务的所有者维护和运行)、高延迟(服务拥有者在他们的业余时间执行流程或者分配给新工程师来做),并且相关性高(服务拥有者知道现实世界已经改变并且能够修复自动化)。
为了减少集群上线的延迟,许多服务团队指导一个单独的“集群上线”团队来运行自动化。这个团队使用工单来启动每个阶段,这样我们就能够跟踪余下的任务,以及这些任务分配给了谁。如果关于自动化模块之间的人际互动发生在同一个房间里的人与人之间,集群上线就可以在更短的时间内发生。最后,我们的流程变成了能力强的、精确的,以及及时的自动化流程!
但是这个状态没有持续太久。现实世界是混乱的:软件、配置、数据等都发生了变化,这导致受影响的系统每天产生超过1000个独立变化。最受自动化Bug所影响的人不再是领域专家,因此自动化的相关性下降了(这意味着新的步骤被遗漏了),能力减弱了(新的功能开关可能导致自动化失败)。然而,这种质量的下降一段时间之后才会影响速度。
自动化代码和单元测试代码一样,当维护团队不再关心它与它所覆盖的代码仓库同步的时候就会逐渐死去。整个世界都在围绕代码改变:DNS的团队增加了新的配置选项,存储团队改变了他们的包名,网络团队需要支持新的设备。
通过消除运维相关服务的团队维护和运行自动化代码的责任,我们创造了一个不合理的组织激励机制:
● 某个最主要任务是加速现存的集群上线的团队是没有动力去减少服务运维团队在生产流程后期运维服务产生的技术负债的。
● 一个不亲自运行自动化的团队是没有动力去建设一个能够很容易自动化的系统的。
● 一个产品经理的时间表如果不受低质量的自动化影响,他将永远优先新功能的开发,而不是简化和自动化。
最可用的工具通常是由那些每天使用它们的人写成的。类似的论点是,产品研发团队将会在保留一些生产运维知识的过程中受益。
集群上线流程再一次成为了高延迟的、不准确的、低能力的流程—最糟糕的结果。然而,一个不相关的安全方面的任务使我们摆脱了这个陷阱。当时,许多分布式自动化都依赖于SSH。从安全的角度来看,这是很笨拙的,因为人们必须拥有机器的root权限才能运行大多数的命令。越来越多的人意识到,先进、持续的安全威胁迫使我们将SRE的特权降到最低,仅仅满足完成其工作所需的最低的特权。我们不得不用一个支持认证、ACL驱动,以及基于RPC的本地管理进程来取代sshd,这被称为Admin服务器,它拥有本地执行更改权限。这样一来,没有人可以绕过审计跟踪来安装或是修改服务器。对Admin服务器代码和Package仓库的修改通过代码评审来把关,这使得越权操作非常困难;赋予别人安装软件包的权限不会允许他们查看日志。Admin服务器会记录RPC请求者、全部参数,以及所有的RPC的结果,以提高调试和安全审计功能。
以服务为导向的集群上线流程
在下一次迭代中,Admin 服务器成为服务团队的工作流程的一部分,这包括作为机器管理的Admin 服务器(安装包和重新启动服务器),以及集群级别的Admin 服务器(如进入排水模式,或者服务上线)。SRE从在自己的主目录里维护shell脚本迁移到了构建评审过的RPC服务器与细粒度的ACL上。
后来,在认识到上线流程必须由服务团队维护之后,我们找到了一种以服务为导向的架构(SOA)来解决集群上线问题的方法:服务拥有者将负责创建一个Admin 服务器,处理系统在集群就绪之后发出的集群上线/下线RPC。反过来,每个团队将按照合同(API)提供自动上线所需的自动化,然而仍然可以随意改变底层实现细节。随着一个集群进入“网络就绪”,自动化系统将给每个负责上线的Admin 服务器发送RPC。
我们现在拥有的是低延迟、能力强以及非常精确的流程;更重要的是,这个流程在变更率、团队的数量,以及服务的数量每年翻一倍的情况下仍然保持可靠。
如前所述,集群上线自动化进化遵循这样一个路径:
1.操作人员触发手动操作(无自动化)。
2.操作人员编写,系统特定的自动化。
3.外部维护的通用自动化。
4.内部维护,系统特定的自动化。
5.不需要人为干预的自治系统。
虽然这种演变宽泛地说是成功的,但Borg的案例研究描述了我们考虑自动化问题的另一种方式。