Nginx多进程模式的“惊群”问题

       什么是“惊群”问题呢?我们可以考虑下面这个场景:某一时刻恰好所有worker子进程都休眠且等待新连接的系统调用(epoll_wait),这时有一个用户向服务器发起了连接,内核在收到TCPSYN包时,会激活所有的休眠的worker子进程,当然,此时只有最先开始执行accept的子进程可以成功创建新的连接,而其他的worker子进程都会accept失败。这些accept失败的子进程被内核唤醒是没有必要的,它们被唤醒后的执行很可能也是多余的,那么这一时刻它们占用了本不需要占用的系统资源,引发了不必要的进程上下文切换,增加了系统开销。(摘自《Nginx模块开发与架构解析》)

    针对惊群问题,可能有的操作系统已经有了应对办法,但实际上,Nginx针对惊群问题的处理也是很巧妙的:它规定同一时刻只能有唯一的worker子进程监听Web端口,这样新连接事件只能唤醒唯一正在监听端口的woker子进程。Nginx用ngx_accept_mutex以及延迟队列等技术结合使用,完成了这一处理。具体怎么做的呢?我们看一下源码就明白了。

    在介绍源码前,先说一下Nginx监听新连接以及处理读写事件的流程:(以epoll事件处理机制举例)

  ngx_worker_process_cycle-->ngx_worker_thread--> ngx_event_process_init-->ngx_process_events_and_timers-->process_events函数-->事件的handler回调函数

       其中process_events是具体事件处理模块(这里是epoll)的ngx_event_actions中的process_events钩子函数。

       ngx_events_process_init函数是ngx_event_core_module模块实现的回调函数,在forkwork子进程后,每一个worker进程会在调用ngx_event_core_module模块的ngx_event_process_init方法后才会正式进入工作循环。下面我们看一下它都做了什么工作:

//这个函数做了很多事情,此函数是在fork出子进程后
//每个work进程调用的
static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{
    ngx_uint_t           m, i;
    ngx_event_t         *rev, *wev;
    ngx_listening_t     *ls;
    ngx_connection_t    *c, *next, *old;
    ngx_core_conf_t     *ccf;
    ngx_event_conf_t    *ecf;
    ngx_event_module_t  *module;

    ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
    ecf = ngx_event_get_conf(cycle->conf_ctx, ngx_event_core_module);

	//当打开accept_mutex负载均衡锁且用Master模式且work进程大于1时
	//才正式确定了进程将使用accept_mutex负载均衡锁
    if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) {
        ngx_use_accept_mutex = 1;
        ngx_accept_mutex_held = 0;
        ngx_accept_mutex_delay = ecf->accept_mutex_delay;

    } else {
        ngx_use_accept_mutex = 0;
    }

#if (NGX_THREADS)
    ngx_posted_events_mutex = ngx_mutex_init(cycle->log, 0);
    if (ngx_posted_events_mutex == NULL) {
        return NGX_ERROR;
    }
#endif

	//初始化红黑树实现的定时器
    if (ngx_event_timer_init(cycle->log) == NGX_ERROR) {
        return NGX_ERROR;
    }


	//在调用use配置项指定的事件模块中,在Ngx_event_module_t
	//接口下,Ngx_event_actions_t中的iNit方法进行这个事件模块
	//的初始化工作
    for (m = 0; ngx_modules[m]; m++) {
        if (ngx_modules[m]->type != NGX_EVENT_MODULE) {
            continue;
        }

        if (ngx_modules[m]->ctx_index != ecf->use) {
            continue;
        }

        module = ngx_modules[m]->ctx;

        if (module->actions.init(cycle, ngx_timer_resolution) != NGX_OK) {
            /* fatal */
            exit(2);
        }

        break;
    }

#if !(NGX_WIN32)

	//如果配置了time_resolution配置项,即表明需要控制时间精度,这时会调用
	//setitimer方法,设置事件间隔为timer_resolution毫秒来回调
	//ngx_timer_signale_handler回调函数
    if (ngx_timer_resolution && !(ngx_event_flags & NGX_USE_TIMER_EVENT)) {
        struct sigaction  sa;
        struct itimerval  itv;

        ngx_memzero(&sa, sizeof(struct sigaction));
        sa.sa_handler = ngx_timer_signal_handler;
        sigemptyset(&sa.sa_mask);

        if (sigaction(SIGALRM, &sa, NULL) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "sigaction(SIGALRM) failed");
            return NGX_ERROR;
        }

        itv.it_interval.tv_sec = ngx_timer_resolution / 1000;
        itv.it_interval.tv_usec = (ngx_timer_resolution % 1000) * 1000;
        itv.it_value.tv_sec = ngx_timer_resolution / 1000;
        itv.it_value.tv_usec = (ngx_timer_resolution % 1000 ) * 1000;

        if (setitimer(ITIMER_REAL, &itv, NULL) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "setitimer() failed");
        }
    }

    if (ngx_event_flags & NGX_USE_FD_EVENT) {
        struct rlimit  rlmt;

        if (getrlimit(RLIMIT_NOFILE, &rlmt) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "getrlimit(RLIMIT_NOFILE) failed");
            return NGX_ERROR;
        }

        cycle->files_n = (ngx_uint_t) rlmt.rlim_cur;

        cycle->files = ngx_calloc(sizeof(ngx_connection_t *) * cycle->files_n,
                                  cycle->log);
        if (cycle->files == NULL) {
            return NGX_ERROR;
        }
    }

#endif

	//预分配Ngx_connection_t数组作为连接池
    cycle->connections =
        ngx_alloc(sizeof(ngx_connection_t) * cycle->connection_n, cycle->log);
    if (cycle->connections == NULL) {
        return NGX_ERROR;
    }

    c = cycle->connections;

	//预分配读事件池
    cycle->read_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n,
                                   cycle->log);
    if (cycle->read_events == NULL) {
        return NGX_ERROR;
    }

    rev = cycle->read_events;
    for (i = 0; i < cycle->connection_n; i++) {
        rev[i].closed = 1;
        rev[i].instance = 1;
#if (NGX_THREADS)
        rev[i].lock = &c[i].lock;
        rev[i].own_lock = &c[i].lock;
#endif
    }

	//预分配写事件池
    cycle->write_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n,
                                    cycle->log);
    if (cycle->write_events == NULL) {
        return NGX_ERROR;
    }

    wev = cycle->write_events;
    for (i = 0; i < cycle->connection_n; i++) {
        wev[i].closed = 1;
#if (NGX_THREADS)
        wev[i].lock = &c[i].lock;
        wev[i].own_lock = &c[i].lock;
#endif
    }

    i = cycle->connection_n;
    next = NULL;

	//按照序号将读/写事件设置到每一个ngx_connection_t连接
	//对象中,同时连接data指针将空闲链表准备好
    do {
        i--;

        c[i].data = next;
        c[i].read = &cycle->read_events[i];
        c[i].write = &cycle->write_events[i];
        c[i].fd = (ngx_socket_t) -1;

        next = &c[i];

#if (NGX_THREADS)
        c[i].lock = 0;
#endif
    } while (i);

    cycle->free_connections = next;
    cycle->free_connection_n = cycle->connection_n;

    /* for each listening socket */

	//在刚刚建立好的连接池中,为所有ngx_listening_t监听对象中的
	//connection成员分配连接,同时对监听端口的读事件设置处理方法为
	//ngx_event_accept,也就是说,有新的连接事件时将调用ngx_event_accept
	//方法建立新的连接
    ls = cycle->listening.elts;
    for (i = 0; i < cycle->listening.nelts; i++) {

        c = ngx_get_connection(ls[i].fd, cycle->log);

        if (c == NULL) {
            return NGX_ERROR;
        }

        c->log = &ls[i].log;

        c->listening = &ls[i];
        ls[i].connection = c;

        rev = c->read;

        rev->log = c->log;
        rev->accept = 1;

#if (NGX_HAVE_DEFERRED_ACCEPT)
        rev->deferred_accept = ls[i].deferred_accept;
#endif

        if (!(ngx_event_flags & NGX_USE_IOCP_EVENT)) {
            if (ls[i].previous) {

                /*
                 * delete the old accept events that were bound to
                 * the old cycle read events array
                 */

                old = ls[i].previous->connection;

                if (ngx_del_event(old->read, NGX_READ_EVENT, NGX_CLOSE_EVENT)
                    == NGX_ERROR)
                {
                    return NGX_ERROR;
                }

                old->fd = (ngx_socket_t) -1;
            }
        }

#if (NGX_WIN32)

        if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
            ngx_iocp_conf_t  *iocpcf;

            rev->handler = ngx_event_acceptex;

            if (ngx_use_accept_mutex) {
                continue;
            }

            if (ngx_add_event(rev, 0, NGX_IOCP_ACCEPT) == NGX_ERROR) {
                return NGX_ERROR;
            }

            ls[i].log.handler = ngx_acceptex_log_error;

            iocpcf = ngx_event_get_conf(cycle->conf_ctx, ngx_iocp_module);
            if (ngx_event_post_acceptex(&ls[i], iocpcf->post_acceptex)
                == NGX_ERROR)
            {
                return NGX_ERROR;
            }

        } else {
            rev->handler = ngx_event_accept;

			//如果是mater模式的就不能直接放到事件驱动模块中
			//而是只有在执行ngx_trylock_accept_mutex函数后,成功获取锁
			//才会将所有的新连接事件加入到epoll中
            if (ngx_use_accept_mutex) {
                continue;
            }

			//将监听对象连接的读事件添加到事件驱动模块中
            if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
                return NGX_ERROR;
            }
        }

