歡迎光臨
每天分享高質量文章

【RPC 專欄】深入理解 RPC 之叢集篇

點選上方“芋道原始碼”,選擇“置頂公眾號”

技術文章第一時間送達!

原始碼精品專欄

 


摘要: 原創出處 https://www.cnkirito.moe/rpc-cluster/ 「老徐」歡迎轉載,保留摘要,謝謝!

  • 叢集概述

  • 負載均衡

  • 負載均衡介面分析

  • 幾種負載均衡演演算法

  • 高可用策略

  • 高可用介面分析

  • 其他叢集相關的知識點

  • 參考資料


上一篇文章分析了服務的註冊與發現,這一篇文章著重分析下 RPC 框架都會用到的叢集的相關知識。

叢集(Cluster)本身並不具備太多知識點,在分散式系統中,叢集一般涵蓋了負載均衡(LoadBalance),高可用(HA),路由(Route)等等概念,每個 RPC 框架對叢集支援的程度不同,本文著重分析前兩者–負載均衡和高可用。

叢集概述

在此之前的《深入理解 RPC》系列文章,對 RPC 的分析著重還是放在服務之間的點對點呼叫,而分散式服務中每個服務必然不止一個實體,不同服務的實體和相同服務的多個實體構成了一個錯綜複雜的分散式環境,在服務治理框架中正是藉助了 Cluster 這一層來應對這一難題。還是以博主較為熟悉的 motan 這個框架來介紹 Cluster 的作用。

先來看看 Cluster 的頂層介面:

@Spi(scope = Scope.PROTOTYPE)
public interface Cluster<Textends Caller<T{
    @Override
    void init();
    void setUrl(URL url);
    void setLoadBalance(LoadBalance loadBalance);//<1>
    void setHaStrategy(HaStrategy haStrategy);//<2>
    void onRefresh(List> referers);
    List> getReferers();
    LoadBalance getLoadBalance();
}

在概述中,我們只關心 Cluster 介面中的兩個方法,它揭示了 Cluster 在服務治理中的地位

<1> 指定負載均衡演演算法

<2> 指定高可用策略(容錯機制)

http://ov0zuistv.bkt.clouddn.com/TIM%E5%9B%BE%E7%89%8720180227151838.png

http://ov0zuistv.bkt.clouddn.com/TIM%E5%9B%BE%E7%89%8720180227151838.png

我們需要對所謂的負載均衡策略和高可用策略有一定的理解,才能夠搞清楚叢集是如何運作的。

負載均衡

說到負載均衡,大多數人可能立刻聯想到了 nginx。負載均衡可以分為服務端負載均衡和客戶端負載均衡,而服務端負載均衡又按照實現方式的不同可以劃分為軟體負載均衡和硬體負載均衡,nginx 便是典型的軟體負載均衡。而我們今天所要介紹的 RPC 中的負載均衡則主要是客戶端負載均衡。如何區分也很簡單,用筆者自己的話來描述下

在 RPC 呼叫中,客戶端持有所有的服務端節點取用,自行透過負載均衡演演算法選擇一個節點進行訪問,這便是客戶端負載均衡。

客戶端如何獲取到所有的服務端節點取用呢?一般是透過配置的方式,或者是從上一篇文章介紹的服務註冊與發現元件中獲取。

負載均衡介面分析

motan 中的負載均衡抽象:

@Spi(scope = Scope.PROTOTYPE)
public interface LoadBalance<T{
    void onRefresh(List> referers);
    Referer select(Request request);//<1>
    void selectToHolder(Request request, List> refersHolder);
    void setWeightString(String weightString);
}

ribbon 中的負載均衡抽象:

public interface IRule{
    public Server choose(Object key);//<1>
    public void setLoadBalancer(ILoadBalancer lb);
    public ILoadBalancer getLoadBalancer();
}

