一、“惊群”问题
概念
Nginx的master进程开始监听web端口,fork出多个worker子进程,这些子进程开始监听同一个端口,假定一段时间没有用户连入服务器,某一时刻恰好所有的子进程都进入休眠等待新连接的到来(阻塞在epoll_wait),这时有一个用户向服务器发来了连接,内核在收到SYN包后激活所有子进程,但是此时只有一个进程成功执行accept建立新连接,其它子进程accept失败,会重新进入休眠,他们的唤醒也是多余的,引发不必要的进程上下文切换,增加系统开销。
解决方式
同一时刻只能有一个进程监听端口,这样就不会发生“惊群”了。此时新连接事件只能唤醒正在监听的唯一一个进程。如何保证一个时刻只能有一个worker进程监听端口呢?Nginx设置了一个accept_mutex锁,在使用accept_mutex锁时,只有进程成功调用ngx_trylock_accept_mutex方法获取锁后才可以监听端口。
if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
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_posted_accept_events队列和ngx_posted_events队列。优先一次调用accept队列中事件的回调函数,然后释放锁,最后调用post队列中的事件的回调函数。
二、负载均衡问题
概念
在多个worker进程争抢处理一个新连接事件时,一定只有一个进程可以成功建立连接,那么如果有的子进程很勤奋他们抢着处理了大部分连接,而有的运气不好只处理了很少的连接,这对多核CPU是很不利的,因为子进程应该是平等的,每个子进程应该尽量独占一个cpu核心,子进程间负载不均衡会影响整个服务性能。
解决方式
实现方法是设置一个负载均衡阀值,全局变量ngx_accept_disabled。
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
这里的connection_n和free_connection_n都是每worker自维护,在启动时,此阀值就是一个负数,其值为连接总数的7/8,其实它的用法很简单,当阀值为负数时,不会进行触发负载均衡操作,但是当它是正数时,即该worker的连接数到达该worker上限的7/8,就会触发负载均衡操作,当为正数是时,当前进程将不再处理新连接事件,而是将阀值
ngx_accept_disabled减一。
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
//只有拿到ngx_accept_mutex锁时,该进程才允许将监听套接字加入epoll
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
}
上面代码说明,当前使用连接达到总连接数的7/8时,就不会再处理新连接事件。达到7/8以下才会再处理新连接事件。这时,worker进程就会减少处理新连接事件的机会,而比较空闲的进程就会有机会去处理新连接事件,以达到均衡负载。