一致性 hash 環

AskaJohnny發表於2022-03-22

一致性 hash 環

最近做專案 做了一個分發器 ,需要 根據請求攜帶的引數 把請求分發到 不同的伺服器上面,最終我選擇使用 一致性hash 環 來實現 ,本篇 就主要講解一下 一致性hash環 它的基本原理

概述

一致性hash演算法 由於 均衡性 永續性的對映特點 被廣泛應用於負載均衡領域,比如 nginx 、dubbo 、等等 內部都有一致性hash 的實現 ,比如 dubbo ,當你呼叫rpc 介面的時候,如果有2個提供者,那麼你可以通過配置 讓其呼叫通過 一致性hash 進行計算 然後分發到具體的 某個例項介面上 。

1.hash演算法 在負載均衡中的問題

先來看看普通的hash演算法的特點,普通的hash演算法就是把一系列輸入 打散成隨機的資料,負載均衡就是利用這一點特性,對於大量請求呼叫,通過一定的 hash將它們均勻的雜湊,從而實現壓力平均化

image-20210918093914555

如果上面圖中 key作為快取的key ,node中寸入該key對應的 value,就是一個 簡單的分散式快取系統了。

問題:可以看出 當 N 節點數發生變化的時候 之前所有的 hash對映幾乎全部失效,如果叢集是無狀態的服務 倒是沒什麼事情,但是如果是 分散式快取這種,比如 對映的key1 原本是去 node1上查詢 快取的value1, 但是當N節點變化後 hash後的 key1 可能去了 node2 這樣 就產生了致命問題。。

2.一致性 hash 演算法

一致性hash演算法就是來解決 上面的問題

2.1 特點 (重要)

下面說明 一致性hash演算法的 2個 重要的特點

  • 平衡性

    平衡性是指雜湊的結果能夠儘可能分佈到所有的緩衝中去,這樣可以使得所有的緩衝空間都得到利用。很多雜湊演算法都能夠滿足這一條件。

  • 單調性

    單調性是指如果已經有一些內容通過雜湊分派到了相應的緩衝中,又有新的緩衝區加入到系統中,那麼雜湊的結果應能夠保證原有已分配的內容可以被對映到新的緩衝區中去,而不會被對映到舊的緩衝集合中的其他緩衝區。簡單的雜湊演算法往往不能滿足單調性的要求

2.2 原理

一致性雜湊將整個雜湊值空間組織成一個虛擬的圓環,如假設某雜湊函式H的值空間為0-2^32-1(即雜湊值是一個32位無符號整形),整個雜湊空間環如下:

就是所有的輸入值都被對映到 0-2^32-1 之間,組成一個圓環

image-20210918095022288

下一步將各個伺服器使用Hash進行一個雜湊,具體可以選擇伺服器的ip或主機名或者其他業務屬性作為關鍵字進行雜湊,這樣每臺機器就能確定其在雜湊環上的位置,這裡假設將上文中四臺伺服器使用ip地址雜湊後在環空間的位置如下:

image-20210918100318922

如果伺服器數量不多且個數相對穩定,我們也可以手動設定這些伺服器的位置,如設A在230-1位置,B在231-1位置,C在3*230-1位置,D在232-1位置,則hash值為02^30-1的資料儲存在A中,hash值為2^30-12^31-1的資料儲存在B中,以此類推。

**定位資料儲存的方法: **將資料key使用相同的函式Hash計算出雜湊值,並確定此資料在環上的位置,從此位置沿環順時針“行走”,第一臺遇到的伺服器就是其應該定位到的伺服器。可以為這些伺服器維護一條二分查詢樹,定位伺服器的過程就是在二分查詢樹中找剛好比其大的節點。

例如我們有Object A、Object B、Object C、Object D四個資料物件,經過雜湊計算後,在環空間上的位置如下:

image-20210918100204431

2.3 一致性hash 優點

如上圖,假如當節點A當機了,那麼只會影響 objectA 它會被重新對映到 NodeB節點,其他的ObjectB ObjectC ObjectD 都不會受到影響,大大提高了容錯性和穩定性

