+86 13541016684Mon. - Fri. 10:00-22:00

基于动态策略的灰度发布系统

基于动态策略的灰度发布系统

调研目的:

1、结合公司实际情况,公司产品能尽早接入灰度控制系统。

2、新产品及早获得用户的意见反馈,提升产品质量。

3、新产品未知问题尽早发现,减少所影响的用户范围。

实现一套灰度发布系统需要的步骤

1、定义目标

用于区分用户,辅助数据统计,保证灰度发布过程中用户体验的连贯性(避免用户在新旧版本中跳变)。匿名Web应用可采用IP、Cookie等,需登录的应用可直接采用应用的帐号体系。

2、目标用户选取策略

即选取哪些用户先行体验新版本,是强制升级还是让用户自主选择等。可考虑的因素很多,包括但不限于地理位置、用户终端特性(如分辨率、性能)、用户自身特 点(性别、年龄、忠诚度等)。对于细微修改(如文案、少量控件位置调整)可直接强制升级,对于大型升级,应让用户自主选择,最好能够提供让用户自主回滚至 旧版本的渠道。对于客户端应用,可以考虑类似Chrome的多channel升级策略,让用户自主选择采用stable、beta、unstable channel的版本。在用户有明确预期的情况下自行承担试用风险。

总结起来选定策略:包括用户规模、发布频率、功能覆盖度、回滚策略、运营策略、新旧系统部署策略等。

3、部署系统

部署新系统、部署用户行为分析系统、设定分流规则、运营数据分析、分流规则调整。

4、发布总结

用户行为分析报告、形成产品功能改进列表。

5、产品完善。

通过分析和收集的信息,修复产品功能

6、新一轮灰度发布或完整发布。

针对上一版本的问题修正情况,继续新一轮产品发布

灰度发布系统对比

1、商用的通过集成sdk实现

例如:https://www.appadhoc.com

2、开源的需要结合自己公司情况搭建

https://github.com/SinaMSRE/ABTestingGateway

https://github.com/boylegu/regal

这里先以开源的为例结合公司情况分析这两套系统

首先任何一套灰度系统,灵活的控制策略是重点。

其次,接入现有系统的容易度。

通过查看文档,两者都有自己的算法策略,前者在接入度和性能上更胜一筹。

这里就以重点以ABTestingGateway为例来讲解这套系统的优缺点

ABTestingGateway 系统架构图

 

ABTestingGateway 是一个可以动态设置分流策略的灰度发布系统,工作在7层,基于nginx和ngx-lua开发,使用 redis 作为分流策略数据库,可以实现动态调度功能。 ABTestingGateway 是在 nginx 转发的框架内,在转向 upstream 前,根据 用户请求特征 和 系统的分流策略 ,查找出目标upstream,进而实现分流。 与以往的基于 nginx 实现的灰度系统中,分流逻辑往往通过 rewrite 阶段的 if 和 rewrite 指令等实现,优点是性能较高,缺点是功能受限、容易出错,以及转发规则固定,只能静态分流。针对这些缺点,我们设计实现了 ABTestingGateway,采用 ngx-lua 实现系统功能,通过启用lua-shared-dict和lua-resty-lock作为系统缓存和缓存锁,系统获得了较为接近原生nginx转发的性 能。

具备的功能:

  支持多种单一分流方式,目前包括iprange、uidrange、uid尾数和指定uid分流
  动态设置分流策略,即时生效,无需重启
  可扩展性,提供了开发框架,开发者可以灵活添加新的分流方式,实现二次开发
  高性能,压测数据接近原生nginx转发
  灰度系统配置写在nginx配置文件中,方便管理员配置
  适用于多种场景:灰度发布、AB测试和负载均衡等

不具备的功能:

  多策略同时生效还不支持
  策略编写使用lua语言

分流功能

转发分流是灰度系统的主要功能,目前 ABTestingGateway 支持 ip段分流(iprange)、uid用户段分流(uidrange)、uid尾数分流(uidsuffix) 和 指定特殊uid分流(uidappoint) 四种方式。

