文章目录

    • 0、分布式系统灰度要实现的功能清单
    • 1、携带灰度因子
      • 1.1、Http请求中
      • 1.2、JVM中
      • 1.3、调用链中
    • 2、前端资源灰度
      • 2.1、方案一:基于verynginx
      • 2.2、方案二:基于istio Envoy
    • 3、Rest API请求灰度
    • 4、分布式RPC(Dubbo)调用灰度
      • 4.1、概览
      • 4.1、实现原理拆解
      • 4.2、Dubbo URL
      • 4.3、Zookeeper与Dubbo
      • 4.4、开发点-1:Provider支持自动注册为Grey版本的Dubbo Service
      • 4.5、开发点-2:Consumer支持自动记录Grey版本的Dubbo Service
      • 4.6、开发点-3:Dubbo应用本地记录灰度生效规则
      • 4.7、开发点-4:Consumer支持动态生效灰度规则配置
    • 5、MQ生产/消费灰度
      • 5.1、区分Topic的方式
      • 5.2、不区分Topic的方式
    • 6、数据库灰度
    • 7、灰度管理控制台

0、分布式系统灰度要实现的功能清单

1.1、灰度因子可全链路传递

  • 后端/前端往cookie里携带灰度因子

  • 浏览器端发起时自动携带

  • 后端分布式节点间自动透传

1.2、前端资源灰度

  • 非灰度/灰度前端资源目录区分存放
  • 前端请求灰度路由

1.3、Rest API请求灰度

  • 非灰度/灰度web并存运行
  • 前端请求灰度路由

1.4、分布式RPC(Dubbo)调用灰度

隔离性

  • 灰度应用的Dubbo服务API,不被环境里其他非灰度应用所调用
  • 灰度应用的Dubbo服务API,可被环境里上游灰度应用所调用
  • 灰度应用调用下游其他Dubbo服务APIs时,按灰度规则进行非灰度/灰度的调用

1.5、MQ生产/消费灰度

隔离性

  • 灰度应用产生的MQ,只被下游灰度应用消费
  • 非灰度应用产生的MQ,只被下游非灰度应用消费

1.6、数据库灰度

一致性

  • 灰度应用对应DB数据的相对(非灰度应用)一致性。
  • [按需]灰度关闭时的(相对非灰度DB的)数据持久性
  • TBD

1、携带灰度因子

1.1、Http请求中

携带位置:

  • Cookie

一般是以下几种灰度因子可参考:

  • Client IP:Http请求自动携带
  • 业务编码:
    • 【方式一】向登录模块提需求,在登录时往cookie里携带你需要的灰度因子(笔者的场景:租户Code)
    • 【方式二】在前置版本中,让前端在发起请求时,将业务编码塞在cookie中。针对前台可匿名访问的页面。
  • (按流量比例:2C会用。2B因参与角色的多样性,不适用单按流量比例)

