Nginx的惊群问题:master进程开始监听Web端口,fork出多个子进程,这些子进程会开始同时监听同一个Web端口。假如在没有用户连入服务器,某一时刻恰好所有的worker子进程都休眠且等待新连接的系统调用;这时有一个用户向服务器发起了连接,内核在收到TCP的SYN包时,会激活所有的休眠worker子进程。最先开始执行accept的子进程可以成功建立新连接,而其他worker子进程都会accept失败,转而继续休眠。这些accept失败的子进程被内核唤醒是不必要的,这一时刻它们占用了本不需要的系统资源,引发了不必要的进程上下文切换,增加了系统开销。
Nginx解决惊群问题的方式是:规定了同一时刻只能有唯一一个worker子进程监听Web端口。因此新连接事件只能唤醒唯一正在监听端口的worker子进程。
要实现这个方案要打开accept_mutex锁,然后调用ngx_trylock_accept_mutex()函数尝试获取accept_mutex锁,如果加锁成功,才能够监听Web端口。
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
//使用进程间的同步锁,试图获取ngx_accept_mutex锁
if (ngx_shmtx_trylock(&ngx_accept_mutex))
{
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"accept mutex locked");
//如果获得ngx_accept_mutex锁,但ngx_accept_mutex_held为1,则
//立刻返回。ngx_accept_mutex_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); //添加失败,则要释放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);
//ngx_accept_mutex函数获取ngx_accept_mutex锁失败
//ngx_accept_mutex为1表示当前进程还在获取到锁的状态,需要处理
if (ngx_accept_mutex)
{
//将监听连接的读时间从事件驱动模块中移除
if (ngx_disable_accept_events(cycle) == NGX_ERROR)
{
return NGX_ERROR;
}
ngx_accept_mutex_held = 0;
}
return NGX_OK;
}
如果ngx_trylock_accept_mutex()函数没有获得锁,接下来调用事件驱动模块的process_events方法时只能处理已有连接上的事件; 如果获得了锁,调用process_events方法时既处理已有连接上的事件,也会处理新连接的事件。
获得ngx_accept_mutex锁的进程会优先处理ngx_posted_accept_events队列中的事件,处理完后要立刻释放该锁,这样减少了锁的占用时间,其他进程可以获取该锁处理新的连接事件。
Nginx的负载均衡问题的方式:
利用一个负载均衡阈值ngx_accept_disabled来管理。这个阈值是进程允许的总连接数的1/8减去空闲连接数。就是当阈值为正整数时当前进程将不再处理新连接事件,取而代之的仅仅是阈值值减1。
因为阈值的初始值为负值:N(总连接数)/8 - N(初始时,空闲值等于进程总允许连接数) = -(7/8)N。所以当Nginx各worker子进程间的负载均衡仅在某个worker子进程处理的连接数达到它最大处理总数的7/8时才会触发,这时该worker进程就会减少处理新连接的机会,其他空闲的worker进程就有机会去处理更多的新连接。
要利用锁机制来做互斥与同步,既避免监听套接口被同时加入到多个进程的事件监控机制里,又避免监听套接口在某一时刻没有被任何一个进程监控。
如果进程并没有处于过载状态,那么就会去争用锁,争锁成功就会把所有监听套接口加入到自身的事件监控机制里;争锁失败就会把监听套接口从自身的事件监控机制里删除。