Nginx 作为业界知名的高性能服务器,被广泛的应用。

Nginx的高性能正是由于其优秀的架构设计,其架构主要包括这几点:

模块化设计、事件驱动架构、请求的多阶段异步处理、管理进程与多工作进程设计、内存池的设计

 

Nginx 官网:http://nginx.org ,nginx-1.17.6.tar.gz (2019-11-19)

Nginx Github:https://github.com/nginx/nginx

Tengine 官网:http://tengine.taobao.org (淘宝)

Tengine Github:https://github.com/alibaba/tengine

 

 

模块化设计

高度模块化的设计是 Nginx 的架构基础。在 Nginx 中,除了少量的核心代码,其他一切皆为模块。

所有模块间是分层次、分类别的,Nginx 官方共有五大类型的模块:核心模块、配置模块、事件模块、HTTP 模块、mail 模块

它们之间的关系如下:

在上图 5 种模块中,配置模块和核心模块是与 Nginx 框架密切相关的。

而事件模块则是 HTTP 模块和 mail 模块的基础。HTTP 模块和 mail 模块的“地位”类似,它们都是更关注于应用层面。

 

事件驱动架构

事件驱动架构,简单的说就是由一些事件发生源来产生事件,由事件收集器来收集、分发事件,然后由事件处理器来处理这些事件(事件处理器需要先在事件收集器里注册自己想处理的事件)。

对于 Nginx 服务器而言,一般由网卡、磁盘产生事件,Nginx 中的事件模块将负责事件的收集、分发操作

而所有的模块都可能是事件消费者,它们首先需要向事件模块注册感兴趣的事件类型,

这样,在有事件产生时,事件模块会把事件分发到相应的模块中进行处理。

对于传统 web 服务器(如 Apache)而言,采用的所谓事件驱动往往局限在 TCP 连接建立、关闭事件上,一个连接建立以后,在其关闭之前的所有操作都不再是事件驱动,这时会退化成按顺序执行每个操作的批处理模式,这样每个请求在连接建立后都将始终占用着系统资源,直到关闭才会释放资源。这种请求占用着服务器资源等待处理的模式会造成服务器资源极大的浪费。

如下图所示,传统 web 服务器往往把一个进程或线程作为时间消费者,当一个请求产生的事件被该进程处理时,直到这个请求处理结束时,进程资源都将被这一请求所占用。

比较典型的例子如 Apache 同步阻塞的多进程模式就是这样的。

传统 web 服务器处理事件的简单模型(矩形代表进程):

Nginx 采用事件驱动架构处理业务的方式与传统的 web 服务器是不同的。它不使用进程或者线程来作为事件消费者,所谓的事件消费者只能是某个模块。只有事件收集、分发器才有资格占用进程资源,它们会在分发某个事件时调用事件消费模块使用当前占用的进程资源。

如下图所示,该图中列出了 5 个不同的事件,在事件收集、分发者进程的一次处理过程中,这 5 个事件按照顺序被收集后,将开始使用当前进程分发事件,从而调用相应的事件消费者来处理事件。当然,这种分发、调用也是有序的。

Nginx 处理事件的简单模型:

由上图可以看出,处理请求事件时,Nginx 的事件消费者只是被事件分发者进程短期调用而已,这种设计使得网络性能、用户感知的请求时延都得到了提升,每个用户的请求所产生的事件会及时响应,整个服务器的网络吞吐量都会由于事件的及时响应而增大。

当然,这也带来一定的要求,即每个事件消费者都不能有阻塞行为,否则将会由于长时间占用事件分发者进程而导致其他事件得不到及时响应,Nginx 的非阻塞特性就是由于它的模块都是满足这个要求的。

 

请求的多阶段异步处理

多阶段异步处理请求与事件驱动架构是密切相关的,也就是说,请求的多阶段异步处理只能基于事件驱动架构实现。

多阶段异步处理就是把一个请求的处理过程按照事件的触发方式划分为多个阶段,每个阶段都可以由事件收集、分发器来触发。

处理获取静态文件的 HTTP 请求时切分的阶段及各阶段的触发事件如下所示:

