我们在做业务项目需求的时候经常会做ABtest,在发布时也会做灰度发布,通常这些ABtest都是在同一应用上做的,即我们在A应用上开发新版的代码,并通过代码控制分桶和打点。但是我们也经常遇到这种情况:新版实验与老版不在同一个应用上,那么之前的方案就无法做切流了。
1. 问题定位:
- 我们希望 分流解决方案将一部分流量分到新应用,另一部分流量分到老应用,且该流量是可以控制的
- 我们希望新老版本有明显的标识来区分用户命中的是新版还是老版(即打点)
- 假如“我们严谨的PD们”想要看AB对比数据,我们还要比较方便的从报表分区新老版命中
上述三条其实基本构成了一个简易的AB系统,类似我们常用的buckettest、BTS等,当然 BTS此类实验平台还有一个比较完善的控制台来控制切流和报表汇总。
2. 问题解决:
一般此类跨应用切流都会有类似的应用依赖访问结构:
虚线是新版的访问路径,对于γ类型,如果要做ABtest,需要在上层vip/lvs层做,过于复杂,因此可以转化成β的结构,或者将B中的 新老应用层级交换一下。对于β形态,我们完全可以将老应用A 当成α形态中的老应用,因此我们只需对α形态进行讨论。
1)思路一:通过发布批次控制切流节奏
这是我们做业务页面迁移时比较常用的方法,即在应用M层修改 反向代理逻辑,使请求转发到新应用B,并通过发布的批数来控制切流节奏。
优点: 修改方便,只需发布一次M,修改出错成本低;
缺点: 无法控制用户访问新版老版,只能由应用M的lvs或VIPServer的负载均衡做随机分流,如果遇到流量不均衡问题,切流会十分不均衡。业务效果无法对比,因为用户会时而刷出新版,时而刷出老版。发布周期长,需要长时间占用发布流程。
上述方案一般用于 只迁移,不做业务数据对比的技术改造升级项目。
2)思路二:在应用M层的tengine/nginx层做分流
优点: 分流策略可以根据cookie、ip、ua等灵活配置,可以比较精确的控制流量分布;
缺点: 需要至少发布两次,配置较为复杂,容易搞出问题
那么我们就研究一下如何在tengine里面做切流吧:
3. 在tengine/nginx 层做AB test
1)分流器设计:
使用 if语句做分流器:
例如我们对/abc/ path下的请求,cookie中含有version=1的转发到老应用,对version=2的转发到新应用:
set $version "default";
if ($http_cookie ~* "version=1") {
set $version v1;
}
if ($http_cookie ~* "version=2") {
set $version v2;
}
location /abc/ {
if ($version = v1) {
proxy_pass http://A_APP;
}
if ($versuib = v2) {
proxy_pass http://B_APP;
}
......
}
复制代码
使用 map做分流器:
例如我们对/abc/ path下的请求,cookie中含有version=1的转发到老应用,对version=2的转发到新应用:
map $COOKIE_version $version {
1 v1;
2 v2;
default default;
}
location /abc/ {
if ($version = v1) {
proxy_pass http://A_APP;
}
if ($versuib = v2) {
proxy_pass http://B_APP;
}
...
}
复制代码
注: $COOKIE_version 是nginx的语法,指获取cookie中key=version的值
使用split_clients 方法:
##下面在http 块中
split_clients "$COOKIE_cna" $appversion {
50% v1;
* v2;
}
##下面在server块中
location /abc/ {
if ($version = v1) {
proxy_pass http://A_APP;
}
if ($versuib = v2) {
proxy_pass http://B_APP;
}
...
}
复制代码
注:cna是我们常用的cookie分流的值,每一个用户的cna是一样的,保证能按照cookie进行分流
使用lua 编写分流脚本:
init_by_lua '
mmh2 = require "murmurhash2"
';
location /abc/ {
set $version "default";
set_by_lua '
local cna = ngx.var.cookie_cna;
local hash_code = mmh2(cna) % 100;
if hash_code >= 50 then
ngx.var.version = v1;
else
ngx.var.version = v2;
end
';
if ($version = v1) {
proxy_pass http://A_APP;
}
if ($versuib = v2) {
proxy_pass http://B_APP;
}
...
}
复制代码
注:mmh2 = require "murmurhash2" 为引入第三方hash函数:murmurhash2;
处理第一次请求时无cookie情况:
按照惯例,第一次无cookie的情况会随机一个数来进行分流,第二次来访问时再根据cookie进行重新分流,虽然会导致有1/2的概率会导致用户第一次访问和第二次不一致,但是由于我们的业务第一次无cookie访问的用户大部分是新用户,有超过60%的用户没有第二次访问,因此这个比例是比较小的。
如果要做到绝对的精确分流,就要对无cookie的用户增加一个cookie来标示其所属的桶。两种方法分别对应:
set_by_lua '
local cna = ngx.var.cookie_cna;
if cna == '' or cna == nil then
math.randomseed(1);
nvx.var.cookie_cna= math.random(0,100);
end
';
复制代码
@@需要进行精确分流的方法:
set $random_num 101;
set_by_lua '
local cna = ngx.var.cookie_cna;
if cna == '' or cna == nil then
math.randomseed(1);
nvx.var.random_num = math.random(0,100);
end
';
if ($random_num != 101) {
add_header Set-Cookie "random_num=$random_num;";
}
## 在后续的判断中首先根据random_num进行分流,再根据cna进行分流
复制代码
2)分流比例控制:
由于上面1) 中的默认都是设置的50%比例切流,如果“我们可爱的PD”要求2:8分咋整?要么我们改一下上面的比例重新发布一下,要么引入实时干预的某个东西。当然重新发布对于我们懒惰的程序员来说是无法忍受的。还好tengine支持访问diamond和tair:
http {
diamond_server jmenv.tbsite.net:8080;
diamond_app group dataid $content $version;
split_clients "$COOKIE_cna" $appversion {
$content v1;
* v2;
}
...
}
复制代码
注: 上面代码未经线上测试,如要使用,请自行测试验证。content变量就是从diamond里面读取到的设置的ratio啦,可以设置为0%,10%,50%等等。
3)分流打点与数据查看:
只分流不打点,业务数据没法看啊,所以我们得想办法把新版和老版本区分开。我们可以在nginx里面搞定或者在新老版本的应用里面 打上对应的业务点位。在我们的实践中,是采用的第二种方案,是为了延续之前做BT的参数,使用resion_trace 字段中的cid打点。你也可以在新版中加个cookie: bts_v = 1, = 2, 然后在日志报表中 捞对应的cookie来判断。
总结
通过上述的三点,我们可以搭建起一套比较完整的切流、动态调整分流比例、查看业务数据 的跨应用切流方案了。
如果你想使用nginx+lua的 超强并发能力,或者对nginx、lua有很深的造诣,或者你想写一堆代码并成为“不可替代的程序员”,可以尝试在tengine/nginx 层做很多业务逻辑,如AB test、连数据库、拼装页面等等。
参考文献:
- www.nginx.com/blog/perfor…
- nginx.org/en/docs/htt…
- gist.github.com/jmervine/60…
- blog.prototypr.io/a-b-testing…
- blog.csdn.net/lzyzsd/arti…
- github.com/CNSRE/ABTes…
- shift-alt-ctrl.iteye.com/blog/232023…
- yanglefeng.iteye.com/blog/133764…
- www.hi-linux.com/posts/34319…