这是 why 技术的第 28 篇原创文章git
以前在《Dubbo 一致性哈希负载均衡的源码和 Bug,了解一下?》中写到了我发现了一个 Dubbo 一致性哈希负载均衡算法的 Bug。github
对于解决方案我是这样写的:算法
特别简单,把获取identityHashCode的方法从System.identityHashCode(invokers)修改成invokers.hashCode()便可。此方案是我提的issue里面的评论,这里System.identityHashCode和 hashCode之间的联系和区别就不进行展开讲述了,不清楚的你们能够自行了解一下。
apache
我说:这里 System.identityHashCode 和 hashCode 之间的联系和区别就不进行展开讲述了,不清楚的你们能够自行了解一下。服务器
可是有读者在后台问我详细缘由,我已经和他聊清楚了。负载均衡
再加上这个BUG 已于近期修复了,且只用了一行代码就修复了,那我就写一下解决方案,以及背后的原理。编辑器
便是对以前文章的一个补充,也是一个独立的知识点。ide
因此本文主要是回答下面这三个问题:工具
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 下面的这个评论,
我才一下回过神来,原来一行代码就能代替我写的工具类了啊。而对于这个知识点,我以前实际上是知道的。
我反思了一下本身为何没有想到这个方案。
其实就是对于已知道的知识点,掌握不够深入致使的,没有达到融会贯通的地步。知其然,也知其因此然,惋惜在须要使用的场景稍稍一变的状况下,就想不起来了。
知道知识点,可是该用的时候却记不起来,这种状况其实挺常见的,那怎么解决呢?
这篇文章就是个人解决方案,记录下来嘛。就像高中的时候人手一本的错题本,作错的题,不会的题都抄下来嘛。没事的时候翻一翻,总有下次碰到的时候。再次碰到时,就是"一雪前耻"的机会。
好了。
才疏学浅,不免会有纰漏,若是你发现了错误的地方,还请你留言给我指出来,我对其加以修改。
感谢您的阅读,感谢您的关注。
以上。
欢迎关注公众号【why 技术】,坚持输出原创。愿你我共同进步。