第 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 组件管理

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

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

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

尽管现在市面上的持续集成服务器在依赖管理方面做得已经相当不错了,但通常开发人员在其开发环境中仍很难对软件应用重复地做整个端到端的构建过程。在理想情况下,当我将几个组件从代码库签出到我的机器上,这几个组件就应该是直接相关联的。而且,一旦修改了其中的几个组件,只要敲一行命令就可以重新以正确的顺序构建这些组件,生成正确的二进制代码,并运行相关的测试。然而遗憾的是,尽管像Ivy和Maven这样的工具以及像Gradle或Buildr这样的脚本编程技术的支持,会令事情变得容易些,但是如果没有聪明的构建工程师的参与,大多数构建系统还是无法达到理想状态的。

关于管理组件和依赖的更多内容请参见第13章。

2.4 软件配置管理

作为关键部件之一,配置信息与产品代码及其数据共同组成了应用程序。软件在构建、部署和运行时,我们可以通过配置信息来改变它的行为。交付团队需要认真考虑设置哪些配置项,在应用的整个生命周期中如何管理它们,以及如何确保这些配置项在多个应用、多个组件以及多项技术中的管理保持一致性。我们认为,应该以对待代码的方式来对待你的系统配置,使其受到正确的管理和测试。

2.4.1 配置与灵活性

每个人都希望使用的软件非常灵活。为什么不呢?可是,灵活性也是有代价的。

就像一个平衡游标,一端是只有单一用途的软件,而且工作得很好,但很难或根本无法改变它的行为。然而另一端则是编程语言,你可以用它编写游戏、应用服务器或股票管理系统,这就是灵活性!显然,大多数软件都在两点之间,而不是这两端点中的任何一个。这些软件被设计用于完成某些特定目的,但在能够完成这些目的的前提下,通常在一定程度内可以通过某些方法改变它们的行为。

对于软件灵活性的期望常常导致一种反模式,即“终极配置”,而这种反模式常被表述为对一个软件项目的需求。如果做得好,它没有什么坏处,但是如果搞不好的话,它会毁了一个项目。

任何改变应用程序的行为,无论修改了什么,都算是编程,即使只是修改一行配置信息。你进行修改所使用的语言可能或多或少地受到限制,但此时仍是在编程。根据定义,要为用户提供的软件配置能力越强,你能置于系统配置的约束就应越少,而你的编程环境也会变得越复杂。

根据我们的经验,“修改配置信息的风险要比修改代码的风险低”这句话就是个错觉。就拿“停止一个正在运行的应用系统”这个需求来说,通过修改代码或修改配置都很容易办到。如果使用修改源代码的方式,可以有多种方式来保证质量,比如编译器会帮我们查语法错误,自动化测试可以拦截很多其他方面的错误。然而,大多数配置信息是没有格式检查,且未经测试的。在大多数系统中,没有什么机制能阻止我们将一个URI“http://www.asciimation.co.nz/”改为“this is not a valid URI”。大多数系统只有在运行时,才能发现这样的更改,此时用户不是惊喜地看到ASCII版的Star Wars,而是看到一堆系统异常报告,因为URI这个类无法解析“this is not a valid URI”。

在构建高度可配置的软件的道路上有很多陷阱,而最糟糕的可能莫过于下面这些。

  • 经常导致分析瘫痪,即问题看上去很严重,而且很棘手,以至于团队花费很多时间思考如何解决它,但最终还是无法解决。
  • 系统配置工作变得非常复杂,以至于抵消了其在灵活性上带来的好处。更有甚者,可能在配置灵活性上花费的成本与定制开发的成本相当。

终极可配置性的危险

我们曾经有个客户,花了三年的时间与一个供应商合作,想在其业务领域使用该供应商提供的软件产品。该产品被设计成具有高灵活性和高可配置性的软件,以便满足客户的需求。然而,最终的结果是只有该产品的产品专家才知道如何配置。

然而,客户担心该系统一时还无法用于生产环境。最后,他找到了我们,而我们的组织花费了八个月的时间,从零开始用Java为其定制了一个满足同样需求的软件。

可配置的软件并不总是像它看起来那么便宜。更好的方法几乎总是先专注于提供具有高价值且可配置程度较低的功能,然后在真正需要时再添加可配置选项。

