微服務-如何做好集羣中服務器的負載均衡

那些負載均衡的面試題

簡單說一下什麼是負載均衡?很多人最怕這種概念性問題

你們公司負載均衡用的什麼?

爲什麼用這種?

它的優缺點

有更好的選擇嗎?

你說這5聯問,誰受得了啊,叢淺到深,一環扣一環,簡直不要了,別怕,仔細閱讀本文,這些問題都會迎刃而解。

什麼是負載均衡?

俗話解釋一下負載均衡:你要在10個餐廳中選一個吃午餐,那麼你選的這個過程就是負載均衡的過程,(面試也是可以這麼說的)。
正規的行話:負載均衡指的是在一個集羣中通過某種硬件設備或者軟件算法來選擇集羣中的一臺機器處理當前請求,以達到大量請求的分散給後端集羣不同機器處理,從而提升高併發能力和容災能力。
百度百科:負載均衡建立在現有網絡結構之上,它提供了一種廉價有效透明的方法擴展網絡設備和服務器的帶寬、增加吞吐量、加強網絡數據處理能力、提高網絡的靈活性和可用性

軟硬件負載均衡詳解

目前負載均衡總的來說分爲三大類:1 硬件設備負載均衡,2 軟件算法負載均衡,3 基於DNS的負載均衡 分別介紹一下這三大類的不同和優缺點。

硬件負載均衡解決方案是直接在服務器和外部網絡間安裝負載均衡設備,這種設備通常稱之爲負載均衡器,由於專門的設備完成專門的任務,獨立於操作系統,整體性能得到大量提高,加上多樣化的負載均衡策略,智能化的流量管理,可達到最佳的負載均衡需求,其主要應用在大型服務器集羣中,比如F5負載均衡器。

軟件負載均衡指的是在服務器的操作系統上安裝負載均衡軟件,從此服務器發出的請求經軟件負載均衡算法路由到後端集羣的某一臺機器上。

DNS負載均衡一般用於地理位置上的負載均衡,比如你的網站在全國範圍內都有海量用戶,那麼當不同用戶訪問網站域名時經過DNS判斷返回給不同地理位置的用戶的不同IP,從而達到就近訪問,流量分擔,提升用戶體驗。

他們的優缺點是什麼呢?

硬件負載均衡一般只是關注網絡流量的負載,至於後端服務器的狀態等他不操心,而且成本貴,往往也是單點,但它也有優點,就是性能好,處理能力強,與操作系統無關性。

軟件負載均衡比較靈活,可調整性大,與軟件算法實現有關係,能夠關注應用服務器的狀態做彙總統計試別的能力,性價比較高,但受軟件安裝的服務器性能影響,同時也沒硬件的性能好,DNS負載均衡也屬於軟件負載均衡的一種。

本文主要分析的也是軟件負載均衡。

常用的負載均衡算法和實現原理

負載均衡中間件現在很多,大家最熟悉的,也是最出名的就屬Nginx了,其次也有很多,比如百度前段時間開源了bfe(百度統一前端),是百度7層流量轉發平臺,還有apache,各種微服務中間件中的負載均衡算法等

我們主要分析下這些中間件負載均衡策略是怎麼實現的?用的什麼算法,重點來了

  1. Random 隨機
  2. Round Robin 輪詢
  3. Weighted Round Robin 加權輪詢
  4. Least Connections 最少連接
  5. Latency-Aware 延遲感知(最小延遲,也就是說那臺機器性能最好,就用那臺)
  6. Source Hashing 源地址散列
  7. Consistency hash 一致性散列(一般在分佈式緩存中比較常見 )

隨機策略指的是在後端集羣機器的IP列表中根據隨機數選擇一個IP作爲此次請求的應答者,當隨機算法足夠好,足夠公平時,在海量請求下,最終後端集羣各個機器承載的流量是均衡, 隨機策略會導致配置較低的機器Down機,從而可能引起雪崩,一般採用隨機算法時建議後端集羣機器配置最好同等的,隨機策略的性能取決與隨機算法的性能。

輪詢策略指的是在集羣中對所有機器編號,假設10臺機器,從0-9,請求來臨時從0號機器開始,後續每來一次請求對編號加1,這樣一直循環,上面的隨機策略其實最後就變成輪詢了,這兩種策略都不關心機器的負載和運行情況,而且對變量操作會引入鎖操作,性能也會下會下降。

加權輪詢策略指的是回給後端集羣每臺機器都分配一個權重,權重高得會承擔更多的流量,相反權重低的分配的流量也會少,這種策略允許後端集羣機器配置差異化,假設有3臺機器(a,b,c),他們的權重分別是(7,2,1),那麼10次請求a機器承擔7次,b機器承擔2次,c機器承擔1次,但是這種承擔法到底怎麼分配呢?有兩種情況如下,我們可以看到第一種請求在a的時候,bc完全空閒,而第二種情況相對均勻一些,Nginx的加權輪詢策略採用的就是第二種情況

  1. (aaaaaaa,bb,c)
  2. (aabaabaaca)

最少連接策略會關注後端集羣各個服務器當前的連接數,選擇一個最少連接數的機器應答當前請求,這種策略實際上關注各個服務器的負載情況,選擇負載最低的機器處理請求,儘可能的提高各個機器的利用率,相對來說比較靈活和智能,實現上也會複雜一些。

延遲感知策略和最少連接是一樣的思想,延遲感知追求極致的性能或者說用戶體驗,總是挑選能夠最快的返回執行結果的機器來訪問,但壞處是當都所有客戶端都認爲某臺服務器最快時,那麼所有請求都發送這臺服務反而可能造成服務壓力過大,性能降低。

