vlambda博客
学习文章列表

Dubbo实现跨机房调用

内容目录

一、背景二、实现方案三、原理分析四、Bug五、总结六、参考

一、背景

在一些跨境业务中,特别是电商或者SAAS场景,用户群体是分离的,经营者在国内,而产品使用者在海外,或者外海用户分布在多个大区,而数据中心在其中一个大区,那么就会存在一些跨大区或者跨机房的服务调用场景,比如海外用户分布在亚洲大区和美洲大区,亚洲大区与美洲大区之间数据节点部署是MS架构,那么如果美洲大区的用户有写操作,就需要调用亚洲大区的服务。

那么就需要在双机房部署的时候,优先调用本机房服务,然后如果本机房没有服务或者不符合要求,那么会调用其他机房的服务。

二、实现方案

多注册中心

dubbo2.7.5版本引入了多注册中心集群负载均衡能力支持,对于多注册中心订阅的场景,选址时的多了一层注册中心集群间的负载均衡:

Dubbo实现跨机房调用

在Cluster Invoker这一级,支持以下选址策略:

  • 指定优先级

<!-- 来自 preferred=“true” 注册中心的地址将被优先选择,只有该中心无可用地址时才 Fallback 到其他注册中心 --><dubbo:registry address="nacos://${nacos.address1}" preferred="true" />
  • 同zone优先

<!-- 选址时会和流量中的 zone key 做匹配,流量会优先派发到相同 zone 的地址 --><dubbo:registry address="nacos://${nacos.address1}" zone="asia" />
  • 权重轮询

<!-- 来自asia和america集群的地址,将以 8:2 的比例来分配流量 --><dubbo:registry id="asia" address="nacos://${nacos.address1}" weight=”80“ /><dubbo:registry id="america" address="nacos://${nacos.address2}" weight=”20“ />
  • 默认,任意可用

配置调整

对于亚洲大区,读写都只需要调用本机房的服务,只需配置:

<dubbo:registry address="nacos://${asia.address}" preferred="true" />

对于美洲大区,需要调用亚洲大区的写服务,因此需要配置美洲和亚洲两个注册中心,并将美洲的注册中心标记为默认:

<dubbo:registry id="america" address="nacos://${america.address}" preferred="true" /><dubbo:registry id="asia" address="nacos://${asia.address}" />

代码实现

@Reference(registry="asia")WriteAPI writeApi;

写服务注入强制指定亚洲大区,这样对于美洲大区调用写服务会调用到亚洲大区,对于亚洲大区调用写服务也会调用本大区服务。

三、原理分析

多注册中心集群负载均衡能力支持主要由ZoneAwareClusterInvoker来实现,核心代码如下:

@Overridepublic Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException { //首先优先选择标记为preferred的注册中心的ClusterInvoker for (Invoker<T> invoker : invokers) { ClusterInvoker<T> clusterInvoker = (ClusterInvoker<T>) invoker; if (clusterInvoker.isAvailable() && clusterInvoker.getRegistryUrl() .getParameter(REGISTRY_KEY + "." + PREFERRED_KEY, false)) { return clusterInvoker.invoke(invocation); } } //然后选择同zone的服务ClusterInvoker String zone = invocation.getAttachment(REGISTRY_ZONE); if (StringUtils.isNotEmpty(zone)) { for (Invoker<T> invoker : invokers) { ClusterInvoker<T> clusterInvoker = (ClusterInvoker<T>) invoker; if (clusterInvoker.isAvailable() && zone.equals(clusterInvoker.getRegistryUrl().getParameter(REGISTRY_KEY + "." + ZONE_KEY))) { return clusterInvoker.invoke(invocation); } } String force = invocation.getAttachment(REGISTRY_ZONE_FORCE); if (StringUtils.isNotEmpty(force) && "true".equalsIgnoreCase(force)) { throw new IllegalStateException(""); } }
//按照权重选择Invoker Invoker<T> balancedInvoker = select(loadbalance, invocation, invokers, null); if (balancedInvoker.isAvailable()) { return balancedInvoker.invoke(invocation); }
//随机选择一个可用的 for (Invoker<T> invoker : invokers) { ClusterInvoker<T> clusterInvoker = (ClusterInvoker<T>) invoker; if (clusterInvoker.isAvailable()) { return clusterInvoker.invoke(invocation); } } //随机选择一个 return invokers.get(0).invoke(invocation);}

从代码中我们可以看出几种策略的优先级,优先选择注册中心标记为preferred的Invoker调用,如果没有则选择同大区的服务调用,否则使用负载均衡根据权重选择Invoker,再者就随机选择一个可用的Invoker,最后如果前边都不满足则随便选择一个Invoker调用。

四、Bug

我们团队使用的dubbo框架版本是2.7.8,但是这个版本有一个比较低级的bug,就是前边代码中优先选择preferred的时候有一行代码获取preferred:

clusterInvoker.getRegistryUrl().getParameter(REGISTRY_KEY + "." + PREFERRED_KEY, false)

这里用REGISTRY_KEY和PREFERRED_KEY进行了连接,getParameter的key是registry.preferred,而注册中心配置的key是preferred,所以配置会不生效,后续高版本对该bug进行了修复:

Dubbo实现跨机房调用

但是升级版本是一个高风险的操作,我们不能拿别人的错误惩罚自己。当然我们还有别的解决方案,既然他要的key是registry.preferred那么我就给你这个key。

Dubbo实现跨机房调用

把registry.preferred=true拼到注册中心url的后边就行了,实测可用。配置改成如下:

<dubbo:registry id="america" address="nacos://${america.address}?registry.preferred=true" /><dubbo:registry id="asia" address="nacos://${asia.address}" />

当然在高版本中直接使用preferred=true替换即可,在不确定当前版本是否有bug的情况下可以两种方式都写上。

五、总结

ZoneAwareClusterInvoker是其他ClusterInvoker的包装,ClusterInvoker又是Invoker的包装,那么ZoneAwareClusterInvoker的调用逻辑就是:

Dubbo实现跨机房调用

回过头来我们思考一个问题,就本篇文章分析的亚洲和美洲双大区注册中心场景中,美洲机房显式配置了两个注册中心,但是对于美洲集群,配置亚洲注册中心的目的只是订阅服务,没有双大区注册服务的诉求,然后dubbo的服务注册和订阅机制中并没有将注册和订阅做隔离,也就是说美洲的服务也会注册到亚洲注册中心,只不过不会有消费这而已,是不是在某种程度上造成了注册中心内存的浪费,以及美洲大区服务启动耗时增加。

本着浪费可耻,节约光荣的原则,那有没有一种机制或者有没有可能对于这种跨大区服务调用的场景,只有订阅服务诉求的情况下,做到服务订阅和服务注册隔离以及可个性化定制?

目前好像暂不支持,如果感兴趣可以自己研究下服务注册和订阅流程的源码,是否能够做到使用SPI或者其他方式做到隔离和定制化,以及实现之后的合理性和价值。

六、参考

https://github.com/apache/dubbo/blob/3.0/dubbo-cluster/src/main/java/org/apache/dubbo/rpc/cluster/support/registry/ZoneAwareClusterInvoker.java

https://dubbo.apache.org/zh/blog/2020/05/18/2.7.5-%E5%8A%9F%E8%83%BD%E8%A7%A3%E6%9E%90/

https://dubbo.apache.org/zh/blog/2019/06/22/%E4%BD%BF%E7%94%A8-dubbo-%E8%BF%9E%E6%8E%A5%E5%BC%82%E6%9E%84%E5%BE%AE%E6%9C%8D%E5%8A%A1%E4%BD%93%E7%B3%BB/

https://github.com/apache/dubbo/pull/7788