当你有了持续集成需要的构建服务器和脚本之后,下一个问题肯定是:“我们该拿这些构建版本怎么办?”持续交付,以自动化或半自动化方式,将构建版本从一个环境提送(promote)到更接近实际生产的交付准备环境;这常常是公司在这方面演进的下一步。
任何规模的公司都可以实施持续交付,但是具体流程会根据公司具体情况差异很大。显而易见,四人全能团队的需要,与大规模、多团队、配备正式 QA 和设备精良的产品支持部门这样的公司,二者一定有很大差别。本文没有试图提出万能方案,而是覆盖多种场景和选择。
选择持续交付工具集
为持续交付选择工具集,是最不重要的决策。当你制定出工作流程之后,只需要选择相匹配的工具集即可。考虑到初始设置和配置的工作量,花几天时间构建自己定制的工具也不是非理性选择。
更重要的是,这样做没有锁定的风险。跟选择版本控制系统不同,你可以使用多种持续交付工具,而且可以互相之间自由切换。QA 团队从开发人员那里拉出构建版本使用一种工具,向准备服务器推送时使用另一种工具,这样的事情也有所耳闻。
基线场景
在基线场景中,我们要以有限的资源找出公司中的模式。像由 3 到 4 人构成的 IT 部门,常常兼做开发和系统管理。这样规模的团队常常支持中小型业务,特别是不以技术为主业务的公司。大型公司也有可能这样组织,把人分成多个、很可能是互相独立的小组,各小组之间也没有交互。
在真正开始进行持续交付之前,需要做一些必要的设置工作。首先,最重要的是要有版本控制系统,以及与之匹配的构建服务器。这第一台构建服务器将会是你的持续集成服务器,它要保证每次签入动作都能成功构建。一般来说,你需要一个“现成”的构建服务器担当此任。能够监控签入动作还要自动初始化构建,要想手工开发这样的东西,真正操作起来常常要比听起来难得多。即使可以在版本控制系统中加入触发机制,完成诸如失败构建通知这样的功能也不值得花太多功夫。
即使在资源有限的作坊式公司,交付准备服务器(staging server)也是持续交付的必要组成部分。交付准备服务器应该尽可能模拟出生产环境。“你有多少预算”常常是首要问题。如果你的生产环境有价值几十万美元的数据库服务器,很可能你无法负担搭建一个同样的交付准备服务器。而且可能你也不想这么做。
在模拟生产环境时,有一个常见错误:过高匹配硬件环境。假如你的生产环境每秒处理 100 个请求,如果为交付准备服务器购买同样硬件,但测试时每秒只执行几个请求,你的结果就是有问题的。理想情况下,为了模拟生产环境请求,你还应该购买并设置负载服务器,但这么做成本太高,而且耗费时间。对于这种规模的团队,降低交付准备服务器处理能力是更佳策略。
另一个常常忽略的需求,是构建的版本化。必须有某种唯一的识别机制,保证可以区分开每个构建和其他构建版本。如果针对单一的公共主干代码,简单的时间戳或是自增长版本号也就够了。本文后面会讨论更复杂的场景。
结构
有了上面列出来的部件,现在你的环境应该跟下面的图很像:
交付策略
构建服务器编译代码,交付准备服务器等候接收构建结果,有了这二者之后,下一步就是决定你的部署策略。小团队有两种选择:“签入时交付”和“定时交付”。
签入时交付
“签入时交付”策略的优势在于马上产生的满足感。根据代码库的规模,从签入新功能代码到能够在交付准备服务器上测试,一两分钟就够了。
这种方式的主要问题在于:交付准备服务器会被蹂躏得不稳定。很多时候,我见到有人试图测试某个功能,突然新的版本推到交付准备服务器上了,破坏了正在运行的测试。更糟糕的是:交付准备服务器常常作为演示服务器使用,在某些重要的演示时,很可能出现严重的后果。
代码混乱是另一个问题。比如,如果三次签入在很短的时间内先后进行,那么可能触发三个构建、交付周期,实际上只有最后一次才是必要的。极端情况,这会带来很大混乱,交付部署环境将无法有充足的时间展示自己的功用。不过,大多数构建服务器都有选项,可以延迟构建的开始,或是在给定时间间隔内避免多次构建。
定期交付
定期交付策略更易于预测。所有人都知道交付何时启动,并可以规划自己的代码签入是在交付之前还是之后进行。典型做法是一天构建/ 交付一次或两次。
每日构建的缺点在于,它会给工作环境带来不必要的压力。开发人员会发现自己要赶着在构建截止时间之前完成一次签入。在每日深夜构建,这时开发人员应该不在工作,可以减轻压力,但也意味着除非到第二天,他们无法执行第二层测试。
当我们开始使用“每日构建”这样的词汇时,很容易忘记我们实际上不是在谈论构建。我们真正在谈论的,是完整的构建、交付周期。有了持续集成,如果构建失败,你马上就能知道。接下来是要修复问题,还是要回滚签入,就很简单了;只要规划好的交付完成了,软件就已经准备好部署到生产环境。
部署到生产环境
该场景的假设是:实在没人能够完成任何特定版本前要做的大量测试工作。只要交付准备服务器上的检查完成,构建版本就会直接提送到生产环境中。但即使如此,也有多种选项,各有其妥协之处。
从交付准备阶段提送
将经过验证的构建版本从交付准备服务器直接提送,是常见的选择。其优势在于高度确定性。理论上,测试一个版本的构建、然后错误地部署另一个版本,这样的事情不可能发生。写脚本把文件从交付准备服务器拷贝到生产环境,也很容易。
但像很多事情一样,理论跟现实总是有差距的。有了到交付准备服务器的自动化推送,很可能出现这样的情况:测试完成,去倒一杯咖啡,回来后工作针对的构建版本可能就完全不同了。更糟糕的情况是:构建代理会开始覆盖交付准备服务器上的文件,而这时这些文件正在推送到生产环境服务器上。
从构建服务器提送
将构建版本从构建服务器直接提送是更好的选择。这可以消除我上面提到的那种方式中的多种时间冲突。而且,人们更容易知道被推送的是哪个版本。
这种方法也有弱点:人们很容易选择错误的版本推送到生产环境中。
重新构建并推送
第三种选择是为持续集成服务器创建新的构建选项,其中包括推送到生产环境这一步。我提醒大家要小心这种方式。虽然理论上重新构建的代码与你测试的代码没有区别,但还是有某些东西增加了出错的可能性。
另一个问题是:这让你可以选择多种编译选项。如果你可以选择,你可能希望在交付准备服务器上使用调试构建版本,在生产环境上使用发布构建版本。如果有些行为在两个构建配置中有变化,这会产生灾难性后果。
虽然使用持续集成服务器做配置很容易,但我再次强调:我不推荐人们选择这种方式。
场景2:加入QA
如果QA 加入进来,事情就变得非常复杂了。因为你现在必须处理跨团队的沟通和规划,你需要更多的构建环境,还有更具意义的所有制感觉。
结构
一旦QA 加入,你可能需要至少3 个非生产环境服务器。在某些特定条件下,代码变更从一台服务器提送到下一台服务器。
集成环境
这是功能代码签入后,构建版本到达的第一台服务器。被称为“集成环境”,是因为签入功能代码会与其他功能代码完全集成。用来现场检查一个构建版本是否适于提供给QA。这个环境中允许不稳定,但是不允许构建版本停留太长时间。
QA 环境
这是 QA 团队完成自己大部分工作的地方,根据需要会从集成环境更新代码。
交付准备环境
交付准备环境现在完全用作演示和在构建进入生产环境前的最后检查。任何构建版本到了这个环境,都应该是牢固可靠的。交付准备环境也许可以连接到生产环境资源,比如数据库和文件系统,但不是必须这么做。
QA 交付选项
从构建服务器向集成环境交付代码,可以使用基线场景中提到的多种方式之一。从集成到 QA 环境就需要动动脑子,因为涉及多个团队。下面是我看到的一些成功模式。
开发者发起
在“开发者发起”模式中,开发人员决定何时进行编译检查和把构建版本提送给 QA。说得难听一点,当 QA 完全“服从”开发人员时,可以使用该模式。可能第一眼看上去,这对开发人员来说还挺好,实际上常常阴暗着某种问题。比如:如果发生某个质量上的问题,QA 人员可能要等很长时间,等着阻碍他们工作的 bug 得到解决。
极端情况下,需要设置自动提送机制,定时自动提送到 QA 环境。
QA 发起
这是更适合大部分团队的典型方式。开发人员仍然参与,他们需要在集成环境中检查他们的构建,然后确定构建版本是否可以提送。
在这种方式下,QA 准备好测试新功能时,他们会拉过来“已知正常”的最新构建版本。通常是 QA 经理完成该工作,一般来说他对于 QA 人员的需要最了解。虽说如此,有些 QA 团队允许所有成员提取新构建版本。
Test Runner 启动
对于想真心做好自动化测试的公司来说,这才是目标。构建版本到达集成服务器后,整个自动化测试套件就开始运行。如果全部通过,构建版本会自动提送到 QA。跟其他自动化交付方式一样,可以采取签入时方式或定时方式。
不要低估这种方式需要投入的成本。不仅需要有完整的测试套件,所有的测试必须还都能通过。构建服务器无法区分测试失败是由于新功能出问题,还是说有些问题需要留待将来解决。
变通方式,是把测试拆分成必须通过(must-pass)的项目和临时项目。测试从临时项目开始,特别是在 TDD 风格编程中用到的测试。只要测试验证正确、有用,代码也可以通过这些测试,就转而处理必须通过的项目。构建服务器不会运行临时测试,但是会看重必须通过的测试项目的结果。
交付准备 / 生产环境交付选项
在持续交付思想指导下,QA 对于接收到的给定构建版本,只有两个选项:构建要么失败,要么提送到交付准备服务器。QA 不会一边处理一个可用的构建版本,同时等待某些功能完成
这的确提出一个问题:如何定义某个构建版本是“可用的”?任何可以安全放到生产环境的构建版本,都是可用的构建版本。如果其中有未完成的功能,但这些功能可以以 as-is 方式工作,那么构建就可以往前提送。除非某个失败的功能会影响应用在生产环境中的使用,构建版本就不能停滞在 QA 这里。
进入交付准备服务器后,构建版本在下个发布周期中就要提送到生产环境。尽管持续部署到生产环境很难完全实现,还是经常听到成功的每周、甚至每日部署。关键点在于:一旦一个某件版本证明没有问题,就需要快速移动到生产环境,这样团队就可以聚焦于下一系列要开发的功能。如果构建版本停留在交付准备阶段长达几周乃至几个月,就会造成无尽的问题。
把 QA 环境和交付准备环境分开,是为了推进工作流。只要一个构建版本提送到交付准备服务器,下一个构建版本就会从集成服务器拉过来。这样一来,交付准备服务器就总会有一个稳定的环境,供利益相关者和其他第三方查看演示版本,同时 QA 仍有自己可以工作的环境。如果把服务器的职责混合起来,当环境锁定时,QA 的工作就必须要停下来。
配置相关问题
一旦有了多个环境之后,配置文件就会成为严重的问题。举个例子,交付准备服务器必须配置正确,避免发生诸如发送测试邮件给所有用户,或是通过正式支付网关下订单等问题。我曾与一个刚入门的开发人员一起工作,他当时试图通过一个配置错误的测试服务器购买几百万美元的债券。(幸运的是,每张债券的价格比实际价格高,因为订单没有成功购买。)
出现这种状况,因为生产环境的配置保存在版本控制系统中,并与应用一起部署。这么做,是要防止出现非生产环境的配置放在版本控制系统中,然后被部署到生产环境服务器上的状况。
要避免上述问题,有个出人意料的解决方法:不要让构建代理的网络账号对配置文件有写权限。这样,如果有人不相信签入了配置文件,部署将会失败,错误就可以得到纠正。
不过这么做也有自己的问题。用这种方法,只要文件中的配置需要修改,就必须手工操作。如果失败了,将会使得环境出现问题,从而带来风险,因为数据更新随时可能发生。
关注点分离和配置文件
“关注点分离”这个词常常用来说明正确的设计决策应该怎么做,如果认真思考谁应该负责做什么,这种方法也是很有帮助的。比如,负责生产环境技术支持的人不关心开发人员选择注入哪种日志记录框架。然而,他们关心数据库连接串和警告发送的邮件地址。
我推荐下面这些类型的配置文件。
环境设置:与特定环境相关的值,必须按照服务器分开设置。只有某些重要事件发生时,才会需要改动它们,比如新数据库或新文件服务器上线。
代码作为配置:类似于驱动依赖注入框架的 XML 文件。虽然看起来像配置文件,但除开发人员外任何人都不应该碰它们。这些文件必须要放在版本控制系统中,而且应该把服务器上的文件标记为只读。
微调配置:这些配置与环境无关,但也许生产环境支持人员需要访问。可能包括比如批量上传批次限制、或是 web 页面请求超时设置等。
这三种配置中,微调配置需要付出最多精力以保证正确。理想状态下,应该有缺省配置放在文件中,并保存于版本控制系统,并可以根据特定服务器修改出多个文件,并且不必控制这些文件的版本。
配置和培训
避免增加不必要的配置设置,有一个技巧:要求为每个配置项提供文档和培训。如果不花时间教给生产环境支持人员何时、为何调整某些值,他们就无法完成这些工作,这些值就成为无法配置的摆设。
场景 3:使用 SOA 的多个团队
使用 SOA 时,通常采取多团队方式。比如,一个团队构建数据库和服务,另一个团队处理 UI,这种情况很常见。有时,两个团队的工作会非常紧密,相关团队成员会常常互相交换。有时,团队可能来自地球两边的不同公司。不管他们怎么切分,基本的模式相同。
结构
如场景 2,服务团队需要一个地方来测试构建版本,这样他们不会对团队之外造成负面影响。同时,UI 开发人员需要一台可信的服务器,能够一直保持稳定,否则他们就无法完成自己的工作。因此,除了我们前面提到的单一团队场景,有必要加入“开发环境”。
不考虑UI 集成阶段,我们看到的顺序与场景2 相同。交付选项相同,附加条件是只有UI 团队参与在这个流程中。UI 团队对于构建版本质量的意见比服务团队的意见重要。
开发交付选项
何时以及如何将代码交付给开发环境,是很多紧张情势的来源。有问题的构建版本到了QA 环境,QA 团队只要让其失败,然后就可以转向其他任务,比如为新功能准备测试,或是改进回归测试。要是有问题的构建版本到了开发环境,整个UI 团队就会发现自己无法工作了。因此,虽然QA 交付中的多种交付模式都可以运作,基于自动化测试的方式目前是最成功的模式。
说明:谁编写集成测试?
处理分离的服务层时,提供服务和消费服务的团队都要编写自动化测试,这很重要。提供服务层的团队最了解服务层内部机制,因此能编写出别人不知道、或是了解其重要性的测试。
不过,这不能成为消费服务的团队不写测试的接口。他们的测试覆盖的场景不仅服务开发者想不到,而且能测试他们对于服务层的理解。比如:UI 开发人员会假定某个给定调用永远不会返回null 或负值。测试过UI 使用的所有参数组合后,他们就可以确保自己的假设没有问题。
如果你的公司有QA 工程师保佑,而不仅仅是只有QA 分析人员,他们会发现自己也要针对服务层开发自动化测试。这常常与自动化UI 测试连在一起,特别是动作结果不需要通过用户界面验证的时候。
场景4:采用并行功能开发的多个团队
这种情况很棘手。目前,前面提到的各个场景都假定只有一个单独的开发主干。一旦开始处理多个开发团队针对同一代码库并行开发,就必须决定何时、如何将功能代码从团队分支转移到基本开发主干中。下面两种模型是我见过的成功范例。
功能推送模型
在该模型中,不管何时,只要确定没有问题,每个团队都会把自己的变更推送到主干中。该模型的优点在于:团队可以自给自足。
合并- 推送
该模型中最常用的策略是:在本地合并及测试。只要测试通过,修改的部分就可以推送到主分支。
该模型最大的风险是缺乏原子合并。很可能某个团队修改了某个函数的名字或签名,而另一个团队正在加入新的文件使用了同一个函数。如果两个更改同时签入,构建版本就会失败,而且版本控制系统不会报告任何冲突。
锁定- 合并- 推送
该方式需要版本控制系统支持锁定。推送新的构建版本时,主干应该是锁定的。新的功能要在本地与主干合并,完成冒烟测试,然后推送到主干。虽然合并的问题可以在锁定时解决,任何测试的失败必须马上把锁释放掉。
功能拉模型
该模型中,团队永远不能发布他们的变更。相反,变更控制团队的人负责将功能拉入到主干中。这样一来,QA 团队就只会接收到他们准备好测试的变更。
功能拉模型需要高级的版本控制系统,支持集成工作项跟踪。仅仅给变更打上任务号是不够的,成员必须要说明“将功能X 合并入分支Y”,并让版本控制系统识别所有需要的变更。这可以手工完成,但是会耗费很长时间,而且很容易出错。
对于简单的合并,变更控制工程师可以自己一个人搞定。复杂的合并,特别是团队们没有定期更新自己分支的情况下,需要开发相关功能的团队协助合并工作。
结构
不管功能特性如何进入主干,结构都是一样的。每个团队都有自己的集成环境,他们会不断发布,就像基线场景一样。这些团队特定的集成环境向通用集成环境导入代码,然后就走正常流程了。
修正补丁(hot fix)如何处理?
所有这些场景中,我们都没有提到修正补丁的概念。这是有意为之,如果遵循持续交付的思想和理念,是没有所谓的修正补丁的。一旦变更进入集成环境,它们就会很快进入生产环境。在这个理论下,不需要修正补丁,只有正常的bug 修复,其优先级偶尔会超过功能特性的开发。
然而,现实世界不是完全由理论指导的。功能常常会停滞在QA 阶段,其时间超过预期,要么是质量问题,或者仅仅是因为它们的规模。与之类似,生产部署也可能延迟,因为业务需要,比如合同强制要求,或是早已公之于众的特定日期升级计划。类似事件发生时,修正补丁就有必要了。此时,最佳方案是抛开流程,完成工作优先。不要让形式成为公司和客户不必要的负担。一旦尘埃落定,危机解决,就可以开始研究问题发生的根本原因了。
持续交付的目标,不是要让修正补丁更易于处理,而是要制定出编码和测试的标准,消除对修正补丁的需要。每次流程失败的时候,就是你学习如何改进代码标准和测试实践的机会,避免重大bug 再次发生。同样地,这也能为你提供理由,检查日程表制定方针中的缺陷,看看是什么导致流程的停滞和问题。如果无法同时在这两方面聚焦,你就永远不能保证所有的bug 修复都可以通过严格控制的流程。
简而言之,持续改进是任何形式的持续交付的根本组件。
关于作者
Jonathan Allen从 2006 年开始就为 InfoQ 撰写新闻报道,目前是.NET 领域的首席编辑。如果您希望为 InfoQ 编写新闻或是有价值的文章,请通过 jonathan@infoq.com 联系他。
查看英文原文: Patterns for Continuous Delivery
给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。
评论