使用Nginx实现A/B测试

背景

我正在研发推荐系统,在设计中,一个推荐接口会有一个或多个RC版本,我需要观测不同版本的有效性,以此选出推荐效果最好的版本上线。

为此,我使用了Nginx控制流量分发,实现A/B测试,然后对接口的不同版本进行分析,以此评估有效性。

这里对使用Nginx实现A/B测试的探索做一个记录。

A/B测试简介

当测试应用程序的更改时,有些因素只能在生产环境中测量,而不能在开发测试平台中测量。示例包括UI更改对用户行为的影响以及对整体性能的影响。一种常见的测试方法是A/B测试-也称为拆分测试-在该测试中,通常是一小部分用户被定向到应用程序的新版本,而大多数用户仍在使用当前版本。

通过A/B测试,我们可以将用户群体按照指定比例分成几部分,每个部分对应一个应用程序版本,然后观测不同版本的性能和有效性,以此来辅助应用程序的更新决策。

使用Nginx实现A/B测试

Nginx和Nginx Plus提供了两种方式控制流量分发:

  • split_clients - 基于hash的流量分发。在Nginx和Nginx Plus中都有效。
  • sticky route - 可以显式控制流量分发。只在Nginx Plus中有效。

两种方式适用于不同的场景,可以根据需要选择。

我使用的是split_clients方式,所以这里主要对此进行介绍,如果需要了解两种方式更详细的用法介绍,可以参考文章底部的参考链接。

使用split_clients方式

split_clients配置块的主要功能是为不同的请求返回不同的值,类似if...else...

语法:

split_clients string $variable { ... }

string位置可以使用任何 Nginx参数 或者字符串。

$variable位置可以自定义参数名,以提供其他配置块引用,需以$开头。

配置示例:

http {
    split_clients "${remote_addr}AAA" $variant {
                   0.5%               .one;
                   2.0%               .two;
                   *                  "";
    }

    server {
        location / {
            index index${variant}.html;

原始字符串的值使用MurmurHash2进行哈希处理。在给定的示例中,哈希值从0到21474835(0.5%)对应于$variant变量的值" .one",哈希值从21474836到107374180(2%)对应于值".two",并且哈希从107374181到4294967295的值对应于值""(空字符串)。

在详细介绍之前,这里将应用程序的更新分为两种:

  • 覆盖式更新 - 同一个应用进程只会存在新旧版本的其中之一。在web应用中比较常见。
  • 新增式更新 - 同一个应用进程同时存在新旧版本。在数据接口应用中比较常见。

下面分别对这两种更新方式进行描述并设计A/B测试。

覆盖式更新

一些开发场景中,应用更新会覆盖原有逻辑,在发布时,按照服务进行分组,在不同的服务组中启动不同的应用版本。对于这种更新,应用的新旧版本uri是保持一致的,在处理分流时,需要分发到不同的服务组。

主要配置实现:

# application version 1a 
upstream version_1a {
    server 10.0.0.100:3001;
    server 10.0.0.101:3001;
}

# application version 1b
upstream version_1b {
    server 10.0.0.104:6002;
    server 10.0.0.105:6002;
}

split_clients "${arg_token}" $appversion {
    95%     version_1a;
    *       version_1b;
}

server {
    # ...
    listen 80;
    location / {
        proxy_set_header Host $host;
        proxy_pass http://$appversion;
    }
}

在这个配置中,split_clients配置块中,两个返回值都是upstream的名称,这个返回之后,server下的location配置块中的proxy_pass http://$appversion;就会转化为具体的upstream名称,例如:proxy_pass http://version_1a;,然后接下来的流程就和直接指定upstream名称一样了。

新增式更新

一些开发场景中,应用更新不会覆盖原有逻辑,而是增加新的版本,在发布时,在每个服务组中同时存在应用的多个版本。对于这种更新,应用的新旧版本uri是有版本号差异的,在处理分流时,需要分发到不同的uri版本。

不同版本的uri示例:

/recommender/ui
/rc0/recommender/ui
/rc1/recommender/ui

主要配置实现:

upstream recommender {
    server 10.1.22.17:3031;
    server 10.1.22.18:3031;
}


split_clients "${arg_userid}" $version {
    5.0%     /rc0;
    5.0%     /rc1;
    *        '';
}

server {
    # ...
    listen 80;
    location / {
        proxy_set_header Host $host;
        proxy_pass http://recommender$version$request_uri;
    }
}

在这个配置中,split_clients配置块中,两个返回值对应的时接口版本前缀,这个返回之后,server下的location配置块中的proxy_pass http://recommender$version$request_uri;进行了拼接,将接口版本前缀增加到uri中,例如:proxy_pass http://recommender/rc0$request_uri;,然后接下来的流程就和直接指定upstream名称一样了。

这里注意proxy_pass http://recommender$version$request_uri;的写法,这是对uri进行了重组,$request_uri一定要加上,否则uri不正确。

使用sticky route方式

没有深入研究sticky route方式,这里只对其进行简单介绍。
sticky route(粘性路由)可以实现显示控制流量分发,通过在cookie或uri中添加route参数,根据route参数的特征进行显示控制客户端和应用版本的映射关系,以保证客户端使用的总是同一个应用版本。
有两种不同的方式使用sticky route

  • 客户端方式。基于NGINX变量选择路由,这些变量包含最初直接从客户端发送的值,例如客户端IP地址或者浏览器专用的HTTP请求头。
  • 服务端或应用端方式。 由应用程序确定首次用户分配给哪个测试组,然后向其发送cookie或重定向URI,其中包括代表所选组的路由指示符。客户端下一次发送请求时,它将显示cookie或使用重定向URI。sticky route指令提取路由指示器并将请求转发到相应的服务器。

使用应用端方式更加繁琐但是更加可控,可以适用更复杂的场景。

示例

示例使用应用端方式。

upstream backend {
    zone backend 64k;
    server 10.0.0.200:8098 route=a;
    server 10.0.0.201:8099 route=b;

    sticky route $route_from_cookie $route_from_uri;
}

在upstream组的sticky route优先设置route的值为一个服务提供的cookie中指定的值(捕获于$route_from_cookie)。如果客户端没有cookie,则route被设置为请求URI中一个参数的值($route_from_uri)。然后route值决定upstream组中哪个server获取请求 —— 如果route是a,第一个server获取请求;如果route是b,第二个server获取请求。两个server对应应用的两个版本。
在实际情况中,ab 是内嵌在更长的字符串中,为了只提取字母,我们可以为cookie和URI各配置一个map配置块。

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

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

在第一个map中,$cookie_route 代表名为ROUTE的cookie的值,第二行的正则表达式使用的是 Perl Compatible Regular Expression (PCRE) 语法,提取值的一部分,并将提取值分配给一个名为route的内部参数,同时也分配给第一行的 $route_from_cookie 参数,使其可用于传递到sticky route指令。
示例:

ROUTE=iDmDe26BdBDS28FuVJlWc1FH4b13x4fn.a

在第二个map中,$arg_route代表URI中一个名为route的值,与cookie方式类似,第二行的正则表达式提取值除了分配给一个名为route的内部参数,同时也分配给第一行的 $route_from_uri 参数。
示例:

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

完整配置示例:

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

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

    upstream backend {
        zone backend 64k;
        server 10.0.0.200:8098 route=a;
        server 10.0.0.201:8099 route=b;

        sticky route $route_from_cookie $route_from_uri;
    }

    server {
        listen 80;

        location / {
            # ...
            proxy_pass http://backend;
        }
    }
}

结束语

我对推荐接口更新和评估的设计是,对外服务的地址是不变的,后端接口可以根据需要使用不同的版本。这样对于调用端是无感知的,对数据服务的更新和新推荐算法的评估也会比较方便,皆大欢喜。

经过探索和测试,我完成了 新增式更新 中的这种设计,达到了我的预期效果,同时这种设计,在需要多个版本共存的情境中也是适用的。

A/B测试是一个有效的方式,用来分析和追踪应用并监控应用性能。
split_client适用于更加随机的场景,通过选择合适的参数组合,可以得到更好的效果。
sticky route适用于静态路由的场景,可以保证客户端和应用版本的固定映射,以此来保证用户看到的总是同一个版本,提高用户体验和保证测试效果有效性。
不同的场景选择不同的方案。

参考:

https://www.nginx.com/blog/performing-a-b-testing-nginx-plus/

https://www.nginx.com/blog/dynamic-a-b-testing-with-nginx-plus/

https://nginx.org/en/docs/http/ngx_http_split_clients_module.html?_ga=2.251322371.442393638.1597455335-461953950.1597455335

https://nginx.org/en/docs/varindex.html?_ga=2.109055299.778078225.1597384417-2056163875.1597384417

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值