一致性 hash 環

一致性 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小屋

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章