Dubbo一致性雜湊負載均衡的原始碼和Bug,瞭解一下?

why技術發表於2019-12-09

file

本文是對於Dubbo負載均衡策略之一的一致性雜湊負載均衡的詳細分析。對原始碼逐行解讀、根據實際執行結果,配以豐富的圖片,可能是東半球講一致性雜湊演算法在Dubbo中的實現最詳細的文章了。

文中所示原始碼,沒有特別標註的地方,均為2.7.4.1版本。

在撰寫本文的過程中,發現了Dubbo2.7.0版本之後的一個bug。會導致效能問題,且目前還未解決,如果你們的負載均衡配置的是一致性雜湊或者考慮使用一致性雜湊的話,可以瞭解一下。

本文目錄

第一節:雜湊演算法

本小節主要是為了介紹一致性雜湊演算法做鋪墊。簡單的介紹了雜湊演算法解決了什麼問題,帶來了什麼問題。

第二節:一致性雜湊演算法

本小節主要是通過作圖對一致性雜湊進行了簡單的介紹。介紹了一致性雜湊是怎麼解決雜湊演算法帶來的問題,怎麼解決資料傾斜的問題。

第三節:一致性雜湊演算法在Dubbo中的應用

本小節是全文重點,通過一個"騷"操作,對Dubbo一致性雜湊演算法的原始碼進行了十分詳細的剖析。從整個類到類裡面的每個方法進行了詳盡的分析,列印了大量的日誌,配合圖片,方便讀者理解。

第四節:我又發現了一個Bug

本小節主要是介紹我在研究Dubbo一致性雜湊負載均衡時遇到的一個問題,深入研究之後發現可能是一個Bug。這一小節就是比較詳盡的介紹了這個Bug現象、原因以及我的解決方案。

第五節:加入節點,畫圖分析

本小節對具體的案例進行了分析,並配以圖片,相信能幫助讀者更加深刻的理解一致性雜湊演算法在Dubbo中的應用。

第六節:一致性雜湊的應用場景

本小節主要介紹幾個應用場景。使用Duboo框架,在什麼樣的需求可以使用一致性雜湊演算法做負載均衡。

PS:前一、二節主要是進行了背景知識的簡單鋪墊,如果你瞭解相關背景知識,可以直接從第三節看起。本文的重點是第三到第五節。如果你只想知道Bug是什麼,可以直接閱讀第四節。

另:閱讀本文需要對Dubbo有一定的瞭解。文章很長,建議收藏慢慢閱讀。一定會有收穫的。

雜湊演算法

在介紹一致性雜湊演算法之前,我們看看雜湊演算法,以及它解決了什麼問題,帶來了什麼問題。

file

如上圖所示,假設0,1,2號伺服器都儲存的有使用者資訊,那麼當我們需要獲取某使用者資訊時,因為我們不知道該使用者資訊存放在哪一臺伺服器中,所以需要分別查詢0,1,2號伺服器。這樣獲取資料的效率是極低的。

對於這樣的場景,我們可以引入雜湊演算法。

還是上面的場景,但前提是每一臺伺服器存放使用者資訊時是根據某一種雜湊演算法存放的。所以取使用者資訊的時候,也按照同樣的雜湊演算法取即可。

file

假設我們要查詢使用者號為100的使用者資訊,經過某個雜湊演算法,比如這裡的userId mod n,即100 mod 3結果為1。所以使用者號100的這個請求最終會被1號伺服器接收並處理。

這樣就解決了無效查詢的問題。

但是這樣的方案會帶來什麼問題呢?

擴容或者縮容時,會導致大量的資料遷移。最少也會影響百分之50的資料。

file

為了說明問題,我們加入一臺伺服器3。伺服器的數量n就從3變成了4。還是查詢使用者號為100的使用者資訊時,100 mod 4結果為0。這時,請求就被0號伺服器接收了。

當伺服器數量為3時,使用者號為100的請求會被1號伺服器處理。

當伺服器數量為4時,使用者號為100的請求會被0號伺服器處理。

所以,當伺服器數量增加或者減少時,一定會涉及到大量資料遷移的問題。可謂是牽一髮而動全身。

