Bootstrap

nginx惊群现象

大笑其实我自己本身也不太懂,大家相互学习学习,这些也是我看过很多东西之后写的。


惊群:

在多线程或者多进程的情况下,多个worker等待同一个socket事件,当事件发生时这些线程同时被内核调度唤醒,但是只是

会有一个worker来处理事件,其他的worker在accept()返回失败后重新休眠,这种性能浪费现象就是惊群,可以想象:这种

现象效率很低。


我们以服务器和客户端的通信为例《基于tcp》

应该说惊群现象它发生在服务器端,主线程(父进程)负责 绑定端口号监听套接字,然后就创建线程(创建子进程),这些

线程会同时循环处理accept连接的套接字,每当有客户端发起一个TCP链接的时候,这些子线程就会同时被唤醒,然后选择其

中一个子线程进行accept的连接,其他的线程都会连接失败,重新进入休眠期。


那么就有人说啦,为什么不直接只是让主线程去accept进行连接,然后用一些同步的实现方法让子进程处理新的链接,这样的

话就是可以避免惊群现象啦,但是,如果是这样的话,它就只能用来进行accept连接,但是这样的话,这对cpu其实是一种资源

浪费,所以呢,惊群现象是必须要解决的。


在linux中,我们通常用epoll解决非阻塞socket,所以accept的调用就不会有惊群现象了,但是我们如果调用epoll_wait,当有新的

连接,多个线程都会在epoll_wait后被唤醒。(epoll_wait 返回就绪fd的个数)


nginx     

eg:主进程(或者线程)绑定端口号和ip,所有的nginx worker进程调用epoll_wait来处理事件,如果不加任何保护,当一个新的连接

来临时,很多进程会在epoll_wait后被唤醒,然后发现自己accept失败。。。。。。。



那我们来看一看nginx怎么解决惊群

每一个worker进程在ngx_process_events_and_timers里处理事件,

ngx_process_events(cycle,timer,flags)封装了不同事件的处理机制,


void ngx_process_events_and_timers(ngx_ctcle_t *cycle)
{
	/*
	表示是否可以通过对accept加锁解决惊群如果
	nginx worker进程数>1时并且配置文件中打开
	accept_mutex锁的时候
	*/
	if(ngx_use_accept_mutex)
	{
		/*
		判断ngx_accept_disabled是否大于0
		ngx_accept_disabled == 单进程所有的连接数的1/8-剩余空间链接数
		nginx.conf中配置每一个nginx worker进程能够处理的最大连接数,
		当达到最大数的7/8时,nginx_accept_disabled就会为正,表示不会
		处理这个新连接,(类似一个负载均衡)
		通过disabled控制是否去竞争accept_mutex;值越大,让出就会越大
		*/
		if(ngx_accept_disabled > 0)
		{
			ngx_accept_disabled--;
		}
		else
		{
			/*
			获取锁accept,监听句柄(accept事件)放入当前worker的epoll中去
			*/
			if(NGX_ERRNO == ngx_trylock_accept_mutex(cycle))
			{
				return ;
			}

			if(ngx_accept_mutex_held)
			{
				/*
				拿到锁之后,将flag置为NGX_POXT_EVENTS,把accept事件放入
				ngx_posted_accept_events链表当中去。EPOLLIN还有EPOLLOUT
				也放入ngx_posted_events当中去,读写事件消耗的事件较长,会在释放锁之后
				延迟处理。
				*/
				flags |= NGX_POSTED_EVENTS;
			}
			else
			{
				/*
				如果没有拿到锁,就不会去处理监听事件,将timer修改成
				ngx_accept_mutex_delay,将以更短的超时时间返回,使得
				没有获得锁的worker去拿到锁的频率更高。
				*/
				if(timer == NGX_TIMER_INFINITE ||timer >ngx_accept_mutex_delay)
				{
					timer = ngx_accept_mutex_delay
				}
			}
		}
	}
	//
	/*
	然后调用ngx_epoll_process_events函数开始处理
	*/
	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_postet_events)
	{
		if(ngx_threaded)
			ngx_wakeup_worker_thread(cycle);
		else
			ngx_event_process_posted(cycle,&ngx_posted_events);
	}
}



大家可能心里还是有一点混乱,我把上面代码先做一个总结:


1》首先判断flags也就是ngx_use_accept_mutex是否大于一:因为只有ngix worker的进程数大于1并且

配置文件中打开accept_mutex锁的时候,才会考虑要不要加锁。


2》然后再判断ngx_accept_disabled是否大于0:在nginx.conf中配置每一个进程可以连接事件的最大数

当达到最大数的7/8时,ngx_accept_disabled就会为正(表示满负荷),当这个数字越大的时候,让出

的机会就越大,通过disabled来控制是否去竞争accept_mutex,这个数字小于0的时候就去竞争accpet_mutex

(这里有负载均衡的意思)


3》获取锁:ngx_trylock_accept_mutex();然后把监听句柄放到当前worker的epoll当中去,


4》拿到锁之后,我们将flags置为NGX_POSITED_EVENTS,我们把accept事件放入ngx_posted_accept_events

链表中,将EPOLLIN|EPOLLOUT事件放到ngx_posted_events中去(读写事件耗时长,将在锁释放后延迟处理)


5》没有拿到锁的worker,timer修改为ngx_accept_mutex_delay,将以更短的超时时间返回,使得没有获得锁的

worker去拿锁的频率更高


6》然后开始处理,当ngx_posted_accept_events中有数据时,就进行新的连接,然后在释放锁之后在进行处理

读写操作。(这里有一个延迟过程)


从上面的代码注释可以得知:

不论有多少的nginx worker进程,同一个时刻只能有一个worker进程在自己的epoll中加入监听句柄,这个处理accept

的nginx worker进程将flags设置为NGX_POST_EVENTS,这样的话,他在接下来ngx_process_events函数(linux中

是ngx_epoll_process_events函数)中不会立刻处理事件,会延后,先处理完所有的accept之后,释放锁之后,然后再

处理正常的读写事件;


7》我们就开始看看ngx_epoll_process_events是怎么处理accept的:::::::::::::::::

static  ngx_int_t ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)  
{  
    /*
    epoll_wait返回就绪fd的个数。
    */ 
    events = epoll_wait(ep, event_list, (int) nevents, timer);  

    /*
    给事件加锁(accept事件)
    */  
    ngx_mutex_lock(ngx_posted_events_mutex);  
  
    for (i = 0; i < events; i++) 
    {  
        c = event_list[i].data.ptr;  
  
        rev = c->read;  //(读事件)
  
        if ((revents & EPOLLIN) && rev->active) 
	{  
            /*
	    有NGX_POST_EVENTS标志的话,就把accept事件放到ngx_posted_accept_events队列中,
	    把正常的事件放到ngx_posted_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) 
	{  
	    /*
	    同理,有NGX_POST_EVENTS标志的话,写事件延迟处理,
	    放到ngx_posted_events队列中  
	    */
            if (flags & NGX_POST_EVENTS) 
	    {  
                ngx_locked_post_event(wev, &ngx_posted_events);  
  
            } 
	    else 
	    {  
                wev->handler(wev);  
            }  
        }  
    }  
  
    ngx_mutex_unlock(ngx_posted_events_mutex);  
  
    return NGX_OK;  
}  

ngx_use_accept_mutex在什么情况下被打开:

当nginx worker的数量大于1时,也就是多个进程可能同时accept一个连接,这事如果配置文件的accept_mutex开关打开了

就将ngx_use_accept_mutex置为1;


还有我刚才在上面写的类似于一个负载均衡:ngx_accept_disabled是怎么控制的呢???

ngx_accept_disabled = ngx_cycle->connection_n /8-ngx_cycle->free_connect_n;

表示:当已使用的连接数占到总是的7/8以上的时候,ngx_accept_disabled为正,这时

worker将ngx_accept_disabled减1,而且本次不会处理这个新连接。


下面是我写的一个普通的epoll实现的一个代码(没有多进程,没有锁),大家可以看一下

捋捋思路

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <assert.h>
#include <unistd.h>
#include <sys/epool.h>

#define MAX 10

void main()
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);

	assert(sockfd != -1);

	struct sockaddr_in ser,cli;

	memset(&ser,0,sizeof(ser));

	ser.sin_family = AF_INET;

	ser.sin_port = (6500);

	ser.sin_addr.s_addr = inet_addr("127.0.0.1");

	int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));

	assert(res != -1);

	listen(sockfd,5);

	int epfd = epoll_create(10);

	struct epoll_event events[MAX];

	events.event = EPOLLIN|EPOLLET;

	event.data.fd = sockfd;

	epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&events);

	while(1)
	{
		int n = epoll_wait(epfd,events,MAX,-1);

		if(n<=0)
		{
			continue;
		}

		int  i = 0;

		for(;i<n;i++)
		{
		    int fd = events[i].data.fd;

		    if(fd == sockfd)
		    {
			int len = sizeof(cli);
                        int c = accept(sockfd,(struct sockaddr*)&cli,&len);

                        events.event = EPOLLIN|EPOLLET;
		  	events.data.fd = c;

			epoll_ctl(epfd,EPOLL_CTL_ADD,c,&events);

		    }
		    else
		    {
                        while(1)
			{
			     char buff[1024] = {0};

			     int res = recv(fd,buff,1023,0);

			     if(res <= 0)
			}
		    }
	      }


       }
}


哈哈,相信大家有了一定的了解了,只不过有一点,就是他们是通过什么方式抢到锁的,这个我就不太清楚了,
估计是有什么特殊的算法吧,

这里有几个我看过的一些问题和大家分享一下:

1.先处理新用户的连接事件,再释放处理新连接的锁,为什这么设计?
如果刚释放锁,就有新连接,刚获得锁的进程要给等待队列中添加sockfd时,此时原获得锁的进程也要从等待队列中删除sockfd,

TCP的三次握手的连接是非线程安全的。为了避免产生错误,使得将sockfd从等待队列中删除后,再让新的进程抢占锁,处理新连接。


2.拿到锁,将任务放在任务队列中,不是立刻去处理,为什这么设计?


每个进程要处理新连接事件,必须拿到锁,当前进程将新连接事件的sokect添加到任务队列中,立即释放锁,让其他进程尽快获得锁,

处理用户的连接。

你可能有个疑问,如果没有加锁,有新事件连接时,所有的进程都会被唤醒执行accept,有且仅有一个进程会accept返回成功,

其他进程都重新进入睡眠状态。现在有了锁,在发生accept之前,进程们要去抢占锁,也是有且仅有一个进程会抢到锁,

其他进程也是重新进入睡眠状态。

即:不论是否有accept锁,都会有很多进程被唤醒在重新进入睡眠状态,那惊群现象是不能解决了??????

其实,锁不能解决惊群现象,惊群现象是没办法解决的,很多进程被同时唤醒是一个必然的过程。

Nginx中通过检查当前进程的连接数是否>最大连接数*7/8来判断当前进程是否能处理新连接,减少被唤醒的进程数量,

也实现了简单的负载均衡。锁只能保证不让所有的进程去调用accept函数,解决了很多进程调用accept返回错误,

锁解决的是惊群现象的错误,并不是解决了惊群现象!

























;