「第12章 低风险发布」

  CREATED BY JENKINSBOT

在前面的几章中,主要讨论了「快速验证环」中「构建」阶段的工作。通过在业务需求协作流程、软件配置管理、持续集成、自动化测试等多方面的管理改进,缩短研发质量反馈时间,提升软件应用的研发速度。

在本章中,我们将主要讨论如何高频、低风险地进行软件部署和发布,尽早让软件在生产环境中运行,如图所示的「快速验证环」中「运行」阶段的工作:

「快速验证环」的「运行」的主要内容包括:

高频发布的背后动机与收益;

降低发布风险的相关方法如蓝绿部署、金丝雀发布(或灰度发布)和暗部署( dark launch);

支撑高频发布的相关技术手段如开关技术、数据迁移方法、抽象分支策略等。

12.1 高频发布是一种趋势 211

自2001年“敏捷宣言”诞生以来,一直明潮暗涌,2007年以前,国内对“敏捷软件开发”的认同度并不高。

甚至,某些传统T企业说:“我们不需要那么快速地交付软件,‘敏捷’不适合我们。”

相反,互联网公司则说:”‘敏捷’两周发布一次,太慢了,不适合我们。”

直到2009年,Flickr的John Allspaw和 Paul Hammond在Velocity 2009年的大会上分享了题目为《10+ Deploys Per Day: Dev and Ops》的报告,让业界同仁眼前一亮。原来,软件发布还可以这么快。

12.1.1 互联网企业的高频发布 212

现在,世界领先的互联网公司都在以“频繁发布”的模式更新它们的软件产品。

例如,早在2011年5月,亚马逊公司的月度统计数字表明,平均每1.6秒就触发一次软件部署操作、当月最高部署频率达到每小时1079次之多。平均1万台服务器会同时收到部署请求,而最高一次是3万台服务器同时执行一个部署操作。

在2017年, Facebook公司每天对其网站推送多次部署,如图a和图b所示。而其移动应用客户端每周向应用市场推送一次,其研发流程如图c所示,每天面向内部员工发布最新的内部全员体验版本,并且面向十万和百万用户推送Apha版和Beta版

「Facebook的部署及发布(
SEPTEMBER 12, 2017 Rapid release at massive scale)」

当然,这些发布中并不全是功能发布,当然也会包含缺陷修复。例如,根据2013年Dror G. Feitelson、 Kent L.Beck等发表的「Development and Deployment at Facebook」一文,图中展示了 Facebook,公司网站毎日发布的内容分布。我们可以看出,其中有50%与问题修改相关:


那是否表明网站质量堪忧呢?根据 Chuck Rossi Kent Beckd在“ Continuous Deploymenof Mobile Software at Facebook”一文中所述,随着部署频率的提升,以及总代码量和提交次数的提高,「严重缺陷数量」并没有随之升高,如图所示(横轴是每个月的代码提交次数,纵轴是生产环境上发现的缺陷数)。尽管严重程度为中级和低级的缺陷数量有所上升,是 Facebook公司可以接受的程度。


Etsy是一家以手工制品交易为主的P2P电商。2014年,它有约60万的月活跃用户量和每月15亿的页面浏览量,销量为19亿美元。2017年,商品销售额达到32亿美元。在第2章中,我们介绍了他们的工作哲学,即“持续试验”。该公司自2009年底开始全面转向持续交付模式。在此之前,他们使用的是瀑布开发模式。当时部署发布操作的特点是“慢、复杂、高度定制化”,而2010年后,部署操作的特点是“快、简单、一致”,如表所列:


Ety毎次部署会包含以下内容:

(1)应用软件服务新增了一个类或方法。

(2)页面上的图片、样式表或者一些模板文件。

(3)内容变更等。

(4)修改配置开关的取值,或者灰度部署。因此,每次发布既可能是对线上问题进行快速响应(例如修复安全风险、功能缺陷、限流、降载或者增减节点),也可能是修改了软件配置项,发布补丁等。

12.1.2 收益与成本共存

在高频发布模式中,每次发布的内容量通常都会少于在低频发布中每次发布的内容量(显然一天可以完成的功能比十天的少)。为什么这么多公司都在向“高频发布”这个方向迈进呢?

