https://mp.weixin.qq.com/s/QFwyP1IF8XTuS5aHaQ9o_Q
之前在《Dubbo一致性哈希负载均衡的源码和Bug,了解一下?》中写到了我发现了一个Dubbo一致性哈希负载均衡算法的Bug。
对于解决方案我是这样写的:
特别简单,把获取identityHashCode的方法从System.identityHashCode(invokers)修改为invokers.hashCode()即可。
此方案是我提的issue里面的评论,这里System.identityHashCode和 hashCode之间的联系和区别就不进行展开讲述了,不清楚的大家可以自行了解一下。
我说:这里System.identityHashCode和hashCode之间的联系和区别就不进行展开讲述了,不清楚的大家可以自行了解一下。
但是有读者在后台问我详细原因,我已经和他聊清楚了。
再加上这个BUG已于近期修复了,且只用了一行代码就修复了,那我就写一下解决方案,以及背后的原理。
即是对之前文章的一个补充,也是一个独立的知识点。
所以本文主要是回答下面这三个问题:
1.什么是System.identityHashCode?
2.什么是hashCode?
3.为什么一行代码就修复了这个BUG?
注:本文Dubbo源码2.7.4.1版本。如果阅读过《Dubbo一致性哈希负载均衡的源码和Bug,了解一下?》可以更好的理解这篇文章。但是没有读过也不会影响阅读。
前情回顾
先通过一个前情回顾,引出本文所要分享的内容。
Dubbo一致性哈希负载均衡算法的设计初衷应该是如果没有服务上下线的操作,后续请求根据已经映射好的哈希环进行处理,不需要重新映射。
然而我在研究其源码时,我发现实际情况是即使在服务端没有上下线操作的时候,一致性哈希负载均衡算法每次都需要重新进行hash环的映射。
实际情况与设计初衷不符。
于是给Dubbo提了一个issue,地址如下:
https://github.com/apache/dubbo/issues/5429
以下内容是对该issue的详细说明:
在Dubbo对应的源码中,只需要一行代码。就可以判断是否有服务上下线的操作:
就是下面这一行代码:
int identityHashCode = System.identityHashCode(invokers);
通过判断invokers(服务提供方List集合)的identityHashCode是否发生了变化,从而判断是否有服务上下线的操作。
但是这行代码,在Dubbo2.7.0版本之后就失效了。
问题出在Dubbo2.7.0版本引入的新特性之一:标签路由。
其对应的源码如下:
org.apache.dubbo.rpc.cluster.router.tag.TagRouter#filterInvoker
通过源码可以看出:在TagRouter中的stream操作,改变了invokers,导致每次调用时其System.identityHashCode(invokers)返回的值不一样。
所以每次调用都会进行哈希环的映射操作,在服务节点多,虚拟节点多的情况下一定会有性能问题。
该问题对应的PR链接如下:
https://github.com/apache/dubbo/pull/5440
修复方法也是特别简单:把获取identityHashCode的方法从System.identityHashCode(invokers)修改为invokers.hashCode()即可。如下图所示:
为什么一行代码就能修复?
为什么把获取identityHashCode的方法从System.identityHashCode(invokers)修改为invokers.hashCode()就可以了呢?
要回答这个问题,我们首先得明白什么是identityHashCode?什么是hashCode?
什么是identityHashCode?我们看看API里面的注释:
返回与默认方法hashCode()返回的给定对象相同的哈希码,无论给定对象的类是否覆盖了hashCode()。空引用的哈希码为零。
另外关于identityHashCode还有下面的三条规则:
1.所以如果两个对象A == B,那么A、B的System.identityHashCode() 必定相等;
2.如果两个对象的System.identityHashCode() 不相等,那他们必定不是同一个对象;
3.但是如果两个对象的System.identityHashCode() 相等,并不保证A==B,因为identityHashCode的底层实现是基于一个伪随机数实现的。
什么是hashCode?大家应该都比较熟了,还是看API上的注释:
再结合下面两个示例代码,深入理解。
示例一:WhyHashCodeDto没有重写hashCode()方法,所以identityHashCode和hashCode的值是一样的:
示例二:如下所示,String是重写了hashCode的方法,所以在下面的例子中identityHashCode不等于hashCode:
带入场景
有了前面的知识铺垫,我们就可以回到Dubbo的一致性哈希算法的场景中去了。
在PR中有一行注释是这样写的:
using the hashcode of list to compute the hash only pay attention to the elements in the list
我们应该只注意list里面的元素就可以了。而这个list里面的元素,就是一个个的服务提供方。
所以,在Dubbo的一致性哈希算法的场景中,我们只需要关心List里面的服务提供方是否有上下线的操作,而不关心这个List是否每次都是新的。
我们再回到源码中,结合源码,然后简化源码:
把上面的源码抽离一下,简化一下,如下:
filterInvoker方法是根据条件过滤invokers,并返回一个List。而我传入的条件是,过滤出invokers中invoker大于0的数据:
filterInvoker(invokers, invoker -> invoker > 0);
执行结果如下:
可以看到经过filterInvoker方法后,由于集合中所有的元素都满足条件,所以过滤前后,集合中的元素并没有发生变化,导致hashCode没有变化。但是由于装元素的容器(集合)已经不是原来的容器了,所以identityHashCode发生了变化。
"因为集合中的元素没有发生变化,导致hashCode没有变化。"这句话的理由是什么?
因为List重写了hashCode()方法,其算出的hashCode只和list中的元素相关:
经过filterInvoker方法后元素还是【1,2,3】,与过滤之前一样,所以hashCode没有变。
"由于装元素的容器(集合)已经不是原来的容器了,所以identityHashCode发生了变化。"这句话的理由又是什么?
可以看到在源码中,Collectors.toList()方法会new List。所以都是新的,那么每次的identityHashCode必不相同。
上面的示例代码,模拟的是没有服务上下线的操作。
接下来,我们模拟一下服务下线的场景:
这次传入的过滤条件为,过滤出invokers中invoker大于1的数据:
filterInvoker(invokers, invoker -> invoker > 1);
输出结果如下:
可以看到,过滤后的集合中只有【2,3】了,所以hashCode发生了变化。
上面的示例在Dubbo的一致性哈希算法的场景中相当于1号服务器下线了,服务列表发生了变化,需要重新进行哈希环的映射。
对应源码如下(PR提交的源码):
因为在标号为①处得到的invokersHashCode和之前的不一样了,所以在标号为②处判断条件为真,进入标号为③的代码处,重新进行Hash环的映射,并选择某个虚拟节点执行该请求。
通过上面模拟的两个示例,再结合下面的源码:
也就回答了为什么把上图中编号为①处的代码替换为标号为②的代码,这一行代码就能修复这个Bug,核心思想就是只关心List集合里面的元素变化,而不关心List集合容器是否发生变化。
最后说一句
最开始找到这个BUG的时候,我自己也是有一套解决方案的。思路也是只关心List里面的元素,而不关心List这个容器,但是实现方式比较复杂,改动点较多,还需要写一个工具类。
但是看到issue下面的这个评论,
我才一下回过神来,原来一行代码就能代替我写的工具类了啊。而对于这个知识点,我之前其实是知道的。
我反思了一下自己为什么没有想到这个方案。
其实就是对于已知道的知识点,掌握不够深刻导致的,没有达到融会贯通的地步。知其然,也知其所以然,可惜在需要使用的场景稍稍一变的情况下,就想不起来了。
知道知识点,但是该用的时候却记不起来,这种情况其实挺常见的,那怎么解决呢?
这篇文章就是我的解决方案,记录下来嘛。就像高中的时候人手一本的错题本,做错的题,不会的题都抄下来嘛。没事的时候翻一翻,总有下次碰到的时候。再次碰到时,就是"一雪前耻"的机会。
好了。
才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。
如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
感谢您的阅读,我的订阅号里全是原创,十分欢迎并感谢您的关注。
以上。
原创不易,欢迎转发,求个关注,文末右下角赏个"在看"吧。