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

为Dubbo贡献源码,梦寐以求的修bug

时间:2023-03-15 09:49:40 科技观察

本文转载请联系捕虫大师公众号。在上一篇《redis在微服务领域的贡献》中,通过一次面试的经历了解到redis在微服务中可以玩的这么流畅,也从源码的角度分析了dubbo的redis注册中心。最后得出的结论是dubbo的redis注册中心无法在生产中使用。原因如下:使用keys命令会阻塞单线程redis。按键执行过程中,其他命令需要排队,没有心跳检测功能。我测试了提供者被kill-9杀死后,消费者是毫无察觉的。但是从实现的角度来说,我们想通过存储的过期时间来判断服务是否可用,也就是需要将url对应的值与当前时间进行比较。如果过期了,应该去掉,但这部分好像不完整。然后我看了最新的代码。发现改进了第一点。使用扫描而不是键。可以简单理解为keys一次性查询redis中的所有key,页面中scan查询key,打通阻塞时间。当服务数量不是特别多的时候,是可以正常运行的,所以第二点还是没有解决。所以我想知道我是否可以优化它并为社区做出贡献?所以我就这么做了。二、先验证一下,步骤如下:使用redis注册中心,启动2个provider,然后启动1个consumer进行消费。为其中一位提供者杀死-9。观察消费者,会发现消费者请求会部分成功,有的会报错,有的会一直报错,不会恢复,也就是意外崩溃的提供者(logout逻辑不执行,可以模拟kill-9)不会从redis注册中心删除。为什么需要启动2个提供程序?因为dubbo在注册中心机制中有推送时保护,当推送提供者列表为空时,本次推送会被忽略。毕竟,不更新提供者总比失去提供者要好。分析解决注意到redis注册中心保存的数据是hash结构,key为url,value为过期时间127.0.0.1:6379>hgetall/dubbo/com.newboo.sample.api。DemoService/providers1)"dubbo://172.23.233.142:20881/com.newboo.sample.api.DemoService?anyhost=true&application=boot-samples-dubbo&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=com.newboo.sample.api.DemoService&metadata-type=remote&methods=sayHello&pid=19807&release=2.7.8&side=provider×tamp=1621857955355"2)"1621858734778"那么好办,能不能定时删除过期数据,通知消费者?又看了一下代码,发现这个思路已经实现了,在启动redis注册中心的时候,每过1/2过期时间就启动一个线程扫描this.expirePeriod=url.getParameter(SESSION_TIMEOUT_KEY,DEFAULT_SESSION_TIMEOUT);this.expireFuture=expireExecutor.scheduleWithFixedDelay(()->{try{deferExpired();//Extendtheexpirationtime}catch(Throwablet){//Defensivefaulttolerancelogger.error("Unexpectedexceptionoccuratdeferexpiretime,cause:"+t.getMessage(),t);}},expirePeriod/2,expirePeriod/2,TimeUnit.MILLISECONDS);每次扫描注册的服务都会被“更新”。这部分暂时不关心。如果是admin,会清理过期的注册信息,并通知privatevoiddeferExpired(){for(URLurl:newHashSet<>(getRegistered())){if(url.getParameter(DYNAMIC_KEY,true)){Stringkey=toCategoryPath(url);if(redisClient.hset(key,url.toFullString(),String.valueOf(System.currentTimeMillis()+expirePeriod))==1){redisClient.publish(key,REGISTER);}}}if(admin){clean();}}这里的admin什么时候为真?如果在订阅的时候订阅了*末尾的服务,那么admin设置为true,可能是dubbo控制台@OverridepublicvoiddoSubscribe(finalURURl,finalNotifyListenerlistener){...try{if(service.endsWith(ANY_VALUE)){admin=true;...}catch(Throwablet){...}}而在前面代码的clean方法中,有一行注释//Themonitoringcenterisresponsiblefordeletingoutdateddirtydata表示admin为true时,可能是监控中心?反正在生产中,很少有公司会使用开源的监控中心或者控制台,大部分都是自己修改或者自研的,这种系统稳定性是无法保证的。要是挂了,岂不是容易出故障。为什么不在消费者端进行服务探索?恰好redis会在推送订阅和更改时获取最新的数据,并在更新提供者时释放事件。如果缓存了这条数据,则检查数据是否每1/2过期时间,如果已经过期,则去redis中获取最新的数据进行检查(防止更新事件丢失)。如果真的过期了,就认为这个provider是不健康的。这个想法比较简单。10分钟写了个demo,用了上面的验证。方法已经验证过了,太好了,好久没有贡献源码给社区了,干脆就这样上传了,过两天就收到评论了,麻烦补充一些ut案例验证一下公关?优特?哦,原来是单元测试。忘了怎么玩开源社区了。我只相信测试代码,所以我去补单元测试。更何况测试比代码难多了。注册中心的通知机制还是异步回调,更难测试。想了一个巧妙的测试方法,自定义通知回调,将回调的内容保存在一个map中,然后在主线程中写一个循环查看。模拟服务被killed-9使用反射获取注册的服务并移除,这样就不会被续费了。解决办法总是比困难多。又过了两天收到评论请用英文评论emmm,忘记用英文了,又过了两天修改后收到评论IsitpossibleforexpireCachetogoleakingforit'snevercleared?expireCache用于caching把url和过期时间的map放在里面就好了,忘了清理,会造成内存泄漏。所以我添加了清理逻辑。这里还有一集。那天21-22点之间,我修复了内存泄漏的bug,写了一个单元测试。测试方法还是和之前一样,通知后主线程循环。本地测试没问题后提交到github。当时在github上编译失败,也没多想。毕竟dubbo项目太大,经常编译不通过。神奇的是那天晚上回去的时候梦见自己写的单元测试可能漏掉了break,导致测试没有及时跳出,所以本地编译成功了,但是github编译失败了(时间到)。第二天一早看到,真是错过了休息时间!!!又过了2天,我收到了评论。还有,doNotify.emm里面没有看到哪里用到了expireCache,看到这里,感觉他们没看懂代码,所以回复了expireCachemarkwhichservicemaybedownandcalldoNotitytofetchlatestdatafromredis.终于,过了几天,这个PR被合并了。