源地址散列策略能夠讓同一客戶端的請求或者同一用戶的請求總是請求在後端同一臺機器上,這種算法根據客戶端IP求出Hash值然後對端集羣總數求餘得到值就是服務器集合的下標,一般這種算法用於緩存命中,或者同一會話請求等,但這種算法也有一定的缺點,某一用戶訪問量(黑產)非常高時可能造成服務端壓力過大或者後端服務Down掉,那麼客戶端就會無法訪問,所以也需要一定的降級策略。

一致性散列是在源地址散列的基礎上發展得來的,什麼意思呢?後端集羣有是個3臺機器(a,b,c),客戶端經過散列對服務器總數取餘後總是請求到a機器,那麼當後端集羣新增或者減少一臺機器時,客戶端散列後對服務器總數取餘後就不再是原來的那臺機器了,這樣原來所有的請求散列後對應的後臺機器都發生了變化,一致性散列就是解決這種問題的.

實現一個負載均衡算法

我們挑選上面一種策略用代碼來實現一下,以便讓大家更深入的理解,選擇一個面試常問的策略,1、加權輪詢算法,這個也比較多,Nginx中默認的算法

加權輪詢算法每臺服務器有三個權重:初始配置的權重,當前權重,有效權重,其中初始配置權重和有效權重是不變的,默認情況有效權重等於初始配置權重,當配置文件的初始配置權重改變時,會觸發有效權重改變,只有當前權重是動態變化的。

每次請求到來時都從服務器列表中選擇一個當前權重最高的,之後將選擇出來的服務器當前權重減去所有服務器權重的和重新賦值給該服務器當前權重,這總算法通過不斷遞減當前權重使得所有服務器都有機會服務請求,比較平滑,代碼實現如下

首先定義一個結構體,加權輪詢算法的核心要素必須有服務器初始配置權重,當前權重(權重在實際運行時可能發生變化)

type SeverWeight struct {
   //配置的權重
   ConfigWeight int
   //當前權重
   CurrentWeight int
   //有效權重(值等於ConfigWeight,不過該字段是用一個配置屬性,供前端修改使用)
   EffectiveWeight int
   //服務器ip
   Ip string
}
//加權輪詢算法
type WeightedRoundRobin struct {
   //機器ip和對應的權重
   IpAndWeightedConfig map[string]int
   //服務器和權重信息
   SwSlice []*SeverWeight
}

根據配置信息創建負責均衡對象,初始化各個字段的值

//初始化加權輪詢對象
func NewWeightedRoundRobin(iwc map[string]int) *WeightedRoundRobin {
   if iwc == nil {
      return nil
   }
   SwSlice := make([]*SeverWeight, 0)
   for k, v := range iwc {
      sw := &SeverWeight{ConfigWeight: v, CurrentWeight: 0,
                                 EffectiveWeight: v, Ip: k}
      SwSlice = append(SwSlice, sw)
   }
   return &WeightedRoundRobin{IpAndWeightedConfig: iwc, SwSlice: SwSlice}
}

這個方法是核心,調用這個方法來決定選擇哪個服務器提供服務,方法的核心邏輯是選擇當前權重最大的服務器提供服務,當前權重不斷在變化,每次當前權重的值都等於當前值加上有效值減去所有服務器的有效權重和(這個算法就是不斷遞減當前服務器的當前權重值,使得按照均勻的變化讓所有服務器都能提供服務)

func (wrr *WeightedRoundRobin) Select() (sw *SeverWeight) {
   total := 0 //統計所有服務器權重和
   for _, v := range wrr.SwSlice { //遍歷服務器
      //當前權重加上有效權重
      v.CurrentWeight += v.EffectiveWeight
      total += v.EffectiveWeight
      //當配置值修改的時候的,有效權重循序漸進的增加
      if v.EffectiveWeight < v.ConfigWeight {
         v.EffectiveWeight++
      }
      //把權重最大的賦值給sw(sw是需要返回的對象)
      if sw == nil || v.CurrentWeight > sw.CurrentWeight {
         sw = v
      }
   }
   //當前返回對象的權重-所有服務器權重和
   sw.CurrentWeight = sw.CurrentWeight - total
   return sw
}

我們再來看一下執行的測試結果,根據測試結果相信大家就能夠明白了,根據下面結果我們確實能夠看到返回的服務器IP是均勻的,比較平滑,不會讓權重低的服務器一直等待。

func TestNewWeightedRoundRobin(t *testing.T) {
   //服務器ip和權重配置 
   config :=map[string]int{"10.1": 7, "10.2": 2, "10.3": 1}
   wrr := NewWeightedRoundRobin(config)
   //發送10次請求
   for i := 0; i < 10; i++ {
      sw := wrr.Select()
      t.Log(sw.Ip)//打印每次請求IP
   }
}
//結果:[10.1,10.1,10.2,10.1,10.1,10.3,10.1,10.1,10.2,10.1]

整個代碼我已提交到github上,大家可以github上下載下來實際運行一下,加深理解,我得github地址如下:

https://github.com/sunpengwei1992/go_common/blob/master/algorithm/load_balance.go

任何一種算法深入研究後都能引出一堆問題來,都可以單獨寫一篇文章出來,本篇重點是在讓大家知道這些算法,以至於見到後不會陌生,需要大家在工作中不斷探索,不斷升級自己的認知,提高思維能力。

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