Nginx惊群问题

Nginx惊群问题

1. 简介

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

其实,在linux2.6内核上,accept系统调用已经不存在惊群了(至少我在2.6.18内核版本上已经不存在)。大家可以写个简单的程序试下,在父进程中bind,listen,然后fork出子进程,所有的子进程都accept这个监听句柄。这样,当新连接过来时,大家会发现,仅有一个子进程返回新建的连接,其他子进程继续休眠在accept调用上,没有被唤醒。

      但是很不幸,通常我们的程序没那么简单,不会愿意阻塞在accept调用上,我们还有许多其他网络读写事件要处理,linux下我们爱用epoll解决非阻塞socket。所以,即使accept调用没有惊群了,我们也还得处理惊群这事,因为epoll有这问题。上面说的测试程序,如果我们在子进程内不是阻塞调用accept,而是用epoll_wait,就会发现,新连接过来时,多个子进程都会在epoll_wait后被唤醒!

      nginx就是这样,master进程监听端口号(例如80),所有的nginx worker进程开始用epoll_wait来处理新事件(linux下),如果不加任何保护,一个新连接来临时,会有多个worker进程在epoll_wait后被唤醒,然后发现自己accept失败。现在,我们可以看看nginx是怎么处理这个惊群问题了。


2. Nginx如何解决惊群问题?

很多操作系统的最新版本的内核已经在事件驱动机制中解决了惊群问题,但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类型的双链表。

每个worker进程都在函数ngx_process_events_and_timers方法中循环处理事件,这也正是事件驱动机制的核心。该函数既会处理普通的网络事件,也会处理定时器事件。该函数中核心的操作主要有三个:1.调用所使用的事件驱动模块实现的process_events方法,处理网络事件。2.处理两个post事件队列中的事件,实际上就是分别调用ngx_event_process_posted(cycle,&ngx_posted_accept_events)和ngx_event_process_posted(cycle,&ngx_posted_events)方法。3.处理定时器事件,实际上就是调用ngx_event_expire_timers()方法。
void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
.......
    //ngx_use_accept_mutex表示是否需要通过对accept加锁来解决惊群问题。当nginx worker进程数>1时且配置文件中打开accept_mutex时,这个标志置为1
    if (ngx_use_accept_mutex) {
    		//ngx_accept_disabled表示此时满负荷,没必要再处理新连接了,我们在nginx.conf曾经配置了每一个nginx worker进程能够处理的最大连接数,当达到最大数的7/8时,ngx_accept_disabled为正,说明本nginx worker进程非常繁忙,将不再去处理新连接,这也是个简单的负载均衡
        if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;

        } else {
        		//获得accept锁,多个worker仅有一个可以得到这把锁。获得锁不是阻塞过程,都是立刻返回,获取成功的话ngx_accept_mutex_held被置为1。拿到锁,意味着监听句柄被放到本进程的epoll中了,如果没有拿到锁,则监听句柄会被从epoll中取出。
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }

						//拿到锁的话,置flag为NGX_POST_EVENTS,这意味着ngx_process_events函数中,任何事件都将延后处理,会把accept事件都放到ngx_posted_accept_events链表中,epollin|epollout事件都放到ngx_posted_events链表中
            if (ngx_accept_mutex_held) {
                flags |= NGX_POST_EVENTS;

            } else {
            	//拿不到锁,也就不会处理监听的句柄,这个timer实际是传给epoll_wait的超时时间,修改为最大ngx_accept_mutex_delay意味着epoll_wait更短的超时返回,以免新连接长时间没有得到处理
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }
........
		//事件处理核心函数
    (void) ngx_process_events(cycle, timer, flags);
........
		//如果ngx_posted_accept_events链表有数据,就开始accept建立新连接
    if (ngx_posted_accept_events) {
        ngx_event_process_posted(cycle, &ngx_posted_accept_events);
    }

		//释放锁后再处理下面的EPOLLIN EPOLLOUT请求
    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);
		//然后再处理正常的数据读写请求。因为这些请求耗时久,所以在ngx_process_events里NGX_POST_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。当flags标志位包含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方法的实现。在打开accept_mutex锁的情况下,只有调用ngx_trylock_accept_mutex方法后,当前的worker进程才会去试着监听web端口,具体实现如下:
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;
}


参考资料:
1.《深入理解Nginx模块开发与架构解析》
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值