對於上述雜湊演算法其優點是簡單易用,大多數分庫分表規則就採取的這種方式。一般是提前根據資料量,預先估算好分割槽數。

其缺點是由於擴容或收縮節點導致節點數量變化時,節點的對映關係需要重新計算,會導致資料進行遷移。所以擴容時通常採用翻倍擴容,避免資料對映全部被打亂,導致全量遷移的情況,這樣只會發生50%的資料遷移。

假設這是一個快取服務,資料的遷移會導致在遷移的時間段內,有快取是失效的。快取失效,可怕啊。還記得我之前的文章嗎,《當週杰倫把QQ音樂幹翻的時候,作為程式猿我看到了什麼?》就是講快取擊穿、快取穿透、快取雪崩的場景和對應的解決方案。

一致性雜湊演算法

為了解決雜湊演算法帶來的資料遷移問題,一致性雜湊演算法應運而生。

對於一致性雜湊演算法,官方說法如下:

一致性雜湊演算法在1997年由麻省理工學院提出,是一種特殊的雜湊演算法,在移除或者新增一個伺服器時,能夠儘可能小地改變已存在的服務請求與處理請求伺服器之間的對映關係。一致性雜湊解決了簡單雜湊演算法在分散式雜湊表( Distributed Hash Table,DHT) 中存在的動態伸縮等問題。

什麼意思呢?我用大白話加畫圖的方式給你簡單的介紹一下。

一致性雜湊,你可以想象成一個雜湊環,它由0到2^32-1個點組成。A,B,C分別是三臺伺服器,每一臺的IP加埠經過雜湊計算後的值,在雜湊環上對應如下:

file

當請求到來時,對請求中的某些引數進行雜湊計算後,也會得出一個雜湊值,此值在雜湊環上也會有對應的位置,這個請求會沿著順時針的方向,尋找最近的伺服器來處理它,如下圖所示:

file

一致性雜湊就是這麼個東西。那它是怎麼解決伺服器的擴容或收縮導致大量的資料遷移的呢?

看一下當我們使用一致性雜湊演算法時,加入伺服器會發什麼事情。

file

當我們加入一個D伺服器後,假設其IP加埠,經過雜湊計算後落在了雜湊環上圖中所示的位置。

這時影響的範圍只有圖中標註了五角星的區間。這個區間的請求從原來的由C伺服器處理變成了由D伺服器請求。而D到C,C到A,A到B這個區間的請求沒有影響,加入D節點後,A、B伺服器是無感知的。

所以,在一致性雜湊演算法中,如果增加一臺伺服器,則受影響的區間僅僅是新伺服器(D)在雜湊環空間中,逆時針方向遇到的第一臺伺服器(B)之間的區間,其它區間(D到C,C到A,A到B)不會受到影響。

在加入了D伺服器的情況下,我們再假設一段時間後,C伺服器當機了:

file

當C伺服器當機後,影響的範圍也是圖中標註了五角星的區間。C節點當機後,B、D伺服器是無感知的。

所以,在一致性雜湊演算法中,如果當機一臺伺服器,則受影響的區間僅僅是當機伺服器(C)在雜湊環空間中,逆時針方向遇到的第一臺伺服器(D)之間的區間,其它區間(C到A,A到B,B到D)不會受到影響。

綜上所述,在一致性雜湊演算法中,不管是增加節點,還是當機節點,受影響的區間僅僅是增加或者當機伺服器在雜湊環空間中,逆時針方向遇到的第一臺伺服器之間的區間,其它區間不會受到影響。

是不是很完美?

不是的,理想和現實的差距是巨大的。

一致性雜湊演算法帶來了什麼問題?

file

當節點很少的時候可能會出現這樣的分佈情況,A服務會承擔大部分請求。這種情況就叫做資料傾斜。

怎麼解決資料傾斜呢?加入虛擬節點。

怎麼去理解這個虛擬節點呢?

首先一個伺服器根據需要可以有多個虛擬節點。假設一臺伺服器有n個虛擬節點。那麼雜湊計算時,可以使用IP+埠+編號的形式進行雜湊值計算。其中的編號就是0到n的數字。由於IP+埠是一樣的,所以這n個節點都是指向的同一臺機器。

