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

标签:
杂谈nginx模块开发upstream负载均衡 |
分类: Nginx模块开发 |
序
上一篇简单介绍了nginx upstream模块的开发。这一篇主要介绍upstream负载均衡模块的开发。我希望能把这个讲得很简单,因为负载均衡的核心是算法,不是体系结构,所以就用nginx自己的IP hash做例子了。然后我们分析nginx的round robin算法,这个算法比较有意思,之前我没读懂,直到最近官方修改了RR算法,我才了解得更深入一点了。最后我们再学习现在nginx的keepalive模块是怎么实现的(体力活),以及我的limit_upstream模块的思路。
体系结构
配置
我们从配置入手比较容易理解。在配置文件中,我们如果需要使用IP hash的负载均衡算法。我们需要写一个类似下面的配置:
upstream test { } |
从配置我们可以看出负载均衡模块的使用思路:如果使用IP hash模块做负载均衡,那么需要使用一条指令通知“ip_hash”通知nginx,否则nginx会使用默认的RR模块。回想一下我们的handler配置,很容易发现他们有共同点。
指令
使用上的共同点决定开发的共同点,我们马上可以看到:
static ngx_command_t }; |
基本上和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) { } |
下面对这段代码进行简单解释。每个upstream的配置结构如下图,这里的uscf就是图中右侧的那个数据结构。这个函数最重要的是设置peer.init_upstream函数指针,加了这个钩子,nginx在ngx_http_upstream_init_main_conf中会调用这个函数初始化upstream,也就用利用我们提供的负载均衡方法了。
此外,这里还设置了一些标志:
l
l
l
l
此外还有下面属性:
l
l
提醒大家注意,配置中这类负载均衡的指令要写在upstream {}的开始处,因为不同的负载均衡模块支持的server属性不同,如果把这行写到后面去了,那么可以使用server设置负载均衡模块不支持的属性,设置不会起作用,但也不会有有任何错误提示信息,有时这样会让使用者迷惑。
也有一些模块,是利用upstream负载均衡模块的特性完成非负载均衡的功能,这些模块可能就不需要重新设置flag标志,比如姚总的upstream_health_check模块,或者maxim的keepalive模块等等。
http://s9/middle/7303a1dcgc050fec1db68&690
初始化配置
初始化配置是upstream负载均衡模块在配置阶段的最后一步, 这一步主要进行的工作除了如字面所述的初始化upstream的配置以外,还有一点就是设置执行阶段的初始化钩子。这一点与handler模块不同,大家需要注意。ngx_http_upstream_init_main_conf中会调用每一个upstream的init函数来完成配置初始化。我们来看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,那么对于每个request,nginx都需要初始化一个独立的计算环境,这就是为什么需要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_session和peer.save_session。
负载均衡
get和free两个函数就是负载均衡的核心,实现其算法。关于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 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; } } 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.
2. |
3.
4.
5.
ngx_http_upstream_init_keepalive_peer { } |
6.
if (ngx_handle_read_event(c->read, 0) != NGX_OK) { } if (c->read->timer_set) { } if (c->write->timer_set) { } 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之后。这个操作是通过request的cleanup回调函数触发的,一定保证连接已关闭:
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算法。同时,大家也看到一起“不务正业”的负载均衡模块。不错,够本了,大家晚安。
http://s10/middle/7303a1dcgc050ed1266d9&690图3
http://s14/middle/7303a1dcgc050ed11efdd&690
图1
http://s12/middle/7303a1dcgc050ed2894eb&690
图2
http://s4/middle/7303a1dcgc050ed2d6cf3&690