什么是惊群问题
Nginx的master进程开始监听web端口,fork出多个worker子进程,这些子进程开始监听同一个端口,假定一段时间没有用户连入服务器,某一时刻恰好所有的子进程都进入休眠等待新连接的到来(阻塞在epoll_wait),这时有一个用户向服务器发来了连接,内核在收到SYN包后激活所有子进程,但是此时只有一个进程成功执行accept建立新连接,其它子进程accept失败,会重新进入休眠,他们的唤醒也是多余的,引发不必要的进程上下文切换,增加系统开销。
Nginx解决惊群问题的方法
既然“惊群”是多个子进程同一时刻监听同一个端口引起的,那么解决的方法也很简单,就是,同一时刻只能有一个进程监听端口,这样就不会发生“惊群”了。此时新连接事件只能唤醒正在监听的唯一一个进程
如何保证一个时刻只能有一个worker进程监听端口呢?Nginx设置了一个accept_mutex锁,在使用accept_mutex锁时,只有进程成功调用ngx_trylock_accept_mutex方法获取锁后才可以监听端口。
下面分析一下ngx_trylock_accept_mutex函数
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
if (ngx_shmtx_trylock(&ngx_accept_mutex)) {//使用进程间同步锁,试图获取accept_mutex锁返回1时表示成功拿到锁,返回0表示失败,这个获取锁的过程是非阻塞的
//一旦锁被其他worker进程占用就会立即返回
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex locked");
if (ngx_accept_mutex_held//ngx_accept_mutex_held是一个标志位,当它为1时,说明进程已经获取到锁了,
&& ngx_accept_events == 0
&& !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
{
return NGX_OK;
}
if (ngx_enable_accept_events(cycle) == NGX_ERROR) {//将所有监听连接的读事件都添加到当前epoll事件驱动模块中
ngx_shmtx_unlock(&ngx_accept_mutex);
return NGX_ERROR;
}
ngx_accept_events = 0;
ngx_accept_mutex_held = 1;将标志位置为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) == NGX_ERROR) {
return NGX_ERROR;
}
ngx_accept_mutex_held = 0;//没有获取到锁时必须设置为0
}
return NGX_OK;
}
因此在调用ngx_trylock_accept_mutex方法后,要么是唯一获取到锁并且监听端口上的新连接事件,要么是没有获取到锁,不会收到新连接事件。
如果该进程没有获取到锁,那么在接下来的process_events方法只能处理已有连接上的事件;如果获取到了锁,调用process_events时就会处理已有连接上的事件和新连接事件,但是,什么时候来释放锁呢?一个worker进程上可能有许多活跃的连接,处理这些连接上的事件会占用很多时间,那么很长时间不释放accept_mutex锁,其他worker进程就很难获得新连接的机会
那么该如何解决长时间占用ngx_accept_mutex锁问题呢?就要依靠ngx_posted_accept_events队列和ngx_posted_events队列了。
下面分析一下ngx_process_events代码
static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
for (i = 0; i < events; i++) {
c = event_list[i].data.ptr;
instance = (uintptr_t) c & 1;
c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);
rev = c->read;
}
//取得发生一个事件
revents = event_list[i].events;
//记录wait的错误返回状态
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);
}
//该事件是一个读事件,并该连接上注册的读事件是active的
if ((revents & (EPOLLERR|EPOLLHUP))
&& (revents & (EPOLLIN|EPOLLOUT)) == 0)
{
revents |= EPOLLIN|EPOLLOUT;
}
if ((revents & EPOLLIN) && rev->active) {
if ((flags & NGX_POST_THREAD_EVENTS) && !rev->accept) {
rev->posted_ready = 1;
} else {
rev->ready = 1;
}
//事件放入到相应的队列中
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) {//非新连接事件,加入到post事件队列,延后执行
if (c->fd == -1 || wev->instance != instance) {
/*
* the stale event from a file descriptor
* that was just closed in this iteration
*/
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll: stale event %p", c);
continue;
}
if (flags & NGX_POST_THREAD_EVENTS) {
wev->posted_ready = 1;
} else {
wev->ready = 1;
}
if (flags & NGX_POST_EVENTS) {
ngx_locked_post_event(wev, &ngx_posted_events);
} else {
wev->handler(wev);
}
}
}
ngx_mutex_unlock(ngx_posted_events_mutex);
return NGX_OK;
}
这样讲事件分为两个队列,在后面的ngx_process_timers_and_events函数中,调用完ngx_process_events函数收集完事件后,优先一次调用accept队列中事件的回调函数,然后释放锁,最后调用post队列中的事件的回调函数
负载均衡问题
什么是负载均衡?
在多个worker进程争抢处理一个新连接事件时,一定只有一个进程可以成功建立连接,那么如果有的子进程很勤奋他们抢着处理了大部分连接,而有的运气不好只处理了很少的连接,这对多核CPU是很不利的,因为子进程应该是平等的,每个子进程应该尽量独占一个cpu核心,子进程间负载不均衡会影响整个服务性能。
如何实现负载均衡呢?
与惊群问题一样,只有打开了accept_mutex锁才会实现worker子进程间的负载均衡。实现方法是设置一个负载均衡阀值,全局变量ngx_accept_disabled。
在ngx_event_accept中会对其赋值
//accept到一个新的连接以后,就重新计算ngx_accept_disabled的值。它主要用来做负载均衡使用
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
在启动时,此阀值就是一个负数,其值为连接总数的7/8,其实它的用法很简单,当阀值为负数时,不会进行触发负载均衡操作,但是当它是正数时,就会触发负载均衡操作,当为正数是时,当前进程将不再处理新连接事件,而是将阀值ngx_accept_disabled减一
ngx_process_timers_and_events函数中
//是否使用accept互斥体。accept mutex的作用就是避免惊群,同时实现负载均衡
if (ngx_use_accept_mutex) {
if (ngx_accept_disabled > 0) { //大于0说明该进程接收的连接过多,放弃一次争抢accept mutex的机会
ngx_accept_disabled--;
} else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
上面代码说明,当前使用连接达到总连接数的7/8时,就不会再处理新连接事件。达到7/8以下才会再处理新连接事件。这时,worker进程就会减少处理新连接事件的机会,而比较空闲的进程就会有机会去处理新连接事件,以达到均衡负载。