【Nginx】惊群问题

转自:江南烟雨

惊群问题的产生

在建立连接的时候,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
    }

    /*ngx_use_accept_mutex表示是否需要通过对accept加锁来解决惊群问题。当使用了master模式,nginx worker进程数>1时且配置文件中打开accept_mutex时,这个标志置为1
    它在函数ngx_event_process_int中被设置,源代码为:
    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_use_accept_mutex) {
        //负载均衡处理
        if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;

        } else {
            //调用ngx_trylock_accept_mutex方法,尝试获取accept锁
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }

            //拿到锁
            if (ngx_accept_mutex_held) {
                /*给flags增加标记NGX_POST_EVENTS,这个标记作为处理时间核心函数ngx_process_events的一个参数,这个函数中所有事件将延后处理。会把accept事件都放到ngx_posted_accept_events链表中,epollin|epollout普通事件都放到ngx_posted_events链表中 */
                flags |= NGX_POST_EVENTS;
            } else {
                /*获取锁失败,意味着既不能让当前worker进程频繁的试图抢锁,也不能让它经过太长事件再去抢锁
                下面的代码:即使开启了timer_resolution时间精度,牙需要让ngx_process_change方法在没有新事件的时候至少等待ngx_accept_mutex_delay毫秒之后再去试图抢锁
                而没有开启时间精度时,如果最近一个定时器事件的超时时间距离现在超过了ngx_accept_mutex_delay毫秒,也要把timer设置为ngx_accept_mutex_delay毫秒,这是因为当前进程虽然没有抢到accept_mutex锁,但也不能让ngx_process_change方法在没有新事件的时候等待的时间超过ngx_accept_mutex_delay,这会影响整个负载均衡机制*/
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }

    //计算ngx_process_events消耗的时间
    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);

    //ngx_posted_accept_events链表有数据,开始accept新连接
    if (ngx_posted_accept_events) {
        ngx_event_process_posted(cycle, &ngx_posted_accept_events);
    }

    //释放锁后再处理ngx_posted_events链表中的普通事件
    if (ngx_accept_mutex_held) {
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }

    //如果ngx_process_events消耗的时间大于0,那么这是可能有新的定时器事件触发
    if (delta) {
        //处理定时器事件
        ngx_event_expire_timers();
    }

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "posted events %p", ngx_posted_events);

    //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) {
                /*如果要在post队列中延后处理该事件,首先要判断它是新连接时间还是普通事件
                以确定是把它加入到ngx_posted_accept_events队列或者ngx_posted_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)
{
    //尝试获取accept_mutex锁。注意是非阻塞的。返回1表示成功,返回0表示失败。
    //ngx_accept_mutex 定义:ngx_shmtx_t    ngx_accept_mutex;(ngx_shmtx_t是Nginx封装的互斥锁,用于经常间同步)
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {

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

        //获取到锁,但是标志位ngx_accept_mutex_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_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);

    //获取锁失败,但是标志位ngx_accept_mutex_held仍然为1,即当前进程还处在获取到锁的状态,这是不正确的
    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只处理已 有连接上的事件。

参考:

http://russelltao.iteye.com/blog/1405352

【Nginx】惊群问题

时间: 2024-08-24 13:08:52

【Nginx】惊群问题的相关文章

nginx惊群问题

*:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* BLOCKS =============================================================================*/ p, blockquote, ul, ol, dl, table, pre { margin: 15px 0; } /* HEAD

【转载】“惊群”,看看nginx是怎么解决它的

原文:http://blog.csdn.net/russell_tao/article/details/7204260 在说nginx前,先来看看什么是“惊群”?简单说来,多线程/多进程(linux下线程进程也没多大区别)等待同一个socket事件,当这个事件发生时,这些线程/进程被同时唤醒,就是惊群.可以想见,效率很低下,许多进程被内核重新调度唤醒,同时去响应这一个事件,当然只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠(也有其他选择).这种性能浪费现象就是惊群. 惊群通常发

Nginx如何解决“惊群”现象

首先解释下什么是"惊群"现象:如果多个工作进程同时拥有某个监听套接口,那么一旦该套接口出现某客户端请求,此时就将引发所有拥有该套接口的工作进程去争抢这个请求,能争抢到的肯定只有某一个工作进程,而其他工作进程注定要无功而返,这种现象即为"惊群". Nginx解决这种"惊群"现象使用的是负载均衡的策略,接下来先结合Nginx的源码详细介绍下Nginx的这种负载均衡策略. 首先是Nginx如何开启负载均衡策略:当然运行的Nginx要是多进程模型,并且工

Nginx源码分析 - 主流程篇 - 多进程的惊群和进程负载均衡处理

Linux2.6版本之前还存在对于socket的accept的惊群现象.之后的版本已经解决掉了这个问题. 惊群是指多个进程/线程在等待同一资源时,每当资源可用,所有的进程/线程都来竞争资源的现象. Nginx采用的是多进程的模式.假设Linux系统是2.6版本以前,当有一个客户端要连到Nginx服务器上,Nginx的N个进程都会去监听socket的accept的,如果全部的N个进程都对这个客户端的socket连接进行了监听,就会造成资源的竞争甚至数据的错乱.我们要保证的是,一个链接在Nginx的

Nginx——事件驱动机制(惊群问题,负载均衡)

事件框架处理流程 每个worker子进程都在ngx_worker_process_cycle方法中循环处理事件,处理分发事件则在ngx_worker_process_cycle方法中调用ngx_process_events_and_timers方法,循环调用该方法就是 在处理所有事件,这正是事件驱动机制的核心.该方法既会处理普通的网络事件,也会处理定时器事件. ngx_process_events_and_timers方法中核心操作主要有以下3个: 1)  调用所使用事件驱动模块实现的proce

“惊群”,看看nginx是怎么解决它的

在说nginx前,先来看看什么是“惊群”?简单说来,多线程/多进程(linux下线程进程也没多大区别)等待同一个socket事件,当这个事件发生时,这些线程/进程被同时唤醒,就是惊群.可以想见,效率很低下,许多进程被内核重新调度唤醒,同时去响应这一个事件,当然只有一个进程能处理事件成功,其他的进程在处理该事件失败后重新休眠(也有其他选择).这种性能浪费现象就是惊群. 惊群通常发生在server 上,当父进程绑定一个端口监听socket,然后fork出多个子进程,子进程们开始循环处理(比如acce

nginx的惊群问题

nginx的master-worker模式 nginx采用master-worker进程的模式,master负责解析配置,启动worker进程和处理信号,比如restart重启worker进程,worker负责真正处理请求.当有多个worker进程时,一个请求将被哪个worker进程处理呢?更具体一点,发送请求的客户端会与哪个worker进程建立TCP连接呢 结论: 1.惊群确实存在于epoll中,而且只在老的linux内核中才会出现 2.nginx的accept_mutex锁让只有一个work

Nginx中的惊群现象解决方法

*什么是惊群现象?Nginx中用了什么方法来避免这种问题的发生?本篇就解决这两个问题...→_→* 惊群现象的定义与危害 在Nginx中,每一个worker进程都是由master进程fork出来的.master进程创建socket后进行listen.bind操作,fork出来的worker继承了socket,调用accpet开始监听等待网络连接 如果这时有多个worker进程都在等待事件的发生.当事件发生时,这些worker进程被同时唤醒,但最终只有一个worker进程可以处理事件成功,其他的w

linux 惊群问题

1. 结论 对于惊群的资料,网上特别多,良莠不齐,也不全面.看的时候,有的资料说,惊群已经解决了,有的资料说,惊群还没解决.. 哪个才是对的?!  一怒之下,在研究各种公开资料的基础上,特意查对了linux源码,总结了此文.希望对有需要的人略有帮助,希望各位大神轻拍,如有错漏,不吝指教,感激不尽.([email protected]) 先说结论吧: 1. Linux多进程accept系统调用的惊群问题(注意,这里没有使用select.epoll等事件机制),在linux 2.6版本之前的版本存在