前言
产品在新功能发布前,为减小发布风险,可以采取小流量测试的方式,或者在确定方案前使用A/B测试来衡量。一般开发人员会跟运维同学合作,通过一些现有平台切换机器或者流量来实现,即基于环境的金丝雀发布或者蓝绿发布。本篇将介绍另外一种简便的方式,功能开关,解释其在持续低风险发布的应用以及相关注意事项,提供一些具体的开发框架及工具供快速使用,并结合具体案例对其进行详细拆解。
功能开关(Feature Toggle)与特性分支(Feature Branches)
功能开关Feature Toggle(也称为Feature Flag)是一种允许控制线上功能开启或者关闭的方式,通常会采取配置文件的方式来控制。提到Feature Toggle功能开关,一般都会跟Feature Branches特性分支(也有译为功能分支)进行比较。两者都与功能特性有关,有什么关联与差别呢?我们可以通过一个简单的示例来比较:
假设产品需要添加一个功能,如果你在主干上进行开发,通常的做法是在前端开发人员在界面上添加功能,然后可能会有其他同学来完成后端服务、安全保障,最后测试及Bug修复并发布上线。如下图所示:
上图中有个明显的问题是主干分支上在功能测试完毕之前是不能进行发布的,必须完备之后才能发布给用户使用。
当然解决方法也很简单,例如常见的使用特性分支来解决。在主干上拉取一个分支,然后在分支上开完测试完之后在合并到主干上,这样就不会影响主干的持续发布了。如果有另外的新的功能那么同样拉取新的分支来解决,这也是通常讲到的分支开发、主干发布模式的典型场景。如下图所示:
支持特性分支的最常见论点之一是它提供了一种机制,可以支持比单个发布周期更长的功能特性。
但这种方式同样存在问题,如果功能比较复杂,开发的周期较长,此期间主干上已经多次修改代码,那么等到分支上开发完之后向主干的合并将是一项繁琐的工作。你必须去处理各种冲突,与其他开发人员沟通修改点,这是很多人不愿意做的。长期存在的分支会导致Big Bang大爆炸式的合并。
你如何使用持续集成让每个人都在主线上工作,而不会在版本中存在半实现的功能?
于是有人提供了新的方案来解决这个问题。例如将开发工作拆分成多个小块,在各个分支上开发测试完成后及时合并到主干中,并且可以先隐藏界面功能,直到所有的功能开发完成之后才展现。这样每次合并的难度会小许多;或是每次将主干上的修改都及时同步到分支上,这样分支上开发完成之后合并到主干上就简单多了。
有什么方式既能避免分支合并的麻烦、保持主干快速迭代随时发布,又能更好的控制新功能的发布、方便的进行小流量或快速回滚操作呢?答案就是功能开关。
功能开关允许关闭未完成的功能,你可以在主干上进行迭代开发,新功能即便未开发完成也不会影响发布,因为它对用户是关闭的。当功能开发完成之后,修改配置而无需修改代码,便可以让功能发布。这种操作甚至可以在线上进行,例如代码已经发布但功能不可见,你可以修改配置让功能对特定的用户(线上测试、小流量或者全量发布等)可见。如果发现新功能存在问题,那么可以通过配置文件来迅速关闭,而无需回滚或前滚(即上线修复分支)。
各自的优缺点
特性分支
优点:
- 同时开发多个特性分支不会影响主干和线上代码
- 在分支上开发新功能时不用担心对其他在开发的功能的影响
- 现有很多持续集成系统支持分支的构建、测试、部署等
缺点也很明显,Martin Fowler的文章中已经做了全面的阐述:
- 分支分出去时间越长往往代码合并难度越大
- 在一个分支中修改了函数名字可能会引入大量编译错误,即语义冲突(semantic conflict)
- 为了减少语义冲突,开发人员会尽量少做重构。l 重构是持续改进代码质量的手段。如果在开发的过程中持续不断的存在特性分支,就会阻碍代码质量的改进。
- 一旦代码库中存在了分支,也就不再是真正的持续集成了。当然你可以给每个分支建立一个对应的CI,但它只能测试当前分支的正确性。如果在一个分支中修改了函数功能,但是在另一个分支还是按照原来的假设在使用,在合并的时候会引入bug,需要大量的时间来修复这些bug。
功能开关
优点:
- 避免了分支合并代码冲突的问题,因为可以基于主干开发
- 每次提交都在主干,迭代速度明显有优势
- 新功能的整个过程都持续集成
缺点:
- 未完成的功能可能会部署到线上,如果配置有误可能将未完成的功能开启。当然可以将界面层最后开发避免过早暴露。
- 主干上担心提交代码影响其他功能。
- 功能开关增加管理复杂度,尤其是长期存在的开关,需要定期清理,否则会造成类似骑士资本的悲剧(参见“4亿美元公司如何在45分钟内因部署失败而破产的故事”)
功能开关是让我们能在Master分支上工作的一个重要利器,通过功能开关的配置使得 Master分支上可以存在“未完成的功能”而且不会对任何其他功能产生影响,但是需要考虑到功能开启后带来的安全隐患。。
我们可以在测试还没有完善的时候就开始在Master分支上工作,这虽然会带来一些负担(你必须对新版本以及旧版本同时进行测试),但是换来的是你可以更快的将功能发布到生产环境、持续迭代、试验新的功能,所以它带来的价值远远的高于产生的负担。
并不存在万能的方案,两种方式都有各自的优缺点。实际应用中我们可以根据业务场景来选择是否用特性分支还是功能开关,并且这两者可以相互结合。例如在前面提到的示例中,可以使用分支来开发细分的子功能保持分支及时合并,同时使用功能开关来控制功能的发布,提升工作效率。
功能开关的种类与生存周期
Pete Hodgson的文章中,从开关存在的时间与决策的动态性两个维度,将功能开关分为以下几类:发布开关、运维开关、实验开关、权限开关。
发布开关
发布开关允许在发布时,将未完成的和未经测试的代码路径关闭,并将其部署到生产环境。
以这种方式使用发布开关是实现“将 (功能) 发布与 (代码) 部署分开”的持续交付原则的最常见方式。
生存期短,功能开关本质上是过渡性的,尽管某些以产品为中心的功能开关可能需要保留更长的时间,但它们通常不应停留超过一两周。针对发布版本制定功能开关的释放决策是有必要的。
静态决策,发布开关的开关决策通常是非常静态的,通过修改配置文件来更改开关决策通常是完全可以接受的。
功能发布后,或等待稳定后就需要马上删除
实验开关
- 实验开关用于支持A/B 测试,通过跟踪不同群组的聚合行为,可以比较效果不同的代码路径。
- 快速实验可以针对特定人群发布功能尽早获得反馈
- 实验开关的生命周期从数小时到数周不等,以产生具有统计意义的实验结果。
- 就其性质而言,实验开关是高度动态的,业务通常会尝试多种实验组合,并支持动态调整。针对特定条件开启或者关闭功能,例如可以设置在指定时间点开启,这样新功能将按照设定自动上线下线,无需手动上线。同时可以在线上直接开启或者关闭,实现快速回滚,通常匹配有GUI的界面提供配置操作。
运维开关
- 运维开关用于控制我们系统行为的操作控制,在推出可能存在不确定性能影响的新功能时可以引入运维开关,以便系统管理员可以在需要时在生产中快速禁用或降级该功能。
- 大多数运维开关的寿命都相对较短,一旦对新功能的操作方面获得信心,就应该停用该标志。
- 然而,系统拥有少量较长寿命的“终止开关”并不少见,它们允许生产环境的管理员在系统承受异常高负载时优雅地降低非重要系统功能,当然也可以通过微服务定义优雅降级策略来达成。
许可开关
- 许可开关可以为特殊用户启用某些功能,例如内部用户先行体验功能,客户定制化功能,只为付费用户启用的高级功能等。许可开关也可以用于早期用户进行产品体验。
- 许可开关在很多方面与金丝雀发布相似。两者之间的区别在于,金丝雀功能面向随机选择的一组用户,而许可开关功能面向一组特定用户。
- 当用作管理仅向高级用户公开的功能时,与其他类别的功能开关相比,许可开关的寿命可能非常长,以年为规模。
- 由于权限是特定于用户的,因此许可开关决定将始终按需进行,因此这是一个非常动态的开关。
发布开关主要是为了隐藏未开发完成的功能,而业务开关则可以帮助我们快速满足某些需求。例如A/B测试,功能开关可以轻松控制展现哪个功能,提升A/B测试的可维护性。我们也可以通过配置里面的逻辑让新功能针对小部分人群甚至是特定地域的人群发布,尽早获取功能的反馈。甚至是可以在线上开启调试,只让新功能对调试人员可见。而这些都只需要配置文件和简单的标记来实现。
定期清理功能标签
不同类型的开关用于不同的目的,生存周期和动态方式均有所不同,因此实现机制以及定期的清理机制也会不同。
功能开关是一种强大的技术,允许团队在不更改代码的情况下修改系统行为,许多团队都在使用它们。但是,它也是一柄双刃剑,功能开关引入了复杂性,你需要对它进行精心的看护。我们可以通过使用智能开关实现实践和适当的工具来管理我们的开关配置来控制这种复杂性,但我们还是应该致力于限制系统中开关的数量和生存周期。
如果不做清理,很快的就会积累大量的过时的功能开关,骑士资本的4亿美元错误是一个警示故事,说明当没有正确管理功能开关时会出现什么问题。所以当功能已经发布到生产环境并且完全可见时,团队需要在适当的时间在代码里移除对应的功能开关。这通常会发生在几个星期后,或者几个月后,具体取决于功能涉及的范围。
谁在用功能开关
功能看起来很酷,但是不是新东西?有谁在用呢,我可不不愿意承担风险
事实上功能开关已经在国外互联网公司中获得广泛的使用。例如Facebook、Google等公司使用基于主干的开发模式来持续集成开发,功能开关是其中一个基础技术。下面这幅图展现了FaceBook开发模式转变历程,可以看到几年前Facebook就开始使用Feature Toggle,使用了功能开关关闭主干上未开发完成的功能来保证快速迭代和高频率的发布。
案例:Azure DevOps团队如何使用功能开关实现产品快速迭代
我们来介绍Azure DevOps团队的案例,看看他们是如何做到每三周完成一次产品迭代上线,如何管理以及控制功能上线的问题,特别是对于还没有完成或者需要在特定时间点开放的功能。以及如何尽早的获取用户反馈,以及完善产品的。
(本案例参考自Azure DevOps技术总监Buck Hodges的Blog,Buck Hodges曾参与TFS第一个版本的TFVC代码管理模块开发工作,并引领团队完成向云以及DevOps的转型。)
功能开关可以实现将应用部署与功能上线分离,避免代码提交部署后不得不暴露功能给用户的问题,实现功能开启或关闭的灵活控制。达到可以以任何条件或范围控制功能的上线,可以部署一个功能然后选择需要将此功能开放给哪些账户或者具体用户,控制粒度范围包括全部用户、特定范围用户、某些租户、具体用户,对功能进行便捷的控制。进而尽快的获取反馈,不仅包括用户反馈,还包括应用运行情况收集。一旦功能出现问题,快速响应,并可以快速的下线功能。
为了实现以上的能力,需要在不重新部署任何应用以及服务的前提下,完成功能开关状态的变更(功能上线、下线),需要所有的服务根据变更自动响应以便最小化影响范围。
最终,团队确立了下述目标:
- 将 “应用部署” 与 ”功能上线“ 分离。
- 功能开关设置粒度控制到单个用户。
- 尽早的获取用户反馈。
- 实现功能快速下线。
- 在不重新部署应用的情况下实现功能上下线。
发布阶段
我们可以通过使用功能开关来获取用户反馈,内部团队也可以通过它来开始试用功能找出缺陷,与其让每一个团队自己内部去定义自己的流程,不如建立一套标准化的功能发布流程,通过这种方式可以在研发阶段尽快的获取用户反馈以及发现缺陷。功能在阶段间推进的速度取决于功能的范围、反馈以及数据统计。这些阶段包含了越来越广泛的用户群体,以及多元化的观点、看法。
- 阶段 0 – 金丝雀
第一个阶段主要是一些VSTS团队的账户以及内部账户,一旦功能开关启用,项目经理便负责通知用户。
- 阶段 1 – MVPs & 某些客户
第二个阶段将包含MVPs(微软最具价值专家),以及选择报名参加试用的客户。项目经理负责邮件通知用户。
- 阶段 2 – 私有预览
私有预览主要是开放一些重要的新功能以及服务给一些客户进行测试。有很多获取 “私有预览” 客户的方式,如社区互动、博客评论等等。同时也可以通过提供邀请码、电子邮件请求、体检计划等方式。团队可以在新的导航面板上直接管理或与 “私有预览” 客户进行沟通交流。
- 阶段 3 – 公共预览
公共预览主要是为了收集一些重要的功能以及服务的反馈,但是还没有完全准备好提供SLA(服务等级协议),公共预览对所有的VSTS客户开放,但是在界面的主入口上会标注 “预览版“,以便客户了解到此功能并不完整。当一个功能进入到公共预览阶段 将会在VSTS的产品更新版块官宣并伴随着市场性宣传。
- 阶段 4 – General Availability (GA)
GA表示功能或服务已经对所有用户开放并提供相应的支持,例如SLA等。
从真实事件吸取的教训
在Connect 2013大会,团队在主题演讲以及Demo之前开启了大量的功能标签,由于在同一时间打开了大量的功能标签,有大量的新的业务逻辑与生产环境系统进行交互,导致系统崩溃。
通过这次事件得到的教训,团队会在活动开始前,确保功能开关至少在生产环境负载下运行24小时,以便于有时间做出合适的响应,选择是修复问题还是关闭功能。
功能开关的实现机理
功能开关的基本思想是通过一个配置文件,为你有待处理的各种功能定义了一堆开关。然后,正在运行的应用程序使用这些切换来决定是否显示新功能。原理示意图如下:
功能开关实质上是代码中的一个if逻辑判断,如果功能开关启用则执行新版本的业务逻辑,否则执行老版本的业务逻辑。让我们再来看微软Azure DevOps的具体例子,在这个示例中需要控制的是 “拉取请求回滚” 功能是否对用户可见,如下图所示:
定义功能开关
首先我们需要定义一个“功能开关”,在这里我们使用XML文件来定义,VSTS的每一个服务都独有一套对应的功能开关,以下是这个功能开关按钮对应的部分代码:
<?xml version="1.0" encoding="utf-8"?<!--In this group we should register TFS specific features and sets their states.<ServicingStepGroup name="TfsFeatureAvailability"<Steps><!-- Feature Availability<ServicingStep name="Register features" stepPerformer="FeatureAvailability"<StepData><!--specifying owner to allow implicit removal of features --><Features owner="TFS"><!-- Begin TFVC/Git --><Feature name="SourceControl.Revert" description="Source control revert features"
当我们部署定义在功能开关里的服务时,部署引擎将会在数据库里创建功能开关。
运行时检测功能开关状态
在代码里使用功能开关很简单,下面是使用TypeScripts创建按钮的代码。
private _addRevertButton(): void {if (FeatureAvailability.isFeatureEnabled(Flags.SourceControlRevert)) {this._calloutButtons.unshift(Dialogs.revertPullRequest(this.props.repositoryContext, this.props.pullRequest.pullRequestContract(), this.props.pullRequest.branchStatusContract().sourceBranchStatus, this.props.pullRequest.branchStatusContract().targetBranchStatus)} > {VCResources.PullRequest_Revert_Button});}}
通过界面控制功能开关状态
也可以通过内部的站点提供在线的功能开关配置,如以下示例的功能开关 “代码回滚” ,可以设置为对某个账户开启、对另一个账号为关闭。通过这种方式可以实现根据租户需求对功能开关进行配置。
关闭功能开关
对新功能的线上监控是非常重要的,如果我们发现某个功能出现问题,可以使用功能开关下线功能。即便在没有重新部署的前提下。仅仅通过一个脚本或者在线的开关就可以将应用回滚至之前的状态。
测试注意事项
隐藏在功能开关背后的新功能被部署到环境上默认设置为关闭。当我们对某些租户或用户开启功能时,新的业务逻辑与老的业务逻辑都会被执行。我们需要同时对新的以及老的业务逻辑进行测试,以确保应用的正常运行。这也是至关重要的一步来保证功能出现问题时可以顺利的下线。
开发框架
有哪些相应的开源框架呢?几乎各种语言都有相应的实现。例如FEX FIS小组提供了基于php和node.js的框架。此外还有多种语言的开源实现:
语言 | Feature Flag框架 |
php | 基于smarty的Feature Flag框架 |
NodeJs | 基于Node前后端解决方案Yogurt的Feature Flag框架 |
java | Togglz |
.NET | FeatureToggle |
Ruby | Rollout、Degrade |
Python | Gargoyle、Nexus admin |
Groovy | GrailsFeatureToggle |
功能开关的使用场景
除了主干开发,什么情况下选择使用功能开关呢?下面是使用功能开关的一些典型场景:
- 在 UI 中隐藏或禁用新功能
- 在应用程序中隐藏或禁用新组件
- 对接口进行版本控制
- 扩展接口
- 支持组件的多个版本
- 将新功能添加到现有应用程序
- 增强现有应用程序中的现有功能
- 支持金丝雀发布,通过针对选定的少量用户组ID进行开关切换
- 用于 A/B 测试的实验切换,通过针对不同的用户组ID进行开关切换
- 为操作人员提供控制的操作切换
- 用于控制不同用户子集的功能访问的权限切换
- 可以看到,由于功能开关本身是对业务功能的控制,所以不适于功能大范围的改动等情况。另外使用过程中需要注意一些问题:
- 只在需要的地方创建开关。美酒虽豪,不可贪杯。滥用任何技术都会出现问题。
- 控制开关的数量。同上,开关应按需使用并及时清除。
- 开关之间代码保持独立。如果代码存在依赖就没法删除,最终维护性反而变差
- 清除发布开关和废弃代码。发布开关应当在功能稳定后删除,旧代码也是。
- 界面层最后暴露。
工具案例:LaunchDarkly 让你的代码和业务逻辑解偶
LaunchDarkly是美国的一家产品测试和优化企业。自从公司在A轮融资260万美元后,公司首席执行官Edith Harbaugh和团队发现了一个新的受众群体:营销人员和业务团队似乎能够成为一个DevOps工具的直接用户。这一潜力也引起了投资者的注意,最终由DFJ领投,Softech和Bloomberg Beta参与了给予LanuchDarkly的870万美元的投资。
2015年获得了A轮投资以后,LaunchDarkly已经帮助了很多优质客户正确的执行他们的DevOps策略,这些客户包括 AppDirect, CirleCI, Lanetix 和 Upserve;而且如微软,Atlassian等DevOps领域的主力玩家都向自己的客户推荐使用LanuchDarkly的服务。原先任职于Tripit的LaunchDarkly CEO Edith Harbaugh是一名优秀的产品经理,她本来认为LaunchDarkly的主要用户会是移动应用开发者,但她最终发现这个市场比她想象的要大多的多。基本上,任何人都可以使用LaunchDarkly的服务,甚至包括业务人员和企业管理者,他们不用编写任何一行代码就可以通过LaunchDarkly的服务改变自己的App的行为,从而完成各种实验,验证自己的市场推广手段是否有效。也正因为此,LaunchDarkly最终获得了投资人的青眯,并得到了870万美金的投资。
LaunchDarkly所提供的SaaS服务实际上是DevOps领域的一系列最佳实践,A/B测试,蓝绿发布,金丝雀发布等等的基础性服务:功能开关。功能开关本来只是开发人员通过一些配置文件来控制代码逻辑的一种方法,但LaunchDarkly通过提供一系列与业务场景紧密相关的特性,让功能开关起到了分离代码和业务逻辑的作用,允许非技术人员可以通过功能开关来完成各种业务操作。比如:销售团队可以通过切换页面上某一促销广告的切换来测试哪种设计会带来更高的转化率。
比较以上同一页面的左下角促销栏位,你可能会觉得左边的设计更加图文并茂会带来更好的额转化率,但实际测试结果却是右边的转化率更高,而且高出5倍之多。
(以上示例来自国内的类似产品吆喝科技:http://www.appadhoc.com/blog/microsoft-homepage-leftside/)
LaunchDarkly所提供的服务可以帮助用户通过可视化的界面来控制程序的行为,通过在以下界面中打开或者关闭某些功能,甚至针对某一部分用户进行类似的设置来验证各种假设的成立。
这些开关不仅仅帮助用户控制逻辑,同时也帮助管理者统计用户行为,进行分析。
或者进行A/B测试,确定哪种方案的转化率更高
在DevOps领域中,我们经常提到打通不同部门,让不同角色的人员协作。要做到这一点,文化和管理方式的转变毋庸置疑是必须的,而类似LaunchDarkly这样的工具也是非常有帮助的,因为它真的能够通过开发人员的工作为业务人员赋能,而不是每次业务修改逻辑都要等待开发团队。
总结
功能开关已经变成开发团队进行功能发布、收集反馈的一个重要组成部分,通过使用功能开关,研发团队和市场人员可以按照自己的节奏做事互不影响,很难想象DevOps链条上缺少这一利器。
- 功能开关与特性分支各有优势,结合使用能发挥更大作用
- 结合业务场景选择合适方案
- 功能开关能支持主干开发,并在控制功能发布上有独特优势
- 实现 “应用部署” 与 “功能上线” 分离。
- 功能发布 “配置粒度” 精准控制到单个用户。
- 尽早的进行功能试验并获取用户反馈。
- 实现功能快速回滚。
- 在不重新部署应用的情况下实现功能上下线。
- 有助于让我们在Master分支上工作。
参考资料
http://martinfowler.com/bliki/FeatureBranch.html
https://martinfowler.com/bliki/FeatureToggle.html
https://martinfowler.com/articles/feature-toggles.html