高频发布的收益有以下几个:

(1)有更多的机会与真实用户互动,从而快速决定或调整自己产品前进的方向。

(2)由于每次变更规模较小,软件系统没有剧烈的变化,从而降低部署风险。

(3)单次部署成本降低,且趋于恒定。如表所列,Ey在使用大版本发布模式时每次部署都需要花费大量的精力与时间:在2010年执行高频发布以后,每次部署所需精力与时间很少,且基本不变。因为频繁的部署操作会令人感到痛苦,就会有动力做很多自动化设施建设,从而减低成本和精力。

(4)出现问题易定位、易修复,且能够快速更正。


据2017年DevOps报告所述,高绩效(high-performance)的团队比低绩效(low-performance)团队相比:

(1)其代码发布频率高出46倍

(2)从代码提交至代码部署所用时间缩短为1/440

(3)平均故障恢复时间缩短为1/96

(4)变更故障率降低为1/5

上述收益来自成熟且自动化的部署与发布操作。如果仍旧坚持低频发布模式所用的研发管理方法,则强行执行高频发布会带来较高的送代成本。

例如,某团队原来每个月手工发布一次,现在决定每周发布一次。暂且不讨论每个版本的质量验证成本会如何变,假设仍旧采用原有手工模式,那么每月的工作量就是原来的4倍。而且,在发布周期缩短后,原来工作模式中并不占用太多成本的操作(如编译时间、测试工作强度等)都会变成较为突出的矛盾。

然而,无论怎样,我们都无法100%消除发布风险。我们要做的是不断寻找降低发布风险的方法。

12.2 降低发布风险的方法

接下来,我们就分别讨论一下降低发布风险的方法,包括蓝绿部署、滚动部署、金丝雀发布(灰度发布)、暗部署。

(!降低发布风险、降低产品风险!)

12.2.1 蓝绿部署(Blue-green Deployment)

蓝绿部署(blue-green deployment)是指准备两套完全一致的运行环境,其中一套环境作为正式生产环境,对外提供软件服务。另一套环境作为新版本的预生产环境,部署软件的新版本,并对其进行验收测试。当确认没有问题后,将访问流量引流到这个新版本所在的环境中,作为正式的生产环境,同时保持旧版本所在环境不变。直至确定新版本没有问题后,再将旧版本所运行的环境作为下一个新版本的预生产环境,部署未来的新版本。

如图所示,“蓝”和“绿”仅代表两个相互独立的部署环境:

当然,这是一个非常理想的情况。现实中,数据库复制的时间成本比较高,而且空间成本也比较高。因此,很多蓝绿部署方案会使用相同的数据库服务,只是软件的部署使用不同的两套环境。如图所示,在这种情况下,同一个数据存储格式必须对新旧两个版本做兼容性处理,使其可以同时服务于两个软件版本对数据的操作。

另外,蓝绿部署中还有一个需要处理的问题:

也就是,当切换发生在用户的一次业务操作过程当中且涉及事务处理时,如何处理数据的一致性问题。

一般来说,切换并不是瞬间完成的。

在切換的过程当中,新的请求直接被导向到新版本的环境、不再允许访问旧环境。

对于那些在切换发生时尚未返回结果的旧有请求,旧版本的环境允许其完成,之后不再接收新的请求即可。

12.2.2 滚动部署(Rolling Deployment)

滚动部署(rolling deployment)是指从服务集群中选择一个或多个服务单元,停止服务后,执行版本更新,再重新将其投入使用。循环往复,直至集群中所有的服务实例都更新到新版本,如图所示。与蓝绿部署相比,这种方式更加节省资源。因为它不需要两套一模一样的服务运行环境。因此,服务器的成本就相当于少了一半。

当新版本出现问题时:

这种滚动部署方式无法像「蓝绿部署」那样只要直接通过前面的「流量负载均衡器」直接切换回旧环境即可,而是必须要对其中已部署新版本的服务器进行回滚。

另一种方式就是快速修复问题,生成第三个版本V3,然后马上发起一次V3的滚动部署。此时,服务集群中就可能会有V1、V2、V3三个版本存在。

12.2.3 金丝雀发布与灰度发布(Canary Release)

