Nginx解决惊群现象

惊群现象:所有的工作进程都在等待一个socket,当socket客户端连接时,所有工作线程都被唤醒,但最终有且仅有一个工作线程去处理该连接,其他进程又要进入睡眠状态。

Nginx通过控制争抢处理socket的进程数量抢占ngx_accept_mutex锁解决惊群现象。只有一个ngx_accept_mutex锁,谁拿到锁,谁处理该socket的请求。

如果当前进程的连接数>最大连接数*7/8,则该进程不参与本轮竞争。

//nginx的每个worker进程在函数ngx_process_events_and_timers中处理事件。下面代码是ngx_process_events_and_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 
        {  
            //工作进程抢占锁,抢占成功的进程将ngx_accept_mutex_held变量置为1。拿到锁,意味着socket被放到本进程的epoll中了,如果没有拿到锁,则socket会被从epoll中取出。  
            //此处trylock是非阻塞锁,如果没有抢占到锁,进程会立刻返回,处理自己监听的描述符上的读写事件。
            if(pthread_mutex_trylock(&ngx_accept_mutex))
            {
                ngx_accept_mutex_held = 1;
            }
            else
            {
                //设置time时间,500ms后就去争抢锁,使得没有拿到锁的worker进程,去拿锁的频繁更高,确保每个进程可以处理几乎相同数量的fd的读写。
                timer = 500;
                ngx_accept_mutex_held = 0;
            }

             //拿到锁的话,置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;
            }
        }
        //继续epoll_wait等待处理事件
        int num = epoll_wait(epollfd, events, length, timer);
        for(int i=0; i<num; ++i)
        {
            ......
            //如果是读事件
            if (revents & EPOLLIN)
            {
                //有NGX_POST_EVENTS标志的话,就把accept事件放到ngx_posted_accept_events队列中,把正常的事件放到ngx_posted_events队列中延迟处理
                //新连接事件队列ngx_posted_accept_events
                //用户读写事件队列ngx_posted_events
                if (flags & NGX_POST_EVENTS)
                {
                    queue = rev->accept ? 
                        &ngx_posted_accept_events:
                        &ngx_posted_events;

                    ngx_post_event(rev, queue);
                }
                else//处理
                {
                    rev->handler(rev);
                }
            }

            //如果是写事件
            if (revents & EPOLLOUT)
            {
                //同理,有NGX_POST_EVENTS标志的话,写事件延迟处理,放到ngx_posted_events队列中 
            if (flags & NGX_POST_EVENTS) 
            {
                ngx_post_event(rev, &ngx_posted_events);
            }
            else//处理
            {
                rev->handler(rev);
            }
        }
    }

    //先处理新用户的连接事件
        ngx_event_process_posted(cycle, &ngx_posted_accept_events);

    //释放处理新连接的锁
    if(ngx_accept_mutex_held)
    {
        pthread_mutex_unlock(&ngx_accept_mutex);
    }

      //再处理已建立连接的用户读写事件
      ngx_event_process_posted(cycle, &ngx_posted_events);
}

nginx从抢锁、释放锁到处理事件的整个过程,我已经结合代码做了注释,相信大家对整个过程应该已经不陌生了。至于pthread_mutex_trylock()中进程是如何抢占锁的,这就有赖于实现抢占的算法了,此处只是解释处理过程,并不关心抢占实现原理。感兴趣的同学可以自己搜索相关资料。

1.先处理新用户的连接事件,再释放处理新连接的锁,为什这么设计?
如果刚释放锁,就有新连接,刚获得锁的进程要给等待队列中添加sockfd时,此时原获得锁的进程也要从等待队列中删除sockfd,TCP的三次握手的连接是非线程安全的。为了避免产生错误,使得将sockfd从等待队列中删除后,再让新的进程抢占锁,处理新连接。

2.拿到锁,将任务放在任务队列中,不是立刻去处理,为什这么设计?
每个进程要处理新连接事件,必须拿到锁,当前进程将新连接事件的sokect添加到任务队列中,立即释放锁,让其他进程尽快获得锁,处理用户的连接。

你可能有个疑问,如果没有加锁,有新事件连接时,所有的进程都会被唤醒执行accept,有且仅有一个进程会accept返回成功,其他进程都重新进入睡眠状态。现在有了锁,在发生accept之前,进程们要去抢占锁,也是有且仅有一个进程会抢到锁,其他进程也是重新进入睡眠状态。即:不论是否有accept锁,都会有很多进程被唤醒再重新进入睡眠状态的过程,那惊群现象如何解释

其实,锁不能解决惊群现象,惊群现象是没办法解决的,很多进程被同时唤醒是一个必然的过程。Nginx中通过检查当前进程的连接数是否>最大连接数*7/8来判断当前进程是否能处理新连接,减少被唤醒的进程数量,也实现了简单的负载均衡。锁只能保证不让所有的进程去调用accept函数,解决了很多进程调用accept返回错误,锁解决的是惊群现象的错误,并不是解决了惊群现象!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值