2.4 一致性hash存在的問題

2.3.1 資料分佈不均勻

當節點Node很少的時候 比如2臺機器,那麼 必然造成大量資料集中在NodeA,少量在NodeB

2.3.2 雪崩

當某個節點當機後,原本屬於它的請求 都會被重新hash 對映到 下游節點,會突然造成下游節點壓力過大 有可能也會造成 下游節點當機,從而形成雪崩 這是致命的

為此 引入了 虛擬節點來解決上面兩個問題

3.帶虛擬節點的一致性hash

就是會在圓環上 根據Node 節點 生成很多的虛擬節點 分佈在圓環上,這樣當 某個 節點掛了後 原本屬於它的請求,會被均衡的分佈到 其他節點上 降低了產生雪崩的情況,也解決了 節點數少導致 請求分佈不均的請求

即對每一個服務節點計算多個雜湊(可以用原節點key+"##xxxk"作為每個虛擬節點的key,然後求hashcode),每個計算結果位置都放置一個此服務節點,稱為虛擬節點。具體做法可以在伺服器ip或主機名的後面增加編號來實現。

image-20210918101705932

看上圖,此時如果 group3節點掛了,那麼請求會被均分到 group2 和 group1上面 ,到此 一致性hash的正確生產的使用方式講解完了 下面來看看一個案例程式碼。

4. 程式碼測試

可供選擇的有很多,memcached官方使用了基於md5的KETAMA演算法,但這裡處於計算效率的考慮,使用了FNV1_32_HASH演算法,如下:

public class HashUtil {
    /**
     * 計算Hash值, 使用FNV1_32_HASH演算法
     * @param str
     * @return
     */
    public static int getHash(String str) {
        final int p = 16777619;
        int hash = (int)2166136261L;
        for (int i = 0; i < str.length(); i++) {
            hash =( hash ^ str.charAt(i) ) * p;
        }
        hash += hash << 13;
        hash ^= hash >> 7;
        hash += hash << 3;
        hash ^= hash >> 17;
        hash += hash << 5;

        if (hash < 0) {
            hash = Math.abs(hash);
        }
        return hash;
    }
}
package com.weareint.dispatchservice.hashloop;

import org.springframework.stereotype.Component;

import java.util.*;

/**
 *
 *
 * <pre>
 *  一致性 hash 虛擬 環
 * </pre>
 *
 * @author johnny
 * @date 2021-08-26 9:22 上午
 */
@Component
public class HashVirtualNodeCircle {

    /** 真實叢集列表 */
    private static List<String> instanceNodes;

    /** 虛擬節點對映關係 */
    private static SortedMap<Integer, String> virtualNodes = new TreeMap<>();

    /** 虛擬節點數 */
    private static final int VIRTUAL_NODE_NUM = 1000;

