Overlapping Experiment Infrastructure:
More, Better, Faster Experimentation
Author: Diane Tang, Ashish Agarwal, Deirdre O’Brien, Mike Meyer
译者:Koala++ / 屈伟
初校版 version 1.1.0
Introduction
Google 是一个数据驱动型公司,这意味着所有对用户的改动的发布,都要决策者以相
应的经验数据作为依据。这些数据大部分是由在线流量上的实验产生的。在 web 的语境下,
一个实验是由一股流量(比如,用户的请求)和在这股流量上进行的修改组成的。用户可见
的修改(比如,修改顶部广告的背景色),以及不可见的修改,比如测试一个新的广告点击
率(CTR)预测算法,都可以通过实验的方式进行的。
要支持数据驱动方法论的挑战在于要跟上创新的速度。我们想支持进行尽可能多的实验,
如果实验平台要限制同时进行的实验的数量,那是绝不可被接受的。我们进行实验是为了测
试一些新的特性和挖掘一些已有特性的提升空间。对于已有特性,实验可以学习到用户的反
应并可以对特性进行优化。试想一下,如果在搜索结果页上的内容都是通过参数控制的,包
括展示方式和算法。通过对参数设置不同的参数值进行实验,我们可以用衡量指标(用户体
验,收入或其它指标)来决定是否要进行哪些修改以得到最好的结果。
对 UI 的修改通常会使用实验来评价用户反应,但需要注意的是算法的修改同样也需要
实验。例如:假设一些组想测试一个新的机器学习算法来预测广告 CTR,或是测试对现有
算法的调整(比如,修改学习速度或是收敛速度)。虽然线下评估可以进行一些分析后,得到
参数的最佳取值区间(不是最佳取值),但最终这些参数还是需要在线上流量进行评估,分析
这些参数在真实的流量上的效果(因为修改可能会影响用户的行为,并改变流量本身的模式,
这是不可能在线下环境评估的)。所以,评价这些机器学习算法是需要通过线上实验的方式
进行的。
设计我们实验平台的目标是:更多,更好,更快。
更多:我们需要能同时进行多个实验的可扩展性。但是我们也需要灵活性:不同的实验需要
不同的配置和不同的流量来衡量实验的统计意义上的显著效果。有些实验只需要修改流量的
一个子集,比如只是日语的流量,出需要取一个合理的流量规模。其它的实验有可能需要修
改所有的流量,这就需要对评价指标进行很大的修正,这样才可以在小流量上进行测试。
更好:不合理的实验是不应该让它在线上流量进行的。合理的但是很差的实验(比如,有
bug 的实验或是无意中产生的很差的实验结果)都应该能很快的被捕获并且停止它的进行。
标准化的标价指标可以让所有的实验进行公平的比较:比如在计算 CTR 指标的时间,两个
实验应该用相同的过滤器去掉爬虫流量。
更快:能够很容易并且很快地建立一个实验。容易到非工程师不需要写代码就可以创建一个
实验。评价指标应该很快的被统计出来,以便分析。简单的迭代可以很快速地进行。理想状
态是,实验系统不仅支持实验,并且可以控制放量,比如,以一种系统的和容易理解的方式
对实验进行放量。
为了达到这些设计的目标,我们不仅需要实验架构来进行更多的实验,并且需要一些工
具和指导过程来支持更多和更快的实验。
对于实验架构,有两个很明确的选择,或是要支持单层实验或是要支持多因素实验。单
层实验意味着每个请求最多只会通过一个实验,单层实验是很容易使用的,并且也具有灵活
性,但是扩展性不足。多因素实验在统计学上进行了大量的讨论,多因素实验中每个参数(因
素)都可以被独立地实验,在实验中每个参数(因素)都可以独立地被实验,每个实验中只
测试一个参数,这个参数会覆盖所有其它实验中的其它参数。每个查询可以同时在 N 个实
验中,其中 N 是参数的个数。虽然这种方法进行了多年的研究和实践,但对于 Google 的
系统却不适用,因为 Google 有几千个参数,并且不能被独立的分析。例如:要对两个参数
进行分析,一个参数是 web 页面的背景色,另一个是文字的颜色,虽然“蓝色”对两个参
数都是合法值,但是如果两个参数都取“蓝色”,那么页面是不可读的。
本文提出的解决方案是将参数分成子集,每个参数子集包含相互不能独立修改的参数。
一个参数子集会与一个包含实验(s)的层相关联,不同层的实验的流量是互不相交的。每个
query 可以在 N 个实验中,其中 N 是层的数量 。
Related Work [Omit]
Background
在讨论 Google 的实验之前,我们先描述一下我们实验架构所处的环境,这样能更清楚
的理解我们实验架构所要设计的目标和所受到的限制。
宏观上看,用户通过它们浏览器发送请求与 Google 交互。请求进入 Google 的服务系
统后,会先后被多个服务处理(运行在服务器上的程序),然后产生面向用户的结果页。比
如,可能有一个服务决定与查询最相关的原生搜索结果,另一个服务决定与查询最相关的广
告(s),还有一个服务将原生搜索结果和广告结果组织到结果页,返回给用户。见图 1。一方
面,这种模块化可以让我们降低延时(不相互依赖的过程可以并行),显然,原生的搜索过
程与广告搜索过程是相互独立的,并能更快速的试验(每个服务都可以独立地进行,并且模
块化的测试可以进行更快速的发布)。另一方面,如果要求每个请求最多只进入一个实验,
那么模块化就需要更精心地设计。可能存在的问题有饥饿(上游可能会优先处理了所有请求,
没有请求走向下游),和偏置(比如,上游处理了所有英语的请求,那么下游就只有非英语
请求)。
图 1:一个请求经过多个模块的例子,信息(和时间)都是从左流向右
每个服务都有二进制推送和数据推送。二进制推送是指发布新的程序(bug 修复,性
能提升,新特性,等等),它一定时期进行一次(比如,每周)。数据推送更频繁(比如,被
拉取或是每几小时推送一次),并且这还涉及了推送更新的数据到相应的程序。数据推送中
还包含了默认参数配置,参数是用来配置程序如何运行,比如,控制结果如何展示的服务也
许有一个参数是决定顶部广告块的背景色。再比如,预测 CTR 的服务可能有参数是控制学
习速度和收敛速度的。程序可能有几百个参数。新的特性可能会添加一个或多个参数:最简
单的场景是,一个参数可以控制打开或关闭新特性,在更复杂的场景中,也许有多个参数决
定新特性如果展示,有数值阈值决定新的特性是否被展示,等等。将程序和数据分离,意味
着如果我们可以找到合适的分离方式,我们就可以同时得到快速影响线上服务的通路,和慢
速影响线上服务的通路(程序是慢的通路,改变参数值是快速的通路)。
一个 web 搜索中的实验是指将一部分请求流量转向一个特定的处理路径,这个处理路
径会改变向用户展示的内容。一个对照实验将一部分请求流量转向一个处理路径,但它并不
改变向用户展示的内容。我们用数据推送来决定实验的配置。在数据推送中,有一个文件决
定程序的默认参数配置。另一个文件决定实验所需要改变的参数的值,实验只用指定实验所
要改变的参数,对于其它参数,都采用默认值。比如,在一个简单实验中,它只改变顶部广
告的背景色这一个参数,它可以改变黄色(默认值)到粉色(实验值)。
实验还需要决定实验所用的流量如何分配。最简单的分配方式是用随机流量,即对每个
请求都进行随机分配。但这样做的问题是如果实验是用户可见的改变(比如,改变背景色),
那么一个用户可能就得到不同的用户体验(背景色不断地在黄色和粉色间转换),这会造成
用户体验不一致。在 web 实验中常用的方法是用 cookie 作为流量分配的依据,cookie 被
网站用来定位唯一用户。实践中,cookie 是机器/浏览器相关的,并可能被清除。然而,虽
然一个 cookie 不能唯一定位一个用户,但对于连续的查询,它可以提供给用户一致性的用
户体验。对于实验流量分配,我们并不直接对每单个 cookie 进行分配,而是用 cookie 模
进行分配:用一个 ID 表示一个 cookie,对这个 ID 模 1000,将模相等的流量聚合为实验
流量,比如模等于 42 的流量(译注:42 这个例子绝对不是巧合,Hacker 必读的《银河系
漫游指南》中 42 是万事万物的答案)。假设 cookie 的分配是随机的,那么随意 cookie 的
模数的请求数据也应该是大致相等的。在实验配置中使用 cookie 的模,也可以很容易地检
查流量之间是否有冲突:实验 1 可能会用 cookie 模 1 和模 2,实验 2 可能使用 cookie 模
queryWebServerSearch ResultServerAds ResultServerWebServeruser
3 和模 4,这两个实验就会有相近的大小,理论上,是可以进行比较的流量。
在数据文件中配置实验,可以让实验更快更方便地创建:数据文件是可读的,并容易手
工编辑,不需要进行代码更动,并可以由非工程师进行创建,并且数据文件会比数据的推送
更频繁,它提供了一条快速对现有参数进行快速通路。
在开发我们的实验架构之前,我们使用一个简单的单层实验平台,在这个架构中,每个
请求最多进行一种实验。先分配 Cookie 模的流量的实验,再分配随机流量的实验。上游服
务会优先分配流量,所以如果上游(即 cookie 模的实验)进行了很多实验,那么下游可能
会得不到足够的流量,即饥饿问题。除了这些问题之外(包括前面提到的饥饿和偏置问题),
单层实验可以满足我们设计目标中的易用和相对的灵活性。但是在 Google 数据驱动的文件
中,单层的方法没有足够的可扩展性:我们无法快速地进行足够多的实验。
Overlapping Experiment infrastructure
在本节中,我们将介绍重叠实验平台,它在尽量保留单层实验平台的优点(易用,快速)
的同时,增加了可扩展性,灵活性,和健壮性。我们还实现了一种可控的,定义明确的逐步
放量的方式。
前面解释过,多因素实验并不适用于 Google 的实验场景,因为实验参数可能并不相互
独立(比如,粉色的字和粉色的背景)。有了这个限制,我们的核心思路是将参数划分到 N
个子集。每个子集都关联着一个实验(s)层,每个请求最多会被 N 个实验处理(每层一个实
验)。每个实验只能修改自己层相关联的参数(即在参数子集中的参数),并且同一参数不能
出现在多个层中。
一个很明显的问题是如何划分参数。首先,我们可以根据模块化的程序(服务)对参数
进行子集划分,不同程序的参数可以划分到不同的子集中(这会解决前面提到的饥饿和偏置
的问题)。然而一个程序所有的参数并不一定要在一个参数子集中,我们可以通过分析(比
如,我们知道某些参数是相互独立的)或是通过以前实验(比如,分析以前将参数放到一起
修改的实验)可以对一个程序的参数进行进一步划分。
事实上,我们设计的更加灵活,我们不止是将参数划分集合,再将子集与层相关联。为
了解释灵活性,我们引入了一些定义。在流量和系统参数的语境下,我们有三个关键的概念:
域是指流量的一个划分(一部分流量的意思)。
层是指系统参数的一个子集。
实验是指在一个流量划分上,进行零个或多个参数的修改,并最后改变请求处理的过程。
域和层可以相互嵌套。域中包含层。层中包含实验,层中也可以包含域。在一个层中嵌
套域可以使这一层中的参数在嵌套域中进行进一步划分。开始时,我们有默认的域和层,它
有包含所有的流量和参数,在默认域和层中,比如我们可以:
简单地将参数分为三层(图 2a),这种情况下,每个请求最多只会同时在三个实验中,
每层一个,每个实验只能修改相应层的参数。
我们可以先将流量分为两个域,一个域只有一个单一层(非重叠域),和一个有三个层
的重叠域(见图 2b),在这种情况下,每个请求会分到非重叠域或是重叠域。请求只能
在非重叠域或重叠域其中之一。如果请求在重叠域,那么请求最多在一个实验中(这个
实验可以改变参数集合中的任意参数的值),如果请求在重叠域,那么请求最多在三个
实验中,每层一个实验。并且对于每个实验,只能使用对应层的参数。
图二:重叠分层示意图
这种嵌套看起来有些复杂,但它有几个好处。1. 使用非重叠域可以让我们同时进行改
变大量参数值的组合实验,这些实验参数也许并不常一起使用,2. 它允许我们进行不同参
数划分方式。比如你可以划分出三个域,一个是非重叠的,一个是有两个层的域(即对参数
集合进行一次划分),第三个域进行其它的划分方式(即有不同数量的层)。3. 嵌套可以更
有效地利用空间,可以根据常用的参数划分方式,和哪些跨层的实验经常进行,注意将一个
层的参数从一个层移到另一个层是很容易的,只要确认参数可以安全地与原有层的参数值重
叠,并注意保证不同层的实验会被独立地进行,对于基于 cookie 模的实验,我们用
mod=f(cookie, layer)%1000,而不是 f(cookie)%1000 的方式,虽然这种方式增加了复
杂性,但它也增加了灵活性。对配置的修改是需要付出代价的,特别是对域的修改,修改域
的流量,即是修改实验的流量,比如如果我们将非重叠流量大小从 10%修改到 15%,这多
出来的 5%流量来自重叠域,以前经过重叠域的请求现在会经过非重叠域。
另一个概念是发布层(Launch layers),发布层与前面介绍的实验层有下面区别:
发布层总是在默认域中(比如,它们有全部流量)。
UI LayerSearch results layerAds result layerUI LayerSearch results layerAds result layerNon-OverLappingdomainUI LayerSearch results layerAds result layerNon-OverLappingdomainLaunch Layer 1Launch Layer 2UI LayerSrch LayerAds result layerSearchLayerLaunch Layer 1Launch Layer 2AdsLayerSrch/UILayer(a)(b)(c)(d)
发布层是对参数的一个独立划分,比如,一个参数最多只能同时在一个发布层和最多一
个正常层中(一个域中)。
为了让发布层和正常层的重复参数配合起来。在发布层中的实验有着稍有不同的语法。
特别是,在发布层的实验会为参数提供默认参数,换言之,如果没有正常实验层的实验
覆盖了默认参数,那么在发布层的行为与正常层实验是一样的,但如果正常实验层的实
验覆盖了默认值,那么实验就会用这个覆盖的值,而不是系统的默认值,或是发布层实
验中的参数值。
发布层的示例在图 2c, d 中,定义发布层的作用是可以将实验效果逐渐发布,并以规范
的方式跟踪,这样发布过程,发布层的作用是对每个层的发布的新特性建一个新的发布层,
并当这个特性发布之后,就将这个层删除(新的特性参数值会作为默认参数存在)。并且因
为发布层实验的流量一般都比较大,所以它们可以用于测试特性之间的相互影响,虽然理论
上我们可以测试正常实验层的特性相互影响(比如,如果参数在同一层,我们可以手工设置
创建实验,如果参数在不同层,我们观察实验的交集流量),但因为在正常层中,实验流量
比较少,交集比较小,所以相互影响很难检测。
实验和域都是在操作一份流量,(我们称这种流量为“分配”的流量),分配流量类型和
分配流量条件是两个分配流量策略的概念。
我们在第三节讨论了两种分流类型,即 cookie 模方式和随机方式,还讨论了为了让层
与层之间实现流量之间相互独立,在 cookie 取模时也考虑了层 id(mod=f(cookie,
layer)%1000)。我们还支持另两种分流类型,用户 id 取模和 cookie 日期取模,用户 id 取
模类似于 cookie 取模,区别仅是对用户 id 取模而不是 cookie,对于 cookie 日期取模,
我们将 cookie 的模与日期结合,所以对于某一天来讲,一个 cookie 或是在一个实验或是
不在,但在一个实验的 cookie 集合是每天不变的。在所有的场景中,是没有办法配置一个
实验能使特定的 cookie 或是用户必通过这个实验。类似的,分析是用聚合的查询词,cookie,
用户集合。同样注意,我们当前只支持的四种分类型,但我们也可以支持其它的分流类型,
比如通过 Hash 查询串分流。
支持多种分流类型的主要目的是为保证连续查询的效果一致性,并有发现任何随时间而
变的学习效果的潜在能力。基于这些原因,我们以特定的顺序对不同的分流类型进行分流:
用户 id,cookie,cookie 日期,随机。一旦顺序中有一个类型满足,都不再考虑下面的分
流类型(图 3),虽然这个顺序最大化地了一致性,但它也有一个缺点,比如,在同一层中
1%的 cookie 模流量会比 1%的随机流量大,在极端情况下,我们会遇到饥饿问题。在实践
中,一层之中一般只应有一种分流类型,实验和对比实验必须使用相同的分流类型,最主要
的影响是不同的分流类型需要不同的实验规模(见 5.2.1 节)。
在通过分流类型选择一部分流量后,分流条件(condition)通过仅分配特定条件的流
量给实验或域,实现了对流量可用性更强的功能。比如,一个实验仅仅改变来自日语的查询,
那么实验配置中配置“日语”条件。我们的条件设置有地区,语言,浏览器等等。有了分流
条件,一个只使用“日语”流量的实验,和一个只使用英语流量的实验,可以使用相同的
cookie 模。另一个使用分流条件的场景是灰度测试新代码(代码是通过二进制推送发布的),
比如,在一股小流量上测试新代码,以保证新代码没有 bug,并与预测一致,然后才能放
到大流量环境中(灰度环境中,通过错误日志和实验监控方式检查 bug)。为了支持这种使
用场景,我们提供了以机器或数据中心为分流条件的分配方式,它进一步限制了一个实验的
流量。虽然灰度实验无法代替严格的测试,但它们是一个有用的补充,因为它既限制了潜在
的错误,并且它让新的代码在真实环境中,从而可以遇到各种在测试环境中很难构造的真实
请求。
图三:决定请求进入域,层,实验的逻辑
分配条件是直接在实验(或域)的配置中指定的,这允许我们在实验的创建时基于数据
文件对流量分配冲突进行检测。如在流量分配类型一节中提到的一样,如果一个请求先满足
了流量分类顺序中的一个类型,它不会再考虑下面的分配类型,即使它不满足这一种分配类
型的分配条件(s)。这很重要,最好以一个例子来说明,如果我们通过特定的 cookie 模来
得到实验的流量,我们将会得到一个无偏的分配。考虑一下一个指定 cookie 模上有两个实
验,一个分配条件为日语流量,另一个分配条件是英语流量,而这个 cookie 模剩余的流量
(即不是日语和英语的流量)将不会分配给以 cookie 分配方式的其它实验,这是为了避免
分配顺序后几种分配方式的偏置,重要的逻辑是不再将上述剩余的流量分配给分配顺序后几
种分配方式的实验了。我们通过将这种不分配的流量分配给一个偏置 id 来避免偏置。
图 3 中展示了一个请求分配给域,层和实验的逻辑。这些逻辑都以动态库的方式实现,
编译链接到二进制之中,所以任何修改(比如,新的分配类型,新的分配条件,等等)都会
在日常的二进制推送时集成到系统中去,动态库允许在系统范围内的一个致性实现,并且意
味着新功能可以马上使用。
在这个架构下,一个特性的评估和发布过程是类似如下过程的:
在合适的模块中,实现新的特性(包括 code review,二进制推送,设置默认参数等
等,如标准的工程实践一样)。
创建一个灰度实验(通过数据推送方式),以保证特性可以正常工作,如果不能正常工
作,那么可能就要写更多的代码。
创建一个实验或是一组实验(通过数据推送的方式)来评估特性,注意配置实验涉及指
定分配类型和相关的分配参数(比如:cookie 模),分配条件,和特性相关的参数。
评估实验指标。根据实验结果,判断是否要进行新一轮的迭代,即通过修改或创建新的
实验,或甚至修改代码从根本上改变特性。
如果特性可以发布,就进行发布过程:创建一个新的发布层和发布层实验,逐步地用发
布层进行放量,并最终删除发布完的发布层,然后将发布层实验的相关参数设为系统默
认参数。
Tools & Process
虽然重叠架构是有能力运行更多的实验,更快速地进行实验,并能同步优化实验效果,
但只依靠架构还是不够的。我们还需要工具,研究,和教育过程来支持更快速的实验。在本
节,我们讨论几个关键的工具和过程,以及它们如果帮助我们扩展的。
Tools
数据文件检查:数据文件的其一优势是它们可以被自动检查错误,这可以避免一些不合
理的实验运行。我们会自动检查法语错误(所有的必填字段都有并且合法),一致性和约束
错误(比如,id 的唯一性,根据所有的参数判断是否实验在正确的层,是否这一层有足够
的流量来支持实验,流量约束检查,如果实验要求的流量已经被另一个实验使用了,等等,
注意当可用的分配条件集合变大时,这些检查就变的复杂了),和基本的实验设置检查(是
否实验有对比实验,并且对比实验在相同的层,是否对比实验与实验的流量分配方式和规模
一致,等等)。
实时监控:我们用实时监控来检测基本的指标(比如 CTR),我们通过实时监控尽快地
发现某个实验是不正常的,实验者可以设置监控指标的期望值区间(也有这些指标的默认波
动区间),如果监控指标超出了期望的波动区间,那么会触发自动告警,然后实验者可以修
改期望区间或停止他们的实验,或调整它们的实验参数值,但它允许实验者可以激进地对于
可能的潜在的变化进行测试,因为错误或预期之外的影响会被很快检测到。
Experiment Design & Sizing
除了基本的对实验配置的基本检查外(比如,每个实验都必须有一个对照实验,它与实