<1> 對比下兩個 RPC 框架對負載均衡的抽象可以發現,其實負載均衡策略乾的事很簡單,就是根據請求傳回一個服務節點。在 motan 中對服務端的點對點呼叫抽象成了 Referer,而在 ribbon 中則是 Server。

幾種負載均衡演演算法

負載均衡演演算法有幾種經典實現,已經是老生常談了,總結後主要有如下幾個:

  1. 輪詢(Round Robin)

  2. 加權輪詢(Weight Round Robin)

  3. 隨機(Random)

  4. 加權隨機(Weight Random)

  5. 源地址雜湊(Hash)

  6. 一致性雜湊(ConsistentHash)

  7. 最小連線數(Least Connections)

  8. 低併發優先(Active Weight)

每個框架支援的實現都不太一樣,如 ribbon 支援的負載均衡策略

策略名 策略描述 實現說明
BestAvailableRule 選擇一個最小併發請求的 server 逐個考察 Server,如果 Server 被 tripped 了,則忽略,在選擇其中 ActiveRequestsCount 最小的 server
AvailabilityFilteringRule 過濾掉那些因為一直連線失敗的被標記為 circuit tripped 的後端 server,並過濾掉那些高併發的的後端 server(active connections 超過配置的閾值) 使用一個 AvailabilityPredicate 來包含過濾 server 的邏輯,其實就就是檢查 status 裡記錄的各個 server 的執行狀態
WeightedResponseTimeRule 根據響應時間分配一個 weight,響應時間越長,weight 越小,被選中的可能性越低。 一個後臺執行緒定期的從 status 裡面讀取評價響應時間,為每個 server 計算一個 weight。Weight 的計算也比較簡單 responsetime 減去每個 server 自己平均的 responsetime 是 server 的權重。當剛開始執行,沒有形成 status 時,使用 RoundRobinRule 策略選擇 server。
RetryRule 對選定的負載均衡策略機上重試機制。 在一個配置時間段內當選擇 server 不成功,則一直嘗試使用 subRule 的方式選擇一個可用的server
RoundRobinRule roundRobin 方式輪詢選擇 server 輪詢 index,選擇 index 對應位置的 server
RandomRule 隨機選擇一個 server 在 index 上隨機,選擇 index 對應位置的 server
ZoneAvoidanceRule 複合判斷 server 所在區域的效能和 server 的可用性選擇 server 使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 來判斷是否選擇某個server,前一個判斷判定一個 zone 的執行效能是否可用,剔除不可用的 zone(的所有 server),AvailabilityPredicate 用於過濾掉連線數過多的 Server。

motan 支援的負載均衡策略

策略名 策略描述
Random 隨機選擇一個 server
RoundRobin roundRobin 方式輪詢選擇 server
ConsistentHash 一致性 Hash,保證同一源地址的請求落到同一個服務端,能夠應對服務端機器的動態上下線(實際上並沒有嚴格做到一致性 hash,motan 的實現只能滿足粘滯 hash,只保證 server 節點變更週期內相同對請求落在相同的 server 上,比較適合用在二級快取場景)
LocalFirst 當 server 串列中包含本地暴露的可用服務時,優先使用此服務。否則使用低併發優先 ActiveWeight 負載均衡策略
ActiveWeight 併發量越小的 server,優先順序越高
ConfigurableWeight 加權隨機

演演算法很多,有些負載均衡演演算法的實現複雜度也很高,請教了一些朋友,發現用的最多還是 RoundRobin,Random 這兩種。可能和他們實現起來很簡單有關,很多運用到 RPC 框架的專案也都是保持了預設配置。

而這兩種經典複雜均衡演演算法實現起來是很簡單的,在此給出網上的簡易實現,方便大家更直觀的瞭解。

服務串列

public class IpMap
{
    // 待路由的Ip串列,Key代表Ip,Value代表該Ip的權重
    public static HashMap serverWeightMap =
            new HashMap();
    static
    {
        serverWeightMap.put("192.168.1.100"1);
        serverWeightMap.put("192.168.1.101"1);
        // 權重為4
        serverWeightMap.put("192.168.1.102"4);
        serverWeightMap.put("192.168.1.103"1);
        serverWeightMap.put("192.168.1.104"1);
        // 權重為3
        serverWeightMap.put("192.168.1.105"3);
        serverWeightMap.put("192.168.1.106"1);
        // 權重為2
        serverWeightMap.put("192.168.1.107"2);
        serverWeightMap.put("192.168.1.108"1);
        serverWeightMap.put("192.168.1.109"1);
        serverWeightMap.put("192.168.1.110"1);
    }
}

輪詢(Round Robin)

public class RoundRobin
{
    private static Integer pos = 0;

    public static String getServer()
    
{
        // 重建一個Map,避免伺服器的上下線導致的併發問題
        Map serverMap =
                new HashMap();
        serverMap.putAll(IpMap.serverWeightMap);

        // 取得Ip地址List
        Set keySet = serverMap.keySet();
        ArrayList keyList = new ArrayList();
        keyList.addAll(keySet);

        String server = null;
        synchronized (pos)
        {
            if (pos > keySet.size())
                pos = 0;
            server = keyList.get(pos);
            pos ++;
        }

        return server;
    }
}

隨機(Random)

public class Random
{
    public static String getServer()
    
{
        // 重建一個Map,避免伺服器的上下線導致的併發問題
        Map serverMap =
                new HashMap();
        serverMap.putAll(IpMap.serverWeightMap);

        // 取得Ip地址List
        Set keySet = serverMap.keySet();
        ArrayList keyList = new ArrayList();
        keyList.addAll(keySet);

        java.util.Random random = new java.util.Random();
        int randomPos = random.nextInt(keyList.size());

        return keyList.get(randomPos);
    }
}

高可用策略

高可用(HA)策略一般也被稱作容錯機制,分散式系統中出錯是常態,但服務卻不能停止響應,6個9一直是各個公司的努力方向。當一次請求失敗之後,是重試呢?還是繼續請求其他機器?抑或是記錄下這次失敗?下麵是叢集中的幾種常用高可用策略:

  1. 失效轉移(failover)

    當出現失敗,重試其他伺服器,通常用於讀操作等冪等行為,重試會帶來更長延遲。該高可用策略會受到負載均衡演演算法的限制,比如失效轉移強調需要重試其他機器,但一致性 Hash 這類負載均衡演演算法便會與其存在衝突(個人認為一致性 Hash 在 RPC 的客戶端負載均衡中意義不是很大)

  2. 快速失敗(failfast)

    只發起一次呼叫,失敗立即報錯,通常用於非冪等性的寫操作。

    如果在 motan,dubbo 等配置中設定了重試次數>0,又配置了該高可用策略,則重試效果也不會生效,由此可見叢集中的各個配置可能是會相互影響的。

  3. 失效安全(failsafe)

    出現異常時忽略,但記錄這一次失敗,存入日誌中。

  4. 失效自動恢復(failback)

    後臺記錄失敗請求,定時重發。通常用於訊息通知操作。

  5. 並行呼叫(forking)

    只要一個成功即傳回,通常用於實時性要求較高的讀操作。需要犧牲一定的服務資源。

  6. 廣播(broadcast)

    廣播呼叫,所有提供逐個呼叫,任意一臺報錯則報錯。通常用於更新提供方本地狀態,速度慢,任意一臺報錯則報錯。

高可用介面分析

以 motan 的 HaStrategy 為例來介紹高可用在叢集中的實現細節

@Spi(scope = Scope.PROTOTYPE)
public interface HaStrategy<T{
    void setUrl(URL url);
    Response call(Request request, LoadBalance loadBalance);//<1>
}

<1> 如我之前所述,高可用策略依賴於請求和一個特定的負載均衡演演算法,傳回一個響應。

快速失敗(failfast)

@SpiMeta(name = "failfast")
public class FailfastHaStrategy<Textends AbstractHaStrategy<T{

