一、Nginx核心原理
本节为大家介绍Nginx的核心原理,包含Reactor模型
、Nginx的模块化设计、Nginx的请求处理阶段.
(本文源自微博客,且已获得授权)
1.1、Reactor模型
Nginx对高并发IO的处理使用了Reactor事件驱动模型
。Reactor模型的基本组件包含时间收集器、事件发送器、事件处理器3个基本单元,其核心实现四将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上,一旦有I/O时间到来或者准备就绪(文件描述符或Socket可读、写),多路复用器返回并将事先注册的响应I/O事件分发到对应的处理器中。
在Reactor模式中,时间收集器、事件发送器、事件处理器则3个基本单元的职责分别如下:
- 时间收集器:负责收集Worker进程的各种I/O请求
- 事件发送器:负责将I/O时间发送到事件处理器
- 事件处理器:负责各种时间的响应工作
Nginx的Reactor模型的设计大致如下:
时间收集器将各个连接通道的IO事件放入一个待处理时间列,通过事件发送器发送给对应的事件处理器来处理。而时间收集器之所能够同时管理上百万连接通道的事件,是基于操作系统提供的“多路IO复用”
技术,常见的包括select、epoll两种模型。
正是由于Nginx使用了高性能的Reactor模式,因此是目前并发能力很高的Web服务器之一,成为迄今为止使用广泛的工业级Web服务器。当然,Nginx也解决了著名的网络读写是C10K
问题。什么是C10K问题呢?网络服务在处理数以万计的客户端连接时,往往出现效率低下甚至完全瘫痪,这类问题就被成为C10K问题。
1.2、Nginx的两类进程
一般来说,Nginx在启动以后会与daemon
方式在后台运行,其后台进程有两种: 一类称为Master进程
(相当于管理进程);另一类称为Worker进程
(工作进程)。Nginx的进程结构大致如下:
Nginx启动方式有两种:
- 单进程启动:此时系统中仅有一个进程,该进程既充当Master管理进程角色,又充当Worker工作进程角色
- 多进程启动:此时系统有且仅有一个Master管理进程,至少有一个Worker工作进程。
(有点类似于RocketMQ的模式)
一般来说,单进程模式用于调试。在生产环境中,一般会配置成多进程模式,并且Worker工作进程的数量和机器CPU核心数配置不一样多。
了解Worker工作进程之前,首先了解一下Master管理进程的主要工作,主要有以下两点:
- Master管理进程主要负责调度Worker工作进程,比如加载配置、启动工作进程、接收来自外界的信号、向各Worker进程发送信号、监控Worker进程的运行状态等。所以Nginx启动以后,我们能看到至少两个Nginx进程
- Master负责创建监听套接口,交由Worker进程进行连接监听。
接下来介绍Nginx的Worker进程。Worker进程主要用来处理网络事件。当一个Worker进程在接受一条连接通道之后,就开始读取请求、解析请求、处理请求,处理完成产生数据以后,再返回给客户端,最后断开连接通道。
各个Worker进程之间是对等且相互独立的,它们同等竞争来自客户端的请求,一个请求只可能在一个Worker进程中处理。则都是典型的Reactor模型中Worker进程(或者线程)的职能。
如果启动了多个Worker进程,那么每个Worker子进程独自尝试接受已连接的Socket监听通道,accept操作默认会上锁,优先使用操作系统的共享内存原子锁,如果操作系统不支持,就是用文件上锁。
经过配置,Worker进程的接受操作也可以不适用锁,在多个进程同时接受时,当一个连接进来的时候多个工作进程同时被唤起,则会导致惊群问题
。而在上锁的场景下,只会有一个Worker阻塞在accept上,其他的进程会因为不能获取锁而阻塞,所以上锁的场景不存在惊群问题。
1.3、Nginx模块化设计
Nginx服务器被分解为多个模块,模块之间严格遵循“高内聚,低耦合”的原则,每个模块都聚焦于一个功能。高度模块化的设计是Nginx的架构基础。
什么是Nginx模块呢?在Nginx的实现中,一个模块包含一系列命令(cmd)和这些命令相对应的处理函数(cmd→handler)。Nginx的Worker进程在执行过程中会通过配置文件的配置指令定位到对应的功能模块的某个命令(cmd),然后调用命令对应的处理函数来完成相应的处理。
Nginx的Worker进程首先会调用Nginx的Core核心模块。大家知道,在Reactor模型中会维护一个运行循环(Run-Loop),主要包括事件收集、事件分发、事件处理,这个工作在Nginx中由Core核心模块负责。Core模块负责执行网络请求处理的基础操作,比如网络读写、存储读写、内容传输、外出过滤以及将请求发往上游服务器等。
Nginx的Core模块是启动时一定会加载的,其他的模块只有在解析配置时遇到了这个模块的命令才会加载对应的模块。Core模块为其他模块构建了基本的运行时环境,并成为其他各模块的协作基础。
除了Core模块外,Nginx还有Event、Conf、HTTP、Mail等一系列模块,并且可以在编译时加入第三方模块。Nginx的模块结构如图:
这里对Nginx的主要模块说明如下:
- Core核心模块:核心模块是Nginx服务器正常运行必不可少的模块,提供错误日志记录、配置文件解析、Reactor事件驱动机制、进程管理等核心功能。
- 标准HTTP模块:标准HTTP模块提供HTTP协议解析相关的功能,比如端口配置、网页编码设置、HTTP响应头设置等。
- 可选HTTP模块:可选HTTP模块主要用于扩展标准的HTTP功能,让Nginx能处理一些特殊的服务,比如Flash多媒体传输、网络传输压缩、安全协议SSL的支持等。
- 邮件服务模块:邮件服务模块主要用于支持Nginx的邮件服务,包括对POP3协议、IMAP协议和SMTP协议的支持。
- 第三方模块:第三方模块是为了扩展Nginx服务器的功能,定制开发者自定义功能,比如JSON支持、Lua支持等。
Nginx的非核心模块可以在编译时按需加入,这里不再赘述。
总之,Nginx通过模块化设计使得大家可以根据需要对功能模块进行适当的选择和修改,编译成具有特定功能的服务器。
1.4、Nginx配置文件上下文结构
前面介绍到,一个Nginx的功能模块包含一系列的命令(cmd)以及与命令对应的处理函数(cmd→handler)。而Nginx根据配置文件中的配置指令就知道对应到哪个模块的哪个命令,然后调用命令对应的处理函数来处理。
一个Nginx配置文件包含若干配置项,每个配置项由配置指令和指令参数两部分组成,可以简单认为配置项是一个键-值对。图中有3个简单的Nginx配置项:
Nginx配置文件中的配置指令如果包含空格,就需要用单引号或双引号引起来。指令参数如果是由简单的字符串构成的,简单配置项就需要以分号结束;指令参数如果是复杂的多行字符串,配置项就需要用花括号“{}”括起来。
Nginx配置项的具体功能与其所处的作用域(上下文、配置块)是强相关的。Nginx指令的作用域配置块大致有5种,它们之间的层次关系如图:
一个标准的Nginx配置文件的上下文结构如下:
nginx
#main全局配置块,例如工作进程数
events { #events事件处理模式配置块,例如IO读写模式、连接数等}
http #HTTP协议配置块
{#HTTP协议的全局配置块server #server虚拟服务器配置块一{#server全局块location [PATTERN] #location路由规则配置块一{}location [PATTERN] #location路由规则配置块二{}}server #server虚拟服务器配置块二{}
#其他HTTP协议的全局配置块
}
mail #mail服务配置块
{#email相关协议,如SMTP/IMAP/POP3的处理配置
}
对以上作用域(上下文、配置块)说明如下:
1.4.1、main全局配置块
配置影响Nginx全局的指令,一般有运行Nginx服务器的用户组、Nginx进程PID存放路径、日志存放路径、配置文件引入、允许生成的Worker进程数等。
1.4.2、events事件处理模式配置块
配置Nginx服务器的IO多路复用模型、客户端的最大连接数限制等。Nginx支持多种IO多路复用模型,可以使用use指令在配置文件中设置IO读写模型。
1.4.3、HTTP协议配置块
可以配置与HTTP协议处理相关的参数,比如keepalive长连接参数、GZIP压缩参数、日志输出参数、mime-type参数、连接超时参数等。
1.4.4、server虚拟服务器配置块
配置虚拟主机的相关参数,如主机名称、端口等。一个HTTP协议配置块中可以有多个server虚拟服务器配置块。
1.4.5、location路由规则块
配置客户端请求的路由匹配规则以及请求过程中的处理流程。一个server虚拟服务器配置块中一般会有多个location路由规则块。
1.4.6、mail服务配置块
Nginx为email相关协议(如SMTP/IMAP/POP3)提供反向代理时,mail服务配置块负责配置一些相关的配置项。
提示:以上介绍的Nginx配置块主要针对的是Nginx基本应用程序配置文件,包括基本配置文件在内,Nginx的常用配置文件大致有下面这些:
- nginx.conf:应用程序基本配置文件
- mime.types:与MIME类型关联的扩展配置文件
- fastcgi.conf:与FastCGI相关的配置文件
- proxy.conf:与Proxy相关的配置文件
- sites.conf:单独配置Nginx提供的虚拟机主机
1.5、Nginx的请求处理流程
Nginx中HTTP请求的处理流程可以分为4步:
- 读取解析请求行
- 读取解析请求头
- 多阶段处理,也就是执行handler处理器列表
- 将结果返回给客户端
Nginx中HTTP请求的处理流程如图:
多阶段处理是Nginx的HTTP处理流程中非常重要的一步。Nginx把请求处理划分成了11个阶段,在完成第一步读取请求行和第二步读取请求头之后,Nginx将整个请求封装到一个请求结构体ngx_http_request_t实例中(相当于Java中的一个请求对象),然后进入第三步多阶段处理,也就是执行handler处理器列表。列表中的每个handler处理器都会对请求对象进行处理,例如重写URI、权限控制、路径查找、生成内容以及记录日志等。
Nginx将HTTP请求处理流程分成了11个阶段,每个阶段都涉及一些handler处理器。HTTP请求到来时,这些组装在一个列表的handler处理器会按组装的先后次序执行。这一点和Netty的处理流水线pipeline在原理上是类同的。
在Nginx进行多阶段处理时,handler处理器的执行次序除了和配置文件中对应指令的配置顺序相关外,还和指令所处的阶段先后次序相关。
Nginx请求处理的11个阶段以及阶段与阶段之间的执行次序如图:
对HTTP请求进行多阶段处理是Nginx模块化非常关键和重要的功能,第三方模块的处理器都在不同的处理阶段注册,例如:
- 用Memcache进行页面缓存的第三方模块
- 用Redis集群进行页面缓存的第三方模块
- 执行Lua脚本的第三方模块
1.6、HTTP请求处理的11个阶段
Nginx请求处理的11个阶段介绍如下
1.6.1、post-read阶段
在完成第一步读取请求行和第二步读取请求头之后就进入多处理阶段,首当其冲的就是post-read阶段。注册在post-read阶段的处理器不多,标准模块的ngx_realip处理器就注册在这个阶段。ngx_realip处理器模块的用途是改写请求的来源地址。
为何要改写请求的来源地址呢?
当Nginx处理的请求经过了某个正向代理服务器(Nginx、CDN)的转发后,请求中的IP地址($remote_addr)可能就不是客户端的真实IP了,变成了下游代理服务器的IP。如何获取用户请求的真实IP地址呢?解决办法之一:在下游的正向代理服务器把请求的原始来源地址编码成某个特殊的HTTP请求头,在Nginx中把这个请求头中编码的地址恢复出来,然后传给Nginx自己后头的上游服务器。ngx_realip模块正是用来处理这个需求的。
下面有一个简单的例子,假定前头的正向代理服务器能将客户端IP编码成某个特殊的HTTP请求头(如X-My-IP),Nginx就可以通过ngx_realip模块的real_ip_header指令将X-My-IP请求头的IP取出,作为请求中的IP地址($remote_addr)。
server {listen 8080;set_real_ip_from 192.168.0.100;real_ip_header X-My-IP;location /test {echo "from: $remote_addr ";}
}
这里的配置是让Nginx把来自正向代理服务器192.168.0.100的所有请求的IP来源地址都改写为请求头X-My-IP所指定的值,放在$remote_addr内置标准变量中。
1.6.2、server-rewrite阶段
server-rewrite阶段,简单地翻译就是server块中的请求地址重写阶段。在进行请求URI与location路由规则匹配之前可以修改请求的URI地址。
大部分直接配置在server配置块中的配置项都运行在server-rewrite阶段。
server {listen 8080;set $a hello; #server-rewrite阶段运行location /test {set $b "$a, world";echo $b;}set $b hello; #server-rewrite阶段运行
}
其中,两个变量赋值的配置项set a h e l l o 和 s e t a hello和set ahello和setb hello直接写在server配置块中,因此它们就运行在server-rewrite阶段。
1.6.3、find-config
紧接在server-rewrite阶段后面的是find-config阶段,也叫配置查找阶段,主要功能是根据请求URL地址去匹配location路由表达式。
find-config阶段由Nginx HTTP Core(ngx_http_core_module)模块全部负责,完成当前请求URL与location配置块之间的配对工作。这个阶段不支持Nginx模块注册处理程序。
在find-config阶段之前,客户端请求并没有与任何location配置块相关联。因此,对于运行在此之前的post-read和server-rewrite阶段来说,只有server配置块以及更外层作用域中的配置项才会起作用,location配置块中的配置项不起作用。
1.6.4、rewrite
由于Nginx已经在find-config阶段完成了当前请求与location的匹配,因此从rewrite阶段开始,location配置块中的指令就可以产生作用。
rewrite阶段也叫请求地址重写阶段,注册在rewrite阶段的指令首先是ngx_rewrite模块的指令,比如break、if、return、rewrite、set等。其次,第三方ngx_lua模块中的set_by_lua指令和rewrite_by_lua指令也能在此阶段注册。
1.6.5、post-rewrite
请求地址URI重写提交(Post)阶段,防止递归修改URI造成死循环(一个请求执行10次就会被Nginx认定为死循环),该阶段只能由NginxHTTP Core(ngx_http_core_module)模块实现。
1.6.6、preaccess
访问权限检查准备阶段,控制访问频率的ngx_limit_req模块和限制并发度的ngx_limit_zone模块的相关指令就注册在此阶段。
1.6.7、access
在访问权限检查阶段,配置指令多是执行访问控制类型的任务,比如检查用户的访问权限、检查用户的来源IP地址是否合法等。在此阶段能注册的指令有:HTTP标准模块ngx_http_access_module的指令、第三方ngx_auth_request模块的指令、第三方ngx_lua模块的access_by_lua指令等。
比如,deny和allow指令属于ngx_http_access_module模块,它的
使用示例如下:
server {#拒绝全部location = /denyall {deny all;}#允许来源IP属于192.168.0.0/24网段或127.0.0.1的请求#其他来源IP全部拒绝location = /allowsome {allow 192.168.0.0/24;allow 127.0.0.1;deny all;echo "you are ok";}
}
如果同一个location块配置了多个allow/deny配置项,access阶段的配置项之间是按配置的先后顺序匹配的,匹配成功一个便跳出。上面的例子中,如果客户端源IP是127.0.0.1,则匹配到“allow127.0.0.1;”配置项后就不再匹配后面的“deny all;”,也就是说该请求不会被拒绝。如果这些配置项的指令来自不同的模块,则每个模块会执行一个访问控制类型的指令。
特别提醒:echo指令用于返回内容,在location上下文中,该指令注册在content生产阶段。由于echo指令不是注册在access阶段,因此在access阶段不执行该指令的配置项。
1.6.8、post-access
访问权限检查提交阶段。如果请求不被允许访问Nginx服务器,该阶段负责就向用户返回错误响应。在access阶段可能存在多个访问控制模块的指令注册,post-access阶段的satisfy配置指令可以用于控制它们彼此之间的协作方式。下面有一个例子:
#satisfy指令进行协调
location = /satisfy-demo {satisfy any;access_by_lua "ngx.exit(ngx.OK)";deny all;echo "hello";
}
在上面的例子中,deny指令属于HTTP标准模块的ngx_http_access_module访问控制模块,而access_by_lua指令属于第三方ngx_lua模块,两个模块都有自己的计算结果,需要经过最终的结果统一。
不同访问控制模块的计算结果统一工作,这里由satisfy指令负责,有两种统一的方式:
- 逻辑或操作:具体的配置项为“satisfy any;”,表示访问控制模块A、B、C或更多,只要其中任意一个通过验证就算通过。
- 逻辑与操作:具体的配置项为“satisfy all;”,表示访问控制模块A、B、C或更多,全部模块都通过验证才能最终通过。
1.6.9、try-files
如果HTTP请求访问静态文件资源,那么try-files配置项可以使这个请求按顺序访问多个静态文件资源,直到某个静态文件资源符合选取条件。这个阶段只有一个标准配置指令try-files,并不支持Nginx模块注册处理程序。
try-files指令接收两个以上任意数量的参数,每个参数都指定了一个URI,Nginx会在try-files阶段依次把前N-1个参数映射为文件系统上的对象(文件或者目录),然后检查这些对象是否存在。若Nginx发现某个文件系统对象存在,则查找成功,进而在try-files阶段把当前请求的URI改写为该对象所对应的参数URI(但不会包含末尾的斜杠字符,也不会发生“内部跳转”)。如果前N-1个参数所对应的文件系统对象都不存在,try-files阶段就会立即发起“内部跳转”,跳转到最
后一个参数(第N个参数)所指定的URI。
下面是一个简单的实例:
root /var/www/; #root指令把“查找文件的根目录”配置为 /var/www/
location = /try_files-demo {try_files /foo /bar /last;
}
#对应到前面try_files的最后一个URI
location /last {echo "uri: $uri ";
}
这里try-files会在文件系统查找前两个参数对应的文件/var/www/foo和/var/www/bar所对应的文件是否存在。如果不存在,此时Nginx就会在try-files阶段发起到最后一个参数所指定的URI(/last)的内部跳转,如图:
1.6.10、content
大部分HTTP模块会介入内容产生阶段,是所有请求处理阶段中重要的阶段。Nginx的echo指令、第三方ngx_lua模块的content_by_lua指令都注册在此阶段。
这里要注意的是,每一个location只能有一个“内容处理程序”,因此,当在location中同时使用多个模块的content阶段指令时,只有一个模块能成功注册成为“内容处理器”。例如echo和content_by_lua同时注册,最终只会有一个生效,但具体是哪一个生效,结果是不稳定的。
1.6.11、log
日志模块处理阶段记录日志。
最后,总结一下:
- Nginx将一个HTTP请求分为11个处理阶段,这样做让每个HTTP模块可以只专注于完成一个独立、简单的功能。而一个请求的完整处理过程由多个HTTP模块共同合作完成,可以极大地提高多个模块合作的协同性、可测试性和可扩展性。
- Nginx请求处理的11个阶段中,有些阶段是必备的,有些阶段是可选的,各个阶段可以允许多个模块的指令同时注册。但是,find-config、post-rewrite、post-access、try-files四个阶段是不允许其他模块的处理指令注册的,它们仅注册了HTTP框架自身实现的几个固定的方法。
- 同一个阶段内的指令,Nginx会按照各个指令的上下文顺序执行对应的handler处理器方法。