ABTestingGateway 依据系统中配置的 运行时信息runtimeInfo 进行分流工作;通过将 runtimeInfo 设置为不同的分流策略,实现运行时分流策略的动态更新,达到动态调度的目的。

分流过程图解

分流运行策略设置

 

系统管理员通过系统管理接口将分流策略policy设置为运行时策略,并指定该策略对应的 分流模块名divModulename 和 用户信息提取模块名userInfoModulename 后,系统可以进行分流工作。 系统对用户请求进行分流时,首先获得系统 运行时信息runtimeInfo 中的信息,然后提取 用户特征userInfo,最后 分流模块divModule 根据 分流策略dviDataKey 和 用户特征userInfo 查找出应该转发到的upstream。如果没有对应的upstream,则将该请求转向默认upstream。

以某个iprange分流策略为例:

      {
          "divtype":"iprange",
          "divdata":[
                      {"range":{"start":1111, "end":2222}, "upstream":"beta1"},
                      {"range":{"start":3333, "end":4444}, "upstream":"beta2"},
                      {"range":{"start":7777, "end":8888}, "upstream":"beta3"}
                    ]
      }

其中divdata中的每个 range:upstream 对中,range 为 ip 段,upstream 为 ip 段对应的后端;range 中的 start 和 end 分别为 ip 段的起始和终止, ip以整型表示。 当灰度系统启用iprange分流方式时,会根据用户请求的ip进行分流转发。 假如用户请求中的ip信息转为整型后是4000,将被转发至beta2 upstream。

管理功能

1、管理员登入后,得到系统信息视图,运行时信息视图,可以进行策略管理和运行时信息管理
2、业务接口层向管理员提供  增/删/查/改  接口
3、适配层将承担业务接口与分流模块的沟通工作
4、适配层提出统一接口,开发人员可以通过实现接口来添加新的分流方式

 

[策略管理接口]
  分流策略检查,参数为一个分流策略数据的json串
  1. /admin/policy/check
  分流策略添加,参数与check接口一致
  2. /admin/policy/set
  分流策略读取,参数为要读取策略的policyid
  3. /admin/policy/get
  分流策略删除,参数为要删除策略的policyid
  4. /admin/policy/del
[运行时信息管理接口]
  设置分流策略为运行时策略,参数为policyid
  1. /admin/runtime/set
  获取系统当前运行时信息,无参数
  2. /admin/runtime/get
  删除系统运行时信息,关闭分流接口,无参数
  3. /admin/runtime/del

系统组件

  tengine-2.1.0
  LuaJIT-2.1-20141128
  ngx_lua-0.9.13
  lua-cjson-2.1.0.2
  redis-2.8.19

系统部署

安转luajit 和 cjson模块

安装luajit
从luajit下载源码,make && make install 顺利安装
缺省路径安装在/usr/local/
export LUAJIT_LIB=/usr/local/lib
export LUAJIT_INC=/usr/local/include/luajit-

安装cjson
从cjson官网 下载源码
解压,编辑Makefile ,修改:
LUA_INCLUDE_DIR = $(PREFIX)/include/luajit-2.0
$ make
cc -c -O3 -Wall -pedantic -DNDEBUG  -I/usr/local/include/luajit-2.0 -fpic -o lua_cjson.o lua_cjson.c
cc -c -O3 -Wall -pedantic -DNDEBUG  -I/usr/local/include/luajit-2.0 -fpic -o strbuf.o strbuf.c
cc -c -O3 -Wall -pedantic -DNDEBUG  -I/usr/local/include/luajit-2.0 -fpic -o fpconv.o fpconv.c
$ sudo make install     
mkdir -p //usr/local/lib/lua/5.1
cp cjson.so //usr/local/lib/lua/5.1
chmod 755 //usr/local/lib/lua/5.1/cjson.so
成功安装后,写个测试代码:
local cjson = require "cjson"
local network = {
  {name = "web001",  IP = "10.10.10.1"},
  {name = "web002",  IP = "10.10.10.2"},
  {name = "web003",  IP = "10.10.10.3"},
  {name = "web004",  IP = "10.10.10.4"},
}
print(cjson.encode(network))
json_text = '[1, {"name":"test"},1111.1111111111,false]'
print(cjson.encode(cjson.decode(json_text)))
repo中的utils/conf文件夹中有灰度系统部署所需的最小示例 
1. git clone https://github.com/SinaMSRE/ABTestingGateway
2. cd /path/to/ABTestingGateway/utils
#启动redis数据库
3. redis-server conf/redis.conf 
#启动upstream server,其中stable为默认upstream
4. /usr/local/nginx/sbin/nginx -p `pwd` -c conf/stable.conf
5. /usr/local/nginx/sbin/nginx -p `pwd` -c conf/beta1.conf
6. /usr/local/nginx/sbin/nginx -p `pwd` -c conf/beta2.conf
7. /usr/local/nginx/sbin/nginx -p `pwd` -c conf/beta3.conf
8. /usr/local/nginx/sbin/nginx -p `pwd` -c conf/beta4.conf
#启动灰度系统,proxy server,灰度系统的配置也写在conf/nginx.conf中
9. /usr/local/nginx/sbin/nginx -p `pwd` -c conf/nginx.conf

