什么是“惊群”问题呢?我们可以考虑下面这个场景:某一时刻恰好所有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框架继续处理,如设置新的读写事件。