這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
輪詢演算法是非常常用的一種調度/負載平衡的演算法。依照百度百科上的解釋:
Round-Robin,輪詢調度,通訊中通道調度的一種策略,該調度策略使使用者輪流使用共用資源,不會考慮瞬時通道條件。從相同數量無線資源(相同調度時間段)被分配給每條通訊鏈路的角度講,輪詢調度可以被視為公平調度。然而,從提供相同服務品質給所有通訊鏈路的角度而言,輪詢調度是不公平的,此時,必須為帶有較差通道條件的通訊鏈路分配更多無線資源(更多時間)。此外,由於輪詢調度在調度過程中不考慮瞬時通道條件,因此它將導致較低的整體系統效能,但與最大載幹比調度相比,在各通訊鏈路間具有更為均衡的服務品質。
更廣泛的輪詢調度應用在廣度的服務調度上面,尤其在面向服務或者是面向微服務的架構中,比可以在很多知名的軟體中看到它的身影,比如LVS、Nginx、Dubblo等。但是正如上面的百度百科中的介紹一樣,輪詢調度有一個很大的問題,那就是它認為所有的服務的效能都是一樣的,每個伺服器都被公平的調度,在伺服器的效能有顯著差別的環境中,效能比較差的伺服器被調度了相同的次數,這不是我們所期望的。所以本文要介紹的是加權的輪詢演算法,輪詢演算法可以看成是加權的輪詢演算法的一個特例,在這種情況下,每個伺服器的權重都是一樣的。
本文介紹了Nginx和LVS的兩種演算法,比較了它們的優缺點,並提供了一個通用的 Go 語言實現的加權輪詢演算法庫: weighted,可以用在負載平衡/調度/微服務網關等場合。
WRR(weighted round-robin) 也是周而復始地輪詢分組服務資源,但不同的是WRR演算法為每個服務資源分派一個權值,當輪詢到某個服務的時候,將根據它所具有權值的大小決定其是否可以提供服務。由於WRR是基於輪詢的,因此它只是在大於一個輪詢周期的時間上才能顯示是公平的。
本文介紹了Nginx和LVS基於權重的輪詢演算法,這兩個演算法都是通過巧妙的運算得到每次要提供的服務物件,但各自又有自己的特點。
Nginx演算法
Nginx基於權重的輪詢演算法的實現可以參考它的一次代碼提交: Upstream: smooth weighted round-robin balancing。
它不但實現了基於權重的輪詢演算法,而且還實現了平滑的演算法。所謂平滑,就是在一段時間內,不僅伺服器被選擇的次數的分布和它們的權重一致,而且調度演算法還比較均勻的選擇伺服器,而不會集中一段時間之內只選擇某一個權重比較高的伺服器。如果使用隨機演算法選擇或者普通的基於權重的輪詢演算法,就比較容易造成某個服務集中被調用壓力過大。
舉個例子,比如權重為{a:5, b:1, c:1)
的一組伺服器,Nginx的平滑的輪詢演算法選擇的序列為{ a, a, b, a, c, a, a }
,這顯然要比{ c, b, a, a, a, a, a }
序列更平滑,更合理,不會造成對a
伺服器的集中訪問。
演算法如下:
on each peer selection we increase current_weight of each eligible peer by its weight, select peer with greatest current_weight and reduce its current_weight by total number of weight points distributed
among peers.
1234567891011121314151617181920212223242526272829 |
func nextWeighted(servers []*Weighted) (best *Weighted) {total := 0for i := 0; i < len(servers); i++ {w := servers[i]if w == nil {continue}w.CurrentWeight += w.EffectiveWeighttotal += w.EffectiveWeightif w.EffectiveWeight < w.Weight {w.EffectiveWeight++}if best == nil || w.CurrentWeight > best.CurrentWeight {best = w}}if best == nil {return nil}best.CurrentWeight -= totalreturn best} |
如果你使用weighted庫,你可以通過下面的幾行代碼就可以使用這個演算法進行調度:
123456789101112 |
func ExampleW_() {w := &W1{}w.Add("a", 5)w.Add("b", 2)w.Add("c", 3)for i := 0; i < 10; i++ {fmt.Printf("%s ", w.Next())}// Output: a c b a a c a b c a} |
LVS演算法
LVS使用的另外一種演算法,它的演算法的介紹可以參考它的網站的wiki。
演算法用虛擬碼表示如下:
1234567891011121314151617181920 |
Supposing that there is a server set S = {S0, S1, …, Sn-1};W(Si) indicates the weight of Si;i indicates the server selected last time, and i is initialized with -1;cw is the current weight in scheduling, and cw is initialized with zero; max(S) is the maximum weight of all the servers in S;gcd(S) is the greatest common divisor of all server weights in S;while (true) { i = (i + 1) mod n; if (i == 0) { cw = cw - gcd(S); if (cw <= 0) { cw = max(S); if (cw == 0) return NULL; } } if (W(Si) >= cw) return Si;} |
可以看到它的代碼邏輯比較簡單,所以效能也很快,但是如果伺服器的權重差別較多,就不會像Nginx那樣比較平滑,可以在短時間內對權重很大的那台伺服器壓力過大。
使用weighted庫和上面一樣簡單,只是把類型W
換成W2
即可:
123456789101112 |
func ExampleW_() {w := &W2{}w.Add("a", 5)w.Add("b", 2)w.Add("c", 3)for i := 0; i < 10; i++ {fmt.Printf("%s ", w.Next())}// Output: a a a c a b c a b c} |
效能比較
可以看到,上面兩種方法的使用都非常的簡單,只需產生一個相應的W
對象,然後加入伺服器和對應的權重即可,通過Next
方法就可以獲得下一個伺服器。
如果伺服器的權重差別很大,出於平滑的考慮,避免短時間內會對伺服器造成衝擊,你可以選擇Nginx的演算法,如果伺服器的差別不是很大,可以考慮使用LVS的演算法,因為測試可以看到它的效能要好於Nginx的演算法:
12 |
BenchmarkW1_Next-4 20000000 50.1 ns/op 0 B/op 0 allocs/opBenchmarkW2_Next-4 50000000 29.1 ns/op 0 B/op 0 allocs/op |
實際上兩者的效能都非常的快,十個伺服器的每次調度也就是幾十納秒的層級,而且沒有額外的對象分配,所以無論使用哪種演算法,這個調度不應成為你整個系統的瓶頸。