《DevOps实践指南》- 读书笔记(三)

Part 3 第一步 :流动的技术实践

第三部分的目标是创建必要的技术实践和架构,从而使开发到运维的工作能够稳定地快速流动,并确保不会造成生产环境的混乱或客户服务的中断。这意味着需要降低在生产环境中部署和发布变更的风险。这一点可以通过一套被称为持续交付的技术实践来实现。

持续交付包括打好自动化部署流水线的基础,确保团队能够使用自动化测试持续验证代码是否处于可部署状态,保证开发人员每天都将代码提交到主干,以及构建有利于实现低风险发布的环境和代码。接下来的几章将重点讨论以下内容 :

  • 为部署流水线奠定基础 ;
  • 实现快速可靠的自动化测试 ;
  • 实现并实践持续集成和持续测试 ;
  • 通过自动化、架构解耦等方式实现低风险发布

这些实践能有效缩短创建类生产环境的前置时间。同时,持续测试可以为每个团队成员快速提供反馈,让小型团队能够安全、独立地开发、测试和向生产环境部署代码,从而使向生产环境的部署和发布成为日常工作的一部分

9. 为部署流水线奠定基础

为了使工作快速可靠地从开发流向运维,应当保证价值流的每个阶段都使用类生产环境。此外,这些环境必须能用自动化的方式进行搭建。在理想情况下,应该使用脚本和存储在版本控制系统中的配置信息按需搭建,而不需要依赖运维团队进行手动操作。部署流水线的目标就是能够基于版本控制系统中的信息重复搭建整套生产环境。

9.1 按需搭建开发环境、测试环境和生产环境

我们不再需要运维团队手动构建和配置环境,而是可以使用自动化的方式完成以下操作:

  • 复制虚拟化环境(如 VMware 虚拟机镜像、执行 Vagrant 脚本,以及启动 Amazon EC2 虚拟机镜像文件);
  • 构建“裸金属物理机”的自动化环境搭建流程(例如,使用 PXE 方式通过基线镜像进行安装);
  • 使用“基础设施即代码”的配置管理工具(例如 Puppet、Chef、Ansible、SaltStack、CFEngine 等);
  • 使用操作系统自动化配置工具(例如 Solaris Jumpstart、Red Hat Kickstart 和 Debian preseed);
  • 使用一组虚拟镜像或容器(例如 Vagrant 和 Docker)搭建环境;
  • 在公有云(例如 Amazon Web Services、Google App Engine 和 Microsoft Azure)、私有云或其他 PaaS(平台即服务,如 OpenShift 和 Cloud Foundry 等)中创建新环境

通过获得完全可控的环境,开发人员能在与生产服务和其他共享资源安全隔离的情况下,快速地重现、定位和修复缺陷。同时,开发人员还可以尝试更改环境和优化创建环境的基础设施代码(例如配置管理脚本),从而进一步在开发和运维之间共享信息。

9.2 应用统一的代码仓库

通过上一阶段的工作,我们已经能够按需创建开发环境、测试环境和生产环境。接下来必须保证软件系统的所有部分都正常工作

版本控制系统记录了对系统中的文件或文件集合所做的变更。这些文件可以是源代码、资源文件或软件开发项目的其他文档。一组变更构成一次提交,也称修订。每个修订版本及其元数据(例如谁在什么时间进行了更改)都以某种方式存储在系统中,从而让我们可以进行提交、对比、合并以及从仓库中还原出以前修订版本的对象。版本控制系统还能通过把生产环境中的对象回滚到之前版本来降低风险。

为了确保即使在发生灾难性事故时,也可以重复且精确地(最好还能快速地)恢复生产环境,必须把下列资源也纳入版本控制系统:

  • 应用的所有代码和依赖项(例如库、静态内容等);
  • 任何用于创建数据库模式的脚本、应用的参考数据等;
  • 上一节描述的所有用于搭建环境的工具和工件(例如 VMware 或 AMI 虚拟机模板、Puppet或 Chef 配置模块等);
  • 任何构建容器所使用的文件(例如 Docker 或 Rocket 的定义文件和 compose 文件等);
  • 所有支持自动化测试和手动测试的脚本;
  • 任何支持代码打包、部署、数据库迁移和环境置备的脚本;
  • 所有项目工件(例如需求文档、部署过程、发布说明等);
  • 所有云平台配置文件(例如 AWS CloudFormation 模板、Microsoft Azure Stack DSC 文件,以及 OpenStack HEAT 模板文件);
  • 创建支持多种基础设施服务(例如企业服务总线、数据库管理系统、DNS 区域文件、防火墙配置规则和其他网络设备)所需的任何其他脚本或配置信息。