    /** 重新整理 服務例項 */
    public void refreshVirtualHashCircle(HashCircleInstanceNodeBuild build) {
        // 當叢集變動時,重新整理hash環,其餘的叢集在hash環上的位置不會發生變動
        virtualNodes.clear();
        // 獲取 最新節點
        instanceNodes = build.instanceNodes();
        // 將虛擬節點對映到Hash環上
        for (String realInstance : instanceNodes) {
            for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
                String virtualNodeName = getVirtualNodeName(realInstance, i);
                int hash = HashUtils.getHash(virtualNodeName);
                System.out.println("[" + virtualNodeName + "] launched @ " + hash);
                virtualNodes.put(hash, virtualNodeName);
            }
        }
    }

    private static String getVirtualNodeName(String realName, int num) {
        return realName + "&&VN" + num;
    }

    private static String getRealNodeName(String virtualName) {
        return virtualName.split("&&")[0];
    }

    private static String getServer(String widgetKey) {
        int hash = HashUtils.getHash(widgetKey);
        // 只取出所有大於該hash值的部分而不必遍歷整個Tree
        SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
        String virtualNodeName;
        if (subMap.isEmpty()) {
            // hash值在最尾部,應該對映到第一個group上
            virtualNodeName = virtualNodes.get(virtualNodes.firstKey());
        } else {
            virtualNodeName = subMap.get(subMap.firstKey());
        }
        return getRealNodeName(virtualNodeName);
    }

    public static void main(String[] args) {
        HashVirtualNodeCircle hashVirtualNodeCircle = new HashVirtualNodeCircle();
        hashVirtualNodeCircle.refreshVirtualHashCircle(
                new HashCircleInstanceNodeBuild() {
                    @Override
                    public List<String> instanceNodes() {
                        LinkedList<String> nodes = new LinkedList<>();
                        nodes.add("192.168.11.23:8090");
                        nodes.add("192.168.11.23:8093");
                        nodes.add("192.168.11.23:8094");
                        return nodes;
                    }
                });

        // 生成隨機數進行測試
        Map<String, Integer> resMap = new HashMap<>();

        List<String> plcList = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            String plchost = "192.168.0." + i + 1;
            for (int j = 0; j < 10; j++) {
                plcList.add(plchost + ":" + j + 100);
            }
        }

        for (int i = 0; i < plcList.size(); i++) {
            String plcwideget = plcList.get(i);
            String group = getServer(plcwideget);
            if (resMap.containsKey(group)) {
                resMap.put(group, resMap.get(group) + 1);
            } else {
                resMap.put(group, 1);
            }
        }

        resMap.forEach(
                (k, v) -> {
                    System.out.println("group " + k + ": " + v + "(" + v / 100.0D + "%)");
                });

        System.out.println("=========================================");

    }
}

可以看到 分佈很均衡

image-20210918102852286

5.Dubbo 一致性Hash 實現

最近給公司做的分發器 使用dubbo 呼叫遠端服務,調研了一下 dubbo 也有自己實現的 一致性hash 不過實際使用起來 發現有些bug ,目前通過SPI機制 自己擴充套件了一下,來看看 dubbo的 一致性hash實現吧

5.1 版本 dubbo2.7.12

我用的版本是 dubbo2.7.12 已經入了 2.7的坑 不少坑都是自己慢慢除錯解決的。

5.2 org.apache.dubbo.rpc.cluster.loadbalance

負載均衡基本就這些 一致性hash,隨機 ,輪訓,。。 沒啥特別的

image-20210918103705714

5.3 dubbo ConsistentHashLoadBalance原始碼

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.dubbo.rpc.cluster.loadbalance;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.support.RpcUtils;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static org.apache.dubbo.common.constants.CommonConstants.COMMA_SPLIT_PATTERN;

