原文地址:「第12章 低风险发布」(永久地址,保存网址不迷路 🙃)
在前面的几章中,主要讨论了「快速验证环」中「构建」阶段的工作。通过在业务需求协作流程、软件配置管理、持续集成、自动化测试等多方面的管理改进,缩短研发质量反馈时间,提升软件应用的研发速度。
在本章中,我们将主要讨论如何高频、低风险地进行软件部署和发布,尽早让软件在生产环境中运行,如图所示的「快速验证环」中「运行」阶段的工作:
「快速验证环」的「运行」的主要内容包括:
12.1 高频发布是一种趋势 211
自2001年“敏捷宣言”诞生以来,一直明潮暗涌,2007年以前,国内对“敏捷软件开发”的认同度并不高。
直到2009年,Flickr的John Allspaw和 Paul Hammond在Velocity 2009年的大会上分享了题目为《10+ Deploys Per Day: Dev and Ops》的报告,让业界同仁眼前一亮。原来,软件发布还可以这么快。
12.1.1 互联网企业的高频发布 212
现在,世界领先的互联网公司都在以“频繁发布”的模式更新它们的软件产品。
(2)页面上的图片、样式表或者一些模板文件。
(3)内容变更等。
(4)修改配置开关的取值,或者灰度部署。因此,每次发布既可能是对线上问题进行快速响应(例如修复安全风险、功能缺陷、限流、降载或者增减节点),也可能是修改了软件配置项,发布补丁等。
12.1.2 收益与成本共存
在高频发布模式中,每次发布的内容量通常都会少于在低频发布中每次发布的内容量(显然一天可以完成的功能比十天的少)。为什么这么多公司都在向“高频发布”这个方向迈进呢?
高频发布的收益有以下几个:
据2017年DevOps报告所述,高绩效(high-performance)的团队比低绩效(low-performance)团队相比:
(2)从代码提交至代码部署所用时间缩短为1/440
(3)平均故障恢复时间缩短为1/96
(4)变更故障率降低为1/5
上述收益来自成熟且自动化的部署与发布操作。如果仍旧坚持低频发布模式所用的研发管理方法,则强行执行高频发布会带来较高的送代成本。
然而,无论怎样,我们都无法100%消除发布风险。我们要做的是不断寻找降低发布风险的方法。
12.2 降低发布风险的方法
接下来,我们就分别讨论一下降低发布风险的方法,包括蓝绿部署、滚动部署、金丝雀发布(灰度发布)、暗部署。
(!降低发布风险、降低产品风险!)
12.2.1 蓝绿部署(Blue-green Deployment)
蓝绿部署(blue-green deployment)是指准备两套完全一致的运行环境,其中一套环境作为正式生产环境,对外提供软件服务。另一套环境作为新版本的预生产环境,部署软件的新版本,并对其进行验收测试。当确认没有问题后,将访问流量引流到这个新版本所在的环境中,作为正式的生产环境,同时保持旧版本所在环境不变。直至确定新版本没有问题后,再将旧版本所运行的环境作为下一个新版本的预生产环境,部署未来的新版本。
如图所示,“蓝”和“绿”仅代表两个相互独立的部署环境:
当然,这是一个非常理想的情况。现实中,数据库复制的时间成本比较高,而且空间成本也比较高。因此,很多蓝绿部署方案会使用相同的数据库服务,只是软件的部署使用不同的两套环境。如图所示,在这种情况下,同一个数据存储格式必须对新旧两个版本做兼容性处理,使其可以同时服务于两个软件版本对数据的操作。
另外,蓝绿部署中还有一个需要处理的问题:
一般来说,切换并不是瞬间完成的。
12.2.2 滚动部署(Rolling Deployment)
滚动部署(rolling deployment)是指从服务集群中选择一个或多个服务单元,停止服务后,执行版本更新,再重新将其投入使用。循环往复,直至集群中所有的服务实例都更新到新版本,如图所示。与蓝绿部署相比,这种方式更加节省资源。因为它不需要两套一模一样的服务运行环境。因此,服务器的成本就相当于少了一半。
当新版本出现问题时:
12.2.3 金丝雀发布与灰度发布(Canary Release)
“金丝雀发布”(canary release)就是泛指通过让一小部分用户先行使用新版本,以便提前发现软件存在的问题,从而避免让更多用户受到伤害的发布方式。因为仅有一小部分用户使用,所以造成的影响也比较小。
“金丝雀发布”的名字来自矿工下井的一个古老实践。17世纪,英国矿井工人发现,金丝雀对瓦斯这种气体十分敏感。当时,采矿工人为了保障自身的安全,毎次下井工作时都带上一只金丝雀。如果井下存在有害气体,在人体还没有察觉到有害气体时,金丝雀就会因无法抵抗瓦斯气体而死亡。此时,矿工就会知道井下有毒气,马上停止工作,回到地面。
「灰度发布」是指将发布分成不同的阶段,每个阶段的用户数量逐级增加。如果新版本在当前阶段没有发现问题,就再扩展用户数量进入下一个阶段,直至扩展到全部用户。它是「金丝雀发布」的一种延伸,也可以说,「金丝雀发布」是「灰度发布」的初始级别,如图所示。对于“划分多少个阶段,每个阶段的用户数量是多少”,要根据产品状态自行定义。
有这样一个案例:
有两种实现方式可以达到「金丝雀(灰度)发布」的效果:
两种方式如图所示:
12.2.4 暗部署(Dark Launch)
「暗部署」(dark launch)是指功能或特性在正式发布之前,将其第一个版本部署到生产环境,以便在向最终用户提供该功能之前,团队可以对其进行测试,并发现可能的错误。(!这个不是予发布!)“暗部署”中的“暗”字,是针对“用户无感知”这一点而言,这可以通过开关技术来实现。例如,下面这个场景:某个互联网公司重新开发了一个在线新闻推荐算法,希望能够为用户推荐更多的优秀内容。但是,由于算法复杂,公司想知道在大量的真实用户访问情况下,这个算法的性能到底如何。这时要如何做呢?
12.3 高频发布支撑技术
在第8章中提到,「发布频率」与「分支策略」有一定的对应关系。当一个软件团队的发布频率高于一周一次时,采用“主干开发,主干发布”才是更为经济的做法。然而,这样做也会遇到一个现实问题:假如某个功能比较复杂,无法在两次发布之间完成开发,那么我们用什么办法来处理这个问题呢?
解决问题的办法有3个:
12.3.1 功能开关技术
什么是“功能开关(Feature Flag/Feature Toggle)"?
以某电商网站为例,该网站使用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)就是一个开发,用于激活你购买的软件。而且不同的授权证书,还可以激活该软件中的不同功能。
对于这类商业套装软件,原来倾向于在对外正式发布的软件包中仅包括完整功能代码,那些未实现的功能代码被禁止带入正式发布包中。这种软件授权通常用于针对不同用户的功能可见性策略,从而完成不同的收费模式。而且,这种开发模式目前仍在使用中。
现在,对高频率的软件部署来说,开关技术被赋予了两种新的用途:
(2)快速止血:一旦生产环境出了问题,直接找到对应功能的开关选项,将其设置为关闭”。
「开关技术」是达成高频部署的一种合理技术手段,尤其是像Etsy公司使用“主干开发,主干发布”的策略,所有开发者直接向主干提交代码,这一手段就更为必要。
当然,使用开关技术也会带来成本:
为了能够最大化利用开关带来的好处,并尽可能减少它带来的成本,应该对开关进行系统化的管理,并尽可能遵循以下原则:
下面是几个常见的开关工具:
「Java」可以使用Togglz,或者Flip;
「Grails」可以使用「grails-feature-toggle」;
「.Net」社区可以参见「Feature Toggle」。
12.3.2 数据迁移技术
任何软件服务都会处理数据,而且会对其中的很多数据进行持久化。随着软件服务时间的增长,数据会越来越多。因此,对数据库结构的修改相对比较复杂,更新耗时较多。
对那些发布频率较低的企业级应用来说,当有新版本的软件发布时,通常要提前停机,然后通过SQL命令直接修改字段结构,整理字段中的所有数据。待全部完成后,再部署新版本软件,最后启动程序,恢复服务。(!品聘!)
# 1.只增不删 #
对每天都需要处理海量数据的互联网应用来说,在高频发布模式下,虽然数据库结的变更不会像应用程序那样可以每天数次,但是每周有一次数据库结构变更可能也是很正常的。如果数据库更新需要较长的时间,那么停机更新的方式显示并不合适。此时,对于数据库结构的变更,最简单的方式就是“字段尽可能只增不删”,即对数据库表中的原有字段不再进行修改和删除操作。
如图所示,原始数据库结构中,配送地址信息被分成3个字段,并且已有历史数据的存储(图中的个人信息并非真实信息,而是简单的虚构示例)。由于这3个字段总是起使用,因此决定合并成一个字段。那么,我们可以增加一个新的字段,名为“配送地址”,并对应用程序进行两部分修改:
这类修改对应用程序的改动相对较小,并且不需要在数据库中处理原有的数据。
# 2.数据迁移 #
在大多数情况下,上面的方法可以应对。但是在某些时候却无法使用,例如,将数据存储系统从H2DB转换成MYSQL,或者将原有系统拆分成多个系统,又或者单表数据量过大等情况。这时需要做大量数据的搬迁工作。
此时做数据迁移工作,通常按照以下5个步骤:
(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_table" => "off"
"read_protile_from_users_table" => "off"
(1)修改代码,将profile写入原来的user_profiles表中,也同时写入users表。
(2)修改第2行的配置开关,改为on,如下所示
"write_profile_to_user_table" => "on"
"read_protile_from_users_table" => "off"
这一步不需要修改生产环境中的代码,而是写一个离线程序,将原来存子user_profiles表中的数据写到users表中的对应的数据列中。运行该离线程序,直到全部数据同步完成
(1)修改应用程序,在需要读取数据的时候,从两个表中分别读取对应的数据,并在内存中进行对比,验证数据是否一致。如果数据不一致,可以写入目志,然后离线处理。也可以根据事先预定义的修订策略,对数据进行修复。
(2)修改第3行的配置开关,让内部员工可以使用users表的信息,修改开关read_protile_from_users_table修改为"staff",如下所示
"write_profile_to_user_table" => "on"
"read_profile_from_users_table" => "staff"
修改第3行的配置开关,让5%的用户可以使用users表的信息,修改开关read_protile_from_users_table为5%,如下所示
"write_profile_to_user_table" => "on"
"read_protile_from_users_table" => "5%"
持续运行足够长的时间,且没有发现问题时,修改开关write_profile_to_user_profiles_table为"off",不再向user_profiles表中写入数据:
"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 影响发布频率的因素
尽管本章一直在讨论高频发布的收益与做法,但并不是说,每日发布适合所有类型的软件。例如,对需要跟随硬件发布的嵌入式软件开发来说,其对外发布的成本非常高。一旦因软件出现问题而导致退货率上升,那么其损失可能相当高。
当我们在决定软件的发布频率时,需要综合考虑以下影响因素:
(2)每次发布或部署的操作执行成本有多高;
(3)出现问题的概率与由这些问题帯来的成本有多少;
(4)维护同一软件的众多不同版本带来的成本;
(5)高频发布模式对工程师的技能要求;
(6)支撑这种高频发布所需要的基础工具设施与流程完善性;
(7)组织对这种高频发布的态度与文化取向;
在这些影响因素中,5、6、7对前面4项的结果也会产生直接影响。很可能由于这3项的原因,使得高频发布的成本高居不下,收益相对较少。此时,就需要企业领导者做出更多的努力,在后面3项上投入更多的精力。
因为部署发布有风险,所以大家均习惯于推退风险,而两次发布之间的间隔越长,累积的代码变更越多,所需质量验证时间就越长。这就形成了一个渐进增强循环,如图所示。
当我们采用本章介绍的方法以后,可以降低部署发布的风险,在提高发布频率的同时,也会鼓舞团队士气。因为毎个人都想尽早看到自己的劳动成果被真正的用户使用。
12.5 小结
本章我们讨论了如何在快速部署发布的情况下,通过多种技术手段降低风险,如开关技术、数据库迁移技术、蓝绿部署、金丝雀(灰度)发布、抽象分支以及暗部署等。并且强调,即便没有使用开关,假如团队能够一直使用“小步完整的代码提交”策略,可比较容易地做到将缺陷快速回滚。
在一些业务场景下,我们的确无法直接高频地对外发布软件。但是,如果我们能够使用本章介绍的方法持续向预生产环境进行发布与部署,就可以尽早获得软件的相关质量反馈,从而减少正式发布后的风险。如果我们能够将每次发布的平均成本降低到足够低,那么将会直接改变团队的产品研发流程。
相关文章
「07」- 部署流水线原则与工具设计
「第1章 持续交付2.0」
「第5章」- 持续交付的软件系统架构
「08」- 利于集成的分支策略