首頁  >  文章  >  後端開發  >  Nginx 中 upstream 機制的負載平衡

Nginx 中 upstream 機制的負載平衡

WBOY
WBOY原創
2016-08-08 09:29:551348瀏覽

目錄

    • 負載平衡
    • 加權輪詢
      • 相關結構體
      • 伺服器
      • 初始化後端伺服器
        • 根據權重選擇後端伺服器
          • 釋放後端伺服器
        • 伺服器總結
      • 負載平衡
      • upstream 機制使得Nginx 以反向代理的形式運行,因此Nginx 接收客戶端的請求,並根據客戶端的請求,Nginx 選擇合適後端伺服器來處理該請求。但若存在多台後端伺服器時,Nginx 是根據怎樣的策略來決定哪個後端伺服器負責處理請求?這就涉及到後端伺服器的負載平衡問題。
      • Nginx 的負載平衡策略可以分為兩大類:內建策略 和 擴充策略。內建策略包含 加權輪詢 和 IP hash,在預設情況下這兩種策略會編譯進 Nginx 內核,只需在 Nginx 配置中指明參數即可。擴充策略有第三方模組策略:fair、URL hash、consistent hash等,預設不編譯進 Nginx 核心。本文只講解 加權輪詢 和 IP_hash 策略。
      加權輪詢
    • 加權輪詢策略是先計算每個後端伺服器的權重,然後選擇權重最高的後端伺服器來處理請求。
    相關結構體
ngx_http_upstream_peer_t 結構體
<code><span>typedef</span><span>struct</span> {
    <span>/* 负载均衡的类型 */</span>
    ngx_http_upstream_init_pt        init_upstream;
    <span>/* 负载均衡类型的初始化函数 */</span>
    ngx_http_upstream_init_peer_pt   init;
    <span>/* 指向 ngx_http_upstream_rr_peers_t 结构体 */</span><span>void</span>                            *data;
} ngx_http_upstream_peer_t;</code>
ngx_http_upstream_server_t 結構體
<code><span>/* 服务器结构体 */</span><span>typedef</span><span>struct</span> {
    <span>/* 指向存储 IP 地址的数组,因为同一个域名可能会有多个 IP 地址 */</span>
    ngx_addr_t                      *addrs;
    <span>/* IP 地址数组中元素个数 */</span>
    ngx_uint_t                       naddrs;
    <span>/* 权重 */</span>
    ngx_uint_t                       weight;
    <span>/* 最大失败次数 */</span>
    ngx_uint_t                       max_fails;
    <span>/* 失败时间阈值 */</span>
    time_t                           fail_timeout;

    <span>/* 标志位,若为 1,表示不参与策略选择 */</span><span>unsigned</span>                         down:<span>1</span>;
    <span>/* 标志位,若为 1,表示为备用服务器 */</span><span>unsigned</span>                         backup:<span>1</span>;
} ngx_http_upstream_server_t;</code>

ngx_http_upstream_rr_peer_peer 結構體

ngx_http_upstream_rr_peer_data_t 結構體

<code>typedef <span>struct</span> {
    <span>/* 后端服务器 IP 地址 */</span><span>struct</span> sockaddr                *sockaddr;
    <span>/* 后端服务器 IP 地址的长度 */</span>
    socklen_t                       socklen;
    <span>/* 后端服务器的名称 */</span>
    ngx_str_t                       name;

    <span>/* 后端服务器当前的权重 */</span>
    ngx_int_t                       current_weight;
    <span>/* 后端服务器有效权重 */</span>
    ngx_int_t                       effective_weight;
    <span>/* 配置项所指定的权重 */</span>
    ngx_int_t                       weight;

    <span>/* 已经失败的次数 */</span>
    ngx_uint_t                      fails;
    <span>/* 访问时间 */</span>
    time_t                          accessed;
    time_t                          <span>checked</span>;

    <span>/* 最大失败次数 */</span>
    ngx_uint_t                      max_fails;
    <span>/* 失败时间阈值 */</span>
    time_t                          fail_timeout;

    <span>/* 后端服务器是否参与策略,若为1,表示不参与 */</span>
    ngx_uint_t                      down;          <span>/* unsigned  down:1; */</span><span>#<span>if</span> (NGX_HTTP_SSL)</span>
    ngx_ssl_session_t              *ssl_session;   <span>/* local to a process */</span><span>#<span>endif</span></span>
} ngx_http_upstream_rr_peer_t;</code>

加權輪詢策略的啟動

在Nginx 啟動過程中,解析完http 配置區塊之後,會呼叫各個http 模組對應的初始函數。對於upstream 機制的

ngx_http_upstream_module

模組來說,對應的main 配置初始函數是

ngx_http_upstream_init_main_conf()

如下所示:

<code><span>typedef</span><span>struct</span> ngx_http_upstream_rr_peers_s  ngx_http_upstream_rr_peers_t;

<span>struct</span> ngx_http_upstream_rr_peers_s {
    <span>/* 竞选队列中后端服务器的数量 */</span>
    ngx_uint_t                      number;

 <span>/* ngx_mutex_t                    *mutex; */</span><span>/* 所有后端服务器总的权重 */</span>
    ngx_uint_t                      total_weight;

    <span>/* 标志位,若为 1,表示后端服务器仅有一台,此时不需要选择策略 */</span><span>unsigned</span>                        single:<span>1</span>;
    <span>/* 标志位,若为 1,表示所有后端服务器总的权重等于服务器的数量 */</span><span>unsigned</span>                        weighted:<span>1</span>;

    ngx_str_t                      *name;

    <span>/* 后端服务器的链表 */</span>
    ngx_http_upstream_rr_peers_t   *next;

    <span>/* 特定的后端服务器 */</span>
    ngx_http_upstream_rr_peer_t     peer[<span>1</span>];
};</code>

ngx_http_upstream_x_http_輪詢策略初始函數為

