Dubbo路由功能
Dubbo在 2.6.6版本之后提供了一个标签路由(tag router)功能来实现标签路由功能。标签路由通过将某一个或多个服务的提供者划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为蓝绿发布、灰度发布等场景的能力基础。
在框架设计层面,路由层往往位于负载均衡层之前,在进行选址时,路由完成的是 N 选 M(M <= N),而负载均衡完成的是 M 选一,共同影响选址逻辑,最后触发调用。
话不多说,直接配置上tag,来查看Dubbo路由的核心实现逻辑
服务提供者:provider01,provider02配置tag为”v1”,provider03配置tag为“v2”,代码如下:
provider01,provider02
@DubboService(weight = 5,loadbalance = "roundrobin",tag = "v1")
public class DemoServiceImpl implements DemoService {
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* The default value of ${dubbo.application.name} is ${spring.application.name}
*/
@Value("${dubbo.application.name}")
private String serviceName;
@Override
public String sayHello(String name) {
return String.format("[%s] : Hello, %s", serviceName, name);
}
}
provider03
@DubboService(weight = 15, loadbalance = "roundrobin",tag = "v2")
public class DemoServiceImpl implements DemoService {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Value("${dubbo.application.name}")
private String serviceName;
@Override
public String sayHello(String name) {
return String.format("[%s] : Hello, %s", serviceName, name);
}
}
服务消费者:consumer设置tag为“v1”
@RestController
@RequestMapping("/test")
@Api(tags = "Dubbo服务测试类")
public class DemoController {
@DubboReference(loadbalance = "roundrobin",timeout = 100000,tag = "v1")
private DemoService demoService;
@ApiOperation(value = "服务调用测试", notes = "")
@GetMapping("sayHello")
public void sayHello() {
String name = demoService.sayHello("ZywooLee-debug");
System.out.println(name);
}
}
Dubbo路由功能的实现在org.apache.dubbo.rpc.cluster.router.tag.TagRouter#route,具体筛选服务提供者在org.apache.dubbo.rpc.cluster.router.tag.TagRouter#filterUsingStaticTag
通过idea的评估工具可查看到:invokers的第一个元素tag属性为v2,第二第三个元素tag属性为v1,然后进入filterInvoker进行筛选服务消费者指定的tag(v1),并返回提供者列表,后续再进入负载均衡实现逻辑。
private <T> List<Invoker<T>> filterInvoker(List<Invoker<T>> invokers, Predicate<Invoker<T>> predicate) {
return invokers.stream()
.filter(predicate)
.collect(Collectors.toList());
}
可以看出,每次服务的调用,都需要进行循环去filter获取符合条件的提供者列表,时间复杂度为O(N).
Dubbo路由改造方案
思考
路由选址的过程不应该发生在每一次请求调用,应该发生在以下场景:
- 地址列表发生变化,重新计算tag对应的路由列表
- 路由规则发生变化(需要开启动态配置推送),此时路由列表也需要重新计算
实现思路
服务地址列表在不发生变化的时候,地址列表基本是固定的,是否可以维护一个数据结构,提前计算好每个标签(tag)对应的地址列表并存储起来,在获取的时候以O(1)的时间复杂度获取,之后地址变更的时候触发修改此数据结构。
利用Dubbo的SPI机制,进行自定义路由实现,以此来进行功能改造,自定义路由ZywooTagRouter继承于TagRouter,代码如下
/**
* @Auther: Zywoo Lee
* @Date: 2022/3/13 16:42
* @Description:
*/
public class ZywooTagRouter extends TagRouter implements ConfigurationListener {
private static final int TAG_ROUTER_DEFAULT_PRIORITY = 100;
/**
* 新增map存储invoker列表
* key:tag
* value:invokers
*/
private Map<String, Object> invokerMap = new ConcurrentHashMap<>();
public ZywooTagRouter(URL url) {
super(url);
this.priority = TAG_ROUTER_DEFAULT_PRIORITY;
}
@Override
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
if (CollectionUtils.isEmpty(invokers)) {
return invokers;
}
String tag = StringUtils.isEmpty(invocation.getAttachment(TAG_KEY)) ? url.getParameter(TAG_KEY) :
invocation.getAttachment(TAG_KEY);
if (invokerMap.containsKey(tag)) {
System.out.println("通过invokerMap获取invoker集合,tag:" + tag);
return (List<Invoker<T>>) invokerMap.get(tag);
}
List<Invoker<T>> result = super.route(invokers,url,invocation);
System.out.println("首次计算invoker集合,tag:" + tag);
invokerMap.put(tag, result);
return result;
}
@Override
public <T> void notify(List<Invoker<T>> invokers) {
System.out.println("加载invokers,清空invokerMap...");
invokerMap.clear();
super.notify(invokers);
}
}
笔者这边没有开启动态配置,所以只监听了地址列表发生变化(当服务提供者下线或者上线)时的路由重新计算。
地址发生变化时的调用链路是
org.apache.dubbo.registry.integration.RegistryDirectory#refreshInvoker
org.apache.dubbo.rpc.cluster.RouterChain#setInvokers
com.dubbo.router.ZywooTagRouter#notify(自定义了ZywooTagRouter继承于TagRouter,以便于地址更新时刷新路由)
org.apache.dubbo.rpc.cluster.router.tag.TagRouter#notify
发起请求可以看到首次请求invokerMap是空的,继续进行第二次请求
可以看到,当服务者列表未变更时,直接获取缓存,不再重新计算。