基于WebFilter实现断言和转发的灰度流量控制方案
1、需求背景
响应Java转Go的趋势,在部分服务已经转Go的情况下,需要把部分打到原Java项目的流量路由到Go项目上。
1.1、为什么是部分流量而不是全部?
因为涉及到的项目是交易核心链路,这也是灰度的重要作用。在进行核心链路的重构替代的时候,必须使用灰度方案,逐步进行迁移。落实到具体操作就是,前期只对小商家、非核心商家的流量路由到Go,确保Go项目没问题再逐步提高灰度的比例。一个好的需求要实现可灰度、可回滚、可监控。
1.2、为什么在服务层做过滤而不是网关?
因为成本原因,网关会逐步下线。目前灰度流量控制方案选择用SDK的方式来实现。假如在网关层做流量控制,被灰度的流量就不会进入到Java项目,Java项目的流量承担会少一点,但是也需要结合网关服务的成本综合考虑。
2、流程图
一些细节:
1、请求的拦截用过滤器而不是拦截器。因为服务中存在别的拦截器或者过滤器,而过滤器会比拦截器先拦截到请求,因此就会减少被路由到Go服务的请求对Java服务的资源的消耗;
2、在根据ChannelID和MerchantID判断是否需要灰度的时候,需要做成只依赖这两个ID和灰度配置去判断,而不要传递整个Servlet在内部做解析。
3、断言模块设计
请求进入到拦截器的第一个部分,需要在这个部分判断是否需要进行灰度。具体的操作为尝试获取目标接口的目标配置,假如获取不到则不参与灰度,假如能够获取到则参参与灰度。
配置的存储结构为Map<接口-配置>,但是假如url是可变的,也就是参数存在于路径上就会出现匹配不到的情况,比如说/config/1和/config/2,虽然都是请求同一个接口,但是由于参数不同,从httpservlet中拿到的uri也会不同,所以没有办法存放。
所以具体设计如下,Map中存放的为<接口/正则表达式-配置>。固定接口就直接存放接口,可变接口就存放正则表达式,如"/getJavaToGoGrayConfigJson/\d+"。
取的时候优先使用精确匹配,假如匹配不到的话再遍历Map的key,尝试进行正则匹配。
private GrayConfig getConfig(String uri) {
if (GRAY_CONFIG.isEmpty() || this.isConfigExpired()) {
// 更新配置信息
try {
this.refreshConfig();
}catch (Exception e){
log.error("GoGrayRouteFilter refreshConfig fail"+e);
}
}
GrayConfig grayConfig = GRAY_CONFIG.get(uri);
if (grayConfig == null) {
for (Map.Entry<String, GrayConfig> grayConfigEntry : GRAY_CONFIG.entrySet()) {
// 前缀匹配,然后进行正则匹配
String regex = grayConfigEntry.getValue().getUri();
Pattern pattern = Pattern.compile(regex);
// 创建 Matcher 对象并进行匹配
Matcher matcher = pattern.matcher(uri);
// 判断是否匹配成功
if (matcher.matches()) {
grayConfig = grayConfigEntry.getValue();
System.out.println("judge gray 匹配成功!");
return grayConfig;
} else {
System.out.println("judge gray 匹配失败!");
}
}
}
return grayConfig;
}
3.1、优化
遍历Map的Key显然不太明智,这里提供了一些优化方案。
1、同时维护一个List,存放Key,遍历的时候遍历这个List;
2、缓存设计,缓存可变接口的请求到Map,下次再请求就可以精确匹配;
3、维护一棵前缀树,通过前缀树进行查找而不是遍历。
4、请求解析模块设计
拿到灰度配置之后,需要根据灰度配置解析请求中的数据,再根据这些数据判断需不需要灰度。请求的数据会存放在三个地方:url、查询参数、请求体。我们需要根据配置从不同的位置拿到请求。
4.1、url
url参数的情况比较多样,可以通过正则表达式来处理。这里需要用到命名组的概念,举个例子,有这么一条请求"/test/1/2",1对应用变量id1