灰度系统使用demo

管理功能

  1. 部署并启动系统
  2. 查询系统运行时信息,得到null
  0> curl 127.0.0.1:8030/admin/runtime/get
  {"errcode":200,"errinfo":"success ","data":{"divModulename":null,"divDataKey":null,"userInfoModulename":null}}
  3. 查询id为9的策略,得到null
  0> curl 127.0.0.1:8030/admin/policy/get?policyid=9
  {"errcode":200,"errinfo":"success ","data":{"divdata":null,"divtype":null}}
  4. 向系统添加策略,返回成功,并返回新添加策略的policyid
         以uidsuffix尾数分流方式为例,示例分流策略为:
              {
                  "divtype":"uidsuffix",
                  "divdata":[
                              {"suffix":"1", "upstream":"beta1"},
                              {"suffix":"3", "upstream":"beta2"},
                              {"suffix":"5", "upstream":"beta1"},
                              {"suffix":"0", "upstream":"beta3"}
                            ]
              }
  添加分流策略接口 /admin/policy/set 接受json化的policy数据
  0> curl 127.0.0.1:8030/admin/policy/set -d '{"divtype":"uidsuffix","divdata":[{"suffix":"1","upstream":"beta1"},{"suffix":"3","upstream":"beta2"},{"suffix":"5","upstream":"beta1"},{"suffix":"0","upstream":"beta3"}]}'
  {"errcode":200,"errinfo":"success  the id of new policy is 0"}
  5. 查看添加结果
  0> curl 127.0.0.1:8030/admin/policy/get?policyid=0
  {"errcode":200,"errinfo":"success ","data":{"divdata":["1","beta1","3","beta2","5","beta1","0","beta3"],"divtype":"uidsuffix"}}
  6. 设置系统运行时策略为 0号策略
  0> curl 127.0.0.1:8030/admin/runtime/set?policyid=0
  {"errcode":200,"errinfo":"success "}
  7. 查看系统运行时信息,得到结果
  0> curl 127.0.0.1:8030/admin/runtime/get
  {"errcode":200,"errinfo":"success ","data":{"divModulename":"abtesting.diversion.uidsuffix","divDataKey":"ab:test:policies:0:divdata","userInfoModulename":"abtesting.userinfo.uidParser"}}
  8. 当访问接口不正确返回时,将返回相应的 错误码 和 错误描述信息
  0> curl 127.0.0.1:8030/admin/policy/get?policyid=abc
  {"errcode":50104,"errinfo":"parameter type error for policyID should be a positive Integer"}

分流功能

  在验证管理功能通过,并设置系统运行时策略后,开始验证分流功能
  1. 分流,不带用户uid,转发至默认upstream
  0> curl 127.0.0.1:8030/
  this is stable server
  2. 分流,带uid为30,根据策略,转发至beta3
  0> curl 127.0.0.1:8030/  -H 'X-Uid:30'
  this is beta3 server
  3. 分流,带uid为33,根据策略,转发至beta2
  0> curl 127.0.0.1:8030/  -H 'X-Uid:33'
  this is beta2 server