不要误解我们的意思,配置并非天生邪恶,但需要采取谨慎的态度来一致地管理它们。现代计算机语言已经采用各种各样的特性和技术来帮助减少错误。在大多数情况下,配置信息却无法使用它们,甚至这些配置的正确性在测试环境和生产环境中也根本无法得到验证。我们认为,对部署活动的冒烟测试(参见5.3.3节)就是一种缓解配置验证问题的方法,我们应始终使用它。

2.4.2 配置的分类

我们可以在构建、部署、测试和发布过程中的任何一点进行配置信息的设置。而且,我们也的确会在多个时间点对应用软件进行相关的配置,如下所示。

  • 在生成二进制文件时,构建脚本可以在构建时引入相关的配置,并将其写入新生成的二进制文件。
  • 打包时将配置信息一同打包到软件中,比如在创建程序集,以及打包ear或gem时。
  • 在安装部署软件程序时,部署脚本或安装程序可以获取必要的配置信息,或者直接要求用户输入这些配置信息。
  • 软件在启动运行时可获取配置。

一般来说,我们并不赞同在构建或打包时就将配置信息植入的做法,而是应使用相同二进制安装包向所有的环境中部署,以确保这个发布的软件就是那个被测试过的软件。根据这一个原则,我们可以推出:在相临的两次部署之间,任何变更都应该作为配置项被捕获和记录,而不应该在编译或打包时植入。

打包配置信息

J2EE规范中的一个严重问题是,配置信息必须和应用软件的其他部分一并打包到.war或.ear文件中。除非你使用其他配置机制,而不是使用该规范规定的机制,否则就意味着,如果多个部署环境需要不同的配置信息,你就不得不为每种环境各自创建一个包括不同配置信息的.war或.ear文件。如果你受这种规范制肘的话,就要找其他方式在部署或运行时来配置应用程序,而下面就是我们的建议。

通常来说,能够在部署时对软件进行配置是非常重要的,这样就可以告诉应用程序在哪儿能找到所需服务,比如数据库、邮件服务器或外部系统。比如,当应用程序运行时的配置信息被存储在数据库中,你可能要在部署应用程序时将数据库的连接参数传入,使应用程序启动时可以从数据库中取到这些信息。

如果你有权限完全控制生产环境,就通常能让部署脚本自行获取这些配置并提供给应用。对于套装软件来说,安装包中通常都有默认的配置信息。做软件测试时,我们仍需要用某种方法在部署过程中修改某些配置信息。

当然,我们还可能要在启动或运行应用程序时修改某些配置。在系统启动时,我们可以通过命令参数或环境变量等形式提供配置信息。另外,你还可以使用同样的机制来做运行时的配置,比如注册表设置、数据库、配置文件,或者使用外部配置服务(比如通过SOAP或REST风格的接口访问)。

2.4.3 应用程序的配置管理

在管理应用程序的配置这个问题上,需要回答三个问题。

(1) 如何描述配置信息?

(2) 部署脚本如何存取这些配置信息?

(3) 在环境、应用程序,以及应用程序各版本之间,每个配置信息有什么不同?

通常配置信息以键值对的形式来表示。1有时可使用系统提供的配置类型来有层次地组织这些配置项。比如Windows属性文件的键-值字符串就是以不同的heading来组织的,而YAML文件在Ruby领域非常流行,Java中的属性文件虽然在格式上相对简单,但在大多数情况下还是能够提供足够灵活性的。将配置信息以XML文件的形式来保存可以对其复杂性起到较好的限制效果。

1从技术上讲,配置信息可以被看做是元组的一个集合。

将应用软件的配置信息保存在哪里呢?显而易见的选择包括数据库、版本控制库、文件目录或注册表等。版本控制库可能是最容易的,只要将配置文件签入就可以了,而且你可以随时拿到任意时间点上的历史配置信息。像源代码一样,将配置选项列表也保存在同一个代码库中是非常值得的。

 注意,存放配置信息的位置与应用程序访问这些配置信息的方式不是一回事儿。应用程序可以通过本地文件系统上的一个文件来获取它的配置信息,也可以通过其他方式(比如Web服务或目录服务,或者数据库)获取。关于这些内容的详细描述请参见下一节。

