写点什么

排查 dubbo 接口重复注销问题,我发现了一个巧妙的设计

用户头像
捉虫大师
关注
发布于: 2021 年 04 月 28 日

背景

我在公司内负责自研的dubbo注册中心相关工作,群里经常接到业务方反馈 dubbo接口注销报错。经排查,确定是同一个接口调用了两次注销接口导致,由于我们的注册中心注销接口不能重复调用,调用第二次会因为实例已经注销而报实例找不到的错误。


虽然这个报错仅会打印一条错误日志,不影响业务,但本着 follow through 的精神,我决定还是一探究竟,更何况重复注销也增加了应用的结束时间,影响了发布回滚速度。

问题复现

拿到业务方的 dubbo 版本,基于开源2.7.3内部定制的一个版本,该版本修改主要涉及安全漏洞修复以及一些业务适配,写了个 demo 跑起来,然后kill,发现果然报错了。


为了确定不是内部修改导致的问题,用开源的 2.7.3 版本再次测试,发现还是报错。


同时为了确定这是一个 bug,我将 dubbo 版本修改为 2.7.7 做测试,发现该版本不再报错。


说明了重复注销至少是开源 dubbo 2.7.3 的一个 bug,在更高的 2.7.7 版本中已经被修复。


于是有了解决方案:升级 dubbo,但如果这么简单就没有这篇文章了。


  1. 内部的 dubbo 已经做了修改,想升级得把改动 merge 到新版本,比较费劲

  2. 就算升级了内部的 dubbo 版本,也不可能这么快速推动业务方升级


所以应该首先找到 bug 是哪里导致的,其次看注册中心的扩展是否可以修复这个问题,如果不能修复,就只能在内部的 dubbo 版本中修复该问题。

问题排查

怀疑 ShutdownHook


由于这几天研究过 ShutdownHook(见《ShutdownHook 原理》),第一时间怀疑 ShutdownHook 可能有问题。


dubbo 2.7.3 代码有关 ShutdownHook 的实现在DubboShutdownHook类,顺着代码梳理出如下关系



看到 dubbo 本身和 spring 都注册了 ShutdownHook,更加怀疑这里是不是 ShutdownHook 注册重复了。于是 debug 看看是否是注册重复了,这里给一个小经验,IntelliIDEA调试 ShutdownHook 执行时,要手动kill进程才会触发 debug,点 IDE 上的关闭按钮不会触发



DubboShutdownHook.doDestroy打上断点,debug 发现只会执行一次,这说明 spring 和 dubbo 的 ShutdownHook 只会注册一次,这是怎么实现的呢?经过很多次测试,发现了 dubbo 一个很牛逼的设计。


DubboShutdownHook 中有registerunregister方法,分别是注册和注销 ShutdownHook,在这两个方法上都打上断点,在程序启动时发现这样一个有趣的执行顺序:



总结一下是 dubbo 本身注册了 ShutdownHook,但如果用到了 spring 框架,spring 框架在初始化时注销了 dubbo 注册的 ShutdownHook,这样就只保留了 spring 的 ShutdownHook,真是秒啊!实现的代码只有这短短几行


public static void addApplicationContext(ApplicationContext context) {    CONTEXTS.add(context);    if (context instanceof ConfigurableApplicationContext) {        ((ConfigurableApplicationContext) context).registerShutdownHook();        DubboShutdownHook.getDubboShutdownHook().unregister();    }    BeanFactoryUtils.addApplicationListener(context, SHUTDOWN_HOOK_LISTENER);}
复制代码


于是怀疑的 ShutdownHook 问题被证明没有任何问题了。


从注销堆栈继续排查


能稳定复现的问题一定很好排查,借助 IDE 的 debug 来看两次注销的调用堆栈,在注册中心扩展的 unregister 方法处加断点,可以看到如下两次来源不同的堆栈信息




代码中体现是



也就是说一次 ShutdownHook 执行,触发了两次注销。


接下来就比较好排查了,一步一步 debug,这里解释下


  • AbstractRegistryFactory.destroyAll()是销毁所有注册中心,销毁时会调研注册中心的注销接口

  • destroyProtocols是销毁所有的protocol,注册中心的 protocol 在销毁时拿到 registry,然后调用了 registry 的注销接口


那么 dubbo 2.7.7 是如何避免这个问题的呢?


在 dubbo 2.7.7 的代码中,注册中心的 protocol 在销毁时获取注册中心稍微增加了点代码



原来在注册中心被销毁后,destroyed 变量被置为 true,从而在 registry protocol 再次获取注册中心时,已经拿不到了原先的注册中心了,拿到的是一个空的注册中心,调用注销,自然没有什么效果。


追溯了下 github,这次 PR 是


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


这个修复在2.7.5就已经修复了

总结

  • dubbo 重复注销问题存在于 2.7.0 ~ 2.7.4 版本,2.7.5 修复,zk 注册中心不会报错,可能无法感知,但它确实存在,也会拖慢应用的关闭速度

  • 通过追查发现,其实该问题可以在注册中心的扩展中解决,让 registry 的 destroy 只能被调用一次

  • 遇到无论多小的问题,有空都去钻研下,你会收货一些新知识,比如这次 dubbo 中 ShutdownHook 如此巧妙的设计




关于作者:公众号"捉虫大师"作者,专注后端的中间件开发,关注我,给你推送最纯粹的技术干货



发布于: 2021 年 04 月 28 日阅读数: 51
用户头像

捉虫大师

关注

还未添加个人签名 2018.09.19 加入

欢迎关注我的公众号“捉虫大师”

评论

发布
暂无评论
排查dubbo接口重复注销问题,我发现了一个巧妙的设计