这个例子中,该请求大致分为 7 个阶段,这些阶段是可以重复发生的,因此,一个下载静态资源请求可能会由于请求数据过大,网速不稳定等因素而被分解为成百上千个上图所列出的阶段。

异步处理和多阶段是相辅相成的,只有把请求分为多个阶段,才有所谓的异步处理。当一个时间被分发到事件消费者中进行处理时,事件消费者处理完这个事件只相当于处理完 1 个请求的阶段。什么时候可以处理下一个阶段呢?这只能等待内核的通知,即当下一次事件出现时,epoll 等事件分发器将会获取到通知,然后去调用事件消费者进行处理。

 

管理进程、多工作进程设计

Nginx 在启动后,会有一个 master 进程和多个 worker 进程。

master 进程主要用来管理worker 进程,包括接收来自外界的信号,向各 worker 进程发送信号,监控 worker 进程的运行状态以及启动 worker 进程。

worker 进程是用来处理来自客户端的请求事件。

多个 worker 进程之间是对等的,它们同等竞争来自客户端的请求,各进程互相独立,一个请求只能在一个 worker 进程中处理。

worker 进程的个数是可以设置的,一般会设置与机器 CPU 核数一致,这里面的原因与事件处理模型有关。

Nginx 的进程模型,可由下图来表示:

在服务器上查看 Nginx 进程:

# ps -ef | grep nginx
root      7976     1  0 18:00 ?        00:00:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
nobody    7977  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7979  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7980  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7981  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7982  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7983  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7984  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7985  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7986  7976  0 18:00 ?        00:00:01 nginx: worker process
nobody    7987  7976  0 18:00 ?        00:00:00 nginx: worker process

 

这种设计带来以下优点:

1) 利用多核系统的并发处理能力

现代操作系统已经支持多核 CPU 架构,这使得多个进程可以分别占用不同的 CPU 核心来工作

Nginx 中所有的 worker 工作进程都是完全平等的。这提高了网络性能、降低了请求的时延。

 

2) 负载均衡

多个 worker 工作进程通过进程间通信来实现负载均衡,即一个请求到来时更容易被分配到负载较轻的 worker 工作进程中处理。

这也在一定程度上提高了网络性能、降低了请求的时延。

 

3) 管理进程会负责监控工作进程的状态,并负责管理其行为

管理进程不会占用多少系统资源,它只是用来启动、停止、监控或使用其他行为来控制工作进程。

首先,这提高了系统的可靠性,当 worker 进程出现问题时,管理进程可以启动新的工作进程来避免系统性能的下降。

其次,管理进程支持 Nginx 服务运行中的程序升级、配置项修改等操作,这种设计使得动态可扩展性、动态定制性较容易实现。

 

 

Nginx 架构模型深入分析

一、Nginx 架构

Nginx由内核和模块组成,从官方文档下的 Modules reference 可以看到一些比较重要的模块,一般分为核心、基础模块、第三方模块

第三方模块,意味着你也可以按照nginx标准去开发符合自己业务的模块插件。

核心主要用于提供Web Server的基本功能,以及Web和Mail反向代理的功能;

还用于启用网络协议,创建必要的运行时环境以及确保不同的模块之间平滑地进行交互。

不过,大多跟协议相关的功能和某应用特有的功能都是由nginx的模块实现的。

这些功能模块大致可以分为:事件模块、阶段性处理器、输出过滤器、变量处理器、协议、upstream和负载均衡几个类别,这些共同组成了nginx的http功能。

事件模块主要用于提供OS独立的(不同操作系统的事件机制有所不同)事件通知机制如kqueue或epoll等。

协议模块则负责实现nginx通过http、tls/ssl、smtp、pop3以及imap与对应的客户端建立会话。

在Nginx内部,进程间的通信是通过模块的pipeline或chain实现的;换句话说,每一个功能或操作都由一个模块来实现。

例如,压缩、通过FastCGI或uwsgi协议与upstream服务器通信,以及与memcached建立会话等。

 

二、Nginx 进程模型

1、多进程模型

Nginx之所以为广大码农喜爱,除了其高性能外,还有其优雅的系统架构。

与Memcached的经典多线程模型相比,Nginx是经典的多进程模型

Nginx启动后以daemon的方式在后台运行,后台进程包含一个master进程和多个worker进程,具体如下图:

 

2、多进程模型的好处

对于每个worker进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销,同时在编程以及问题查找时,也会方便很多。

其次,采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快启动新的worker进程,并且独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在,以上也是Nginx高效的另一个原因了。

# ps -ef | grep nginx
root      7976     1  0 18:00 ?        00:00:00 nginx: master process /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
nobody    7977  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7979  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7980  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7981  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7982  7976  0 18:00 ?        00:00:00 nginx: worker process
nobody    7983  7976  0 18:00 ?        00:00:00 nginx: worker process

 

3、master与worker功能

3.1 master进程主要用来管理worker进程,具体包括如下4个主要功能

1)接收来自外界的信号。

2)向各worker进程发送信号。

3)监控woker进程的运行状态。

4)当woker进程退出后(异常情况下),会自动重新启动新的woker进程。

 

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

1)多个worker进程之间是对等且相互独立的,他们同等竞争来自客户端的请求。

2)一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。

3)worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致。更多的worker数,只会导致进程来竞争cpu资源了,从而带来不必要的上下文切换。

4)nginx为了更好的利用多核特性,具有cpu绑定选项,可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效。

 

 

三、进程控制方式

对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。

 

四、守护线程 daemon

nginx在启动后,在unix系统中会以daemon的方式在后台运行,后台进程包含一个master进程和多个worker进程。

当然nginx也是支持多线程的方式的,只是主流的方式还是多进程的方式,也是nginx的默认方式

 

五、网络事件模块

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.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处理
	//1、获得锁则加入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);
}

 

七、相对于线程,采用进程的优点

进程之间不共享资源,不需要加锁,所以省掉了锁带来的开销。

采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快重新启动新的worker进程。

编程上更加容易。

 

八、多线程的问题

而多线程在多并发情况下,线程的内存占用大,线程上下文切换造成CPU大量的开销。

想想apache的常用工作方式(apache 也有异步非阻塞版本,但因其与自带某些模块冲突,所以不常用),每个请求会独占一个工作线程,当并发数上到几千时,就同时有几千的线程在处理请求了。

这对操作系统来说,是个不小的挑战,线程带来的内存占用非常大,线程的上下文切换带来的cpu开销很大,自然性能就上不去了,而这些开销完全是没有意义的。

 

九、异步非阻塞

9.1. 什么是异步?

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

 

9.2 什么事阻塞非阻塞?

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

 

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

不需要创建线程,每个请求只占用少量的内存

没有上下文切换,事件处理非常轻量

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

 

十、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);
}

 

总结

通过这篇文章,我们对 Nginx 服务器的 整体架构 有了一个整体的认识。

包括其 模块化的设计多进程异步非阻塞 的请求处理方式、事件驱动模型 等。

通过这些理论知识,才能更好地领悟 Nginx 的设计思想,对于学习 Nginx 来说有很大的帮助。

 

架构师学习的知识点:

高可用、高并发、高性能及分布式、JVM性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,,Zookeeper,Tomcat,Docker,Dubbo,Nginx 等多个知识点

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

趁年轻,使劲拼,给未来的自己一个交代!


 

本文转自Nginx 架构模型分析  (掘金)

 

 

参考推荐:

Nginx 设计原理与技术架构

Nginx 百万并发的优化之道

Nginx location 正则匹配规则

Nginx 配置 Location 指令块

Nginx 配置文件详解

Nginx使用ssl模块配置HTTPS

Nginx Redirect重定向所有子域名到www

Nginx rewrite 隐藏 .php 后缀

LNMP(CentOS+Nginx+Mysql+PHP)服务器环境配置

Nginx 限制单个IP的并发连接数/速度防止恶意攻击/蜘蛛爬虫采集

Nginx 支持动态配置的开源Web服务器

Nginx 配置Apache前端服务器

PHP解决网站大数据大流量与高并发

PHP 高并发下请求 Redis 异常处理

Apache、Nginx、Tomcat 区别

Apache 和 Nginx 对比