Nginx模块开发(十二)(续):upstream负载均衡

上一篇简单介绍了nginx upstream模块的开发。这一篇主要介绍upstream负载均衡模块的开发。我希望能把这个讲得很简单,因为负载均衡的核心是算法,不是体系结构,所以就用nginx自己的IP hash做例子了。然后我们分析nginxround robin算法,这个算法比较有意思,之前我没读懂,直到最近官方修改了RR算法,我才了解得更深入一点了。最后我们再学习现在nginxkeepalive模块是怎么实现的(体力活),以及我的limit_upstream模块的思路。

体系结构

配置

我们从配置入手比较容易理解。在配置文件中,我们如果需要使用IP hash的负载均衡算法。我们需要写一个类似下面的配置:

upstream test {

    ip_hash;

    server 192.168.0.1;

    server 192.168.0.2;

}

从配置我们可以看出负载均衡模块的使用思路:如果使用IP hash模块做负载均衡,那么需要使用一条指令通知“ip_hash”通知nginx,否则nginx会使用默认的RR模块。回想一下我们的handler配置,很容易发现他们有共同点。

指令

使用上的共同点决定开发的共同点,我们马上可以看到:

static ngx_command_t  ngx_http_upstream_ip_hash_commands[] = {

    { ngx_string("ip_hash"),

      NGX_HTTP_UPS_CONF|NGX_CONF_NOARGS,

      ngx_http_upstream_ip_hash,

      0,

      0,

      NULL },

 

      ngx_null_command

};

基本上和handler的指令一致,除了指令属性是NGX_HTTP_UPS_CONF,这个属性我们以前没有见过,他表示该指令的适用范围是upstream{}

钩子

按照handler的步骤,大家应该知道这里就是模块的切入点了。我们看看IP hash模块的代码。这段我原封不动的贴过来,因为所有的负载均衡模块的钩子代码都是类似这样的:

static char *

ngx_http_upstream_ip_hash(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)

{

    ngx_http_upstream_srv_conf_t  *uscf;

 

    uscf = ngx_http_conf_get_module_srv_conf(cf, ngx_http_upstream_module);

 

    uscf->peer.init_upstream = ngx_http_upstream_init_ip_hash;

 

    uscf->flags = NGX_HTTP_UPSTREAM_CREATE

                  |NGX_HTTP_UPSTREAM_MAX_FAILS

                  |NGX_HTTP_UPSTREAM_FAIL_TIMEOUT

                  |NGX_HTTP_UPSTREAM_DOWN;

 

    return NGX_CONF_OK;

}

下面对这段代码进行简单解释。每个upstream的配置结构如下图,这里的uscf就是图中右侧的那个数据结构。这个函数最重要的是设置peer.init_upstream函数指针,加了这个钩子,nginxngx_http_upstream_init_main_conf中会调用这个函数初始化upstream,也就用利用我们提供的负载均衡方法了。

此外,这里还设置了一些标志:

l  NGX_HTTP_UPSTREAM_CREATE:创建标志,如果含有创建标志的话,nginx会检查重复创建,以及必要参数是否填写;

l  NGX_HTTP_UPSTREAM_MAX_FAILS:可以在server中使用max_fails属性;

l  NGX_HTTP_UPSTREAM_FAIL_TIMEOUT:可以在server中使用fail_timeout属性;

l  NGX_HTTP_UPSTREAM_DOWN:可以在server中使用down属性;

此外还有下面属性:

l  NGX_HTTP_UPSTREAM_WEIGHT:可以在server中使用weight属性;

l  NGX_HTTP_UPSTREAM_BACKUP:可以在server中使用backup属性。

 

提醒大家注意,配置中这类负载均衡的指令要写在upstream {}的开始处,因为不同的负载均衡模块支持的server属性不同,如果把这行写到后面去了,那么可以使用server设置负载均衡模块不支持的属性,设置不会起作用,但也不会有有任何错误提示信息,有时这样会让使用者迷惑。

也有一些模块,是利用upstream负载均衡模块的特性完成非负载均衡的功能,这些模块可能就不需要重新设置flag标志,比如姚总的upstream_health_check模块,或者maximkeepalive模块等等。