压测结果

灰度系统在理想情况下可以达到十分接近原生nginx转发的性能。

产生图中压测结果的场景是:用户请求经过proxy server转向upstream server,访问1KB大小的静态文件。

线上部署简图

ABTestingGateway之添加新的分流方式

ABTestingGateway 的基于分流策略的动态更新来实现动态调度的。 当开发者需要结合自身需求添加新的分流方式时,首先需要为其指定分流策略divPolicy,然后开发分流模块divModule和响应的信息提取模块 uesrInfoModule。 下面我们以一个小例子来说明添加新分流方式的方法,我们的新需求是按照请求url的arg参数中的city字段分流。 为新的分流方式制定分流策略

ABTestingGateway的分流策略有固定格式:

{
"divtype":"arg_city",
"divdata":[
{"city":"BJ", "upstream":"beta1"},
{"city":"SH", "upstream":"beta2"},
{"city":"TJ", "upstream":"beta1"},
{"city":"CQ", "upstream":"beta3"}]
}

分流策略的divtype在下一步是分流模块名的关键部分。 分流策略的divdata是策略内容,由于是按照city字段分流,这种kv形式的策略,在数据库层面可以采用redis的hash实现,在缓存层可以采用ngx_lua的sharedDict实现。 开发分流模块divModule

ABTestingGateway的分流模块都在/lib/abtesting/diversion/文件夹中,其下的每个lua文件是一个分流模块,比 如iprange分流方式的分流模块就是lib/abtesting/diversion/iprange.lua,而我们的arg_city分流方式根 据divtype就是lib/abtesting/diversion/arg_city.lua。

分流模块主要有两个功能,一是分流策略的管理功能,包括检查策略合法、添加策略set、读取策略get;二是分流功能getUpstream,这个接口得到用户请求对应的upstream。

arg_city.lua是一个典型Lua Module实现:

local modulename = "abtestingDiversionArgCity"
local _M    = {}
local mt    = { __index = _M }
_M._VERSION = "0.0.1"
_M.new = function(self, database, policyLib)
  self.database = database
  self.policyLib = policyLib
  return setmetatable(self, mt)
end
_M.check = function(self, policy)
  ...
end
_M.set = function(self, policy)
  ...
end
_M.get = function(self)
  ...
end
_M.getUpstream = function(self, city)
  ...
end
return _M

1. 分流模块初始化方法

_M.new = function(self, database, policyLib)
  if not database then
      error{ERRORINFO.PARAMETER_NONE, 'need avaliable redis db'}
  end if not policyLib then
      error{ERRORINFO.PARAMETER_NONE, 'need avaliable policy lib'}
  end
  self.database = database
  self.policyLib = policyLib
  return setmetatable(self, mt)
end

在分流模块初始化方法中,database是策略数据库,目前是redis;policyLib是分流策略在数据库中的key。 而error{ERRORINFO.PARAMETER_NONE, ‘need avaliable redis db’}是ABTestingGateway设计的基于xpcall的防御性机制,用于处理捕获异常。ERRORINFO作为系统的错误码编号,具体内容 在/lib/abtesting/error/errcode.lua中

2.策略检查 check方法

主要功能是对用户输入的策略进行合法性检查

_M.check = function(self, policy)
  for _, v in pairs(policy) do
      local city      = v[k_city]
      local upstream  = v[k_upstream]
      if not city or not upstream then
          local info = ERRORINFO.POLICY_INVALID_ERROR 
          local desc = ' need '..k_city..' and '..k_upstream
          return {false, info, desc}
      end
  end
  return {true}
end
3.策略添加 set方法

向系统中添加用户策略,这里的策略policy是经过check后的。

_M.set = function(self, policy)
  local database  = self.database 
  local policyLib = self.policyLib
  database:init_pipeline()
  for _, v in pairs(policy) do
      database:hset(policyLib, v[k_city], v[k_upstream])
  end
  local ok, err = database:commit_pipeline()
  if not ok then 
      error{ERRORINFO.REDIS_ERROR, err} 
  end
