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

来源:互联网 发布:centos7修改ssh端口 编辑:程序博客网 时间:2024/06/14 12:10

       什么是“惊群”问题呢?我们可以考虑下面这个场景:某一时刻恰好所有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_tngx_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函数很关键,下面是它的源码:

voidngx_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_tngx_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_tngx_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 0
原创粉丝点击