仅能重现生产环境之前的状态是不够的,还必须能够重现整个预生产环境和构建过程。因此,需要把构建过程所依赖的一切也都纳入版本控制系统,这包括所用工具(例如编译器和测试工具)及其所依赖的环境。

实际上,几乎在所有情况下,环境的可配置参数都要比代码的可配置参数多出好几个量级。所以,环境最需要使用版本控制。

9.3 使基础设施的重建更容易

当我们能按需快速地重建应用和环境时,一旦出现问题,便可以快速进行构建,而不必花时间修复。

通过重复创建环境,我们能够将更多的服务器添加到资源池,从而轻松地增加容量(即水平扩容)。同时,也避免了当不可再现的基础设施发生灾难性故障后必须恢复服务的痛苦。这些灾难性故障通常是由多年来无记录的手动变更引发的。

为了确保环境的一致性,所有对生产环境的变更(配置变更、打补丁、升级等)都需要被复制到所有的预生产环境以及新搭建的环境中。

可以依靠自动化配置管理系统保证一致性(例如 Puppet、Chef、Ansible、Salt、Bosh 等),也可以通过自动化构建机制,创建新的虚拟机或容器,将其部署到生产环境,再销毁或移除旧资源。

后一种模式被称为不可变基础设施,即生产环境不再允许任何手动操作。变更生产环境的唯一途径是把变更先检入版本控制系统,然后从头开始重新构建代码和环境。这样做杜绝了差异蔓延到生产环境中的可能性。

为了杜绝不受控制的配置差异,可以禁止远程登录生产服务器,或定期删除和替换生产环境中的实例,从而确保移除手动变更。这会促使所有人都通过版本控制系统用正确的方式进行变更。这些措施能系统地减少基础设施偏离已知良好状态的可能性(例如出现配置漂移、脆弱的工件、摆设、雪花服务器等)。

此外,必须保证预生产环境是最新的,特别是要让开发人员使用最新的环境。

9.4 运行在类生产环境里才算“完成”

现在我们已经可以按需搭建环境,而且一切都处于版本控制之下。接下来的目标,是确保开发团队在日常工作中使用这些环境。需要在离项目结束还有很长一段时间时,或在首次向生产环境部署前,就确认应用能在类生产环境中正常运行。

我们的目标,是在整个项目中,确保开发和 QA 能日益频繁地把代码与类生产环境常规性地集成起来。我们通过扩展“完成”的定义来实现这一点。“完成”是指不仅实现了功能正确的代码,而且在每个迭代周期结束时,已经在类生产环境中集成和测试了可工作和可交付的代码。

通过让开发团队和运维团队共同掌握代码和环境互动的方式,并尽早频繁地实施代码部署,生产环境的部署风险得以显著降低。这也避免了在项目的最后时刻才发现架构问题,并完全消除了这一类安全隐患

9.5 小结

构建从开发到运维的快速工作流,需要确保任何人都能按需获得类生产环境。通过让开发人员在软件项目的最初阶段就使用类生产环境,可以显著降低生产环境出现问题的风险。这也是证实运维能提高开发效率的诸多实践之一。通过扩展“完成”一词的定义,规定开发人员在类生产环境中运行代码。

此外,通过把所有生产工件纳入版本控制系统,我们有了“唯一的事实来源”,这使我们能够用快速、可重复和文档化的方式重新搭建整个生产环境,并在运维工作中采用和开发工作一致的实践。通过使基础设施的重建比修复更容易,我们能够更轻松、更快速地解决问题,团队产能也更容易提升。

10. 实现快速可靠的自动化测试

在日常工作中,开发人员和 QA 人员使用类生产环境运行应用。对于每个特性而言,代码都已经在类生产环境中集成和运行,而且所有变更都已经提交到版本控制系统。但是,如果等到所有的开发工作完成之后再由单独的 QA 部门通过专门的测试阶段发现和修复错误,那么结果往往并不理想。而且,如果每年只能进行几次测试,那么开发人员就只能在引入变更的几个月后,才知道他们所犯的错误。到那时,很难查清问题的原因,而开发人员不得不急着解决问题,这极大地削弱了他们从错误中学习的能力。

自动化测试解决了一个重要且令人不安的问题。Gary Gruver 说:“如果没有自动化测试,那么我们编写的代码越多,测试代码所花费的时间和金钱也会越多。在大多数情况下,这种商业模式对于任何技术组织而言都是无法扩展的。”

10.1 对代码和环境做持续构建、测试和集成