end

arg_city的分流策略在redis中采用hash结构存储。

4.策略读取 get方法

从数据库中读取用户策略的数据

_M.get = function(self)
  local database  = self.database 
  local policyLib = self.policyLib
  local data, err = database:hgetall(policyLib)
  if not data then 
      error{ERRORINFO.REDIS_ERROR, err} 
  end
  return data
end

目前只是将策略数据从redis中读出,然后以json形式发送给client,至于如何解析json字符串为策略数据,可以在系统的/admin/policy/get接口实现,也可以在client中实现。目前ABTestingGateway没有实现。

5.获取用户请求对应的upstream

从数据库中读取用户策略的数据

_M.getUpstream = function(self, city)    
  local database  = self.database
  local policyLib = self.policyLib
  local upstream, err = database:hget(policyLib , city)
  if not upstream then error{ERRORINFO.REDIS_ERROR, err} end
  if upstream == ngx.null then
      return nil
  else
      return upstream
  end
end

分流模块获取upstream的方法,策略key为policylib,用户请求特征为city。getUpstream得到结果后返回,系统分流接口将请求转发至目标upstream。 分流方式对应的 用户特征提取模块 在getUpstream方法中,分流模块根据用户请求中的city来计算upstream,这个city相当于用户请求特征。每种分流方式需要指定用户特征提取模块,由它提取用户请求的特征。 分流策略中的divtype将用来指定用户特征提取模块。ABTestingGateway的所有用户特征提取模块都在lib/abtesting/userinfo/文件夹,其下的每个lua文件是一个分流模块。

在lib/abtesting/utils/init.lua中

_M.divtypes = {
  ["iprange"]     = 'ipParser',  
  ["uidrange"]    = 'uidParser',
  ["uidsuffix"]   = 'uidParser',
  ["uidappoint"]  = 'uidParser',
  ["arg_city"]    = 'cityParser'
}

每种divtype会有对应的提取模块,因此divtype为arg_city的分流方式对应的用户信息提取模块就是lib/abtesting/userinfo/cityParser.lua。

local _M = {
  _VERSION = '0.01'
}
_M.get = function()
  local u = ngx.var.arg_city
  ngx.log(ngx.ERR, u)
  return u
end
return _M

以上就是向系统添加新的分流方式的具体步骤。

结合公司情况针对这套系统需要解决的问题

1、如何与现有环境结合进行系统部署

解决项目并行运行的两种方式:

在实际开发中经常涉及到项目的升级,而该升级不能简单的上线就完事了,需要验证该升级是否兼容老的上线,因此可能需要并行运行两个项目一段时间进行数据比 对和校验,待没问题后再进行上线。这其实就需要进行流量复制,把流量复制到其他服务器上,一种方式是使用如tcpcopy引流;另外我们还可以使用 nginx的HttpLuaModule模块中的ngx.location.capture_multi进行并发执行来模拟复制。

流量复制

方式是使用如tcpcopy引流;

tcpcopy是一种应用请求复制(基于tcp的packets)工具,其应用领域较广,目前已经应用于国内各大互联网公司。

总体说来,tcpcopy主要有如下功能:
1)分布式压力测试工具,利用在线数据,可以测试系统能够承受的压力大小(远比ab压力测试工具真实地多),也可以提前发现一些bug
2)普通上线测试,可以发现新系统是否稳定,提前发现上线过程中会出现的诸多问题,让开发者有信心上线
3)对比试验,同样请求,针对不同或不同版本程序,可以做性能对比等试验
4)利用多种手段,构造无限在线压力,满足中小网站压力测试要求
5)实战演习(架构师必备)

tcpcopy可以用于实时和离线回放领域,并且tcpcopy支持mysql协议的复制,开源二年以来,功能上越来越完善。 如果你对上线没有信心,如果你的单元测试不够充分,如果你对新系统不够有把握,如果你对未来的请求压力无法预测,tcpcopy可以帮助你解决上述难题。

使用nginx的HttpLuaModule模块中的ngx.location.capture_multi进行并发执行来模拟复制。

2、分流策略的定制测试

3、管理系统的强化