博客 | NGINX

使用 NGINX 和 NGINX Plus 执行 A/B 测试

NGINX-F5-horiz-black-type-RGB 的一部分
凯文·琼斯缩略图
凯文·琼斯
2016 年 7 月 25 日发布

当您测试应用的更改时,有些因素只能在生产环境中而不是在开发测试平台上进行测量。 示例包括 UI 变化对用户行为的影响以及对整体性能的影响。 一种常见的测试方法是A/B 测试(也称为拆分测试),其中一部分(通常较小)用户被引导至应用的新版本,而大多数用户继续使用当前版本。

在这篇博文中,我们将探讨为什么在部署 Web应用的新版本时执行 A/B 测试很重要,以及如何使用NGINXNGINX Plus来控制用户看到的应用版本。 配置示例说明了如何使用 NGINX 和 NGINX Plus 指令、参数和变量来实现准确且可衡量的 A/B 测试。

为什么进行 A/B 测试?

正如我们所提到的,A/B 测试使您能够衡量两个版本之间的应用性能或有效性的差异。 也许您的开发团队想要改变 UI 中按钮的视觉排列或彻底改革整个购物车流程,但想要比较交易的成交率以确保更改具有预期的业务影响。 使用 A/B 测试,您可以将一定百分比的流量发送到新版本,将剩余流量发送到旧版本,并衡量两个版本的应用的有效性。

或者也许您关心的不是对用户行为的影响,而是对性能的影响。 假设您计划对 Web应用部署大量更改,但又认为在质量保证环境中进行的测试并不能真正捕捉到对生产性能的可能影响。 在这种情况下,A/B 部署允许您将新版本展示给一小部分指定百分比的访问者,以衡量更改对性能的影响,并逐渐增加百分比,直到最终将更改后的应用推出给所有用户。

使用 NGINX 和 NGINX Plus 进行 A/B 测试

NGINX 和 NGINX Plus 提供了几种控制 Web应用流量发送位置的方法。 第一种方法在两种产品中都可用,而第二种方法仅在 NGINX Plus 中可用。

这两种方法都根据捕获客户端特征(例如其 IP 地址)或请求 URI(例如命名参数)的一个或多个 NGINX 变量的值来选择请求的目的地,但它们之间的差异使它们适用于不同的 A/B 测试用例:

  • split_clients方法根据从请求中提取的变量值的哈希值来选择请求的目的地。 所有可能的哈希值集合在应用版本之间进行分配,并且您可以为每个应用分配不同比例的集合。 目的地的选择最终是随机的。
  • 粘性路由方法为您提供了对每个请求的目的地的更大控制权。 应用的选择基于变量值本身(而不是哈希),因此您可以明确设置哪个应用接收具有特定变量值的请求。 您还可以使用正则表达式根据变量值的部分内容来做出决策,并且可以优先选择一个变量而不是另一个变量作为决策的基础。

使用split_clients方法

在这个方法中, split_clients配置块为每个请求设置一个变量,该变量决定proxy_pass指令将请求发送到哪个上游组。 在下面的示例配置中, $appversion变量的值决定了proxy_pass指令发送请求的位置。 split_clients块使用哈希函数动态地将变量的值设置为两个上游组名之一,即version_1aversion_1b

