在Agile Development中如何保持质量以及如何快速测试功能是个复杂的话题,涉及到A/B test, User tracking, unit test, code review flow, CI及Analytics等等多个方面。在这里鹅厂Turing Lab副总监 张力柯 先从一个概念的实施谈起:Feature Flag

现在都提倡敏捷开发,基本上已经成了现代软件开发的尤其是移动互联网app开发的标准模式。快速迭代、快速试错成了每个人开口闭口都在谈的东西,但是问题是,如何在快速迭代中保证产品质量,如何在快速试错的同时尽量避免你的“错”去影响到实际用户,以及如何知道是“对”还是“错”?实际上,这才是区分一个开发团队优秀与否的关键,如果只是求快,有大把外包公司排着队。。。 

在Agile Development中如何保持质量以及如何快速测试功能是个复杂的话题,涉及到A/B test, User tracking, unit test, code review flow, CI及Analytics等等多个方面。在这里先从一个概念的实施谈起:Feature Flag.

 

1)What is Feature Flag? 

我们假设现在你的app已经完成核心功能,打个比方说你是一个交友软件能够用两个用户账号互相发消息了,现在你在考虑是不是要加入微信登录或者QQ登录,你不知道是不是两者都要还是只需要一个看起来比较简洁,或者是不是年轻人更喜欢QQ登录,或者要不要加个facebook登录看起来更国际范。。。这时候你很可能就需要实现Feature Flag并以此作为数据驱动的来源。

啥叫feature flag?也就是功能开关标志,顾名思义,很简单,无非是有个后台控制去开启/关闭某个功能。简单吧?然而,在开发/测试人员一天埋没在各种bug里面,一天在发布新版本上线时彻夜不眠时,是否想过其实你缺的就是个功能开关。实际上,Feature Flag在硅谷FLAGUAPS等一线互联网公司及大大小小startup的开发流程中,早已成为标配。这不仅仅只是为了开关某个功能,而是要在敏捷开发/快速试错/数据驱动决策这整个流程中不可缺少的一环。

首先来说数据驱动,这个词语已经流传了很多年,各家公司都号称自己是data driven. 那么到底是不是,很简单,看他们的产品和代码是否支持A/B Test. 如何看?很简单:一个新功能,能否只对Android用户开放而不对iOS用户开放?能否收集两天数据后别换版本,直接后台控制对iOS开放而对Android用户关闭?当然,这一般还会对用户群比例作出限制,这是另外一个细节,在这里先就不谈,大家应该对A/B test的概念不陌生。技术实施上可以复杂可以简单,但是区分一个团队是否是做到了data driven(或者拍脑袋driven),就要看A/B test是否普及。在国外FLAGUAPS等公司,A/B测试是任何新功能发布(哪怕改一个按钮位置)必须加入的环节。当然,这也可能是资本主义的糟粕。。。

至于Feature Flag,一个软件是由各种功能组合起来,那么对于每一个功能,理论上是应该能够随时开启或关闭的。这里又涉及到一个core flow的问题,就是一个软件的核心最基本的流程是可以没有关闭选项的,打个比方说微信的功能入口,对“看一看”这些功能在初上线的时候,是应该能后台控制关闭或开启,并能针对不同用户/设备/地域等进行控制,但对于聊天这个选项,是可以没有关闭选项的(这就看设计者的要求了,原理上唯一不能关闭的就是进入app,其它都该能够有开关选项)。

2) Who should be using feature flag?

必须强调一点,Feature Flag这个概念并非程序员专用,这实际上是个产品开发设计流程的概念,PM/client developer/backend developer/test engineer/data engineer/data scientist都必须熟悉这个概念并在日常工作中保持同步,各方的职责大致如下: 

- PM: 必须知道当前哪些新功能处于Feature Flag 的保护下,哪些已经经过反复验证,不再需要保护;一般来说一个简单的规则是,从第一次正式发布开始(beta之后),所有新功能都必须用feature flag保护

- 客户端开发:在初期比较简单,但往往在产品进入中期而且开发团队扩大后,各种feature flag容易混合在一起,有可能造成严重的逻辑问题,需要特别注意。这跟multi-threading programming有点相像,互相lock导致逻辑异常

- 后端开发/Data Engineer:两者往往要共同配合来实现Feature Flag的后端设计及控制界面,尤其是和用户画像(user profile)的关联

- Data Scientist:Data Scientist在其中的作用更多是在系统已经搭好后,根据业务特定来设计Feature Flag的设定策略,比如到底对哪些用户开放功能,要track哪些行为等,然后在搜集到一定数据后进行分析,再决定是否调整。

  

3) How Feature Flag works?

如果想体验,可以尝试launchdarkly.com的例子,其逻辑也简单:

如上面代码所示,对客户端来说很简单的东西,只是获得该功能是否应该启用,然后执行不同代码去实现。 

那么你说了,这不就是直接在MySQL里面设一个key名字再设一个boolean field就完了么,分分钟搞定 ----- 很遗憾,这种处理,相当于你只打出一个Hello World就说实现了起点文学。。。