将那些特定于测试环境或生产环境的实际配置信息存放于与源代码分离的单独代码库中通常是非常必要的。因为这些信息与源代码的变更频率是不同的。不过,当使用这种方法时,需要注意:配置信息的版本一定要与相应的应用软件的版本相匹配。这种分离方式特别有利于重要信息的安全性,对于这些重要信息(如密码和数字证书等)的存取需要施加限制。

 小提示:不要把密码签入到版本控制系统中,也不要把它硬编码到应用程序中。

要是让运维人员知道你这么做,一定会让你卷铺盖走人的。所以,别给他们这样的机会。如果你坚持要将密码存在某处而不是自己记住的话,可以试着把它加密后放在用户主目录下。

这种方法的另一种极糟的使用方式是,将应用程序某一层上的密码保存在需要访问它的那层代码或文件系统中。实际上,用户在部署时应该每次都手工输入密码。对于多层应用系统来说,有多种方式来处理验证问题。比如,你可以使用证书、目录服务,或者一个单点登录系统。

数据库、文件目录和注册表是比较方便存储配置信息的场所,它们可以被远程访问。但是,为了审计性和可回滚性,一定要将配置项的修改历史保留下来。你可以通过某种系统自动地实现这一功能,也可以让版本控制系统充当这一角色,写一个脚本,根据需要将适当版本的配置信息加载到数据库或文件目录中。

  1. 获取配置信息

    管理配置最有效的方法是让所有的应用程序通过一个中央服务系统得到它们所需要的配置信息。对于套装软件来说,这是很常见的一种方式,就像很多专业服务提供商在因特网上为企业提供多种企业内部应用和软件服务一样。这些方案之间的主要区别只是在于何时注入配置信息,是在套装软件打包时,还是在部署时或运行时。

    对于应用程序访问配置信息来说,可能最简单的方法就是使用文件系统。这样做的好处是可以跨平台和得到各种语言的支持,但不太适合applet这种沙盒运行时。如果将配置项保存在文件系统中,一旦应用需要运行于集群环境里,配置信息的同步就会成为一个问题。

    还有一种方式是从某个中心仓库(如关系型数据库管理系统、LDAP或某种Web服务)中获取配置信息。一个名为ESCAPE [apvrEr]的开源工具可以通过一种REST式接口方便地管理和获取配置信息。应用程序可以执行一个HTTP GET请求,在URI中包含应用程序名和环境名称,从而获取相应的配置信息。这种机制对于在部署或运行时进行应用软件的配置更有效。将环境名称传给部署脚本(通过一个属性、命令行开关或环境变量),然后由脚本从配置服务中读取适当的配置信息,提供给应用程序使用,比如将其写入文件系统上的一个文件中。

    无论配置信息是什么样的存储形态,我们建议使用一个简单的Facade类,让它提供与下面类似的接口:

    getThisProperty()
    getThatProperty()
    
    

    将应用的技术细节与外界相隔离,这样就可以在测试代码中模拟它,并在需要时改变其存储机制。

  2. 为配置项建模

    每个配置都是一个元组,所以应用程序的配置信息由一系列的元组构成。然而,这些元组及其值取决于三方面,即应用程序、该应用程序的版本、该版本所运行的环境(例如开发环境、用户验收测试环境、性能测试环境、试运行环境或生产环境)。

    例如,报表软件1.0版本的配置元组集合与其2.2版本是不同的。当然,它也与项目管理软件1.0版本所使用的配置元组集不同。而且,这些元组的值取决于它们所处的部署环境。比如,在用户验收测试环境中的应用程序所使用的数据库服务器通常与生产环境中的不同,甚至在不同的开发机器上也不相同。这种情况同样适用于套装软件和外部集成系统。比如,在做集成测试时,我们所使用的某个外部服务就可能与真正的用户使用客户端访问时所使用的外部服务不同。

    无论你使用哪种方式来存储配置信息,放在源代码控制中的XML文件也好,或REST式Web服务中也好,都要能够满足不同的要求。下面列举了一些在对配置信息建模时需要考虑的用例。

    • 新增一个环境(比如一个新的开发工作站,或性能测试环境)。在这种情况下你要能为这个配置应用的新环境指定一套新的配置信息。
    • 创建应用程序的一个新版本,通常需要添加一些配置设置,删除一些过时的配置设置。此时应该确保在部署新版本时,可以使用新的配置设置,但是一旦需要回滚时,还能够使用旧版本的配置设置。
    • 将新版本从一个环境迁移到另一个环境,比如从测试环境挪到试运行环境。此时应该确保新环境上的新配置项都有效,而且为其设置了正确的值。
    • 重定向到一个数据库服务器。应该只需要简单地修改所有配置设置,就能让它指向新的数据库服务器。
    • 通过虚拟化技术管理环境。应该能够使用虚拟技术管理工具创建某种指定的环境,并且配置好所有的虚拟机。你也许需要将这种虚拟环境中的配置信息作为某特定版本的应用软件在虚拟环境中的标准配置信息。

    在不同环境之间管理配置信息的一种方法是,把预期的生产环境中的配置信息作为默认配置,而在其他环境中,通过适当的方式覆盖这些默认值(确保你有预防措施,以防生产环境受到配置失误的影响)。也就是说,尽量减少配置项,最好只保留那些与应用软件具体运行环境密切相关的配置项。这样,做环境配置时就非常简单了。然而,这也取决于组织对该应用程序的生产环境是否有特殊约束。比如,有些组织就要求生产环境与其他环境的配置信息不能放在一起。

  3. 系统配置的测试

    与应用程序和构建脚本一样,配置设置也需要测试。对于系统配置测试来说,包括以下两部分。

    一是要保证配置设置中对外部服务的引用是良好的。比如,作为部署脚本的一部分,我们要确保消息总线(messaging bus)在配置信息中所指定的地址已启动并运行,并确保应用程序所用的模拟订单执行服务在功能测试环境中能够正常工作。最起码,要保证能够与所有的外部服务相连通。如果应用程序所依赖的任何部分没有准备好,部署或安装脚本都应该报错,这相当于配置设置的冒烟测试。

    二是当应用程序一旦安装好,就要在其上运行一些冒烟测试,以验证它运行正常。对于系统配置的测试,我们只要测试与配置有关的功能就可以了。在理想情况下,一旦测试结果与预期不符,这些测试应该能够自动停止软件的运行,并显示安装或部署失败。

