紧跟着上一节所讲的,Nginx里面worker子进程之间相互独立,各自接收新的连接,接收数据报文,处理请求,等等。
但是,由于监听socket是由父进程创建的,所以在子进程调用accept函数接收新的连接时可能会出现所谓的惊群(thundering herd), 其实这个现象即使出现,我个人认为对性能的影响也不会太大,不就是一次accept操作失败么。另外呢,子进程之间虽然是独立工作的,但是,也可能出现不同的子进程之间负载不均衡的情况,比如某些子进程处理1000个连接,有的子进程处理100个连接,差了十倍。
Nginx里面,针对上面提到的两点,对程序进行了一些优化。来看看ngx_process_events_and_timers
函数里面的代码,这个函数是worker子进程中服务器主循环每次循环都会调用的函数:
// 如果使用了accept mutex
if (ngx_use_accept_mutex) {
// 如果当前禁止accept操作的计数还大于0
if (ngx_accept_disabled > 0) {
// 将这个计数减一
ngx_accept_disabled--;
} else {
// 尝试获取accept锁
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
// 如果获取到了accept锁
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
} else {
// 否则, 如果满足下面两个条件,将定时器置为ngx_accept_mutex_delay, 这个值是一个配置参数指定的
if (timer == NGX_TIMER_INFINITE
|| timer > ngx_accept_mutex_delay)
{
timer = ngx_accept_mutex_delay;
}
}
}
}
// 记下当前时间点
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);
// 如果有accept事件发生, 就去处理accept事件,其实最后就是调用accept函数接收新的连接
if (ngx_posted_accept_events) {
ngx_event_process_posted(cycle, &ngx_posted_accept_events);
}
// 已经处理完接收新连接的事件了,如果前面获取到了accept锁,那就解锁
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
}
if (delta) {
ngx_event_expire_timers();
}
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"posted events %p", ngx_posted_events);
// 除了accepet事件之外的其他事件放在这个队列中, 如果队列不为空,就去处理相关的事件
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_event_accept中被赋值的:
ngx_accept_disabled = ngx_cycle->connection_n / 8
- ngx_cycle->free_connection_n;
简单的理解,当前已经接收的连接(connection_n)越多,可用的空闲连接(free_connection_n)越少,这个值越大。
所以呢,假如某个子进程,当前已经接收了一定数量的连接,那么这个计数值就会相应的大,也就是说,这个子进程将会暂缓(注意,不是暂停)接收新的连接,而去专注于处理当前已经接收的连接;反之,如果某子进程当前处理的连接数量少,那么这个计数就会相应的小,于是这个较为空闲的子进程就会更多的接收新的连接请求。这个策略,确保了各个worker子进程之间负载是相对均衡的,不会出现某些子进程接收的连接特别多,某些又特别少。
同样的,在这段代码中,看到了即使现在子进程可以接收新的连接,也需要去竞争以获取accept锁,只有获得了这个锁才能接收新的连接,所以避免了前面提到的惊群现象的发生。
另外,在linux的新版内核中,加入了一个所谓的"cpu affinity"的特性,利用这个特性,你可以指定哪些进程只能运行在哪些CPU上面,我对这个不太熟悉,查看了一些资料,提到的说法是,有了这个特性,比如进程A在CPU1上面运行,那么在CPU1上面会cache一些与该进程相关的数据,那么再次调用的时候性能方面就会高效一些,反之,如果频繁的切换到不同的CPU上面去运行,那么之前在别的CPU上面的cache就会失效。
Nginx里面针对Linux内核的这个特性做了一些优化,它也可以在配置文件中指定哪些子进程可以运行在哪些CPU上面,这个优化与配置选项worker_cpu_affinity有关。可以在 http://wiki.nginx.org/NginxCoreModule
找到关于这个配置相关的信息。