/**
 * ConsistentHashLoadBalance
 */
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
    public static final String NAME = "consistenthash";

    /**
     * Hash nodes name
     */
    public static final String HASH_NODES = "hash.nodes";

    /**
     * Hash arguments name
     */
    public static final String HASH_ARGUMENTS = "hash.arguments";

    private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<String, ConsistentHashSelector<?>>();

    @SuppressWarnings("unchecked")
    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        String methodName = RpcUtils.getMethodName(invocation);
        String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
        // using the hashcode of list to compute the hash only pay attention to the elements in the list
        //有bug  1.invokers 老是變化,導致 不斷的 在建立 ConsistentHashSelector 
				//      
        int invokersHashCode = invokers.hashCode();
        ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
        if (selector == null || selector.identityHashCode != invokersHashCode) {
            selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, invokersHashCode));
            selector = (ConsistentHashSelector<T>) selectors.get(key);
        }
        return selector.select(invocation);
    }

    private static final class ConsistentHashSelector<T> {

        private final TreeMap<Long, Invoker<T>> virtualInvokers;

        private final int replicaNumber;

        private final int identityHashCode;

        private final int[] argumentIndex;

        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();
            //預設虛擬機器節點數 160 ,可以通過
          /**
          dubbo:
            consumer:
    					parameters:
      					hash:
        					nodes: 560 #指定虛擬分片結點數  最終virtualInvokers = nodes * invokerCount
        					arguments: 0  指定的是 哪個引數作為 key
          	**/
            this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
          //預設取 第 0 個引數 作為 hash的key
            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]);
            }
            for (Invoker<T> invoker : invokers) {
                String address = invoker.getUrl().getAddress();
                for (int i = 0; i < replicaNumber / 4; i++) {
                    byte[] digest = Bytes.getMD5(address + i);
                    for (int h = 0; h < 4; h++) {
                        long m = hash(digest, h);
                        virtualInvokers.put(m, invoker);
                    }
                }
            }
        }

        public Invoker<T> select(Invocation invocation) {
            String key = toKey(invocation.getArguments());
            byte[] digest = Bytes.getMD5(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();
        }

        private Invoker<T> selectForKey(long hash) {
            Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
            if (entry == null) {
                entry = virtualInvokers.firstEntry();
            }
            return entry.getValue();
        }

        private long hash(byte[] digest, int number) {
            return (((long) (digest[3 + number * 4] & 0xFF) << 24)
                    | ((long) (digest[2 + number * 4] & 0xFF) << 16)
                    | ((long) (digest[1 + number * 4] & 0xFF) << 8)
                    | (digest[number * 4] & 0xFF))
                    & 0xFFFFFFFFL;
        }
    }

}

6.Dubbo 的 一致性 hash的 bug 和 一些配置引數

6.1 invokers 不斷的變化

經過除錯 發現 invokers 時不時變化導致 一致在 rehash,其實 很多時候只是 節點的順序變化而已

解決辦法: 我直接把 invokers 節點數 取出來進行排序後拼接 成一個字串 進行 計算hashcode 就不會總變化了

String invokeKey =
        invokers.stream()
                .filter(Node::isAvailable)
                // 按照ip 和port 排序
                .sorted(Comparator.comparing(invoke -> invoke.getUrl().getIp()))
                .sorted(Comparator.comparing(invoke -> invoke.getUrl().getPort()))
                .map(invoke -> invoke.getUrl().getIp() + ":" + invoke.getUrl().getPort())
                .collect(Collectors.joining(","));

6.2 注意 isAvailable

invokers中存在 節點不可用的,如果對於節點不可用的 直接過濾 需要注意 isAvailable

6.3 設定虛擬節點發片數

dubbo:
	consumer:
    parameters:
      hash:
        nodes: 560 #指定虛擬分片結點數  最終virtualInvokers = nodes * invokerCount ,預設是160
   

6.4 設定方法的哪個引數作為 hash的key

dubbo:
	consumer:
  	parameters:
    	hash:    	
      	arguments: 0  #預設就是0 

7.SPI 擴充套件Dubbo 一致性hash 演算法 ExtendConsistentHashLoadBalance

7.1 官方文件

官方文件很詳細了

https://dubbo.apache.org/zh/docs/v3.0/references/spis/load-balance/

image-20210918105003326

7.2 ExtendConsistentHashLoadBalance 擴充套件實現

package com.weareint.dispatchservice.extendconsistenthash;

import com.atw.mdc.entity.protocol.webRequest.PlcReadSimpleRequest;
import com.atw.mdc.entity.protocol.webRequest.PlcWriteSimpleRequest;
import com.weareint.dispatchservice.event.ConnectionRouteEvent;
import com.weareint.dispatchservice.event.NodeChangeEvent;
import com.weareint.dispatchservice.event.NodeChangeService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.Node;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance;
import org.apache.dubbo.rpc.support.RpcUtils;
import org.springframework.context.ApplicationEventPublisher;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;

import static org.apache.dubbo.common.constants.CommonConstants.COMMA_SPLIT_PATTERN;

/**
 *
 *
 * <pre>
 * 擴充套件 一致性Hash 環  主要是把 hash 的key 只通過 plc 的deviceCode 進行hash , 然後後續新增 Redis 路由表 進行
 * </pre>
 *
 * @author johnny
 * @date 2021-08-26 5:32 下午
 */
