第 2 章 配置管理

第 2 章 配置管理

2.1 引言

配置管理是一个被广泛使用的名词,往往作为版本控制的同义词。为了陈述清晰起见,在这里我们给出本书中对配置管理的定义:

配置管理是指一个过程,通过该过程,所有与项目相关的产物,以及它们之间的关系都被唯一定义、修改、存储和检索。

配置管理策略将决定如何管理项目中发生的一切变化。因此,它记录了你的系统以及应用程序的演进过程。另外,它也是对团队成员协作方式的管理。作为配置管理策略的一个结果,虽然第二点至关重要,但常常被忽视。

虽然版本控制系统是配置管理中最显而易见的工具(团队规模再小,也应该使用版本控制系统),但决定使用一个版本控制工具仅仅是制定配置管理策略的第一步而已。

假如项目中有良好的配置管理策略,那么你对下列所有问题的回答都应该是“YES”。

  • 你能否完全再现你所需要的任何环境(这里的环境包括操作系统的版本及其补丁级别、网络配置、软件组合,以及部署在其上的软件应用及其配置)?
  • 你能很轻松地对上述内容进行增量式修改,并将修改部署到任意一种或所有环境中吗?
  • 你能否很容易地看到已被部署到某个具体环境中的某次修改,并能追溯到修改源,知道是谁做的修改,什么时候做的修改吗?
  • 你能满足所有必须遵守的规程章则吗?
  • 是否每个团队成员都能很容易地得到他们所需要的信息,并进行必要的修改呢?这个配置管理策略是否会妨碍高效交付,导致周期时间增加,反馈减少呢?

最后这一点非常重要。因为我们常常遇到这样的情况:配置管理策略完全满足前面四个要点,但这恰恰成了团队间协作的一个巨大障碍。事实上,如果我们能够给予配置管理策略足够的重视,那么最后一点与其他四点之间是可以不对立的。我们不可能在本章中解决所有这些问题,但当你读完这本书后,问题的答案就显而易见了。在本章中,我们将讨论三个问题。

(1)为管理应用程序的构建、部署、测试和发布过程做好准备。我们从两个方面解决这个问题:对所有内容进行版本控制;管理依赖关系。

(2)管理应用软件的配置信息。

(3)整个环境的配置管理,这包括应用程序所依赖的软件、硬件和基础设施。另外还有环境管理背后的原则,包括操作系统、应用服务器、数据库和其他COTS(商业现货)软件。

2.2 使用版本控制

版本控制系统(也称为源代码控制管理系统或修订控制系统)是保存文件多个版本的一种机制。当修改某个文件后,你仍旧可以访问该文件之前的任意一个修订版本。它也是我们共同合作交付软件时所使用的一种机制。

第一个流行的版本控制系统是一个UNIX下的专有工具,称为SCCS(Source Code Control System,源代码控制系统),可以追溯到20世纪70年代。它被RCS(Revision Control System,修订控制系统)和后来的CVS(Concurrent Versions System,并发版本控制系统)所取代。虽然这三种系统的市场份额越来越小,但至今仍旧有人在使用。现在市面上有很多更好用的版本控制系统,既有开源的,也有商业版的,而且都是针对各种不同的应用环境设计的。一般来说,包括Subversion、Mercurial和Git在内的开源工具就可以满足绝大多数团队的需求。我们会花更多的时间来探讨版本控制系统和它们的使用模式包括分支与合并(详见第14章)。

本质上来讲,版本控制系统的目的有两个。首先,它要保留每个文件的所有版本的历史信息,并使之易于查找。这种系统还提供一种基于元数据(这些元数据用于描述数据的存储信息)的访问方式,使元数据与某个单个文件或文件集合相链接。其次,它让分布式团队(无论是空间上不在一起,还是不同的时区)可以愉快地协作。

那么,为什么要这样做呢?理由可能很多,但最关键的是它能回答下面这些问题。

  • 对于我们开发的应用软件,某个特定的版本是由哪些文件和配置组成的?如何再现一份与生产环境一模一样的软硬件环境?
  • 什么时候修改了什么内容,是谁修改的,以及为什么要修改?因此,我们很容易知道应用软件在何时出了错,出错的过程,甚至出错的原因。

这是版本控制的基本原理和根本目的。现在,大多数项目都使用版本控制系统。如果你还没有使用的话,请在阅读完下面几节后就马上把书放到一边,为项目建立版本控制库去吧。下面是我们对高效使用版本控制系统的几点建议。

2.2.1 对所有内容进行版本控制

我们使用“版本控制”(version control)这个术语而不是“源代码控制”(source control)的理由是,版本控制不仅仅针对源代码。每个与所开发的软件相关的产物都应被置于版本控制之下。开发人员不但要用它来管理和控制源代码,还要把测试代码、数据库脚本、构建和部署脚本、文档、库文件和应用软件所用的配置文件都纳入到版本控制之中,甚至把编译器以及工具集等也放在里面,以便让新加入项目的成员可以很容易地从零开始工作。

为了重新搭建测试环境和生产环境,将所有必需的信息保存起来也是很重要的。这里必需的信息包括应用程序所需的支撑软件的配置信息、构成对应系统环境的操作系统配置信息、DNS区域文件和防火墙配置等。你至少要将那些用于重新创建应用程序的安装文件和安装环境所必需的所有信息保存在版本控制存储库之中。

我们的目标是能够随时获取软件在整个生命周期中任意时间点的文件状态。这样我们就可以选择从开发环境至生产环境整个环节中的任意时间点,并将系统恢复到该时间点的状态。我们甚至可以把开发团队所需的开发环境配置也置于版本控制中,如此一来,团队中的每个成员都能够轻松使用完全相同的设置。分析人员应该把需求文档保存到版本控制存储库中。测试人员也应该将自己的测试脚本和过程保存在版本控制存储库中。项目经理则应该将发布计划、进度表和风险日志也保存在这里。总之,每个成员都应该将与项目相关的任何文件及其修订状态保存在版本控制存储库之中。

将所有东西都提交到版本控制库中

许多年前,本书作者之一参与了某个项目开发相关的工作,该项目由三个子系统组成,分别由位于三个不同地点的三支团队开发。每个子系统都使用IBM MQSeries基于某种专用消息协议相互通信。这是在使用持续集成之前,预防配置管理问题的一种手段。

我们对源代码的版本控制一直都非常严格,因为我们在该项目之前就得到过教训。然而,我们的版本控制也仅仅做到了源代码的版本控制而已。

当临近项目的第一个版本发布时间点时,我们要将这三个独立的子系统集成在一起。可是,我们发现其中某个团队使用的消息协议规范与其他两个团队使用的不一致。事实上,该团队所用的实现文档是六个月前的一个版本。结果,为了修复这个问题且保证这个项目不拖期,在之后的很多天里,我们不得不加班到深夜。

假如当初我们把这些文档签入版本控制系统中,这个问题就不会发生,也就不用加班了!假如我们使用了持续集成,项目工期也会大大缩短。

我们无论怎么强调“做好配置管理”都不算过分。它是本书其他内容的基础。如果没有将项目中的所有源产物(source artifact)全部放到版本控制之中,就无法享受到本书中所提到的任何好处。我们所讨论的有关加快发布周期和提高软件质量的所有实践,从持续集成、自动化测试,到一键式部署,都依赖于下面这个前提:与项目相关的所有东西都在版本控制库中。

除了存储源代码和配置信息,很多项目还将其应用服务器、编译器、虚拟机以及其他相关工具的二进制镜像也放在版本控制库中。这是非常实用的,它可以加快新环境的创建。更重要的是,它可以确保基础配置的完整性。只要能从版本控制库中取出所需要的一切,就能保证为开发、测试,甚至生产环境提供一个稳定的平台。然后你可以将整个环境(包括配置基线上的操作系统)做成一个虚拟镜像,放在版本控制库中,这可以作为更高级别的保证措施,而且可以提高部署的简单性。

这种策略在控制和行为保障方面建立了基础。对于在这种严格配置管理策略约束下的系统来说,根本不存在整个流程的后期还会出错的可能性。这种水准的配置管理可以确保在保证存储库完整性的情况下,我们在任何时候都能拿到应用软件的一个可工作的版本。即使编译器、编程语言或与该项目有关的其他工具都模棱两可时,也足以给你安全保证了。

但我们并不推荐将源代码编译后得到的二进制文件也纳入到版本控制中,有以下几个理由。首先,它们通常比较大,而且(与编译程序不同)会让存储所需要的空间快速膨胀,因为我们每次签入代码,在编译和自动提交测试通过后,都会生成新的二进制文件。其次,如果有自动化构建系统,那么只要重新运行构建脚本,就可以利用源代码重新生成需要的二进制文件。这样的话,根本没有必要把这类二进制产物放在版本控制库中。请注意,我们并不推荐在同一个自动化构建过程中进行重复编译。因为如果需要二进制产物的话,我们只要通过构建系统把源代码再重新打包生成一次就可以了。最后,我们使用修订版本号来标识产品的版本。如果我们把构建生成的二进制文件也储存在版本控制库中,那么在存储库中的一个版本就会有两个不同的源,一个是源代码,另一个是二进制文件。尽管看上去这有点儿含糊,但创建部署流水线(本书的主要议题之一)时就显得极为重要了。

版本控制:“删除”的自由

