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

Linux2.6版本之前还存在对于socket的accept的惊群现象。之后的版本已经解决掉了这个问题。

惊群是指多个进程/线程在等待同一资源时,每当资源可用,所有的进程/线程都来竞争资源的现象。

Nginx采用的是多进程的模式。假设Linux系统是2.6版本以前,当有一个客户端要连到Nginx服务器上,Nginx的N个进程都会去监听socket的accept的,如果全部的N个进程都对这个客户端的socket连接进行了监听,就会造成资源的竞争甚至数据的错乱我们要保证的是,一个链接在Nginx的一个进程上处理,包括accept和read/write事件。

Nginx解决惊群和进程负载均衡处理的要点

1. Nginx的N个进程会争抢文件锁,当只有拿到文件锁的进程,才能处理accept的事件

2. 没有拿到文件锁的进程,只能处理当前连接对象的read事件

3. 当单个进程总的connection连接数达到总数的7/8的时候,就不会再接收新的accpet事件。

4. 如果拿到锁的进程能很快处理完accpet,而没拿到锁的一直在等待(等待时延:ngx_accept_mutex_delay),容易造成进程忙的很忙,空的很空

具体的实现

ngx_process_events_and_timers 进程事件分发器

此方法为进程实现的核心函数。主要作用:事件分发;惊群处理;简单的负载均衡。

负载均衡:

1.  当事件配置初始化的时候,会设置一个全局变量:ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;

2.  当ngx_accept_disabled为正数的时候,connection达到连接总数的7/8的时候,就不再处理新的连接accept事件,只处理当前连接的read事件

惊群处理:

1. 通过ngx_trylock_accept_mutex争抢文件锁,拿到文件锁的,才可以处理accept事件。

2. ngx_accept_mutex_held是拿到锁的一个标志,当拿到锁了,flags会被设置成NGX_POST_EVENTS,这个标志会在事件处理函数ngx_process_events中将所有事件(accept和read)放入对应的ngx_posted_accept_events和ngx_posted_events队列中进行延后处理

3. 当没有拿到锁,调用事件处理函数ngx_process_events的时候,可以明确都是read的事件,所以可以直接调用事件ev->handler方法回调处理。

4. 拿到锁的进程,接下来会优先处理ngx_posted_accept_events队列上的accept事件,处理函数:ngx_event_process_posted

5. 处理完accept事件后,就将文件锁释放

6. 接下来处理ngx_posted_events队列上的read事件,处理函数:ngx_event_process_posted


/**
 * 进程事件分发器
 */
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_WIN32)

		/* handle signals from master in case of network inactivity */

		if (timer == NGX_TIMER_INFINITE || timer > 500) {
			timer = 500;
		}

#endif
	}

	/**
	 * ngx_use_accept_mutex变量代表是否使用accept互斥体
	 * 默认是使用,可以通过accept_mutex off;指令关闭;
	 * accept mutex 的作用就是避免惊群,同时实现负载均衡
	 */
	if (ngx_use_accept_mutex) {

		/**
		 * 	ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
		 * 	当connection达到连接总数的7/8的时候,就不再处理新的连接accept事件,只处理当前连接的read事件
		 * 	这个是比较简单的一种负载均衡方法
		 */
		if (ngx_accept_disabled > 0) {
			ngx_accept_disabled--;

		} else {
			/* 获取锁失败 */
			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 {

				/**
				 * 1. 获取锁失败,意味着既不能让当前worker进程频繁的试图抢锁,也不能让它经过太长事件再去抢锁
				 * 2. 开启了timer_resolution时间精度,需要让ngx_process_change方法在没有新事件的时候至少等待ngx_accept_mutex_delay毫秒之后再去试图抢锁
				 * 3. 没有开启时间精度时,如果最近一个定时器事件的超时时间距离现在超过了ngx_accept_mutex_delay毫秒,也要把timer设置为ngx_accept_mutex_delay毫秒
				 * 4. 不能让ngx_process_change方法在没有新事件的时候等待的时间超过ngx_accept_mutex_delay,这会影响整个负载均衡机制
				 * 5. 如果拿到锁的进程能很快处理完accpet,而没拿到锁的一直在等待,容易造成进程忙的很忙,空的很空
				 */
				if (timer == NGX_TIMER_INFINITE
						|| timer > ngx_accept_mutex_delay) {
					timer = ngx_accept_mutex_delay;
				}
			}
		}
	}

	delta = ngx_current_msec;

	/**
	 * 事件调度函数
	 * 1. 当拿到锁,flags=NGX_POST_EVENTS的时候,不会直接处理事件,
	 * 将accept事件放到ngx_posted_accept_events,read事件放到ngx_posted_events队列
	 * 2. 当没有拿到锁,则处理的全部是read事件,直接进行回调函数处理
	 * 参数:timer-epoll_wait超时时间  (ngx_accept_mutex_delay-延迟拿锁事件   NGX_TIMER_INFINITE-正常的epollwait等待事件)
	 */
	(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);
	/**
	 * 1. ngx_posted_accept_events是一个事件队列,暂存epoll从监听套接口wait到的accept事件
	 * 2. 这个方法是循环处理accpet事件列队上的accpet事件
	 */
	ngx_event_process_posted(cycle, &ngx_posted_accept_events);

	/**
	 * 如果拿到锁,处理完accept事件后,则释放锁
	 */
	if (ngx_accept_mutex_held) {
		ngx_shmtx_unlock(&ngx_accept_mutex);
	}

	if (delta) {
		ngx_event_expire_timers();
	}

	/**
	 *1. 普通事件都会存放在ngx_posted_events队列上
	 *2. 这个方法是循环处理read事件列队上的read事件
	 */
	ngx_event_process_posted(cycle, &ngx_posted_events);
}

ngx_trylock_accept_mutex 获取accept锁

1. ngx_accept_mutex_held是拿到锁的唯一标识的全局变量。

2. 当拿到锁,则调用ngx_enable_accept_events,将新的connection加入event事件上

3. 如果没有拿到锁,则调用ngx_disable_accept_events。

/**
 * 获取accept锁
 */
ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle) {
	/**
	 * 拿到锁
	 */
	if (ngx_shmtx_trylock(&ngx_accept_mutex)) {

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

		/* 多次进来,判断是否已经拿到锁 */
		if (ngx_accept_mutex_held && ngx_accept_events == 0) {
			return NGX_OK;
		}

		/* 调用ngx_enable_accept_events,开启监听accpet事件*/
		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) {
		/* 没有拿到锁,调用ngx_disable_accept_events,将accpet事件删除 */
		if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
			return NGX_ERROR;
		}

		ngx_accept_mutex_held = 0;
	}

	return NGX_OK;
}

ngx_enable_accept_events 和 ngx_disable_accept_events

/**
 * 开启accept事件的监听
 * 并将accept事件加入到event上
 */
static ngx_int_t ngx_enable_accept_events(ngx_cycle_t *cycle) {
	ngx_uint_t i;
	ngx_listening_t *ls;
	ngx_connection_t *c;

	ls = cycle->listening.elts;
	for (i = 0; i < cycle->listening.nelts; i++) {

		c = ls[i].connection;

		if (c == NULL || c->read->active) {
			continue;
		}

		if (ngx_add_event(c->read, NGX_READ_EVENT, 0) == NGX_ERROR) {
			return NGX_ERROR;
		}
	}

	return NGX_OK;
}

/**
 * 关闭accept事件的监听
 * 并将accept事件从event上删除
 */
static ngx_int_t ngx_disable_accept_events(ngx_cycle_t *cycle, ngx_uint_t all) {
	ngx_uint_t i;
	ngx_listening_t *ls;
	ngx_connection_t *c;

	ls = cycle->listening.elts;
	for (i = 0; i < cycle->listening.nelts; i++) {

		c = ls[i].connection;

		/* 如果c->read->active,则表示是活跃的连接,已经被使用中 */
		if (c == NULL || !c->read->active) {
			continue;
		}

#if (NGX_HAVE_REUSEPORT)

		/*
		 * do not disable accept on worker‘s own sockets
		 * when disabling accept events due to accept mutex
		 */

		if (ls[i].reuseport && !all) {
			continue;
		}

#endif

		/* 删除事件 */
		if (ngx_del_event(c->read, NGX_READ_EVENT,
				NGX_DISABLE_EVENT) == NGX_ERROR) {
			return NGX_ERROR;
		}
	}

	return NGX_OK;
}

ngx_event_process_posted 事件队列处理

对ngx_posted_accept_events或ngx_posted_events队列上的accept/read事件进行回调处理。

/**
 * 处理事件队列
 *
 */
void ngx_event_process_posted(ngx_cycle_t *cycle, ngx_queue_t *posted) {
	ngx_queue_t *q;
	ngx_event_t *ev;

	while (!ngx_queue_empty(posted)) {

		q = ngx_queue_head(posted);
		ev = ngx_queue_data(q, ngx_event_t, queue);

		ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
				"posted event %p", ev);

		ngx_delete_posted_event(ev);

		/* 事件回调函数 */
		ev->handler(ev);
	}
}

ngx_process_events 事件的核心处理函数

这个方法,我们主要看epoll模型下的ngx_epoll_process_events方法(ngx_epoll_module.c)

1. 如果抢到了锁,则会将accpet/read事件放到队列上延后处理。

2. 没有抢到锁的进程都是处理当前连接的read事件,所以直接进行处理。

        /* 读取事件 EPOLLIN */
        if ((revents & EPOLLIN) && rev->active) {

#if (NGX_HAVE_EPOLLRDHUP)
            if (revents & EPOLLRDHUP) {
                rev->pending_eof = 1;
            }

            rev->available = 1;
#endif

            rev->ready = 1;

            /* 如果事件抢到锁,则放入事件队列 */
            if (flags & NGX_POST_EVENTS) {
                queue = rev->accept ? &ngx_posted_accept_events
                                    : &ngx_posted_events;

                ngx_post_event(rev, queue);

            } else {
            	/* 没有抢到锁,则直接处理read事件*/
                rev->handler(rev);
            }
        }

下一章节开始,我们会进入Nginx的事件模块。

