当前位置: 首页 > 科技观察

这个Dubbo注册中心扩展很有意思!

时间:2023-03-19 12:24:23 科技观察

大家好~我是小楼。其实这篇文章早就写好了。本来上班第一天就发出来的,一直拖到现在。是因为发烧躺了3天,今天上班比较好,晚上过来打最后一瓶点滴。生病真的很痛苦,大家要多休息,多运动,保持良好的抵抗力~今天想和大家聊聊在Dubbo源码中实现的一个registry扩展。很特别,也帮我解决了一个困扰我很久的问题。刚在生产中使用,效果很好。我迫不及待地想和你分享。Dubbo的扩展性非常灵活,无需侵入源代码即可加载自定义扩展。它可以扩展协议、序列化方法、注册中心、线程池、过滤器、负载均衡策略、路由策略、动态代理等,甚至“扩展本身”也可以扩展。在介绍今天的注册机扩展之前,先提一个问题供大家思考。如何低成本迁移注册中心?有时候因为各种原因需要迁移Dubbo注册中心,或者因为Nacos比较香,想从Zookeeper迁移到Nacos,或者因为前段时间Consul在国内被禁止使用。迁移注册中心大致有两种方案:方案一:利用Dubbo提供的多注册中心能力,Provider先进行双注册,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:同步组件+2;Consumer消费新注册中心+1;Provider离线旧注册中心+1;总成本是4。有没有成本更低的解决方案?首先,我们不考虑同步组件的引入,其次,Provider和Consumer可以不修改就解决吗?我觉得理论上是可以解决的,因为Java的字节码是可以动态修改的,这个目的肯定可以达到,但是复杂度和风险会很高。退一步说,难道每个应用只修改发布一次就可以完成迁移吗?dubbo配置多注册中心可以参考本文《几个你不知道的dubbo注册中心细节》,你会发现多注册中心是通过配置文件来配置的,如下dubbo.registries.zk1.address=zookeeper://127.0.0.1:2181dubbo。registries.zk2.address=zookeeper://127.0.0.1:2182只修改一次代码,需要把这个配置做成动态的,有点难,但不是不可能,可以在应用启动的时候远程加载配置,或者可以通过替换配置文件来达到目的。但这仅仅解决了部分问题,还有两个问题需要解决:Dubbo注册和订阅都是在应用启动时发生的,应用启动后无法修改。这根本不是不可能的。如果使用api方式接入Dubbo,可以改代码实现,但几乎不会用到这种方式;Dubbo消费不能动态切换。多个注册中心消费时,Dubbo默认行为是选择第一个可用的注册中心调用,不能主动切换;如果实现主动切换,稳定性会提高很多,万一新的注册中心出现问题,可以及时切换回来。这里简单说明一下第二点的消费逻辑。老版本(<2.7.5)的逻辑比较简单粗暴,代码位于RegistryAwareClusterInvoker:选择第一个可用的注册中心调用新版本(>=2.7.5)更丰富一点,代码是位于ZoneAwareClusterInvoker:选择一个可用的preferencepreference配置的注册中心调用,注意这个preference配置的key在不同的版本是不一样的,有点坑,如果不匹配1,选择相同的partition和可用注册中心调用,分区也是通过参数配置的。这个主要是跨机房就近访问。如果1和2不匹配,则通过负载均衡算法选择一个可用的注册中心进行调用。如果1、2、3不匹配,则选择可用的注册中心。如果以上都不满足,请使用第一注册中心调用。可见新版本功能丰富,但有版本要求,控制键也有变化。就算搜索也有bug,所以如果是单一的稳定高版本,可以通过这个来搞定,但是大部分还是达不到这个要求。很长一段时间,我都没有想到解决这个问题的好方法。甚至我们公司也有能力直接修改Dubbo源码实现动态切换消费,但是这种侵入式的修改不能持续到有一天我浏览了Dubbo源码。当时无意中看到了MultipleRegistry,仿佛发现了一个新世界。用开悟来形容也不为过。MultipleRegistry,有趣!MultipleRegistry是Dubbo2.7.2引入的注册中心扩展。注册中心扩展已圈测!意思就是说这个扩展可以在>=2.7.0的任何版本上运行,稍加修改也可以在2.7上运行。下面的版本用的是什么注册表扩展?事实上,这个扩展并不是对实际注册表的扩展,而是一个包装器。它不提供发现服务注册的能力,它只是聚合了其他注册中心而升起的一个空壳。为什么这个“空壳”这么厉害?下面我们来分析分析源码。由于手头正好有3.0.0版本的源码,下面的源码分析都是基于Dubbo3.0.0版本的,不用担心版本问题。这个扩展自2.7.2推出以来几乎没有什么变化,只有bugfix,所以什么版本都基本一样。只分析接口层的服务发现,应用层的服务发现暂不分析。原理类似。不过在说源码之前,我们得先说说Dubbo注册中心插件的运行原理,不然可能看不懂源码。以开发注册中心扩展为例:Dubbo注册中心扩展需要实现RegistryService和RegistryFactory接口publicinterfaceRegistryService{voidregister(URLurl);无效注销(网址网址);voidsubscribe(URLurl,NotifyListener监听器);voidunsubscribe(URLurl,NotifyListener监听器);Listlookup(URLurl);}这里的五个接口分别是注册、注销、订阅、取消订阅、查询,这些接口在Dubbo应用启动时都会被调用。都是比较容易理解的,还要提到订阅接口。subscribe传入一个NotifyListener参数,可以理解为回调。当监听的URL发生变化时,调用该NotifyListener通知Dubbo。publicinterfaceNotifyListener{voidnotify(Listurls);}NotifyListener也是一个只有一个通知方法的接口。该方法传入的参数是消费URL的所有Provider列表。@SPI("dubbo")publicinterfaceRegistryFactory{@Adaptive({"protocol"})RegistrygetRegistry(URLurl);}RegistryFactory是一个工厂类,描述了如何创建Registry扩展,URL为zookeeper://127.0在配置中。0.1:2181也需要遵守DubboSPI加载规则扩展才能正确加载。这些内容官方文档比较清楚。有问题可以参考Dubbo官方文档。简介到此结束,接下来重点介绍MultipleRegistry。先看初始化。代码只挑出关键点。初始化MultipleRegistry时,分别初始化注册和订阅的注册中心。这些注册表来自MultipleRegistry的URL配置。URL上的键分别是service-registry和reference-registry。经测试,url的参数中出现奇怪的字符会导致编译失败,但这不是重点,基本的还是可以的,这个配置不是必须的。publicMultipleRegistry(URLurl,booleaninitServiceRegistry,booleaninitReferenceRegistry){...MapregistryMap=newHashMap<>();//初始化注册中心if(initServiceRegistry){initServiceRegistry(url,registryMap);}//初始化订阅注册中心if(initReferenceRegistry){initReferenceRegistry(url,registryMap);}...}再来看注册和订阅:注册比较简单,只需要注册所有刚刚初始化的serviceRegistriespublicvoidregister(URLurl){super.register(url);for(Registryregistry:serviceRegistries.values()){registry.register(url);}}订阅的时候也是订阅referenceRegistries的各个注册中心,不过这里不同的是NotifyListener的妙用。publicvoidsubscribe(URLurl,NotifyListenerlistener){MultipleNotifyListenerWrappermultipleNotifyListenerWrapper=newMultipleNotifyListenerWrapper(listener);multipleNotifyListenerMap.put(监听器,multipleNotifyListenerWrapper);for(Registryregistry:referenceRegistries.values()){SingleNotifyListenersingleNotifyListener=newSingleNotifyListener(multipleNotifyListenerWrapper,registry);multipleNotifyListenerWrapper.putRegistryMap(registry.getUrl(),singleNotifyListener);registry.subscribe(url,singleNotifyListener);}super.subscribe(url,multipleNotifyListenerWrapper);MultipleNotifyListenerWrapper和SingleNotifyListener分别是什么?MultipleNotifyListenerWrapper包装了原始的NotifyListener并持有对SingleNotifyListener的引用。它提供了一个方法notifySourceListener合并持有的SingleNotifyListener中最后更改的URL列表并调用原始NotifyListener.notify()保护类MultipleNotifyListenerWrapperimplementsNotifyListener{MapregistryMap=newConcurrentHashMap(4);NotifyListenersourceNotifyListener;...publicsynchronizedvoidnotifyListenURL=newURL>ArrayList();网址空网址=空;对于(SingleNotifyListenersingleNotifyListener:registryMap.values()){ListtmpUrls=singleNotifyListener.getUrlList();如果(CollectionUtils.isEmpty(tmpUrls)){继续;}//空协议if(tmpUrls.size()==1&&tmpUrls.get(0)!=null&&EMPTY_PROTOCOL.equals(tmpUrls.get(0).getProtocol())){//如果只有一个为空if(emptyURL==null){emptyURL=tmpUrls.get(0);}继续;}notifyURLs.addAll(tmpUrls);}//如果没有通知URL,添加空协议URLif(emptyURL!=null&¬ifyURLs.isEmpty()){notifyURLs.add(emptyURL);}this.notify(notifyURLs);}...}再看SingleNotifyListener,它的notify去调用MultipleNotifyListenerWrapper的notifySourceListenerclassSingleNotifyListenerimplementsNotifyListener{MultipleNotifyListenerWrappermultipleNotifyListenerWrapper;注册登记处;易失性列表urlList;@Overridepublicsynchronizedvoidnotify(Listurls){this.urlList=urls;if(multipleNotifyListenerWrapper!=null){this.mmultipleNotifyListenerWrapper.notifySourceListener();}}...}仔细思考,我们发现:MultipleNotifyListenerWrapper是一个注册表扩展包装器,它本身没有通知能力,只能依赖真正的注册表扩展的通知能力。SingleNotifyListener是一个真正的注册表通知回调,用于调用MultipleNotifyListenerWrapper的notifySourceListener,调用前可以合并数据。仔细阅读上面的文章,你会发现这不就是注册中心插件的一个包吗?就是这样?开悟在哪里?别着急,我们来看看作者为什么要写这样的扩展。他原本打算解决什么问题?参考这个问题:https://github.com/apache/dubbo/issues/3932作者说:我们可以在运行离线(注销)服务的时候,如果有一个dubbo服务同时注册了Zookeeper和Nacos,而我只想注销其中一个注册中心,MultipleRegistry可以解决这种情况。作者的初衷很简单,但是当我看到这个实现的时候,灵感就来了,感觉如果这个实现稍微改一下,就是Dubbo多注册中心迁移的神器。Dubbo多注册中心迁移神器Dubbo多注册中心迁移神器有哪些特点?可动态(远程配置)注册到一个或多个注册中心,无需重启程序即可动态调整。动态(远程配置))消耗一个或多个注册中心,也可以在不重启程序的情况下动态调整消耗。有一个自下而上的逻辑。比如配置了Zookeeper消费,但是Zookeeper上可能只有A服务,B服务不存在,那么调用B服务就可以使用其他注册中心的Provider,这样就保证了不使用上下游依赖在注册表迁移过程中。分别监控相应的配置项,按需注册和消费。目前MultipleRegistry已经实现了Dubbo应用运行。配置项变更事件驱动Provider:触发重新注册或注销事件,根据最新的配置项重新注册需要注册的注册中心。注册一次,需要注销的注册中心注销Consumer:触发重新订阅和取消订阅,消费逻辑,重写MultipleNotifyListenerWrapper中notifySourceListener的合并逻辑,可以实现有线消费和非对应Provider消费。当然,如果配置发生变化,就需要触发一个notify。按照这个思路,我实现了一个在线运行的版本!但是,加上公司内部的配置中心。如果想解耦,可以使用DubboSPI扩展扩展“读监控配置变更部分”。引中引有点风骚~本文有点长。最后,回顾一下我们聊的内容:首先,文章从一个Dubbo说起注册中心迁移的成本,现有的方案成本都比较高,我们一直在努力寻找成本更低、兼容性更好的方案.最后在浏览Dubbo源码的过程中找到了MultipleRegistry的源码。经过研究,我发现只需要稍微修改一下就可以满足我们定义的完美动态注册表。写这篇文章的时候,又尝试去搜索Dubbo动态注册中心,发现一篇文章《桐人技术分享》《平滑迁移 Dubbo 服务的思考》提到了阿里云某产品的实现,和上面提到的解决方案类似。如果你恰好有这个需求,可以使用上面的思路来实现。这并不复杂。有没有赚到一个亿的感觉?