Open feign动态切流实现

背景

最近在做服务拆分,涉及到feign接口迁移,因为调用方比较多,希望可以有一些其他的方案,在调用方不做代码修改的情况下,也可以实现流量迁移,同时可以控制切流的节奏

目标

1、运行时做feign调用切流

2、切流比例可以动态控制

方案

上文,我们做过Open Feign源码分析(感兴趣的同学可以出门左转查看上一篇文章《Open feign源码分析》),可以得知Open feign接口在调用时:最终会生成RequestTemplate,通过操作RequestTemplate发送http请求,解析http响应;同时Open feign提供了运行时增强的入口,通过RequestIntercepter实现

public interface RequestInterceptor {    
    void apply(RequestTemplate template);
}

接下来,我们查看RequestTemplate

public final class RequestTemplate implements Serializable {

  private static final Pattern QUERY_STRING_PATTERN = Pattern.compile("(?<!\\{)\\?");
  private final Map<String, QueryTemplate> queries = new LinkedHashMap<>();
  private final Map<String, HeaderTemplate> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
  private String target;
  private String fragment;
  private boolean resolved = false;
  private UriTemplate uriTemplate;
  private BodyTemplate bodyTemplate;
  private HttpMethod method;
  private transient Charset charset = Util.UTF_8;
  private Request.Body body = Request.Body.empty();
  private boolean decodeSlash = true;
  private CollectionFormat collectionFormat = CollectionFormat.EXPLODED;
  private MethodMetadata methodMetadata;
  private Target<?> feignTarget;
  ...
}

其中MethodMetadata属性记录了原始Open feign 接口的方法

那如何做动态切流呢?

假设提供服务的新老服务serviceA和serviceB有以下特点

1、接口path都相同

2、接口入参格式相同

3、接口出参格式相同

那么,我们只需要在调用serviceA时,将地址换成serviceB的地址,即可实现流量打到serviceB,且不出现数据解析异常

基于以上分析,我们可以在迁移到新服务时,参考上面3点要求做实现

接下来的问题就是,如何在调用serviceA时将地址换成serviceB的地址,在java体系中,我们很容易想到的就是AOP,那么如何实现呢?

首先,我们先定义一个注解,注解中包含了新服务的地址,以及切流的配置

从注解定义,我们可以看到切流控制可以精确到方法级别

接下来,我们定义一个类,用于从配置中心后去切流的配置

然后,我们在定义一个接口,用于判断是否切流

接着,我们定义一个默认实现

接下来,我们定义注解解析类

这个类的主要逻辑就是从方法或者接口申明上去找@ForwardTraffic注解,找到之后,获取到注解的targetService和rateKey属性

再接下来,我们定义拦截器

我们重点看apply方法,其主要逻辑如下

1、通过RequestTemplate.methodMetadata去查找如是否被@ForwardTraffic标记,如果被标记,则返回对应的targetService和rateKey

2、如果没有被标记,也就是targetService和rateKey为空,此时不做任何操作

3、根据rateKey判断是否需要切流(通过比例控制),如果未命中,此时不做任何操作

4、如果命中,则替换地址,也就是replaceHost方法

这里需要注意,因为我们通常不希望将地址写死,因此这里参考open feign原始实现,copy了resolve方法,支持通过环境变量和配置来动态解析目标地址,也就是说支持targetService是一个spring el表达式

至此,我们只需要在feign client定义的方法上加上对应的@ForwardTraffic注解,调用方升级下版本即可实现运行时切流了。

到这里,我们的目标完成了大半;为什么是大半?因为大型项目中,通常调用方不一定全部是java,也有可能是go或者python,因此可能还是会有其他流量打到旧的服务上。

因此我们还需要在旧的服务上实现流量的转发逻辑

那么如何实现呢?

还记得上面我们说的3个假设吗?

1、新老服务path一样

2、接口入参格式一样

3、接口出参格式一样

因此,我们可以有几种方式来做切流

基于网关的切流

假设内部调用先通过网关,然后再到目标服务,那么我们可以在网关层面直接做流量的转发,不论是基于nginx的流量转发还是基于shenyu的流量转发都可以做到按照比例做流量切分,这里不做详细介绍

基于目标服务的切流

在客户端控制的部分,我们已经定义了注解以及切流的开关控制类,那么我们是否可以复用这一部分呢?假设可以复用这部分,我们做一个Contoroller层面的AOP不就可以完成服务端的切流了吗

那如何复用呢?

对于切流开关控制类,比较容易,只需要我们引入对应的bean即可

对于注解我们应该如何复用呢?

答案也很简单,我们只需要在controller层面声明实现了某个Feign Client,我们即可将对应的controller方法和feign 接口方法建立映射关系,且不会影响到现有controller逻辑,如下所示

好,有了映射关系,我们即可通过AOP来做切流。我们需要对所有的controller方法做AOP吗?答案是没必要,我们只需要对需要做切流的方法做AOP即可

因此,我们需要先定义一个注解,然后在根据注解实现AOP

接下来,我们先实现拦截器的逻辑

拦截器实现了MethodIntercepter,我们重点看下invoke方法,主要逻辑如下

1、通过method获取到对应的Feign client class定义

2、通过反射获取到Feign Client 方法上的@ForwardTraffic注解,并获取targetService和rateKey属性

3、根据rateKey判断是否需要切流

4、如果需要切流,根据targetService生成一个新的feign client

5、通过反射调用新的feign client(简化流量转发逻辑,将新的http调用代理给feign client)

对于步骤4,是一个小知识点,感兴趣的同学可以出门左转(《spring cloud手动创建feign client》)

接下来,我们需要定义好切面

这里不做过多介绍,知名的开源组件都有类似的实现,大同小异

最后做一个自动配置类即可

到这里,我们就大功告成了,感兴趣的同学可以试试

最后更新于