Nginx模块开发(十二)(续):upstream负载均衡

初始化配置

初始化配置是upstream负载均衡模块在配置阶段的最后一步, 这一步主要进行的工作除了如字面所述的初始化upstream的配置以外,还有一点就是设置执行阶段的初始化钩子。这一点与handler模块不同,大家需要注意。ngx_http_upstream_init_main_conf中会调用每一个upstreaminit函数来完成配置初始化。我们来看IP hash模块的配置初始化:

ngx_http_upstream_init_round_robin(cf, us);

us->peer.init = ngx_http_upstream_init_ip_hash_peer;

这里IP hash模块首先对RR模块进行了初始化,然后再设置自己的执行阶段初始化钩子。这是因为IP hash模块在某server掉线以后会使用RR模块的算法计算备用server。这个思路大家也可以稍微借鉴一下。


初始化请求

现在进入到nginx执行的时期。每个request来到nginx以后,如果发现需要访问upstream,就会执行对应的peer.init函数。注意,是每个请求都会执行peer.init,为什么呢?因为upstream中的server可能掉线,而upstream提供的一个特性是某台server掉线了,可以使用同一upstream中的其他server或者后备server,那么对于每个requestnginx都需要初始化一个独立的计算环境,这就是为什么需要peer.init而不是放在init_upstream中的原因。

为了讨论peer.init的核心,我们还是看IP hash模块的实现:

r->upstream->peer.data = &iphp->rrp;

r->upstream->peer.get = ngx_http_upstream_get_ip_hash_peer;

第一行是设置数据指针,这个指针就是指向计算环境的数据结构的。

第二行是设置从upstream pool中取出某个server的回调的函数指针,负载均衡的算法就是在这个函数中实现的。

另外nginx还有一个r->upstream->peer.free的回调指针,是在某个upstream使用完server以后的进行调用,keepalive模块使用到了这个回调,我们后面会分析。

如果是SSL的话,nginx还提供两个回调函数peer.set_sessionpeer.save_session

负载均衡

getfree两个函数就是负载均衡的核心,实现其算法。关于IP hash的算法不做分析。这里只分析下get的返回值:

NGX_DONE:表示是已经建立的连接,不需要再connect,可以直接使用;

NGX_OK:表示分配到一个server,但没有建立连接,需要connect

NGX_BUSY:所有的server都不可用。

其他值没有意义。

Nginx RR算法

经典版算法

if (peer[i].current_weight <= 0) { continue; }

if (peer[n].current_weight * 1000 / peer[i].current_weight

                    > peer[n].weight * 1000 / peer[i].weight) { return n; }

else { n = i; }

我们举个例子来说明这个算法:{ a, b, c }三个服务器,weight值是{ 5, 1, 2 },那么分配的过程参见下面这张表:


 

selected server

current_weights

reason

c

{ 5, 1, 2 }

第二个if无法满足

b

{ 5, 1, 1 }

1 / 1 > 1 / 2

a

{ 5, 0, 1 }

5 / 1 > 5 / 2

a

{ 4, 0, 1 }

4 / 1 > 5 / 2

a

{ 3, 0, 1 }

3 / 1 > 5 / 2

c

{ 2, 0, 1 }

第二个if 无法满足

a

{ 2, 0, 0 }

没得选了

a

{ 1, 0, 0 }

没得选了

这么看效果还不错,但是如果仔细看会发现有缺陷。就是weight小的server分配不均。其实b在第四或者第五位被分配是比较好的。可能有人会说为什么要这样吹毛求疵呢。那我们设法将第六位被分配的c去掉,其实很简单,也就是weight设置成{ 5, 1, 1 },那么分配序列就成了c, b, a, a, a, a, a,将这个算法的缺点放到最大。

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!

Keepalive

maxim有一个keepalive模块,我们来分析一下这个模块。之所以放在upstream负载均衡中来介绍是因为它也是使用的负载均衡的这套体系来实现的,虽然它的功能和负载均衡搭不上边。

