1. 程式人生 > >Nginx的負載均衡 - 加權輪詢 (Weighted Round Robin)

Nginx的負載均衡 - 加權輪詢 (Weighted Round Robin)

Nginx版本:1.9.1

 

演算法介紹

 

來看一個簡單的Nginx負載均衡配置。

http {
    upstream cluster {
        server a weight=5;
        server b weight=1;
        server c weight=1;
    }
 
    server {
        listen 80;
 
        location / {
            proxy_pass http://cluster;
        }
    }
}
當在upstream配置塊中沒有指定使用的負載均衡演算法時,預設使用的是加權輪詢。

按照上述配置,Nginx每收到7個客戶端的請求,會把其中的5個轉發給後端a,把其中的1個轉發給後端b,

把其中的1個轉發給後端c。

 

這就是所謂的加權輪詢,看起來很簡單,但是最早使用的加權輪詢演算法有個問題,就是7個請求對應的

後端序列是這樣的:{ c, b, a, a, a, a, a },會有5個連續的請求落在後端a上,分佈不太均勻。

目前使用的加權輪詢叫做平滑的加權輪詢(smooth weighted round-robin balancing),它和前者的區別是:

每7個請求對應的後端序列為 { a, a, b, a, c, a, a },轉發給後端a的5個請求現在分散開來,不再是連續的。

 

摘錄此演算法的描述:

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.

To preserve weight reduction in case of failures the effective_weight variable was introduced,

which usually matches peer's weight, but is reduced temoprarily on peer failures.[1]

 

每個後端peer都有三個權重變數,先解釋下它們的含義。

(1) weight

配置檔案中指定的該後端的權重,這個值是固定不變的。

(2) effective_weight

後端的有效權重,初始值為weight。

在釋放後端時,如果發現和後端的通訊過程中發生了錯誤,就減小effective_weight。

此後有新的請求過來時,在選取後端的過程中,再逐步增加effective_weight,最終又恢復到weight。

之所以增加這個欄位,是為了當後端發生錯誤時,降低其權重。

(3) current_weight

後端目前的權重,一開始為0,之後會動態調整。那麼是怎麼個動態調整呢?

每次選取後端時,會遍歷叢集中所有後端,對於每個後端,讓它的current_weight增加它的effective_weight,

同時累加所有後端的effective_weight,儲存為total。

如果該後端的current_weight是最大的,就選定這個後端,然後把它的current_weight減去total。

如果該後端沒有被選定,那麼current_weight不用減小。

 

弄清了三個weight欄位的含義後,加權輪詢演算法可描述為:

1. 對於每個請求,遍歷叢集中的所有可用後端,對於每個後端peer執行:

    peer->current_weight += peer->effecitve_weight。

    同時累加所有peer的effective_weight,儲存為total。

2. 從叢集中選出current_weight最大的peer,作為本次選定的後端。

3. 對於本次選定的後端,執行:peer->current_weight -= total。

 

上述描述可能不太直觀,來看個例子。

現在使用以下的upstream配置塊:

upstream backend {

    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 }

 

通過上述過程,可得以下結論:

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

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

3. 每經過7個請求後,a、b、c的current_weight又回到初始值{ 0, 0, 0 },因此上述流程是不斷迴圈的。

這個平滑的加權輪詢演算法背後應該有數學論證,這裡就不繼續研究了:)

 

本模組的資料結構

 

ngx_http_upstream_rr_peer_t

 

表示一臺後端伺服器。peer就是對端,指的是上游伺服器端。

struct ngx_http_upstream_rr_peer_s {
    struct sockaddr *sockaddr; /* 後端伺服器的地址 */
    socklen_t socklen; /* 地址的長度*/
    ngx_str_t name; /* 後端伺服器地址的字串,server.addrs[i].name */
    ngx_str_t server; /* server的名稱,server.name */
     
    ngx_int_t current_weight; /* 當前的權重,動態調整,初始值為0 */
    ngx_int_t effective_weight; /* 有效的權重,會因為失敗而降低 */
    ngx_int_t weight; /* 配置項指定的權重,固定值 */
 
    ngx_uint_t conns; /* 當前連線數 */
 
    ngx_uint_t fails; /* "一段時間內",已經失敗的次數 */
    time_t accessed; /* 最近一次失敗的時間點 */
    time_t checked; /* 用於檢查是否超過了"一段時間" */
 
    ngx_uint_t max_fails; /* "一段時間內",最大的失敗次數,固定值 */
    time_t fail_timeout; /* "一段時間"的值,固定值 */
    ngx_uint_t down; /* 伺服器永久不可用的標誌 */
    ...
    ngx_http_upstream_rr_peer_t *next; /* 指向下一個後端,用於構成連結串列 */
    ...
} ngx_http_upstream_rr_peer_t;
 

ngx_http_upstream_rr_peers_t

 

表示一組後端伺服器,比如一個後端叢集。

struct ngx_http_upstream_rr_peers_s {
    ngx_uint_t number; /* 後端伺服器的數量 */
    ...
    ngx_uint_t total_weight; /* 所有後端伺服器權重的累加值 */
 
    unsigned single:1; /* 是否只有一臺後端伺服器 */
    unsigned weighted:1; /* 是否使用權重 */
 
    ngx_str_t *name; /* upstream配置塊的名稱 */
 
    ngx_http_upstream_rr_peers_t *next; /* backup伺服器叢集 */
    ngx_http_upstream_rr_peer_t *peer; /* 後端伺服器組成的連結串列 */
};

ngx_http_upstream_rr_peer_data_t

 

儲存每個請求的負載均衡資料。