如下圖所示:

file

在沒有加入虛擬節點之前,A伺服器承擔了絕大多數的請求。但是假設每個伺服器有一個虛擬節點(A-1,B-1,C-1),經過雜湊計算後落在瞭如上圖所示的位置。那麼A伺服器的承擔的請求就在一定程度上(圖中標註了五角星的部分)分攤給了B-1、C-1虛擬節點,實際上就是分攤給了B、C伺服器。

一致性雜湊演算法中,加入虛擬節點,可以解決資料傾斜問題。

當你在面試的過程中,如果聽到了類似於資料傾斜的字眼。那大概率是在問你一致性雜湊演算法和虛擬節點。

在介紹了相關背景後,我們可以去看看一致性雜湊演算法在Dubbo中的應用了。

一致性雜湊演算法在Dubbo中的應用

經過《一文講透Dubbo負載均衡之最小活躍數演算法》這篇文章我們知道Dubbo中負載均衡的實現是通過org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance中的doSelect抽象方法實現的,一致性雜湊負載均衡的實現類如下所示: org.apache.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance

file

由於一致性雜湊實現類看起來稍微有點抽象,不太好演示,所以我想到了一個"騷"操作。前面的文章說過LoadBalance是一個SPI介面:

file

既然是一個SPI介面,那我們可以自己擴充套件一個一模一樣的演算法,只是在演算法裡面加入一點輸出語句方便我們觀察情況。怎麼擴充套件SPI介面就不描述了,只要記住程式碼裡面的輸出語句都是額外加的,此外沒有任何改動即可,如下:

file

整個類如下圖片所示,請先看完整個類,有一個整體的概念後,我會進行方法級別的分析。

file

圖片很長,其中我加了很多註釋和輸出語句,可以點開大圖檢視,一定會幫你更加好的理解一致性雜湊在Dubbo中的應用:

把程式碼也貼在這裡

