基於開源Tars的動態負載均衡實踐

1、背景

vivo 互聯網領域的部分業務在微服務的實踐過程中基於不少綜合因素的考慮選擇了TARS微服務框架。java

官方的描述是:TARS是一個支持多語言、內嵌服務治理功能,與Devops能很好協同的微服務框架。咱們在開源的基礎上作了不少適配內部系統的事情,好比與CICD構建發佈系統、單點登陸系統的打通,但不是此次咱們要介紹的重點。這裏想着重介紹一下咱們在現有的負載均衡算法以外實現的動態負載均衡算法。算法

2、什麼是負載均衡

維基百科的定義以下:負載平衡(Load balancing)是一種電子計算機技術,用來在多個計算機(計算機集羣)、網絡鏈接、CPU、磁盤驅動器或其餘資源中分配負載,以達到優化資源使用、最大化吞吐率、最小化響應時間、同時避免過載的目的。使用帶有負載平衡的多個服務器組件,取代單一的組件,能夠經過冗餘提升可靠性。負載平衡服務一般是由專用軟件和硬件來完成。主要做用是將大量做業合理地分攤到多個操做單元上進行執行,用於解決互聯網架構中的高併發和高可用的問題。docker

這段話很好理解,本質上是一種解決分佈式服務應對大量併發請求時流量分配問題的方法。緩存

3、TARS 支持哪些負載均衡算法

TARS支持三種負載均衡算法,基於輪詢的負載均衡算法、基於權重分配的輪詢負載均衡算法、一致性hash負載均衡算法。函數入口是selectAdapterProxy,代碼在 TarsCpp 文件裏,感興趣的能夠從這個函數開始深刻了解。服務器

3.1 基於輪詢的負載均衡算法

基於輪詢的負載均衡算法實現很簡單,原理就是將全部提供服務的可用 ip 造成一個調用列表。當有請求到來時將請求按時間順序逐個分配給請求列表中的每一個機器,若是分配到了最後列表中的最後一個節點則再從列表第一個節點從新開始循環。這樣就達到了流量分散的目的,儘量的平衡每一臺機器的負載,提升機器的使用效率。這個算法基本上能知足大量的分佈式場景了,這也是TARS默認的負載均衡算法。網絡

可是若是每一個節點的處理能力不同呢?雖然流量是均分的,可是因爲中間有處理能力較弱的節點,這些節點仍然存在過載的可能性。因而咱們就有了下面這種負載均衡算法。架構

3.2 基於權重分配的輪詢負載均衡算法

權重分配顧名思義就是給每一個節點賦值一個固定的權重,這個權重表示每一個節點能夠分配到流量的機率。舉個例子,有5個節點,配置的權重分別是4,1,1,1,3,若是有100個請求過來,則對應分配到的流量也分別是40,10,10,10,30。這樣就實現了按配置的權重來分配客戶端的請求了。這裏有個細節須要注意一下,在實現加權輪詢的時候必定要是平滑的。也就是說假若有10個請求,不能前4次都落在第1個節點上。併發

業界已經有了不少平滑加權輪詢的算法,感興趣的讀者能夠自行搜索瞭解。負載均衡

3.3 一致性Hash

不少時候在一些存在緩存的業務場景中,咱們除了對流量平均分配有需求,同時也對同一個客戶端請求應該儘量落在同一個節點上有需求。框架

假設有這樣一種場景,某業務有1000萬用戶,每一個用戶有一個標識id和一組用戶信息。用戶標識id和用戶信息是一一對應的關係,這個映射關係存在於DB中,而且其它全部模塊須要去查詢這個映射關係並從中獲取一些必要的用戶字段信息。在大併發的場景下,直接請求DB系統確定是抗不住的,因而咱們天然就想到用緩存的方案去解決。是每一個節點都須要去存儲全量的用戶信息麼?雖然能夠,但不是最佳方案,萬一用戶規模從1000萬上升到1億呢?很顯然這種解決方案隨着用戶規模的上升,變得捉襟見肘,很快就會出現瓶頸甚至沒法知足需求。因而就須要一致性hash算法來解決這個問題。一致性hash算法提供了相同輸入下請求儘量落在同一個節點的保證。

爲何說是儘量?由於節點會出現故障下線,也有可能由於擴容而新增,一致性hash算法是可以在這種變化的狀況下作到儘可能減小緩存重建的。TARS使用的hash算法有兩種,一種是對key求md5值後,取地址偏移作異或操做,另外一種是ketama hash。

4、爲何須要動態負載均衡?

咱們目前的服務大部分仍是跑在以虛擬機爲主的機器上,所以混合部署(一個節點部署多個服務)是常見現象。在混合部署的狀況下,若是一個服務代碼有bug了佔用大量的CPU或內存,那麼必然跟他一塊兒部署的服務都會受到影響。

那麼若是仍然採用上述三種負載均衡算法的狀況下,就有問題了,被影響的機器仍然會按指定的規則分配到流量。也許有人會想,基於權重的輪詢負載均衡算法不是能夠配置有問題的節點爲低權重而後分配到不多的流量麼?確實能夠,可是這種方法每每處理不及時,若是是發生在半夜呢?而且在故障解除後須要再手動配置回去,增長了運維成本。所以咱們須要一種動態調整的負載均衡算法來自動調整流量的分配,儘量的保證這種異常狀況下的服務質量。

從這裏咱們也不難看出,要實現動態負載均衡功能的核心其實只須要根據服務的負載動態的調整不一樣節點的權重就能夠了。這其實也是業界經常使用的一些作法,都是經過週期性地獲取服務器狀態信息,動態地計算出當前每臺服務器應具備的權值。

5、動態負載均衡策略

在這裏咱們採用的也是基於各類負載因子的方式對可用節點動態計算權重,將權重返回後複用TARS靜態權重節點選擇算法。咱們選擇的負載因子有:接口5分鐘平均耗時/接口5分鐘超時率/接口5分鐘異常率/CPU負載/內存使用率/網卡負載。負載因子支持動態擴展。

總體功能圖以下:

5.1 總體交互時序圖

rpc調用時,EndpointManager按期得到可用節點集合。節點附帶權重信息。業務在發起調用時根據業務方指定的負載均衡算法選擇對應的節點;

RegistrServer按期從db/監控中習獲取超時率和平均耗時等信息。從其它平臺(好比CMDB)得到機器負載類信息,好比cpu/內存等。全部計算過程線程異步執行緩存在本地;

EndpointManager根據得到的權重執行選擇策略。下圖爲節點權重變化對請求流量分配的影響:

5.2 節點更新和負載均衡策略

節點全部性能數據每60秒更新一次,使用線程定時更新;

計算全部節點權重值和取值範圍,存入內存緩存;

主調獲取到節點權重信息後執行當前靜態權重負載均衡算法選擇節點;

兜底策略:若是全部節點要重都同樣或者異常則默認採用輪詢的方式選擇節點;

5.3 負載的計算方式

負載計算方式:每一個負載因子設定權重值和對應的重要程度(按百分比表示),根據具體的重要程度調整設置,最後會根據全部負載因子算出的權重值乘對應的百分比後算出總值。好比:耗時權重爲10,超時率權重爲20,對應的重要程度分別爲40%和60%,則總和爲10 0.4 + 20 0.6 = 16。對應每一個負載因子計算的方式以下(當前咱們只使用了平均耗時和超時率這兩個負載因子,這也是最容易在TARS當前系統中能獲取到的數據):

一、按每臺機器在總耗時的佔比反比例分配權重:權重 = 初始權重 *(耗時總和 - 單臺機器平均耗時)/ 耗時總和(不足之處在於並不徹底是按耗時比分配流量);

二、超時率權重:超時率權重 = 初始權重 - 超時率 初始權重 90%,折算90%是由於100%超時時也多是由於流量過大致使的,保留小流量試探請求;