我们的目标是让开发人员在日常工作中创建自动化测试套件,并在开发早期就保证产品质量。这样做有利于建立快速的反馈回路,帮助开发人员尽早发现问题,并在约束(例如时间和资源)最少时快速解决问题。

创建自动化测试套件的目的是提高集成频率,使测试从阶段性活动演变成持续性活动。通过搭建部署流水线(见图 10-1),当新的变更进入版本控制系统时,就会触发一系列自动化测试。
在这里插入图片描述
部署流水线确保所有检入版本控制系统的代码都是自动化构建的,并在类生产环境中测试过。这样一来,当开发人员提交代码变更后,立即就能获得关于构建、测试或集成错误的反馈,从而使开发人员能够立刻修复这些错误。正确的持续集成实践总是可以确保代码处于可部署和可交付的状态。

为了实现这一点,必须在专用环境中创建自动化构建和测试流程。这样做至关重要,原因如下。

  • 在任何时候,构建和测试流程都能够运行,无论工程师的个人工作习惯如何。
  • 独立的构建和测试流程确保工程师能理解构建、打包、运行和测试代码所需的全部依赖项(即消除“应用在开发人员的笔记本电脑上能运行,但是在生产环境中不行”的问题)。
  • 将应用的可执行文件和配置打包,并可以在环境中重复安装(例如 Linux 上的 RPM、yum和 npm 或 Windows 上的 OneGet,也可使用开发框架特定的打包格式,如 Java 的 EAR 和WAR 文件,或 Ruby 的 gem 文件)。
  • 将应用打包到可部署的容器中(例如 Docker、Rkt、LXD 和 AMI),而不是把程序代码打包。
  • 以一致、可重复的方式进行类生产环境的配置(例如从环境中移除编译器,关闭调试标志等)

部署流水线的目的是给价值流中的所有成员(特别是开发人员)提供尽可能快速的反馈,帮助他们及时识别可能让代码偏离可部署状态的变更,包括代码、环境因素、自动化测试甚至部署流水线基础设施(例如 Jenkins 的设置)的任何改变。

有了部署流水线基础设施之后,还必须有持续集成实践,这需要以下 3 个方面的配合:

  • 全面且可靠的自动化测试套件,用于验证可部署状态;
  • 一种在验证测试失败时,可以“停掉整条生产线”的文化;
  • 开发人员在主干上工作,并小批量提交变更,而不是在生命周期很长的特性分支上工作

10.2 构建快速可靠的自动化测试套件

每当有新的变更检入版本控制系统时,就需要在构建和测试环境中运行快速的自动化测试。通过这种方式,可以像谷歌的 GWS 团队那样,立刻发现和解决所有集成问题。这样就能维持较小的代码集成量,并保证代码始终处于可部署状态。

通常,自动化测试从快到慢分为如下几类

  • 单元测试:通常独立测试每个方法、类或函数。它的目的是确保代码按照开发人员的设计运行。由于诸多原因(如需要进行快速和无状态的测试),通常会使用打桩(stub out)的方式,隔离数据库和其他外部依赖(例如,把函数修改为返回静态的预定义值,而不是调用数据库)。
  • 验收测试:通常整体测试应用,确保各个功能模块按照设计正常工作(例如符合用户故事的业务验收标准,API 能正确调用),而且没有引入回归错误(即没有破坏以前正常的功能)。Jez Humble 和 David Farley 认为单元测试和验收测试的区别在于:“单元测试的目的是证明应用的某一部分符合程序员的预期……验收测试的目的则是证明应用能满足客户的愿望,而不仅仅是符合程序员的预期。”在构建的版本通过单元测试后,部署流水线就对其执行验收测试。任何通过验收测试的构建版本通常都可用于手动测试(例如探索性测试、用户界面测试等)和集成测试。
  • 集成测试:保证应用能与生产环境中的其他应用和服务正确地交互,而不再调用打桩的接口。Jez Humble 和 David Farley 写道:“大部分系统集成测试工作都是在部署应用的新版本,并使它们能正常协作。在这种情况下,冒烟测试通常是指针对整个应用进行的一组成熟的验收测试。”只有通过了单元测试和验收测试的构建版本才能执行集成测试。因为集成测试通常是脆弱的,所以应该尽量减少集成测试的次数,并且要在单元测试和验收测试期间,尽可能多地找出缺陷。一个至关重要的架构需求是,在执行验收测试时能够调用虚拟或模拟的远程服务。