“金丝雀发布”(canary release)就是泛指通过让一小部分用户先行使用新版本,以便提前发现软件存在的问题,从而避免让更多用户受到伤害的发布方式。因为仅有一小部分用户使用,所以造成的影响也比较小。

“金丝雀发布”的名字来自矿工下井的一个古老实践。17世纪,英国矿井工人发现,金丝雀对瓦斯这种气体十分敏感。当时,采矿工人为了保障自身的安全,毎次下井工作时都带上一只金丝雀。如果井下存在有害气体,在人体还没有察觉到有害气体时,金丝雀就会因无法抵抗瓦斯气体而死亡。此时,矿工就会知道井下有毒气,马上停止工作,回到地面。

「灰度发布」是指将发布分成不同的阶段,每个阶段的用户数量逐级增加。如果新版本在当前阶段没有发现问题,就再扩展用户数量进入下一个阶段,直至扩展到全部用户。它是「金丝雀发布」的一种延伸,也可以说,「金丝雀发布」是「灰度发布」的初始级别,如图所示。对于“划分多少个阶段,每个阶段的用户数量是多少”,要根据产品状态自行定义。

有这样一个案例:

在2012年,Facebook公司对其网站首页做过一次大改版,引入了用户首页的大图展示,并以「灰度发布」进行发布。当用户数量为总用户数的1%时,网站的很多项数据(如浏览量、页面打开率等)都有所下降。经过讨论后,团队认为是这些用户对新版本暂时性不适应,决定继续扩大用户数。当用户超过10%以后,数据表明,各项关键业务指标仍旧表现不佳,因此Facebook公司最终放弃了这次首页大改版,恢复了原有的版本。

有两种实现方式可以达到「金丝雀(灰度)发布」的效果:

一是通过开关隔离方式实现(「开关技术」的具体方式「功能技术开关」部分),它是指:将软件的新版本部署到生产环境中的所有节点,通过配置开关的方式,针对不同范围的用户开放新功能。例如网络接入层、Web层、业务逻辑层都可以用于设计灰度方案,但具体使用哪种方案,需要根据具体业务物景确定。如,利用网关做灰度控制、有基于IP或cookie的、有基于某个请求参数做流量切换白名单的(UUID、手机号)、也有基于地域或者流量百分比来切换的等。

二是通过前面讲过的「滚动部署」方式实现,它是指:将软件的新版本部署到生产环境中的一部分节点上,那么原有对这些节点的访问流量就会使用这个新版本的功能,而其他节点上的访问流量仍旧使用原版本的功能。当确认没有风险后,再用新版本逐步替换其他节点的旧版本,直至全部替换完成。

两种方式如图所示:

12.2.4 暗部署(Dark Launch)

「暗部署」(dark launch)是指功能或特性在正式发布之前,将其第一个版本部署到生产环境,以便在向最终用户提供该功能之前,团队可以对其进行测试,并发现可能的错误。(!这个不是予发布!)“暗部署”中的“暗”字,是针对“用户无感知”这一点而言,这可以通过开关技术来实现。例如,下面这个场景:某个互联网公司重新开发了一个在线新闻推荐算法,希望能够为用户推荐更多的优秀内容。但是,由于算法复杂,公司想知道在大量的真实用户访问情况下,这个算法的性能到底如何。这时要如何做呢?

我们可以为这个算法配置一个开关,并将其部署到生产环境中。当这个开关打开时,就会有流量进入这个算法。但是用户并不知道他用的是旧算法,还是新算法。如果这个算法的性能不够好,我们可以马上关闭这个开关,让用户使用原来的旧算法,图中给出的是其操作步骤。


我们还可以使用「第10章」介绍过的流量克隆方式来进行。即对每个请求都克隆一的发送给这个新算法。但这个新算法并不向用户反馈结果,而是由开发人员自己收集数据,图中为其工作流程。


12.3 高频发布支撑技术

在第8章中提到,「发布频率」与「分支策略」有一定的对应关系。当一个软件团队的发布频率高于一周一次时,采用“主干开发,主干发布”才是更为经济的做法。然而,这样做也会遇到一个现实问题:假如某个功能比较复杂,无法在两次发布之间完成开发,那么我们用什么办法来处理这个问题呢?