这个模块很简单,直接画个图说明。

1.         keepalive带一个参数指定keepalive的连接数量ninit_upstream首先创建两个queue,一个是cached,一个是freefree中有n个元素:(图1在最后)

2.         一个请求的upstream使用完成了,使用peer.free回调实现connection不释放,而是保存在从free中取出的一个元素中,再将这个元素插入cached  (图2在最后)

 

3.         一个请求的upstream使用完成了,如free中没有可用单元,那么使用cache中的最后一个单元保存connection  (图3在最后)

4.         一个请求如果需要分配upstreamserver,使用peer.get回调实现从cached出取出一个元素返回,并将这个元素插回到free中:  (图4在最后)

5.         一个请求如果需要分配upstreamserver,如果cached中没有元素,走原来的负载均衡算法。大家肯定在想,之前的负载均衡算法是什么意思?就是之前的啦:

ngx_http_upstream_init_keepalive_peer {

    kp->original_get_peer = r->upstream->peer.get;

    kp->original_free_peer = r->upstream->peer.free;

    r->upstream->peer.get = ngx_http_upstream_get_keepalive_peer;

    r->upstream->peer.free = ngx_http_upstream_free_keepalive_peer;

}

6.         peer.free回收连接的时候,模块添加了一个epoll事件监测服务器端断开连接:

if (ngx_handle_read_event(c->read, 0) != NGX_OK) {

    goto invalid;

}

if (c->read->timer_set) {

    ngx_del_timer(c->read);

}

if (c->write->timer_set) {

    ngx_del_timer(c->write);

}

c->write->handler = ngx_http_upstream_keepalive_dummy_handler;

c->read->handler = ngx_http_upstream_keepalive_close_handler;

所以这个模块只是缓存连接池,被动的缓存一定数量的连接,因为无法限制并发所以在高并发的情况下会和后端服务器瞬间建立大量连接,无法实现以少量长连接来实现高并发的目的。

limit_upstream

针对上面的问题,我开发了一个模块limit_upstream,用于限制一个nginx(含所有worker)对后端一个upstream每台server的连接数。对于任意一个server,如果和它建立的连接数(含长连接)已经超过了设定值,那么这个请求将被阻塞,执行连接释放后执行或者超时。

这里使用了类似于keepalive模块的方法,但是区别也很大。keepalive借助ngx_http_upstream_init_main_conf完成自身初始化,而我使用模块自己的init_main进行,这样在目前可以保证limit_upstream在所有upstream负载均衡模块之后初始化,也就可以知道这些模块设置的upstream最终配置。

limit_upstream的工作思路类似于limit_request,是利用红黑树保存server的连接计数,对于阻塞的请求,设置定时器以使他们检测自身超时。唤醒操作是由每个worker自己完成的。这里没有在peer.free中实现,因为连接实际close是在peer.free之后。这个操作是通过requestcleanup回调函数触发的,一定保证连接已关闭:

cln = ngx_http_cleanup_add(ctx->r, 0);

cln->handler = ngx_http_limit_upstream_cleanup;

cln->data = ctx;

因为nginx没有更靠谱的进程间通知机制,所以这里对请求能否创建upstream连接有一条特殊规定,就是如果某个worker还没有保持着至少一条已经打开的连接,即使计数已到,这次仍然允许worker建立连接,这样可以使所有的worker都能够工作,不会有的worker上面的请求全部都在排队而无法唤醒的情况出现。不过这种设计仍然会出现worker串行处理request的情况出现,目前还没有想到更好的办法。

模块的代码在https://github.com/cfsego/nginx-limit-upstream可以找到。

小结

这次介绍了upstream负载均衡模块的写法,也和大家一起比较了两种RR算法。同时,大家也看到一起“不务正业”的负载均衡模块。不错,够本了,大家晚安。

Nginx模块开发(十二)(续):upstream负载均衡
图3
Nginx模块开发(十二)(续):upstream负载均衡
图1
Nginx模块开发(十二)(续):upstream负载均衡
图2
Nginx模块开发(十二)(续):upstream负载均衡
  图4
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值