2.4.4 跨应用的配置管理

在大中型组织中,通常会同时管理很多应用程序,而软件配置管理的复杂性也会大大增加。这类组织中一般都会有遗留系统,而且很可能某个遗留系统的配置项让人很难搞得清楚明白。这种情况下,最重要的任务之一就是,要为每个应用程序维护一份所有配置选项的索引表,记录这些配置保存在什么地方,它们的生命周期是多长,以及如何修改它们。

如果可能的话,运行每个应用程序的构建脚本时应该自动生成一份这类信息。即使无法做到这一点,也要把它记录在Wiki上,或其他文档管理系统中。

当管理那些并非完全由用户安装的应用程序时,了解每个应用程序的当前配置信息是非常重要的。我们的目的是:系统运维团队可以通过生产系统的监控平台了解每个软件应用的配置信息,并能看到每种环境中所运行的软件到底是哪一个版本。像Nagios、OpenNMS和惠普的OpenView都提供了记录这类信息的功能。另外,如果是用自动化方式来管理构建和部署过程,那么应该一直用这个自动化过程来应用配置信息,而且如果是自动化过程的话,它应该已经被保存在版本控制库中或像Escape这样的工具中了。

如果应用程序之间有依赖关系,部署有先后次序的话,实时存取配置信息的能力就特别重要。很容易因配置信息设置不当而浪费很多时间,甚至导致整套服务无法正常运行,而这类问题是极难诊断的。

每个应用程序的配置项管理都应该作为项目启动阶段的一个议题,纳入计划当中。需要分析当前的运维环境中其他应用程序是如何管理配置信息的,考虑在新开发的应用中是否能够使用相同的配置管理方法。我们通常在需要时才临时决定如何管理配置信息,其后果是每个应用的配置信息被放在不同的位置,而应用程序又以不同的方式获取这些配置。这会给确定“哪些环境中有哪些配置”带来不必要的困难。

2.4.5 管理配置信息的原则