public class WhyConsistentHashLoadBalance 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, WhyConsistentHashLoadBalance.ConsistentHashSelector<?>> selectors = 
                                    new ConcurrentHashMap<String, WhyConsistentHashLoadBalance.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;
        System.out.println("從selectors中獲取value的key=" + key);
        //獲取invokers的hashcode
        int identityHashCode = System.identityHashCode(invokers);
        WhyConsistentHashLoadBalance.ConsistentHashSelector<T> selector = 
                                                (WhyConsistentHashLoadBalance.ConsistentHashSelector<T>) selectors.get(key);
        //如果invokers是一個新的List物件,意味著服務提供者數量發生了變化,可能新增也可能減少了。
        //此時selector.identityHashCode!=identityHashCode條件成立
        //如果是第一次呼叫此時selector == null條件成立
        if (selector == null || selector.identityHashCode != identityHashCode) {
            System.out.println("是新的invokers:" + identityHashCode + ",原:" + (selector == null ? "null" : selector.identityHashCode));
            //建立新的ConsistentHashSelector
            selectors.put(key, new WhyConsistentHashLoadBalance.ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
            selector = (WhyConsistentHashLoadBalance.ConsistentHashSelector<T>) selectors.get(key);
            
            System.out.println("雜湊環構建完成,詳情如下:");
            for (Map.Entry<Long, Invoker<T>> entry : selector.virtualInvokers.entrySet()) {
                System.out.println("key(雜湊值)=" + entry.getKey() + ",value(虛擬節點)=" + entry.getValue());
            }
        }
        //呼叫ConsistentHashSelector的select方法選擇Invoker
        System.out.println("開始呼叫ConsistentHashSelector的select方法選擇Invoker");
        return selector.select(invocation);
    }

    private static final class ConsistentHashSelector<T> {
        //使用TreeMap儲存Invoker的虛擬節點
        private final TreeMap<Long, Invoker<T>> virtualInvokers;
        //虛擬節點數
        private final int replicaNumber;
        //hashCode
        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();
            System.out.println("CHS中url為=" + url);
            //即使啟動多個invoker,每個invoker對應的url上的虛擬節點數配置的都是一樣的
            //這裡預設是160個。本文中的示例程式碼設定為4個。
            this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
            //所有輸出語句都是我加的,CHS是ConsistentHashSelector的縮寫
            System.out.println("CHS中url上的【hash.nodes】為=" + replicaNumber);
            //獲取參與雜湊計算的引數下標值,預設對第一個引數進行雜湊運算
            //本文中的示例程式碼使用預設配置,所以這裡的index長度為1。
            String[] index = COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, HASH_ARGUMENTS, "0"));
            System.out.println("CHS中url上的【hash.arguments】為=" + Arrays.toString(index));
            //for迴圈,對argumentIndex進行賦值操作。
            argumentIndex = new int[index.length];
            for (int i = 0; i < index.length; i++) {
                argumentIndex[i] = Integer.parseInt(index[i]);
            }
            System.out.println("CHS中argumentIndex陣列為=" + Arrays.toString(argumentIndex));
            //本文中啟動了2個服務提供者,所以invokers=2
            for (Invoker<T> invoker : invokers) {
                //獲取每個invoker的地址
                String address = invoker.getUrl().getAddress();
                System.out.println("CHS中invoker的地址為=" + address);
                for (int i = 0; i < replicaNumber / 4; i++) {
                    //對address+i進行md5運算得到一個長度為16的位元組陣列
                    byte[] digest = md5(address + i);
                    System.out.println("CHS中對" + address + i + "進行md5計算");
                    //對digest部分位元組進行4次hash運算得到四個不同的long型正整數
                    for (int h = 0; h < 4; h++) {
                        //h=0時,取digest中下標為0~3的4個位元組進行位運算
                        //h=1時,取digest中下標為4~7的4個位元組進行位運算
                        //h=2,h=3時過程同上
                        long m = hash(digest, h);
                        System.out.println("CHS中對digest進行第"+h+"次hash計算後的值:"+m+",當前invoker="+invoker);
                        //將hash到invoker的對映關係儲存到virtualInvokers中,
                        //virtualInvokers需要提供高效的查詢操作,因此選用TreeMap作為儲存結構
                        virtualInvokers.put(m, invoker);
                    }
                }
            }
        }

        public Invoker<T> select(Invocation invocation) {
            String key = toKey(invocation.getArguments());
            System.out.println("CHS的select方法根據argumentIndex取出invocation中參與hash計算的key="+key);
            byte[] digest = md5(key);
            //取digest陣列的前四個位元組進行hash運算,再將hash值傳給selectForKey方法,
            //尋找合適的Invoker
            long hash = hash(digest, 0);
            System.out.println("CHS的select方法中key=" + key + "經過雜湊計算後hash=" + hash);
            return selectForKey(hash);        
        }

        //根據argumentIndex將引數轉化為key。
        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) {
            //到TreeMap中查詢第一個節點值大於或等於當前hash的Invoker
            Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
            //如果hash大於Invoker在圓環上最大的位置,此時entry=null,
            //需要將TreeMap的頭節點賦值給entry
            if (entry == null) {
                entry = virtualInvokers.firstEntry();
            }
            System.out.println("CHS的selectForKey方法根據key="+hash+"選擇出來的invoker="+entry.getValue());
            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();
        }

    }
}

複製程式碼

改造之後,我們先把程式跑起來,有了輸出就好分析了。

服務端程式碼如下:

file

其中的埠是需要手動修改的,我分別啟動服務在20881和20882埠。

專案中provider.xml配置如下:

file

consumer.xml配置如下:

file

然後,啟動在20881和20882埠分別啟動兩個服務端。客戶端消費如下:

file

執行結果輸出如下,可以先看個大概的輸出,下面會對每一部分輸出進行逐一的解讀。

file

好了,用例也跑起來了,日誌也有了。接下來開始結合程式碼和日誌進行方法級別的分析。

首先是doSelect方法的入口:

file

從上圖我們知道了,第一次呼叫需要對selectors進行put操作,selectors的key是介面中定義的方法,value是ConsistentHashSelector內部類。

ConsistentHashSelector通過呼叫其建構函式進行初始化的。invokers(服務端)作為引數傳遞到了建構函式中,建構函式裡面的邏輯,就是把服務端對映到雜湊環上的過程,請看下圖,結合程式碼,仔細分析輸出資料:

file

從上圖可以看出,當ConsistentHashSelector的構造方法呼叫完成後,8個虛擬節點在雜湊環上已經對映完成。兩臺伺服器,每一臺4個虛擬節點組成了這8個虛擬節點。

doSelect方法繼續執行,並列印出每個虛擬節點的雜湊值和對應的服務端,請仔細品讀下圖:

file

說明一下:上面圖中的雜湊環是沒有考慮比例的,僅僅是展現了兩個伺服器在雜湊環上的相對位置。而且為了演示說明方便,僅僅只有8個節點。假設我們有4臺伺服器,每臺伺服器的虛擬節點是預設值(160),這個情況下雜湊環上一共有160*4=640個節點。

雜湊環對映完成後,接下來的邏輯是把這次請求經過雜湊計算後,對映到雜湊環上,並順時針方向尋找遇到的第一個節點,讓該節點處理該請求:

file

還記得地址為468e8565的A伺服器是什麼埠嗎?前面的圖片中有哦,該服務對應的埠是20882。

file

最後我們看看輸出結果:

file

和我們預期的一致。整個呼叫就算是完成了。

再對兩個方法進行一個補充說明。

第一個方法是selectForKey,這個方法裡面邏輯如下圖所示:

file

虛擬節點都儲存在TreeMap中。順時針查詢的邏輯由TreeMap保證。看一下下面的Demo你就明白了。

file

第二個方法是hash方法,其中的& 0xFFFFFFFFL的目的如下:

file

&是位運算子,而0xFFFFFFFFL轉換為四位元組表現後,其低32位全是1,所以保證了雜湊環的範圍是[0,Integer.MAX_VALUE]:

file

所以這裡我們可以改造這個雜湊環的範圍,假設我們改為100000。十進位制的100000對於的16進製為186A0。所以我們改造後的雜湊演算法為:

file

再次呼叫後可以看到,計算後的雜湊值都在10萬以內。但是分佈極不均勻,說明修改資料後這個雜湊演算法不是一個優秀的雜湊演算法:

file

以上,就是對一致性雜湊演算法在Dubbo中的實現的解讀。需要特殊說明一下的是,和上週分享的最小活躍數負載均衡演算法不同,一致性雜湊負載均衡策略和權重沒有任何關係。

我又發現了一個BUG

在上篇文章中,我介紹了Dubbo 2.6.5版本之前,最小活躍數演算法的兩個bug。很不幸,這次我又發現了Dubbo 2.7.4.1版本,一致性雜湊負載均衡策略的一個bug,我提交了issue,截止目前還未解決。

issue地址如下: github.com/apache/dubb…

file

我在這裡詳細說一下這個Bug現象、原因和我的解決方案。

現象如下,我們呼叫三次服務端:

file

輸出日誌如下(有部分刪減):

file

可以看到,在三次呼叫的過程中並沒有發生服務的上下線操作,但是每一次呼叫都重新進行了雜湊環的對映。而我們預期的結果是應該只有在第一次呼叫的時候進行雜湊環的對映,如果沒有服務上下線的操作,後續請求根據已經對映好的雜湊環進行處理。

上面輸出的原因是由於每次呼叫的invokers的identityHashCode發生了變化:

file

我們看一下三次呼叫invokers的情況:

file

經過debug我們可以看出因為每次呼叫的invokers地址值不是同一個,所以System.identityHashCode(invokers)方法返回的值都不一樣。

接下來的問題就是為什麼每次呼叫的invokers地址值都不一樣呢?

經過Debug之後,可以找到這個地方: org.apache.dubbo.rpc.cluster.RouterChain#route

file

問題就出在這個TagRouter中: org.apache.dubbo.rpc.cluster.router.tag.TagRouter#filterInvoker

file

所以,在TagRouter中的stream操作,改變了invokers,導致每次呼叫時其 System.identityHashCode(invokers)返回的值不一樣。所以每次呼叫都會進行雜湊環的對映操作,在服務節點多,虛擬節點多的情況下會有一定的效能問題。

