欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  后端开发

Nginx 中 upstream 机制的负载均衡

程序员文章站 2022-04-28 18:29:49
...
目录

    • 负载均衡
    • 加权轮询
      • 相关结构体
      • 加权轮询策略的启动
      • 加权轮询工作流程
        • 初始化服务器列表
        • 选择合适的后端服务器
          • 初始化后端服务器
          • 根据权重选择后端服务器
        • 释放后端服务器
    • IP 哈希
      • 初始化后端服务器列表
      • 选择后端服务器
    • 总结

负载均衡

upstream 机制使得 Nginx 以反向代理的形式运行,因此 Nginx 接收客户端的请求,并根据客户端的请求,Nginx 选择合适后端服务器来处理该请求。但是若存在多台后端服务器时,Nginx 是根据怎样的策略来决定哪个后端服务器负责处理请求?这就涉及到后端服务器的负载均衡问题。
Nginx 的负载均衡策略可以划分为两大类:内置策略 和 扩展策略。内置策略包含 加权轮询 和 IP hash,在默认情况下这两种策略会编译进 Nginx 内核,只需在 Nginx 配置中指明参数即可。扩展策略有第三方模块策略:fair、URL hash、consistent hash等,默认不编译进 Nginx 内核。本文只讲解 加权轮询 和 IP_hash 策略。

加权轮询

加权轮询策略是先计算每个后端服务器的权重,然后选择权重最高的后端服务器来处理请求。

相关结构体

ngx_http_upstream_peer_t 结构体

typedefstruct {
    /* 负载均衡的类型 */
    ngx_http_upstream_init_pt        init_upstream;
    /* 负载均衡类型的初始化函数 */
    ngx_http_upstream_init_peer_pt   init;
    /* 指向 ngx_http_upstream_rr_peers_t 结构体 */void                            *data;
} ngx_http_upstream_peer_t;

ngx_http_upstream_server_t 结构体

/* 服务器结构体 */typedefstruct {
    /* 指向存储 IP 地址的数组,因为同一个域名可能会有多个 IP 地址 */
    ngx_addr_t                      *addrs;
    /* IP 地址数组中元素个数 */
    ngx_uint_t                       naddrs;
    /* 权重 */
    ngx_uint_t                       weight;
    /* 最大失败次数 */
    ngx_uint_t                       max_fails;
    /* 失败时间阈值 */
    time_t                           fail_timeout;

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

ngx_http_upstream_rr_peer_t 结构体

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

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

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

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

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

ngx_http_upstream_rr_peers_t 结构体

typedefstruct ngx_http_upstream_rr_peers_s  ngx_http_upstream_rr_peers_t;

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

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

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

    ngx_str_t                      *name;

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

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

ngx_http_upstream_rr_peer_data_t 结构体

typedef struct {
    ngx_http_upstream_rr_peers_t   *peers;
    ngx_uint_t                      current;
    uintptr_t                      *tried;
    uintptr_t                       data;
} ngx_http_upstream_rr_peer_data_t;

加权轮询策略的启动

在 Nginx 启动过程中,在解析完 http 配置块之后,会调用各个 http 模块对应的初始函数。对于 upstream 机制的 ngx_http_upstream_module 模块来说,对应的 main 配置初始函数是ngx_http_upstream_init_main_conf()如下所示:

for (i = 0; i upstreams.nelts; i++) {  

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

    if (init(cf, uscfp[i]) != NGX_OK) {  
            return NGX_CONF_ERROR;  
    }  
}  

ngx_http_upstream_module 模块中,如果用户没有做任何策略选择,那么执行默认采用加权轮询策略初始函数为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,该方法完成请求初始化工作。

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

    ngx_http_upstream_connect(r, u);  
}  

完成客户端请求的初始化工作之后,会选择一个后端服务器来处理该请求,选择后端服务器由函数 ngx_http_upstream_get_round_robin_peer 实现。该函数在 ngx_event_connect_peer中被调用。

ngx_int_t  
ngx_event_connect_peer(ngx_peer_connection_t *pc)  
{  
.../* 调用 ngx_http_upstream_get_round_robin_peer  */
   rc = pc->get(pc, pc->data);  
   if (rc != NGX_OK) {  
       return rc;  
   }  

  s = ngx_socket(pc->sockaddr->sa_family, SOCK_STREAM, 0);  
...}  

当已经选择一台后端服务器来处理请求时,接下来就会测试该后端服务器的连接情况,测试连接由函数 ngx_http_upstream_test_connect 实现,在函数 ngx_http_upstream_send_request 中被调用。

static void  
ngx_http_upstream_send_request(ngx_http_request_t *r, ngx_http_upstream_t *u)  
{  
...if (!u->request_sent && ngx_http_upstream_test_connect(c) != NGX_OK) {  
        /* 测试连接失败 */ 
        ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_ERROR);  
        return;  
    }  
