一种简单可行的abtest流量切换实现方案
为什么需要abtest
线上交易系统快发展,业务功能不断迭代,每周按固定频次上线新功能,难免会有一些BUG,全量上线,出现错误后回滚,导致业务单量损失,我们要将这种损失减少或者尽量降低,这就是需要abtest的原因。关于蓝绿部署,灰度发布,金丝雀等应用部署方案不作讨论,最终原理都是一样的,即通过较少量用户体验来发布某些应用新功能。
实现思路
abtest对于业务开发来说,最好是独立的,也就是我们需要在业务开发之外实现,无感知切入abtest,同时注意保持业务一致性,例如在某一时期,a用户始终看到A版,B用户始终看到B版。最终我们选用nginx+lua方案,通过在nginx中执行嵌入的lua脚本,动态计算upstream,将不同的用户导向不同的程序版本,达到abtest的目的。
具体实现
我们通过提取某一个特征cookie标识用户,该cookie在一定周期内针对同一个用户不是随意改变的。假如存在这个cookie,名称为__abc=testuser.123123,如果cookie值为数值化,可以直接进行模运算取余,如果是字符型,先进行一个hash运算得到数值,再进行模运算取余。
如果业务系统不存在特征cookie,条件允许可以在网站域下种一个新的cookie。
数据流示意图如下:
用户b的cookie特征提取为001,跟配置的分流比例300比较,符合条件,将upstream改为b.domain.com, 用户b一直访问新版本程序。
nginx安装lua模块
lua-nginx-module官方文档 ,请参考https://github.com/openresty/lua-nginx-module#installation,也可以直接安装openresty。
nginx conf配置
lua_package_path "/XXXX/servers/lualib/?.lua;;";
lua_package_cpath "/XXXX/servers/lualib/?.so;;";#dns解析服务器,如果redis使用域名连接,可能需要配置dns
resolver 192.168.2.2 192.168.2.3;#初始化全局变量,包括是否启用分流,流量切换比例, 默认为false不启用,流量切换比例0,不分流
init_by_lua_file /XXXX/conf/abtesting/init.lua;#定时从redis中刷新 是否启用分流 和 流量切换比例值
init_worker_by_lua_file /XXXX/conf/abtesting/worker.lua;#默认A版
upstream tomcat_a.domian.com {server 127.0.0.1:1601 weight=100 max_fails=2 fail_timeout=30s ;server 192.168.0.1:80 weight=1 max_fails=2 fail_timeout=30s ;
}#新功能B版
upstream tomcat_b.domain.com {server 192.168.0.2:80 weight=100 max_fails=2 fail_timeout=30s ;server 192.168.0.3:80 weight=1 max_fails=2 fail_timeout=30s ;
}server {listen 80;set $default_backend 'tomcat_a.domain.com';location / {proxy_next_upstream http_500 http_502 http_503 http_504 error timeout; proxy_set_header Host 'y.domain.com';proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;expires 0;set $backend $default_backend;#此处计算可能会修改backend这个nginx变量,也就是变量修改了upstream#具体执行逻辑是提取用户特征,也就是__abc这个cookie值,是否满足具体规则rewrite_by_lua_file '/XXXX/conf/abtesting/diversion.lua';proxy_pass http://$backend;}}
初始化脚本
global_configs = {["divEnable"] = false, -- 分流开关,true表示开启["newTrafficRate"] = 0, -- 分流比例,0-1000, 1000表示全部流量,100%["redis"] = {ap_host='192.168.1.10', -- redis主机ip或者是hostap_port=6379, -- redis主机端口ap_key='testToken' -- redis连接密码}
}
定时任务脚本
-- 每隔10秒定时执行,可以自行调整定时任务间隔
local start_delay = 10
local new_timer = ngx.timer.at
local log = ngx.log
local ERR = ngx.ERR
local refresh
local get_redis
local close_redis-- redis中分流开关key
local switch_key = "abtest:switch:global"
-- redis中 分流比例key
local traffic_key = "abtest:limit:traffic"-- 连接redis
get_redis = function()local redis = require "resty.redis"local red = redis:new()local ok, err = red:connect(global_configs['redis']['ap_host'],global_configs['redis']['ap_port'])if ok and global_configs['redis']['ap_key'] thenok, err = red:auth(global_configs['redis']['ap_key'])endreturn red, ok, err
end-- 关闭redis连接
close_redis = function(red)if not red thenreturnendlocal ok, err = red:close()if not ok thenngx.log(ngx.ERR,"fail to close redis connection : ", err)end
end-- 真实执行的任务
local function do_refresh()local red, ok, err = get_redis()if not ok thenlog(ERR, "redis is not ready!")returnendlocal traficLimitStr, err = red:get(traffic_key)-- 从redis中刷新 开关 值local enable, err = red:get(switch_key)if err thenlog(ERR, err)elseif ngx.null ~= enable thenglobal_configs["divEnable"] = ("true" == enable) and true or falseendend-- 从redis中刷新 流量比例 值local trafficLimitStr, err = red:get(traffic_key)if err thenlog(ERR, err)elseif ngx.null ~= trafficLimitStr and tonumber(trafficLimitStr) > 0 thenglobal_configs["newTrafficRate"] = tonumber(trafficLimitStr)log(ERR, "update newTrafficRate: ", global_configs["newTrafficRate"])endendreturn close_redis(red)
end-- 任务执行与下次延时处理
refresh = function(premature)if not premature thendo_refresh()local ok, e = new_timer(start_delay, refresh)if not ok thenlog(ERR, "failed to create timer: ", e)returnendend
end-- 程序入口,第一次nginx timer at定时执行
local ok, e = new_timer(start_delay, refresh)
if not ok thenlog(ERR, "failed to create timer: ", e)return
end
分流计算脚本
if not global_configs["divEnable"] thenreturn
endlocal abc = ngx.var.cookie___abcif abc then -- abc这个cookie可能是 123123.0xab23eff1,或者是 1231123.123123123这种,我们提取第二段值的最后3个字符,可能是10进制或者16进制数字,最终值可能会大于1000,所以取余local v = ngx.re.match(abc, [[^\d+\.([0-9a-fA-FxX]+)([0-9a-fA-F]{3})\.]]) if v and v[2] thenlocal ckVal = (tonumber(v[2]) or tonumber(v[2], 16) ) % 1000if ckVal and (ckVal < global_configs["newTrafficRate"]) then ngx.var.backend = "tomcat_b.domain.com" end endend
最后我们简单做个操作界面,用于动态改变redis中的值
点击切换开关,改写流量切换比例
其他
如果cookie是字符串,可以先进行hash运算,下面是一个基于ffi的hash实现可以参考下,文件名是murmurhash2.lua
local ffi = require "ffi"
local ffi_cast = ffi.cast
local C = ffi.C
local tonumber = tonumberffi.cdef[[
typedef unsigned char u_char;
uint32_t ngx_murmur_hash2(u_char *data, size_t len);
]]return function(value)return tonumber(C.ngx_murmur_hash2(ffi_cast('uint8_t *', value), #value))
end
调用示例代码方式如下
local mmh2 = require("abtesting.murmurhash2")-- 对string类型的特征cookie进行hash计算,hash函数是nginx默认实现ngx_murmur_hash2
local hash = mmh2(uid)
local suffix = hash % 1000;
微信关注公众号获取更多精彩内容
一种简单可行的abtest流量切换实现方案相关推荐
- 一种简单可落地的分布式事务实践方案
欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析.实际应用.架构思维.职场分享.产品思考等等,欢迎大家加我微信「java_front」一起交流学习 1 案例背景 用户在电商网站 ...
- ABTest流量分发和业界的一些经验
流量为王的时代,如何精准的利用用户的流量进行分析和产品的迭代?ABTest就是其中不可缺少的一环,那么ABTest是什么呢?下面来一层一层揭开它神秘的面纱. 0.流量分发 在互联网流量的分发模式中,主 ...
- ABTest流量分发和业界的一些做法经验
流量为王的时代,如何精准的利用用户的流量进行分析和产品的迭代?ABTest就是其中不可缺少的一环,那么ABTest是什么呢?下面来一层一层揭开它神秘的面纱. 0.流量分发 在互联网流量的分发模式中,主 ...
- GIT将本地项目上传到Github(两种简单、方便的方法)
GIT将本地项目上传到Github(两种简单.方便的方法) 一.第一种方法: 首先你需要一个github账号,所有还没有的话先去注册吧! https://github.com/ 我们使用git需要先安 ...
- git学习(10):Git的使用--如何将本地项目上传到Github(两种简单、方便的方法)
将本地项目上传到Github(两种简单.方便的方法) 一.第一种方法: 首先你需要一个github账号,所有还没有的话先去注册吧! https://github.com/ 我们使用git需要先安装gi ...
- (转载)一种简单而有趣的数据结构——并查集
一种简单而有趣的数据结构--并查集 作者:goal00001111(高粱) 一个秘密生物武器落到某地区,导致当地村民丧失部分记忆,只认得自己最熟悉的人,而忘记自己是哪个村子的人了.大家汇集到一个广场, ...
- 一种简单而有趣的数据结构——并查集
一种简单而有趣的数据结构--并查集 作者:goal00001111(高粱) 一个秘密生物武器落到某地区,导致当地村民丧失部分记忆,只认得自己最熟悉的人,而忘记自己是哪个村子的人了.大家汇集到一个广场, ...
- 一种简单快速的方式实现 Android App 的夜间模式
博主声明: 转载请在开头附加本文链接及作者信息,并标记为转载.本文由博主 威威喵 原创,请多支持与指教. 本文首发于此 博主:威威喵 | 博客主页:https://blog.csdn.net/ ...
- 四种简单的图像显著性区域特征提取方法-----AC/HC/LC/FT。
四种简单的图像显著性区域特征提取方法-----> AC/HC/LC/FT. 分类: 图像处理 2014-08-03 12:40 4088人阅读 评论(4) 收藏 举报 salient regio ...
最新文章
- MySQL高级 之 explain执行计划详解
- 职责链模式 php,php Chain of Responsibility 职责链模式
- 一个傻瓜式构建可视化 web的 Python 神器
- windows中怎么添加定时任务
- C#的特性Attribute
- 【转】Windows Azure的账户体系
- Linux系统编程40:多线程之基于环形队列的生产者与消费者模型
- Java zip and unzip demo
- 5G产业最新投资机会,25页PPT
- Navicat Report Viewer 如何连接到 MySQL 数据库
- 杭电4554 叛逆的小明
- [bzoj 4939][Ynoi 2016]掉进兔子洞
- Swift iOS HealthKit 使用案例: 获取体温列表 HKHealthStore
- photoshop中魔棒工具的使用
- java版本PID放大/eTerm放大软件介绍
- 『原创』老范的来电防火墙v1.0发布了(图文)
- PayPal 更换汇率结算方式 降低手续费,PayPal汇率结算 改为 银行汇率结算
- 算法与数据结构之队列
- 学校计算机科室管理制度,学校科室管理制度资料.doc
- 【概率论】- (2)假设检验