今天想和大家聊聊Dubbo源码中实现的一个注册中心扩展。它很特殊,也帮我解决了一个困扰已久的问题,刚刚在生产中用了,效果很好,迫不及待想分享给大家。

Dubbo的扩展性非常灵活,可以无侵入源码加载自定义扩展。能扩展协议、序列化方式、注册中心、线程池、过滤器、负载均衡策略、路由策略、动态代理等等,甚至「扩展本身」也可以扩展。

在介绍今天的这个注册中心扩展之前,先抛出一个问题,大家思考一下。

如何低成本迁移注册中心?

有时出于各种目的需要迁移Dubbo的注册中心,或因为觉得Nacos比较香,想从Zookeeper迁移到Nacos,或因前段时间曝出Consul禁止在中国境内使用。

迁移注册中心的方案大致有两种:

  • 方案一:使用Dubbo提供的多注册中心能力,Provider先进行双注册,Consumer逐步迁移消费新注册中心,最后下线老注册中心。该方案的缺点是修改时有上下游依赖关系。

  • 方案二:使用一个同步工具把老注册中心的数据同步到新注册中心,Consumer逐步迁移到新注册中心,最后下线老注册中心。同步工具有开源的Nacos-sync,我之前的文章《zookeeper到nacos的迁移实践》就提到了这个方案。这个方案的缺点是架构变得复杂,需要解决同步数据的顺序性、一致性、同步组件的高可用等问题。

Nacos-sync 参考 https://github.com/nacos-group/nacos-sync

我们从「业务方成本」和「基础架构成本」两个角度考虑一下这两个方案:

业务方成本我们以每次业务方修改并上线代码为1个单位,基础架构成本以增加一个新服务端组件为2个单位,从复杂度上来说基础架构成本远远高于业务方修改并上线代码,但这里我们认为只是2倍关系,做过基础组件开发的同学肯定感同身受,推动别人改代码比自己埋头写代码要难。

我们统计下上述方案中,迁移一对Consumer和Provider总共需要的成本是多少:

  • 方案一:Provider双注册+1;Consumer消费新注册中心+1;Provider下线旧注册中心+1;总成本为3
  • 方案二:同步组件+2;Consumer消费新注册中心+1;Provider下线旧注册中心+1;总成本为4

有没有成本更低的方案?

首先我们不考虑引入同步组件,其次Provider和Consumer能否不修改就能解决?我觉得理论上肯定可以解决,因为Java的字节码是可以动态修改的,肯定能达到这个目的,但这样的复杂度和风险会非常高。

退一步能否每个应用只修改发布一次就完成迁移?

Dubbo配置多注册中心可以参考这篇文章《几个你不知道的dubbo注册中心细节》,你会发现多注册中心是通过配置文件配置的,如下

dubbo.registries.zk1.address=zookeeper://127.0.0.1:2181
dubbo.registries.zk2.address=zookeeper://127.0.0.1:2182

只修改一次代码,就必须把这个配置变成动态的,有点难,但不是做不到,可在应用启动时远程加载配置,或者采取替换配置文件的方式来达到目的。

但这只解决了部分问题,还有两个问题需要解决:

  • Dubbo注册、订阅都发生在应用启动时,应用启动后就没法修改了。也不是完全不能,如果采用了api的方式接入Dubbo可以通过改代码来实现,但几乎这种方式不会被采用;
  • Dubbo消费没法动态切换,多注册中心消费时,Dubbo默认的行为是挑第一个可用的注册中心进行调用,无法主动地进行切换;如果实现了主动切换还有个好处是稳定性提高了很多,万一新注册中心出现问题还可以及时切回去。

这里针对第二点的消费逻辑做一点简单说明,老版本(<2.7.5)逻辑比较简单粗暴,代码位于RegistryAwareClusterInvoker

  1. 挑选第一个可用的注册中心进行调用

新版本(>=2.7.5)则稍微丰富一点,代码位于ZoneAwareClusterInvoker

  1. 挑选一个可用且带preferred偏好配置的注册中心进行调用,注意这个偏好配置不同版本key还不一样,有点坑
  2. 如果都不符合1,则挑选同一个分区且可用的注册中心进行调用,分区也是通过参数配置,这个主要是为了跨机房的就近访问
  3. 如果1、2都不符合,通过一个负载均衡算法挑选出一个可用的注册中心进行调用
  4. 如果1、2、3都不合法,则挑选一个可用的注册中心
  5. 如果上述都不符合,则使用第一个注册中心进行调用

可以看出新版本功能很丰富,但它是有版本要求的,而且控制的key也变来变去,甚至去搜一下也有Bug存在,所以如果是单一稳定的高版本是可以通过这个来做,但大部分还是达不到这个要求。

很长一段时间以来,我都没有想到一个好的办法来解决这个问题,甚至我们公司内部有直接修改Dubbo源码来实现动态切换消费的能力,但这种入侵修改无法持续,直到有一天浏览Dubbo源码时,无意间看到了MultipleRegistry,仿佛发现了新大陆,用醍醐灌顶来形容一点不为过。

MultipleRegistry,有点意思!

MultipleRegistry是Dubbo 2.7.2引入的一个注册中心扩展,注册中心扩展圈起来,要考!意味着这个扩展可以在任何>=2.7.0版本上运行,稍微改改也能在2.7以下的版本使用

这究竟是个什么注册中心的扩展呢?

实际上这个扩展并不是一个实际的注册中心的扩展,而是一个包装,它本身不提供服务注册发现的能力,它只是把其他注册中心聚合起来的一个空壳。

为什么这个「空壳」这么厉害呢?下面我们就来分析分析源码。

由于刚好手上有3.0.0版本的源码,所以接下来的源码分析基于Dubbo 3.0.0版本,也不用担心版本问题,这个扩展自从2.7.2引入之后几乎没有大的改动,只有Bugfix,所以什么版本基本都差不多。只分析接口级服务发现,应用级的暂时不分析,原理类似。

不过在讲源码之前,还得说说Dubbo注册中心插件的运行原理,否则源码可能看不懂,我们以开发一个注册中心扩展为例:

  1. Dubbo注册中心扩展需实现RegistryService和RegistryFactory接口
public interface RegistryService {

    void register(URL url);

    void unregister(URL url);

    void subscribe(URL url, NotifyListener listener);

    void unsubscribe(URL url, NotifyListener listener);

    List<URL> lookup(URL url);
}

这里的五个接口分别是注册、注销、订阅、取消订阅、查询,在Dubbo应用启动时会调用这些接口。

都比较好理解,需要提一下subscribe接口。

subscribe传入了一个NotifyListener参数,可以理解为一个回调,当监听的的URL发生变化时,调用这个NotifyListener通知Dubbo。

public interface NotifyListener {
void notify(List<URL> urls);
}

NotifyListener也是个接口,只有一个notify方法,这个方法传入的参数是所消费的URL的所有Provider列表。

@SPI("dubbo")
public interface RegistryFactory {
@Adaptive({"protocol"})
Registry getRegistry(URL url);
}

RegistryFactory是描述了如何创建Registry扩展的工厂类,URL就是配置中

zookeeper://127.0.0.1:2181
  1. 还需要遵守Dubbo SPI的加载规则扩展才能被正确加载

这些内容官方文档中说的比较清楚,如果有疑问可以看看Dubbo的官方文档说明。

简单介绍到此结束,接下来重点介绍MultipleRegistry

首先看初始化,代码只挑出重点,在初始化MultipleRegistry时,分别对注册和订阅的注册中心进行初始化,这些注册中心来自MultipleRegistry的URL配置,URL上的key分别为service-registryreference-registry,实际测试下来URL的参数中带奇怪的字符会导致编译不通过,不过这并不是重点,基本的还是可用,而且也不一定要采用这种配置。

public MultipleRegistry(URL url, boolean initServiceRegistry, boolean initReferenceRegistry) {
...
Map<String, Registry> registryMap = new HashMap<>();
// 初始化注册的注册中心
if (initServiceRegistry) {
initServiceRegistry(url, registryMap);
}
// 初始化订阅的注册中心
if (initReferenceRegistry) {
initReferenceRegistry(url, registryMap);
}
...
}

我们再看注册和订阅:

注册比较简单,只需要对刚刚初始化的serviceRegistries都进行注册即可

 public void register(URL url) {
super.register(url);
for (Registry registry : serviceRegistries.values()) {
registry.register(url);
}
}

订阅时也是针对referenceRegistries的每个注册中心都订阅,但这里有个不同的点是NotifyListener的妙用。

 public void subscribe(URL url, NotifyListener listener) {
MultipleNotifyListenerWrapper multipleNotifyListenerWrapper = new MultipleNotifyListenerWrapper(listener);
multipleNotifyListenerMap.put(listener, multipleNotifyListenerWrapper);
for (Registry registry : referenceRegistries.values()) {
SingleNotifyListener singleNotifyListener = new SingleNotifyListener(multipleNotifyListenerWrapper, registry);
multipleNotifyListenerWrapper.putRegistryMap(registry.getUrl(), singleNotifyListener);
registry.subscribe(url, singleNotifyListener);
}
super.subscribe(url, multipleNotifyListenerWrapper);
}

先用MultipleNotifyListenerWrapper把最原始的NotifyListener包装起来,NotifyListener传给每个被包装的注册中心。MultipleNotifyListenerWrapper和SingleNotifyListener分别是什么?

MultipleNotifyListenerWrapper将原始的NotifyListener进行包装,且持有SingleNotifyListener的引用,它提供了一个方法notifySourceListener的方法,将持有的SingleNotifyListener中上次变更的URL列表进行merge后调用最原始的NotifyListener.notify()

protected class MultipleNotifyListenerWrapper implements NotifyListener {

    Map<URL, SingleNotifyListener> registryMap = new ConcurrentHashMap<URL, SingleNotifyListener>(4);
NotifyListener sourceNotifyListener;
... public synchronized void notifySourceListener() {
List<URL> notifyURLs = new ArrayList<URL>();
URL emptyURL = null;
for (SingleNotifyListener singleNotifyListener : registryMap.values()) {
List<URL> tmpUrls = singleNotifyListener.getUrlList();
if (CollectionUtils.isEmpty(tmpUrls)) {
continue;
}
// empty protocol
if (tmpUrls.size() == 1
&& tmpUrls.get(0) != null
&& EMPTY_PROTOCOL.equals(tmpUrls.get(0).getProtocol())) {
// if only one empty
if (emptyURL == null) {
emptyURL = tmpUrls.get(0);
}
continue;
}
notifyURLs.addAll(tmpUrls);
}
// if no notify URL, add empty protocol URL
if (emptyURL != null && notifyURLs.isEmpty()) {
notifyURLs.add(emptyURL);
}
this.notify(notifyURLs);
}
...
}

再看SingleNotifyListener,它的notify去调用MultipleNotifyListenerWrapper的notifySourceListener

class SingleNotifyListener implements NotifyListener {

    MultipleNotifyListenerWrapper multipleNotifyListenerWrapper;
Registry registry;
volatile List<URL> urlList; @Override
public synchronized void notify(List<URL> urls) {
this.urlList = urls;
if (multipleNotifyListenerWrapper != null) {
this.multipleNotifyListenerWrapper.notifySourceListener();
}
}
...
}

仔细思考我们发现:

  • MultipleNotifyListenerWrapper是个注册中心扩展的包装,它本身是没有通知能力的,只能借助的真实注册中心扩展的通知能力
  • SingleNotifyListener是真实的注册中心的通知回调,由它去调用MultipleNotifyListenerWrapper的notifySourceListener,调用前可将数据进行merge

如果你仔细读完上面的文章你会发现,这不就是包装了一下注册中心扩展吗?就这?哪里醍醐灌顶了?

不着急,我们先扒一扒作者为什么写这样一个扩展,他的初衷是想解决什么问题?

参考这个issue:https://github.com/apache/dubbo/issues/3932

作者说:我们可以在程序运行时下线(注销)服务,如果有个Dubbo服务同时注册了Zookeeper和Nacos,而我只想注销其中一个注册中心,MultipleRegistry就可以解决这种场景。

作者的初衷很简单,但当我看到这个实现时,灵光乍现,感觉这个实现如果稍微改一改,简直就是一个Dubbo多注册中心迁移神器

Dubbo多注册中心迁移神器

Dubbo多注册中心迁移神器具有什么样的特性?

  • 可以动态(远程配置)地注册到一个或多个注册中心,且在程序不重启的情况下可以动态调整
  • 可以动态(远程配置)地消费某一个或多个注册中心,同样可以在程序不重启的情况下可以动态调整
  • 消费有兜底逻辑,比如配置了消费Zookeeper,但Zookeeper上可能只有A服务,B服务不存在,那么调用B服务时可以用其他注册中心的Provider来兜底,这就保证了注册中心迁移过程中没用上下游的依赖

如果上面说的能够领会到,这些需求实现起来就很简单:

  • 启动时,Provider和Consumer都分别监听对应的配置项,按需注册和消费,目前MultipleRegistry已经实现
  • Dubbo应用运行中,配置项变更事件驱动
    • Provider:触发一个重新注册、注销的事件,根据最新的配置项将需要注册的注册中心再注册一遍,需要注销的注册中心注销
    • Consumer:触发重新进行订阅和取消订阅,
  • 消费兜底逻辑,将MultipleNotifyListenerWrapper中的notifySourceListener的merge逻辑进行重写,可以实现有线消费、无对应Provider兜底消费。当然如果配置变更也需要触发一次notify

按照这个思路,我已经实现了一个版本在线上跑了起来!不过耦合了公司内部的配置中心。

如果想不耦合,可以采用Dubbo SPI扩展的方式来扩展「读取监听配置变更部分」,扩展中的扩展,有点骚~

这篇文章有点长,最后来回顾一下讲了啥:

首先文章从一个Dubbo注册中心迁移成本的问题讲起,现有的方案成本都是比较高,一直苦苦找寻更低成本、兼容性更强的方案。终于在一次浏览Dubbo源码过程中发现了MultipleRegistry源码,经过研究发现只需要经过稍微的修改就能符合我们对完美动态注册中心的定义。

在我写这篇文章的时候,又试图搜索了一下Dubbo动态注册中心,发现了「Kirito的技术分享」的一篇文章《平滑迁移 Dubbo 服务的思考》提到了阿里云的一个产品的实现和上文提到的方案类似。

如果刚好你也有这个需求,可以用上文的思路实现看看,并不复杂,是不是感觉赚了一个亿。

搜索关注微信公众号"捉虫大师",后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。

这个Dubbo注册中心扩展,有点意思!的更多相关文章

  1. 灵感乍现!造了个与众不同的Dubbo注册中心扩展轮子

    hello大家好呀,我是小楼. 作为一名基础组件开发,服务好每一位业务开发同学是我们的义务(KPI). 客服群里经常有业务开发同学丢来一段代码.一个报错,而我们,当然要微笑服务,耐心解答. 有的问题, ...

  2. ZooKeeper 集群的安装、配置---Dubbo 注册中心

    ZooKeeper 集群的安装.配置.高可用测试 Dubbo 注册中心集群 Zookeeper-3.4.6 Dubbo 建议使用 Zookeeper 作为服务的注册中心. Zookeeper 集群中只 ...

  3. dubbo注册中心zookeeper出现异常 Opening socket connection to server 10.70.42.99/10.70.42.99:2181. Will not attempt to authenticate using SASL (无法定位登录配置)

    linux下,zookeeper安装并启动起来了 DEMO时,JAVA控制台出现: INFO 2014-03-06 09:48:41,276 (ClientCnxn.java:966) - Openi ...

  4. Dubbo框架介绍与安装 Dubbo 注册中心(Zookeeper-3.4.6)

    背景 随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进. • 单一应用架构 • 当网站流量很小时, ...

  5. 2016年工作中遇到的问题41-50:Dubbo注册中心奇葩问题,wifi热点坑了

    41.获得JSON中的变量.//显示json串中的某个变量,name是变量名function json(json,name){ var jsonObj = eval(json); return jso ...

  6. dubbo注册中心占位符无法解析问题

    dubbo注册中心占位符无法解析问题 1.背景 最近搞了2个老项目,想把他们融合到一起.这俩项目情况简介如下: 项目一:基于SpringMVC + dubbo,配置读取本地properties文件,少 ...

  7. dubbo注册中心占位符无法解析问题(二)

    dubbo注册中心占位符无法解析问题 前面分析了dubbo注册中心占位符无法解析的问题. 并给出了2种解决办法: 降低mybatis-spring的版本至2.0.1及以下 自定义MapperScann ...

  8. 几个你不知道的dubbo注册中心细节

    你会正确配置backup地址吗? 在配置dubbo注册中心时,一般会这样写 dubbo.registry.protocol=zookeeper dubbo.registry.address=127.0 ...

  9. 基于ZooKeeper的Dubbo注册中心

    SOA服务治理 dubbo_zk 服务总线 感兴趣的M我微信:wonter 微信扫描,人人 CTO 大本营 基于SOA架构的TDD测试驱动开发模式 服务治理要先于SOA 简述我的SOA服务治理 从页面 ...

随机推荐

  1. MySQL客户端mysql常用命令

    通过MySQL自带的mysql命令行工具, 执行MySQL的相关命令. 1.连接MySQL服务端 mysql -uUserName -pPassword -h HostName_IP -P 3306 ...

  2. 浅议像素化与体素化Part.1——平面图形像素化

    什么是像素化 学计算机的人往往都比较清楚图形和图像的区别,而且往往能够从数据结构的角度理解这两者的区别,一般来说,图形是由几何空间中的基本图元所组成,表现为用外部轮廓线条勾勒成的矢量图.例如由计算机绘 ...

  3. nalu,在java中使用lambda查询数据库

    不忘初心 最开始接触写代码的时候,用的是C井,查数据库直接硬编码sql,挺难受的. 后来学习到EntityFramework,用起来是真香,都是强类型,各种智能提示,代码写起来极度舒适,效率起飞. 最 ...

  4. update sql时,常记错同时更新多个参数用and,正确是用逗号

    记录一下,经常记错的一个点,在update多个参数时,多个参数之间用and连接,这个时候,语句就会报错了 其实,正确的是用逗号隔开, 使用SQL中的update更新多个字段值,set后面的条件要用逗号 ...

  5. Python3.7 比较两个Excel文件指定列的值的异同,并将核对结果写入Excel中(含升级版本)

    背景: 最近工作中需要核对客户的历史数据, 接近400个产品,需要核对的列有15列,行数有8000+行 肉眼核对简直要吐血 心想着反正在学python呢 人生苦短 何不用python写个脚本 帮助我核 ...

  6. Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=256M; support was removed in 8.0

    目录 启动一个Java Standalone程序时报错 解决办法 解释 参考 启动一个Java Standalone程序时报错 Java HotSpot(TM) 64-Bit Server VM wa ...

  7. zabbix监控图形中文乱码的解决方法

    问题描述: 最近搭建了一套zabbix,当我把语言切换到中文的时候,发现监控的图形界面中一些中文参数乱码,但是图形界面在英文环境下完全没有乱码问题.如下图(中文界面): 解决方法: 解决方法有两种,方 ...

  8. java 线程 总结

    1.前言 (1)线程的上一级是进程,进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的. (2)线程与进程相似,但线程是一个比进程更小的执行单位,也被称为轻量级进程.一个进程在其执行 ...

  9. ElasticSearch的应用

    一.介绍 全文检索技术: 分布式: Restful风格: 近实时搜索 二.部署 下载:https://thans.cn/mirror/elasticsearch.html 新建用户,并登录: 解压: ...

  10. Teamcenter无法创建多余账号怎么办?

    西门子的产品Teamcenter,用户账号的许可是命名的许可类型,数量是限定的:例如,账号许可购买了25个,那么活动账号已经达到25了,再创建第26个账号将无法创建.没办法创建多余的账号,怎么办? 当 ...