当nginx作为代理服务器时,需要将客户端的请求转发给后端服务器进行处理,如果后端服务器有多台,那如何选择合适的后端服务器来处理当前请求,也就是本篇文章要介绍的内容。nginx尽可能的把请求分摊到各个后端服务器进行处理,以保证服务的可用性和可靠行,提供给客户端更好的用户体验。负载均衡的直接目的只有一个,尽量发挥多个后端服务器的整体性能,实现1+1大于2的效果。
nginx提供了两种策略,用于选择一个合适的后端服务器处理客户端请求。一种是加权轮询策略,顾名思义就是根据每台服务器的权重进行选择,这将导致一个问题,同一个客户端的不同请求有可能被不同的后端服务器进行处理,不能维护会话的保持。 另一种是ip哈希策略,也就是根据客户端的ip地址进行哈希运算,从而被分配到一个固定的后端服务器。ip哈希策略虽然能保证同一个客户端的不同请求都被同一个后端服务器进行处理,能够做到会话的保持,但如果客户端是经过nat地址映射后,将导致某台后端服务器的压力剧增。如果ip哈希失败次数超过20次,也会退化为加权轮询策略,使用加权轮询策略选择一台后端服务器。
需要注意的是客户端与nginx服务器之间是一个长连接,一个tcp连接可以处理多个http请求,也就是当http请求关闭后,这个tcp连接并没有关闭。 而nginx与后端服务器是一个短连接, 当nginx与后端服务器的某个请求交互完成了,nginx与后端服务器对应的tcp连接也就被关闭了,是一个短连接。
本篇文章只分析加权轮询策略,至于ip哈希策略留给读者去分析。ip哈希是基于加权轮询实现的,如果加权轮询理解了,那ip哈希自然而然也就理解了。
一、负载均衡配置的解析
假设nginx.conf配置文件中指定了下面的这个负载均衡配置
upstream backend_server
{
server www.sangfor.com weight=5; //这个域名解析后得到172.16.7.151; 172.16.7.152; 172.16.7.153三个ip地址
server www.sundray.com weight=4 max_fails=3 fail_timeout=30; //这个域名解析后得到172.16.7.154;172.16.7.155
server 172.16.7.156 backup; //备机
server 172.16.7.157 down; //该服务器已经宕机,不作为后端服务器
}
经过配置解析后,得到下面这种数据结构。配置解析后的数据保存到了负载均衡模块的配置ngx_http_upstream_srv_conf_s中的servers数组中。数组中的每一个元素都是上面配置中每一个server解析后的内容。需要注意的是一个域名有可能解析后有多个ip地址。
来看下代码的实现。
//开始解析http块
static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
//开始解析http块
rv = ngx_conf_parse(cf, NULL);
}
//ip_hash配置解析,设置哈希策略的回调
static char * ngx_http_upstream_ip_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
uscf->peer.init_upstream = ngx_http_upstream_init_ip_hash;
return NGX_CONF_OK;
}
ip_hash配置项的回调函数为ngx_http_upstream_ip_hash,因此在解析完配置后,如果指定了ip_hash配置项则会设置init_upstream回调为:ngx_http_upstream_init_ip_hash ,表示使用ip哈希策略,否则使用默认的加权轮询策略。
二、加权轮询数据结构的维护
在解析完配置下后,又会调用各个模块的init_main_conf方法,负载均衡的init_main_conf回调为: ngx_http_upstream_init_main_conf
//开始解析http块
static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
//开始解析http块
rv = ngx_conf_parse(cf, NULL);
for (m = 0; ngx_modules[m]; m++)
{
if (ngx_modules[m]->type != NGX_HTTP_MODULE)
{
continue;
}
//调用各个模块的init_main_conf回调,负载均衡的回调为: ngx_http_upstream_init_main_conf
if (module->init_main_conf)
{
rv = module->init_main_conf(cf, ctx->main_conf[mi]);
}
}
}
ngx_http_upstream_init_main_conf函数创建负载均衡的内部结构,如果是ip哈希策略则调用
ngx_http_upstream_init_ip_hash,否则调用默认的加权轮询策略的回调:
ngx_http_upstream_init_round_robin, 先来看下加权轮询策略的实现。
//负载均衡的main块初始化操作
//1、负载均衡策略的初始化,加权轮询策略或者ip哈希策略初始化操作
//2、创建负载均衡模块的头部哈希表
static char * ngx_http_upstream_init_main_conf(ngx_conf_t *cf, void *conf)
{
//赋值均衡策略初始化
for (i = 0; i < umcf->upstreams.nelts; i++)
{
//如果init_upstream为空, 则默认使用加权轮询负载均衡策略;如果是ip哈希策略则为;ngx_http_upstream_init_ip_hash
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_init_round_robin函数前,先来看下这个函数维护的数据结构:
加权轮询策略下,使用了两个链表节点。一个链表节点存放所有的主后端服务器列表, 另一个链表节点存放所有的备后端服务器列表。ngx_http_upstream_init_round_robin函数内部就是将配置解析后的所有后端服务器,按照主备分离思想。把所有的主服务器放到一块,把所有的备服务器放到一块。这样在选择一个后端服务器时,优先选择主后端服务器, 当主后端服务器都选择失败后,才会从备后端服务器中选择。
//加权策略初始化, 创建后端服务器链表(最多两个节点,一个主服务器节点,存放所有主服务器;
//一个备服务器节点,存放所有的备服务器)
ngx_int_t ngx_http_upstream_init_round_robin(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
{
//在选择后端服务器前,该回调用于创建查询接口的参数
us->peer.init = ngx_http_upstream_init_round_robin_peer;
//配置了负载均衡模块,也就是指定了upstream xxx{}等
if (us->servers)
{
server = us->servers->elts;
//统计主后端服务器的个数
for (i = 0; i < us->servers->nelts; i++)
{
if (server[i].backup)
{
continue;
}
n += server[i].naddrs;
}
//开辟主后端服务器的地址空间
peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peers_t)
+ sizeof(ngx_http_upstream_rr_peer_t) * (n - 1));
peers->single = (n == 1);
//保存主后端服务器的信息
for (i = 0; i < us->servers->nelts; i++)
{
for (j = 0; j < server[i].naddrs; j++)
{
if (server[i].backup)
{
continue;
}
peers->peer[n].sockaddr = server[i].addrs[j].sockaddr;
}
}
//主服务器列表节点当做链表的第一个节点
us->peer.data = peers;
//主后端服务器安装权限从大道小排序
ngx_sort(&peers->peer[0], (size_t) n, sizeof(ngx_http_upstream_rr_peer_t),ngx_http_upstream_cmp_servers);
//统计备后端服务区个数
for (i = 0; i < us->servers->nelts; i++)
{
if (!server[i].backup)
{
continue;
}
n += server[i].naddrs;
}
//开辟备后端服务器的地址空间
backup = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peers_t)
+ sizeof(ngx_http_upstream_rr_peer_t) * (n - 1));
//保存备后端服务器的信息
for (i = 0; i < us->servers->nelts; i++)
{
for (j = 0; j < server[i].naddrs; j++)
{
if (!server[i].backup)
{
continue;
}
backup->peer[n].sockaddr = server[i].addrs[j].sockaddr;
}
}
//被服务器列表放到主服务器列表后面
peers->next = backup;
//权重权重从大到小排序备服务器列表
ngx_sort(&backup->peer[0], (size_t) n, sizeof(ngx_http_upstream_rr_peer_t),
ngx_http_upstream_cmp_servers);
}
}
三、与后端服务器建立tcp连接前的准备工作
在nginx与后端服务器建立tcp连接前,需要设置获取后端服务器的回调,以及创建回调参数。
//负载均衡模块初始化,与上游服务器建立一个tcp连接。
//同时将客户端发来的请求头部,包体转为fastcgi格式的内容
static void ngx_http_upstream_init_request(ngx_http_request_t *r)
{
//加权轮询策略为ngx_http_upstream_init_round_robin_peer,
//设置获取后端服务器的回调
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);
}
对于每一个来自客户端的请求,nginx都需要与后端服务器建立一个tcp连接,如果是加权轮询策略,则使用ngx_http_upstream_init_round_robin_peer函数获取后端服务器,以及创建获取后端服务器需要的参数。
//nginx与后端服务器建立tcp连接前的初始化,设置获取后端服务器的回调,以及创建回调参数
ngx_int_t ngx_http_upstream_init_round_robin_peer(ngx_http_request_t *r,
ngx_http_upstream_srv_conf_t *us)
{
rrp = r->upstream->peer.data;
//创建的这个结构,将作为ngx_http_upstream_get_round_robin_peer的参数
if (rrp == NULL)
{
rrp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_rr_peer_data_t));
r->upstream->peer.data = rrp;
}
//仍然指向的是配置解析后,加权轮询策略维护的数据结构
rrp->peers = us->peer.data;
//获取后端服务器的个数
n = rrp->peers->number;
//一个整数占4个字节, 一共32个位。每一位代表一个后端服务器。
//如果后端服务器的个数小于一个整数位大小,也就是小于32,则使用data空间
if (n <= 8 * sizeof(uintptr_t))
{
rrp->tried = &rrp->data;
rrp->data = 0;
}
else
{
//如果后端服务器的个数大于一个整数的总位数(也就是32), 则计算需要多少个整数才能存放
//所有的后端服务器。
n = (n + (8 * sizeof(uintptr_t) - 1)) / (8 * sizeof(uintptr_t));
rrp->tried = ngx_pcalloc(r->pool, n * sizeof(uintptr_t));
}
//设置获取后端服务器,释放后端服务器的回调
r->upstream->peer.get = ngx_http_upstream_get_round_robin_peer;
r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer;
r->upstream->peer.tries = rrp->peers->number;
return NGX_OK;
}
函数内部设置获取后端服务器的回调为:
ngx_http_upstream_get_round_robin_peer,释放后端服务器的回调为:
ngx_http_upstream_free_round_robin_peer。同时也创建了每一个客户端请求都独立拥有的一个私有结构ngx_http_upstream_rr_peer_data_t。看下这个结构的定义:peers仍然指向解析配置时创建的后端服务器链表,每一个客户端创建的私有结构,peers成员都指向这个链表,这样每一个客户端根据这个链表中的后端服务器权重信息动态的选择一个后端服务器,同时每一个客户端都会修改这个链表中某些服务器的权重, 最终使得后端服务器的负载按照权重的比例,比较均衡的分部,例如5:3:2。另外需要注意的是tried指向的空间每一个位都代表一个后端服务器,如果该后端服务器被选中了,则相应的位会被设置为1
typedef struct
{
ngx_http_upstream_rr_peers_t *peers; //后端服务器链表头,指向配置ngx_http_upstream_srv_conf_s中的peer
ngx_uint_t current; //当前使用的是哪一个后端服务器,假设有0-59供60个后端服务器,如果current=4,则表示使用第5个服务器
uintptr_t *tried; //数组中的每一位代表已经选中的后端服务器,如果后端服务器的总数小于一个4字节的整数空间,也就是32位,则tried指向下面这个data空间
//如果大于一个4字节的整数空间,也就是32位,则开辟一个空间,tried指向这片空间
uintptr_t data;
} ngx_http_upstream_rr_peer_data_t;
四、选则后端服务器
在nginx与后端服务器建立tcp连接时,会根据加权轮询策略选择一个后端服务器,进而获取到后端服务器的ip,端口信息,从而与它建立连接。
//获取一个后端服务器的连接地址,并与后端服务器进行连接
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);
}
来看下ngx_http_upstream_get_round_robin_peer函数的实现,在只有一台服务器的时候,没得选择,这能选择这唯一的一台。
(1)在有多台服务器的情况下,如果是第一次选择,则按照权重的大小,每次从后端服务器选择一台服务器。如果已经宕机了的服务器,则不会被使用;如果已经被选中了的,也不会被使用;如果在一定时间内失败次数超过限制大小的也不会被选中。
//加权轮询策略时从后端服务器选择一个服务器地址,保存到pc的sockaddr成员中
ngx_int_t ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc,void *data)
{
//第一次选择情况
if (pc->tries == rrp->peers->number)
{
for ( ;; )
{
//按照每台后端服务器的权重比例进而选择一台后端服务器
rrp->current = ngx_http_upstream_get_peer(rrp->peers);
//n表示第几个整数
n = rrp->current / (8 * sizeof(uintptr_t));
//m表示这个整数32位中的哪一位
m = (uintptr_t) 1 << rrp->current % (8 * sizeof(uintptr_t));
if (!(rrp->tried[n] & m))
{
//进入这个条件表示未选中情况
peer = &rrp->peers->peer[rrp->current];
if (!peer->down)
{
//选择到了则退出
if (peer->max_fails == 0 || peer->fails < peer->max_fails)
{
break;
}
//条件大于的情况下,表示还没有达到超时时间
if (now - peer->accessed > peer->fail_timeout)
{
peer->fails = 0;
break;
}
peer->current_weight = 0; //值为0表示不参与选择
}
else
{
rrp->tried[n] |= m;
}
pc->tries--;
}
}
peer->current_weight--;
}
//保存选择的服务器ip信息
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
}
//根据权重的比例选择一个后端服务器
static ngx_uint_t ngx_http_upstream_get_peer(ngx_http_upstream_rr_peers_t *peers)
{
//核心算法,每一个重点都会修改这个权重,从而实现负载均衡
if (peer[n].current_weight * 1000 / peer[i].current_weight
> peer[n].weight * 1000 / peer[i].weight)
{
return n;
}
}
(2)如果不是第一次选择,也就是说,之前选中的后端服务器不可用,有可能该后端服务器关闭了。这样就需要重新选择一台服务器,重新选择时不再按照权重比例来选择,而是从当前这台服务器往后开始查找,直到找到一台可用的服务器。
//从后端服务器选择一个服务器地址,保存到pc的sockaddr成员中
ngx_int_t ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc,void *data)
{
//使用循环方式,从current往后的服务器开始选择,而不在是按照权重选择。具体为什么这么做,不得而知
for ( ;; )
{
//n表示第几个整数
n = rrp->current / (8 * sizeof(uintptr_t));
//m表示这个整数32位中的哪一位
m = (uintptr_t) 1 << rrp->current % (8 * sizeof(uintptr_t));
if (!(rrp->tried[n] & m))
{
peer = &rrp->peers->peer[rrp->current];
if (!peer->down) //down=1表示宕机
{
}
else
{
rrp->tried[n] |= m;
}
pc->tries--;
}
rrp->current++;
}
//保存选择的服务器ip信息
pc->sockaddr = peer->sockaddr;
pc->socklen = peer->socklen;
}
(3)那如果主服务器列表都选择失败,该怎么办呢? nginx从备服务器列表中选择一台服务器。备服务器的选择和主服务器的选择是一模一样的,因此递归调用这个函数。那如果主备服务器都选择失败怎么办? 此时函数返回NGX_BUSY, 然后给客户端返回一个502错误。
//加权轮询策略时从后端服务器选择一个服务器地址,保存到pc的sockaddr成员中
ngx_int_t ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc,void *data)
{
//在主后端服务器全部选择失败的情况下, 开始选择备用的后端服务器
peers = rrp->peers;
if (peers->next)
{
//切换到备服务器链表节点
rrp->peers = peers->next;
pc->tries = rrp->peers->number;
n = rrp->peers->number / (8 * sizeof(uintptr_t)) + 1;
for (i = 0; i < n; i++)
{
rrp->tried[i] = 0;
}
//被服务器的选择和主服务器的选择是一样的,因此重新调用该函数(递归)
rc = ngx_http_upstream_get_round_robin_peer(pc, rrp);
}
//执行到这里,主备服务器都选择失败
for (i = 0; i < peers->number; i++)
{
peers->peer[i].fails = 0;
}
return NGX_BUSY;
}
五、再次选择后端服务器
当选中一个后端服务器后,nginx会与这个后端服务器建立tcp连接。如果建立tcp连接的时候,连接超时;或者发送请求数据给后端服务器失败; 或者接收后端服务器的响应超时等,这些操作都会导致重新获取一个后端服务器。调用ngx_http_upstream_next这个函数可以重新获取一个后端服务器。来看下这个函数的实现。
//尝试选择一个新的后端服务器并与它建立tcp连接。如果尝试完了所有后端服务器都没有找到一个可用的
//后端服务器,则会结束请求
static void ngx_http_upstream_next(ngx_http_request_t *r, ngx_http_upstream_t *u, ngx_uint_t ft_type)
{
//该后端服务器不可用,则释放这个已经选择的后端服务器,以便下面重新选择一个后端服务器
if (ft_type != NGX_HTTP_UPSTREAM_FT_NOLIVE)
{
u->peer.free(&u->peer, u->peer.data, state);
}
if (status)
{
//如果尝试完所有的后端服务器都没有一个可用的服务器,则给客户端返回错误。当然,
//如果fastcgi_next_upstream指定了在后端服务器返回对应的错误码时,才寻找下一个后端服务器,否则立即给可以的返回错误
//,参考ngx_conf_set_bitmask_slot实现
if (u->peer.tries == 0 || !(u->conf->next_upstream & ft_type))
{
ngx_http_upstream_finalize_request(r, u, status);
return;
}
}
//与后端服务器的连接存在,则先关闭, 下面重新选择一个后端服务器并与它建立tcp连接
if (u->peer.connection)
{
ngx_close_connection(u->peer.connection);
}
//重新选择一个后端服务器并与它建立tcp连接
ngx_http_upstream_connect(r, u);
}