流量染色SDK设计的思考

流量染色SDK设计的思考

  • 引言
  • 痛点在哪里
  • 流量染色
    • 本地启动随意注册问题
    • 应用级别的灰度
    • 服务的优雅下线
    • 生产环境发布提速
    • 全链路压测
  • 流量染色的实现
    • 流量标如何透传
    • 流量路由如何路由到染色节点
    • 染色流量入口携带染色标
  • 小结


笔者之前实习过程中负责过部门稳定性基建工作开展,其中一项任务就是负责流量染色SDK的实现和验证,具体来说,我负责的只是染色全流程中的一环,但是本文我想借助得物技术团队发表的流量染色实践系列文章,结合我自身实际开发经验,来聊聊我个人的一些拙见。

得物技术团队系列文章:

  • 连流量染色都没有,你说要搞微服务?
  • 得物染色环境落地实践

引言

在微服务架构下,服务数量多导致的链路依赖问题会成为开发和维护过程中一块挥之不去的噩梦,使用流量染色技术便可以很好的处理这类问题。

流量染色简单来说就是对请求的流量打上标签进行染色,然后该请求在整个链路中都会携带整个标签信息,可以通过标签进行流量的调度等功能。

基于流量染色可以实现很多功能,比如灰度逻辑,蓝绿部署,泳道隔离,全链路压测等。


痛点在哪里

当我们存在多个需求进行并发开发时,如果各个需求都需要独立测试,那么最直接的做法就是多部署几套环境,但是这会导致以下问题:

  1. 资源成本高 : 扩展一套环境,除了服务本身,往往还会依赖很多基础组件,如: 注册中心,配置中心,消息队列等。
  2. 链路依赖治理困难 : 我们需要确保整条服务链路是可用的,如: A依赖B,B依赖C … 最差的情况就是全量部署;同时如果该环境只使用一次,并且存在大量类似的环境时,容易导致服务调用链路复杂且混乱。

流量染色

上述问题的一种解决方案就是流量染色,也可以理解为环境隔离,具体做法分为以下三步:

  1. 在注册中心为每个服务额外维护一个染色标识,可以理解为版本标识
  2. 在流量的入口处,对请求添加染色标识
  3. 在基础框架层,对染色标识进行解析,透传和服务路由


具体来说,当我们准备开发一个需求时,我们需要在被改动的应用中配置一个版本,这个版本信息会存储在注册中心的元数据里面。

然后就去创建一个属于该需求的泳道(独立环境)进行部署,只需要部署这个需求改动的应用即可。该应用依赖的下游应用无需重新部署,因为在当前环境找不到对应的服务提供者就去路由到稳定环境找,如果稳定环境中也没有就报错。

流量染色SDK设计的思考_第1张图片

  1. 服务可以按照流量标把流量路由到相应染色服务上
  2. 如果染色标对应染色环境没有此服务,则流量会走到基准环境
  3. 如果染色环境服务添加了,没有部署,或者部署了服务进程挂了,则流量会报错而并非走到基准环境(避免一些服务异常问题没有暴露)
  4. DB、MQ、Redis等中间件期望用同一套,避免浪费

借助流量染色,我们可以实现如下目标:

  1. 完成对测试环境的治理,降低测试环境部署多套带来的昂贵成本,同时提升测试效率
  2. 借助流量染色还可以完成应用级别灰度,服务优雅下线,生产环境发布提速,全链路压测等功能

本地启动随意注册问题

研发有时候会在本地启动服务,用于调试某个问题,好处就是能够快速复现测试环境的问题,及时发现问题代码。但是由于本地启动的服务也会注册到注册中心里面,这样测试环境的请求就有可能会路由到研发本地启动的这个服务上,研发本地的这个服务代码有可能不是最新的,导致调用异常。

该问题目前常用的解决方案是通过在本地启动时屏蔽掉服务的注册功能,也就是不注册上去,这样就不会被正常的测试请求路由到。

如果有了流量染色的功能,研发本地启动服务的时候指定一个属于自己的版本号,只要不跟正常测试的版本一致即可。正常测试的请求就不会路由到研发注册的这个实例上。


应用级别的灰度

针对接口级别的灰度,目前都是在应用内进行灰度控制。但是应用级别的,目前没有特别好的方式来控制灰度。

比如有一个技术需求,需要将Redis的Client从Lettuce换成Jedis,这种场景的灰度就是应用级别的,目前的做法就是发布一个节点,然后结束发布流程,具体能被灰度到的量是由服务实例的总数量来决定的,没办法灵活控制。

如果有流量染色,可以新发一个节点,这个节点的版本升级一下,比如之前的版本是V1,那么新发的就是V2版本。首先V1版本肯定是承载生产所有流量的,可以通过网关进行控制让流量按某种方式转发到V2版本,比如用户白名单,地区,用户比例等等。有问题也可以随时将流量切回V1,非常方便。