当面对项目最后期限的压力时,无论“完成”的定义如何,开发人员都可能不再在日常工作中编写单元测试。为了发现并杜绝这种情况,需要度量测试覆盖率(取决于类数、代码行数、排列组合等),还要把度量结果可视化,甚至可以在测试覆盖率低于一定水平时(例如当类的单元测试率不足 80%时)使测试套件的验证结果显示失败。

10.2.1 在自动化测试中尽早发现错误

自动化测试套件的一个设计目标是能尽早地在测试中发现错误。因此,要在执行那些耗时的自动化测试(如验收测试和集成测试)之前,执行完速度更快的自动化测试(如单元测试)。这两种测试都要先于手动测试执行。

因此,每当验收测试或集成测试发现一个错误,就应该编写相应的单元测试,以便更快、更早、更廉价地识别这个错误。Martin Fowler 描述过“理想的测试金字塔”这一概念,即使用单元测试捕获大部分错误,如图 10-2 所示。相比之下,许多测试项目恰恰相反,人们把大部分时间和精力都花在手动测试和集成测试上。
在这里插入图片描述
如果编写和维护单元测试或验收测试既困难又昂贵,说明架构可能过于耦合,即各个模块之间不再有(或者从来就没有)明显的边界。在这种情况下,需要构建更松散耦合的系统,使模块可以不依赖于集成环境进行独立测试。即使对于最复杂的应用,也可以在几分钟内完成验收测试。

10.2.2 尽可能并行地快速执行测试

我们希望能快速地执行测试,所以需要设计并行测试,这可能会用到多台服务器。我们还想并行地运行不同类型的测试,例如,当某次构建通过验收测试后,就可以并行地执行安全测试和性能测试,如图 10-3 所示。在构建版本通过所有自动化测试之前,手动的探索性测试可以做,也可以不做(探索性测试可以加快反馈速度,但也可能针对最终会失败的构建版本进行手动测试)
在这里插入图片描述
任何通过所有自动化测试的构建版本都可用于探索性测试以及其他形式的手动测试或资源密集型测试(如性能测试)。应该尽可能频繁和全面地执行所有这些测试,要么持续执行,要么定期执行。

10.2.3 先编写自动化测试

要确保自动化测试可靠,最有效的一个方法是通过测试驱动开发(Test-Driven Development,TDD)和验收测试驱动开发(Acceptance Test-Driven Development,ATDD)等技术在日常工作中编写自动化测试。

Kent Beck 在 20 世纪 90 年代末将 TDD 作为极限编程的一部分提了出来。这项技术共有以下3 个步骤

  1. 确保测试失败,“为想要增加的功能编写测试用例”,检入测试用例;
  2. 确保测试通过,“编写实现功能的代码,直到测试通过”,检入代码;
  3. “重构新旧代码,优化结构”,确保测试都能通过,再次检入代码。

自动化测试套件和程序代码一同被检入版本控制系统,以提供一套可用且最新的系统规范。如果开发人员想了解如何使用系统,可以查看测试套件,找到演示如何调用系统 API 的示例。

10.2.4 尽量将手动测试自动化

自动化测试的目的是尽可能多地发现代码错误,并且减少对手动测试的依赖。
“虽然测试可以自动化,但是质量的创造过程不可以。让人类去执行那些本应该自动化执行的测试是在浪费人类的潜能。”

通过执行自动化测试,所有测试人员(当然包括开发人员)得以去做那些不能被自动化的高价值活动,如探索性测试或优化测试流程本身。然而,单纯地将所有手动测试自动化,可能产生不良后果——谁都不希望自动化测试不可靠或出现误报(即因为代码正确,所以测试本应该通过,可是由于性能不佳、超时、不受控的启动状态,或者因为使用了数据库打桩或共享的测试环境而导致的非预期状态,使得测试失败)。

换言之,应该从少量可靠的自动化测试开始,并随着时间的推移不断增加。这样一来,系统的保障级别随之提高,并能快速检测出所有让代码偏离可部署状态的变更。

10.2.5 在测试套件中集成性能测试

在集成测试期间或者应用部署到生产环境之后,我们经常会发现应用的性能不佳。性能问题往往很难检测,性能可能随着时间的推移逐渐变差,在发现问题时早就为时已晚(例如没有索引的数据库查询)。而且,很多问题都难以解决,尤其是当问题源于以前所做的架构决策时,或者源于之前没有发现的网络、数据库、存储或其他系统的限制时,更是如此。

编写和执行自动化性能测试的目标是验证整个应用栈(代码、数据库、存储、网络、虚拟化等)的性能,并把它作为部署流水线的一部分,这样才能尽早发现问题,并以最低的成本和最快的速度解决问题。

如果能了解应用和环境在类生产负载下的表现,就可以做出更好的容量规划,以及检测出如下情况:

  • 数据库查询时间非线性增加(例如忘记为数据库创建索引,导致页面加载时间从 100 毫秒增加为 30 秒);
  • 代码变更导致数据库调用次数、存储空间使用量或者网络流量增加数倍。

10.2.6 在测试套件中集成非功能性需求测试

除了测试代码并验证它符合预期且能在类生产负载下正常运行,还需要验证系统的其他质量属性。这些质量属性通常被称为非功能性需求,包括可用性、可扩展性、容量以及安全性等。

许多非功能性需求是通过正确配置环境实现的,因此必须编写相应的自动化测试,用于验证环境搭建和配置的正确性。例如,应该保证以下几项的一致性和正确性,这是很多非功能性需求所依赖的(例如安全性、性能和可用性)

  • 所使用的应用、数据库和软件库等;
  • 编程语言的解释器和编译器等;
  • 操作系统(例如启用审核日志记录等);
  • 所有依赖项

当使用基础设施即代码的配置管理工具时(例如 Puppet、Chef、Ansible、SaltStack 或 Bosh),可以用测试代码时所用的框架测试环境是否正确配置及正常运行(例如将环境测试编写成Cucumber 或者 Gherkin 测试)。

此外,与在部署流水线中针对应用进行代码分析一样(如静态代码分析和测试覆盖率分析),还要用工具(例如 Chef 的 Foodcritic 或 Puppet 的 puppet-lint)对构建环境的代码进行分析。还应该把所有安全性加固检查作为自动化测试的一部分,以保证所有相关配置都是正确的(例如服务器规格)

在任何时候,自动化测试都能够验证代码处于可部署状态。务必建立安灯绳机制,以便在部署流水线失败时,能够立刻采取一切必要措施将构建版本恢复到绿色状态。

10.3 在部署流水线失败时拉下安灯绳

当构建版本在部署流水线中处于绿色状态时,我们就可以放心地将代码变更部署到生产环境中。

为了让部署流水线始终保持绿色状态,务必创建虚拟的安灯绳,它类似于丰田生产系统中的那个物理装置。一旦某个开发人员提交的代码变更导致构建或自动化测试失败,在这个问题被解决之前,不允许提交任何新的变更。如果有人在解决问题时需要帮助,他们能获取任何所需资源,就像本章开头描述的谷歌的案例那样。

当部署流水线失败时,至少要告知整个团队。所有人要么都去一同解决问题,要么回滚代码,甚至可以把版本控制系统配置为拒绝后续的代码提交,直到部署流水线的第一阶段(即构建和单元测试)恢复成绿色状态。如果问题源于自动化测试产生的误报,那么应该重写或删除该测试。团队的所有成员都应该有权限进行回滚操作(笔者持怀疑态度),以便使部署流水线恢复到绿色状态。

当部署流水线的后期阶段(例如验收测试或性能测试)失败时,不应停止所有新工作,而应让一部分开发人员和测试人员随时待命,他们负责在问题发生时立即加以解决。这些开发人员和测试人员还应在部署流水线的早期阶段执行新的测试,用来捕获这些问题引入的回归错误。例如,如果在验收测试中发现一个缺陷,就应该编写一个单元测试来捕获这个问题。同样,如果在探索性测试中发现缺陷,就应该编写对应的单元测试或验收测试。

从某个角度来说,这个步骤比进行构建和测试服务器更具有挑战性——那些是纯粹的技术活动,而这个步骤需要改变人的行为和提供激励机制。

为何拉下安灯绳
如果不拉下安灯绳,也不立即解决部署流水线的问题,就会导致应用和环境更难恢复到可部署状态。想想以下情况

  • 有人提交的代码造成构建或自动化测试失败,但没有人修复。
  • 其他人在已经失败的构建版本上又提交了一份代码变更。这当然无法通过自动化测试,但没有人看到这些有助于发现新缺陷的测试结果,更不用说修复了。
  • 现有的测试都不能可靠地执行,因此不太可能编写新的测试用例。(为何较这个劲?连当前的测试都不能通过。)

10.4 小结

在本章中,我们创建了一组全面的自动化测试,用来确保构建始终处于绿色的可部署状态。我们在部署流水线中组织好了测试套件和测试活动,还建立了规范,要求无论谁的代码变更导致自动化测试失败,大家都要竭尽全力地将系统恢复到绿色状态。

这种方式为持续集成奠定了基础,使很多小型团队能够独立、安全地开发、测试和部署代码,从而向客户交付价值

猜你喜欢

转载自blog.csdn.net/u010230019/article/details/132729753
今日推荐