ngx_http_upstream_init_round_robin

。否則的話執行的是

uscfp[i]->peer.init_upstream

指標函數。

當接收到來自客戶端的請求時,Nginx 會呼叫

ngx_http_upstream_init_request

初始化請求的過程中,呼叫

uscf->peer.init(r, uscf)

,對於upstream 機制的加權輪查詢策略來說就是

ngx_http_upstream_init_round_robin_peer,該方法完成請求初始化工作。

<code><span>typedef</span> struct {
    ngx_http_upstream_rr_peers_t   *peers;
    ngx_uint_t                      current;
    uintptr_t                      *tried;
    uintptr_t                       <span><span>data</span>;</span>
} ngx_http_upstream_rr_peer_data_t;</code>
完成客戶端請求的初始化工作之後,會選擇一個後端伺服器來處理該請求,選擇後端伺服器由函數 ngx_http_upstream_get_round_robin_peer 實作。函數在

ngx_event_connect_peer中被呼叫。

<code><span>for</span> (i = <span>0</span>; i < umcf->upstreams.nelts; i++) {  

    init = uscfp[i]<span>-></span>peer.init_upstream ? 
        uscfp[i]<span>-></span>peer.<span>init_upstream</span>:                                  ngx_http_upstream_init_round_robin;  

    <span>if</span> (init(cf, uscfp[i]) != NGX_OK) {  
            <span>return</span> NGX_CONF_ERROR;  
    }  
}  </code>
當已經選擇一台後端伺服器來處理請求時,接下來就會測試該後端伺服器的連接情況,測試連接由函數 ngx_http_upstream_test_connect 實現,在函數 ngx_http_upstream_send_request 實現,在函數

ngx_http_upstream_send_request 中被呼叫。

<code>static void  
ngx_http_upstream_init_request(ngx_http_request_t *r)  
{  
<span>...</span><span>if</span> (uscf->peer.init(r, uscf) != NGX_OK) {  
        ngx_http_upstream_finalize_request(r, u,  
                                           NGX_HTTP_INTERNAL_SERVER_ERROR);  
        <span>return</span>;  
    }  

    ngx_http_upstream_connect(r, u);  
}  </code>
若連線測試失敗,會由函數 ngx_http_upstream_next 啟動再次測試,若測試成功,則處理完請求之後,會呼叫 ngx_http_upstream_free_round_robin_peer 釋放後端伺服器。

加權輪詢工作流程加權輪詢策略的基本工作流程是:初始化負載平衡伺服器列表,初始化後端伺服器,選擇合適後端伺服器處理請求,釋放後端伺服器。

初始化伺服器清單

初始化伺服器清單由函數ngx_http_upstream_init_round_robin 實現,該函數的執行流程如下所示:

第一種情況:若非備用機製配置中配置了伺服器配置伺服器列表,並將其掛載到us->peer.data 中;初始化備用伺服器列表,並將其掛載到

peers->next

中;

第二種情況:採用預設的方式proxy_pass 配置後端伺服器位址;

初始化非備用伺服器列表,並將其掛載到us->peer.data