流量染色SDK设计的思考_第2张图片


服务的优雅下线

服务要想无损进行优雅下线,还是需要做很多工作的,比如目前发布时会先将要发布的服务从注册中心注销掉,但是应用内部还是会有服务实例信息的缓存,需要等到一定的时间缓存完成清除后,对应的目标实例才不会被请求到。

如果基于染色去实现的话,将需要下线的实例信息(IP:PORT)通过配置中心推送给网关进行染色处理,染色信息跟随着请求贯穿整个链路,应用内的负载均衡组件,MQ等中间件会对要下线的目标实例信息进行过滤,这样就不会有流量到要下线的实例上去。


生产环境发布提速

目前,主流的发布都是滚动部署,滚动发布的好处是成本低,不用额外增加部署的资源,一个萝卜一个坑,慢慢替换就是。不好的点在于发布时间长,全链路依赖太严重,如果发布之前依赖关系错乱了,那就是一个线上故障。

要解决这个发布速度的问题,可以基于流量染色来实现蓝绿部署。也就是在发布的时候重新部署一个V2的版本,这个V2版本的实例数量跟V1保持一致,由于这个V2版本是没有流量的,所以不存在依赖关系,大家可以同时发布,等到全部发完之后,就可以通过网关进行流量分发了,先分发一点点流量到V2版本进行验证,如果没有问题就可以慢慢放大流量,然后将V1版本的容器释放掉。

流量染色SDK设计的思考_第3张图片
发布速度确实提升了,可是问题在于蓝绿部署的成本太高了,资源成本要翻倍,虽然发布后老的资源就回收了,但是你总的资源池还是得容纳下这2个版本并行才行。

那有没有折中的方式,既能提高发布效率又能不增加资源成本呢?

  • 可以在发布的时候采用替换的形式,先发布一半的实例,这一半的实例就是我们的V2版本,发布时是没有流量的,所以还是可以并行的去发布。

发布完成后,开始放量到V2版本,然后验证。验证之后就可以发布另一半的实例了,这样的方式总的资源是没有变化的,但是有一个比较严重的问题就是直接停掉了一半的实例,剩下的实例能不能支撑当前的流量,因为交易内的应用都是面向C端用户的,流量很有可能在短时间内达到很高的量。


全链路压测

全链路压测对于电商业务来说必不可少,每年有N次大促,都需要提前进行压测来确保大促的稳定。其中全链路压测最核心的一点就是流量的区分,需要区分流量是正常的用户请求还是压测平台的压测流量。

只有区分了流量,才能将压测流量进行对应的路由,比如数据库,Redis等流量需要路由到影子库中。基于流量染色就很容易给流量打标,从而区分流量的类型。

这一块内容也是笔者实习期间负责的流量染色SDK功能,具体实现思路如下图所示:
流量染色SDK设计的思考_第4张图片
整个流量染色SDK的核心其实就是一个切面,处于可扩展性设计,我将整个染色SDK分成了四个模块,如下图所示:

流量染色SDK设计的思考_第5张图片

  1. 配置中心默认使用Apollo,这里由于部门业务关系,就直接写死了,防止耦合的话,可以考虑SPI配合Provider机制完成动态切换。
  2. 灰度key生成模块和灰度命中计算模块负责对带有压测标识的请求进行计算,得出一个灰度值,并与配置中心中的灰度阈值进行比对,判断当前请求是否需要放行,借此我们可以轻松控制压测请求走缓存和走DB的比例。
  3. 压测请求解析模块负责过滤出那些携带有压测标志的请求,并将压测信息设置到线程的上下文环境中。

流量染色的实现

这里有三个点很关键:

  1. 应用需要具备版本概念

    • 可以将版本信息放入项目的配置文件中,在项目启动时,将此版本信息跟自身实例信息一起注册到注册中心里面,这些信息被称为元数据。
    • 在控制流量路由的时候根据染色信息进行对应的匹配,先从注册中心获取可用服务列表,在根据当前请求版本,过滤掉不符合要求的服务实例,在剩余服务实例集合中进行负载均衡。
  2. 染色信息全链路透传

    • 可借鉴分布式链路追踪思路,在每次请求入口处生成一个唯一的TraceID,通过这个TraceId就可以将整个链路串联起来,就像TracId需要全链路传递一样,流量染色的信息也需要全链路传递。
    • 应用之间的透传可以借助独立的Agent包进行传递,或者在基础框架中进行埋点传递。如果内网之间采用Http进行接口调用,那么就在请求头中将信息进行传递。如果是RPC的方式,则用RpcContext进行传递。
    • 应用内部透传,可以借助ThreadLocal,在发起接口调用的时候从ThreadLocal中取出继续透传。但是这里需要注意线程池切换的场景,在这种场景下,我们需要考虑transmittable-thread-local。
  3. 流量路由控制

    • 当流量有了标签信息,剩下的工作就是要根据标签信息将请求路由到正确的实例上。如果内部框架是Spring Cloud体系,可以通过Ribbon去控制路由。如果是Dubbo体系,可以通过继承Dubbo的AbstractRouter重新制定路由逻辑。如果是内部自研的RPC框架,肯定留有对应的扩展去控制路由。

