什么是“惊群”问题呢?我们可以考虑下面这个场景:某一时刻恰好所有worker子进程都休眠且等待新连接的系统调用(如epoll_wait),这时有一个用户向服务器发起了连接,内核在收到TCP的SYN包时,会激活所有的休眠的worker子进程,当然,此时只有最先开始执行accept的子进程可以成功创建新的连接,而其他的worker子进程都会accept失败。这些accept失败的子进程被内核唤醒是没有必要的,它们被唤醒后的执行很可能也是多余的,那么这一时刻它们占用了本不需要占用的系统资源,引发了不必要的进程上下文切换,增加了系统开销。(摘自《Nginx模块开发与架构解析》)
针对惊群问题,可能有的操作系统已经有了应对办法,但实际上,Nginx针对惊群问题的处理也是很巧妙的:它规定同一时刻只能有唯一的worker子进程监听Web端口,这样新连接事件只能唤醒唯一正在监听端口的woker子进程。Nginx用ngx_accept_mutex以及延迟队列等技术结合使用,完成了这一处理。具体怎么做的呢?我们看一下源码就明白了。
在介绍源码前,先说一下Nginx监听新连接以及处理读写事件的流程:(以epoll事件处理机制举例)
ngx_worker_process_cycle-->ngx_worker_thread--> ngx_event_process_init-->ngx_process_events_and_timers-->process_events函数-->事件的handler回调函数
其中process_events是具体事件处理模块(这里是epoll)的ngx_event_actions中的process_events钩子函数。
ngx_events_process_init函数是ngx_event_core_module模块实现的回调函数,在fork出work子进程后,每一个worker进程会在调用ngx_event_core_module模块的ngx_event_process_init方法后才会正式进入工作循环。下面我们看一下它都做了什么工作:
//这个函数做了很多事情,此函数是在fork出子进程后
//每个work进程调用的
static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{
ngx_uint_t m, i;
ngx_event_t *rev, *wev;
ngx_listening_t *ls;
ngx_connection_t *c, *next, *old;
ngx_core_conf_t *ccf;
ngx_event_conf_t *ecf;
ngx_event_module_t *module;
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
ecf = ngx_event_get_conf(cycle->conf_ctx, ngx_event_core_module);
//当打开accept_mutex负载均衡锁且用Master模式且work进程大于1时
//才正式确定了进程将使用accept_mutex负载均衡锁
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;
}
#if (NGX_THREADS)
ngx_posted_events_mutex = ngx_mutex_init(cycle->log, 0);
if (ngx_posted_events_mutex == NULL) {
return NGX_ERROR;
}
#endif
//初始化红黑树实现的定时器
if (ngx_event_timer_init(cycle->log) == NGX_ERROR) {
return NGX_ERROR;
}
//在调用use配置项指定的事件模块中,在Ngx_event_module_t
//接口下,Ngx_event_actions_t中的iNit方法进行这个事件模块
//的初始化工作
for (m = 0; ngx_modules[m]; m++) {
if (ngx_modules[m]->type != NGX_EVENT_MODULE) {
continue;
}
if (ngx_modules[m]->ctx_index != ecf->use) {
continue;
}
module = ngx_modules[m]->ctx;
if (module->actions.init(cycle, ngx_timer_resolution) != NGX_OK) {
/* fatal */
exit(2);
}
break;
}
#if !(NGX_WIN32)
//如果配置了time_resolution配置项,即表明需要控制时间精度,这时会调用
//setitimer方法,设置事件间隔为timer_resolution毫秒来回调
//ngx_timer_signale_handler回调函数
if (ngx_timer_resolution && !(ngx_event_flags & NGX_USE_TIMER_EVENT)) {
struct sigaction sa;
struct itimerval itv;
ngx_memzero(&sa, sizeof(struct sigaction));
sa.sa_handler = ngx_timer_signal_handler;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGALRM, &sa, NULL) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"sigaction(SIGALRM) failed");
return NGX_ERROR;
}
itv.it_interval.tv_sec = ngx_timer_resolution / 1000;
itv.it_interval.tv_usec = (ngx_timer_resolution % 1000) * 1000;
itv.it_value.tv_sec = ngx_timer_resolution / 1000;
itv.it_value.tv_usec = (ngx_timer_resolution % 1000 ) * 1000;
if (setitimer(ITIMER_REAL, &itv, NULL) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"setitimer() failed");
}
}
if (ngx_event_flags & NGX_USE_FD_EVENT) {
struct rlimit rlmt;
if (getrlimit(RLIMIT_NOFILE, &rlmt) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"getrlimit(RLIMIT_NOFILE) failed");
return NGX_ERROR;
}
cycle->files_n = (ngx_uint_t) rlmt.rlim_cur;
cycle->files = ngx_calloc(sizeof(ngx_connection_t *) * cycle->files_n,
cycle->log);
if (cycle->files == NULL) {
return NGX_ERROR;
}
}
#endif
//预分配Ngx_connection_t数组作为连接池
cycle->connections =
ngx_alloc(sizeof(ngx_connection_t) * cycle->connection_n, cycle->log);
if (cycle->connections == NULL) {
return NGX_ERROR;
}
c = cycle->connections;
//预分配读事件池
cycle->read_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n,
cycle->log);
if (cycle->read_events == NULL) {
return NGX_ERROR;
}
rev = cycle->read_events;
for (i = 0; i < cycle->connection_n; i++) {
rev[i].closed = 1;
rev[i].instance = 1;
#if (NGX_THREADS)
rev[i].lock = &c[i].lock;
rev[i].own_lock = &c[i].lock;
#endif
}
//预分配写事件池
cycle->write_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n,
cycle->log);
if (cycle->write_events == NULL) {
return NGX_ERROR;
}
wev = cycle->write_events;
for (i = 0; i < cycle->connection_n; i++) {
wev[i].closed = 1;
#if (NGX_THREADS)
wev[i].lock = &c[i].lock;
wev[i].own_lock = &c[i].lock;
#endif
}
i = cycle->connection_n;
next = NULL;
//按照序号将读/写事件设置到每一个ngx_connection_t连接
//对象中,同时连接data指针将空闲链表准备好
do {
i--;
c[i].data = next;
c[i].read = &cycle->read_events[i];
c[i].write = &cycle->write_events[i];
c[i].fd = (ngx_socket_t) -1;
next = &c[i];
#if (NGX_THREADS)
c[i].lock = 0;
#endif
} while (i);
cycle->free_connections = next;
cycle->free_connection_n = cycle->connection_n;
/* for each listening socket */
//在刚刚建立好的连接池中,为所有ngx_listening_t监听对象中的
//connection成员分配连接,同时对监听端口的读事件设置处理方法为
//ngx_event_accept,也就是说,有新的连接事件时将调用ngx_event_accept
//方法建立新的连接
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
c = ngx_get_connection(ls[i].fd, cycle->log);
if (c == NULL) {
return NGX_ERROR;
}
c->log = &ls[i].log;
c->listening = &ls[i];
ls[i].connection = c;
rev = c->read;
rev->log = c->log;
rev->accept = 1;
#if (NGX_HAVE_DEFERRED_ACCEPT)
rev->deferred_accept = ls[i].deferred_accept;
#endif
if (!(ngx_event_flags & NGX_USE_IOCP_EVENT)) {
if (ls[i].previous) {
/*
* delete the old accept events that were bound to
* the old cycle read events array
*/
old = ls[i].previous->connection;
if (ngx_del_event(old->read, NGX_READ_EVENT, NGX_CLOSE_EVENT)
== NGX_ERROR)
{
return NGX_ERROR;
}
old->fd = (ngx_socket_t) -1;
}
}
#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;
//如果是mater模式的就不能直接放到事件驱动模块中
//而是只有在执行ngx_trylock_accept_mutex函数后,成功获取锁
//才会将所有的新连接事件加入到epoll中
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) {
continue;
}
if (ngx_event_flags & NGX_USE_RTSIG_EVENT) {
if (ngx_add_conn(c) == NGX_ERROR) {
return NGX_ERROR;
}
} else {
if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
}
#endif
}
return NGX_OK;
}
可以看出这个函数做了一些关键的初始化工作,如初始化红黑树定时器、初始化连接池、初始化读写事件池、将新连接事件的handler函数设置为ngx_event_accept以及对于单进程模式,还要向epoll中添加新连接事件。另外一个需要注意地方的是:当处于Master多进程模式时,ngx_use_accept_mutex为true,这个时候新连接事件是没有添加的epoll中的,下面我们会介绍在什么时候添加。
之后每个Worker子进程正式进入工作循环中,在ngx_worker_process_cycle函数中循环调用ngx_process_events_and_timers函数处理事件。ngx_process_events_timers函数很关键,下面是它的源码:
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
ngx_uint_t flags;
ngx_msec_t timer, delta;
//设置了时间精度
if (ngx_timer_resolution) {
timer = NGX_TIMER_INFINITE;
flags = 0;
} else {
//得到最近超时的timue,并将flags设置为NGX_UPDATE_TIME
timer = ngx_event_find_timer();
flags = NGX_UPDATE_TIME;
#if (NGX_THREADS)
if (timer == NGX_TIMER_INFINITE || timer > 500) {
timer = 500;
}
#endif
}
if (ngx_use_accept_mutex) {
//如果ngx_accept_disabled大于0,说明本进程连接太多,不处理新的连接
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
//开始处理新连接事件,这时将flags标志位加上NGX_POST_EVENTS。
//这样在ngx_epoll_module的ngx_epoll_process_events这个方法中
//是不会立刻调用事件的handler回调方法的
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
} else {
//未获取到accept_mutex锁,意味着不能让当前的worker进程
//频繁地试图抢锁,也不能让它经过太长时间再去抢锁
if (timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
}
//调用Ngx_process_events方法,并计算Ngx_process_events执行时消耗的
//时间,delta会影响触发定时器的执行
delta = ngx_current_msec;
//执行网络事件
(void) ngx_process_events(cycle, timer, flags);
delta = ngx_current_msec - delta;
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"timer delta: %M", delta);
//下面执行队列中的事件
if (ngx_posted_accept_events) {
ngx_event_process_posted(cycle, &ngx_posted_accept_events);
}
//执行完新连接事件后,释放锁
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
}
//如果Ngx_posted_events消耗的时间大于0,而且这是可能
//有新的定时器事件被触发,那么处理定时器事件
if (delta) {
ngx_event_expire_timers();
}
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"posted events %p", ngx_posted_events);
//处理普通读写事件
if (ngx_posted_events) {
if (ngx_threaded) {
ngx_wakeup_worker_thread(cycle);
} else {
ngx_event_process_posted(cycle, &ngx_posted_events);
}
}
}
上面代码中的ngx_accept_disabled是负载均衡阈值,它决定着一个进程最多能处理多少连接,如果超过一个阈值就不能再处理新连接事件了。ngx_trylock_accept_mutex(cycle)函数也是很关键的,上面提到的Master模式新连接事件的添加就是在这个函数中进行的。如下所示:
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
//使用进程间的同步锁,试图获取ngx_accept_mutex锁
//返回1表示成功拿到锁,返回0表示获取锁失败。这个
//获取锁的过程是非阻塞的,此时一旦锁被其他worker子进程
//占用,立刻返回
if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex locked");
//当ngx_accept_mutext_held为1时表示当前进程已经获取到锁
if (ngx_accept_mutex_held
&& ngx_accept_events == 0
&& !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
{
return NGX_OK;
}
//将所有监听连接的读事件添加到当前的epoll等事件驱动模块
if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
ngx_shmtx_unlock(&ngx_accept_mutex);
return NGX_ERROR;
}
//经过Ngx_enable_accept_events方法的调用,当前进程的事件
//驱动模块已经开始监听所有的端口,这时需要把Ngx_accept_mutex_held
//标志位置为1方便本进程的其他模块了解它目前已经获取到锁
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);
//获取锁失败但是标志位仍未1肯定是有问题的,需要处理
if (ngx_accept_mutex_held) {
//将所有本进程监听连接的读事件从事件驱动模块中移除
if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
return NGX_ERROR;
}
//将标志位置1
ngx_accept_mutex_held = 0;
}
return NGX_OK;
}
则ngx_worker_process_cycle执行完ngx_trylock_accept_mutex后,如果获取锁成功,则当前进程将可以监听所有的新连接事件,并设置标志位;如果获取锁失败,这接下来仍然可以调用ngx_process_events函数进而调用的epoll模块的process events函数继续处理进程之前的那些普通事件:
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
int events;
uint32_t revents;
ngx_int_t instance, i;
ngx_uint_t level;
ngx_err_t err;
ngx_event_t *rev, *wev, **queue;
ngx_connection_t *c;
/* NGX_TIMER_INFINITE == INFTIM */
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll timer: %M", timer);
//调用epoll_wait获取事件
events = epoll_wait(ep, event_list, (int) nevents, timer);
err = (events == -1) ? ngx_errno : 0;
//Nginx对时间的缓存和管理,当flags标志位指示要更新时间时,就在这里更新
if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
ngx_time_update();
}
if (err) {
if (err == NGX_EINTR) {
if (ngx_event_timer_alarm) {
ngx_event_timer_alarm = 0;
return NGX_OK;
}
level = NGX_LOG_INFO;
} else {
level = NGX_LOG_ALERT;
}
ngx_log_error(level, cycle->log, err, "epoll_wait() failed");
return NGX_ERROR;
}
if (events == 0) {
if (timer != NGX_TIMER_INFINITE) {
return NGX_OK;
}
ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
"epoll_wait() returned no events without timeout");
return NGX_ERROR;
}
//上锁
ngx_mutex_lock(ngx_posted_events_mutex);
//遍历本次epoll_wait返回的所有事件
for (i = 0; i < events; i++) {
//ptr就是连接的地址(C语言没有类的类型转换一说,只能传递地址)但是最后
//一位有特殊的含义,需要把它屏蔽掉
c = event_list[i].data.ptr;
//将地址的最后一位取出来,用instance变量标识
instance = (uintptr_t) c & 1;
//无论是32位还是64位机器,其地址的最后一位肯定是0,可以用下面这行语句
//把ngx_connection_t的地址还原到真正的地址值
c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);
//取出读事件
rev = c->read;
//判断这个事件是否为过期事件
if (c->fd == -1 || rev->instance != instance) {
/*
* the stale event from a file descriptor
* that was just closed in this iteration
*/
//如果fd套接字的描述符为-1或者Instance标志位不相等时表示这个事件
//已经过期了,不用处理
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll: stale event %p", c);
continue;
}
//取出事件类型
revents = event_list[i].events;
ngx_log_debug3(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll: fd:%d ev:%04XD d:%p",
c->fd, revents, event_list[i].data.ptr);
if (revents & (EPOLLERR|EPOLLHUP)) {
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll_wait() error on fd:%d ev:%04XD",
c->fd, revents);
}
#if 0
if (revents & ~(EPOLLIN|EPOLLOUT|EPOLLERR|EPOLLHUP)) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
"strange epoll_wait() events fd:%d ev:%04XD",
c->fd, revents);
}
#endif
if ((revents & (EPOLLERR|EPOLLHUP))
&& (revents & (EPOLLIN|EPOLLOUT)) == 0)
{
/*
* if the error events were returned without EPOLLIN or EPOLLOUT,
* then add these flags to handle the events at least in one
* active handler
*/
revents |= EPOLLIN|EPOLLOUT;
}
//如果是读事件且该事件是活跃的
if ((revents & EPOLLIN) && rev->active) {
if ((flags & NGX_POST_THREAD_EVENTS) && !rev->accept) {
rev->posted_ready = 1;
} else {
rev->ready = 1;
}
//flags参数中含有NGX_POST_EVENTS表示这批事件要延后处理
if (flags & NGX_POST_EVENTS) {
queue = (ngx_event_t **) (rev->accept ?
&ngx_posted_accept_events : &ngx_posted_events);
ngx_locked_post_event(rev, queue);
} else {
//立即调用这个事件相应的回调方法来处理这个事件
rev->handler(rev);
}
}
wev = c->write;
if ((revents & EPOLLOUT) && wev->active) {
if (flags & NGX_POST_THREAD_EVENTS) {
wev->posted_ready = 1;
} else {
wev->ready = 1;
}
if (flags & NGX_POST_EVENTS) {
//将这个事件添加到post队列中延后处理
ngx_locked_post_event(wev, &ngx_posted_events);
} else {
wev->handler(wev);
}
}
}
ngx_mutex_unlock(ngx_posted_events_mutex);
return NGX_OK;
}
可以看出NGX_POST_EVENTS标志位决定了当前从epoll中获取的事件是立刻handler还是放入posted队列中延后处理。
再次回到ngx_process_events_and_timers函数,可以看到在执行完网络事件后,还需要执行两个posted延迟队列里的事件,为什么需要两个延迟队列呢?这里也非常巧妙,Nginx用这两个队列将事件做了分类。试想一下,一个进程获取了ngx_accept_mutex锁后,什么时候释放呢?是要等到所有的事件都结束吗?如果普通网络事件很耗时怎么办?所以Nginx将队列分为ngx_posted_accept_events和ngx_posted_events,前者存放新连接事件,后者存放普通事件,在进程处理完新连接事件后,立即释放锁,这样大大减少了ngx_accept_mutex锁的占用时间。
总之,最终从队列中还是调用事件的handler函数,拿新连接事件举例,新连接事件的handler函数是之前提过的ngx_event_accept函数,在这个函数中调用accept函数创建连接,最终这个连接可能要交给HTTP框架继续处理,如设置新的读写事件。