Dubbo路由功能

Dubbo在 2.6.6版本之后提供了一个标签路由(tag router)功能来实现标签路由功能。标签路由通过将某一个或多个服务的提供者划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为蓝绿发布、灰度发布等场景的能力基础。

在框架设计层面,路由层往往位于负载均衡层之前,在进行选址时,路由完成的是 N 选 M(M <= N),而负载均衡完成的是 M 选一,共同影响选址逻辑,最后触发调用。

话不多说,直接配置上tag,来查看Dubbo路由的核心实现逻辑

服务提供者:provider01,provider02配置tag为”v1”,provider03配置tag为“v2”,代码如下:

provider01,provider02

  1. @DubboService(weight = 5,loadbalance = "roundrobin",tag = "v1")
  2. public class DemoServiceImpl implements DemoService {
  3. private final Logger logger = LoggerFactory.getLogger(getClass());
  4. /**
  5. * The default value of ${dubbo.application.name} is ${spring.application.name}
  6. */
  7. @Value("${dubbo.application.name}")
  8. private String serviceName;
  9. @Override
  10. public String sayHello(String name) {
  11. return String.format("[%s] : Hello, %s", serviceName, name);
  12. }
  13. }

provider03

  1. @DubboService(weight = 15, loadbalance = "roundrobin",tag = "v2")
  2. public class DemoServiceImpl implements DemoService {
  3. private final Logger logger = LoggerFactory.getLogger(getClass());
  4. @Value("${dubbo.application.name}")
  5. private String serviceName;
  6. @Override
  7. public String sayHello(String name) {
  8. return String.format("[%s] : Hello, %s", serviceName, name);
  9. }
  10. }

服务消费者:consumer设置tag为“v1”

  1. @RestController
  2. @RequestMapping("/test")
  3. @Api(tags = "Dubbo服务测试类")
  4. public class DemoController {
  5. @DubboReference(loadbalance = "roundrobin",timeout = 100000,tag = "v1")
  6. private DemoService demoService;
  7. @ApiOperation(value = "服务调用测试", notes = "")
  8. @GetMapping("sayHello")
  9. public void sayHello() {
  10. String name = demoService.sayHello("ZywooLee-debug");
  11. System.out.println(name);
  12. }
  13. }

Dubbo路由功能的实现在org.apache.dubbo.rpc.cluster.router.tag.TagRouter#route,具体筛选服务提供者在org.apache.dubbo.rpc.cluster.router.tag.TagRouter#filterUsingStaticTag

image.png

通过idea的评估工具可查看到:invokers的第一个元素tag属性为v2,第二第三个元素tag属性为v1,然后进入filterInvoker进行筛选服务消费者指定的tag(v1),并返回提供者列表,后续再进入负载均衡实现逻辑。

  1. private <T> List<Invoker<T>> filterInvoker(List<Invoker<T>> invokers, Predicate<Invoker<T>> predicate) {
  2. return invokers.stream()
  3. .filter(predicate)
  4. .collect(Collectors.toList());
  5. }

可以看出,每次服务的调用,都需要进行循环去filter获取符合条件的提供者列表,时间复杂度为O(N).

Dubbo路由改造方案

思考

路由选址的过程不应该发生在每一次请求调用,应该发生在以下场景:

  1. 地址列表发生变化,重新计算tag对应的路由列表
  2. 路由规则发生变化(需要开启动态配置推送),此时路由列表也需要重新计算

实现思路

服务地址列表在不发生变化的时候,地址列表基本是固定的,是否可以维护一个数据结构,提前计算好每个标签(tag)对应的地址列表并存储起来,在获取的时候以O(1)的时间复杂度获取,之后地址变更的时候触发修改此数据结构。

利用Dubbo的SPI机制,进行自定义路由实现,以此来进行功能改造,自定义路由ZywooTagRouter继承于TagRouter,代码如下

  1. /**
  2. * @Auther: Zywoo Lee
  3. * @Date: 2022/3/13 16:42
  4. * @Description:
  5. */
  6. public class ZywooTagRouter extends TagRouter implements ConfigurationListener {
  7. private static final int TAG_ROUTER_DEFAULT_PRIORITY = 100;
  8. /**
  9. * 新增map存储invoker列表
  10. * key:tag
  11. * value:invokers
  12. */
  13. private Map<String, Object> invokerMap = new ConcurrentHashMap<>();
  14. public ZywooTagRouter(URL url) {
  15. super(url);
  16. this.priority = TAG_ROUTER_DEFAULT_PRIORITY;
  17. }
  18. @Override
  19. public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
  20. if (CollectionUtils.isEmpty(invokers)) {
  21. return invokers;
  22. }
  23. String tag = StringUtils.isEmpty(invocation.getAttachment(TAG_KEY)) ? url.getParameter(TAG_KEY) :
  24. invocation.getAttachment(TAG_KEY);
  25. if (invokerMap.containsKey(tag)) {
  26. System.out.println("通过invokerMap获取invoker集合,tag:" + tag);
  27. return (List<Invoker<T>>) invokerMap.get(tag);
  28. }
  29. List<Invoker<T>> result = super.route(invokers,url,invocation);
  30. System.out.println("首次计算invoker集合,tag:" + tag);
  31. invokerMap.put(tag, result);
  32. return result;
  33. }
  34. @Override
  35. public <T> void notify(List<Invoker<T>> invokers) {
  36. System.out.println("加载invokers,清空invokerMap...");
  37. invokerMap.clear();
  38. super.notify(invokers);
  39. }
  40. }

笔者这边没有开启动态配置,所以只监听了地址列表发生变化(当服务提供者下线或者上线)时的路由重新计算。
地址发生变化时的调用链路是
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
image.png
发起请求
image.png可以看到首次请求invokerMap是空的,继续进行第二次请求

image.png

可以看到,当服务者列表未变更时,直接获取缓存,不再重新计算。