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

vivo網際網路技術發表於2021-05-31

一、背景

vivo 網際網路領域的部分業務在微服務的實踐過程當中基於很多綜合因素的考慮選擇了TARS微服務框架。

官方的描述是:TARS是一個支援多語言、內嵌服務治理功能,與Devops能很好協同的微服務框架。我們在開源的基礎上做了很多適配內部系統的事情,比如與CICD構建釋出系統、單點登入系統的打通,但不是這次我們要介紹的重點。這裡想著重介紹一下我們在現有的負載均衡演算法之外實現的動態負載均衡演算法。

二、什麼是負載均衡

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

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

三、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。

四、為什麼需要動態負載均衡?

我們目前的服務大部分還是跑在以虛擬機器為主的機器上,因此混合部署(一個節點部署多個服務)是常見現象。在混合部署的情況下,如果一個服務程式碼有bug了佔用大量的CPU或記憶體,那麼必然跟他一起部署的服務都會受到影響。

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

從這裡我們也不難看出,要實現動態負載均衡功能的核心其實只需要根據服務的負載動態的調整不同節點的權重就可以了。這其實也是業界常用的一些做法,都是通過週期性地獲取伺服器狀態資訊,動態地計算出當前每臺伺服器應具有的權值。

五、動態負載均衡策略

在這裡我們採用的也是基於各種負載因子的方式對可用節點動態計算權重,將權重返回後複用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當前系統中能獲取到的資料):

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

2、超時率權重:超時率權重 = 初始權重 - 超時率 * 初始權重 * 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框架處發現不同節點採用不同的負載均衡演算法則強制將所有節點調整為輪詢方式。

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

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

七、下一步計劃

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

最後也歡迎大家與我們討論交流,一起為TARS開源做貢獻。

作者:vivo網際網路伺服器團隊-Yang Minshan

相關文章