負載均衡之加權輪詢算法(轉)

一:輪詢算法(Round-Robin)nginx

  輪詢算法是最簡單的一種負載均衡算法。它的原理是把來自用戶的請求輪流分配給內部的服務器:從服務器1開始,直到服務器N,而後從新開始循環。算法

  算法的優勢是其簡潔性,它無需記錄當前全部鏈接的狀態,因此它是一種無狀態調度。後端

 

  假設有N臺服務器:S = {S1, S2, …, Sn},一個指示變量i表示上一次選擇的服務器ID。變量i被初始化爲N-1。該算法的僞代碼以下:數組

  j = i;
  do
  {
    j = (j + 1) mod n;
    i = j;
    return Si;
  } while (j != i);
  return NULL;服務器


  輪詢算法假設全部服務器的處理性能都相同,不關心每臺服務器的當前鏈接數和響應速度。當請求服務間隔時間變化比較大時,輪詢算法容易致使服務器間的負載不平衡。因此此種均衡算法適合於服務器組中的全部服務器都有相同的軟硬件配置而且平均服務請求相對均衡的狀況。

負載均衡

 

二:加權輪詢算法(WeightedRound-Robin)函數

  輪詢算法並無考慮每臺服務器的處理能力,實際中可能並非這種狀況。因爲每臺服務器的配置、安裝的業務應用等不一樣,其處理能力會不同。因此,加權輪詢算法的原理就是:根據服務器的不一樣處理能力,給每一個服務器分配不一樣的權值,使其可以接受相應權值數的服務請求。性能

 

  首先看一個簡單的Nginx負載均衡配置。spa

http {
upstream cluster {
server a weight=1;
server b weight=2;
server c weight=4;
}
...
}
  按照上述配置,Nginx每收到7個客戶端的請求,會把其中的1個轉發給後端a,把其中的2個轉發給後端b,把其中的4個轉發給後端c。code

 

  加權輪詢算法的結果,就是要生成一個服務器序列。每當有請求到來時,就依次從該序列中取出下一個服務器用於處理該請求。好比針對上面的例子,加權輪詢算法會生成序列{c, c, b, c, a, b, c}。這樣,每收到7個客戶端的請求,會把其中的1個轉發給後端a,把其中的2個轉發給後端b,把其中的4個轉發給後端c。收到的第8個請求,從新從該序列的頭部開始輪詢。

  總之,加權輪詢算法要生成一個服務器序列,該序列中包含n個服務器。n是全部服務器的權重之和。在該序列中,每一個服務器的出現的次數,等於其權重值。而且,生成的序列中,服務器的分佈應該儘量的均勻。好比序列{a, a, a, a, a, b, c}中,前五個請求都會分配給服務器a,這就是一種不均勻的分配方法,更好的序列應該是:{a, a, b, a, c, a, a}。

  下面介紹兩種加權輪詢算法:

 

1:普通加權輪詢算法

         這種算法的原理是:在服務器數組S中,首先計算全部服務器權重的最大值max(S),以及全部服務器權重的最大公約數gcd(S)。

         index表示本次請求到來時,選擇的服務器的索引,初始值爲-1;current_weight表示當前調度的權值,初始值爲max(S)。

         當請求到來時,從index+1開始輪詢服務器數組S,找到其中權重大於current_weight的第一個服務器,用於處理該請求。記錄其索引到結果序列中。

  在輪詢服務器數組時,若是到達了數組末尾,則從新從頭開始搜索,而且減少current_weight的值:current_weight -= gcd(S)。若是current_weight等於0,則將其重置爲max(S)。

 

  該算法的實現代碼以下:

#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> typedef struct { int weight; char name[2]; }server; int getsum(int *set, int size) { int i = 0; int res = 0; for (i = 0; i < size; i++) res += set[i]; return res; } int gcd(int a, int b) { int c; while(b) { c = b; b = a % b; a = c; } return a; } int getgcd(int *set, int size) { int i = 0; int res = set[0]; for (i = 1; i < size; i++) res = gcd(res, set[i]); return res; } int getmax(int *set, int size) { int i = 0; int res = set[0]; for (i = 1; i < size; i++) { if (res < set[i]) res = set[i]; } return res; } int lb_wrr__getwrr(server *ss, int size, int gcd, int maxweight, int *i, int *cw) { while (1) { *i = (*i + 1) % size; if (*i == 0) { *cw = *cw - gcd; if (*cw <= 0) { *cw = maxweight; if (*cw == 0) { return -1; } } } if (ss[*i].weight >= *cw) { return *i; } } } void wrr(server *ss, int *weights, int size) { int i = 0; int gcd = getgcd(weights, size); int max = getmax(weights, size); int sum = getsum(weights, size); int index = -1; int curweight = 0; for (i = 0; i < sum; i++) { lb_wrr__getwrr(ss, size, gcd, max, &(index), &(curweight)); printf("%s(%d) ", ss[index].name, ss[index].weight); } printf("\n"); return; } server *initServers(char **names, int *weights, int size) { int i = 0; server *ss = calloc(size, sizeof(server)); for (i = 0; i < size; i++) { ss[i].weight = weights[i]; memcpy(ss[i].name, names[i], 2); } return ss; } int main() { int i = 0; int weights[] = {1, 2, 4}; char *names[] = {"a", "b", "c"}; int size = sizeof(weights) / sizeof(int); server *ss = initServers(names, weights, size); printf("server is "); for (i = 0; i < size; i++) { printf("%s(%d) ", ss[i].name, ss[i].weight); } printf("\n"); printf("\nwrr sequence is "); wrr(ss, weights, size); return; }

 


  上面的代碼中,算法的核心部分就是wrr和lb_wrr__getwrr函數。在wrr函數中,首先計算全部服務器權重的最大公約數gcd,權重最大值max,以及權重之和sum。

  初始時,index爲-1,curweight爲0,而後依次調用lb_wrr__getwrr函數,獲得本次選擇的服務器索引index。

 

  算法的核心思想體如今lb_wrr__getwrr函數中。以例子說明更好理解一些:對於服務器數組{a(1), b(2), c(4)}而言,gcd爲1,maxweight爲4。

  第1次調用該函數時,i(index)爲-1,cw(current_weight)爲0,進入循環後,i首先被置爲0,所以cw被置爲maxweight。從i開始輪詢服務器數組ss,第一個權重大於等於cw的服務器是c,所以,i被置爲2,並返回其值。

  第2次調用該函數時,i爲2,cw爲maxweight。進入循環後,i首先被置爲0,所以cw被置爲cw-gcd,也就是3。從i開始輪詢服務器數組ss,第一個權重大於等於cw的服務器仍是c,所以,i被置爲2,並返回其值。

  第3次調用該函數時,i爲2,cw爲3。進入循環後,i首先被置爲0,所以cw被置爲cw-gcd,也就是2。從i開始輪詢服務器數組ss,第一個權重大於等於cw的服務器是b,所以,i被置爲1,並返回其值。

  第4次調用該函數時,i爲1,cw爲2。進入循環後,i首先被置爲2,從i開始輪詢服務器數組ss,第一個權重大於等於cw的服務器是c,所以,i被置爲2,並返回其值。

  第5次調用該函數時,i爲2,cw爲2。進入循環後,i首先被置爲0,所以cw被置爲cw-gcd,也就是1。從i開始輪詢服務器數組ss,第一個權重大於等於cw的服務器是a,所以,i被置爲0,並返回其值。

  第6次調用該函數時,i爲0,cw爲1。進入循環後,i首先被置爲1,從i開始輪詢服務器數組ss,第一個權重大於等於cw的服務器是b,所以,i被置爲1,並返回其值。

  第7次調用該函數時,i爲1,cw爲1。進入循環後,i首先被置爲2,從i開始輪詢服務器數組ss,第一個權重大於等於cw的服務器是c,所以,i被置爲2,並返回其值。

 

  通過7(1+2+4)次調用以後,每一個服務器被選中的次數正好是其權重值。上面程序的運行結果以下:

server is a(1) b(2) c(4)

wrr sequence is c(4) c(4) b(2) c(4) a(1) b(2) c(4)

         若是有新的請求到來,第8次調用該函數時,i爲2,cw爲1。進入循環後,i首先被置爲0,cw被置爲cw-gcd,也就是0,所以cw被重置爲maxweight。這種狀況就跟第一次調用該函數時同樣了。所以,7次是一個輪迴,7次以後,重複以前的過程。

 

        這背後的數學原理,本身思考了一下,總結以下:

  current_weight的值,其變化序列就是一個等差序列:max, max-gcd, max-2gcd, …, 0(max),將current_weight從max變爲0的過程,稱爲一個輪迴。

  針對每一個current_weight,該算法就是要把服務器數組從頭至尾掃描一遍,將其中權重大於等於current_weight的全部服務器填充到結果序列中。掃描完一遍服務器數組以後,將current_weight變爲下一個值,再一次從頭至尾掃描服務器數組。

  在current_weight變化過程當中,無論current_weight當前爲什麼值,具備max權重的服務器每次確定會被選中。所以,具備max權重的服務器會在序列中出現max/gcd次(等差序列中的項數)。

  更通常的,當current_weight變爲x以後,權重爲x的服務器,在current_weight接下來的變化過程當中,每次都會被選中,所以,具備x權重的服務器,會在序列中出現x/gcd次。因此,每一個服務器在結果序列中出現的次數,是與其權重成正比的,這就是符合加權輪詢算法的要求了。

 

2:平滑的加權輪詢

         上面的加權輪詢算法有個缺陷,就是某些狀況下生成的序列是不均勻的。好比針對這樣的配置:

http {
upstream cluster {
server a weight=5;
server b weight=1;
server c weight=1;
}
...
}
         生成的序列是這樣的:{a,a, a, a, a, c, b}。會有5個連續的請求落在後端a上,分佈不太均勻。

 

  在Nginx源碼中,實現了一種叫作平滑的加權輪詢(smooth weighted round-robin balancing)的算法,它生成的序列更加均勻。好比前面的例子,它生成的序列爲{ a, a, b, a, c, a, a},轉發給後端a的5個請求如今分散開來,再也不是連續的。

 

  該算法的原理以下:

  每一個服務器都有兩個權重變量:

  a:weight,配置文件中指定的該服務器的權重,這個值是固定不變的;

  b:current_weight,服務器目前的權重。一開始爲0,以後會動態調整。

 

  每次當請求到來,選取服務器時,會遍歷數組中全部服務器。對於每一個服務器,讓它的current_weight增長它的weight;

  同時累加全部服務器的weight,並保存爲total。

  遍歷完全部服務器以後,若是該服務器的current_weight是最大的,就選擇這個服務器處理本次請求。最後把該服務器的current_weight減去total。

 

  上述描述可能不太直觀,來看個例子。好比針對這樣的配置:

http {
upstream cluster {
server a weight=4;
server b weight=2;
server c weight=1;
}
...
}
  按照這個配置,每7個客戶端請求中,a會被選中4次、b會被選中2次、c會被選中1次,且分佈平滑。咱們來算算看是否是這樣子的。

  initial  current_weight  of a, b, c is {0, 0, 0}

 

  經過上述過程,可得如下結論:

  a:7個請求中,a、b、c分別被選取了四、二、1次,符合它們的權重值。

  b:7個請求中,a、b、c被選取的順序爲a, b,a, c, a, b, a,分佈均勻,權重大的後端a沒有被連續選取。

  c:每通過7個請求後,a、b、c的current_weight又回到初始值{0, 0,0},所以上述流程是不斷循環的。

 

  根據該算法實現的代碼以下:

#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> typedef struct { int weight; int cur_weight; char name[3]; }server; int getsum(int *set, int size) { int i = 0; int res = 0; for (i = 0; i < size; i++) res += set[i]; return res; } server *initServers(char **names, int *weights, int size) { int i = 0; server *ss = calloc(size+1, sizeof(server)); for (i = 0; i < size; i++) { ss[i].weight = weights[i]; memcpy(ss[i].name, names[i], 3); ss[i].cur_weight = 0; } return ss; } int getNextServerIndex(server *ss, int size) { int i ; int index = -1; int total = 0; for (i = 0; i < size; i++) { ss[i].cur_weight += ss[i].weight; total += ss[i].weight; if (index == -1 || ss[index].cur_weight < ss[i].cur_weight) { index = i; } } ss[index].cur_weight -= total; return index; } void wrr_nginx(server *ss, int *weights, int size) { int i = 0; int index = -1; int sum = getsum(weights, size); for (i = 0; i < sum; i++) { index = getNextServerIndex(ss, size); printf("%s(%d) ", ss[index].name, ss[index].weight); } printf("\n"); } int main() { int i = 0; int weights[] = {4, 2, 1}; char *names[] = {"a", "b", "c"}; int size = sizeof(weights) / sizeof(int); server *ss = initServers(names, weights, size); printf("server is "); for (i = 0; i < size; i++) { printf("%s(%d) ", ss[i].name, ss[i].weight); } printf("\n"); printf("\nwrr_nginx sequence is "); wrr_nginx(ss, weights, size); return; }

 

 

         上述代碼的運行結果以下:

server is a(4) b(2) c(1)

wrr_nginx sequence is a(4) b(2) a(4) c(1) a(4) b(2) a(4)


         若是服務器配置爲:{a(5),b(1), c(1)},則運行結果以下:

server is a(5) b(1) c(1)

wrr_nginx sequence is a(5) a(5) b(1) a(5) c(1) a(5) a(5)

         可見,該算法生成的序列確實更加均勻。

相關文章
相關標籤/搜索