版本控制库中包含每个文件的每一个版本,它的好处就是:可以随时删除你认为不必要的文件。只要有版本控制系统,对于“是否可以删除这个文件?”这个问题,你可以轻松地回答“Yes”。如果事实证明你的删除决定是错的,只要从早期版本中把它再找回来就行了。

这种“自由删除”是维护大型配置集合向前迈进的重要一步。保证大型团队能高效工作的关键就在于一致性和良好的组织性。“打破陈规”的能力使团队可以勇敢地尝试新的想法或实现方式,提高代码质量。

2.2.2 频繁提交代码到主干

使用版本控制时,有两点需要牢记在心。首先,只有频繁提交代码,你才能享受版本控制所带来的众多好处,比如能够轻松地回滚到最近某个无错误的版本。

其次,一旦将变更提交到版本控制中,那么团队的所有人都能看到这些变更,也能签出它。而且,如果使用了持续集成(像我们推荐的那样),你所做的修改还会触发一次构建,本次构建很有可能会最终进入验收测试,甚至被部署到生产环境。

由于提交就意味着公开,所以无论修改的是什么,都要确保它不会破坏原有的系统,这一点非常重要。对于开发人员来说,由于其工作内容的本质,他必须谨慎地对待其提交可能带来的影响。如果某位开发人员正在做某项复杂任务,那么只有工作全部完成后,他才能提交代码。而且提交时,他要有足够的信心说:我的代码没问题,不会影响系统的其他功能。

在一些团队中,这种限制很可能导致开发人员需要几天甚至几个星期才能提交一次代码。这种很长时间才提交的做法是有问题的。因为提交越频繁,越能够体现出版本控制的好处。除非每个人都频繁提交,否则“安全地对系统进行重构”这件事基本上是不可能完成的任务。因为长时间不提交代码会让合并工作变得过于复杂。如果你频繁提交,其他人可以看到你的修改且可与之交互,你也可以清楚地知道你的修改是否破坏了应用程序,而且每次合并工作的工作量会一直很小,易于管理。

有些人解决这个两难问题的方法是,在版本控制系统中为新功能建立单独的分支。到某个时间点后,如果这些修改的质量令人满意,就将其合并到主干。这类似于“两阶段提交”。实际上,有些版本控制系统就是以这种方式工作的。

然而,我们对这样的做法持反对意见,除非是第14章提到的那三种例外情况。在这一点上有一些争议,尤其是在使用ClearCase以及相似工具的用户中。我们认为,这种方法存在以下几个问题。

  • 它违背了持续集成的宗旨,因为创建分支的做法推迟了新功能的整合,只有当该分支被合并时才可能发现集成问题。
  • 如果多个开发者同时分别创建了多个分支,问题会成指数增加,而合并过程也会极其复杂。
  • 尽管有一些好用的工具有自动合并功能,但它们无法解决语义冲突。例如,某人在一个分支上重命名了一个方法,而另一个人在另一分支上对该方法增加了一次调用。
  • 它让重构代码库变得非常困难,因为分支往往涉及多个文件,会让合并变得更加困难。

我们将在第14章更详细地讨论分支与合并的复杂性。

一个更好的解决方案是尽量使用增量方式开发新功能,并频繁且有规律地向版本控制系统提交代码。这会让软件能一直保持在集成以后的可工作状态。而且,你的软件会一直被测试,因为每次提交代码时,持续集成服务器就会从代码主干上运行自动测试。这会减小因重构引起的大规模合并导致冲突的可能性,确保集成问题能够被及时发现,此时修复这些问题的成本很低,从而提高软件开发质量。我们将在第13章中详细讨论避免分支的技术。

为了确保提交代码时不破坏已有的应用程序,有两个实践非常有效。一是在提交代码之前运行测试套件。这个测试套件应该是一个快速运转(一般少于10分钟)且相对比较全面的测试集合,以验证你没有引入明显的回归缺陷。很多持续集成服务器都提供名为“预测试提交”(pretested commit)的功能,让你在提交之前可以在类生产环境中执行这些测试。

二是增量式引入变化。我们建议每完成一个小功能或一次重构之后就提交代码。如果能正确地使用这一技术,你每天最少可以提交一次,通常能达到每天提交多次。如果你还未习惯于这种技术的话,肯定会以为是“天方夜谭”。但我们向你保证,这种技术能够带来相当高效的软件交付过程。

2.2.3 使用意义明显的提交注释

每个版本管理工具都提供“写注释功能”。但这些注释很容易被忽视,而且很多人习惯于忽略它。写描述性提交注释的最重要原因在于:当构建失败以后,你知道是谁破坏了构建,以及他为什么破坏了构建。当然,这并不是唯一原因。很多时候,提交人没有写足够的描述信息,其原因通常是由于正在抓紧解决某个非常复杂的问题。我们可能常常遇到下面的场景。