解决问题的办法有3个:

(1)拆分功能。将一个功能进行分解,分解为更小的在一个开发周期内能够完成的功能集。我们在第6章中也有过介绍,即将一个功能分成迭代周期内可交付的子功能,如图所示。当然,如果不是新功能开发,而只是“技术性改造”(!“四类工作”中的”技术项目“!),也可以使用后面介绍的「抽象分支技术」来实现。


(2)先后再前。先实现服务端功能,再实现用户界面。即首先实现用户不可见的那部分功能,同时要确保不影响原有的功能。这样,即便这一功能代码被带到生产环境中,因为没有操作入口,也不会对发布有影响,同时还可以使用「暗部署」+「流量克隆」方式来验证新功能在后台服务端实现方面的质量,如图所示。


(3)功能开关技术。通过开关来隐藏未开发完成的功能。这是我们接下来介绍的重点。

12.3.1 功能开关技术

什么是“功能开关(Feature Flag/Feature Toggle)"?

从代码的角度来讲,每个开关的本质就是一个"if……else”条件语句块。

以某电商网站为例,该网站使用PHP编程语言实现其网站功能。文件switch.config代码片断如下:

$cfg['new_search'] = array('enabled' => 'off');
$cfg['sign in'] = array('enabled' => 'on');
$cfg['checkout'] = array('enabled' => 'on');
$cfg['homepage'] = array('enabled' => 'on');

当需要设计一个新的商品搜索算法时,在配置文件switch. config中加人上面的第一行代码。同时,在使用商品搜索算法的相关代码位置上,添加条件判断语句。当新的搜索算法开关new_search为on时,就执行do_solr(),否则就执行do_grep()。如下所示:

if($cfg['new_search'] == 'on'){
	$results = do_solr(); // 调用新的商品搜索算法
} else {
	$results = do_grep(); // 调用旧的商品搜索算法
}

我们可以看到,当代码执行到这里时,它会从配置文件switch.config中读取配置new_search,并按照我们设计的逻辑选择不同的路径来执行。当希望让不同的用户群使新的搜索功能时,通常可以对配置项new_search进行修改来达到目标,如下所示:

$cfg['new_search'] = array('enabled' => 'on'); // 所有用户可用
$cfg['new_search'] = array('enabled' => 'staff'); // 内部员エ可用

$cfg['new_search'] = array('enabled' => '1%'); // 1%的用户可用
$cfg['new_search'] = array('enabled' => 'users', 'user_list' => 'Jelly'); // 针对具体用户白名单可用

开关技术本身并不是一种新技术。例如,对很多商业套装软件来说,通常软件授权证书(license)就是一个开发,用于激活你购买的软件。而且不同的授权证书,还可以激活该软件中的不同功能。

对于这类商业套装软件,原来倾向于在对外正式发布的软件包中仅包括完整功能代码,那些未实现的功能代码被禁止带入正式发布包中。这种软件授权通常用于针对不同用户的功能可见性策略,从而完成不同的收费模式。而且,这种开发模式目前仍在使用中。

现在,对高频率的软件部署来说,开关技术被赋予了两种新的用途:

(1)隔离:即将未完成功能的代码隔离在执行路径之外,使之对用户不产生影响

(2)快速止血:一旦生产环境出了问题,直接找到对应功能的开关选项,将其设置为关闭”。

「开关技术」是达成高频部署的一种合理技术手段,尤其是像Etsy公司使用“主干开发,主干发布”的策略,所有开发者直接向主干提交代码,这一手段就更为必要。

当然,使用开关技术也会带来成本:

首先,每个开关选项最少有两个状态,“开”和“关”。

当我们在发布之前对软件进行功能验证时,需要考虑每个开关在系统中的状态,有时甚至要进行组合测试。

开关的数量越多,可能就会产生越多组合测试的成本。

其次,并不是所有的开关代码都能以优雅的方式实现,给代码的编写和维护都带来一定的复杂性,需要细心设计。

最后,开关在系统中存在的时间越长,维护它的成本就越高。

为了能够最大化利用开关带来的好处,并尽可能减少它带来的成本,应该对开关进行系统化的管理,并尽可能遵循以下原则:

(1)在满足业务需求的前提下,尽可能少用开关技术。由于开关本质上是if…ese的语句,它会带来程序的复杂性,尤其是代码设计混乱、代码模块职责不清晰时,更容易出错。

(2)如果在“分支”和“开关”之间选择,尽可能选择「开关技术」。首先,使用开关方式,可以小步迭代;其次,可以在主干上与他人代码频繁集成,尽早发现设计冲突问题最后,创建分支会带来后期的分支合入以及多次测试成本。

(3)软件团队应对开关配置项进行统一管理,方便査找和査看状态。

(4)尽可能使用统一的开关框架和开关策略。开关策略是指开关的定义、命名,以及如何配置。

(5)定期检査和清理不必要的开关项。

下面是几个常见的开关工具:

「gflag」是由谷歌公司贡献的C/C++的开源工具;

「Java」可以使用Togglz,或者Flip;

「Grails」可以使用「grails-feature-toggle」;

「.Net」社区可以参见「Feature Toggle」。

12.3.2 数据迁移技术

任何软件服务都会处理数据,而且会对其中的很多数据进行持久化。随着软件服务时间的增长,数据会越来越多。因此,对数据库结构的修改相对比较复杂,更新耗时较多。

对那些发布频率较低的企业级应用来说,当有新版本的软件发布时,通常要提前停机,然后通过SQL命令直接修改字段结构,整理字段中的所有数据。待全部完成后,再部署新版本软件,最后启动程序,恢复服务。(!品聘!)

# 1.只增不删 #
对每天都需要处理海量数据的互联网应用来说,在高频发布模式下,虽然数据库结的变更不会像应用程序那样可以每天数次,但是每周有一次数据库结构变更可能也是很正常的。如果数据库更新需要较长的时间,那么停机更新的方式显示并不合适。此时,对于数据库结构的变更,最简单的方式就是“字段尽可能只增不删”,即对数据库表中的原有字段不再进行修改和删除操作。

如图所示,原始数据库结构中,配送地址信息被分成3个字段,并且已有历史数据的存储(图中的个人信息并非真实信息,而是简单的虚构示例)。由于这3个字段总是起使用,因此决定合并成一个字段。那么,我们可以增加一个新的字段,名为“配送地址”,并对应用程序进行两部分修改:

(1)由于无法确定是否还有其他程序使用原有的3个字段,因此写入信息时,同时向所有字段保存对应的信息。

(2)当需要读取这个信息时,可以先从配送地址这个字段读取信息,如果返同为空,说明这是记录,需要从原来的3个字段分别读取信息,并自行拼接在一起。

这类修改对应用程序的改动相对较小,并且不需要在数据库中处理原有的数据。

# 2.数据迁移 #
在大多数情况下,上面的方法可以应对。但是在某些时候却无法使用,例如,将数据存储系统从H2DB转换成MYSQL,或者将原有系统拆分成多个系统,又或者单表数据量过大等情况。这时需要做大量数据的搬迁工作。

此时做数据迁移工作,通常按照以下5个步骤:

(1)为数据库结构增加一个新版本。(!两个模型!)

(2)修改应用程序,同时向两个版本的结构中写入数据。

(3)编写脚本程序,以「后台服务」的方式将原来的历史数据回填到新版本的结构中。

(4)修改应用程序,从新旧两个版本中读取数据,并进行比较,确保一致。

(5)当确认无误后,修改应用程序,只向新版本结构写入数据。可以将原来的旧版本数据保留一段时间,以防止未预料的问题出现。

# 数据库中两表合并的过程 #(!案例!)
在某互联网公司就遇到过类似情况。由于刚开始的时候团队经验较少,因此在设计数据结构时,为了存储注册用户的信息,设计了两张数据库表:一张名为 users(保存了用户的基础信息),另一张名为user_profiles(保存了用户的扩展信息)。其目的是为了后续业务扩展时,可以不必修改users表,而只根据不同的业务,增加user_prof1les中信息即可。

然而,系统运行一段时间后,团队发现user_profiles的使用次数并不多,但是每次用户服务读取信息时,都要分别从两个数据库表中获取数据,速度也比较慢。因此,团队打算将user_profiles表中的数据合并到users表中,并将user_profiles删除。

