转自:http://blog.csdn.net/xiajun07061225/article/details/9260535
惊群问题(thundering herd)的产生
在建立连接的时候,Nginx处于充分发挥多核CPU架构性能的考虑,使用了多个worker子进程监听相同端口的设计,这样多个子进程在accept建立新连接时会有争抢,这会带来著名的“惊群”问题,子进程数量越多越明显,这会造成系统性能的下降。
一般情况下,有多少CPU核心就有配置多少个worker子进程。假设现在没有用户连入服务器,某一时刻恰好所有的子进程都休眠且等待新连接的系统调用(如epoll_wait),这时有一个用户向服务器发起了连接,内核在收到TCP的SYN包时,会激活所有的休眠worker子进程。最终只有最先开始执行accept的子进程可以成功建立新连接,而其他worker子进程都将accept失败。这些accept失败的子进程被内核唤醒是不必要的,他们被唤醒会的执行很可能是多余的,那么这一时刻他们占用了本不需要占用的资源,引发了不必要的进程切换,增加了系统开销。
如何解决惊群问题-post事件处理机制
很多操作系统的最新版本的内核已经在事件驱动机制中解决了惊群问题,但Nginx作为可移植性极高的web服务器,还是在自身的应用层面上较好的解决了这一问题。 Nginx规定了同一时刻只有唯一一个worker子进程监听web端口,这一就不会发生惊群了,此时新连接事件只能唤醒唯一的正在监听端口的worker子进程。
如何限制在某一时刻是有一个子进程监听web端口呢?在打开accept_mutex锁的情况下,只有调用ngx_trylock_accept_mutex方法后,当前的worker进程才会去试着监听web端口。
那么,什么时候释放ngx_accept_mutex锁呢? 显然不能等到这批事件全部执行完。因为这个worker进程上可能有许多活跃的连接,处理这些连接上的事件会占用很长时间,其他worker进程很难得到处理新连接的机会。
如何解决长时间占用ngx_accept_mutex的问题呢?这就要依靠post事件处理机制,Nginx设计了两个队列:ngx_posted_accept_events队列(存放新连接事件的队列)和ngx_posted_events队列(存放普通事件的队列)。这两个队列都是ngx_event_t类型的双链表。定义如下: - ngx_thread_volatile ngx_event_t *ngx_posted_accept_events;
- ngx_thread_volatile ngx_event_t *ngx_posted_events;
下面结合具体代码进行分析惊群问题的解决。
首先看worker进程中ngx_process_events_and_timers事件处理函数(src/event/ngx.event.c),它处于worker进程的ngx_worker_process_cycle方法中,循环处理时间,是事件驱动机制的核心,既会处理普通的网络事件,也会处理定时器事件。ngx_process_events_and_timers是Nginx实际处理web业务的方法,所有业务的执行都是由它开始的,它涉及Nginx完整的事件驱动机制!!特别重要~ - void
- ngx_process_events_and_timers(ngx_cycle_t *cycle)
- {
- ngx_uint_t flags;
- ngx_msec_t timer, delta;
-
- if (ngx_timer_resolution) {
- timer = NGX_TIMER_INFINITE;
- flags = 0;
-
- } else {
- timer = ngx_event_find_timer();
- flags = NGX_UPDATE_TIME;
-
- #if (NGX_THREADS)
-
- if (timer == NGX_TIMER_INFINITE || timer > 500) {
- timer = 500;
- }
-
- #endif
- }
-
-
-
-
-
-
-
-
-
-
-
- if (ngx_use_accept_mutex) {
-
- if (ngx_accept_disabled > 0) {
- ngx_accept_disabled--;
-
- } else {
-
- if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
- return;
- }
-
-
- if (ngx_accept_mutex_held) {
-
- flags |= NGX_POST_EVENTS;
- } else {
-
-
-
- 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);
-
-
- if (ngx_posted_accept_events) {
- ngx_event_process_posted(cycle, &ngx_posted_accept_events);
- }
-
-
- 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);
-
-
- if (ngx_posted_events) {
- if (ngx_threaded) {
- ngx_wakeup_worker_thread(cycle);
-
- } else {
- ngx_event_process_posted(cycle, &ngx_posted_events);
- }
- }
- }
上面代码中要进行说明的是,flags被设置后作为函数ngx_process_events方法的一个参数,在epoll模块中这个接口的实现方法是ngx_epoll_process_events(其具体代码见http://blog.csdn.net/xiajun07061225/article/details/9250341)。当falgs标志位含有nGX_POST_EVENTS时是不会立即调用事件的handler回调方法的,代码如下所示:-
- 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);
- }
通过上面的代码可以看出,先处理ngx_posted_accept_events队列中的事件,处理完毕后立即释放ngx_accept_mutex锁,接着再处理ngx_posted_events队列中事件。这样大大减少了ngx_accept_mutex锁占用的时间
下面看看ngx_trylock_accept_mutex的具体实现(src/event/ngx_event_accept.c): - 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");
-
-
- if (ngx_accept_mutex_held
- && ngx_accept_events == 0
- && !(ngx_event_flags & NGX_USE_RTSIG_EVENT))
- {
- 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_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;
- }
-
- return NGX_OK;
- }
调用这个方法的结果是,要么唯一获取到锁且其epoll等事件驱动模块开始监控web端口上的新连接事件。这种情况下调用process_events方法时就会既处理已有连接上的事件,也处理新连接的事件。要么没有获取到锁,当前进程不会收到新连接事件。这种情况下process_events只处理已有连接上的事件。
参考资料:《深入理解Nginx》
|