Nginx解决惊群现象

来源:互联网 发布:伊美服饰淘宝网 编辑:程序博客网 时间:2024/05/14 22:42

惊群现象:所有的工作进程都在等待一个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返回错误,锁解决的是惊群现象的错误,并不是解决了惊群现象!