负载均衡
事件管理机制中的负载均衡处理在配置Nginx执行时,工作进程会有很多,由于各个工作进程相互独立接受客户端请求,处理,响应,而会出现的有的工作进程要处理很多请求等待处理,而有的工作进程则处于空闲状态而出现的负载不均衡的情况。
还有一种是在多个服务器间的负载均衡。
事件管理机制中的负载均衡采用轮询的方法,在多个工作进程之间选择一个适合的工作进程来处理到来得请求,并且加入事件监控机制中,从而带来新的请求,否则从事件监控机制中删除,避免带来的新的请求而带来更大的负载。一个工作进程是否过载,取决于当前的请求数是否大于当前工作进程的最大可承受连接数的7/8;如果进程没有出出现过载状态,那就去争取锁,实际上是争取监听套接口的监控权,监听成功就会把所有的监听套接口加入到自身事件监控机制中(这里的事件套接口,他们总是被当做一个整体加入或者删除),争取失败就会吧监听套接口从自身的事件监控机制中删除。当争取到锁后,所有持锁者必须尽量缩短自身拥有锁的时间按,所以所有事件都会被延迟到释放锁后去处理,把锁尽快释放,缩短自身持锁时间能让其他进程有争取锁的机会。
关于服务器端的负载均衡是后端服务器之间的均衡。
Nginx的负载均衡的策略是加权轮询,IP哈希,fair,一致哈希。
加权轮询是默认的策略,加权轮询是因为各个服务器可能又不同的权重,每个服务器多次链接失败或者处理出错后则在一段时间内不进行轮询。
upstream backend { server backend1.example.com weight = 5; server 127.0.0.1:8080 max_fails = 3 fail_timeout = 30s; server UNIX:/tmp/backend3 backup; server 192.168.0.1:9000 down; }
默认情况下,请求被分散在使用加权轮询的Nginx负载均衡服务器上。在上面的例子中,每七个请求按照下面分配:五个分为server backend1.example.com,一个分给127.0.0.1:8080
一个分给UNIX:/tmp/backend3 ,如果在服务器通信时发生一个错误,这个请求会被传递给下一个服务器,以此类推直到所有的功能服务器都被尝试。如果不能从所有这些Nginx服务器上获得响应,客户端就会返回最后一个连接服务器的处理结果。
weight:权重,默认为1
max_fails和fail_timeout默认分别为1 和10 ,表示后台服务器在fail_timeout时间内发生了max_fail次链接失败。
backup:备机,平常不被选择
down:即主动标记其为宕机状态,不参与被选择。
选择后端服务器
全局初始化完成后,当一个客户端请求过来时,Nginx是要选择合适的后端服务器来处理请求。在正式开始选择前,Nginx还要单独为本轮选择做一些初始化(针对一个客户端请求,Nginx会进行多次尝试选择,尝试失败后才返回502,所以注意一轮选择与一次选择的区别)
注:如果第一次选择的服务器因为连接失败或者其他情况导致重新选择另外一台服务器时,Nginx采用的是一般的遍历,并不根据权重来进行选择,大致是因为如果还是按照权重进行选择,可能会的到相同的服务器,并且最优的服务器已经连接失败那就只能说明,当前连接最优的服务器策略不稳定,所以干脆直接遍历也许反而来的更简单有效。
对后端服务器做一次选择的逻辑在函数
后端服务器权重计算:
static 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 = NULL; total = 0; for (i = 0; i < rrp->peers->number; i++) { //计算当前服务器的标记位在位图中的位置 n = i / (8 * sizeof(uintptr_t)); m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t)); //已经选择过,跳过 if (rrp->tried[n] & m) { continue; } //当前服务器对象 peer = &rrp->peers->peer[i]; //当前服务器已宕机,排除 if (peer->down) { continue; } //根据指定一段时间内最大失败次数做判断 if (peer->max_fails && peer->fails >= peer->max_fails && now - peer->checked <= peer->fail_timeout) { continue; } peer->current_weight += peer->effective_weight; total += peer->effective_weight; if (peer->effective_weight < peer->weight) { peer->effective_weight++; } if (best == NULL || peer->current_weight > best->current_weight) { best = peer; } } if (best == NULL) { return NULL; } //所选择的服务器在服务器列表中的位置 i = best - &rrp->peers->peer[0]; rrp->current = i; n = i / (8 * sizeof(uintptr_t)); m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t)); //位图相应位置置位 rrp->tried[n] |= m; best->current_weight -= total; best->checked = now; return best; }
2012.5.14开发版算法更新
为了解决这个问题,nginx官方修改了算法,具体见此处。下面摘抄出其核心代码:
foreach peer in peers
{ peer->current_weight += peer->effective_weight; total += peer->effective_weight; if (best == NULL || peer->current_weight > best->current_weight)
{ best = peer; } } best->current_weight -= total;
对于我们自己的例子,这里也演算一下:这个算法应该说就是毒化的加权动态优先级算法,最大的特点有两点:一是优先级current_weight的变化量是权effective_weight,二是对所选server的优先级进行大规模毒化,毒化程度是所有server的权值之和。这种算法的结果特点一定是权高的server一定先被选中,并且更频繁的被选中,而权低的server也会慢慢的提升优先级而被选中。对于上面的边界情况,这种算法得到的序列是a, a, b, a, c, a, a,均匀程度提升非常显著。
selected server |
current_weight before selected |
current_weight after selected |
a |
{ 5, 1, 2 } |
{ -3, 1, 2 } |
b |
{ 2, 2, 4 } |
{ 2, 2, -4 } |
a |
{ 7, 3, -2 } |
{ -1, 3, -2 } |
a |
{ 4, 4, 0 } |
{ -4, 4, 0 } |
b |
{ 1, 5, 2 } |
{ 1, -3, 2 } |
a |
{ 6, -2, 4 } |
{ -2, -2, 4 } |
b |
{ 3, -1, 6 } |
{ 3, -1, -2 } |
a |
{ 8, 0, 0 } |
{ 0, 0, 0 } |
经过一轮选择以后,优先级恢复到初始状态。这个性质使得代码得以缩短。Cool!
IP哈希:
sstatic ngx_int_t ngx_http_upstream_get_ip_hash_peer(ngx_peer_connection_t *pc, void *data) { ngx_http_upstream_ip_hash_peer_data_t *iphp = data; 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(NGX_LOG_DEBUG_HTTP, pc->log, 0, "get ip hash peer, try: %ui", pc->tries); /* TODO: cached */ ngx_http_upstream_rr_peers_wlock(iphp->rrp.peers); //判断是否只有一台服务器或者失败次数大于20 if (iphp->tries > 20 || iphp->rrp.peers->single) { ngx_http_upstream_rr_peers_unlock(iphp->rrp.peers); return iphp->get_rr_peer(pc, &iphp->rrp); } now = ngx_time(); pc->cached = 0; pc->connection = NULL; hash = iphp->hash; for ( ;; ) { for (i = 0; i < (ngx_uint_t) iphp->addrlen; i++) { hash = (hash * 113 + iphp->addr[i]) % 6271; } //113质数 使得哈希更离散 w = hash % iphp->rrp.peers->total_weight;
//根据哈希结果得到被选中的后端服务器 peer = iphp->rrp.peers->peer; p = 0; while (w >= peer->weight) { w -= peer->weight; peer = peer->next; p++; } n = p / (8 * sizeof(uintptr_t)); m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t)); if (iphp->rrp.tried[n] & m) { goto next; } ngx_log_debug2(NGX_LOG_DEBUG_HTTP, pc->log, 0,"get ip hash peer, hash: %ui %04XL", p, (uint64_t) m); if (peer->down) { goto next; } if (peer->max_fails && peer->fails >= peer->max_fails && now - peer->checked <= peer->fail_timeout) { goto next; } break; next: if (++iphp->tries > 20) { ngx_http_upstream_rr_peers_unlock(iphp->rrp.peers); return iphp->get_rr_peer(pc, &iphp->rrp); } } iphp->rrp.current = peer; pc->sockaddr = peer->sockaddr; pc->socklen = peer->socklen; pc->name = &peer->name; peer->conns++; if (now - peer->checked > peer->fail_timeout) { peer->checked = now; } ngx_http_upstream_rr_peers_unlock(iphp->rrp.peers); iphp->rrp.tried[n] |= m; iphp->hash = hash; return NGX_OK; }
一个域名可能对应多个IP地址。
server weight 字段,作为server权重,对应虚拟节点数目。
具体算法,将每个server虚拟节点n个节点,均匀分布到hash环上,每次请求,根据配置的参数计算出一个hash值,在hash环上查找离这个hash最近的虚拟节点,对应的server作为该次请求的后端机器。