到這一步,問題又發生了變化。這個TagRouter怎麼來的呢?

如果瞭解Dubbo 2.7.x版本新特性的朋友可能知道,標籤路由是Dubbo2.7引入的新功能。

file

通過載入下面的配置載入了RouterFactrory:

META-INF\dubbo\internal\org.apache.dubbo.rpc.cluster.RouterFactory(Dubbo 2.7.0版本之前)

META-INF\dubbo\internal\com.alibaba.dubbo.rpc.cluster.RouterFactory(Dubbo 2.7.0之前)

下面是Dubbo 2.6.7(2.6.x的最後一個版本)和Dubbo 2.7.0版本該檔案的對比:

file

可以看到確實是在Dubbo2.7.0之後引入了TagRouter。

至此,Dubbo 2.7.0版本之後,一致性雜湊負載均衡演算法的Bug的來龍去脈也介紹清楚了。目前該Bug還未解決。

解決方案是什麼呢?特別簡單,把獲取identityHashCode的方法從System.identityHashCode(invokers)修改為invokers.hashCode()即可。

此方案是我提的issue裡面的評論,這裡System.identityHashCode和 hashCode之間的聯絡和區別就不進行展開講述了,不清楚的大家可以自行了解一下。

file

改完之後,我們再看看執行效果:

file

可以看到第二次呼叫的時候並沒有進行雜湊環的對映操作,而是直接取到了值,進行呼叫。

加入節點,畫圖分析

最後,我再分析一種情況。在A、B、C三個伺服器(20881、20882、20883埠)都在正常執行,雜湊對映已經完成的情況下,我們再啟動一個D節點(20884埠),這時的日誌輸出和對應的雜湊環變化情況如下:

file

根據日誌作圖如下:

file

根據輸出日誌和上圖再加上原始碼,你再細細回味一下。我個人覺得還是講的非常詳細了,可能是東半球講一致性雜湊演算法在Dubbo中的實現最詳細的文章了。

一致性雜湊的應用場景

當大家談到一致性雜湊演算法的時候,首先的第一印象應該是在快取場景下的使用,因為在一個優秀的雜湊演算法加持下,其上下線節點對整體資料的影響(遷移)都是比較友好的。

但是想一下為什麼Dubbo在負載均衡策略裡面提供了基於一致性雜湊的負載均衡策略?它的實際使用場景是什麼?

我最開始也想不明白。我想的是在Dubbo的場景下,假設需求是想要一個使用者的請求一直讓一臺伺服器處理,那我們可以採用一致性雜湊負載均衡策略,把使用者號進行雜湊計算,可以實現這樣的需求。但是這樣的需求未免有點太牽強了,適用場景略小。

直到有天晚上,我睡覺之前,電光火石之間突然想到了一個稍微適用的場景了。

如果需求是需要保證某一類請求必須順序處理呢?

如果你用其他負載均衡策略,請求分發到了不同的機器上去,就很難保證請求的順序處理了。比如A,B請求要求順序處理,現在A請求先傳送,被負載到了A伺服器上,B請求後傳送,被負載到了B伺服器上。而B伺服器由於效能好或者當前沒有其他請求或者其他原因極有可能在A伺服器還在處理A請求之前就把B請求處理完成了。這樣不符合我們的要求。

這時,一致性雜湊負載均衡策略就上場了,它幫我們保證了某一類請求都傳送到固定的機器上去執行。比如把同一個使用者的請求傳送到同一臺機器上去執行,就意味著把某一類請求傳送到同一臺機器上去執行。所以我們只需要在該機器上執行的程式中保證順序執行就行了,比如你加一個佇列。

一致性雜湊演算法+佇列,可以實現順序處理的需求。

最後說一句

這是Dubbo負載均衡演算法的第二篇文章,上週寫了一篇《一文講透Dubbo負載均衡之最小活躍數演算法》,也是非常詳細,可以看看哦。

才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言給我指出來,我對其加以修改。

如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。

感謝您的閱讀,我的訂閱號裡全是原創,十分歡迎並感謝您的關注。

以上。

原創不易,歡迎轉發,求個關注,賞個"在看"吧。

公眾號-why技術

相關文章