You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by li...@apache.org on 2020/03/27 03:42:15 UTC
[dubbo-website] branch master updated: Post the updated content
(#568)
This is an automated email from the ASF dual-hosted git repository.
liujun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-website.git
The following commit(s) were added to refs/heads/master by this push:
new da65728 Post the updated content (#568)
da65728 is described below
commit da657288a0b017692e5c698f4f34e60802cce7dd
Author: Kyle <fi...@hotmail.com>
AuthorDate: Fri Mar 27 11:42:05 2020 +0800
Post the updated content (#568)
---
blog/zh-cn/dubbo-consistent-hash-implementation.md | 160 ++++++++++++++++++++-
1 file changed, 158 insertions(+), 2 deletions(-)
diff --git a/blog/zh-cn/dubbo-consistent-hash-implementation.md b/blog/zh-cn/dubbo-consistent-hash-implementation.md
index 29ffb67..247e36c 100644
--- a/blog/zh-cn/dubbo-consistent-hash-implementation.md
+++ b/blog/zh-cn/dubbo-consistent-hash-implementation.md
@@ -102,7 +102,7 @@ Dubbo实现的是客户端负载均衡。关于服务接口代理类的实现,
> 服务引入:http://dubbo.apache.org/zh-cn/docs/source_code_guide/refer-service.html。
-在接口代理类生成、并且装配好后,服务的调用基本是这样一个流程:proxy -> MockClusterInvoker -> 集群策略(如:FailoverClusterInvoker) -> 根据选定的负载均衡策略确定选定的远程调用对象Invoker。
+在接口代理类生成、并且装配好后,服务的调用基本是这样一个流程:proxy -> MockClusterInvoker -> 集群策略(如:FailoverClusterInvoker) -> 初始化负载均衡策略 -> 根据选定的负载均衡策略确定Invoker。
**负载均衡策略的初始化**是在AbstractClusterInvoker中的initLoadBalance方法中初始化的:
@@ -125,4 +125,160 @@ protected LoadBalance initLoadBalance(List<Invoker<T>> invokers, Invocation invo
-所有的负载均衡策略都会继承LoadBalance接口。在各种集群策略中,最终都会调用AbstractClusterInvoker的select方法,而AbstractClusterInvoker会在doSelect中,**调用LoadBalance的select方法,这里即开始了负载均衡策略的执行。**
\ No newline at end of file
+所有的负载均衡策略都会继承LoadBalance接口。在各种集群策略中,最终都会调用AbstractClusterInvoker的select方法,而AbstractClusterInvoker会在doSelect中,**调用LoadBalance的select方法,这里即开始了负载均衡策略的执行。**
+
+
+
+### 三、Dubbo一致性Hash负载均衡的实现
+
+需要说明的一点是,我所说的**负载均衡策略的执行**,即是在所有的Provider中选出一个,作为当前Consumer的远程调用对象。在代码中,Provider被封装成了Invoker实体,所以直接说来,负载均衡策略的执行就是在Invoker列表中选出一个Invoker。
+
+所以,对比普通一致性Hash的实现,Dubbo的一致性Hash算法也可以分为两步:
+
+**1、映射Provider至Hash值区间中(实际中映射的是Invoker);**
+
+**2、映射请求,然后找到大于请求Hash值的第一个Invoker。**
+
+
+
+#### **a、映射Invoker**
+
+Dubbo中所有的负载均衡实现类都继承了AbstractLoadBalance,调用LoadBalance的select方法时,实际上调用的是AbstractLoadBalance的实现:
+
+```java
+@Override
+public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
+ if (CollectionUtils.isEmpty(invokers)) {
+ return null;
+ }
+ if (invokers.size() == 1) {
+ return invokers.get(0);
+ }
+ // doSelect这里进入具体负载均衡算法的执行逻辑
+ return doSelect(invokers, url, invocation);
+}
+```
+
+可以看到这里调用了doSelect,Dubbo一致性Hash的具体实现类名字是**ConsistentHashLoadBalance**,让我们来看看它的doSelect方法干了啥:
+
+```java
+@Override
+protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
+ String methodName = RpcUtils.getMethodName(invocation);
+ // key格式:接口名.方法名
+ String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
+ // identityHashCode 用来识别invokers是否发生过变更
+ int identityHashCode = System.identityHashCode(invokers);
+ ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
+ if (selector == null || selector.identityHashCode != identityHashCode) {
+ // 若不存在"接口.方法名"对应的选择器,或是Invoker列表已经发生了变更,则初始化一个选择器
+ selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
+ selector = (ConsistentHashSelector<T>) selectors.get(key);
+ }
+ return selector.select(invocation);
+}
+```
+
+这里有个很重要的概念:**选择器——selector**。这是Dubbo一致性Hash实现中,承载着整个映射关系的数据结构。它里面主要有这么几个参数:
+
+```java
+/**
+ * 存储Hash值与节点映射关系的TreeMap
+ */
+private final TreeMap<Long, Invoker<T>> virtualInvokers;
+
+/**
+ * 节点数目
+ */
+private final int replicaNumber;
+
+/**
+ * 用来识别Invoker列表是否发生变更的Hash码
+ */
+private final int identityHashCode;
+
+/**
+ * 请求中用来作Hash映射的参数的索引
+ */
+private final int[] argumentIndex;
+```
+
+在新建ConsistentHashSelector对象的时候,就会遍历所有Invoker对象,然后计算出其地址(ip+port)对应的md5码,并按照配置的节点数目replicaNumber的值来初始化服务节点和所有虚拟节点:
+
+```java
+ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
+ this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
+ this.identityHashCode = identityHashCode;
+ URL url = invokers.get(0).getUrl();
+ // 获取配置的节点数目
+ this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
+ // 获取配置的用作Hash映射的参数的索引
+ String[] index = COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, HASH_ARGUMENTS, "0"));
+ argumentIndex = new int[index.length];
+ for (int i = 0; i < index.length; i++) {
+ argumentIndex[i] = Integer.parseInt(index[i]);
+ }
+ // 遍历所有Invoker对象
+ for (Invoker<T> invoker : invokers) {
+ // 获取Provider的ip+port
+ String address = invoker.getUrl().getAddress();
+ for (int i = 0; i < replicaNumber / 4; i++) {
+ byte[] digest = md5(address + i);
+ for (int h = 0; h < 4; h++) {
+ long m = hash(digest, h);
+ virtualInvokers.put(m, invoker);
+ }
+ }
+ }
+}
+```
+
+这里值得注意的是:以replicaNumber取默认值160为例,假设当前遍历到的Invoker地址为127.0.0.1:20880,它会依次获得“127.0.0.1:208800”、“127.0.0.1:208801”、......、“127.0.0.1:2088040”的md5摘要,在每次获得摘要之后,还会对该摘要进行四次数位级别的散列。大致可以猜到其目的应该是为了加强散列效果。(希望有人能告诉我相关的理论依据。)
+
+代码中**virtualInvokers.put(m, invoker)**即是存储当前计算出的Hash值与Invoker的映射关系。
+
+这段代码简单说来,就是为每个Invoker都创建replicaNumber个节点,Hash值与Invoker的映射关系即象征着一个节点,这个关系存储在TreeMap中。
+
+
+
+#### **b、映射请求**
+
+让我们重新回到ConsistentHashLoadBalance的**doSelect**方法,若没有找到selector则会新建selector,找到selector后便会调用selector的select方法:
+
+```java
+public Invoker<T> select(Invocation invocation) {
+ // 根据invocation的【参数值】来确定key,默认使用第一个参数来做hash计算
+ String key = toKey(invocation.getArguments());
+ // 获取【参数值】的md5编码
+ byte[] digest = md5(key);
+ return selectForKey(hash(digest, 0));
+}
+
+// 根据参数索引获取参数,并将所有参数拼接成字符串
+private String toKey(Object[] args) {
+ StringBuilder buf = new StringBuilder();
+ for (int i : argumentIndex) {
+ if (i >= 0 && i < args.length) {
+ buf.append(args[i]);
+ }
+ }
+ return buf.toString();
+}
+
+// 根据参数字符串的md5编码找出Invoker
+private Invoker<T> selectForKey(long hash) {
+ Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
+ if (entry == null) {
+ entry = virtualInvokers.firstEntry();
+ }
+ return entry.getValue();
+}
+```
+
+argumentIndex是在初始化Selector的时候一起赋值的,代表着需要用哪几个请求参数作Hash映射获取Invoker。比如:有方法methodA(Integer a, Integer b, Integer c),如果argumentIndex的值为{0,2},那么即用a和c拼接的字符串来计算Hash值。
+
+我们已经知道virtualInvokers是一个TreeMap,TreeMap的底层实现是红黑树。对于TreeMap的方法ceilingEntry(hash),它的作用是用来**获取比传入值大的第一个元素**。可以看到,这一点与一般的一致性Hash算法的处理逻辑完全是相同的。
+
+但这里的回环逻辑有点不同。对于取模运算来讲,大于最大值后,会自动回环从0开始,而这里的逻辑是:当没有比传入ceilingEntry()方法中的值大的元素的时候,virtualInvokers.ceilingEntry(hash)必然会得到null,于是,就用virtualInvokers.firstEntry()来获取整个TreeMap的第一个元素。
+
+从selectForKey中获取到Invoker后,负载均衡策略也就算是执行完毕了。后续获取远程调用客户端等调用流程不再赘述。
\ No newline at end of file