    @Override
    public Response call(Request request, LoadBalance loadBalance) {
        Referer refer = loadBalance.select(request);
        return refer.call(request);
    }
}

motan 實現了兩個高可用策略,其一便是 failfast,非常簡單,只進行一次負載均衡節點的選取,接著發起點對點的呼叫。

失效轉移(failover)

@SpiMeta(name = "failover")
public class FailoverHaStrategy<Textends AbstractHaStrategy<T{

    protected ThreadLocal>> referersHolder = new ThreadLocal>>() {
        @Override
        protected java.util.List> initialValue() {
            return new ArrayList>();
        }
    };

    @Override
    public Response call(Request request, LoadBalance loadBalance) {

        List> referers = selectReferers(request, loadBalance);
        if (referers.isEmpty()) {
            throw new MotanServiceException(String.format("FailoverHaStrategy No referers for request:%s, loadbalance:%s", request,
                    loadBalance));
        }
        URL refUrl = referers.get(0).getUrl();
        // 先使用method的配置
        int tryCount =
                refUrl.getMethodParameter(request.getMethodName(), request.getParamtersDesc(), URLParamType.retries.getName(),
                        URLParamType.retries.getIntValue());
        // 如果有問題,則設定為不重試
        if (tryCount 0) {
            tryCount = 0;
        }
       // 只有 failover 策略才會有重試
        for (int i = 0; i <= tryCount; i++) {
            Referer refer = referers.get(i % referers.size());
            try {
                request.setRetries(i);
                return refer.call(request);
            } catch (RuntimeException e) {
                // 對於業務異常,直接丟擲
                if (ExceptionUtil.isBizException(e)) {
                    throw e;
                } else if (i >= tryCount) {
                    throw e;
                }
                LoggerUtil.warn(String.format("FailoverHaStrategy Call false for request:%s error=%s", request, e.getMessage()));
            }
        }

        throw new MotanFrameworkException("FailoverHaStrategy.call should not come here!");
    }

    protected List> selectReferers(Request request, LoadBalance loadBalance) {
        List> referers = referersHolder.get();
        referers.clear();
        loadBalance.selectToHolder(request, referers);
        return referers;
    }

}

其二的高可用策略是 failover,實現相對複雜一些,容忍在重試次數內的失敗呼叫。這也是 motan 提供的預設策略。

其他叢集相關的知識點

在 Dubbo 中也有 cluster 這一分層,除了 loadbalance 和 ha 這兩層之外還包含了路由(Router)用來做讀寫分離,應用隔離;合併結果(Merger)用來做響應結果的分組聚合。

在 SpringCloud-Netflix 中整合了 Zuul 來做服務端的負載均衡

參考資料

  1. 幾種簡單的負載均衡演演算法及其Java程式碼實現

  2. 搜尋業務和技術介紹及容錯機制

666. 彩蛋




如果你對 Dubbo 感興趣,歡迎加入我的知識星球一起交流。

知識星球

目前在知識星球(https://t.zsxq.com/2VbiaEu)更新瞭如下 Dubbo 原始碼解析如下:

01. 除錯環境搭建
02. 專案結構一覽
03. 配置 Configuration
04. 核心流程一覽

05. 拓展機制 SPI

06. 執行緒池

07. 服務暴露 Export

08. 服務取用 Refer

09. 註冊中心 Registry

10. 動態編譯 Compile

11. 動態代理 Proxy

12. 服務呼叫 Invoke

13. 呼叫特性 

14. 過濾器 Filter

15. NIO 伺服器

16. P2P 伺服器

17. HTTP 伺服器

18. 序列化 Serialization

19. 叢集容錯 Cluster

20. 優雅停機

21. 日誌適配

22. 狀態檢查

23. 監控中心 Monitor

24. 管理中心 Admin

25. 運維命令 QOS

26. 鏈路追蹤 Tracing


一共 60 篇++

贊(0)

分享創造快樂