(1) 你发现了一个缺陷,结果追溯到一行相当晦涩的代码。

(2) 你通过查看版本控制系统的日志,查找放入这行代码的人,以及他是什么时候放入的。

(3) 可是,放入这行代码的人去度假或者回家了,而他写的提交注释只有简单的几个字,即“已修复令人费解的缺陷”。

(4) 为了修复这个缺陷,你修改了这行晦涩代码。

(5) 但是却把其他功能破坏了。

(6) 你只能再花几个小时的时间,让软件恢复到可工作状态。

在这种情况下,如果之前那个修复缺陷的人能够解释清楚当初为何修改这行代码的话,你可能就会节省大量的调试时间。这种情况越多,你就越希望提交注释能够写得清楚明了。无论提交注释写得多么短小精悍,你也得不到奖励。然而,多写几行字来描述你做了什么,会为将来节省很多时间。

我们喜欢的一种注释风格是这样的:第一段是简短的总结性描述,接下来的几段描述更多的细节。简短的总结性描述怎么写呢?它就像是报纸的标题一样,要给读者足够的信息,以便让读者知道是否还需要继续读下去。

这个注释中还应该包括一个链接,可以链接到项目管理工具中的一个功能或缺陷,从而知道为什么要修改这段代码。在我们曾经工作过的很多团队中,系统管理员会监控版本控制系统,假如注释中不包含这种信息,你就无法提交代码。

2.3 依赖管理

在软件项目中,最常见的外部依赖就是其使用的第三方库文件,以及该软件需要用到的正由其他团队开发的模块或组件间的关系。库一般是以二进制文件的形式部署,不会被你自己的团队修改,而且也不经常更新。然而,组件和模块会被其他团队频繁修改。

我们将在第13章花较多的篇幅讨论依赖问题。在这里,我们只讨论依赖管理中的几个关键点,因为它会影响配置管理。

2.3.1 外部库文件管理

外部库文件通常是以二进制形式存在,除非你使用的是解释型语言。即使是解释型语言,外部库文件也通常会安装在全局系统路径中,并由包管理系统来管理,比如Ruby的Gems和Perl的modules。

对于“是否将这些库文件放到版本控制库中”这个问题,业界还有一些争议。例如,Maven(Java的一种构建工具)允许指定应用程序所依赖的jar文件,并会从因特网上的代码库下载(如果有本地缓存库的话,也可以从本地缓存中取得)。

这么做既有缺点,也有好处。例如,一个新加入项目的成员为了能开始工作,可能必须从因特网下载库文件(或至少是恰好够用的那部分内容),但可以大大缩小源代码库的尺寸,让我们可以在较短的时间内签出全部代码。

我们建议在本地保存一份外部库的副本(如果使用Maven,应该创建一个本地仓库,里面存放那些在你的公司中统一使用的外部库)。如果你必须遵守某些规章制度,这种做法是非常必要的,而且它也能使项目可以快速启动。这样,你就总能再现构建过程。此外,我们还要强调的是,在构建系统中,应该始终指定项目所需外部库的确切版本。如果不这么做的话,很可能无法保证每次都能够完全再现你的构建版本。假如不能指定具体版本,你可能会遇到这样的情况:你花了很长时间来跟踪调试一个非常奇怪的问题或错误,可最终发现是由于库文件的版本不符导致的。

那么是否一定要把外部依赖库文件放在版本控制库中呢?其实,放与不放,各有利弊。如果放了,那我们更容易将软件的版本与正确的库文件版本相关联,但它也可能使源代码库的体积更大,并且签出时间也会变长。

2.3.2 组件管理

将整个应用软件分成一系列的组件进行开发(小型应用除外)是个不错的实践。这能让某些变更的影响范围比较小,从而减少回归缺陷。另外,它还有利于重用,使大项目的开发过程更加高效。

典型情况下,我们总是做一次独立且完整的构建,生成整个应用的二进制代码或安装文件,且通常会同时进行单元测试。这种方法对于构建中小规模的软件应用是最为高效的,当然这也与构建项目所使用的构建工具和技术有关。

随着系统不断变大,或者当有其他项目依赖于我们所开发的组件时,我们就需要将这几个组件的构建分成不同的构建流水线了。如果你正是这么干的,需要特别注意的一点就是,这些构建流水线之间的依赖应该是二进制文件依赖,而不是源文件依赖。因为,如果每次都要重新编译其依赖文件,不但执行效率较低,而且还存在一种可能性,即新编译出来的文件与你之前已测试过的那个依赖文件有差异。虽然使用这种二进制包依赖的方法会给问题追踪带来困难,尤其是那些因修改上游源文件而导致下游组件出错的问题,但是一个好的持续集成服务器产品可以帮助解决这个问题。