在面向大中型团队(如Linkedin/Facebook/Uber/Airbnb等等)的产品开发环境中,Feature Flag是一个整体的系统,包括在提交时对Feature Flag的检查处理,监控Feature Flag的生命周期,和其它服务的对接等等。具体说的话,一个用于中型团队的Feature Flag系统应该能实现以下功能:

简化的客户端代码实现=>提交时的相关代码检查修改(避免命名冲突)=>merge后在后端系统的脚本自动生成该Flag=>和各种其它service(用户画像、版本系统等等)的关联=>对Flag本身的监控和统计=>一定时间后(比如3个月后)对该flag是否应该移除发出通知=>移除flag或继续监控

4) Feature Flag的应用目标

Feature Flag 的意义并不是简单的启用和停止某个功能,而是针对不同人群来启用功能并搜集数据进行分析,其背后的支撑是海量的用户数据和精准的用户画像。利用Feature Flag,我们其实希望达到这些目的:

A/B Test

能够进行简单的用户二分类,对不同类的群体试验不同的功能。这是最常见也是最基础的功能。一般我们称用作基准的用户群为control group,用于试验新功能的群体为treatment group. 在此不多说,网上例子很多。

灰度测试

如果我们能够获得客户端用户信息、设备信息、地域信息、时间信息等,我们可以进行更细致的群体划分来进行灰度测试。但是这一点则对客户端的数据收集及后端用户画像等数据提出了更多要求,这就不再是什么在数据库中设一个开关的问题,而是一个完整的后端服务来返回True|False,而这个后端服务会跟用户信息、用户画像、设备信息、功能设定等多个数据源或服务进行交互。

举个例来说,我们可以对游戏中的某个道具外观做调整看是不是更吸引用户去购买,但我们想做灰度测试,这个灰度测试就可以是比如:在北京地区的20~25岁男性且手机内存在4G以上的Android用户,和西藏地区20~25岁使用Android手机且内存不到2G的男性用户(用于比较大城市和边远地区用户的行为差异) 

难点:需要完善细致的用户画像和其他相关数据服务的支撑,并需要数据分析人员对具体灰度测试的目标和结果制定相关方案。打个比方说你可能发现测试群体的行为和你预料的不一样,那么你是马上修改功能呢,还是换个群体再测试,这都是需要经验的积累和理性的计划。

UI设计的A/B Test

我们甚至可以通过功能开关来实现对不同用户群的不同UI设计。然而,这对代码实现提出了较高标准,在iOS开发中很多公司不允许使用Storyboard进行界面设计,而强制要求所有UI都要用代码实现,一则是方便code review,二则就是为了做UI上的A/B test。又打个比方说,我们可能会纠结某个道具列表是横向滚动还是纵向滚动,那么就可以通过Feature Flag来控制,例如对50%的用户是横向,对剩下的是纵向滚动。

难点:这种需求对代码实现要求极高,要求程序员能设计出能支持不同布局的局部UI框架,一般只在时间比较充裕时可以采用。

功能回滚(Rollback)

这是Feature Flag的主要应用场景,通常发生在新版本发布后出现的非核心功能bug,为了不影响主干新功能,我们必须把出现bug的功能给关掉。在缺乏Feature Flag的开发中,我们对每一个新功能的发布都提心吊胆,开发者被埋没在已有的无数bug中,只知道bug越来越多,哪些优先哪些不优先全凭策划一张嘴。测试人员提心吊胆生怕漏测背锅,一旦有问题没发现影响了核心流程,或者导致crash,搞不好就是百万千万的收入损失,然后迎来新版本上线前后的彻夜不眠。

有了Feature Flag,尽管发现了Bug仍然需要紧急修复,但至少在能保证核心流程不crash情况下,新增功能如果出了问题,关闭掉就是,压力大幅度降低,尤其对于测试一方,只需要做好bug的上报和功能的关闭,至于定位问题,在压力不那么大情况下就可以交由开发人员去完成。在现在的新一代互联网公司中,凡是能关掉而不影响核心流程的功能bug,统统可以作为次优先级问题,而开发经理、测试经理和PM在听闻某功能出现bug尤其是会导致crash的bug时,往往会第一时间询问是否能关闭此功能,是否已经通过feature flag进行控制,如果答案是Yes,大家都会松一口气,好歹有时间来修复。

难点:通过Feature Flag来进行功能回滚,看似只是一个控制方式,实质是对开发/测试的一次重新分工。在传统测试体系中,由于没有全面普及Feature Flag,凡是会导致较严重后果(程序crash,金钱交易问题等)的bug都会成为紧急问题,为了提高效率,测试人员往往要负担重现bug、bug定位甚至是对代码debug的工作,以便交到开发人员手中的是一份完善的包括具体bug复现、问题根源及代码位置的报告,以加快开发人员的bug fix速度。

在应用Feature Flag之后,由于我们能够对产生问题的新功能直接关闭或者有针对性关闭,为开发人员赢来大量的bug fix时间,那么对测试人员在bug fix上的要求就不再那么急迫,测试人员可以把时间用于更完善的bug记录、跟踪、统计、上报等功能上,而无需花大量时间去研究如何重现bug或者对其进行代码定位。而对于开发人员,他们就要更多地去考虑如何对自己所负责的模块进行细粒度的Feature Flag保护,以便在发生问题时不至于把自己的大模块都整个关闭,例如: 

在上面这几行代码中,各种Feature Flag非常容易纠缠在一起,这时候需要开发人员非常小心去理顺其中逻辑,能避免这种情况就尽量避免。

这里要非常强调一个概念:如果说10年前大家关注的是如何快速定位bug并快速fix,那么经过这么些年的敏捷开发和mobile app经验,现代软件开发的关注重点在于如何能提前预防和减少bug所带来的影响,因为无数事实已经证明,急急推出一个含有bug的新功能所造成的损失远大于延迟推出。

Analytics(数据分析)

实际上,在Feature Flag所控制的代码中,由于是新功能,通常需要对其作比较详细的记录,也就是data tracking(埋点),方便数据科学家对该功能所带来的效果和潜在的问题进行全面的分析。这通常需要和数据科学家根据业务来共同确定。关于tracking在开发过程中何时进行以及如何进行,则是另外再谈的问题。

 

5)客户端的设计

从软件架构来看,Feature Flag的实现上有很多可以挖掘和优化。比如说下面的代码:

if (FEATURE_ENALBED(WECHAT_LOGIN)) {

}

 其中FEATURE_ENABLED这个在C++中可以用Macro来实现,或者封装成别的功能,在大型客户端项目中(如Linkedin/Uber/Airbnb等等),仅仅一个简单的判断是否enable是不够的,还可以分作比如:

OPTIMISTIC_FEATURE_ENABLED(…)   //return True if not defined in backend

DEBUG_FEATURE_ENABLED(…)          // return True only in DEBUG mode

也就是实际上有些是缺省打开,有些是缺省关闭,还有些只对DEBUG起效果,通过客户端的代码来控制不同环境下的开关; 

往往我们并不想用比如”WECHAT_LOGIN”这样来显式定义Feature,很容易遇到命名冲突的问题(注意,我们在考虑一个几十人同时每天提交代码的环境,命名问题是很容易发生的)。在实践中,这个工作通常会在代码提交前通过相关的代码审核工具如Lint等结合脚本工具来进行自动修正,例如上 面例子,在提交代码时会自动修改为类似下面的代码:

直到这一步为止,所有工作都是客户端代码完成,因为代码并未提交到主干,没有必要在后端数据库生成该Feature Flag。 一般来说,直到代码提交并通过code review后,在merge入master之后才会由相关脚本文件去在特定的Feature Flag数据库中写入该标记。

 

6)后端系统

如前所述,Feature Flag的目标有很大部分是用于灰度发布测试和功能回滚。如果说功能回滚是为了隐藏bug,那么对于灰度发布测试,就需要能够对Feature Flag做详尽的用户群体分类。用户画像的划分说来话长,再说Talk is cheap,这里用某国外打车软件的Feature Flag控制体系来举个例,其中你需要设定以下参数:

分组(缺省情况下你可以只设定treatment group,但是也可以自定义分组)

对每一组可以设定:

- 版本号(可以多个或者条件控制,例如>3.41之类的)

- 用户选择(特定UUID/只对员工开放/特定群体/etc.)

- 设备类型(ios/android/OS version/etc.)

- 地域控制(城市/省/国家)

- IP控制(对移动app很少用)

基本上通过对这些参数的排列组合,已经足够获取大量的分析数据。

另外客户端通常也可以在测试版本中实现对Feature Flag的override开关,方便开发人员的测试。

 

7) 主干功能

现在你已经能够通过Feature Flag来对不同用户测试不同的设计,是不是急不可待准备发布了呢?

如果是一个几十人上百人的开发团队,现在还不要着急,注意到我在说功能回滚时反复提到核心主干功能,这些核心功能流程不应受到feature flag的影响,是原型设计中就验证无误的。通常我们要确信这些核心功能会被unit test(单元测试)所覆盖,并且验证无论新增的feature flag是启用了还是关闭,都不会对核心主干流程有影响(比如说不管支护宝怎么弄,转账付款等基本功能不能受影响),所以单元测试就显得特别重要。

注意到一个问题,很多人会觉得单元测试很无聊,无非是验证一些很简单的逻辑。然而实际上单元测试的目的并非验证你自己写的某个功能是正确的,而是为了在团队扩大后,为了防止别人的代码对你这块产生影响,避免大量新的其他代码变化在某个时刻影响到该功能。换句话说,单元测试的目的是block potential changes,而不是仅仅在做verification. 之前km有帖子问现在开发是不是不需要写大段注释了,回答是Yes&No. 现在推行单元测试,从某个角度来说是代替了注释(talk is cheap, show me the code),你能通过单元测试就说明你的代码和别的部分的接口没有问题,否则就是有问题。至于unit test这方面在现代软件开发中的具体实施流程,则是另一个话题了。