nginx处理惊群问题源码分析
所谓惊群问题(thundering herd problem)是说多个进程共享一个fd的监听事件,当IO就绪后多个进程都被唤醒,但只有一个进程处理,其他进程白白被唤醒而浪费系统资源。
nginx的惊群问题同样如此,master进程在生成listenfd后,fork出的worker子进程都继承了listenfd。每个worker进程有自己的epoll实例,会把listenfd加到关注集合里。
那么当一个新的客户端连接建立后,这些worker进程都会从epoll_wait()返回,当然最后只有一个worker进程处理,其他进程还要再进入wait 浪费了资源(epoll_wait 系统调用难免要用户态内核态切换)。
惊群处理
nginx处理惊群问题也比较简单,一句话描述:每个worker进程每次epoll_wait()前,先获取全局锁,获取到的进程才能把listenfd 加到epoll。该进程处理完连接事件后,再把锁释放。
全局一把锁,多个worker进程共享锁是共享内存的IPC通信。
源码片段
nginx 版本release-1.12
事件处理
删减部分代码,主要为了说明惊群处理的加锁。代码间注释来说明问题。
void
ngx_process_events_and_timers(ngx_cycle_t * cycle) {
// nginx.conf events模块可配置 是否使用锁来解决惊群问题
if (ngx_use_accept_mutex) {
// ngx_trylock_accept_mutex 尝试获取listen的锁,加锁成功才能把listenfd加到epoll
// 多个worker进程间 加锁是共享内存的方式来通信
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
// 持有锁后,flags加个标识,后处理事件 为了尽快释放这个锁
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
}
}
// 阻塞在epoll_wati等IO事件
(void) ngx_process_events(cycle, timer, flags);
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle - > log, 0,
"timer delta: %M", delta);
// 释放锁之前,把连接事件处理完
ngx_event_process_posted(cycle, & ngx_posted_accept_events);
// 释放锁,但这个变量值 ngx_accept_mutex_held没改
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock( & ngx_accept_mutex);
}
// 锁释放后,处理其他可读可写事件
ngx_event_process_posted(cycle, & ngx_posted_events);
}
加锁处理
ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t * cycle) {
if (ngx_shmtx_trylock( & ngx_accept_mutex)) {// 加锁
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle - > log, 0,
"accept mutex locked");
// 注意下边对这俩变量的赋值
// 说明当前进程又来获取锁成功了,listenfd本来就在epoll,不用动直接返回
if (ngx_accept_mutex_held && ngx_accept_events == 0) {
return NGX_OK;
}
// 加锁成功后才能把listenfd加到epoll
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_log_debug1(NGX_LOG_DEBUG_EVENT, cycle - > log, 0,
"accept mutex lock failed: %ui", ngx_accept_mutex_held);
// 加锁失败走到这
// 说明当前进程上次获取锁成功,这次失败了
if (ngx_accept_mutex_held) {
// 那就要把listenfd从我这个worker进程的epoll拿出去,以便我下次wait时没有这个fd,其他进程加锁成功放自己epoll里了。
// 注意拿走这个listenfd的时机,是在第二次获取锁失败是才剔除的,因为如果当前进程又拿到的话免去再放了,如上的描述
if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
return NGX_ERROR;
}
// 锁失败,把这个变量改0
ngx_accept_mutex_held = 0;
}
return NGX_OK;
}