时间: 2024-11-02 23:33:43

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

Nginx源码分析 - 主流程篇 - 多进程实现

默认情况下,Nginx都是多进程的运行模式.Nginx和Memcached不一样,是多进程的模式.采用多进程模式最大的好处: 1. 每个进程的资源独立 2. 不需要添加各种繁琐的锁了 Nginx多进程实现的流程图 Nginx多进程具体实现 1. ngx_master_process_cycle 进入多进程模式 ngx_master_process_cycle方法主要做了两个工作: 1. 主进程进行信号的监听和处理 2. 开启子进程 /** * Nginx的多进程运行模式 */ void ngx_

Nginx源码分析 - 主流程篇 - 全局变量cycle初始化

Nginx的大部分初始化工作主要围绕一个类型为ngx_cycle_t类型的全局变量(cycle)展开.本文主要讲解cycle的数据结构以及初始化过程中干了什么事情. cycle的初始化过程在/src/core/cycle.c文件中. 数据结构 ngx_cycle_t的数据结构 /** * Nginx全局变量cycle */ struct ngx_cycle_s { void ****conf_ctx; /* 配置文件 上下文的数组,每个模块的配置信息*/ ngx_pool_t *pool; /*

nginx源码分析--模块分类

ngx-modules Nginx 主要的模块大致可以分为四类: handler – 协同完成客户端请求的处理.产生响应数据.比如模块, ngx_http_rewrite_module, ngx_http_log_module, ngx_http_static_module. filter – 对 handler 产生的响应数据做各种过滤处理.比如模块, ngx_http_not_modified_filter_module, ngx_http_header_filter_module. ups

nginx源码分析--GDB调试

利用gdb[i]调试nginx[ii]和利用gdb调试其它程序没有两样,不过nginx可以是daemon程序,也可以以多进程运行,因此利用gdb调试和平常会有些许不一样.当然,我们可以选择将nginx设置为非daemon模式并以单进程运行,而这需做如下设置即可: daemon off; master_process off; 这是第一种情况: 这种设置下的nginx在gdb下调试很普通,过程可以[iii]是这样: 执行命令: [email protected]:/usr/local/nginx/

Nginx源码分析 - Nginx启动以及IOCP模型

Nginx 源码分析 - Nginx启动以及IOCP模型 版本及平台信息 本文档针对Nginx1.11.7版本,分析Windows下的相关代码,虽然服务器可能用linux更多,但是windows平台下的代码也基本相似 ,另外windows的IOCP完成端口,异步IO模型非常优秀,很值得一看. Nginx启动 曾经有朋友问我,面对一个大项目的源代码,应该从何读起呢?我给他举了一个例子,我们学校大一大二是在紫金港校区,到了 大三搬到玉泉校区,但是大一的时候也会有时候有事情要去玉泉办.偶尔会去玉泉,但

nginx源码分析之网络初始化

nginx作为一个高性能的HTTP服务器,网络的处理是其核心,了解网络的初始化有助于加深对nginx网络处理的了解,本文主要通过nginx的源代码来分析其网络初始化. 从配置文件中读取初始化信息 与网络有关的配置命令主要有两个:listen和sever_name.首先先了解这两个命令的用法. listen listen命令设置nginx监听地址,nginx从这里接受请求.对于IP协议,这个地址就是address和port:对于UNIX域套接字协议,这个地址就是path. 一条listen指令只能

[nginx] nginx源码分析--健康检查模块锁分析

健康检查模块 见前文:[nginx] nginx源码分析--健康检查模块 其中有一张框架图, 接下来的内容,将会利用到这个图中的内容. [classic_tong @ https:////www.cnblogs.com/hugetong/p/12274125.html ]  描述 我们知道nginx是多进程的,每个进程都保存了相同的配置.但是实际上, 并不需要每一个进程对每一个后端服务器进行. 于是健康检查模块在这里需要一个进程间同步机制,用来协商哪一个进程对 哪一个后端服务器进行检查. 接下来

nginx源码分析--进程间通信机制 &amp; 同步机制

Nginx源码分析-进程间通信机制 从nginx的进程模型可以知道,master进程和worker进程需要通信,nginx中通信的方式有套接字.共享内存.信号.对于master进程,从外部接受信号,master进程主要就是监控.接受外部信号,将有必要的信号传递给worker进程,master进程大部分时间都是阻塞在sigsuspend()函数调用上.Worker进程屏蔽了所有的外部信号,那么Master进程就通过套接字和worker进程通信,worker进程修改全局变量,使得worker进程接受

nginx源码分析--nginx模块解析

nginx的模块非常之多,可以认为所有代码都是以模块的形式组织,这包括核心模块和功能模块,针对不同的应用场合,并非所有的功能模块都要被用到,附录A给出的是默认configure(即简单的http服务器应用)下被连接的模块,这里虽说是模块连接,但nginx不会像apache或lighttpd那样在编译时生成so动态库而在程序执行时再进行动态加载,nginx模块源文件会在生成nginx时就直接被编译到其二进制执行文件中,所以如果要选用不同的功能模块,必须对nginx做重新配置和编译.对于功能模块的选