首先解释下什么是“惊群”现象:如果多个工作进程同时拥有某个监听套接口,那么一旦该套接口出现某客户端请求,此时就将引发所有拥有该套接口的工作进程去争抢这个请求,能争抢到的肯定只有某一个工作进程,而其他工作进程注定要无功而返,这种现象即为“惊群”。
Nginx解决这种“惊群”现象使用的是负载均衡的策略,接下来先结合Nginx的源码详细介绍下Nginx的这种负载均衡策略。
首先是Nginx如何开启负载均衡策略:当然运行的Nginx要是多进程模型,并且工作进程数目大于1。这很好理解,只有在拥有多个工作进程争抢一个套接口时才会出现惊群现象,也才会需要负载均衡的策略。
if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) {
ngx_use_accept_mutex = 1;
ngx_accept_mutex_held = 0;
ngx_accept_mutex_delay = ecf->accept_mutex_delay;
} else {
ngx_use_accept_mutex = 0;
}
其中变量ngx_use_accept_mutex就是用于标识是否打开负载均衡策略。这里的负载均衡策略又叫做“ 前端负载均衡”,因为它用于将客户端请求合理地分配给工作进程的方法;而“ 后端负载均衡”是用于为处理客户端请求,来合理选择一个后端服务器的策略。
接下来介绍负载均衡策略如何“合理的”将请求分配给工作进程,ngx_process_events_and_timers()有如下代码:
if (ngx_use_accept_mutex) {
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
} else {
if (timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
}
可以看到这段代码只有在开启负载均衡(即ngx_use_accept_mutex = 1)后才生效。在该逻辑内,首先通过检测变量ngx_accept_diabled值是够大于0来判断当前进程是否已经过载,为什么可以这样判断需要理解变量ngx_accept_diabled值的含义,这在accept()接受新连接请求的处理函数ngx_event_accept()内可以看到。
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
其中ngx_cycle->connection_n表示一个工作进程中的最大可承受连接数,可以通过worker_connections指令配置,其默认值为512.另外一个变量ngx_cycle->free_connection_n则表示当前的可用连接数,假设当前活动连接数为X,那么该值为ngx_cycle->connection_n - X;故此ngx_accept_diabled的值为:
ngx_accept_diabled = X - ngx_cycle->connection_n * 7 / 8;
也就是说如果当前活动连接数(X)超过最大可承受连接数的7/8,则表示发生过载,变量ngx_accept_diabled的值将大于0,并且该值越大表示过载越大,当前进程的负载越重。
回过去再看函数ngx_process_events_and_timers()内的代码,当进程处于过载状态时,所做的工作仅仅只是对变量ngx_accept_diabled自减1,这表示既然经过了一轮事件处理,那么负载肯定有所减小,所以也相应的调整变量ngx_accept_diabled的值。经过一段时间,ngx_accept_diabled又会降到0以下,便可争取锁获取新的请求连接。所以可以看出最大可承受连接数的7/8是一个负载均衡点,当某工作进程的负载达到这个临界点时,它就不会去尝试获取互斥锁,从而让新增负载均衡到其他工作进程上。
如果进程并没有处于过载状态,那么就会去争用锁,当然,实际上是争用监听套接口的监控权,争锁成功就会把所有监听套接口加入到自身的事件监控机制里(如果原本不在);争锁失败就会把监听套接口从自身的事件监控机制里删除(如果原本在)。
变量ngx_accept_mutex_held的值用于标识当前是否拥有锁,注意这一点很重要,因为如果当前拥有锁,则个flags变量打个NGX_POST_EVENTS标记,这表示所有发生的事件都将延后处理。这是任何架构设计都必须遵守的一个约定,即持锁者必须尽量缩短自身持锁的时间。所以把大部分事件延迟到释放锁之后再去处理,把锁尽快释放,缩短自身持锁的时间让其他进程尽可能的有机会获取锁。如果当前进程没有拥有锁,那么就把事件监控机制阻塞点(比如epoll_wait())的超时时间限制在一个比较短的范围内,超时越快,那么也就更频繁地从阻塞中跳出来,也就有更多的机会去争抢到互斥锁。
前面说到当事件到来时,不会立即处理,而是会设置一个延迟处理的标识(NGX_POST_EVENTS)。延迟处理的原理就是当一个事件发生时,会将事件以链表的形式缓存起来。
讲了这么多好像和解决惊群现在没有关系一样,其实上面所讲的内容大体上已经解释了Nginx如何避免惊群现象的原理了。总结下面两点:
第一,如果在处理新建连接事件的过程中,在监听套接字接口上又来了新的请求会怎么样?这没有关系,当前进程只处理已缓存的事件,新的请求将被阻塞在监听套接字接口上,并且监听套接字接口是以水平方式加入到事件监控机制里的,所以等到下一轮被哪个进程争取到锁并加在事件监控机制里时才会触发而被抓取出来。
第二,在进程处理事件时,只是把锁释放了,而没有将监听套接字接口从事件监控机制里删除,所以当处理缓存事件过程中,互斥锁可能会被另外一个进程争抢到并把所有监听套接字接口加入到它的事件监控机制里。因此严格来说,在同一时刻,监听套接字可能被多个进程拥有,但是,在同一时刻,监听套接口只可能被一个进程监控,因此在进程处理完缓存事件之后去争抢锁,发现锁被其他进程占用而争抢失败,会把所有监听套接口从自身的事件监控机制删除,然后才进行事件监控。在同一时刻,监听套接口只可能被一个进程监控,这也就意味着Nginx根本不会受到惊群的影响。