對應代碼實現以下:

void LoadBalanceThread::calculateWeight(LoadCache &loadCache)
{
    for (auto &loadPair : loadCache)
    {
        ostringstream log;
        const auto ITEM_SIZE(static_cast<int>(loadPair.second.vtBalanceItem.size()));
        int aveTime(loadPair.second.aveTimeSum / ITEM_SIZE);
        log << "aveTime: " << aveTime << "|"
            << "vtBalanceItem size: " << ITEM_SIZE << "|";
        for (auto &loadInfo : loadPair.second.vtBalanceItem)
        {
            // 按每臺機器在總耗時的佔比反比例分配權重:權重 = 初始權重 *(耗時總和 - 單臺機器平均耗時)/ 耗時總和
            TLOGDEBUG("loadPair.second.aveTimeSum: " << loadPair.second.aveTimeSum << endl);
            int aveTimeWeight(loadPair.second.aveTimeSum ? (DEFAULT_WEIGHT * ITEM_SIZE * (loadPair.second.aveTimeSum - loadInfo.aveTime) / loadPair.second.aveTimeSum) : 0);
            aveTimeWeight = aveTimeWeight <= 0 ? MIN_WEIGHT : aveTimeWeight;
            // 超時率權重:超時率權重 = 初始權重 - 超時率 * 初始權重 * 90%,折算90%是由於100%超時時也多是由於流量過大致使的,保留小流量試探請求
            int timeoutRateWeight(loadInfo.succCount ? (DEFAULT_WEIGHT - static_cast<int>(loadInfo.timeoutCount * TIMEOUT_WEIGHT_FACTOR / (loadInfo.succCount           
+ loadInfo.timeoutCount))) : (loadInfo.timeoutCount ? MIN_WEIGHT : DEFAULT_WEIGHT));
            // 各種權重乘對應比例後相加求和
            loadInfo.weight = aveTimeWeight * getProportion(TIME_CONSUMING_WEIGHT_PROPORTION) / WEIGHT_PERCENT_UNIT
                              + timeoutRateWeight * getProportion(TIMEOUT_WEIGHT_PROPORTION) / WEIGHT_PERCENT_UNIT ;
 
            log << "aveTimeWeight: " << aveTimeWeight << ", "
                << "timeoutRateWeight: " << timeoutRateWeight << ", "
                << "loadInfo.weight: " << loadInfo.weight << "; ";
        }
 
        TLOGDEBUG(log.str() << "|" << endl);
    }
}

相關代碼實如今RegistryServer,代碼文件以下圖:

核心實現是LoadBalanceThread類,歡迎你們指正。

5.4 使用方式

  1. 在Servant管理處配置-w -v 參數便可支持動態負載均衡,不配置則不啓用。

以下圖:

  1. 注意:須要所有節點啓用才生效,不然rpc框架處發現不一樣節點採用不一樣的負載均衡算法則強制將全部節點調整爲輪詢方式。

6、動態負載均衡適用的場景

若是你的服務是跑在Docker容器上的,那可能不太須要動態負載均衡這個特性。直接使用Docker的調度能力進行服務的自動伸縮,或者在部署上直接將Docker分配的粒度拆小,讓服務獨佔docker就不存在相互影響的問題了。若是服務是混合部署的,而且服務大機率會受到其它服務的影響,好比某個服務直接把cpu佔滿,那建議開啓這個功能。

7、下一步計劃

目前的實現中只考慮了平均耗時和超時率兩個因子,這能在必定程度上反映服務能力提供狀況,但不夠徹底。所以,將來咱們還會考慮加入cpu使用狀況這些能更好反映節點負載的指標。以及,在主調方根據返回碼來調整權重的一些策略。

最後也歡迎你們與咱們討論交流,一塊兒爲TARS開源作貢獻。

做者:vivo互聯網服務器團隊-Yang Minshan
相關文章
相關標籤/搜索