中;

    • 該方法執行完成之後得到的結構如下圖所示:
    • <code>ngx_int_t  
      ngx_event_connect_peer(ngx_peer_connection_t *pc)  
      {  
      <span>...</span>/* 调用 ngx_http_upstream_get_round_robin_peer  */
         rc = pc->get(pc, pc->data);  
         <span>if</span> (rc != NGX_OK) {  
             <span>return</span> rc;  
         }  
      
        s = ngx_socket(pc->sockaddr->sa_family, SOCK_STREAM, <span>0</span>);  
      <span>...</span>}  </code>
      選擇合適的後端伺服器
    • 在選擇合適的後端伺服器處理客戶請求時,首先需要初始化後端伺服器,然後根據後端伺服器的權重,選擇權重最高的後端伺服器來處理請求。
    初始化后端服务器

    上面的初始化负载服务器列表的全局初始化工作完成之后,当客户端发起请求时,Nginx 会选择一个合适的后端服务器来处理该请求。在本轮选择后端服务器之前,Nginx 会对后端服务器进行初始化工作,该工作由函数 ngx_http_upstream_init_round_robin_peer 实现。

    ngx_http_upstream_init_round_robin_peer 函数的执行流程如下所示:

    • 计算服务器列表中的数量 n,n 的取值为 非备用后端服务器数量 与 备用后端服务器数量 较大者;
    • 根据 n 的取值,创建一个位图 tried,该位图是记录后端服务器是否被选择过:
      • 若 n 不大于 32, 只需要在一个 int 中记录所有后端服务器的状态;
      • 若 n 大于 32,则需要从内存池申请内存来存储所有后端服务器的状态;
    • 设置 ngx_peer_connection_t 结构体中 get 的回调方法为 ngx_http_upstream_get_round_robin_peerfree 的回调方法为 ngx_http_upstream_free_round_robin_peer,设置 tries 重试连接的次数为非备用后端服务器的个数;
    <code><span>/* 当客户端发起请求时,upstream 机制为本轮选择一个后端服务器做初始化工作 */</span>
    ngx_int_t
    ngx_http_upstream_init_round_robin_peer(ngx_http_request_t <span>*</span>r,
        ngx_http_upstream_srv_conf_t <span>*</span>us)
    {
        ngx_uint_t                         n;
        ngx_http_upstream_rr_peer_data_t  <span>*</span>rrp;
    
        <span>/* 注意:r->upstream->peer 是 ngx_peer_connection_t 结构体类型 */</span><span>/* 获取当前客户端请求中的 ngx_http_upstream_rr_peer_data_t 结构体 */</span>
        rrp <span>=</span> r<span>-></span>upstream<span>-></span>peer<span>.</span><span>data</span>;
    
        <span>if</span> (rrp <span>==</span><span>NULL</span>) {
            rrp <span>=</span> ngx_palloc(r<span>-></span>pool, sizeof(ngx_http_upstream_rr_peer_data_t));
            <span>if</span> (rrp <span>==</span><span>NULL</span>) {
                <span>return</span> NGX_ERROR;
            }
    
            r<span>-></span>upstream<span>-></span>peer<span>.</span><span>data</span><span>=</span> rrp;
        }
    
        <span>/* 获取非备用后端服务器列表 */</span>
        rrp<span>-></span>peers <span>=</span> us<span>-></span>peer<span>.</span><span>data</span>;
        rrp<span>-></span>current <span>=</span><span>0</span>;<span>/* 若采用遍历方式选择后端服务器时,作为起始节点编号 */</span><span>/* 下面是取值 n,若存在备用后端服务器列表,则 n 的值为非备用后端服务器个数 与 备用后端服务器个数 之间的较大者 */</span>    n <span>=</span> rrp<span>-></span>peers<span>-></span>number;
    
        <span>if</span> (rrp<span>-></span>peers<span>-></span>next <span>&&</span> rrp<span>-></span>peers<span>-></span>next<span>-></span>number <span>></span> n) {
            n <span>=</span> rrp<span>-></span>peers<span>-></span>next<span>-></span>number;
        }
    
        <span>/* rrp->tried 是一个位图,在本轮选择中,该位图记录各个后端服务器是否被选择过 */</span><span>/*
         * 如果后端服务器数量 n 不大于 32,则只需在一个 int 中即可记录下所有后端服务器状态;
         * 如果后端服务器数量 n 大于 32,则需在内存池中申请内存来存储所有后端服务器的状态;
         */</span><span>if</span> (n <span><=</span><span>8</span><span>*</span> sizeof(uintptr_t)) {
            rrp<span>-></span>tried <span>=</span><span>&</span>rrp<span>-></span><span>data</span>;
            rrp<span>-></span><span>data</span><span>=</span><span>0</span>;
    
        } <span>else</span> {
            n <span>=</span> (n <span>+</span> (<span>8</span><span>*</span> sizeof(uintptr_t) <span>-</span><span>1</span>)) <span>/</span> (<span>8</span><span>*</span> sizeof(uintptr_t));
    
            rrp<span>-></span>tried <span>=</span> ngx_pcalloc(r<span>-></span>pool, n <span>*</span> sizeof(uintptr_t));
            <span>if</span> (rrp<span>-></span>tried <span>==</span><span>NULL</span>) {
                <span>return</span> NGX_ERROR;
            }
        }
    
        <span>/*
         * 设置 ngx_peer_connection_t 结构体中 get 、free 的回调方法;
         * 设置 ngx_peer_connection_t 结构体中 tries 重试连接的次数为非备用后端服务器的个数;
         */</span>
        r<span>-></span>upstream<span>-></span>peer<span>.</span>get <span>=</span> ngx_http_upstream_get_round_robin_peer;
        r<span>-></span>upstream<span>-></span>peer<span>.</span>free <span>=</span> ngx_http_upstream_free_round_robin_peer;
        r<span>-></span>upstream<span>-></span>peer<span>.</span>tries <span>=</span> rrp<span>-></span>peers<span>-></span>number;
    <span>#if</span> (NGX_HTTP_SSL)
        r<span>-></span>upstream<span>-></span>peer<span>.</span>set_session <span>=</span>
                                   ngx_http_upstream_set_round_robin_peer_session;
        r<span>-></span>upstream<span>-></span>peer<span>.</span>save_session <span>=</span>
                                   ngx_http_upstream_save_round_robin_peer_session;
    <span>#endif</span><span>return</span> NGX_OK;
    }</code>
    根据权重选择后端服务器

    完成后端服务器的初始化工作之后,根据各个后端服务器的权重来选择权重最高的后端服务器处理客户端请求,由函数 ngx_http_upstream_get_round_robin_peer 实现。

    ngx_http_upstream_get_round_robin_peer 函数的执行流程如下所示:

    • 步骤1:检查 ngx_http_upstream_rr_peers_t 结构体中的 single 标志位:
      • single 标志位为 1,表示只有一台非备用后端服务器:
        • 接着检查该非备用后端服务器的 down 标志位:
          • down 标志位为 0,则选择该非备用后端服务器来处理请求;
          • down 标志位为 1, 该非备用后端服务器表示不参与策略选择,则跳至 goto failed 步骤从备用后端服务器列表中选择后端服务器来处理请求;
      • single 标志位为 0,则表示不止一台非备用后端服务器,则调用 ngx_http_upstream_get_peer 方法根据非备用后端服务器的权重来选择一台后端服务器处理请求,根据该方法的返回值 peer 进行判断:
        • 若该方法返回值 peer = NULL, 表示在非备用后端服务器列表中没有选中到合适的后端服务器来处理请求,则跳至 goto failed 从备用后端服务器列表中选择一台后端服务器来处理请求;
        • 若该方法返回值 peer 不为 NULL,表示已经选中了合适的后端服务器来处理请求,设置该服务器重试连接次数 tries,并 return NGX_OK 从当前函数返回;
    • goto failed 步骤:计算备用后端服务器在位图 tried 中的位置 n,并把他们在位图的记录都设置为 0,此时,把备用后端服务器列表作为参数调用 ngx_http_upstream_get_round_robin_peer 选择一台后端服务器来处理请求;
    <code>/* 选择一个后端服务器来处理请求 */
    <span>ngx_int_t</span><span>ngx_http_upstream_get_round_robin_peer</span>(ngx_peer_connection_t *pc, void *<span><span>data</span>)</span>
    {
        ngx_http_upstream_rr_peer_data_t  *rrp = <span><span>data</span>;</span>    ngx_int_t                      rc;
        ngx_uint_t                     i, n;
        ngx_http_upstream_rr_peer_t   *peer;
        ngx_http_upstream_rr_peers_t  *peers;
    
        ngx_log_debug1(<span>NGX_LOG_DEBUG_HTTP</span>, pc->log, <span>0</span>,
                       <span>"get rr peer, try: %ui"</span>, pc->tries);
    
        /* ngx_lock_mutex(rrp->peers->mutex); */
    
        pc->cached = <span>0</span>;
        pc->connection = <span>NULL</span>;
    
        /*
         * 检查 ngx_http_upstream_rr_peers_t 结构体中的 single 标志位;
         * 若 single 标志位为 <span>1</span>,表示只有一台非备用后端服务器,
         * 接着检查该非备用后端服务器的 down 标志位,若 down 标志位为 <span>0</span>,则选择该非备用后端服务器来处理请求;
         * 若 down 标志位为 <span>1</span>, 该非备用后端服务器表示不参与策略选择,
         * 则跳至 goto failed 步骤从备用后端服务器列表中选择后端服务器来处理请求;
         */
        <span>if</span> (rrp->peers->single) {
            peer = &rrp->peers->peer[<span>0</span>];
    
            <span>if</span> (peer->down) {
                goto failed;
            }
    
        } <span>else</span> {/* 若 single 标志位为 <span>0</span>,表示不止一台非备用后端服务器 */
    
            /* there are several peers */
    
            /* 根据非备用后端服务器的权重来选择一台后端服务器处理请求 */
            peer = ngx_http_upstream_get_peer(rrp);
    
            <span>if</span> (peer == <span>NULL</span>) {
                /*
                 * 若从非备用后端服务器列表中没有选择一台合适的后端服务器处理请求,
                 * 则 goto failed 从备用后端服务器列表中选择一台后端服务器来处理请求;
                 */
                goto failed;
            }
    
            ngx_log_debug2(<span>NGX_LOG_DEBUG_HTTP</span>, pc->log, <span>0</span>,
                           <span>"get rr peer, current: %ui %i"</span>,
                           rrp->current, peer->current_weight);
        }
    
        /*
         * 若从非备用后端服务器列表中已经选到了一台合适的后端服务器处理请求;
         * 则获取该后端服务器的地址信息;
         */
        pc->sockaddr = peer->sockaddr;/* 获取被选中的非备用后端服务器的地址 */
        pc->socklen = peer->socklen;/* 获取被选中的非备用后端服务器的地址长度 */
        pc->name = &peer->name;/* 获取被选中的非备用后端服务器的域名 */
    
        /* ngx_unlock_mutex(rrp->peers->mutex); */
    
        /*
         * 检查被选中的非备用后端服务器重试连接的次数为 <span>1</span>,且存在备用后端服务器列表,
         * 则将该非备用后端服务器重试连接的次数设置为 备用后端服务器个数加 <span>1</span>;
         * 否则不用重新设置;
         */
        <span>if</span> (pc->tries == <span>1</span> && rrp->peers->next) {
            pc->tries += rrp->peers->next->number;
        }
    
        /* 到此,表示已经选择到了一台合适的非备用后端服务器来处理请求,则成功返回 */
        return <span>NGX_OK</span>;
    
    <span>failed</span>:
          /*
           * 若从非备用后端服务器列表中没有选择到后端服务器处理请求,
           * 若存在备用后端服务器,则从备用后端服务器列表中选择一台后端服务器来处理请求;
           */
    
        peers = rrp->peers;
    
        /* 若存在备用后端服务器,则从备用后端服务器列表中选择一台后端服务器来处理请求;*/
        <span>if</span> (peers->next) {
    
            /* ngx_unlock_mutex(peers->mutex); */
    
            ngx_log_debug0(<span>NGX_LOG_DEBUG_HTTP</span>, pc->log, <span>0</span>, <span>"backup servers"</span>);
    
            /* 获取备用后端服务器列表 */
            rrp->peers = peers->next;
            /* 把后端服务器重试连接的次数 tries 设置为备用后端服务器个数 number */
            pc->tries = rrp->peers->number;
    
            /* 计算备用后端服务器在位图中的位置 n */
            n = (rrp->peers->number + (<span>8</span> * sizeof(uintptr_t) - <span>1</span>))
                    / (<span>8</span> * sizeof(uintptr_t));
    
            /* 初始化备用后端服务器在位图 rrp->tried[i] 中的值为 <span>0</span> */
            for (i = <span>0</span>; i < n; i++) {
                 rrp->tried[i] = <span>0</span>;
            }
    
            /* 把备用后端服务器列表当前非备用后端服务器列表递归调用 ngx_http_upstream_get_round_robin_peer 选择一台后端服务器 */
            rc = ngx_http_upstream_get_round_robin_peer(pc, rrp);
    
            /* 若选择成功则返回 */
            <span>if</span> (rc != <span>NGX_BUSY</span>) {
                return rc;
            }
    
            /* ngx_lock_mutex(peers->mutex); */
        }
    
        /*
         * 若从备用后端服务器列表中也没有选择到一台后端服务器处理请求,
         * 则重新设置非备用后端服务器连接失败的次数 fails 为 <span>0</span> ,以便重新被选择;
         */
        /* all peers failed, mark them <span>as</span> live for quick recovery */
    
        for (i = <span>0</span>; i < peers->number; i++) {
            peers->peer[i].fails = <span>0</span>;
        }
    
        /* ngx_unlock_mutex(peers->mutex); */
    
        pc->name = peers->name;
    
        /* 选择失败,则返回 */
        return <span>NGX_BUSY</span>;
    }</code>

    ngx_http_upstream_get_peer 函数是计算每一个后端服务器的权重值,并选择一个权重最高的后端服务器。

    ngx_http_upstream_get_peer 函数的执行流程如下所示:

    • for 循环遍历后端服务器列表,计算当前后端服务器在位图 tried 中的位置 n,判断当前服务器是否在位图中记录过,若已经记录过,则 continue 继续检查下一个后端服务器;若没有记录过则继续当前后端服务器检查;
    • 检查当前后端服务器的标志位 down,若该标志位为 1,表示该后端服务器不参与选择策略,则 continue 继续检查下一个后端服务器;若该标志位为 0,继续当前后端服务器的检查;
    • 若当前后端服务器的连接失败次数已到达 max_failes,且睡眠时间还没到 fail_timedout ,则 continue 继续检查下一个后端服务器;否则继续当前后端服务器的检查;
    • 计算当前后端服务器的权重,设置当前后端服务器的权重 current_weight 的值为原始值加上 effective_weight;设置总的权重 total 为原始值加上 effective_weight;
    • 判断当前后端服务器是否异常,若 effective_weight 小于 weight,表示正常,则调整 effective_weight 的值 effective_weight++;
    • 根据权重在后端服务器列表中选择权重最高的后端服务器 best;
    • 计算被选中后端服务器咋服务器列表中的为 i,记录被选中后端服务器在 ngx_http_upstream_rr_peer_data_t 结构体 current 成员的值,在释放后端服务器时会用到该值;
    • 计算被选中后端服务器在位图中的位置 n,并在该位置记录 best 后端服务器已经被选中过;
    • 更新被选中后端服务器的权重,并返回被选中的后端服务器 best;
    <code><span>/* 根据后端服务器的权重来选择一台后端服务器处理请求 */</span><span>static</span> ngx_http_upstream_rr_peer_t *
    ngx_http_upstream_get_peer(ngx_http_upstream_rr_peer_data_t *rrp)
    {
        time_t                        now;
        uintptr_t                     m;
        ngx_int_t                     total;
        ngx_uint_t                    i, n;
        ngx_http_upstream_rr_peer_t  *peer, *best;
    
        now = ngx_time();
    
        best = <span>NULL</span>;
        total = <span>0</span>;
    
        <span>/* 遍历后端服务器列表 */</span><span>for</span> (i = <span>0</span>; i < rrp->peers->number; i++) {
    
            <span>/* 计算当前后端服务器在位图中的位置 n */</span>
            n = i / (<span>8</span> * <span>sizeof</span>(uintptr_t));
            m = (uintptr_t) <span>1</span> << i % (<span>8</span> * <span>sizeof</span>(uintptr_t));
    
            <span>/* 当前后端服务器在位图中已经有记录,则不再次被选择,即 continue 检查下一个后端服务器 */</span><span>if</span> (rrp->tried[n] & m) {
                <span>continue</span>;
            }
    
            <span>/* 若当前后端服务器在位图中没有记录,则可能被选中,接着计算其权重 */</span>
            peer = &rrp->peers->peer[i];
    
            <span>/* 检查当前后端服务器的 down 标志位,若为 1 表示不参与策略选择,则 continue 检查下一个后端服务器 */</span><span>if</span> (peer->down) {
                <span>continue</span>;
            }
    
            <span>/*
             * 当前后端服务器的 down 标志位为 0,接着检查当前后端服务器连接失败的次数是否已经达到 max_fails;
             * 且睡眠的时间还没到 fail_timeout,则当前后端服务器不被选择,continue 检查下一个后端服务器;
             */</span><span>if</span> (peer->max_fails
                && peer->fails >= peer->max_fails
                && now - peer->checked <= peer->fail_timeout)
            {
                <span>continue</span>;
            }
    
            <span>/* 若当前后端服务器可能被选中,则计算其权重 */</span><span>/*
             * 在上面初始化过程中 current_weight = 0,effective_weight = weight;
             * 此时,设置当前后端服务器的权重 current_weight 的值为原始值加上 effective_weight;
             * 设置总的权重为原始值加上 effective_weight;
             */</span>
            peer->current_weight += peer->effective_weight;
            total += peer->effective_weight;
    
            <span>/* 服务器正常,调整 effective_weight 的值 */</span><span>if</span> (peer->effective_weight < peer->weight) {
                peer->effective_weight++;
            }
    
            <span>/* 若当前后端服务器的权重 current_weight 大于目前 best 服务器的权重,则当前后端服务器被选中 */</span><span>if</span> (best == <span>NULL</span> || peer->current_weight > best->current_weight) {
                best = peer;
            }
        }
    
        <span>if</span> (best == <span>NULL</span>) {
            <span>return</span><span>NULL</span>;
        }
    
        <span>/* 计算被选中后端服务器在服务器列表中的位置 i */</span>
        i = best - &rrp->peers->peer[<span>0</span>];
    
        <span>/* 记录被选中后端服务器在 ngx_http_upstream_rr_peer_data_t 结构体 current 成员的值,在释放后端服务器时会用到该值 */</span>
        rrp->current = i;
    
        <span>/* 计算被选中后端服务器在位图中的位置 */</span>
        n = i / (<span>8</span> * <span>sizeof</span>(uintptr_t));
        m = (uintptr_t) <span>1</span> << i % (<span>8</span> * <span>sizeof</span>(uintptr_t));
    
        <span>/* 在位图相应的位置记录被选中后端服务器 */</span>
        rrp->tried[n] |= m;
    
        <span>/* 更新被选中后端服务器的权重 */</span>
        best->current_weight -= total;
    
        <span>if</span> (now - best->checked > best->fail_timeout) {
            best->checked = now;
        }
    
        <span>/* 返回被选中的后端服务器 */</span><span>return</span> best;
    }</code>

    释放后端服务器

    成功连接后端服务器并且正常处理完成客户端请求后需释放后端服务器,由函数 ngx_http_upstream_free_round_robin_peer 实现。

    <code>/* 释放后端服务器 */
    <span>void</span><span>ngx_http_upstream_free_round_robin_peer</span>(ngx_peer_connection_t *pc, void *<span><span>data</span>,</span>
        ngx_uint_t state)
    {
        ngx_http_upstream_rr_peer_data_t  *rrp = <span><span>data</span>;</span>    time_t                       now;
        ngx_http_upstream_rr_peer_t  *peer;
    
        ngx_log_debug2(<span>NGX_LOG_DEBUG_HTTP</span>, pc->log, <span>0</span>,
                       <span>"free rr peer %ui %ui"</span>, pc->tries, state);
    
        /* <span>TODO</span>: <span>NGX_PEER_KEEPALIVE</span> */
    
        /* 若只有一个后端服务器,则设置 ngx_peer_connection_t 结构体成员 tries 为 <span>0</span>,并 return 返回 */
        <span>if</span> (rrp->peers->single) {
            pc->tries = <span>0</span>;
            return;
        }
    
        /* 若不止一个后端服务器,则执行以下程序 */
    
        /* 获取已经被选中的后端服务器 */
        peer = &rrp->peers->peer[rrp->current];
    
        /*
         * 若在本轮被选中的后端服务器在进行连接测试时失败,或者在处理请求过程中失败,
         * 则需要进行重新选择后端服务器;
         */
        <span>if</span> (state & <span>NGX_PEER_FAILED</span>) {
            now = ngx_time();
    
            /* ngx_lock_mutex(rrp->peers->mutex); */
    
            peer->fails++;/* 增加当前后端服务器失败的次数 */
            /* 设置当前后端服务器访问的时间 */
            peer->accessed = now;
            peer->checked = now;
    
            <span>if</span> (peer->max_fails) {
                /* 由于当前后端服务器失败,表示发生异常,此时降低 effective_weight 的值 */
                peer->effective_weight -= peer->weight / peer->max_fails;
            }
    
            ngx_log_debug2(<span>NGX_LOG_DEBUG_HTTP</span>, pc->log, <span>0</span>,
                           <span>"free rr peer failed: %ui %i"</span>,
                           rrp->current, peer->effective_weight);
    
            /* 保证 effective_weight 的值不能小于 <span>0</span> */
            <span>if</span> (peer->effective_weight < <span>0</span>) {
                peer->effective_weight = <span>0</span>;
            }
    
            /* ngx_unlock_mutex(rrp->peers->mutex); */
    
        } <span>else</span> {/* 若被选中的后端服务器成功处理请求,并返回,则将其 fails 设置为 <span>0</span> */
    
            /* mark peer live <span>if</span> check passed */
    
            /* 若 fail_timeout 时间已过,则将其 fails 设置为 <span>0</span> */
            <span>if</span> (peer->accessed < peer->checked) {
                peer->fails = <span>0</span>;
            }
        }
    
        /* 减少 tries 的值 */
        <span>if</span> (pc->tries) {
            pc->tries<span>--;</span>
        }
    
        /* ngx_unlock_mutex(rrp->peers->mutex); */
    }</code>

    IP 哈希

    IP 哈希策略选择后端服务器时,将来自同一个 IP 地址的客户端请求分发到同一台后端服务器处理。在 Nginx 中,IP 哈希策略的一些初始化工作是基于加权轮询策略的,这样减少了一些工作。

    Nginx 使用 IP 哈希负载均衡策略时,在进行策略选择之前由 ngx_http_upstream_init_ip_hash 函数进行全局初始化工作,其实该函数也是调用加权轮询策略的全局初始化函数。当一个客户端请求过来时,Nginx 将调用 ngx_http_upstream_init_ip_hash_peer() 为选择后端服务器处理该请求做初始化工作。在多次哈希选择失败后,Nginx 会将选择策略退化到加权轮询。

    ngx_http_upstream_get_ip_hash_peer 函数会在选择后端服务器时计算客户端请求 IP 地址的哈希值,并根据哈希值得到被选中的后端服务器,判断其是否可用,如果可用则保存服务器地址,若不可用则在上次哈希选择结果基础上再次进行哈希选择。如果哈希选择失败次数达到 20 次以上,此时回退到采用轮询策略进行选择。

    初始化后端服务器列表

    初始化服务器列表工作是调用加权轮询策略的初始化函数,只是最后设置 IP 哈希的回调方法为 ngx_http_upstream_init_ip_hash_peer

    <code><span>static</span> ngx_int_t
    ngx_http_upstream_init_ip_hash(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
    {
        <span>/* 调用加权轮询策略的初始化函数 */</span><span>if</span> (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK) {
            <span>return</span> NGX_ERROR;
        }
    
        <span>/* 由于 ngx_http_upstream_init_round_robin 方法的选择后端服务器处理客户请求的初始化函数
         * 为 us->peer.init = ngx_http_upstream_init_round_robin_peer;
         */</span><span>/* 重新设置 ngx_http_upstream_peer_t 结构体中 init 的回调方法为  ngx_http_upstream_init_ip_hash_peer */</span>    us->peer.init = ngx_http_upstream_init_ip_hash_peer;
    
        <span>return</span> NGX_OK;
    }</code>

    选择后端服务器

    选择后端服务器之前会调用函数 ngx_http_upstream_init_ip_hash_peer 进行一些服务器初始化工作。最终由函数 ngx_http_upstream_get_ip_hash_peer 进行 IP 哈希选择。

    ngx_http_upstream_init_ip_hash_peer 函数执行流程:

    • 调用加权轮询策略的初始化函数 ngx_http_upstream_init_round_robin_peer
    • 设置 IP hash 的决策函数为 ngx_http_upstream_get_ip_hash_peer
    • 保存客户端 IP 地址;
    • 初始化 ngx_http_upstream_ip_hash_peer_data_t结构体成员 hash 值为 89;tries 重试连接次数为 0;get_rr_peer 为加权轮询的决策函数 ngx_http_upstream_get_round_robin_peer

    ngx_http_upstream_get_ip_hash_peer 函数执行流程:

    • 若重试连接的次数 tries 大于 20,或 只有一台后端服务器,则直接调用加权轮询策略 get_rr_peer 选择当前后端服务器处理请求;
    • 计算 IP 地址的 hash 值,下面根据哈希值进行选择后端服务器;
    • ngx_http_upstream_rr_peers_t 结构体中 weighted 标志位为 1,则被选中的后端服务器在后端服务器列表中的位置为 hash 值与后端服务器数量的余数 p;
    • ngx_http_upstream_rr_peers_t 结构体中 weighted 标志位为 0,首先计算 hash 值与后端服务器总权重的余数 w; 将 w 值减去后端服务器的权重,直到有一个后端服务器使 w 值小于 0,则选中该后端服务器来处理请求,并记录在后端服务器列表中的位置 p;
    • 计算被选中后端服务器在位图中的位置 n;
    • 若当前被选中的后端服务器已经在位图记录过,则跳至 goto next 执行;
    • 检查当前被选中后端服务器的 down 标志位:
      • 若该标志位为1,则跳至 goto next_try 执行;
      • 若 down 标志位为 0,接着检查当前被选中后端服务器失败连接次数是否到达 max_fails,若已经达到 max_fails 次,并且睡眠时间还没到 fail_timeout,则跳至 goto next_try 执行;
    • 若不满足以上条件,表示选择成功,记录当前后端服务器的地址信息,把当前后端服务器记录在位图相应的位置,更新哈希值,最后返回该后端服务器;
    • goto next:tries 重试连接的次数加 1,并判断 tries 是否大于阈值 20,若大于,则采用加权轮询策略;
    • goto next_try :把当前后端服务器记录在位图中,减少当前后端服务器重试连接的次数 tries;
    <code>static ngx_int_t
    ngx_http_upstream_init_ip_hash_peer(ngx_http_request_t <span>*</span>r,
        ngx_http_upstream_srv_conf_t <span>*</span>us)
    {
        struct sockaddr_in                     <span>*</span>sin;
    <span>#if</span> (NGX_HAVE_INET6)
        struct sockaddr_in6                    <span>*</span>sin6;
    <span>#endif</span>
        ngx_http_upstream_ip_hash_peer_data_t  <span>*</span>iphp;
    
        <span>/* 分配 ngx_http_upstream_ip_hash_peer_data_t 结构体内存空间 */</span>
        iphp <span>=</span> ngx_palloc(r<span>-></span>pool, sizeof(ngx_http_upstream_ip_hash_peer_data_t));
        <span>if</span> (iphp <span>==</span><span>NULL</span>) {
            <span>return</span> NGX_ERROR;
        }
    
        r<span>-></span>upstream<span>-></span>peer<span>.</span><span>data</span><span>=</span><span>&</span>iphp<span>-></span>rrp;
    
        <span>/* 调用加权轮询策略的初始化函数 ngx_http_upstream_init_round_robin_peer */</span><span>if</span> (ngx_http_upstream_init_round_robin_peer(r, us) <span>!=</span> NGX_OK) {
            <span>return</span> NGX_ERROR;
        }
    
        <span>/* 设置 IP hash 的决策函数  */</span>
        r<span>-></span>upstream<span>-></span>peer<span>.</span>get <span>=</span> ngx_http_upstream_get_ip_hash_peer;
    
        switch (r<span>-></span>connection<span>-></span>sockaddr<span>-></span>sa_family) {
    
        <span>/* 保存客户端 IP 地址 */</span><span>/* IPv4 地址 */</span><span>case</span> AF_INET:
            sin <span>=</span> (struct sockaddr_in <span>*</span>) r<span>-></span>connection<span>-></span>sockaddr;
            iphp<span>-></span>addr <span>=</span> (u_char <span>*</span>) <span>&</span>sin<span>-></span>sin_addr<span>.</span>s_addr;
            iphp<span>-></span>addrlen <span>=</span><span>3</span>;
            break;
    
        <span>/* IPv6 地址 */</span><span>#if</span> (NGX_HAVE_INET6)
        <span>case</span> AF_INET6:
            sin6 <span>=</span> (struct sockaddr_in6 <span>*</span>) r<span>-></span>connection<span>-></span>sockaddr;
            iphp<span>-></span>addr <span>=</span> (u_char <span>*</span>) <span>&</span>sin6<span>-></span>sin6_addr<span>.</span>s6_addr;
            iphp<span>-></span>addrlen <span>=</span><span>16</span>;
            break;
    <span>#endif</span><span>/* 非法地址 */</span>
        default:
            iphp<span>-></span>addr <span>=</span> ngx_http_upstream_ip_hash_pseudo_addr;
            iphp<span>-></span>addrlen <span>=</span><span>3</span>;
        }
    
        <span>/* 初始化 ngx_http_upstream_ip_hash_peer_data_t结构体成员 */</span>
        iphp<span>-></span>hash <span>=</span><span>89</span>;
        iphp<span>-></span>tries <span>=</span><span>0</span>;
        <span>/* 这个是设置为加权轮询策略的决策函数 */</span>
        iphp<span>-></span>get_rr_peer <span>=</span> ngx_http_upstream_get_round_robin_peer;
    
        <span>return</span> NGX_OK;
    }</code>
    <code>/* 选择后端服务器处理请求 */
    <span>static</span> ngx_int_t
    <span>ngx_http_upstream_get_ip_hash_peer</span>(ngx_peer_connection_t *pc, void *<span><span>data</span>)</span>
    {
        ngx_http_upstream_ip_hash_peer_data_t  *iphp = <span><span>data</span>;</span>    time_t                        now;
        ngx_int_t                     w;
        uintptr_t                     m;
        ngx_uint_t                    i, n, p, hash;
        ngx_http_upstream_rr_peer_t  *peer;
    
        ngx_log_debug1(<span>NGX_LOG_DEBUG_HTTP</span>, pc->log, <span>0</span>,
                       <span>"get ip hash peer, try: %ui"</span>, pc->tries);
    
        /* <span>TODO</span>: cached */
    
        /* 若重试连接的次数 tries 大于 <span>20</span>,或 只有一台后端服务器,则直接调用加权轮询策略选择当前后端服务器处理请求 */
        <span>if</span> (iphp->tries > <span>20</span> || iphp->rrp.peers->single) {
            return iphp->get_rr_peer(pc, &iphp->rrp);
        }
    
        now = ngx_time();
    
        pc->cached = <span>0</span>;
        pc->connection = <span>NULL</span>;
    
        hash = iphp->hash;
    
        for ( ;; ) {
    
            /* 计算 <span>IP</span> 地址的 hash 值 */
            for (i = <span>0</span>; i < (ngx_uint_t) iphp->addrlen; i++) {
                hash = (hash * <span>113</span> + iphp->addr[i]) % <span>6271</span>;/* hash 函数 */
            }
    
            /* 以下是根据 hash 值选择合适的后端服务器来处理请求 */
    
            /* 若 ngx_http_upstream_rr_peers_t 结构体中 weighted 标志位为 <span>1</span>,
             * 表示所有后端服务器的总权重 与 后端服务器的数量 相等,
             * 则被选中的后端服务器在后端服务器列表中的位置为 hash 值与后端服务器数量的余数 p;
             */
            <span>if</span> (!iphp->rrp.peers->weighted) {
                p = hash % iphp->rrp.peers->number;
    
            } <span>else</span> {
                /* 若 ngx_http_upstream_rr_peers_t 结构体中 weighted 标志位为 <span>0</span>,
                 * 首先计算 hash 值与后端服务器总权重的余数 w;
                 * 将 w 值减去后端服务器的权重,直到有一个后端服务器使 w 值小于 <span>0</span>,
                 * 则选中该后端服务器来处理请求,并记录在后端服务器列表中的位置 p;
                 */
                w = hash % iphp->rrp.peers->total_weight;
    
                for (i = <span>0</span>; i < iphp->rrp.peers->number; i++) {
                    w -= iphp->rrp.peers->peer[i].weight;
                    <span>if</span> (w < <span>0</span>) {
                        break;
                    }
                }
    
                p = i;
            }
    
            /* 计算被选中后端服务器在位图中的位置 n */
            n = p / (<span>8</span> * sizeof(uintptr_t));
            m = (uintptr_t) <span>1</span> << p % (<span>8</span> * sizeof(uintptr_t));
    
            /* 若当前被选中的后端服务器已经在位图记录过,则跳至 goto next 执行 */
            <span>if</span> (iphp->rrp.tried[n] & m) {
                goto next;
            }
    
            ngx_log_debug2(<span>NGX_LOG_DEBUG_HTTP</span>, pc->log, <span>0</span>,
                           <span>"get ip hash peer, hash: %ui %04XA"</span>, p, m);
    
            /* 获取当前被选中的后端服务器 */
            peer = &iphp->rrp.peers->peer[p];
    
            /* ngx_lock_mutex(iphp->rrp.peers->mutex); */
    
            /* 检查当前被选中后端服务器的 down 标志位,若该标志位为<span>1</span>,则跳至 goto next_try 执行 */
            <span>if</span> (peer->down) {
                goto next_try;
            }
    
            /* 若 down 标志位为 <span>0</span>,接着检查当前被选中后端服务器失败连接次数是否到达 max_fails,
             * 若已经达到 max_fails 次,并且睡眠时间还没到 fail_timeout,则跳至 goto next_try 执行;
             */
            <span>if</span> (peer->max_fails
                && peer->fails >= peer->max_fails
                && now - peer->checked <= peer->fail_timeout)
            {
                goto next_try;
            }
    
            /* 若不满足以上条件,则表示选择后方服务器成功 */
            break;
    
        next_try:
    
            /* 把当前后端服务器记录在位图中 */
            iphp->rrp.tried[n] |= m;
    
            /* ngx_unlock_mutex(iphp->rrp.peers->mutex); */
    
            /* 减少当前后端服务器重试连接的次数 */
            pc->tries<span>--;</span>    next:
    
            /* tries 重试连接的次数加 <span>1</span>,并判断 tries 是否大于阈值 <span>20</span>,若大于,则采用加权轮询策略 */
            <span>if</span> (++iphp->tries >= <span>20</span>) {
                return iphp->get_rr_peer(pc, &iphp->rrp);
            }
        }
    
        /* 到此已经成功选择了后端服务器来处理请求 */
    
        /* 记录当前后端服务器在后端服务器列表中的位置,该位置方便释放后端服务器调用 */
        iphp->rrp.current = p;
    
        /* 记录当前后端服务器的地址信息 */
        pc->sockaddr = peer->sockaddr;
        pc->socklen = peer->socklen;
        pc->name = &peer->name;
    
        <span>if</span> (now - peer->checked > peer->fail_timeout) {
            peer->checked = now;
        }
    
        /* ngx_unlock_mutex(iphp->rrp.peers->mutex); */
    
        /* 把当前后端服务器记录在位图相应的位置 */
        iphp->rrp.tried[n] |= m;
        /* 记录 hash 值 */
        iphp->hash = hash;
    
        return <span>NGX_OK</span>;
    }</code>

    总结

    加权轮询策略:不依赖于客户端的任何信息,完全依靠后端服务器的情况来进行选择。但是同一个客户端的多次请求可能会被分配到不同的后端服务器进行处理,无法满足做会话保持的应用的需求。

    IP哈希策略:把同一个 IP 地址的客户端请求分配到同一台服务器处理,避免了加权轮询无法适用会话保持的需求。但是来自同一的 IP 地址的请求比较多时,会导致某台后端服务器的压力可能非常大,而其他后端服务器却空闲的不均衡情况。

    以上就介绍了Nginx 中 upstream 机制的负载均衡,包括了方面的内容,希望对PHP教程有兴趣的朋友有所帮助。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn