持续交付读书笔记
持续交付:发布可靠软件的系统方法#
Jez Humble; David Farley
第 2 章 配置管理#
当评估第三方产品或服务时,应该问自己如下问题。我们可以自行部署它吗?我们能对它的配置做有效的版本控制吗?如何使它适应我们的自动化部署策略?
第 3 章 持续集成#
持续集成是一种实践,不是一个工具,它的有效性依赖于团队纪律。
还应该考虑把重构作为高效软件开发的基石。
每次向该代码库提交代码时,就尝试让它与指定的主库合并,并运行构建。
第 6 章 构建与部署的脚本化#
环境管理的核心原则之一就是:对测试和生产环境的修改只能由自动化过程执行。 也就是说,我们不应该手工远程登录到这些环境上执行部署工作,而应该将其完全脚本化。
交付团队的确应该花上一些时间和精力写正确的构建和部署脚本。这不是让团队中的实习生拿来练手的东西。
第 7 章 提交阶段#
开发人员和运维人员都必须要习惯构建系统的维护工作,而且要对其负责。
提交测试中,绝大部分应由单元测试组成。单元测试最重要都特点是运行速度非常快。有时候,我们会因为测试套件运行不够快而令构建失败。 第二个重要的特点是它们应覆盖代码库的大部分(经验表明一般为 80%左右)
第 8 章 自动化验收测试#
在迭代交付方法中,分析人员会花大量时间定义验收条件。团队用这些验收条件来评判某个具体需求是否被满足。 最开始,分析人员会与测试人员和客户紧密合作,定义验收条件。在这个阶段,鼓励分析人员和测试人员协作不仅对双方都有利,并且能使流程更加有效。 分析师会有所收获,因为测试人员会根据他们的经验提供一些信息,比如哪些事情可能或应该用于定义用户故事是否做完了。 而测试人员在测试这些需求之前,就能获得对这些需求本质的理解。
一旦验收条件定义完成,在开始实现这个需求之前,分析人员、测试人员应该和将要实现这个需求的开发人员碰一下头儿(有客户在就更好了)。 分析人员讲解需求,以及它的业务上下文,并检查一遍验收条件。 然后,测试人员和开发人员讨论,并就“实现哪些自动化验收测试来证明验收条件被满足”达成一致。
这个简短的碰头会是迭代交付过程中的一个重要组成部分,确保实现需求的每一方都能很好地理解需求以及他们在交付过程中的角色。 这种方法可以避免分析人员创建那种难以实现或测试的“象牙塔”式需求,也避免测试人员由于自己对系统的错误理解,把正常的系统行为当成缺陷写在报告里,还可以避免开发人员实现一些不相干的、客户并不想要的功能。
但是他们常常希望在代码上开很多秘密的后门,用于验证结果。这就不对了。 正如我们所说的,自动化测试会给你压力,让你的代码更趋向于模块化和更好的封装性。 但是如果你通过破坏封装性让它变得可测试,那么通常就会错过达到同一目的的好方法。
就单元测试来说,在单个测试范围之内,应该避免所有异步情况,也要避免跨越测试边界的情况。后者会引起难以发现的偶然性测试失败。
由于在不同软件项目间作出有意义的对比是非常困难的(并非不可能),所以,即使用自动化验收测试会得到数倍的收益,我们也很难为你提供任何数据支持我们下面这个断言。 我们只能向你保证,虽然在我们参与的项目中,确保验收测试持续运行是一项很困难的工作,而且带来了一些复杂问题,但是,我们从来没有后悔使用验收测试。
第 9 章 非功能需求的测试#
因此,软件工程协会( Software Engineering Institute)的 ATAM( Architectural Tradeoff Analysis Method,架构权衡分析方法)就是通过对系统非功能需求(称为“质量属性”)进行完整分析,帮助团队选择一种合适的架构。
容量测试阶段的关键在于,它要告诉我们是否存在问题,以便我们可以修复它。不要枉自猜测,而要先进行度量。
这种测试扮演着性能冒烟测试的角色,它的目的并不是为了验证应用程序满足所有的性能要求,而是起到错误趋势上的警示作用,以便在性能出现问题之前就可以处理。
第 10 章 应用程序的部署与发布#
客户端处理升级方式由如下几种方式:
- 让软件自己检测是否有新版本,并提示用户下载并升级到最新版本
- 在后台下载,并提醒用户安装
- 在后台下载并在应用程序下次启动时悄悄升级
如果你比较保守的话,选项(1)和(2)可能看起来更有吸引力。 然而,在大多数情况下,这是一个错误的选择。作为应该程序的开发人员,你希望让用户有更多的选择。 可是,对于升级这件事而言,用户可能并不了解为什么他需要推迟升级。 如果你没有提供什么有意义的信息,还让他们考虑是否需要升级的话,其结果通常是用户选择不升级,仅仅因为升级可能会引起问题。
为了提供一个坚如磐石的升级体验,你需要处理二进制包、数据和配置信息的迁移工作。 无论哪种情况,升级过程应该保留一份旧版本的副本,直至完全确信升级已经成功。 如果升级失败,应该悄悄地恢复二进制包、数据和配置信息。 一种比较容易的方法是在安装目录中让一个文件夹包含当前版本的所有信息,并创建一个新文件夹用于保存新版本的所有信息。 然后只要通过重命名目录或创建新版本的一个引用就可以了(在UNIX系统中,通常是使用符号链接做到这一点)。
应该把对升级过程的测试也作为部署流水线的一部分。 可以在部署流水线为这个目的专门设置一个阶段。 在该阶段中,脚本选择基于真实数据和配置信息的初始状态(这些真实数据和配置信息来自于那些非常友好的用户),运行升级过程达到最新版本。 这些活动应该在具有代表性的目标环境中自动完成。
第 11 章 基础设施和环境管理#
强调合作是 DevOps运动的核心原则之一。 DevOps运动的目标是将敏捷方法引入到系统管理和 IT运营世界中。 这场运动的另一个核心原则是,利用敏捷技术对基础设施进行有效管理。
缺乏经验的开发人员最常犯的一个编码错误就是吞噬错误信息( swallow error)。
与交付流程的其他方面一样,你应该把创建和维护基础设施需要的所有内容都进行版本控制。至少对下述内容应该这么做。
- 操作系统的安装定义项(比如使用的Debian Preseed、RedHat Kickstart和Solaris Jumpstart)。
- 数据中心自动化工具的配置信息,比如Puppet或CfEngine。
- 通用基础设施配置信息,比如DNS 区域文件(zone file)、DHCP和SMTP服务器配置文件、防火墙配置文件等。
- 用于管理基础设施的所有脚本
当处理基础设施时,需要重点考虑的一个因素是共享到什么程度。 如果某些基础设施的配置信息只与某个特定的应用程序相关,那么它就应该是那个特定应用程序的部署流水线的一部分,而不需要它自己的一个独立生命周期管理。 然而,如果某些基础设施是多个应用程序共享的,那么你就面临这样一个问题:管理应用程序和应用程序所依赖的基础设施之间的版本依赖。
配置管理过程的目标是,保证配置管理是声明式且幂等的( idempotent),即无论基础设施的初始状态是什么样,一旦执行了配置操作后,基础设施或系统所处的状态就一定是你所期望的状态,即使某个配置项进行了重复设置对配置结果也没有影响。
与用你所喜欢的编程语言写的程序一样,与中间件相关联的配置信息也是系统的一部分。 很多现代中间件支持配置脚本化方式:XML的配置方式比较常见,并且还提供一些简单的命令行工具来做脚本化。 学习并使用这些工具,像管理系统中的其他代码一样,将这些文件进行版本管理。 如果你有选择权的话,选择那些支持这类特性的中间件。 根据我们的经验,这些工具的重要性要比华丽的管理工具高得多,甚至高于对最新标准的兼容性。
可惜的是,虽然商业产品的目标是提供“企业级的服务”,但现在市面上很多(通常也很昂贵)的中间件产品在部署和配置管理的方便性面前败下阵来。 根据我们的经验,那些成功的项目通常都具有这种能力做到干脆利落且可靠的部署。 我们认为,除非能以自动化方式进行部署和配置,否则它就不适合企业级应用。 如果不能把重要的配置信息保存在版本控制中,并以可控的方式来管理变更的话,那么这种技术会成为高质量交付的障碍。
通常,在脚本化配置这方面,走在前面的往往是开源的系统和组件。 因此,对于基础设施的问题来说,开源解决方案通常更容易管理和集成。遗憾的是,并不是整个软件行业对这件事都达成了共识。
对于改变组件目前所用的软件平台来说,很多组织是非常谨慎的,因为它们已经在该平台上花了不少钱。 然而,这种说法,被称为沉没成本谬误,它并没有考虑转移到新技术上失去的机会成本。 邀请一些足够资深的人或者友好的核审员来评估你所面临的效率损失的财务后果,然后让他们找到更好的代替品。 在我们的一个项目中,我们维护了一个“痛苦注册表”( pain-register),即每天因低效技术而损失的时间。 一个月后就很容易展示出该技术对快速交付产生的影响。
但“基于云的服务的安全性比部署到公司自己的基础设施上的对外开发服务低”这种说法是缺少基本理由支持的。
在使用云计算时,常常提到“遵从性”(compliance),并把它看为一种约束条件。 然而,问题通常不是说:因为没有遵守各种规定,所以限制使用太多的云计算。 由于很多规定( regulation)没有考虑到云计算的问题,所以在云计算这个上下文中,这些规定的含义没有被很好地诠释,或者没有被充分地解释清楚。
第 12 章 数据管理#
使用敏捷方法进行开发的一个原则是:通过每次修改后进行重构以便使技术债最小化,从而得以优化设计。
常常有这样一种倾向,即创建一个连贯的“故事”(将多个测试场景串在一起),让一些测试顺序执行。 这种方法的出发点是已创建的数据是有连续性的,这样可以将测试用例的建立和销毁工作最小化。 而且,每个测试本身也会简单一点儿,因为它不再负责管理自己的测试数据了。 另外,作为一个整体,测试套件运行得更快,因为它不用花太多时间创建和销毁测试数据了。 有时候,这种做法很诱人,但在我们看来,这是应该予以抵制的一种诱惑。
如果你发现自己很难为某个测试准备数据的话,这是一个明显的信号,表示你的设计需要更好地解耦。
第 13 章 组件和依赖管理#
持续交付让我们每天都能发布软件的几个新的可工作版本。 也就是说,保持应用程序处于随时可发布的状态。 然而,在大型重构或添加复杂新功能时又怎么办呢? 从版本控制库上拉一个新的分支看上去好像是解决这个问题的一个方案。但我们强烈感觉到这是错误的做法。
对于“应用程序功能的可用性”这个问题,持续集成可以给你某种程度上的自信。 而部署流水线(持续集成的扩展)用于确保软件一直处于可发布状态。 但是,这两个实践都依赖于一件事,即主干开发模式。
为了能够将大块变更分解成一系列的小修改,分析工作就要扮演非常重要的角色了。 首先需要用各种各样的方式将一个需求分解成较小的任务。然后将这些任务再划分成更小的增量修改。 这种额外的分析工作常常会使修改的错误更少、目的性更强。 当然如果修改是增量式的,也就可以“边走边评估”( take stock as you go along),并决定是否需要继续做和如何继续。
然而,有时候某些修改太难做增量式开发了。此时,应该考虑“通过抽象来模拟分支”(branching by abstraction)的方法。
根据我们的经验,在项目开始时,最好不要马上创建插件 API。 相反,先创建一种实现方式,然后再创建第二种,之后从这些实现方式中抽取总结出API。 随着不断增加实现方式,并在这些实现方式中增加更多的功能,你会发现,API变化得非常快。 假如你打算向外界公布这些API,让其他人用这些API来开发插件的话,最好还是等它们稳定下来再这么做。
无论是使用“通过抽象来模拟分支”还是其他方法,若有全面的自动化验收测试套件,一定会取得巨大的收益。 因为当应用程序的一大块代码要被修改时,由于单元测试和组件测试的粒度太小,所以它们不足以对业务功能形成保护。
因为对组件的一个要求就是它应该可独立部署,所以类通常不能算做组件。
我们并不建议让每个团队各自负责一个独立的组件。因为在大多数情况下,需求不会按组件的边界来分。 根据我们的经验,那些有能力开发端到端功能的跨功能团队更加高效。尽管一个团队负责一个组件看上去好像更高效,但事实并非如此。
“每个团队负责一个组件”这种工作方式有一个非常严重的风险,那就是整个应用程序到项目后期才能工作,因为没人愿意去集成这些组件。
上面列表中的第(4 代码的编译和链接时间太长)和第(5 在开发环境中打开项目的时间太长)条理由常常是因无法满足模块化的拙劣设计而表现出来的症状。 一个设计良好的代码库应该遵守 DRY原则,并由遵从迪米特法则且具有良好封装性的对象组成,通常会使开发更高效、更容易。
值得注意的是康威法则(Conway’s Law),即“设计系统的组织不可避免地要产生与其组织的沟通结构一样的设计”。 例如,开源项目的开发人员只通过电子邮件来交流,所以,项目代码更趋向于较少接口的模块化特点。 由都坐在一起的小团队开发出来的产品更趋向于紧耦合、非模块化特点。 请细心地考虑如何组建开发团队,因为它会影响应用程序的架构。
集成流水线的起点是:从所有组件流水线中得到组成该应用系统的二进制包。 集成流水线的第一个阶段应该是将这些二进制文件组装在一起,创建一个部署安装包。 第二个阶段应该将其部署到一个类生产环境中,并在其上运行冒烟测试,快速验证是否有最基本的集成问题。
集成流水线是每个个体组件流水线的扩展。所以双方向的可视化是非常重要的。
第 14 章 版本控制进阶#
让持续集成成为可能的一个最重要实践就是每个人每天至少向主干提交一次。 因此,如果你每天将分支合并到主线一次(而不只是拉分支出去),那就没什么。如果你没这么做,你就没有做持续集成。 的确,有一种思想流派认为,从精益的角度来讲,分支上的工作就是浪费,即它们是库存,因为它们没有被放到最终的产品里。
首先从主干开发说起,因为这种开发方法经常被忽视。实际上,它是一个极其有效的开发方法,也是唯一使你能执行持续集成的方法。
主干开发的优点:“90%的配置管理过程( SCM process)都在强调代码基线的晋级,用来弥补缺少主线的问题。”
主干开发的一个结果就是:每次向主干签入并不都是可发布状态。 如果你使用分支方式做特性开发,或者使用基于流的开发通过多级直至发布级别来晋级变更,那么这可能看上去是对主干开发实践的一个“击倒性”反驳。 如果每次都晋级到主干,那么如何管理一个有很多开发人员,且有多个版本发布的大型团队呢? 这个问题的答案是:软件需要良好的组件化、增量式开发和特性隐藏(feature hiding)。
我们这里的建议并不是一个技术上的解决方案,而是一种实践:一直向主干提交代码,并且至少每天一次。 假如你认为,对代码做重大修改时不适合这么做的话,那我们有理由认为,你也许根本没有努力尝试过。 根据我们的经验,虽然使用一系列小的增量步骤来实现某个功能,又要保持软件一直处于可用状态的这种做法有时需要花更长的时间,但其收益也是巨大的。
有一种情况,“创建分支”是可以接受的,那就是在某个版本即将发布之前。 一旦创建了这个分支,该发布版本的测试和验证全部在该分支上进行,而最新的开发工作仍旧在主干上进行。
按功能特性分支想要有效果,就要有如下一些前提条件:
- 每天都要把主干上的所有变更合并到每个分支上。
- 每个特性分支都应该是短生命周期的,理想情况下应该只有几天,绝对不能超过一个迭代周期。
- 活跃分支的数量在任意时刻都应该少于或等于正在开发当中的用户故事的数量。除非已经把开发的用户故事合并回主干,否则谁都不能创建新分支。
- 在合并回主干之前,该用户故事应该已经由测试人员验收通过了。只有验收通过的用户故事才能合并回主干。
- 重构必须即时合并,从而将合并冲突最小化。这个约束非常重要,但也可能非常痛苦,进而限制了这种模式的使用。
- 技术负责人的一部分职责就是保证主干的可发布状态。他应该检查所有的合并(可以通过查看补丁的方式进行检查)。他有权拒绝可能破坏主干代码的补丁。
值得提醒一下的是,分支操作基本上是和持续集成背道而驰的。 即使在每个分支上都做了持续集成,它也并没真正解决集成问题,因为事实上你仍旧没有对这些分支进行集成。 此时,最接近持续集成的做法就是让持续集成系统把每个分支都合并到一个假想的“主干”,这个假想的主干就是所有人都合并到主干后,该主干所处的状态,并在其上运行所有的自动化测试。
按功能特性分支: 我们以极谨慎的态度推荐这种策略,因为它与商业软件开发最常见的一个反模式紧密相关。 这种反模式既邪恶,又很常见,那就是开发人员通过分支来创建特性,而这种分支会独立持续存在很长时间。 同时,其他开发人员也创建其他分支。只有当接近发布时间点时,所有的分支才会被合并到主干上。
很重要的一点是,每次创建分支,都要认识到它带来的成本。 这种成本在于“增加了风险”,而唯一最小化风险的方法就是无论由于什么样的理由创建了分支,都要努力保证任何活跃分支每天(甚至更频繁地)合并回主干。 不这么做的话,这个过程就不再是持续集成了。
第 15 章 持续交付管理#
像精益制造业一样,没有频繁交付的软件就是仓库中的库存。它已经花钱制造完了,却还没有为你赚钱,实际上保管它也是花钱的。
选择一个领域集中发力,该领域是你的薄弱环节,你的痛点所在。 价值流分析方法(value stream mapping)会帮你识别在组织中对哪个领域进行改进最有意义。
实施变革:首先,创建一个实施计划。可能最常见的方法就是先验证一下。比如,先选择组织中真正感到痛苦的那部分人,这些人会有强烈的动机去实施这种变革,
我们能够提供的一个最重要的建议就是增量式地实施改进,并随着进展不断衡量其影响。 如果想让整个组织中一下子从第一级直接跨越到第五级,那么一定会失败。
“启动阶段”是对开始写产品代码前这段时间最简单的描述。一般来说,此时会对需求进行收集和分析,并对项目的范围和计划进行初步规划。 该阶段中最重要的部分(也是决定项目成功概率的部分)是让所有项目干系人在一起面对面的工作,包括开发人员、客户、运营人员和管理层。 这些人之间的对话才是真正的交付物,因为这会让所有人对需要解决的问题域,以及解决问题的方法有一个共同的理解。
那些试图避免变更的项目常常以失败而告终。 在项目的这个阶段进行非常详尽的计划、估算和设计就是在浪费时间和金钱。
在启动阶段之后,就应该建立初始的项目基础设施了。 虽然这个阶段的真正目标是准备好基本的项目基础设施,而且也不应该被看成是一个真正的开发迭代,但是拿一个真实的需求让整个基础设施运行起来是非常有用的。 建立一个测试环境却无内容可测,或者建立了一个版本控制库却无内容可存,这是一个既没有产出也很低效的开始。 与此相反,要找到一个实际的(也许是最简单的)需求,解决一个实际的问题,并建立一些初始的设计方向。
当管理没有使用迭代开发方法的大型项目时,所有关于项目进展的度量都是主观的,根本没有办法确认项目的真正进度。 在非迭代方法中看到的那些漂亮图表都是基于所剩时间的估算和对后续集成、部署和测试阶段中风险与成本的猜测。
如果代码基被缺乏经验的初级开发人员搞坏了,任何开发过程都无法自动修复它。
敏捷过程中的各种要素常常以微妙的方式互相作用,很容易误解其价值所在, 尤其是对于那些没有迭代过程实践背景的人来说,更是如此。 所以,开始时应该坚信书中所写的都是正确的,并遵循书中所写的这个流程。 在这一点上,怎么强调都不算过分。当你看到它是如何发挥作用以后,才能开始对其进行裁剪,以适应你的组织。
值得注意的是,并不是所有的风险都需要缓解策略。对于那些灾难性的事件,没什么方法可以用来缓解。 比如,一个巨大的小行星会摧毁地球上的所有生命就是一个极端例子,但你一定已经理解我说的是什么意思了。
制定和实施成本太高或时间消耗太长的风险缓解策略也是没有必要的,比如对于某个小公司的工时和票据管理系统来说,一个多地区多节点的备份系统是没有必要的。
可以通过非常简单的计算决定使用什么策略来缓解风险:缓解策略的成本是否高于风险的严重度?如果回答是肯定的,那么缓解策略可能就没有必要实施了。
不要去干扰一个能够按计划定期交付软件的团队(尽管它可能会有几个缺陷),这一点非常关键。 然而,更重要的是,快速发现那些从外部看来一切都很好,而实际上正在走向失败的项目。 幸运的是,迭代开发方法的收益之一就是,很容易就能发现这样的问题是否存在。 如果你正在进行迭代开发,那么每个迭代结束时你都应该在类生产环境中演示一下这个可工作的软件。 这是实际进度的最佳证明。
很多公司坚信,文档是审计的关键。我们的想法有些不同。让你按照某种方法做事的那张纸无法保证你真的是那么做的。 世界上有很多关于咨询顾问的故事(比如ISO 9001认证审核的故事)。 顾问会指导工作人员如何正确应对审查人员的询问,并告知需要提供一堆文件来“证明”他们的实施是符合标准的。
对于每个项目的成功来说,管理都是至关重要的。 良好的管理所创建的流程令软件更高效地交付,同时确保风险被适当地管理,规章制定被严格遵守。 然而,太多的组织(虽然有良好的意图)却创建了无法满足上述目标的较差的管理结构。