typedef struct {
     ngx_http_upstream_rr_peers_t *peers; /* 後端叢集 */
     ngx_http_upstream_rr_peer_t *current; /* 當前使用的後端伺服器 */
     uintptr_t *tried; /* 指向後端伺服器的點陣圖 */
     uintptr_t data; /* 當後端伺服器的數量較少時,用於存放其點陣圖 */
} ngx_http_upstream_rr_peer_data_t;
 

通用的資料結構

 

以下是所有負載均衡模組都會使用到的一些資料結構。

 

ngx_http_upstream_server_t

 

表示upstream配置塊中的一條server指令。

typedef struct {
     ngx_str_t name; /* 伺服器的名稱 */
     ngx_addr_t *addrs; /* 伺服器地址的陣列,因為同一個域名可能解析為多個IP */
     ngx_uint_t naddrs; /* 伺服器地址陣列的元素個數 */
     ngx_uint_t weight; /* 伺服器的權重 */
     ngx_uint_t max_fails; /* 一段時間內,訪問失敗的次數超過此值,判定伺服器不可用 */
     time_t fail_timeout; /* 上述“一段時間”的長度 */
 
     unsigned down:1; /* 伺服器不可用的標誌 */
     unsigned backup:1; /* 伺服器為備用的標誌 */
} ngx_http_upstream_server_t;
 

server指令

Syntax: server address [parameters];

Context: upstream

Defines the address and other parameters of a server.

The address can be specified as domain name or IP address, with an optional port, or...

If a port is not specified, the port 80 is used. A domain name that resolves to serveral IP

addresses defines multiple servers at once.

 

server指令支援如下引數

weight = number

    sets the weight of the server, by default 1.

max_fails = number

    By default, the number of unsuccessful attempts is set to 1.

    The zero value disables the accounting of attempts.

fail_timout = number

    By default it is set to 10 seconds.

backup

    marks the server as a backup server.

down

    marks the server as permanently unavailable.

 

ngx_peer_connection_t

 

表示本機和後端的連線,也叫主動連線,用於upstream機制。

struct ngx_peer_connection_s {
     ngx_connection_t *connection; /* 後端連線 */
 
     struct sockaddr *sockaddr; /* 後端伺服器的地址 */
     socklen_t socklen; /* 後端伺服器地址的長度 */
     ngx_str_t *name; /* 後端伺服器的名稱 */
     
     ngx_uint_t tries; /* 對於一個請求,允許嘗試的後端伺服器個數 */
     ngx_event_get_peer_pt get; /* 負載均衡模組實現,用於選取一個後端伺服器 */
     ngx_event_free_peer_pt free; /* 負載均衡模組實現,用於釋放一個後端伺服器 */
     void *data; /* 請求的負載均衡資料,一般指向ngx_http_upstream_<name>_peer_data_t */
     ...
     ngx_addr_t *local; /* 本機地址 */
     int rcvbuf; /* 套接字接收緩衝區的大小 */
     ngx_log_t *log;
 
     unsigned cached:1;
     unsigned log_error:2;
};
ngx_peer_connection_t *pc;

pc->get 就是負載均衡模組中,用於選取後端伺服器的函式。

當選定一臺後端伺服器時,把它的地址資訊儲存在pc->sockaddr、pc->socklen、pc->name。

pc->tries表示對於一個請求,最多能嘗試多少個後端。當嘗試一個後端失敗時,會呼叫pc->free,

一個主要目的就是更新pc->tries,比如pc->tries--。如果pc->tries降到0,就不再嘗試了。

在請求的負載均衡資料初始化函式peer.init中,會給該請求建立一個ngx_http_upstream_<name>_peer_data_t例項,

用於儲存該請求的負載均衡資料,pc->data就是該例項的地址。

 

ngx_http_upstream_peer_t

 

儲存upstream塊的資料,是負載均衡中一個很重要的結構體。

typedef struct {
    /* upstream塊的初始化函式,ngx_http_upstream_module建立main配置時呼叫。
      * 針對每個upstream塊。
      */
    ngx_http_upstream_init_pt init_upstream;
 
    /* request在初始化upstream機制時呼叫,初始化該請求的負載均衡資料。
      * 針對每個request。
      */
    ngx_http_upstream_init_peer_pt init;
 
    void *data; /* 儲存upstream塊的資料 */
} ngx_http_upstream_peer_t;
upstream塊的資料,在解析配置檔案時就建立和初始化了。

如果寫了一個新的負載均衡模組,則需要在它的指令解析函式中指定init_upstream的值,

用來建立和初始化包含該指令的upstream配置塊的資料。

 

ngx_http_upstream_srv_conf_t

 

ngx_http_upstream_module的server塊。

struct ngx_http_upstream_srv_conf_s {
     ngx_http_upstream_peer_t peer; /* upstream塊的資料 */
     void **srv_conf; /* 所有HTTP模組的server conf */
     ngx_array_t *server; /* upstream塊的server陣列,元素型別為ngx_http_upstream_server_t */
     ngx_uint_t flags; /* upstream塊的server指令支援的引數 */
     ngx_str_t host; /* upstream塊的名稱 */
     u_char *file_name;
     ngx_uint_t line;
     in_port_t port; /* 使用的埠 */
     in_port_t default_port; /* 預設的埠 */
     ngx_uint_t no_port;
     ...
};
 
#define ngx_http_conf_upstream_srv_conf(uscf, module) uscf->srv_conf[module.ctx_index]
 

Reference

 

[1]. https://github.com/phusion/nginx/commit/27e94984486058d73157038f7950a0a36ecc6e35
--------------------- 
作者:zhangskd 
來源:CSDN 
原文:https://blog.csdn.net/zhangskd/article/details/50194069 
版權宣告:本文為博主原創文章,轉載請附上博文連結!