@Slf4j
public class ExtendConsistentHashLoadBalance extends AbstractLoadBalance {

    public static ApplicationEventPublisher publisher;
    public static NodeChangeService nodeChangeService;

    public static final String NAME = "consistenthash";

    /** Hash nodes name */
    public static final String HASH_NODES = "hash.nodes";

    /** Hash arguments name */
    public static final String HASH_ARGUMENTS = "hash.arguments";

    private final ConcurrentMap<String, ExtendConsistentHashLoadBalance.ConsistentHashSelector<?>>
            selectors =
                    new ConcurrentHashMap<
                            String, ExtendConsistentHashLoadBalance.ConsistentHashSelector<?>>();

    public ConcurrentMap<String, ConsistentHashSelector<?>> getSelectors() {
        return selectors;
    }

    @SuppressWarnings("unchecked")
    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        String methodName = RpcUtils.getMethodName(invocation);
        // String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
        // 只要是 servicekey 就好
        String key = invokers.get(0).getUrl().getServiceKey();
        // String key = invokers.get(0).getUrl().getParameter("remote.application");
        if (log.isDebugEnabled()) {
            log.info("【remoteApplication:{}】", key);
        }
        // using the hashcode of list to compute the hash only pay attention to the elements in the
        String invokeKey =
                invokers.stream()
                        .filter(Node::isAvailable)
                        // 按照ip 和port 排序
                        .sorted(Comparator.comparing(invoke -> invoke.getUrl().getIp()))
                        .sorted(Comparator.comparing(invoke -> invoke.getUrl().getPort()))
                        .map(invoke -> invoke.getUrl().getIp() + ":" + invoke.getUrl().getPort())
                        .collect(Collectors.joining(","));

        if (log.isDebugEnabled()) {
            log.info("【invokeKey : {}】", invokeKey);
        }
        int invokersHashCode = invokeKey.hashCode();
        ExtendConsistentHashLoadBalance.ConsistentHashSelector<T> selector = null;
        // 同時submit 可能會有問題 加個鎖 可能會有第一次提交生成 虛擬節點     
        selector = (ExtendConsistentHashLoadBalance.ConsistentHashSelector<T>) selectors.get(key);
        // 判斷是否invokers 有變化  selector.identityHashCode != invokersHashCode
        if (selector == null || selector.identityHashCode != invokersHashCode) {
            synchronized (ExtendConsistentHashLoadBalance.class) {
                selector =
                        (ExtendConsistentHashLoadBalance.ConsistentHashSelector<T>)
                                selectors.get(key);
                if (selector == null || selector.identityHashCode != invokersHashCode) {
                    // 這個 isAvailable 要存在 否則有bug
                    List<Invoker<T>> availableInvoker =
                            invokers.stream()
                                    .filter(Node::isAvailable)
                                    .collect(Collectors.toList());
                    ConsistentHashSelector<T> tConsistentHashSelector =
                            new ConsistentHashSelector<>(
                                    availableInvoker, methodName, invokersHashCode);
                    selectors.put(key, tConsistentHashSelector);
                    selector =
                            (ExtendConsistentHashLoadBalance.ConsistentHashSelector<T>)
                                    selectors.get(key);
                    log.info(
                            "【new selector by invokeKey : {} , availableInvoker:{}】",
                            invokeKey,
                            availableInvoker.stream()
                                    .map(
                                            invoke ->
                                                    invoke.getUrl().getIp()
                                                            + ":"
                                                            + invoke.getUrl().getPort())
                                    .collect(Collectors.joining(",")));

                    NodeChangeEvent event = new NodeChangeEvent(this, tConsistentHashSelector);
                    publisher.publishEvent(event);
                }
            }
        }
        String hashKey = selector.toKey(invocation.getArguments());
        Invoker<T> select = selector.select(hashKey);
        log.info(
                "【plcDeviceCode: {} dispatch to  ip : {}:{}",
                hashKey,
                select.getUrl().getIp(),
                select.getUrl().getPort());
        // deviceCode route table  To Redis
        ConnectionRouteEvent event = new ConnectionRouteEvent(this, hashKey, select, selector);
        publisher.publishEvent(event);
        return select;
    }

    public static final class ConsistentHashSelector<T> {

        private final TreeMap<Long, Invoker<T>> virtualInvokers;

        public final List<Invoker<T>> invokers;

        private final int replicaNumber;

        private final int identityHashCode;

        private final int[] argumentIndex;

        ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
            this.invokers = invokers;
            this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
            this.identityHashCode = identityHashCode;
            URL url = invokers.get(0).getUrl();
            this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
            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]);
            }
            for (Invoker<T> invoker : invokers) {
                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);
                    }
                }
            }
        }

        public Invoker<T> select(String key) {
            byte[] digest = md5(key);
            return selectForKey(hash(digest, 0));
        }

        @SuppressWarnings("unchecked")
        public String toKey(Object[] args) {
            StringBuilder buf = new StringBuilder();
            for (int i : argumentIndex) {
                if (i >= 0 && i < args.length) {
                    // 只取 PlcReadSimpleRequest的 DeviceCode 作為 hash的key
                    if (args[i] instanceof ArrayList) {
                        ArrayList<PlcReadSimpleRequest> list =
                                (ArrayList<PlcReadSimpleRequest>) args[i];
                        buf.append(list.get(0).getDeviceCode());
                        // 只取 PlcWriteSimpleRequest DeviceCode 作為 hash的key
                    } else if (args[i] instanceof PlcWriteSimpleRequest) {
                        PlcWriteSimpleRequest req = (PlcWriteSimpleRequest) args[i];
                        buf.append(req.getDeviceCode());
                    } else if (args[i] instanceof String) {
                        // PlcConnectionRequest req = (PlcConnectionRequest) args[i];
                        // 關閉連線
                        String deviceCode = (String) args[i];
                        buf.append(deviceCode);
                    } else {
                        buf.append(args[i]);
                    }
                }
            }
            return buf.toString();
        }

        private Invoker<T> selectForKey(long hash) {
            Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
            if (entry == null) {
                entry = virtualInvokers.firstEntry();
            }
            return entry.getValue();
        }

        private long hash(byte[] digest, int number) {
            return (((long) (digest[3 + number * 4] & 0xFF) << 24)
                            | ((long) (digest[2 + number * 4] & 0xFF) << 16)
                            | ((long) (digest[1 + number * 4] & 0xFF) << 8)
                            | (digest[number * 4] & 0xFF))
                    & 0xFFFFFFFFL;
        }

        private byte[] md5(String value) {
            MessageDigest md5;
            try {
                md5 = MessageDigest.getInstance("MD5");
            } catch (NoSuchAlgorithmException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
            md5.reset();
            byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
            md5.update(bytes);
            return md5.digest();
        }

        public int getIdentityHashCode() {
            return identityHashCode;
        }
    }
}

7.3 配置spi擴充套件

image-20210918105358574

extendconsistenthash=com.weareint.dispatchservice.extendconsistenthash.ExtendConsistentHashLoadBalance

7.4 使用自定義的擴充套件 loadbalance

@DubboReference(loadbalance = "extendconsistenthash")
private IDeviceWriteService deviceWriteService;

7.5 已知擴充套件

image-20210826173550143

總結

本篇主要講解了 什麼是一致性 hash 它有哪些優點和存在的問題,以及 帶虛擬節點的一致性hash,最後介紹了一些 dubbo 的一致性hash 實現,dubbo自帶的有bug ,但是提供了 spi 擴充套件機制 你可以自己去實現 ,目前我就是這樣去解決的 。

參考文章 :
https://www.cnblogs.com/fengyun2050/p/12808951.html,
https://blog.csdn.net/wudiyong22/article/details/78687246

歡迎大家訪問 個人部落格 Johnny小屋

相關文章