我们要把应用程序的配置信息当做代码一样看待,恰当地管理它,并对它进行测试。当创建应用程序的配置信息时,应该考虑以下几个方面。

  • 在应用程序的生命周期中,我们应该在什么时候注入哪类配置信息。是在打包的时候,还是在部署或安装的时候?是在软件启动时,还是在运行时?要与系统运维和支持团队一同讨论,看看他们有什么样的需求。
  • 将应用程序的配置项与源代码保存在同一个存储库中,但要把配置项的值保存在别处。另外,配置设置与代码的生命周期完全不同,而像用户密码这类的敏感信息就不应该放到版本控制库中。
  • 应该总是通过自动化的过程将配置项从保存配置信息的存储库中取出并设置好,这样就能很容易地掌握不同环境中的配置信息了。
  • 配置系统应该能依据应用、应用软件的版本、将要部署的环境,为打包、安装以及部署脚本提供不同的配置值。每个人都应该能够非常容易地看到当前软件的某个特定版本部署到各种环境上的具体配置信息。
  • 对每个配置项都应用明确的命名习惯,避免使用晦涩难懂的名称,使其他人不需要说明手册就能明白这些配置项的含义。
  • 确保配置信息是模块化且封闭的,使得对某处配置项的修改不会影响到那些与其无关的配置项。
  • DRY(Don't Repeat Yourself )原则。定义好配置中的每个元素,使每个配置元素在整个系统中都是唯一的,其含义绝不与其他元素重叠。
  • 最少化,即配置信息应尽可能简单且集中。除非有要求或必须使用,否则不要新增配置项。
  • 避免对配置信息的过分设计,应尽可能简单。
  • 确保测试已覆盖到部署或安装时的配置操作。检查应用程序所依赖的其他服务是否有效,使用冒烟测试来诊断依赖于配置项的相关功能是否都能正常工作。

2.5 环境管理

没有哪个应用程序是孤岛。每个应用程序都依赖于硬件、软件、基础设施以及外部系统才能正常工作。本书中,我们把所有这些内容都称作应用程序的环境。在第11章,我们会详细讲述环境管理,但在此处(配置管理这个上下文中),我们还是需要先了解一些内容。

在做应用程序的环境管理时,我们需要记住的原则是:环境的配置和应用程序的配置同样重要。例如,如果应用程序需要用到消息总线,那么只有正确配置了这个消息总线,应用程序才能正常工作。操作系统的配置也同样重要。比如,应用程序可能依赖于操作系统中大量的文件描述符(file descriptor),如果操作系统中文件描述符数量的默认值比较低的话,应用程序可能根本无法工作。

“临时决定”是管理配置信息最糟糕的方法。这样就会导致使用手工方式安装软件的必要部分,或需要手工编辑一些相关的配置文件。当然,这也是最常见的方法。虽然看起来简单,但几乎对于所有系统(非常小的系统除外),它都有几个很常见的问题。最容易想到的危险场景就是,当使用新的配置无法正常工作时,不管什么原因,都很难恢复到之前某个已知的正常状态,因为根本无法找到以前的配置信息记录。这里把不良环境管理可能带来的问题总结如下。

  • 配置信息的集合非常大;
  • 一丁点变化就能让整个应用坏掉,或者严重降低它的性能。
  • 一旦系统出现问题,需要资深人员花费不确定的时间来找到问题根源并修复它。
  • 很难准确地再现那些手工配置的环境,因此给测试验证带来很大困难。
  • 很难维护一个不使用配置信息的环境,因此维护这种环境下的行为也很难,尤其是不同的节点有不同的配置时。

The Visible Ops Handbook一书中,其作者把手工配置的环境称作“艺术作品”。所以,为了降低环境管理的成本和风险,有必要将环境变成可量产的对象,使对其进行的操作具有可重复性且时间是可预测的。在我们参与过的项目中,有太多项目因较差劲的配置管理而导致相当大的开销(比如,需要付费给一个或多个单独负责这方面的团队)。它还总是给开发过程拖后腿,使得开发环境、测试环境,以及生产环境的部署工作变得更复杂,成本更高。

环境管理的关键在于通过一个全自动过程来创建环境,使创建全新的环境总是要比修复已受损的旧环境容易得多。重现环境的能力是非常必要的,原因如下。

  • 可以避免知识遗失问题。比如某人离职且无法与他联系上,但只有他明白某个配置项所代表的意思。一旦这类配置项不能正常工作,通常都意味着较长的停机时间。这是一个很大却不必要的风险。
  • 修复某个环境可能需要花费数小时的时间。所以,我们最好能在可预见的时间里重建环境,并将它恢复到某个已知的正常状态下。
  • 创建一个和生产环境相同的测试环境是非常必要的。对于软件配置而言,测试环境应该和生产环境一模一样。这样,配置问题更容易被在早期发现。

需要考虑的环境配置信息如下:

  • 环境中各种各样的操作系统,包括其版本、补丁级别以及配置设置;
  • 应用程序所依赖的需要安装到每个环境中的软件包,以及这些软件包的具体版本和配置;
  • 应用程序正常工作所必需的网络拓扑结构;
  • 应用程序所依赖的所有外部服务,以及这些服务的版本和配置信息;
  • 现有的数据以及其他相关信息(比如生产数据库)。

其实高效配置管理策略的两个基本原则是:(1) 将二进制文件与配置信息分离; (2) 将所有的配置信息保存在一处。如果应用了这两个基本原则,你就能将“在系统不停机的情况下,创建新环境、升级系统部分功能或增加新的配置项”等工作变成一个简单的自动化过程。

所有这些都需要考虑。尽管把操作系统也提交到版本控制库中的做法显然不合理,但这并不意味着将它的配置信息提交到版本控制库中不合理。远程安装系统与环境管理工具(如Puppet 、CfEngine)的结合使用让我们可以直接对操作系统进行集中管理和配置。这个问题将在第11章详细讨论。

对于大多数应用来说,将这些原则应用于其所依赖的第三方软件更为重要。好的软件应该有一个能通过命令行执行的安装程序且不需要任何用户干预。应用程序的配置可以通过版本控制系统来管理,而且不需要手工干预。如果第三方软件依赖无法满足这样的要求,你就要设法找到替代品。使用第三方软件时,这应该是一个重要的评估依据。当评估第三方产品或服务时,应该问自己如下问题。

  • 我们可以自行部署它吗?
  • 我们能对它的配置做有效的版本控制吗?
  • 如何使它适应我们的自动化部署策略?

如果这几个问题的答案都是否定的或负面的,可以有几种不同的应对方式,我们会在第11章详细描述。

我们要将处于某个正确部署状态的环境作为配置管理中的一个基线。自动化环境准备系统应该能够从项目部署的历史中找到任一特定基线进行重建。只要对应用程序所在环境的任何配置做修改,就应该把这个修改保存起来,并创建一个新的基线版本,将此时的应用程序版本与这个基线版本关联在一起。这样就可以保证下次部署应用程序或创建新环境时,这些修改也会被包含在内。

实际上,你应该像对待源代码一样对待环境,增量式地修改,并将修改提交到版本控制库中。对每个修改都要进行测试,以确保它不会破坏在这个新版本的环境中运行的应用程序。

对基础设施进行配置管理

我们最近有两个项目的开发经历证明,配置管理的有效性对项目有很大的影响。

第一个项目中使用了一个消息中间件。该项目有正确的配置管理策略,以及很好的模块化设计。我们打算升级到这个中间件的最新版本。供应商承诺这个最新版本会解决我们所担心的大多数问题。

我们的客户和供应商都明显认为这次升级是件大事。他们虽然筹划了几个月的时间,但仍旧担心这会对开发团队有破坏性的影响。我们团队的两位成员按照本节所描述的方式准备了一个新的基线,我们对其进行了本地测试,包括利用它的试用版执行我们全部的验收测试套件。测试中发现了一些问题。

我们修复了最明显的问题,但它仍不能通过所有的验收测试。然而,我们非常有信心能很快修复它们,因为它们的修复方法都简单明了,而且最糟糕的情况也就是回滚到之前放在版本控制系统中的基线上。与开发团队的其他成员达成一致后,我们提交了这次修改,以便整个团队可以一起修复那些由于升级消息中间件而导致的问题。整个过程只用了一天,其间我们运行了所有的自动化测试来验证工作。在接下来的迭代中,我们在手工测试中比较细心,但并没有发现任何相关问题。我们的自动化测试覆盖率被证明是非常不错的。

在第二个项目中,我们面对的是一套运行多年、性能不佳且错误频出的遗留系统。我们的任务是做一些修缮工作。我们接手时,根本没有自动化测试,仅对源代码做了最基本的配置管理。我们的任务之一就是升级应用服务器的版本,因为供应商已不再为原有的版本提供技术服务了。对于这种状态下的应用程序,即没有持续集成系统,也没有自动化测试,这个过程走得还算平稳。然而,从修改、测试到最终部署到生产环境,一个六人团队用了两个月的时间才完成。

当然,软件项目之间的直接对比是不可能的。每个项目所用的技术有很大不同,代码库也很不相同。但是,这两个项目都涉及了同样的任务,即升级核心中间件。一个花了六个人两月的工夫,而另一个只用了两个人半天的时间就搞定了。

2.5.1 环境管理的工具

在以自动化方式管理操作系统配置的工具中,Puppet和CfEngine是两个代表。使用这些工具,你能以声明方式来定义一些事情,如哪些用户可以登录你的服务器,应该安装什么软件,而这些定义可以保存在版本控制库中。运行在系统中的代理(agent)会从版本控制库中取出最新的配置,更新操作系统以及安装在其之上的软件。对于应用了这些工具的系统来说,根本没必要登录到服务器上去操作,所有的修改都可能通过版本控制系统来发起,因而你也能够得到每次变化的完整记录,即谁在什么时候做了什么样的修改。

虚拟化技术也可以提高环境管理过程的效率。不必利用自动化过程从无到有地创建一个新环境,你可以轻易地得到一份环境副本,并把它作为一个基线保存起来。这样一来,创建新环境也就是小事一桩,点一下按钮就可以搞定。虚拟化技术还有其他好处,比如它可以整合硬件,使硬件平台标准化,即使你的应用程序需要一些不同的环境也没有问题。

我们将在第11章详细讨论这些工具。

2.5.2 变更过程管理

最后要强调的是,对环境的变更过程进行管理是必要的。应该严格控制生产环境,未经组织内部正式的变更管理过程,任何人不得对其进行修改。这么做的原因很简单:即便很微小的变化也可能把环境破坏掉。任何变更在上线之前都必须经过测试,因而要将其编成脚本,放在版本控制系统中。这样,一旦该修改被认可,就可以通过自动化的方式将其放在生产环境中。

这样,对于环境的修改和对软件的修改就没什么分别了。它也和应用程序的代码一样,需要经历构建、部署、测试和发布整个过程。

在这方面,应该像对待生产环境一样对待测试环境。测试环境所需的核准流程通常会简单一些,应该由管理测试环境的人来控制。但在其他方面,其配置管理应该与生产环境中的配置管理没什么不同。这是非常必要的,因为通过频繁向测试环境部署,可以测试用于向生产环境进行部署的流程。值得重申的是,测试环境的软件配置应该非常接近于生产环境。如果能够做到这一点,向生产环境部署时,就不会有什么异常事件发生了。然而,这并不是说测试环境必须和昂贵的生产环境一模一样,而是说只要我们使用同样的机制来管理、部署和配置这两类环境就行了。

2.6 小结

配置管理是本书其他内容的基础。没有配置管理,根本谈不上持续集成、发布管理以及部署流水线。它对交付团队内部的协作也会起到巨大的促进作用。我们希望读者清楚地认识到,这不只是选择和使用什么样工具的问题,尽管这非常重要,但更重要的是,如何正确地使用最佳实践。

如果配置管理流程比较好的话,对于下面的问题,你的回答都应该是肯定的。

  • 是否仅依靠保存于版本控制系统中的数据(除了生产数据),就可以从无到有重建生产系统?
  • 是否可以将应用程序回滚到以前某个正确的状态下?
  • 是否能确保在测试、试运行和正式上线时以同样的方式创建部署环境?

如果回答是否定的,那么你的组织正处于风险之中。我们建议为下面的内容制定出一个保存基线和控制变更的策略:

  • 应用程序的源代码、构建脚本、测试、文档、需求、数据库脚本、代码库以及配置文件;
  • 用于开发、测试和运维的工具集;
  • 用于开发、测试和生产运行的所有环境;
  • 与应用程序相关的整个软件栈,包括二进制代码及相关配置;
  • 在应用程序的整个生产周期(包括构建、部署、测试以及运维)的任意一种环境上,与该应用程序相关联的配置。

目录