流量标如何透传

  1. 首先流量标在流量入口层会放到http header里面
  2. 流量到网关后,服务链路上面流量标往下透传的方式是通过从header里面获取染色标,并塞到ThreadLocal里面向下透传。(这边需要处理跨线程透传的问题)

流量路由如何路由到染色节点

这里分两块考虑:

  • rpc调用,拿到染色标之后,如何找到染色节点?这里要解决的是怎么识别染色节点
  • MQ消息,producer如何发送带染色标的消息,consumer如何处理带染色标的消息

下面先来我们依次来看看上面两个问题在得物内部的具体实现:

服务注册–识别染色节点:

  1. 首先染色环境创建的时候,会定义好染色标
  2. 在此染色环境添加服务部署的时候,默认会把染色标注入到环境变量COLORING_ENV
  3. 容器发布配置页面会自动增加COLORING_ENV变量
  4. 至此,服务启动时已可以读到COLORING_ENV环境标变量了,下一步就看注册中心怎么去区分染色节点了
  5. 首先服务在添加到染色环境的时候,服务会在注册中心染色场增加一个节点,标明该服务在此染色环境是有服务节点存在的
  6. 染色场主要解决的问题是:如果染色节点挂了,染色环境流量应该判断该染色环境是否应该有染色节点,有的话就报错,没有的话才会走到基准环境。避免测试问题未暴露。
  7. 其次在服务注册时候,服务节点信息和方法注册会携带染色标< coloring_env >
  8. 至此,注册中心就可以基于染色标识别染色节点,业务服务可以根据Trace中的染色标结合注册中心染色节点做染色流量路由。

MQ改造–识别和处理MQ消息:

MQ主要解决的是,染色环境的消息生产者producer发送的消息,只被染色环境的消费者消费,染色环境如果没有消费节点,则由基准环境消费者消费。

这里之前讨论了两种做法:

  • 第一种是基于Topic隔离的方案,每套染色环境使用不同的topic进行通信,这样隔离性比较好,消息不容易串掉。
  • 第二种是Topic不隔离,所有染色环境共用一个topic,生产者Producer在生产消息时候把染色标带上,consumer每套染色环境有一个,consumer在做消费时候会判断消息里面的染色标和本地染色标是否一致,如果一致则消费,如果不一致则直接返回ACK不走具体消费逻辑。

得物技术团队内部选择的是第二种方案,下面基于第二种方案做详细介绍:

流量染色SDK设计的思考_第6张图片
如图所示:

  1. ServiceB_Color1会自动注册GID_Color1_Topic消费组,监听Topic_A。Color2和Color3环境一样。
  2. 带Color1的消息由ServiceA_Color1生产,ServiceB_Color1消费。
  3. 带Color2的消息由ServiceA_Color2生产,ServiceB消费,因为ServiceB在Color2染色环境没有节点
  4. 带Color3的消息由于染色环境Color3没有ServiceA_Color3节点,则带Color3的流量会打到基准环境ServiceA,
  5. 此时ServiceA会生产带Color3的消息,此消息由ServiceB_Color3消费

染色流量入口携带染色标

解决完染色标透传,以及染色标逻辑处理后,剩下就是如何在流量发起方把染色标给带上了,其实就是把染色标塞到header里面的x-infr-flowtype字段。

其中染色环境列表的获取由发布平台提供接口给到各流量入口方去选择。

目前业务推广过程中,主要遇到的入口方大致有以下几种:
流量染色SDK设计的思考_第7张图片

至此整个业务改造基本完成,从染色流量如何构造、流量标如何透传、染色节点如何识别以及识别后重点染色逻辑如何处理等一整套流程就清晰了。


小结

笔者实习期间只负责了染色全流程链路中最简单的一环,也就是借助流量染色技术实现压测演练过程中的动态化压力调整,而在阅读了得物技术团队的流量染色落地实践系列文章后,才发觉先前自己对流量染色的整体认知水平偏低,所以特总结此文。

流量染色技术总体而言还是比较好理解的,借助流量染色技术可以解决测试环境冲突和测试环境稳定性的问题,并且相较之前多套独立环境的方案,在成本上也有比较大的节省。并且我们也可以尝试用染色的能力解决生产灰度发布问题,相信也会有不错的效果。

你可能感兴趣的:(#,技术杂谈,java,数据库,redis)