目前IT行业中,似乎“要不要做持续集成?”已经不再是讨论的焦点,取而代之的是“如何进行持续集成?”。在前一篇文章中,我介绍了Cruise团队持续集成的演进过程。在最后,还曾提及Cruise团队的持续部署。本文将结合团队的实际情况,与大家分享持续部署的实践心得。

“最后一哩”问题

持续集成解决了软件开发中的部分问题,但还有更为重要的一部分有待解决,即“通过什么样的方法,可以让软件尽快地在真正的生产环境下运行,从而实现软件的价值”。在软件开发过程中,“从功能开发完成开始直到将其部署至生产环境中正式运行”这一阶段被称为“最后一哩”。如果从一开始就对产品发布足够重视的话,那么这“最后一哩”可能只需要几分钟,甚至几秒钟就完成了。然而,事实上大多数项目在这一阶段会花上几个星期,更有甚者可能会是几个月。

为什么会这样呢?对于复杂软件来说,无论什么环境中的部署(测试环境,试运行环境,还是生产)都很困难。当软件第一次被部署到非开发环境去测试,或者当软件功能及其环境有较大变化时,通常都会暴露出很多问题。而在做用户验收测试时,常常会发现更多的问题,例如不能满足非功能需求,用户操作不方便,功能与用 户真正需要的东西相差太远等等。而开发团队只有修复这些缺陷后,才能再次部署测试。于是,这个过程会不断反复,直至该软件足够稳定,才可以部署到生产环境中。

即然部署到测试环境都这么困难,那么在生产环境中部署的风险岂不会更大吗?而且,更为严重的是:当生产环境部署出现问题时,摆在你面前的选择就所剩无几啦(通常是回滚到以前的状态,而“回滚”这段时间的停机成本是相当高的)。

上述原因就会导致大多数组织对产品的发布采用“保守策略”,即降低软件的发布频率,这也导致两次发布之间的版本特性差异相对较大。这样一来,发布风险并未因发布间隔时间加长而降低,反而更高了。当各方面的因素结合在一起时,软件发布这一环节就变得昂贵而又缓慢啦。而通常“发布过程与频率”决定了产品在市场中的位置。

那么,如何更好地解决“最后一哩”这一问题呢?实现持续部署! 即将持续集成实践扩展到整个软件生命周期频繁且规律性地自动构建代码并将其部署到测试环境中,然后通过一系列的测试,选择适当的版本部署到预演环境中试运行,最后选择稳定的版本部署到生产环境中,从而使开发团队尽早从最终客户那里得到反馈,而最终客户尽早得到软件的价值。

“持续部署”源于部署时的痛苦

在使用Cruise来构建Cruise本身以后的第二周后,当我们想再次升级它时,因没有事先考虑好升级方式,结果用了两人天才把它搞定。从那以后,我们就开始了Cruise的持续部署之旅。

持续部署环境

目前Cruise的研发环境中,有一个被称为“UAT(User Acceptance Testing)”的测试环境,目前它由一台Cruise Server和近20台Agent组成,用于Cruise团队自身的持续集成与部署管理。还有另一个被称为“Production”的预发布生产环境,它由一台Cruise Server和近70台Agent组成,由多个项目组同时使用。“Production”环境是真实的生产环境,部署失败,就意味着损失。因此,虽然部署工作可以通过自动化脚本完成,但我们还是在“UAT”和“Production”两个Stage之前加上了人工开关(manual approval),如下图所示。前三个Stage全部是自动触发,其后全部为手工触发。每个待发布的版本都会先被部署到UAT环境中,实际上也就做了试 运行,如果该版本稳定,则部署到“Production”环境中。这样就使部署风险尽在掌控之中。

走向“持续部署”_生产环境

让持续部署成功的要点

1. 充分而广泛的自动化测试覆盖

目前我们的测试包括单元测试、End2End测试、功能测试和性能测试。其中单元测试、End2End测试及功能测试都在同一个Pipeline中,每次代码提交都会运行这些测试。而性能测试在另一个Pipeline中,用于每次部署后,收集UAT环境和Production环境的性能指标。由于部署频率足够,我们可以掌握性能数据的微小变化,据此来采取相应的优化措施。

写单元测试已经成为不争的事实,自不必说。另外,由于Cruise与很多版本管理软件打交道,这里所说的End2End测试是指与这些外部接口的测试。而功能测试是指将Cruise Server和Agent真正在测试机器上运行起来后,再运行TWIST自动化测试套件。我们对功能测试的原则就是每个Story都要有功能测试覆盖,QA与开发人员共用编写功能测试用例,由开发人员实现之,而且功能测试要让真实的Cruise Server和Agent进行通信的基础上进行。TWIST是我们公司的另一款产品,用于自动化功能测试,其测试编辑界面如下所示:

走向“持续部署”_单元测试_02

2. 尽可能短的测试反馈时间

尽管测试数量较大,测试的绝对运行时间较长,但结合Cruise本身提供的并行运行特性,团队成员胡凯,Derek和李彦辉自行开发的测试负载均衡工具(Test-load-balancer)将所有测试分成若干份,Cruise将其分配到Agent集群中同时运行,使单元测试或其它测试在可接受的相对时间内完成(单元测试在15分钟之内,功能测试在30分钟之内)。近期还将增加数个Agent,以便继续缩短测试需要的时间。

3. 部署过程自动化

当部署复杂软件时,都会使用人工过程,而且可能会花上几天的时间。而这种部署过程通常比较复杂,而且很难可靠地重复操作。因此,人们会写一些文档帮助这一过程,但文档常常更新不及时。有时,还需要几个关键性人物同时在场才能完成。

当每次由不同的人员进行部署操作时,出错的概率就增加,所以要尽可能少的人工步骤。

在Cruise的Pipeline中,尽管由人来触发两个环境中的部署,但部署过程本身是自动化的。在部署过程中,Cruise的安装包会自动关闭服务器,更新自身程序和升级数据库,然后再重新启动。所有的Agent也会以Server为准,自动更新到与其相同的版本,而不必人工去升级每个 Agent(每次为70个Agent的手动升级也是很大的成本,所以我们做了自动升级这个特性)。

4. 部署过程要保证数据安全

如果因为持续部署而导致数据丢失或错误,会得不偿失。所以,我们每次修改数据库结构或配置文件的结构后,都会写出相应的迁移脚本,并在部署过程中运行。

目前我们使用由Thoughtworkers开发的开源项目DBDeploy来做数据库升级。对于XML配置文件的修改,我们也自行开发了一个迁移框架。

5. 在稳定的前提下,尽早部署

有人会问:“为什么要持续部署?你又如何知道部署的版本是否稳定呢?宕机了怎么办?”的确,没有哪个开发人员希望持续集成服务器在工作时间内宕机。尽管我们无法百分之百确保每个部署版本都稳定,但在可预见的范围内稳定就可以了,否则我们就无法解决“最后一哩”问题。

Cruise在最初三个迭代(迭代时间为一个星期)后,就开始用Cruise来做自己的持续集成服务器了(即UAT环境)。我们让它在UAT环境上 运行了两周,没有发现什么问题,说明版本相对稳定,就将它部署到“Production”环境上了。在那以后,随着用户的增多,很多人认为由于部署失败而 导致持续集成服务器宕机的风险要高于那些新特性和修复的缺陷。因此,我们的客户要求新版本部署至 “Production”环境之前,一定要在UAT环境上运行。目前,Cruise部署到UAT环境的频率不固定(一般为两天至一周),而部署到 Production环境的频率为一周。也就是说,Production环境上的版本要落后于UAT一周的时间。

6. 完善的风险缓解措施

随着项目的进行,难免会有部署失败的情况,所以一定要有风险缓解措施。例如:

(1) 部署尽可能在用户少的时候;(2) 部署时必须有技术人员在场;(2) 每次部署前备份原始数据;(3) 时刻准备回滚脚本。

7. 将同样的产物部署到不同的环境中

让你的产品可以部署到不同的环境中,如果这些环境的环境配置不同,则把有关环境配置的内容排除在你的产品之外。如果你有很多个配置变量,请让这些配置保存在同一处,而且有默认值。

基于这一原则,Cruise唯一的配置就是XML文件。

8. 不断的反思与重构

这一点就没有什么可说的了。它适用于所有的活动。

小结

实践表明,建立自动化部署管道的益处很多。在过去的几年中,ThoughtWorks利用这一方法帮助很多项目组和公司解决了他们的“最后一哩”问题。例如,在某项目中,通过自动化部署过程,使部署频率从几天一次提高到每天一次,而且该过程耗时少于15中分钟(其仅有一分钟的停机时间)。这对软件整个生命周期的交付阶段有着积极作用,只要按下鼠标就可以准备好所需要测试环境,从而减少了人为失误造成的不必要的损失,显著降低软件发布的风险。另外,频繁且轻松的发布让开发人员全神贯注于他们想做的事情:开发新的功能。