#else

        rev->handler = ngx_event_accept;

        if (ngx_use_accept_mutex) {
            continue;
        }

        if (ngx_event_flags & NGX_USE_RTSIG_EVENT) {
            if (ngx_add_conn(c) == NGX_ERROR) {
                return NGX_ERROR;
            }

        } else {
            if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
                return NGX_ERROR;
            }
        }

#endif

    }

    return NGX_OK;
}

        可以看出这个函数做了一些关键的初始化工作,如初始化红黑树定时器、初始化连接池、初始化读写事件池、将新连接事件的handler函数设置为ngx_event_accept以及对于单进程模式,还要向epoll中添加新连接事件。另外一个需要注意地方的是:当处于Master多进程模式时,ngx_use_accept_mutex为true,这个时候新连接事件是没有添加的epoll中的,下面我们会介绍在什么时候添加。

       之后每个Worker子进程正式进入工作循环中,在ngx_worker_process_cycle函数中循环调用ngx_process_events_and_timers函数处理事件。ngx_process_events_timers函数很关键,下面是它的源码:

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 {
		//得到最近超时的timue,并将flags设置为NGX_UPDATE_TIME
        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) {
		//如果ngx_accept_disabled大于0,说明本进程连接太多,不处理新的连接
        if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;

        } else {
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }

			//开始处理新连接事件,这时将flags标志位加上NGX_POST_EVENTS。
			//这样在ngx_epoll_module的ngx_epoll_process_events这个方法中
			//是不会立刻调用事件的handler回调方法的
            if (ngx_accept_mutex_held) {
                flags |= NGX_POST_EVENTS;
					
            } else {
				//未获取到accept_mutex锁,意味着不能让当前的worker进程
				//频繁地试图抢锁,也不能让它经过太长时间再去抢锁
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }

	//调用Ngx_process_events方法,并计算Ngx_process_events执行时消耗的
	//时间,delta会影响触发定时器的执行
    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);
    }

	//如果Ngx_posted_events消耗的时间大于0,而且这是可能
	//有新的定时器事件被触发,那么处理定时器事件
    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);
        }
    }
}

        上面代码中的ngx_accept_disabled是负载均衡阈值,它决定着一个进程最多能处理多少连接,如果超过一个阈值就不能再处理新连接事件了。ngx_trylock_accept_mutex(cycle)函数也是很关键的,上面提到的Master模式新连接事件的添加就是在这个函数中进行的。如下所示: 

ngx_int_t
ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
	//使用进程间的同步锁,试图获取ngx_accept_mutex锁
	//返回1表示成功拿到锁,返回0表示获取锁失败。这个
	//获取锁的过程是非阻塞的,此时一旦锁被其他worker子进程
	//占用,立刻返回
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {

        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "accept mutex locked");

		//当ngx_accept_mutext_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);
            return NGX_ERROR;
        }

		//经过Ngx_enable_accept_events方法的调用,当前进程的事件
		//驱动模块已经开始监听所有的端口,这时需要把Ngx_accept_mutex_held
		//标志位置为1方便本进程的其他模块了解它目前已经获取到锁
        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);

	//获取锁失败但是标志位仍未1肯定是有问题的,需要处理
    if (ngx_accept_mutex_held) {
		//将所有本进程监听连接的读事件从事件驱动模块中移除
        if (ngx_disable_accept_events(cycle) == NGX_ERROR) {
            return NGX_ERROR;
        }

		//将标志位置1
        ngx_accept_mutex_held = 0;
    }

    return NGX_OK;
}

       则ngx_worker_process_cycle执行完ngx_trylock_accept_mutex后,如果获取锁成功,则当前进程将可以监听所有的新连接事件,并设置标志位;如果获取锁失败,这接下来仍然可以调用ngx_process_events函数进而调用的epoll模块的process events函数继续处理进程之前的那些普通事件:

static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
    int                events;
    uint32_t           revents;
    ngx_int_t          instance, i;
    ngx_uint_t         level;
    ngx_err_t          err;
    ngx_event_t       *rev, *wev, **queue;
    ngx_connection_t  *c;

    /* NGX_TIMER_INFINITE == INFTIM */

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "epoll timer: %M", timer);

	//调用epoll_wait获取事件
    events = epoll_wait(ep, event_list, (int) nevents, timer);

    err = (events == -1) ? ngx_errno : 0;

	//Nginx对时间的缓存和管理,当flags标志位指示要更新时间时,就在这里更新
    if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
        ngx_time_update();
    }

    if (err) {
        if (err == NGX_EINTR) {

            if (ngx_event_timer_alarm) {
                ngx_event_timer_alarm = 0;
                return NGX_OK;
            }

            level = NGX_LOG_INFO;

        } else {
            level = NGX_LOG_ALERT;
        }

        ngx_log_error(level, cycle->log, err, "epoll_wait() failed");
        return NGX_ERROR;
    }

    if (events == 0) {
        if (timer != NGX_TIMER_INFINITE) {
            return NGX_OK;
        }

        ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
                      "epoll_wait() returned no events without timeout");
        return NGX_ERROR;
    }

	//上锁
    ngx_mutex_lock(ngx_posted_events_mutex);

	//遍历本次epoll_wait返回的所有事件
    for (i = 0; i < events; i++) {

		//ptr就是连接的地址(C语言没有类的类型转换一说,只能传递地址)但是最后
		//一位有特殊的含义,需要把它屏蔽掉
        c = event_list[i].data.ptr;

		//将地址的最后一位取出来,用instance变量标识
        instance = (uintptr_t) c & 1;

		//无论是32位还是64位机器,其地址的最后一位肯定是0,可以用下面这行语句
		//把ngx_connection_t的地址还原到真正的地址值
        c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);

        //取出读事件
		rev = c->read;

		//判断这个事件是否为过期事件
        if (c->fd == -1 || rev->instance != instance) {

            /*
             * the stale event from a file descriptor
             * that was just closed in this iteration
             */

			//如果fd套接字的描述符为-1或者Instance标志位不相等时表示这个事件
			//已经过期了,不用处理
            ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                           "epoll: stale event %p", c);
            continue;
        }

		//取出事件类型
        revents = event_list[i].events;

        ngx_log_debug3(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "epoll: fd:%d ev:%04XD d:%p",
                       c->fd, revents, event_list[i].data.ptr);

        if (revents & (EPOLLERR|EPOLLHUP)) {
            ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                           "epoll_wait() error on fd:%d ev:%04XD",
                           c->fd, revents);
        }

#if 0
        if (revents & ~(EPOLLIN|EPOLLOUT|EPOLLERR|EPOLLHUP)) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
                          "strange epoll_wait() events fd:%d ev:%04XD",
                          c->fd, revents);
        }
#endif

        if ((revents & (EPOLLERR|EPOLLHUP))
             && (revents & (EPOLLIN|EPOLLOUT)) == 0)
        {
            /*
             * if the error events were returned without EPOLLIN or EPOLLOUT,
             * then add these flags to handle the events at least in one
             * active handler
             */

            revents |= EPOLLIN|EPOLLOUT;
        }

		//如果是读事件且该事件是活跃的
        if ((revents & EPOLLIN) && rev->active) {

			if ((flags & NGX_POST_THREAD_EVENTS) && !rev->accept) {
                rev->posted_ready = 1;

            } else {
                rev->ready = 1;
            }

			//flags参数中含有NGX_POST_EVENTS表示这批事件要延后处理
            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);
            }
        }

        wev = c->write;

        if ((revents & EPOLLOUT) && wev->active) {

            if (flags & NGX_POST_THREAD_EVENTS) {
                wev->posted_ready = 1;

            } else {
                wev->ready = 1;
            }

            if (flags & NGX_POST_EVENTS) {
				//将这个事件添加到post队列中延后处理
                ngx_locked_post_event(wev, &ngx_posted_events);

            } else {
                wev->handler(wev);
            }
        }
    }

    ngx_mutex_unlock(ngx_posted_events_mutex);

    return NGX_OK;
}

       可以看出NGX_POST_EVENTS标志位决定了当前从epoll中获取的事件是立刻handler还是放入posted队列中延后处理。

       再次回到ngx_process_events_and_timers函数,可以看到在执行完网络事件后,还需要执行两个posted延迟队列里的事件,为什么需要两个延迟队列呢?这里也非常巧妙,Nginx用这两个队列将事件做了分类。试想一下,一个进程获取了ngx_accept_mutex锁后,什么时候释放呢?是要等到所有的事件都结束吗?如果普通网络事件很耗时怎么办?所以Nginx将队列分为ngx_posted_accept_events和ngx_posted_events,前者存放新连接事件,后者存放普通事件,在进程处理完新连接事件后,立即释放锁,这样大大减少了ngx_accept_mutex锁的占用时间。

       总之,最终从队列中还是调用事件的handler函数,拿新连接事件举例,新连接事件的handler函数是之前提过的ngx_event_accept函数,在这个函数中调用accept函数创建连接,最终这个连接可能要交给HTTP框架继续处理,如设置新的读写事件。

  

 

  

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值