http { # ... #application版本 1a 上游 version_1a { 服务器 10.0.0.100:3001; 服务器 10.0.0.101:3001; } #application版本 1b 上游 version_1b { 服务器 10.0.0.104:6002; 服务器 10.0.0.105:6002; } split_clients "${arg_token} “$appversion { 95% version_1a; * version_1b; } 服务器 { # ... listen 80; 位置 / { proxy_set_header 主机 $host; proxy_pass http://$appversion; } } }

split_clients指令的第一个参数是字符串( “${arg_token} “在我们的例子中),在每次请求期间使用 MurmurHash2 函数进行散列。 URI 参数在 NGINX 中可用作名为$arg_ name的变量——在我们的示例中, $arg_token变量捕获名为token的 URI 参数。 您可以使用任何NGINX 变量或变量字符串作为要进行散列的字符串。 例如,您可以对客户端的 IP 地址( $remote_addr变量)、端口( $remote_port )或两者的组合进行散列。 您希望使用在 NGINX 处理请求之前生成的变量。包含有关客户端初始请求的信息的变量是理想的;示例包括前面提到的客户端的 IP 地址/端口、请求 URI 甚至 HTTP 请求标头。

split_clients指令的第二个参数(在我们的示例中为$appversion )是根据第一个参数的哈希值动态设置的变量。 花括号内的语句将哈希表划分为“桶”,每个桶包含一定百分比的可能哈希值。 您可以创建任意数量的存储桶,并且它们不必都具有相同的大小。 请注意,最后一个存储桶的百分比始终由星号 (*) 表示,而不是特定数字,因为哈希值的数量可能无法均匀地分成指定的百分比。

在我们的示例中,我们将 95% 的哈希值放在与version_1a上游组关联的存储桶中,其余的哈希值放在与version_1b关联的第二个存储桶中。 可能的哈希值范围是从 0 到 4,294,967,295,因此第一个存储桶包含从 0 到大约 4,080,218,930(总数的 95%)的值。 $appversion变量设置为与包含$arg_token变量哈希值的存储桶关联的上游。 举一个具体的例子,哈希值 100,000,000 属于第一个存储桶,因此$appversion被动态设置为version_1a

测试split_clients配置

为了验证split_clients配置块是否按预期工作,我们创建了一个测试配置,将请求按照与上述相同的比例(95%和余数)划分到两个上游组之间。 我们配置了组中的虚拟服务器以返回一个字符串,指示哪个组( version_1aversion_1b )处理了请求(您可以在此处查看测试配置)。 然后我们使用curl生成 20 个请求,通过在urandom文件上运行cat命令随机设置 URI 参数token的值。 这纯粹是为了演示和随机化的目的。 正如我们预期的那样,每 20 个请求中就有 1 个(95%)是由version_1b提供的(为简洁起见,我们仅显示其中 10 个请求)。

# for x in {1..20}; do curl 127.0.0.1?token=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1); done令牌:p3Fsa86HfJDFwZ9ZnYz4QbaZLzcb70Ka 由站点 version_1a 提供。 
令牌:a7z7afUz7zQijXwsLGfj6mWyPU8sCRIZ 由站点 version_1a 提供。 
令牌: CjIr6W57NtPzChf4qbYMzD1Estif7jOH 由站点 version_1a 提供服务。...省略了 10 个请求的输出...
令牌:gXq8cbG3jhl1LuYmICPTfDQT855gaO5y 由站点 version_1a 提供。 
令牌: VVqGuNN3cRU2slsl5AWOR93aQX8nXOpk 由站点 version_1a 提供。 
令牌:z7KnewxTX5Sp6wscT0fgmTDohTsQuCmy由站点 version_1b 提供!!
令牌:fWOgH9WRrR0kLJZcIaYchpLhceaQgPD1 由站点 version_1a 提供。 
令牌:mTADMXrVnwnr1cd5JE6QCSkgTwfWUnDk 由站点 version_1a 提供。
令牌:w7AzSNmNJtxWZaH6cXe2PWIFqst2o3oP 由站点 version_1a 提供。 
令牌: QR7ay0dA39MmVlXtzgOVsj6SBTPi8ECC 由站点 version_1a 提供。

使用粘性路由方法

在某些情况下,您可能希望通过根据 NGINX 变量的全部或部分值做出客户端路由决策来定义静态路由。 您可以使用粘性路由指令执行此操作,该指令仅在 NGINX Plus 中可用。 该指令采用一个或多个参数的列表,并将路由设置为列表中第一个非空参数的值。 我们可以使用此功能优先对请求中的哪个变量控制目的地的选择进行排名,从而在单个配置中容纳多种流量分割方法。

使用此方法有两种不同的方法。

  • 使用客户端方法,您可以根据 NGINX 变量选择路由,这些变量包含最初直接从客户端发送的值,例如客户端的 IP 地址或特定于浏览器的 HTTP 请求标头,如客户端的User-Agent
  • 通过服务器端或应用端方法,您的应用将决定将首次用户分配到哪个测试组,并向其发送包含代表所选组的路由指示符的 cookie 或重定向 URI。 客户端下次发送请求时,它会显示 cookie 或使用重定向 URI;粘性路由指令会提取路由指示符并将请求转发到适当的服务器。

我们在示例中使用应用端方法:上游组中的粘性路由指令优先将路由设置为服务器提供的 cookie 中指定的值(在$route_from_cookie中捕获)。 如果客户端没有 cookie,则将路由设置为请求 URI ( $route_from_uri ) 的参数值。 然后,路由值决定上游组中的哪个服务器获取请求——如果路由为a则获取第一个服务器,如果路由为b则获取第二个服务器(这两个服务器对应我们应用的两个版本)。

上游后端 { 区域后端 64k;
服务器 10.0.0.200:8098 路由=a;
服务器 10.0.0.201:8099 路由=b;

粘性路由 $route_from_cookie $route_from_uri;
}

但是ab嵌入在实际的 cookie 或 URI 中的更长的字符串中。为了仅提取字母,我们为每个 cookie 和 URI 配置一个map配置块:

映射 $cookie_route $route_from_cookie { ~.(?P<route>w+)$ $route;
}

映射 $arg_route $route_from_uri {
~.(?P<route>w+)$ $route;
}

在第一个map块中, $cookie_route变量代表名为ROUTE的 cookie 的值。 第二行的正则表达式使用Perl 兼容正则表达式(PCRE) 语法,将部分值(在本例中为句点后的字符串 ( w+ ))提取到命名捕获组路由中,并将其分配给具有该名称的内部变量。 该值也被分配给第一行的$route_from_cookie变量,这使得它可以传递给粘性路由指令。

举例来说,第一个map块从此 cookie 中提取值“ a ”并将其分配给$route_from_cookie

路线=iDmDe26BdBDS28FuVJlWc1FH4b13x4fn .a

在第二个map块中, $arg_route变量表示请求 URI 中名为route的参数。与 cookie 一样,第二行的正则表达式提取 URI 的一部分 - 在本例中,它是route参数中句点后的字符串 ( w+ )。 该值被读入命名的捕获组,分配给内部变量,并且还分配给$route_from_uri变量。

举例来说,第二个map块从此 URI 中提取值b并将其分配给$route_from_uri

www.example.com/shopping/my-cart?route=iLbLr35AeAET39GvWK2Xd2GI5c24y5go.b

这是完整的示例配置。

http { # ...
映射 $cookie_route $route_from_cookie {
~.(?P<route>w+)$ $route;
}

映射 $arg_route $route_from_uri {
~.(?P<route>w+)$ $route;
}

上游后端 {
区域后端 64k;
服务器 10.0.0.200:8098 路由=a;
服务器 10.0.0.201:8099 路由=b;

粘性路由 $route_from_cookie $route_from_uri;
}

服务器 {
监听 80;

位置 / {
# ...
proxy_pass http://backend;
}
}
}

测试粘性路由配置

至于split_clients方法,我们创建了一个测试配置,您可以在此处访问。 我们使用curl发送名为ROUTE的 cookie 或在 URI 中包含路由参数。cookie 或参数的值是通过在urandom文件上运行cat命令生成的随机字符串,并附加.a.b

首先,我们用以.a结尾的 cookie 进行测试:

# curl --cookie "ROUTE=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1).a" 127.0.0.1 Cookie 值: R0TdyJOJvxBkLC3f75Coa29I1pPySOeQ.a 请求 URI:/ 结果: 站点 A - 在端口 8089 上运行

然后我们用以.b结尾的 cookie 进行测试。

# curl --cookie "ROUTE=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1).b" 127.0.0.1 Cookie 值: JhdJZrScTnPBLhqmzK3podNRcJAIc8ST.b 请求 URI:/ 结果: 站点 B - 在端口 8099 上运行

最后,我们在请求 URI 中使用以.a结尾的路由参数,而不是使用 cookie 进行测试。 输出确认,当没有 cookie( Cookie字段为空)时,NGINX Plus 使用从 URI 派生的路由值。

# curl 127.0.0.1?route=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1).a Cookie 值:
请求 URI:/?route=yNp8pHskvukXK6XqbWefhVUcOBjbJv4v.a 结果: 站点 A - 在端口 8089 上运行

记录并分析结果

我们在此处描述的测试类型足以验证配置是否按预期分配请求,但解释实际 A/B 测试的结果需要更详细的记录和对请求处理方式的分析。 进行日志记录和分析的正确方法取决于许多因素,超出了本文的讨论范围,但 NGINX 和 NGINX Plus 提供了复杂的内置日志记录和请求处理监控。

您可以使用log_format指令定义包含任何 NGINX 变量的自定义日志格式。 NGINX 日志中记录的变量值可供稍后分析。 有关自定义日志记录和运行时监控的详细信息,请参阅NGINX Plus 管理指南

最后要考虑的一些事情

在设计实验或 A/B 测试计划时,请确保在应用版本之间分配请求的方式不会预先确定结果。 如果您想要进行完全随机的实验,使用split_clients方法并对多个变量的组合进行散列可提供最佳结果。 例如,基于请求中的 cookie 和用户 ID 的组合生成唯一的实验令牌可以提供比仅对客户端的浏览器类型和版本进行散列更随机的测试模式,因为很有可能许多用户拥有相同类型和版本的浏览器,因此所有用户都将被定向到同一版本的应用。

您还需要考虑到许多用户属于所谓的混合组。 他们通过多种设备访问网络应用——可能包括工作和家用电脑,也可能包括平板电脑或智能手机等移动设备。 此类用户拥有多个客户端 IP 地址,因此如果您使用客户端 IP 地址作为选择应用版本的基础,他们可能会看到您的应用的两个版本,从而破坏您的实验结果。

可能最简单的解决方案是要求用户登录,以便您可以跟踪他们的会话 cookie,就像我们的粘性路由方法示例一样。 这样,您就可以跟踪他们,并始终将他们发送到他们第一次参加测试时看到的相同版本。 如果您不能这样做,有时将用户分成在测试过程中不太可能发生变化的组是有意义的,例如使用地理位置向洛杉矶的用户显示一个版本,向旧金山的用户显示另一个版本。

结论

A/B 测试是一种有效的方法,通过在备用服务器之间分割不同数量的流量来分析和跟踪应用的变化以及监控应用性能。 NGINX 和 NGINX Plus 都提供了指令、参数和变量,可用于构建可靠的 A/B 测试框架。 它们还使您能够记录有关每个请求的有价值的详细信息。 享受测试吧!

亲自尝试 NGINX Plus 和粘性路由方法 - 立即开始30 天免费试用联系我们讨论您的用例


“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”