那么如何设计这次变更流程呢?

# 第一步:修改数据库结构 #

(1)写一个SQL脚本,将user_profiles表中各列结构加入users表中

(2)修改应用程序,加入3个新配置开关项,如下所示:
“write_profile_to_user_profiles_table” => “on”

“write_profile_to_user_table” => “off”

“read_protile_from_users_table” => “off”
(3)修改完成后,发布这次版本修改

# 第二步:修改应用程序,同时向两个版本的结构中写入数据 #

(1)修改代码,将profile写入原来的user_profiles表中,也同时写入users表。

(2)修改第2行的配置开关,改为on,如下所示
“write_profile_to_user_profiles_table” => “on”

“write_profile_to_user_table” => “on”

“read_protile_from_users_table” => “off”
(3)修改完成后,发布这次版本修改。

# 第三步:编写一个可离线执行的后台脚本,批量将原来的历史数据回填到新版本的结构中 #

这一步不需要修改生产环境中的代码,而是写一个离线程序,将原来存子user_profiles表中的数据写到users表中的对应的数据列中。运行该离线程序,直到全部数据同步完成

# 第四步:从新旧两个版本中读取数据,并进行比较,确保一致 #

(1)修改应用程序,在需要读取数据的时候,从两个表中分别读取对应的数据,并在内存中进行对比,验证数据是否一致。如果数据不一致,可以写入目志,然后离线处理。也可以根据事先预定义的修订策略,对数据进行修复。

(2)修改第3行的配置开关,让内部员工可以使用users表的信息,修改开关read_protile_from_users_table修改为”staff”,如下所示
“write_profile_to_user_profiles_table” => “on”

“write_profile_to_user_table” => “on”

“read_profile_from_users_table” => “staff”
(3)修改完成后,发布这次版本修改。此时,相当于发布了员エ内部体验版本。由员工来验证数据的一致性,直至确认无误。

# 第五步:当确认无误后,修改应用程序,只向新版本结构写入数据 #

修改第3行的配置开关,让5%的用户可以使用users表的信息,修改开关read_protile_from_users_table为5%,如下所示
“write_profile_to_user_profiles_table” => “on”

“write_profile_to_user_table” => “on”

“read_protile_from_users_table” => “5%”
修改完成后,发布这次版本修改。确认运行无误,重复这一步,让更多的用户用users表中的信息,直至100%,即最后一个配置项从5%变为'on'。

# 第六步:放弃旧版本(这是一个可选步骤)#

持续运行足够长的时间,且没有发现问题时,修改开关write_profile_to_user_profiles_table为”off”,不再向user_profiles表中写入数据:
“write_profile_to_user_profiles_table” => “off”

“write_profile_to_user_table” => “on”

“read_profile_from_users_table” => “on”
修改完成后,发布这次配置变更

12.3.3 抽象分支方法

当我们进行大的架构改动时,通常会需要较长的时间。传统的做法如图12-16所示在当前的产品代码分支上创建一个新的分支,用于大规模重写,然后再将新增功能移植到这个分支上。大规模重写的这个分支在很长一段时间内无法发布,直到最后全部修改完成后。这种方式无法做到持续发布,业务需求的实现会有阶段性停滞,架构调整后第一次发布时出现问题的概率较大,需要一定的质量打磨周期。

「抽象分支方法」是在不创建真实分支的情况下,通过设计手段,将大的重构项目分解成很多个小的代码变更步骤,逐步完成重大的代码架构调整。例如,希望将软件中的一部分代码使用另外一种方式实现,使用“抽象分支方法”的过程如图12-17所示

图(a)所示的情况,应该在软件代码中找到将要替換的那部分代码;图(b)所示的情况,应在这块代码与其他代码之间插入一段隔离代码,它们都通过隔离代码进行交互;图(c)所示的情况应实现新的代码,逐步替代旧代码;图(d)所示的情况应直至特代源定的旧代码实现。

通过这种方式,我们可以做到:在不创建代码分支的情况下,达到“创建分支进行重构”的同样结果。其好处在于:

重构的同时也能交付业务功能需求;

可以逐步验证架构调整的方向和正确性;

如果遇到緊急的情況,很容易暂停,而且不浪费之前的工作量;