...}  

若连接测试失败,会由函数 ngx_http_upstream_next 发起再次测试,若测试成功,则处理完请求之后,会调用 ngx_http_upstream_free_round_robin_peer 释放后端服务器。

加权轮询工作流程

加权轮询策略的基本工作过程是:初始化负载均衡服务器列表,初始化后端服务器,选择合适后端服务器处理请求,释放后端服务器。

初始化服务器列表

初始化服务器列表由函数 ngx_http_upstream_init_round_robin 实现,该函数的执行流程如下所示:

  • 第一种情况:若 upstream 机制配置项中配置了服务器:
    • 初始化非备用服务器列表,并将其挂载到 us->peer.data 中;
    • 初始化备用服务器列表,并将其挂载到 peers->next 中;
  • 第二种情况:采用默认的方式 proxy_pass 配置后端服务器地址;
    • 初始化非备用服务器列表,并将其挂载到 us->peer.data 中;

该方法执行完成之后得到的结构如下图所示:

Nginx 中 upstream 机制的负载均衡

/* 初始化服务器负载均衡列表 */
ngx_int_t
ngx_http_upstream_init_round_robin(ngx_conf_t *cf,
    ngx_http_upstream_srv_conf_t *us)
{
    ngx_url_t                      u;
    ngx_uint_t                     i, j, n, w;
    ngx_http_upstream_server_t    *server;
    ngx_http_upstream_rr_peers_t  *peers, *backup;

    /* 设置 ngx_http_upstream_peer_t 结构体中 init 的回调方法 */
    us->peer.init = ngx_http_upstream_init_round_robin_peer;

    /* 第一种情况:若 upstream 机制中有配置后端服务器 */if (us->servers) {
        /* ngx_http_upstream_srv_conf_t us 结构体成员 servers 是一个指向服务器数组 ngx_array_t 的指针,*/server = us->servers->elts;

        n = 0;
        w = 0;
        /* 在这里说明下:一个域名可能会对应多个 IP 地址,upstream 机制中把一个 IP 地址看作一个后端服务器 *//* 遍历服务器数组中所有后端服务器,统计非备用后端服务器的 IP 地址总个数(即非备用后端服务器总的个数) 和 总权重 */for (i = 0; i servers->nelts; i++) {
            /* 若当前服务器是备用服务器,则 continue 跳过以下检查,继续检查下一个服务器 */if (server[i].backup) {ngx_http_upstream_peer_t
                continue;
            }

            /* 统计所有非备用后端服务器 IP 地址总的个数(即非备用后端服务器总的个数) */
            n += server[i].naddrs;
            /* 统计所有非备用后端服务器总的权重 */
            w += server[i].naddrs * server[i].weight;
        }

        /* 若 upstream 机制中配置项指令没有设置后端服务器,则出错返回 */if (n == 0) {
            ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                          "no servers in upstream \"%V\" in %s:%ui",
                          &us->host, us->file_name, us->line);
            return NGX_ERROR;
        }

        /* 值得注意的是:备用后端服务器列表 和 非备用后端服务器列表 是分开挂载的,因此需要分开设置 *//* 为非备用后端服务器分配内存空间 */
        peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peers_t)
                              + sizeof(ngx_http_upstream_rr_peer_t) * (n - 1));
        if (peers == NULL) {
            return NGX_ERROR;
        }

        /* 初始化非备用后端服务器列表 ngx_http_upstream_rr_peers_t 结构体 */
        peers->single = (n == 1);/* 表示只有一个非备用后端服务器 */
        peers->number = n;/* 非备用后端服务器总的个数 */
        peers->weighted = (w != n);/* 设置默认权重为 1 或 0 */
        peers->total_weight = w;/* 设置非备用后端服务器总的权重 */
        peers->name = &us->host;/* 非备用后端服务器名称 */        n = 0;

        /* 遍历服务器数组中所有后端服务器,初始化非备用后端服务器 */for (i = 0; i servers->nelts; i++) {
            if (server[i].backup) {/* 若为备用服务器则 continue 跳过 */continue;
            }
            /* 以下关于 ngx_http_upstream_rr_peer_t 结构体中三个权重值的说明 *//*
             * effective_weight 相当于质量(来源于配置文件配置项的 weight),current_weight 相当于重量。
             * 前者反应本质,一般是不变的。current_weight 是运行时的动态权值,它的变化基于 effective_weight。
             * 但是 effective_weight 在其对应的 peer 服务异常时,会被调低,
             * 当服务恢复正常时,effective_weight 会逐渐恢复到实际值(配置项的weight);
             *//* 遍历非备用后端服务器所对应 IP 地址数组中的所有 IP 地址(即一个后端服务器域名可能会对应多个 IP 地址) */for (j = 0; j server