nginx架构模型分析

1、Nginx架构


Nginx由内核和模块组成,从官方文档http://nginx.org/en/docs/下的Modules reference能够看到一些比较重要的模块,通常分为核心、基础模块以及第三方模块。nginx

第三方模块意味着你也能够按照nginx标准去开发符合本身业务的模块插件。web

核心主要用于提供Web Server的基本功能,以及Web和Mail反向代理的功能;还用于启用网络协议,建立必要的运行时环境以及确保不一样的模块之间平滑地进行交互。sql

不过,大多跟协议相关的功能和某应用特有的功能都是由nginx的模块实现的。这些功能模块大体能够分为:事件模块、阶段性处理器、输出过滤器、变量处理器、协议、upstream和负载均衡几个类别,这些共同组成了nginx的http功能。事件模块主要用于提供OS独立的(不一样操做系统的事件机制有所不一样)事件通知机制如kqueue或epoll等。协议模块则负责实现nginx经过http、tls/ssl、smtp、pop3以及imap与对应的客户端创建会话。在Nginx内部,进程间的通讯是经过模块的pipeline或chain实现的;换句话说,每个功能或操做都由一个模块来实现。例如,压缩、经过FastCGI或uwsgi协议与upstream服务器通讯,以及与memcached创建会话等。apache

2、nginx进程模型


2.1 多进程模型

Nginx之因此为广大码农喜好,除了其高性能外,还有其优雅的系统架构。与Memcached的经典多线程模型相比,Nginx是经典的多进程模型。Nginx启动后以daemon的方式在后台运行,后台进程包含一个master进程和多个worker进程,具体以下图:编程

2.2 多进程模型的好处 对于每一个worker进程来讲,独立的进程,不须要加锁,因此省掉了锁带来的开销,同时在编程以及问题查找时,也会方便不少。其次,采用独立的进程,可让互相之间不会影响,一个进程退出后,其它进程还在工做,服务不会中断,master进程则很快启动新的worker进程,而且独立的进程,可让互相之间不会影响,一个进程退出后,其它进程还在,以上也是Nginx高效的另外一个缘由了。bash

2.3 master与worker功能

2.3.1 master进程主要用来管理worker进程,具体包括以下4个主要功能服务器

  • 接收来自外界的信号。
  • 向各worker进程发送信号。
  • 监控woker进程的运行状态。
  • 当woker进程退出后(异常状况下),会自动从新启动新的woker进程。

2.3.2 woker进程主要用来处理基本的网络事件网络

  • 多个worker进程之间是对等且相互独立的,他们同等竞争来自客户端的请求。
  • 一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。
  • worker进程的个数是能够设置的,通常咱们会设置与机器cpu核数一致。更多的worker数,只会致使进程来竞争cpu资源了,从而带来没必要要的上下文切换。并且,nginx为了更好的利用多核特性,具备cpu绑定选项,咱们能够将某一个进程绑定在某一个核上,这样就不会由于进程的切换带来cache的失效。

3、进程控制方式


对Nginx进程的控制主要是经过master进程来作到的,主要有两种方式:多线程

3.1 手动发送信号

从图1能够看出,master接收信号以管理众woker进程,那么,能够经过kill向master进程发送信号,好比kill -HUP pid用以通知Nginx从容重启。所谓从容重启就是不中断服务:master进程在接收到信号后,会先从新加载配置,而后再启动新进程开始接收新请求,并向全部老进程发送信号告知再也不接收新请求并在处理完全部未处理完的请求后自动退出。架构

3.2 自动发送信号

能够经过带命令行参数启动新进程来发送信号给master进程,好比./nginx -s reload用以启动一个新的Nginx进程,而新进程在解析到reload参数后会向master进程发送信号(新进程会帮咱们把手动发送信号中的动做自动完成)。固然也能够这样./nginx -s stop来中止Nginx。

4、守护线程 daemon


4.1 守护线程

nginx在启动后,在unix系统中会以daemon的方式在后台运行,后台进程包含一个master进程和多个worker进程。固然nginx也是支持多线程的方式的,只是咱们主流的方式仍是多进程的方式,也是nginx的默认方式。

5、网络事件模块


Nginx(多进程)采用异步非阻塞的方式来处理网络事件,相似于Libevent(单进程单线程),具体过程以下图:

master进程先建好须要listen的socket后,而后再fork出多个woker进程,这样每一个work进程均可以去accept这个socket。当一个client链接到来时,全部accept的work进程都会受到通知,但只有一个进程能够accept成功,其它的则会accept失败。Nginx提供了一把共享锁accept_mutex来保证同一时刻只有一个work进程在accept链接,从而解决惊群问题。当一个worker进程accept这个链接后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开链接,这样一个完成的请求就结束了。

对于一个基本的web服务器来讲,事件一般有三种类型,网络事件、信号、定时器。从上面的讲解中知道,网络事件经过异步非阻塞能够很好的解决掉。如何处理信号与定时器?

  • 信号的处理。(待补充)
  • 定时器。nginx里面的定时器事件是放在一颗维护定时器的红黑树里面,每次在进入epoll_wait前,先从该红黑树里面拿到全部定时器事件的最小时间,在计算出epoll_wait的超时时间后进入epoll_wait。(待补充)

6、惊群现象


6.1 什么事惊群现象

惊群简单来讲就是多个进程或者线程在等待同一个事件,当事件发生时,全部线程和进程都会被内核唤醒。唤醒后一般只有一个进程得到了该事件并进行处理,其余进程发现获取事件失败后又继续进入了等待状态,在必定程度上下降了系统性能。具体来讲惊群一般发生在服务器的监听等待调用上,服务器建立监听socket,后fork多个进程,在每一个进程中调用accept或者epoll_wait等待终端的链接。

6.2 nginx的惊群现象

每一个worker进程都是从master进程fork过来。在master进程里面,先创建好须要listen的socket之 后,而后再fork出多个worker进程,这样每一个worker进程均可以去accept这个socket(固然不是同一个socket,只是每一个进程 的这个socket会监控在同一个ip地址与端口,这个在网络协议里面是容许的)。通常来讲,当一个链接进来后,全部在accept在这个socket上 面的进程,都会收到通知,而只有一个进程能够accept这个链接,其它的则accept失败。

6.3 nginx如何处理惊群

内核解决epoll的惊群效应是比较晚的,所以nginx自身解决了该问题(更准确的说是避免了)。其具体思路是:不让多个进程在同一时间监听接受链接的socket,而是让每一个进程轮流监听,这样当有链接过来的时候,就只有一个进程在监听那确定就没有惊群的问题。具体作法是:利用一把进程间锁,每一个进程中都尝试得到这把锁,若是获取成功将监听socket加入wait集合中,并设置超时等待链接到来,没有得到所的进程则将监听socket从wait集合去除。这里只是简单讨论nginx在处理惊群问题基本作法,实际其代码还处理了不少细节问题,例如简单的链接的负载均衡、定时事件处理等等。 核心的代码以下

void ngx_process_events_and_timers(ngx_cycle_t *cycle){
	...
	    //这里面会对监听socket处理
	//一、得到锁则加入wait集合,没有得到则去除
	if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
		return;
	}
	...
	    //设置网络读写事件延迟处理标志,即在释放锁后处理
	if (ngx_accept_mutex_held) {
		flags |= NGX_POST_EVENTS;
	}
	...
	    //这里面epollwait等待网络事件
	//网络链接事件,放入ngx_posted_accept_events队列
	//网络读写事件,放入ngx_posted_events队列
	(void) ngx_process_events(cycle, timer, flags);
	...
	    //先处理网络链接事件,只有获取到锁,这里才会有链接事件
	ngx_event_process_posted(cycle, &ngx_posted_accept_events);
	//释放锁,让其余进程也可以拿到
	if (ngx_accept_mutex_held) {
		ngx_shmtx_unlock(&ngx_accept_mutex);
	}
	//处理网络读写事件
	ngx_event_process_posted(cycle, &ngx_posted_events);
}
复制代码

7、相对于线程,采用进程的优势


  • 进程之间不共享资源,不须要加锁,因此省掉了锁带来的开销。
  • 采用独立的进程,可让互相之间不会影响,一个进程退出后,其它进程还在工做,服务不会中断,master进程则很快从新启动新的worker进程。
  • 编程上更加容易。

8、多线程的问题


而多线程在多并发状况下,线程的内存占用大,线程上下文切换形成CPU大量的开销。想一想apache的经常使用工做方式(apache 也有异步非阻塞版本,但因其与自带某些模块冲突,因此不经常使用),每一个请求会独占一个工做线程,当并发数上到几千时,就同时有几千的线程在处理请求了。这对 操做系统来讲,是个不小的挑战,线程带来的内存占用很是大,线程的上下文切换带来的cpu开销很大,天然性能就上不去了,而这些开销彻底是没有意义的。

9、异步非阻塞


9.1. 什么是异步?

异步的概念和同步相对的,也就是否是事件之间不是同时发生的。

9.2 什么事阻塞非阻塞?

非阻塞的概念是和阻塞对应的,阻塞是事件按顺序执行,每一事件都要等待上一事件的完成,而非阻塞是若是事件没有准备好,这个事件能够直接返回,过一段时间再进行处理询问,这期间能够作其余事情。可是,屡次询问也会带来额外的开销。

9.3 Nginx采用异步非阻塞的好处?

  • 不须要建立线程,每一个请求只占用少许的内存
  • 没有上下文切换,事件处理很是轻量

淘宝tengine团队说测试结果是“24G内存机器上,处理并发请求可达200万”。

10、libevent 概览


支持Libevent运转的就是一个大循环,这个主循环体如今event_base_loop(Event.c/1533)函数里,该函数的执行流程以下:

上图的简单描述就是:

  • 校订系统当前时间。
  • 将当前时间与存放时间的最小堆中的时间依次进行比较,将全部时间小于当前时间的定时器事件从堆中取出来加入到活动事件队列中。
  • 调用I/O封装(好比:Epoll)的事件分发函数dispatch函数,以当前时间与时间堆中的最小值之间的差值(最小堆取最小值复杂度为O(1))做为Epoll/epoll_wait(Epoll.c/dispatch/407)的timeout值,在其中将触发的I/O和信号事件加入到活动事件队列中。
  • 调用函数event_process_active(Event.c/1406)遍历活动事件队列,依次调用注册的回调函数处理相应事件。

附上event_base_loop源码以下:

int event_base_loop(struct event_base *base, int flags)
        {
	const struct eventop *evsel = base->evsel;
	struct timeval tv;
	struct timeval *tv_p;
	int res, done, retval = 0;
	/* Grab the lock.  We will release it inside evsel.dispatch, and again
                * as we invoke user callbacks. */
	EVBASE_ACQUIRE_LOCK(base, th_base_lock);
	if (base->running_loop) {
		event_warnx("%s: reentrant invocation. Only one event_base_loop"
		                               " can run on each event_base at once.", __func__);
		EVBASE_RELEASE_LOCK(base, th_base_lock);
		return -1;
	}
	base->running_loop = 1;
	clear_time_cache(base);
	if (base->sig.ev_signal_added && base->sig.ev_n_signals_added)
	                        evsig_set_base(base);
	done = 0;
	#ifndef _EVENT_DISABLE_THREAD_SUPPORT
	                        base->th_owner_id = EVTHREAD_GET_ID();
	#endif
	                base->event_gotterm = base->event_break = 0;
	while (!done) {
		base->event_continue = 0;
		/* Terminate the loop if we have been asked to */
		if (base->event_gotterm) {
			break;
		}
		if (base->event_break) {
			break;
		}
		timeout_correct(base, &tv);
		tv_p = &tv;
		if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
			timeout_next(base, &tv_p);
		} else {
			/*
                            * if we have active events, we just poll new events
                            * without waiting.
                            */
			evutil_timerclear(&tv);
		}
		/* If we have no events, we just exit */
		if (!event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) {
			event_debug(("%s: no events registered.", __func__));
			retval = 1;
			goto done;
		}
		/* update last old time */
		gettime(base, &base->event_tv);
		clear_time_cache(base);
		res = evsel->dispatch(base, tv_p);
		if (res == -1) {
			event_debug(("%s: dispatch returned unsuccessfully.",
			                                    __func__));
			retval = -1;
			goto done;
		}
		update_time_cache(base);
		timeout_process(base);
		if (N_ACTIVE_CALLBACKS(base)) {
			int n = event_process_active(base);
			if ((flags & EVLOOP_ONCE)
			                               && N_ACTIVE_CALLBACKS(base) == 0
			                               && n != 0)
			                                    done = 1;
		} else if (flags & EVLOOP_NONBLOCK)
		                            done = 1;
	}
	event_debug(("%s: asked to terminate loop.", __func__));
	done:
	            clear_time_cache(base);
	base->running_loop = 0;
	EVBASE_RELEASE_LOCK(base, th_base_lock);
	return (retval);
}
复制代码

写在最后

最后,欢迎作Java的工程师朋友们加入Java高级架构进阶Qqun:963944895

群内有技术大咖指点难题,还提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)

比你优秀的对手在学习,你的仇人在磨刀,你的闺蜜在减肥,隔壁老王在练腰, 咱们必须不断学习,不然咱们将被学习者超越!

趁年轻,使劲拼,给将来的本身一个交代!

相关文章
相关标签/搜索