接上一节,由于nginx的I/O模型框架是从listen开始处理异步通知,因此存在当同一个connect到达os的内核并完成三次握手,会同时唤醒多个work process来处理这个异步通知。
实际上,根据相关资料,Linux内核本身已经解决了群惊的问题,当多个线程在阻塞模式同时调用一个socket的accept并阻塞,当有请求到来的时候,只有一个进程的accept调用成功并返回,其他进程继续阻塞在accept上面并保持睡眠状态。
但由于nginx使用的是异步I/O的统一框架,并提供多个I/O模型,像select,epoll,queue这样的unix平台下的模型在异步通知这方面仍然存在群惊的情况,因此nginx必须处理这些情况,另外需要注意的是,win平台的overlap,iocp并不存在群惊的情况,因为这2个模型可以投递异步accept,每个connect仅会唤醒一个异步accept。但为了统一网络框架,也只好按unix的方式统一处理。
这里有个全局变量ngx_use_accept_mutex用于控制是否开启“防群惊”模式,同时有一个
在ngx_event_process_init中,会首先判断是否开启 防群惊模式,如果未开启,则直接注册ngx_event_accept。代码如下
#if (NGX_WIN32)
if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
ngx_iocp_conf_t *iocpcf;
rev->handler = ngx_event_acceptex;
if (ngx_use_accept_mutex) {
continue;
}
if (ngx_add_event(rev, 0, NGX_IOCP_ACCEPT) == NGX_ERROR) {
return NGX_ERROR;
}
ls[i].log.handler = ngx_acceptex_log_error;
iocpcf = ngx_event_get_conf(cycle->conf_ctx, ngx_iocp_module);
if (ngx_event_post_acceptex(&ls[i], iocpcf->post_acceptex)
== NGX_ERROR)
{
return NGX_ERROR;
}
} else {
rev->handler = ngx_event_accept;
if (ngx_use_accept_mutex) {
continue;
}
if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
}
#else
rev->handler = ngx_event_accept;
if (ngx_use_accept_mutex
#if (NGX_HAVE_REUSEPORT)
&& !ls[i].reuseport
#endif
)
{
continue;
}
if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
#endif
如果开启,这里不会直接注册accept的回调:ngx_event_accept,而是在work process的进程函数的ngx_process_events_and_timers里面有一个获取互斥锁的操作,关键点是获取互斥锁的函数ngx_trylock_accept_mutex,代码如下
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex locked");
if (ngx_accept_mutex_held && ngx_accept_events == 0) {
return NGX_OK;
}
if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
ngx_shmtx_unlock(&ngx_accept_mutex);
return NGX_ERROR;
}
ngx_accept_events = 0;
ngx_accept_mutex_held = 1;
return NGX_OK;
}
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex lock failed: %ui", ngx_accept_mutex_held);
if (ngx_accept_mutex_held) {
if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
return NGX_ERROR;
}
ngx_accept_mutex_held = 0;
}
return NGX_OK;
}
此函数 首先调用ngx_shmtx_trylock获取互斥锁,ngx_shmtx_trylock是异步版的,不管是否成功都会立即返回,不会造成进程/线程的阻塞,它的阻塞版是ngx_shmtx_lock,后来章节再详细介绍区别。如果成功拿到互斥锁,当前进程就获得了调用accept的权限,接着,判断ngx_accept_mutex_held为1和ngx_accept_events 为0,这2个条件都为真说明当前进程之前就是互斥锁的持有者,不需要其他操作直接返回NGX_OK,否则调用ngx_enable_accept_events,遍历所有处于listen状态的socket,为每个连接设置异步读通知,然后直接返回。
如果获取互斥锁失败,而且ngx_accept_mutex_held的值为1,则调用ngx_disable_accept_events取消当前进程对每个socket的异步通知状态。
ngx_trylock_accept_mutex返回之后,如果调用成功并且持有互斥锁,给flags添加标志NGX_POST_EVENTS,随后调用ngx_process_events,前面章节已经介绍过这是真正执行异步调用的函数,如果flags包含NGX_POST_EVENTS,ngx_process_events会获取的所有已经就绪的I/O请求放到等待队列中。代码如下:
if ((revents & EPOLLIN) && rev->active) {
#if (NGX_HAVE_EPOLLRDHUP)
if (revents & EPOLLRDHUP) {
rev->pending_eof = 1;
}
#endif
rev->ready = 1;
if (flags & NGX_POST_EVENTS) {
queue = rev->accept ? &ngx_posted_accept_events
: &ngx_posted_events;
ngx_post_event(rev, queue);
} else {
rev->handler(rev);
}
}
其中accept单独放到ngx_posted_accept_events队列,其他读写请求放到ngx_posted_events队列。
然后立即处理调用ngx_event_process_posted处理异步accept请求,完成后释放accept的互斥锁。
ngx_event_process_posted(cycle, &ngx_posted_accept_events);
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
}
这样做的原因是当前进程在获取accept锁之后应该尽快调用accept()并释放锁,以便让其他进程尽快accept互斥锁,避免降低整个服务器接受连接的效率。释放之后再调用ngx_event_process_posted处理缓存在等待队列中的其他读写请求。
最后说说ngx_accept_disabled,这个全局变量用来实现work process的简单负载均衡的功能。每次成功调用accept之后会重置这个值
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
c = ngx_get_connection(s, ev->log);
if (c == NULL) {
if (ngx_close_socket(s) == -1) {
ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,
ngx_close_socket_n " failed");
}
return;
}
ngx_cycle->free_connection_n表示当前进程剩余可用连接,当ngx_cycle->free_connection_n小于1/8最大连接的时候,即ngx_accept_disabled>0的时候,说明进程比较繁忙,当前进程会放弃获取accept互斥锁,并将ngx_accept_disabled减少1,直到减少到<=0,进程最终会得到获取accept锁的机会,避免繁忙进程陷入一直无法获取accept锁的状态。