走向“持续部署”
这是一篇2009年发表我的个人博客上的文章,
讲述我和Jez Humble如何在自己的产品团队中实践持续交付。
这个产品原名为“Cruise”,是CruiseControl的企业版。
现已更名为“goCD”,并已开源,地址为:github.com/gocd/gocd/
在上一篇文章中,我介绍了Cruise团队持续集成的演进过程。而2009年的软件开发领域,“要不要做持续集成?”这个问题已经不再是大家讨论的焦点,取而代之的是“如何进行持续集成”这个热门话题,而且对持续集成的生命周期有了新的理解和定义。本文将结合Cruise团队的实际情况,与大家分享持续部署的实践。
持续集成解决了软件开发中的部分问题,但还有更为重要的一部分工作有待解决,即“通过什么样的方法,可以让软件尽快地在真正的生产环境下运行,从而实现软件的价值?”。
在软件开发过程中,“从功能开发完成开始直到将其部署至生产环境中正事运行”这一阶段被称为“最后一哩”。如果从一开始就对产品发布足够重视的话,那么这“最后一哩”可能只需要几分钟,甚至几秒钟就完成了。然而,很多项目在这一阶段会花上数天,数个星期,更有甚者可能会是几个月。
为什么会这样呢?对于复杂软件来说,无论在什么环境中的部署(测试环境,试运行环境,还是生产环境)都很困难。当软件第一次被部署到非开发环境去测试,或者当软件功能及其运行环境有较大变化时,通常都会暴露出很多问题。在做用户验收测试时,常常会发现更多的问题,例如不能满足非功能需求,用户操作不方便,功能与用户真正需要的东西相差太远等等。开发团队只有修复这些缺陷后,才能再次部署测试。于是,这个过程会不断反复,直至该软件足够稳定,才可以部署到生产环境中。
即然部署到测试环境都这么困难,那么在生产环境中部署的风险岂不会更大吗?而且,更为严重的是:当生产环境部署出现问题时,摆在你面前的选择就所剩无几(通常是回滚到以前的状态,而“回滚”这段时间的停机成本是相当高的)。大多数组织将对软件产品的新版本发布采取“保守策略”,即降低软件的发布频率,这也导致两次发布之间的版本特性差异相对较大。
这样一来,发布风险并未因发布间隔时间加长而降低,反而更高。当各方面的因素综合在一起,“软件发布”这一环节就变得昂贵而又缓慢啦。与之相矛盾的是,“版本的发布过程与频率”很大程度上决定了产品可能占据的市场地位。
那么,如何更好地解决“最后一哩”这一问题呢?实现持续部署。
持续部署:将持续集成实践扩展到整个软件生命周期,即:频繁且规律性地自动构建代码并将其部署到测试环境中,选择适当的版本部署到预演环境中试运行,并在以上环境中通过一系列的自动化测试,最后选择稳定的版本部署到生产环境中,从而使开发团队尽早从最终客户那里得到反馈,而最终客户尽早得到软件的价值。
在Cruise项目刚刚启动开发之时,我们并没有专门考虑该软件系统的部署问题。但这是一款给软件产品开发团队用的软件,我们就应该自己率先使用它,即Facebook所用的开发实践——“吃自己的狗粮(Dogfooding)”。
我们决定这么做时,项目已经开始一个月了。由于没有事先考虑好升级方式,第一次升级时非常痛苦,原有的构建历史数据无法保留,而且升级过程中手工操作非常多。最后,花费了一人一天才把它搞定。从那以后,我们就算正式开启了Cruise的持续部署之旅。
Cruise是典型的Server/Agent架构。我们的测试环境包含很多种。
其中一个是用户验收测试环境(User Acceptance Testing,简称UAT环境),它由一台Cruise Server和近20台Agent组成,用于我们团队自身的持续集成与软件部署管理。
还有另一个被称为“Production”环境,它由一台Cruise Server和近70台Agent组成,是同时为其他数个项目组提供持续集成服务的,它算是一个生产环境,因为它服务于多个真正的客户项目,一旦Cruise部署失败,就意味着这些项目会承担损失,如数据丢失,团队无法工作。
虽然部署工作可以通过自动化脚本完成,但我们还是在“UAT”和“Production”两个阶段之间加上了手动开关(在Cruise中被称为manual approval),如下图所示。
第一个阶段名为“dev”,运行单元测试和代码静态检查;
第二个阶段名为“ft”,是功能集成测试,主要是与VCS打交道的集成测试;
第三个阶段名为“twist-tests”,是用Twist写的功能测试;
第四个阶段名为“dist”,是自动生成各种操作系统对应的安装包;
第五个阶段是“UAT”,是将软件自动部署到我们的手工测试环境;
第六个阶段是“production”,是我们为其它项目组提供的持续集成生产环境;
第七个阶段名为“publish”,就是上传并全球发布阶段。
前三个阶段均为自动触发,即:一旦构建成功,即自动触发下一个阶段的构建,但之后的阶段均为手工触发。
目前我们的测试包括单元测试、功能集成测试、功能验收测试和性能测试。其中单元测试、功能集成测试及功能验收测试都在同一个Pipeline中,每次代码提交都会运行这些测试。而性能测试在另一个Pipeline中,用于每次部署后,收集UAT环境和Production环境的性能指标。由于部署频率足够高,我们可以掌握性能数据的微小变化,据此来判定是否需要采取相应的优化措施。
写单元测试已经是团队的习惯,自不必说。
由于Cruise与很多代码版本管理软件(VCS)打交道,这里所说的功能集成测试是指与这些外部第三方软件交互接口的测试。
功能验收测试是指将Cruise Server和Agent在测试机器上运行起来后,再运行TWIST自动化测试套件。我们对功能验收测试的原则是:每个Story都要有功能测试覆盖,QA与开发人员共用编写功能测试用例,由开发人员实现之。而且,功能验收测试要让真实的Cruise Server和Agent进行通信的基础上进行。TWIST是我们公司的另一款产品,用于自动化功能测试,其测试编辑界面如下所示:
尽管测试数量较大,测试的绝对运行时间较长,但结合Cruise本身提供的并行运行特性,团队成员胡凯,Derek和李彦辉自行开发的测试负载均衡工具(Test-load-balancer),该工具可以将所有测试用例自动分成若干个测试子集,Cruise将这些测试子集分发到Agent集群中并行运行,使单元测试或其它测试在团队可接受的时间长度内返回测试结果(单元测试在15分钟之内,功能测试在30分钟之内)。近期还将增加数个Agent,以便继续缩短测试需要的时间。
当每次由不同人员进行部署操作时,出错的概率会增加,所以要尽可能少的人工步骤。在Cruise的Pipeline中,尽管由人来触发其中两个环境的部署,但部署过程本身是自动化的。在部署过程中,Cruise的安装包会自动关闭服务器,更新自身程序和升级数据库,然后再重新启动。所有的Agent也会以Server为准,自动更新到与其相同的版本,而不必人工去升级每个Agent(每次为70个Agent的手动升级也是很大的成本,所以我们做了自动升级这个特性)。
有人会问:“为什么要持续部署?你又如何知道部署的版本是否质量可控呢?宕机了怎么办?”
的确,没有哪个开发人员希望持续集成服务器在工作时间内宕机。但尽管我们无法百分之百确保每个部署版本都稳定,但在可预见的范围内质量可控就可以了,否则我们还是要面对“最后一哩”问题。
Cruise在最初几个迭代(迭代时间为一个星期)后,就开始用Cruise来做自己的持续集成服务器(即UAT环境)。我们让它在UAT环境上运行了两周,如果没有发现什么问题,说明版本相对稳定,就将它部署到“Production”环境上。
随着用户的增多,很多人认为,由于部署失败而导致持续集成服务器宕机的风险要高于那些新特性和修复的缺陷。因此,我们的用户要求新版本部署至 “Production”环境之前,一定要在UAT环境上运行。
项目发布两个版本以后,我们团队增加了部署频率。部署到UAT环境的频率不固定(一般为两天至一周),而部署到Production环境的频率为每周一次。也就是说,Production环境上的版本要落后于UAT一周的时间。
随着项目的进行,难免会有部署失败的情况,所以一定要有风险缓解措施。例如:
(1) 部署尽可能在用户少的时候;
(2) 部署时必须有技术人员在场;
(3) 每次部署前备份原始数据;
(4) 时刻准备回滚脚本。
实践表明,建立自动化部署管道(为什么当时的名字有点土呢?)的益处很多。在过去的几年中,ThoughtWorks利用这一方法帮助很多项目组和公司解决了他们的“最后一哩”问题。
例如,在某项目中,通过自动化部署过程,使部署频率从几天一次提高到每天一次,而且该过程耗时少于15中分钟(且仅有一分钟的停机时间)。这对软件整个生命周期的交付阶段有着积极作用,只要按下鼠标就可以准备好所需要测试环境,从而减少了人为失误造成的不必要的损失,显著降低软件发布的风险。
另外,频繁且轻松的发布让开发人员全神贯注于他们想做的事情:开发新的功能。