Nginx 中 upstream 机制的负载均衡
- 负载均衡
- 加权轮询
- 相关结构体
- 加权轮询策略的启动
- 加权轮询工作流程
- 初始化服务器列表
- 选择合适的后端服务器
- 初始化后端服务器
- 根据权重选择后端服务器
- 释放后端服务器
- 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
中;
- 初始化非备用服务器列表,并将其挂载到
该方法执行完成之后得到的结构如下图所示:
/* 初始化服务器负载均衡列表 */
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