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

灵感来袭!打造了有特色的Dubbo注册中心扩展轮

时间:2023-03-20 01:25:16 科技观察

大家好,我是小楼。作为基础组件开发人员,服务好每一位业务开发同学是我们的义务(KPI)。在客服群里,经常有业务开发的同学丢一段代码,报错,我们当然要微笑服务,耐心解答。有些问题根据多年踩坑的经验,一眼就能看出来。有的问题看代码一眼就能看出来,有的问题光看是看不出来的。这个时候只能下载代码在本地运行。熟悉我的朋友都知道,我从事dubbo相关的开发(客户)交付(服务)工作多年,所以说下dubbo排错过程中的一件有趣的事情。通常遇到解决不了的问题,先git拉代码,然后导入IDEA,找到main方法点击开始,运行一小会儿,肯定会出现一个小错误,比如这个:Socketerroroccurred:localhost/127.0.0.1:2181:Connectionrefused看到2181端口就知道本地没有安装zookeeper(以下简称zk)。问题不大。Docker直接拉取一个zk镜像,启动一个容器。有了这样的习惯积累,etcd、redis、mysql等容器已经在低端Mac上跑起来了,重要的是开了N个IDEA窗口。每当启动新项目时,风扇呼呼作响,直接卡死IDEA。这时候我陷入了思考,能不能少跑点程序呢?etcd、redis、mysql暂时不确定,不过dubbo的注册中心我比较熟悉!当然,柿子一定要软。在开始梳理需求之前,得先梳理一下需求,于是脑海中闪过无数瞬间在本地测试时遇到的dubbo注册中心相关的问题,但仔细一看,不外乎两个类型:作为提供者:最重要的是不要阻止应用程序启动。作为消费者:不要阻止应用程序启动。可以发现并呼叫本地提供商。可以调用远程提供程序。可以手动指定和调用任何提供程序。除了这两个功能需求,我们还要解决我们原来的问题:“不要依赖第三方服务”(比如zk)。从一开始研究的时候就想到了使用dubboregistryextension来实现这个功能。为了不重蹈覆辙,查看了dubbo源码,看看有没有对应的实现:发现除了dubbo-registry-multicast,都是依赖第三方提供的服务,那么这个multicast是什么?dubbo官方文档说的很清楚:乍一看满足了我们的需求,但是细细想来还是有几点不尽如人意:远程提供者可能找不到,如果Everyone的代码使用zk,如果下拉编码,把注册中心改成组播,就找不到远程服务了;您不能手动指定和调用任何提供商。产品设计服务的发现必须要有一个载体,要么通过第三方组件,要么通过互联网。但是我们忽略了,在本地,磁盘也可以作为载体。提供者注册写入磁盘文件,消费者订阅读取磁盘文件。当磁盘文件更改时,会通知消费者。看起来像这样:这种设计有什么好处?它不依赖于其他服务,只是读写文件。它将阻止应用程序启动。当消费者和提供者都在本地时,它们可以像其他注册中心(如zk、nacos等)一样工作,对开发者完全透明。您可以手动修改并指定调用任何提供商。唯一的缺点就是找不到remoteprovider,不过我们可以手动指定,没问题。我们的产品设计基于dubbo2.7.x版本的接口级服务发现,因为这个版本用的最多。首先要考虑的是如何组织服务发现文件。既然是接口级的服务发现,我们就以服务名作为文件名,每个服务都有一个文件:其次,每个文件的内容如何组织?最简单的方法就是使用dubbo注册的url直接写到文件里,一行一个url,像这样:但是你可能发现了一个问题,dubbo的url有点长~做起来会不会很困难如果我手动指定它呢?这个问题很容易解决,我们实现了一个简写版本的URL,比如如果有这样的简写行,就会恢复成可用的URL。127.0.0.1:20880代码实现在实现之前,首先要了解dubbo注册中心的扩展是怎么写的。这块,直接参考官方文档:https://dubbo.apache.org/zh/docs/v2.7/dev/impls/registry/虽然我觉得你以后可能无法实现dubboregistryextension阅读文档,但不要惊慌,先往下看,也许看完这篇文章你可以自己写一篇。先看代码结构:项目名称为:dubbo-registry-mock,与dubbo源码中的命名风格一致。MockRegistry是注册表的核心实现。MockRegistryFactory是mockregistry的工厂,dubbo会通过这个类创建MockRegistry。org.apache.dubbo.registry.RegistryFactory文件指定了如何加载MockRegistryFactory,即dubbo的SPI发现文件。dubbo的registry配置只需要改成:dubbo.registry.address=mock://127.0.0.1:2181这里只有mock起作用,ip和port不重要,只是占一个地方。dubbo应用启动时,读取配置的mock后,会在resources/META-INF.dubbo下寻找org.apache.dubbo.registry.RegistryFactory文件,其内容为:mock=org.newboo.MockRegistryFactory,所以去新建一个MockRegistryFactory。注:newboo.org是我曾经注册过用来托管博客的域名,后来没有续费,现在这个包名经常出现在我的测试代码中。MockRegistryFactory也很简单,直接新建一个MockRegistry:}}最后看核心实现MockRegistry类:publicMockRegistry(URLurl){super(url);StringbasePath=DISCOVERY_DEFAULT_DIR;如果(StringUtils.isNotEmpty(url.getParameter(DISCOVERY_FILE_DIR_KEY))){basePath=url.getParameter(DISCOVERY_FILE_DIR_KEY);}mockService=newMockService(basePath);ScheduledExecutorServicescheduledExecutorService=Executors.newScheduledThreadPool(1,newNamedThreadFactory("file_scan",true));scheduledExecutorService.scheduleWithFixedDelay(newSubscribeScan(),1000L,5000,TimeUnit.MILLISECONDS);}这个构造方法做了3件事:获取basePath,也就是服务发现文件夹的base路径,有默认值,也可以根据url的参数进行调整,比如:dubbo.registry.address=mock://127.0.0.1:2181?discovery_file=/tmp/mock-registry2new一个MockService,承载了核心的服务发现逻辑,后面会讲到。启动定时任务,每5秒扫描一次文件,查看文件是否有变化,如果有变化通知消费者,后面会详细介绍。MockRegistry继承自FailbackRegistry,只需要实现其doRegister、doUnregister、doSubscribe、doUnsubscribe、isAvailable方法即可。其中isAvailable是判断注册中心是否可用,我们直接返回true即可。doUnsubscribe就是退订,这里什么都不用做。我们将其余3个方法的逻辑封装在MockService中:@OverridepublicvoiddoRegister(URLurl){try{mockService.writeUrl(url);}catch(Throwablee){thrownewRpcException("注册失败"+url,e);}}@OverridepublicvoiddoUnregister(URLurl){try{mockService.removeUrl(url);}catch(Throwablee){thrownewRpcException("注销失败"+url,e);}}@OverridepublicvoiddoSubscribe(URLurl,NotifyListenerlistener){try{Listurls=mockService.getUrls(url.getServiceInterface());listener.notify(网址);}catch(ServiceNotChangeExceptionignored){}catch(Throwablee){thrownewRpcException("订阅失败"+url,e);}}writeUrl直接获取文件名,只需在文件中追加一行新的URL:publicvoidwriteUrl(URLurl)throwsIOException{StringfileName=pathCenter.getServicePath(url.getServiceInterface());//写入文件Stringline=url.toFullString();FileUtil.appendLine(fileName,line);}removeUrl首先读取文件,删除要注销的URL,然后覆盖剩余内容写回文件:publicvoidremoveUrl(URLurl)throwsIOException{StringfileName=pathCenter.getServicePath(url.getServiceInterface());字符串行=url.toFullString();Listlines=FileUtil.readLines(fileName);lines=LinesUtil.removeLine(lines,line);FileUtil.writeLines(fileName,lines);}getUrls扫描文件,如果文件有变化,格式化它读取的最新的URL并返回,格式化的原因是因为可能有缩写的URL(见上文),文件是否发生变化直接根据文件的最后更新时间判断,精确到毫秒,本地测试就可以了:publicListgetUrls(Stringservice)throwsException{if(!scan(service)){throw新的ServiceNotChangeException();}StringfileName=pathCenter.getServicePath(服务);Listlines=FileUtil.readLines(fileName);列表urls=newArrayList<>(lines.size());for(Stringline:lines){if(!LinesUtil.isSkipLine(line)){urls.add(format(line));}}返回网址;}如果扫描返回false,说明文件没有变化,忽略本次扫描的最后一个SubscribeScan,直接取出subscribe接口,执行一次doSubscribe:publicclassSubscribeScanimplementsRunnable{@Overridepublicvoidrun(){try{//已经订阅的urlMap>subscribeds=getSubscribed();如果(订阅==空||订阅。isEmpty()){返回;}for(Map.Entry>entry:subscribes.entrySet()){for(NotifyListenerlistener:entry.getValue()){doSubscribe(entry.getKey(),listener);}}}catch(Throwablet){//ignore}}}看到这里有的同学可能会问,为什么监听文件变化需要轮询而不是WatchService呢?写的时候查了一下,调试了一下,发现WatchService真正的实现是PollingWatchService,而且也是用Polling来实现的,不信可以打开这个类看看。感觉和自己写的没什么区别,所以就自己写了。完整代码已经上传到github:https://github.com/lkxiaolou/dubbo-registry-mock。为了让这个项目看起来更丰满,我还写了一个README:最后,如果你耐心看完这篇文章,对dubbo有所了解,相信你已经可以自己写一个dubbo注册中心扩展了。如果你经常在本地做测试,也可以使用我写的mockregistry来试试。当然,代码和思路还有改进的余地。如果你有更好的想法,也可以和我交流。