能够强化团队的合作性;

可以使软件架构更模块化,变得更容易维护;

使用“抽象分支方法”也有成本,例如,整个修改的时间周期可能会拉长,由于是选代完成,总体工作量比一次性完成的情况要大。

框架IBatis和Hibernate,是两种对象关系映射框架(Object Relational Mapping,ORM)。GoCD团队曾使用抽象分支方法成功将IBatis替换成 Hibernate,并且有两个对外发布的版本同时包含了这两个ORM框架。在使用这种抽象分支方法之前,团队也曾尝试使用从主干上创建分支进行框架替换,但是失败了。也就是说,团队大多数人在主干上开发功能,分支上做框架替换,每天将主干代码同步到分支上。原来以为3周可以完成的任务,6周也没有能够完成。这也说明,当进行大的改造时,如果使用创建分支的方式,通常必须停止大部分的新功能开发,否则很难成功。

12.3.4 升级替代回滚

俗语说,“常在河边走,哪能不湿鞋”。我们总会遇到部署或发布后出现一些问题,需要马上修复。如果你已经使用我们前面介绍的「开关技术」,那么这并不是什么困难的问题,你只需要将出现问题的新功能开关重新配置一下,让功能不可见即可。

但是,如果这个功能没有使用开关技术,怎么办呢?

根据 Dror G. Feitelson,Kent L.Beck等在"Development and Deployment at Facebook一文中提到, Facebook的处理的方法是:尽可能以代码升级方式代替二进制回滚。也就是说,典型的回滚操作通常是:将与待修复的问题相关的某次提交以及与之相关的任何提交一同从代码仓库中直接剔除,然后再次提交,等待下一次发布即可(!即仓库回滚!)。这样,工程师有充分的时间来研究和真正修复这个问题。之所以能够这么做,得益于 Facebook工程师的代码提交遵循“小步、独立、频繁”的原则,并且发布频率比较高。 Facebook工程师平均每天提交代码0.75次,平均每人毎天提交约100行代码的修改,如图所示。

12.4 影响发布频率的因素

尽管本章一直在讨论高频发布的收益与做法,但并不是说,每日发布适合所有类型的软件。例如,对需要跟随硬件发布的嵌入式软件开发来说,其对外发布的成本非常高。一旦因软件出现问题而导致退货率上升,那么其损失可能相当高。

当我们在决定软件的发布频率时,需要综合考虑以下影响因素:

(1)增量发布带来的收益和可能性。

(2)每次发布或部署的操作执行成本有多高;

(3)出现问题的概率与由这些问题帯来的成本有多少;

(4)维护同一软件的众多不同版本带来的成本;

(5)高频发布模式对工程师的技能要求;

(6)支撑这种高频发布所需要的基础工具设施与流程完善性;

(7)组织对这种高频发布的态度与文化取向;

在这些影响因素中,5、6、7对前面4项的结果也会产生直接影响。很可能由于这3项的原因,使得高频发布的成本高居不下,收益相对较少。此时,就需要企业领导者做出更多的努力,在后面3项上投入更多的精力。

因为部署发布有风险,所以大家均习惯于推退风险,而两次发布之间的间隔越长,累积的代码变更越多,所需质量验证时间就越长。这就形成了一个渐进增强循环,如图所示。

当我们采用本章介绍的方法以后,可以降低部署发布的风险,在提高发布频率的同时,也会鼓舞团队士气。因为毎个人都想尽早看到自己的劳动成果被真正的用户使用。

12.5 小结

本章我们讨论了如何在快速部署发布的情况下,通过多种技术手段降低风险,如开关技术、数据库迁移技术、蓝绿部署、金丝雀(灰度)发布、抽象分支以及暗部署等。并且强调,即便没有使用开关,假如团队能够一直使用“小步完整的代码提交”策略,可比较容易地做到将缺陷快速回滚。
在一些业务场景下,我们的确无法直接高频地对外发布软件。但是,如果我们能够使用本章介绍的方法持续向预生产环境进行发布与部署,就可以尽早获得软件的相关质量反馈,从而减少正式发布后的风险。如果我们能够将每次发布的平均成本降低到足够低,那么将会直接改变团队的产品研发流程。

参考文献

相关链接