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》)
接下来,我们需要定义好切面
这里不做过多介绍,知名的开源组件都有类似的实现,大同小异
最后做一个自动配置类即可
到这里,我们就大功告成了,感兴趣的同学可以试试
最后更新于