可以带着下面的问题逐个分析:
相对于
AbTest
是不是重复了?或者两者的差异是什么?为什么会有这样的需求?前端灰度能够真正解决业务上的哪些问题?
实际的灰度需求应该是怎样的?
一、AbTest 是个啥?如何区分和灰度的功能?
AbTest
也是某种程度上的功能灰度,但是和灰度发布有着本质上的区别是两种产品类型,有的同学觉得有 AbTest
就够了,这里稍微说一下两者在前端功能上的区别,根据以往对 AbTest 在前端页面功能使用的理解,我认为主要作用是根据一定数量的样本流量(通常是有状态),通过一系列功能标签控制在前端视觉上呈现 “千人千面” 的页面功能差异,不仅可以在不同的流量终端呈现不同的功能差异,更重要的是支持这一特性后可以在产品功能上评估某一功能是否能给整体产品带来正向价值。...
const { preview_btn_flag } = this.computed_flags
if (preview_btn_flag === 1) {
return <button>点赞</button>
}
...
通过不同标签或者标签组合来确定某一业务功能,如下图:
(1) userAgent
(2) 请求 url 带指定参数
(3) 请求 cookie 中状态,结合虚机上多版本的发布支撑,通过流量的规则仲裁服务返回执行的资源版本
二、为什么会有这样的需求?能够解决业务的哪些问题?
【发现问题】
【分析需求】
现有的前端项目部署方式
三、实际的灰度需求应该是怎样的?
规则管理后台基于一些状态配置应用的灰度规则信息通过 ack 后,推入 apollo 配置中心,通过 apollo 长轮询机制读取灰度规则缓存到机器(避免规则获取异常或者频繁获取的情况)并将规则缓存在内存共享到指定全局变量:
支持多种条件的规则配置,对于灰度过程中出现的异常状况,支持 一键中止 ,或者 全量灰度 秒级生效。
nginx.conf
lua_shared_dict apollo_config 5m;
init_worker_by_lua_file /xx/apollo_openresty.lua;
apollo_openresty.lua
长轮询读取 apollo 中规则写入 apollo_config 中,部分 apollo client 代码
...
local status, err = pcall(function ()
local res, error = httpc:request_uri(home_url, {
method = "GET",
query = queryString
})
if res.status == 200 then
notificationId = doProcessFirstTime(res.body)
ngx.log(ngx.INFO, "first time request success, return notificationId is "..notificationId)
end
end)
if not status then
ngx.log(ngx.ERR, "first time request is failed ... ")
else
-- rolling
while true do
local ok, errors = pcall(function ()
-- sleep 5s
ngx.sleep(5)
local query = "cluster="..cluster.."&appId="..appId.."¬ifications=".."%5B%7B%22namespaceName%22%3A%22"..namespaceName.."%22%2C%22notificationId%22%3A"..notificationId.."%7D%5D"
ngx.log(ngx.INFO, "long pulling request, send notificationId is "..notificationId)
local resp, err = httpc:request_uri(home_url, {
method = "GET",
query = query
})
if err == nil then
notificationId = doProcessLongPulling(httpc, configUrl, apollo, resp.body, resp.status, notificationId, fullName)
ngx.log(ngx.INFO, "notificationId is "..notificationId)
end
end)
if not ok then
ngx.log(ngx.ERR, "http long pulling is error "..errors)
end
end
end
...
规则同步到机器后在 nginx 具体的业务 location 上通过 try_files 查找读取规则 lua 脚本
location 配置案例
通过 @lua location 中流量仲裁脚本,返回执行版本
listen 80;
...
location @lua {
content_by_lua_file /path/fe-gray.lua;
try_files /index.html =404;
}
location @stable {
root $stable_root;
try_files $uri $uri/ /index.html =404;
}
location @gray {
root $gray_root;
try_files $uri $uri/ /index.html =404;
}
...
fe-gray.lua 包含,流量仲裁日志写入以及异常兜底 stable 稳定版本逻辑
...
-- 执行灰度版本
local exec_gray = function()
if type(ua_string) ~= 'string' then ua_string = '-' end
log("APPID=" .. tostring(appid) .. "$$MSG=" .. INFO_MSG.EXEC_GRAY ..
"$$GRAY_VERSION=" .. gray_version .. "$$IP=" .. ip .. "$$UA=" ..
ua_string, "info")
ngx.header["Exec-Version"] = "gray"
ngx.header["g-v"] = gray_version
return ngx.exec(NGX_LOCATION.GRAY)
end
-- 执行稳定兜底版本
local exec_stable = function()
if type(ua_string) ~= 'string' then ua_string = '-' end
log("APPID=" .. tostring(appid) .. "$$MSG=" .. INFO_MSG.EXEC_STABLE ..
"$$STABLE_VERSION=" .. stable_version .. "$$IP=" .. ip .. "$$UA=" ..
ua_string, "info")
ngx.header["Exec-Version"] = "stable"
ngx.header["s-v"] = stable_version
return ngx.exec(NGX_LOCATION.STABLE)
end
local success, msg = pcall(function()
...
local rule_info = apollo_rule()
local rule_info_table = cjson.decode(rule_info)
local gray_status = rule_info_table["gray_status"]
local rules = rule_info_table["rules"]
if gray_status ~= nil and gray_status == 1 then
log("APPID=" .. tostring(appid) .. "$$MSG=" .. INFO_MSG.STOP_GRAY, "info")
return exec_stable()
end
if gray_status ~= nil and gray_status == 2 then
log("APPID=" .. tostring(appid) .. "$$MSG=" .. INFO_MSG.FULL_GRAY, "info")
return exec_gray()
end
if rules then
if rules[1] == nil then
log("APPID=" .. tostring(appid) .. "$$MSG=" .. INFO_MSG.STOP_GRAY,
"info")
return exec_stable()
end
rule_exec(rules)
else
log(INFO_MSG.APPID_ERROR)
return exec_stable()
end
end)
if not success then
log(msg, "err")
return exec_stable()
end
...
为了更好的在用户端发现代码执行对应的版本和发布版本等相关的数据,在响应头中添加了 exec-version 字段,通过 stable gray 辨识页面版本,以及预留的 s-v g-v 关联发布的版本数据。
上面还有提到很关键的一个地方,灰度过程业务方非常关注过程中灰度流量的命中数据,因此在上面的代码中可以看到很多日志相关的数据,这些数据就是用来提取分析流量数据的,打入日志的数据滚动到文件,通过 filebeat 采集到 kafka 消费到 ES 兜底日志数据,同时通过 Node 进行消费清洗落库,提供前端可以查询的数据:
整体的支撑情况如下:
实时流量数据:
流量地区分布:
实时生产灰度百分比数据: