曹进(jay)
背景
在业系统的迭代过程中,系统的测试用例的覆盖率依赖于测试人员对系统的熟悉程度的,即使是全部做白盒测试,也比较难保证100%的覆盖率,同时测试用例本身的正确性保证也是一个问题。基于这个前提,系统的发布就是有一定的概率会对线上用户的正常使用造成影响,引发客诉的。既然线上发布带来的问题,没办法100%避免,那缩小发布的影响范围就非常必要。
灰度发布就是一种缩小问题影响范围的常规手段。所谓灰度发布,就是使用技术手段,让线上发布的功能仅对线上部分用户可见,这样新发布仅会影响这一部分用户,不会影响其他用户。
我们是怎么做的
常见的灰度发布方案
灰度发布常见的就是两种方式:
A. 服务接入。应用系统通过硬编码的方式接入灰度接口根据接口的返回值做相关逻辑的判断来决定是否走新的代码逻辑
B. 物理部署层。使用一个或几个单独服务节点部署新的代码逻辑,让一部分用户的请求路由这些节点上
使用单独的灰度节点来部署新的代码,端上的请求根据一定的规则从api网关、nginx服务器等入口开始将整个链路都路由到下游所有应用的灰度节点上。这样就从部署层面解决了灰度流量和正常流量在整个链路的串联和隔离问题。如下图所示:对于从API尽量的灰度流量路由到应用mall的红色灰度节点,同时对于mall的下游应用icp也是路由到红色的灰度节点。
在当前微服务架构的大背景下,这两种方式都要解决灰度流量和正常流量在整个链路的串联和隔离问题。
方式 | 优势 | 劣势 |
---|---|---|
服务接入 | 1、部署成本更低,不需要太多的运维成本。 2、复杂性较低,实现成本较低,后期维护较容易。 |
1、对业务系统有代码侵入性,发布完成后还需要清理灰度逻辑代码。
2、灰度判断代码的正确性没办法完全保证。 |
物理部署层 | 1、对业务系统无代码侵入性,使用灰度发布无感知。
2、能以较低成本的支持整个链路的完整性。 3、灰度流量和正常流量隔离得比较彻底,能较大程度上做到正常流量和灰度流量相互影响的问题。 |
1、实现成本较高,需要在基础设施层对灰度流量标识进行全链路标识。因为微服务框架采用的是dubbo,为实现全链路的流量标识,需要推进整个技术部所有的应用升级dubbo。
2、部署的成本较高,每个参与灰度发布的应用都需要单独的节点部署,需要有独立的发布系统支持。 |
A方案虽然实现和维护成本低,但因为后期项目发布上线后,都要手动清理灰度代码,为此还要走一遍研发流程,在整个公司都要求快速迭代的背景是是不可接受的,因此我们选择了B方案。
具体实现
上面已经提到,我们这边基本上所有的系统都已接api网关,前端部署是分离的。这种部署方式首先要解决的问题就是前端页面和后端请求的串联,就是说如果一次发布中页面使用了新接口,不仅要保证这个新的页面只有部分用户才能看到,同时新的接口也应该要保证只有同一部分用户才能访问到。
这里其实就涉及到两个层面的流量灰度,第一:前端页面的灰度。要保证前端新页面也是只有部分用户能看到。第二:后端接口的灰度。因为是服务化架构,灰度流量和正常流量在整个请求链路上都是隔离的。
前端灰度发布
因为我们的前端资源文件是部署到阿里云的oss存储上的,要做到正常和灰度用户能访问到不同的资源文件,就需要在使用不同的目录来存储灰度资源和线上资源。(图上的release存放线上资源,beta存放灰度资源),每次灰度发布更新资源到beta目录,beta发布完成,进行线上正式发布时,再将资源更新到release目录。
那前端资源请求是怎么转发到不同的目录呢?我们使用了nignx lua对nginx的请求转发做了扩展,在扩展里会判断当前用户是否是灰度用户,如果是灰度用户则会将当前用户的请求转发到beta目录,否则转发到线上目录。
灰度逻辑的判断使用到了灰度规则,灰度规则的配置数据是lua里请求灰度发布系统gatekeeper的接口获取的。lua每隔1s中获取一次,获取到规则数据后缓存到nginx的worker本地内存中,整体的架构设计图如下:
规则的初始化是在nginx 的init_worker_by_lua_file中做的,这样多个woker拉取到的规则数据的时间点不一样,可能会导致多个work进程中的规则缓存数据不一样,这样同个用户的请求同个页面的多次请求,可能一部分是请求到灰度页面,一部分是请求到线上页面,这种情况是不能接受的。为了保证一个woker对自己本地缓存的修改,能让其他woker可见,我们使用了nginx的共享内存,即在nginx配置文件中添加如下配置:
lua_shared_dict betaRuleCache 1m;
这块betaRuleCache这块内存区域对多个woker是共享的,这样一个worker对缓存的变更,能保证其他woker也能读到最新的数据。worker可以采用下面的方式来写入数据
ngx.shared.betaRuleCache:set("key","value")
获取数据
ngx.shared.betaRuleCache:get("key")
那具体lua扩展是怎么去做规则计算和请求转发的逻辑是怎么样的呢?从下面的代码可以看到会从当前请求中获取到userId,然后判断该userId是否在灰度名单中,如果在名单中,则会将hipac_env设置为beta。
function setBetaHeader(tbl)
if tbl==nil then
log(ERR,"ruleConfig cache is empty or wrong!")
return
end
for k,v in pairs(tbl) do
ngx.header["app-config"]='{"beta":"'..k..'"}'
break
end
end
function dispatchReq()
local userId = getUserIdFromReq(ngx.var.host)
if userId == nil then
log(notice,"can not get userId.host:"..ngx.var.host)
return
end
log(notice,"++++++++++userId:"..userId)
local betaRuleCacheTable = cjson.decode(ngx.shared.betaRuleCache:get(betaRuleConfigLocalCacheKey))
local isInWhiteList = isInWhiteBlakcList(userId,betaRuleCacheTable)
--走beta逻辑
if isInWhiteList and not string.find(ngx.var.uri,'beta',1) then
ngx.var.hipac_env = "beta"
setBetaHeader(betaRuleCacheTable)
end
return beta
end
dispatchReq()
然后nginx通过下面的alias指令转发到beta目录,下面的配置虽然是nginx server的本地目录,但因为和阿里云oss目录做了映射,所以也是能访问到最新资源文件的。
location /crm/ {
include common/header.item;
alias /XXXXXXXXXXXXXXXXXXXXXXXXXX/$hipac_env/;
try_files index.html =404;
}
后端灰度发布
因为后端是微服务架构,后端灰度发布要解决的主要问题就是灰度流量标识和全链路的串联。我们当前的服务架构采用的框架是dubbo,所有一个关键问题是dubbo要支持请求流量的标记和全链路的透传。通过调研知道,apache dubbo 2.7.4.1之后的版本支持标签路由,正好能解决流量标签的全链路透传和灰度节点路由问题。
从下面的架构图可以看出,所有的接口请求从HOP(API网关)进来,然后HOP通过dubbo 泛化调用再转发到对应的后端服务。在HOP层缓存当前的灰度规则,请求过来时,会根据当前用户信息和灰度规则,判断dubbo 泛化调用时是否透传灰度标签。灰度规则匹配的请求会在dubbo调用时带上标签,否则,不带灰度标签。对于带标签的请求,dubbo在路由时会判断如果存在打标签的provider,则路由到打标签的provider。否则,路由到没有标签的provider,这样灰度用户请求就会路由到灰度节点上了。对于不带标签的请求,dubbo只会路由到未打标签的provider,这是为了保证线上流量不会路由到灰度节点,避免对线上真实的用户产生影响。
那灰度标签是什么时候打上去的呢?灰度标签是在应用灰度发布成功后,通过发布系统打上去的。这样就能保证打上标签的节点的代码一定是最新的。
关于灰度标签的全链路透传还有个问题。如果dubbo调用是在异步线程中去做的,dubbo就没办法将标签传递过去了。为了解决标签的异步传递问题,我们引入了阿里开源的线程上下文传递工具transmittable-thread-local,它的底层是通过agent来支持标签的线程池等异步场景下的标签传递问题。
未来展望
目前我们已经实现了微服务层的全链路灰度发布,同时跟我们自研的统一研发平台做了集成,整体用户体验也比较流畅。但灰度发布其实还有很多可以挖掘的东西,比如可观测性,让用户能直观感受到灰度和线上流量的走向;消息链路的隔离,因为我们目前使用的是RocketMQ,社区目前这块也还是空白,需要我们去进一步调研;更丰富的流量规则,支持线上真实用户的灰度等。