导读:大型互联网公司推出新功能之前,会选择一小部分用户进行灰度及 A/B 测试,并根据这些反馈对功能进行改进。知名的有 facebook 的 Gatekeeper,LinkedIn 的 XLNT 等,最近 Dropbox 也公布了其灰度发布平台。本文由魏佳翻译,转载请注明来自高可用架构。
像 Dropbox 这样的 SaaS 公司需要持续升级迭代他们的系统,并且这会涉及整个架构栈的所有层。当需要调整某些基础架构,发布一个新功能,或是设置 A/B 测试时,最重要的是我们如何快速变更并体现在产品中。
对我们的代码进行更改,然后“简单地”推送出去不是一个好选择:因为推送变更代码到 Web 服务器这个流程可能需要数小时,而发布新的移动版或桌面版客户端则需更长时间。同时在任何情况下,完整的代码部署是非常危险的,因为它可能会引入新的代码缺陷:
我们想要把一些可配置的 “开关” 添加到我们的产品中,并且根据我们的需要来动态打开或关闭。这能带给我们很好的灵活性,以及安全的实时的调整能力。
为了满足这种需要,我们构建了一个名为 Stormcrow 的平台,它允许我们编辑和部署 “feature gates”(可以称作功能门或开关)。它是一个可配置的代码路径,通过请求 Stormcrow 来确定如何执行。典型的代码使用看起来像这样:
可在更改后10分钟内于生产环境生效。
可用于所有 Dropbox 系统,从底层的基础架构到桌面版或移动版产品。
提供高级的用户投放功能,比如可以根据我们数据仓库中的用户画像来细分用户。
构建一个像这样通用的功能开关系统并不容易,因为它需要具有足够的表达或描述能力来处理不同的用例,同时要足够健壮来应对 Dropbox 巨大的流量。 接下来就让我一一描述系统的工作原理和我们的一些经验教训。
示例
假设我们想执行一项 A/B 测试,看看德国用户喜欢什么颜色的按钮。 进一步,假设我们已经知道英语为母语的用户喜欢蓝色按钮。那么在 Stormcrow UI 中,我们可以这样配置功能:
这表明我们将对“德语区域的用户”分别以 33% 和 33% 的比例显示红色按钮(RED_BUTTON)和蓝色按钮(BLUE_BUTTON),剩余 34% 的用户则不显示该按钮。同时,使用英语会话(译者注,指浏览器语言或客户端语言)的用户则 100% 显示蓝色按钮(BLUE_BUTTON)。除此之外的所有剩余用户并不参与这项 A/B 测试。
需要注意的是,对于给定的任意特性/功能,可以使用不同的群体(population)类型:上面例子分别使用“用户”和“会话”来定义群体,前者仅代表已登录的用户,后者代表对我们网站的任何访问者。
在 Stormcow 中,有一系列的群体,这些群体按照自顶向下的方式进行匹配:首先,我们将尝试匹配群体1,如果失败了,我们在匹配群体2,依此类推。一旦我们匹配一个群体,我们就选择一个变量(variant)应用到这个群体上。
通过用户的 ID 与种子(图中右上角的小灰色框)hash 而实现随机。Stormcrow 用户也可以通过种子来实现特殊的行为。例如,如果想要两个不同的特性/功能分配给完全相同的用户们,那可以指定相同的种子。
如何定义群体?
要了解群体是如何定义的,我们需要了解两个概念:
选择器(selector)是一个代码对象,它被传递进 Stormcrow 以帮助它做出判断。例如,user 和 session 对象都可以用作选择器。
数据字段(datafield)是一段代码,它接收一个或多个选择器,并提取指定类型的值:布尔值、日期、数字、集合或字符串。 然后将它们通过简单的规则引擎(使用这些值执行逻辑计算)组合成数据字段。
这里是一个真实的数据字段例子,user_email :
@dataField 装饰器(decorator)指定该数据字段需要一个 USER 对象,并且将产生一个 STRING。 它还包括一段帮助文本,由此我们可以自动生成文档。函数的实际主体只是将用户的电子邮件从对象中取出。
定义数据字段后,您可以在定义群体中使用它。这里有一个例子,它要么匹配 Gmail 和 Yahoo 用户并且显式排出两个特定的用户,要么匹配 [email protected] 这个用户:
因为可以运行任意逻辑计算,所以数据字段十分强大。在 Dropbox 内部定义了很多数据字段,来支持我们所有的用例,不通的团队如果需要他们可以不断得添加新的数据字段。
基于 Hive 的群体分类:连接我们的分析数据仓库
即使具有创建任意数据字段的能力,我们也面临一个限制:我们只能依赖我们服务器代码可访问的信息(来定义群体),也就是已经加载的模型或数据库中。但是 Dropbox 还有另一个大数据源:我们基于 Hive 的分析数据仓库。有时候 Dropboxer 想要通过写一个 HiveQL 来查询一组任意的用户,这就可以利用各种历史日志和分析数据。
为了使 Stormcrow 可以利用通过 Hive 查询获得的群体,我们需要将其从分析仓库移到可扩展的,同时可被生产代码检索的数据存储中。为此我们构建了一个每天运行的数据管道,它将当天的所有基于 Hive 查询的群体导出到生产环境中。
这种方法的主要缺点是数据滞后。与数据字段不同(数据字段总是生成最新的数据),基于 Hive 导出的数据库仅仅每天更新一次。虽然这对于某些类型的开关是不可接受的,但它对于群体变化缓慢的场景来说很有用。
基于 Hive 的群体分类体现了表达力和数据新鲜度之间的权衡:对复杂分析数据执行特征开关比对常用数据进行开关具有更多的滞后和数据工程工作。
派生群体:从简单的群体定义中构建复杂群体
Stormcrow 最强大的功能之一是定义群体的能力。比如派生群体,下面示例是一个群体,它匹配 a)“Android设备”用户和 b)功能 recents_web_comments 值为 OFF。
这个功能帮我们避免了有些复杂匹配规则不断得被复制和粘贴。相反,Dropbox 的功能开关旨在构建一组核心的基本群体,可以混合和匹配以满足任意复杂的匹配需求。我们在实践中发现,设计派生群体层次结构与重构代码非常相似。
实际上,可以将派生群体视为替换代码中的 “if” 语句,以便在实验之间进行选择。而不是写形如“如果用户匹配在实验 A 中就显示东西 A,否则如果匹配在实验 B 中就显示东西 B”这样的逻辑。
选择器推断:通过推断附加信息使得 API 更易于使用
像其他复杂的软件系统一样,我们的代码中使用了很多 Dropbox 内部模型。例如,user 模型表示单个用户帐户,team 模型表示 Dropbox 业务团队。 identity 模型代表配对的帐户:它将个人和商业用户模型绑定到单个对象中。我们所有的模型都通过各种一对多和多对一的关系连接。
在 Dropbox 产品代码中,我们通常可以访问一个或多个这类模型。为了开发人员的方便,如果 Stormcrow 理解我们的模型关系就可以自动推断额外的选择器。例如,开发者可以访问用户对象 u 并且希望查询针对团队而开关的功能。 他们可以这样写:
Stormcrow 自动推断来获取更多的信息,所以开发人员只需要写:
在 Stormcrow 中,我们将 Dropbox 的模型关系表示成一个图,我们称之为选择器推断图。在这个图中,每个节点都是模型类型,从节点 A 到节点 B 的边意味着我们可以从模型 A 推断除模型 B。当 Stormcrow 调用时,我们做的第一件事是获取指定的选择器,接着在图中计算其传递闭包(transitive closure)。
当然,推断可能因为额外的计算或网络调用而造成性能损耗。为了使它更高效,推断会产生 thunk,它们会被延迟求解(evaluate),这样我们只有在实际需要选择器来作出开关决策时才计算它们。 请参阅下面的“性能风险”
这是我们真实的选择器推断图。每个节点表示 Stormcrow 中的选择器类型。例如,viewer 是一个非常方便的模型,因为我们可以使用它来推断 session,team,user 和 identity。
我们发现选择器推断为开发人员带来了巨大便利,同时易于理解。当然我们也有检查工具来确保开发人员不会传递错误的选择器。请参阅后面的“审核挑战”
部署:Web 和内部基础设施
如果你有大量生产服务器,如何将功能门控的配置部署到它们上面呢?很显然,我们需要将功能门控相关的数据保存在数据库中,但是那么就需要一次网络调用来检索。dropbox.com 上一次典型的页面加载中可能会涉及大量的功能门控,这会导致对数据库的大量读取。即使使用精心设计的缓存系统(例如使用本地缓存+ memcached)缓解这些问题,数据库也会成为系统的单点故障。
相反,我们将一个名为 stormcrow_config.json 的 JSON 文件部署到所有的生产服务器上。这个部署仅仅使用我们的内部推送系统,并且在每次对 Stormcrow 配置进行更改时推送。
我们所有的服务器都运行一个称为“Stormcrow 加载器”的后台线程,它监视磁盘上的 stormcrow_config.json 副本,当它改变时就重新加载它。这让 Stormcrow 不用中断服务器就可以重新加载。
如果由于某种原因找不到配置文件,那么 Stormcrow 也能够回退到直接访问数据库,但是对于任何大流量的系统来说,这是非常危险和不推荐的做法。
部署:桌面端和移动端
对桌面版和移动版的功能开关稍有不同。对于这些客户端,它们通常是批量请求开关相关的信息。比如从后端获得的 Stormcrow:
这两种平台上的客户端还会传递一个或多个包含平台特定信息的特殊选择器。移动客户端传递一个选择器,提供关于正在使用的 App 和设备本身的信息。桌面客户端则传递一个带有桌面主机信息的选择器。与其他选择器一样,Stormcrow 有可根据这些平台特殊的选择器来定义规则的数据字段。
监控
Stormcrow 中所有被开关的功能的每次分配和曝光,都会记录到我们的实时监控系统 Vortex 中。 Stormcrow UI 中嵌入了图表,用户可以利用其来跟踪分配和曝光的速率。例如,下图显示了三种不同的变量(黄色,蓝色和绿色),以及每个变量随时间曝光给用户的数量。 每次修改功能(或功能所依赖的群体)时,图表中会用垂直线注释。这使我们很容易地看到变更的影响。在该图中,我们可以看到绿色和蓝色变量在第一次修改之后收敛(垂直线),同时黄色变量上升。
用户还可以单击底部的链接,使用我们的 Vortex 或其他数据工具更详细地挖掘数据。
性能风险
由于 Stormcrow 的模块化数据字段设计,Dropbox 的开发人员可能直接或间接得编写严重影响性能的数据字段。比如:有人创建一个新的数据字段,对于他们的小型业务来说是非常安全的,但这个数据字段也可被其他人使用,这就可能造成小型业务系统无法承载大量的请求流量。
这告诉我们一个重要的教训:在功能开关中要避免数据库调用或其他 I/O!
相反,调用方应该传递进来尽可能多的信息。这样避免 Stormcrow 因为推断而产生的隐式 I/O ,同时调用方的显式 I/O 也使得调用方更好评估性能的损耗。
理想情况下,Stormcrow 应是完全“纯粹的”(以函数式编程而言),并不会执行任何 I/O。目前我们还没有能够做到这一点,因为一些实际的原因:有时为调用方提供一个便捷的 API 意味着 Stormcrow 需要做更多并付出性能代价。
审计挑战
功能开关有一方面比较棘手,因为它们没有被纳入版本控制中:它们可以独立于使用方代码进行更改。通过我们的“每日推送”系统(对于我们的后端)或通过桌面版或移动版客户端的发布过程,Dropbox 中的代码上线以可预测的方式进行。 但对于功能开关的变更,由于它们的性质,可以发生在白天或晚上的任何时间。因此,拥有完善的审计工具很重要,因为我们可以尽可能快地跟踪功能开关相关的回归。
Stormcrow 通过提供完整的审核历史记录,以及静态分析代码库中的功能来解决此问题。
审核历史记录很简单:我们对给定功能和群体的所有历史修改显示一个类似 “News Feed” 的视图。
对代码库的静态分析更有趣。我们运行一个名为“Stormcrow 静态分析器”的特殊服务。 它会拉取我们的代码并扫描它,搜索 Stormcrow 相关功能的使用。 对于给定的需要开关的功能,它会生成:
一个当前 master branch 中所有出现此功能的列表。
一个“历史视图”,展示了此功能相关的所有提交记录。
例如,下面是静态分析器对一个名为 can_see_weathervane 功能的输出:
静态分析器还会执行另一项重要的任务,那就是,我们的生产代码中找到的与单元测试正在测试相匹配的变量。 它会发送“nag”邮件给该功能的所有者告知相关问题,比如已经弃用的功能应该从代码库中删除。
质量保障和测试
针对功能的手工测试,Stormcrow 支持“重覆盖”(override)。重覆盖允许 Dropboxers 临时将自己放入任一群体中。 我们还有一个“数据字段重覆盖”(datafields override)的概念,可以临时更改单个数据字段的值。 比如,临时将语言区域设置为德语,来测试德语下的体验。
对于单元测试,我们运行一个”模拟的“(mocked)Stormcrow,并对每个开关功能都返回一个“默认”变量用于测试。
总结
在 Dropbox 当前系统的体量和规模下构建出这样一个统一的功能开关服务需要方方面面的仔细考虑,从基础设施层面,到数据获取和配置管理,再到 UI 设计和相关工具链。我们希望这篇文章对于正在打造自己的功能开关系统的同行有所帮助。
https://mp.weixin.qq.com/s/1pTsY-WkMED1wfbv3TyTww