1.2、JVM中

  • 在JVM启动参数上带上自定义标识:useGreyMode

    • java -DuseGreyMode=1

  • 在运行期代码中,读取此标识是否为True

    • public Boolean isGreyMode(){String useGreyMode = System.getProperty("useGreyMode", "0");if("1".equals(useGreyMode)){//灰度模式return Boolean.TRUE;}return Boolean.FALSE;
      }
      

1.3、调用链中

  • 在Web层,从Http报文中解析得到灰度因子,例如:租户Code、IP
  • 将租户Code、IP放入ThreadLocal进行调用链上的透传
  • 在RPC层面,自动将ThreadLocal中的Code、IP放入RPC报文的指定位置,并由RPC的接收方进行解析,同样再次放入下游调用链的ThreadLocal中。
  • 注意点:ThreadLocal使用TransmittableThreadLocal(https://github.com/alibaba/transmittable-thread-local)
    • 为什么?在RPC和框架层面往往会有线程池模型,而常规的ThreadLocal在跨线程时就会值丢失。目前市面上只有TransmittableThreadLocal是支持线程池级别的ThreadLocal传递的。

2、前端资源灰度

要点:

  • 非灰度/灰度前端资源目录区分存放
  • 前端请求按灰度规则路由
  • 灰度规则可动态下发生效

2.1、方案一:基于verynginx

为什么verynginx?

  • 【为什么多一层】在nginx之下,再加一个本业务专用的verynginx,与其他业务的转发规则分离,可将转发规则的影响控制到本业务。
  • 【为什么选verynginx】verynginx 内部是基于OpenResty,而OpenResty = nginx + lua 的框架。即nginx能做的转发和静态资源路由,verynginx都支持。而且,verynginx对配置更友好:界面化、json化、支持被动(不用轮询去查)变更生效,整体上对于灰度控制的场景使用更简单。
  • https://github.com/alexazhou/VeryNginx

verynginx的配置动态生效方式:

分布式系统灰度发布实践-编程知识网

config.json配置示例:

分布式系统灰度发布实践-编程知识网

  • 注:这里会遇到一个开发点
    • 自研的灰度管理控制台中的“灰度规则”,是一种数据配置结构。
    • verynginx中config.json,又是一种数据配置结构。
    • 最终,是要将灰度管理控制台的具体规则,体现到verynginx中的配置文件中去。
    • 在这里,有通过python实现一个转换写入工具。

具体实操步骤:

1、【前端资源目录-灰度发布】联系运维,在发布系统中,增加一个前端资源的灰度发布选项。执行灰度发布时,本次构建的前端资源目录不覆盖线上原有verynginx机器上前端资源目录,而是存放在“隔壁”的灰度前端资源目录中。

2、【verynginx配置转换&同步工具】熟悉灰度管理控制台配置数据结构和verynginx配置数据结构,实现一个转换、写入工具(例如:python写一个)。

  • python程序生成verynginx用的的config.json转发配置文件后,覆盖原config.json即可。配置立马会生效。免去了verynginx主动轮询。

2.2、方案二:基于istio Envoy

  • TBD

3、Rest API请求灰度

即Web层的灰度

(跟前端资源灰度路由逻辑一样,差异在于matcher匹配后的转发)

在前端资源灰度路由的基础上,增量开发【verynginx配置转换&同步工具】,实现灰度管理控制台Rest API URL级的灰度配置转换和同步至verynginx。

4、分布式RPC(Dubbo)调用灰度

4.1、概览

服务端分布式多实例环境中,灰度的关键点:灰度和非灰度应用之间的调用隔离

  • 灰度应用的Dubbo服务API,不被环境里其他非灰度应用所调用
  • 灰度应用的Dubbo服务API,可被环境里上游灰度应用所调用
  • 灰度应用调用下游其他Dubbo服务APIs时,按灰度规则进行非灰度/灰度的调用

分布式系统灰度发布实践-编程知识网

4.1、实现原理拆解

1、Dubbo分为Consumer和Provider,分别对应Dubbo内部的Invoker和Exporter对象。一个Dubbo服务,可同时具备Consumer和Provider。

2、想要:灰度应用的Dubbo服务API,被环境里其他非灰度/灰度应用有选择的调用,要实现几个点:

  • 灰度应用暴露、注册Dubbo Provider服务时,需要带有灰度标记,供调用方识别。
  • 其他关注、订阅此Dubbo Provider的应用,需能同时订阅感知到此App的非灰度版本的Dubbo Provider和灰度版本的Dubbo Provider。并维护记录在本地Invoker List中。
  • 当非灰度应用和灰度应用做向外接口调用时,按对应Dubbo Service API是否被灰度,来选择正常的Invoker还是选择灰度的Invoker。

3、向外调用其他Dubbo API的灰度控制粒度是Dubbo Service级别(一个Dubbo Service是一个Interface定义)。

4.2、Dubbo URL

Dubbo通过Dubbo URL,在分布式系统里完成Dubbo服务信息暴露和订阅。

Dubbo通过URL中要素的匹配,完成关心的Dubbo Service的命中和维护。

  • Dubbo服务注册:
dubbo://10.31.21.75:58090/com.example.service.item.ItemStockServiceFacade?anyhost=true&application=
item-service&dubbo=2.5.3-RC1&heartbeat=10000&interface=com.example.service.item.facade.
ItemStockServiceFacade&logger=slf4j&methods=xxx,xxx,xxx&pid=1&retries=0&revision=1.0.0&
side=provider&threads=500&timestamp=1611044123684&version=1.0.0
  • Dubbo服务订阅:
consumer://10.31.24.15/com.example.service.item.facade.ItemStockServiceFacade?application=
order-center&category=consumers&check=false&default.check=false&default.retries=0&dubbo=2.5.3-RC1&
interface=com.example.service.item.facade.ItemStockServiceFacade&methods=xxx,xxx,xxx&pid=1&
revision=1.0.0&side=consumer&timeout=10000&timestamp=1610941900528&version=1.0.0

4.3、Zookeeper与Dubbo

上述Dubbo URL的信息,会在Zookeeper以ZNode节点树的形式存储。

并辅以Zookeeper的临时ZNode完成服务下线处理。辅以Watch机制完成服务变更的通知、处理。

分布式系统灰度发布实践-编程知识网

4.4、开发点-1:Provider支持自动注册为Grey版本的Dubbo Service

4.3.1、Dubbo的SPI点:Configurator

Dubbo Configurator SPI扩展点:

  • 支持以扩展实现点的方式,对Dubbo Provider以Dubbo Service URL协议向注册中心前,对Dubbo Service URL进行干预:编辑/改写。

  • Dubbo源码调用路径:export ——> doExport ——> doExportUrls ——> doExportUrlsFor1Protocol ——> ExtensionLoader.getExtensionLoader(ConfiguratorFactory.class).getExtension(url.getProtocol()).getConfigurator(url).configure(url);

4.3.2、注册为Grey版本的Dubbo Service的代码示例:

  • 实现一个Dubbo ConfiguratorFactory
public class GreyConfiguratorFactory implements ConfiguratorFactory {@Overridepublic Configurator getConfigurator(URL url) {return new GreyConfigurator();}
}

(SPI的机制)并在resouces/META-INF/dubbo/internal/下,新建一个com.alibaba.dubbo.rpc.cluster.ConfiguratorFactory的文件,文件内容为

dubbo=com.frame.sdk.grey.config.GreyConfiguratorFactory

  • 实现GreyConfigurator
public class GreyConfigurator implements Configurator {@Overridepublic URL getUrl() {return null;}@Overridepublic URL configure(URL url) {String greyMode = System.getProperty("useGreyMode", "0");//开启灰度if("1".equals(greyMode)){url = doGrey(url);}return url;}private URL doGrey(URL url){//非提供者不更改版本号if(!"provider".equals(url.getParameter(Constants.SIDE_KEY))){return url;}url = url.addParameter(Constants.VERSION_KEY, "grey_1.0.0");return url;}@Overridepublic int compareTo(Configurator o) {return 0;}
}

4.5、开发点-2:Consumer支持自动记录Grey版本的Dubbo Service

改写思路:

  • 根据Dubbo URL在Zookeeper里的存储方式,Dubbo verson被存储在ZNode树的叶子节点的data上。
  • 故,在某个Dubbo Service的ZNode树中,增加/移除某个灰度Dubbo版本的URL的叶子节点,仍会触发ZNode变更事件,最终对应订阅了此Dubbo Service的所有Dubbo应用都会收到一个zookeeper watch事件,去处理分布式环境下dubbo service的变化。
  • 那么,从dubbo consumer的Zookeeper watch事件的处理回调开始分析。

分布式系统灰度发布实践-编程知识网

那,那怎么办呢?———— 自己实现一个替换掉

先锁定我们需要加逻辑的代码位置:

ZookeeperRegistryFactory ——> ZookeeperRegistry ( doSubscribe ——> listener ——> childChanged ——> ZookeeperRegistry.this.notify(…) ) ——> FailbackRegistry#notify ——> AbstractRegistry#notify ——> UrlUtils.isMatch

分布式系统灰度发布实践-编程知识网

分布式系统灰度发布实践-编程知识网

分布式系统灰度发布实践-编程知识网

其实,要改的地方就是这么一处。但由于没有扩展点,故,一路上所有的代码均需要从dubbo源码里拷贝出来,然后只针对这个isMatch方法,扩展一下识别灰度版本provider的逻辑。

4.6、开发点-3:Dubbo应用本地记录灰度生效规则

应用记录有关dubbo service的灰度规则,主要是用于灰度dubbo服务的上下游调用串联。

一般由consumer端记录,然后在做具体下游dubbo服务负载均衡调用时,按灰度规则路由到对应dubbo provider。

具体,与下面#4.7章节一起讲述

4.7、开发点-4:Consumer支持动态生效灰度规则配置

有了第二步,支持本地记录"grey_1.0.0"版本的dubbo service后,那么剩下的工作就是在dubbo invoke调用的时候,能按灰度生效情况进行区别的调用grey_1.0.0版本的dubbo provider和非grey版本的dubbo provider了。

先阅读下dubbo做API调用时的源码实现:

分布式系统灰度发布实践-编程知识网

分布式系统灰度发布实践-编程知识网

如果不熟悉源码实现思路,也可以debug断点跟踪一下调用链路,找到发起调用的点。

那代码怎么写呢?

  • dubbo的LoadBalance是支持SPI扩展的,那么就自己实现带灰度的GreyLoadBalance,并使其生效。(记得在resouces下增加SPI声明,否则没效果哦)
  • 在GreyLoadBalance里,在每次调用时,去读取#4.6章节记录在应用本地的灰度规则进行匹配。最终在重载的doSelect方法里,根据灰度策略返回灰度的invoker或非灰度的invoker。

代码实现:

分布式系统灰度发布实践-编程知识网

分布式系统灰度发布实践-编程知识网

改写的部分,主要是多做了一次灰度和非灰度Invokers的分离,并按灰度规则择其一,传入getInvoker方法进行具体某一个invoker的选择,而getInvoker内的实现可以完全拷贝dubbo源码的实现:Random、RoundRobin等。

5、MQ生产/消费灰度

隔离性

  • 灰度应用产生的MQ,只被下游灰度应用消费
  • 非灰度应用产生的MQ,只被下游非灰度应用消费

(具体使用时,需要分析下是否涉及MQ业务,若MQ业务未变化,可以不处理MQ的灰度,MQ方面视为正常应用就行)

以RocketMQ为例

讨论之下,有两种思路:

1、区分Topic:灰度应用动态自适应的用“灰度Topic”发消息。且自适应的订阅“灰度Topic”的消息。

  • 【推荐】隔离性好。

2、不区分Topic:灰度和非灰度应用保持同一个Topic不变。在consumer端根据上下文以及灰度因子有选择性的消费MQ。此处,灰度应用的consumer需以“广播消费”的模式订阅消息,使得能收到此topic下所有的消息,才能有选择的消费。

  • 次选方案。且,灰度和非灰度的应用都要接入MQ消费的SDK,才能对MQ的消费进行灰度策略的控制。

》》 两种方案有共性的难点,需要对MQ的“订阅、生产、消费”进行额外的逻辑包装。如果团队技术建设中,本身就对MQ有做一次接口抽象包装,那么改造和替换起来相对顺利些。否则成本会非常大,或者用难度高的动态字节码织入的方式。

5.1、区分Topic的方式

RocketMQ知识点铺垫:

1、RocketMQ的架构图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CZ5lY1xY-1611387861864)(分布式系统灰度实践.assets/image-20210123143344027.png)]

  • 【Name Server】Name Server是互相独立的,每个Name Server含有全量的信息(例如Topic以及Topic对应的broker)
  • 【broker】与所有Name Server建立长连接
  • 【producer】与某个Name Server建立连接,查询topic所在broker,并与broker master建立连接
  • 【consumer】与某个Name Server建立连接,查询topic所在broker,与broker master 和broker slave 分别建立连接。broker master会根据情况建议consumer从哪里拉消息

2、RocketMQ的生产/消费代码样例:

分布式系统灰度发布实践-编程知识网

分布式系统灰度发布实践-编程知识网

故,做一下实现分析:

  • 【消息生产端】

    • 切面/代理拦截MQ SDK中的各种SendXXXMessage的方法(推荐字节码织入)
    • 判断本应用是否是灰度模式,若是,进入下面逻辑
    • 替换入参msg中Topic为灰度Topic,就可以了。MQ内部实现会做好TopicPublishInfo的获取。
  • 【消息消费端】

    • 同理,切面/代理拦截MQ SDK中DefaultMQPushConsumer#subscribe(String topic, … )方法(推荐字节码织入),改写Topic。
  • 【MQ链路传递灰度因子】

    • 工具类:灰度因子ThreadLocal To MQ、MQ To ThreadLocal的工具类,供切面代码调用。
    • 对于生产端是sendXXXMessage系列接口
    • 对于消费端是consumeMessage系列接口

5.2、不区分Topic的方式

笔者团队是有自己的MQ抽象包装层的,用于适配多环境下的多种MQ选型。故,实际代码层面仅贴有参考性的部分。讲思路为主。

开发改造点:

1、【必要条件】MQ中需有能传递灰度因子的“位置”,RocketMQ中是Message基础机构就有properties这个Map可以扩展存放自定义信息。

2、【工具类】灰度因子ThreadLocal To MQ、MQ To ThreadLocal的工具类

3、【灰度消费端】consumer实例初始化时,识别灰度状态,设置消费模式为广播模式:consumer.setMessageModel(MessageModel.BROADCASTING);

4、【灰度消费端】consumer实例的MessageListener回调,在判断消费接收到的消息判断方法中,增加灰度规则匹配的逻辑。

相关代码:

分布式系统灰度发布实践-编程知识网

分布式系统灰度发布实践-编程知识网

分布式系统灰度发布实践-编程知识网

6、数据库灰度

一般灰度会侧重在“前端和业务逻辑”的多版本并行看效果,很少涉及需要数据库也要一起灰度的,数据库层面仍保持一套数据库模型。

若因Java业务逻辑的变更,涉及需要区分两套数据库模型的情况:

1、灰度版本有增加库表字段的情况:数据库加字段可保持量版本兼容。(前提:业务SQL里无select * 的情况)

2、灰度版本有删除库表字段的情况:灰度版本SQL不读不写这些字段即可。

3、灰度版本对已有资源做定义转变不兼容的:在分布式系统中,一般是禁止这种行为的。因为这种行为会反应就在业务模块透出的API返回值DTO上,导致API行为不兼容,给调用方造成改造工作量。一般操作上是铺垫新字段做过渡。

4、灰度版本是大重构版本,库表模型大改的情况:那么在Java DataSource层面就按灰度模式进行区分了。灰度的写灰度的模型,非灰度的写灰度的模型。

  • 但是,要考虑:
    • 灰度版本是在线上真实运行,承接一部分真实流量和业务的。
    • 若最终将灰度版本下线后,正式版本上应仍能查询到之前灰度版本处理过的数据。
    • 若最终将灰度版本升级为正式版本,这2个库表模型的数据要融合成一个模型。
  • 这里就出现复杂性了
    • 灰度版本和灰度数据库上线运行时,需要将已有业务数据库的里数据“dump+增量”的方式同步到灰度数据库。因为线上数据库里实时都在产生数据变更。
    • 灰度版本数据落DB时,需要进行双写,一份写灰度DB,一份写非灰度DB。非灰度版本数据落DB时,是否需要双写按场景而定。
    • (PS:灰度版本和非灰度版本能支持互相数据双写,那一定程度上数据库模型仍是兼容,是否可以做好兼容而简化方案)

还有一种情况,灰度版本DB和非灰度版本DB可以完全隔离,SaaS租户化场景下可能存在的业务场景。

这就与全链路压测的“影子库表”的思路接近了,与DB实时双写的方案相比,也会简单很多。

总之,DB层面是否灰度需要细细分析业务场景,而且优先考虑兼容,一套库表。

7、灰度管理控制台

7.1、灰度配合的单位

以一个分布式服务应用为一个灰度配置单位,在应用上去挂在对应应用级的灰度策略。

分布式系统灰度发布实践-编程知识网

7.2、灰度策略示例

分布式系统灰度发布实践-编程知识网

分布式系统灰度发布实践-编程知识网