Nginx是一个高性能的HTTP和反向代理服务器,以其稳定性、丰富的功能集、简单的配置以及低资源消耗而闻名。自2004年发布以来,Nginx迅速成为Web服务器领域的领先者,特别是在处理高并发请求方面表现出色。它不仅可以用作Web服务器,还可以作为负载均衡器、缓存服务器以及HTTP/2和HTTPS的服务器。
多进程模型
Nginx的多进程模型主要由以下两部分组成:
- 主进程(Master Process):负责启动子进程、加载配置文件、平滑升级以及维护子进程的运行状态。
- 工作进程(Worker Processes):处理实际的请求,包括HTTP请求、HTTPS请求、处理静态文件、反向代理等。
Master 进程
当 Nginx 启动时,首先运行的是 master 进程。它负责读取并解析配置文件,初始化必要的资源(如打开监听的端口、设置日志等),然后根据配置创建 worker 进程。
Master 进程的主要职责是管理和监控所有 worker 进程。它会接收来自外部的操作信号,并将这些信号传递给 worker 进程以执行相应的操作,比如重新加载配置或平滑重启服务。
在 Unix/Linux 系统中,master 进程通常以 root 用户身份运行,以便能够绑定到特权端口(如80或443)。一旦完成了初始设置,master 就会降低自己的权限,并以非特权用户的身份启动 worker 进程,从而提升了系统的安全性。
如果某个 worker 进程意外终止,master 进程会检测到这一情况并自动重启新的 worker 来替代它,确保服务持续可用。
Worker 进程
每个 worker 都是一个独立的工作进程,它们负责实际的HTTP请求处理。worker 进程的数量可以根据服务器的 CPU 核心数进行配置,一般推荐每个 CPU 核心对应一个 worker,以充分利用硬件资源。
每个 worker 内部实现了一个事件驱动的循环,利用操作系统提供的 I/O 多路复用机制(如 Linux 上的 epoll
或 FreeBSD 上的 kqueue
)来高效地处理大量的并发连接。
Worker 进程之间相互独立,彼此不共享内存或其他资源,这减少了因单个工作进程崩溃而导致整个服务失效的风险。同时,这也意味着不同 worker 之间的状态是完全隔离的,一个 worker 不会直接影响其他 worker 的工作。
多进程模型的工作原理
启动阶段
在 Unix/Linux 系统中启动 Nginx 时,首先会创建一个 master 进程。这个 master 进程最初以 root 用户身份运行,以便它可以绑定到特权端口(如 HTTP 协议的80端口或 HTTPS 协议的443端口)。完成必要的初始化后,master 进程会降低权限,并根据配置文件中的设置,以非特权用户的身份启动多个 worker 进程。这些 worker 进程负责实际的客户端请求处理。Master 进程的主要任务包括读取并解析配置文件、初始化全局资源(如监听端口和日志记录),以及创建指定数量的 worker 进程。
连接分配
当有新的客户端连接请求到来时,所有 worker 进程都尝试接受新连接。为了防止多个 worker 同时接受同一个连接而造成冲突(即“惊群效应”),Nginx 实现了 accept_mutex
机制。这意味着在任意时刻,只有获得互斥锁的一个 worker 可以成功接受新连接。具体来说,在 master 进程建立好监听套接字之后,它会 fork 出多个 worker 子进程,每个子进程继承了这个监听套接字。当新连接到来时,所有 worker 进程都会检测到监听套接字变为可读状态,但只有抢到了 accept_mutex
的那个 worker 才会注册读事件并在读事件中调用 accept()
来接受连接。
请求处理
一旦某个 worker 成功接受了连接,它就会异步地处理来自客户端的请求。Nginx 的 worker 进程采用了非阻塞 I/O 模型,允许它们同时处理多个请求,不会因为等待一个请求完成而被阻塞。每个请求会在一个独立的上下文中被处理,这个上下文贯穿于请求生命周期的所有阶段——从接收请求到生成响应,再到最终发送回客户端。
请求处理过程可以概括为以下几个步骤:
- 接受连接:worker 抢占
accept_mutex
并调用accept()
接受新连接。 - 读取请求:从客户端读取完整的 HTTP 请求数据。
- 解析请求:分析请求头和体,确定所需的资源或操作。
- 处理请求:根据请求的内容执行相应的处理逻辑,这可能涉及静态文件服务、动态内容生成、代理请求等。
- 返回响应:将处理结果封装成 HTTP 响应,发送给客户端。
- 断开连接:请求完成后关闭连接,释放相关资源。
整个请求周期内,所有的活动都是由同一个 worker 进程完成的。
信号处理
Master 进程还扮演着信号处理器的角色,它监听来自外部的操作信号,并相应地通知所有的 worker 进程采取行动。这使得管理员可以方便地对 Nginx 进行管理,例如重新加载配置文件、平滑重启服务或停止服务等。
下面是一些常用的信号及其作用:
HUP (SIGHUP): 平滑重启或热更新配置。当发送
HUP
信号给 master 进程时,master 会重新加载配置文件,然后启动一批新的 worker 进程来应用新的配置。旧的 worker 继续处理现有的请求直到完成,从而确保服务不中断。具体过程如下:
- Master 接收到
HUP
信号后,重新加载配置文件。 - 启动新的 worker 进程,这些新进程将使用更新后的配置。
- 向所有老的 worker 发送信号,通知它们不再接受新的请求。
- 老的 worker 在处理完当前正在进行的请求后,安全退出。
- Master 接收到
QUIT (SIGQUIT): 优雅地停止服务。发送
QUIT
信号后,worker 进程会停止接受新的请求,并在处理完现有请求后退出。这种方式可以确保所有正在处理的请求都能顺利完成,而不影响用户体验。TERM/INT (SIGTERM/SIGINT): 强制终止服务。发送
TERM
或INT
信号会导致立即终止服务,适用于紧急情况下的快速关闭。
除了直接发送信号外,自 Nginx 0.8 版本以来,还引入了一系列命令行参数来简化管理。例如,./nginx -s reload
用于重新加载配置文件,而 ./nginx -s stop
则用于停止 Nginx 的运行。这些命令实际上是启动一个新的 Nginx 进程,该进程解析命令行参数后向 master 发送相应的信号,进而触发上述的操作流程。
事件模型
Nginx 采用事件驱动架构(Event-Driven Architecture),基于异步非阻塞 I/O 模型。这种设计使得 Nginx 能够高效处理大量并发连接,而不会因为阻塞操作导致性能下降。
事件循环与多路复用技术
在每个 worker 进程中,Nginx 实现了一个事件循环(event loop)。这个循环不断地检查哪些文件描述符(如套接字)已经准备好进行读写操作,并调用相应的回调函数来处理这些准备好的事件。为了实现高效的 I/O 操作,Nginx 使用了操作系统提供的多种 I/O 多路复用技术:
- select: 最基础的多路复用方法,适用于所有平台。它可以监视多个文件描述符的状态变化。
- poll: 类似于
select
,但性能更好,因为它的内部结构更紧凑,减少了不必要的内存复制。 - epoll (Linux): 提供了更好的性能和可扩展性,特别是当有大量文件描述符时。它将文件描述符的管理交给内核,只有当实际发生事件时才通知应用程序。
- kqueue (FreeBSD 和其他 BSD 系统): 提供类似
epoll
的功能,支持边缘触发和水平触发两种模式。 - /dev/poll (Solaris 和一些 Unix 变体): 类似于
epoll
的接口,用于提高 I/O 处理效率。 - event ports (Solaris): 一种高级的事件通知机制,专为 Solaris 设计。
根据运行的操作系统不同,Nginx 会选择最适合的多路复用技术,默认情况下会自动选择最优选项。
非阻塞 I/O 模型
Nginx 的事件模型依赖于非阻塞 I/O 模型,这意味着所有的 I/O 操作都不会导致进程挂起或等待。相反,如果一个 I/O 操作无法立即完成(例如,没有数据可读或不能立即写入),它会迅速返回一个错误代码(通常是 EAGAIN
或 EWOULDBLOCK
),表示当前操作未就绪。随后,Nginx 会继续处理其他事件,直到该 I/O 操作再次变得可用为止。
这种方式避免了传统阻塞 I/O 所带来的上下文切换开销,显著提升了系统的吞吐量和响应速度。每个 worker 进程可以在不阻塞的情况下处理多个并发连接,极大地提高了资源利用率和服务能力。
异步事件处理
除了非阻塞特性外,Nginx 还实现了真正的异步事件处理。当某个事件发生时(如新的连接到来、数据到达等),Nginx 不会立刻去处理这个事件,而是将其添加到一个待处理队列中。然后,在事件循环的下一次迭代中,Nginx 会从队列中取出这些事件并执行相应的回调函数来处理它们。这样的设计不仅简化了编程逻辑,而且保证了即使面对大量的并发事件,系统也能保持良好的性能表现。
此外,Nginx 内部还维护了一个定时器系统,用于管理超时事件。每当进入事件循环之前,Nginx 会计算出下一个最近的定时器到期时间,并设置为本次循环的最大等待时间。如果在指定时间内没有任何事件发生,则认为定时器到期,此时会触发相应的超时处理逻辑。
基本功能
- 静态文件服务:Nginx可以高效地服务静态文件,如HTML、CSS、JavaScript和图片。通过在
location
块中指定文件路径,Nginx可以直接将请求的文件发送给客户端。nginxlocation / { root /usr/share/nginx/html; index index.html index.htm; }
- 反向代理:Nginx可以用作反向代理服务器,将客户端的请求转发到其他服务器。这在提高网站性能和安全性方面非常有用。nginx
location / { proxy_pass http://backend_servers; }
- 负载均衡:Nginx可以实现负载均衡,将客户端的请求分配到多个后端服务器。这有助于提高网站的可用性和可靠性。nginx
upstream backend_servers { server backend1.cengxuyuan.cn; server backend2.cengxuyuan.cn; } server { location / { proxy_pass http://backend_servers; } }
HTTP请求处理流程
Nginx的HTTP请求处理流程是一个高度模块化和阶段化的过程。
初始处理
读取请求行
请求行是 HTTP 请求的第一个行,它包含了三个关键元素:
- 请求方法:指明了请求的类型,如 GET、POST、PUT、DELETE 等。
- 请求 URI:指明了请求的目标资源,它可以是一个绝对路径或者是与服务器相关的相对路径。
- HTTP 版本:指明了请求使用的 HTTP 协议版本,如 HTTP/1.1 或 HTTP/2。
当请求到达时,Nginx 会首先解析请求行,以确定请求的方法、目标资源和协议版本。
读取请求头
请求头是 HTTP 请求的一部分,它紧跟在请求行之后,包含了关于请求的附加信息。这些信息可以包括客户端信息、认证信息、接受的内容类型、语言偏好等等。
请求头提供了有关请求者(客户端)和请求本身的重要细节,这些细节对于服务器理解请求的上下文至关重要。例如,通过查看 Accept-Language
头,Nginx 可以决定以何种语言返回响应。
在初始处理阶段结束后,Nginx 会创建一个 ngx_http_request_t
结构体来保存请求信息,并将进入下一阶段——多阶段处理。这个阶段包括了 post-read
、server-rewrite
、find-config
、rewrite
、post-rewrite
、preaccess
、access
、post-access
、try-files
、content
以及 log
阶段。每个阶段都有特定的功能和处理逻辑,共同构成了 Nginx 完整的请求处理流程。
多阶段处理
post-read阶段
ngx_realip
模块是 post-read
阶段的一个典型示例。它用于处理客户端的真实 IP 地址。当请求通过代理服务器(如 CDN)时,客户端的真实 IP 可能会被代理服务器修改。ngx_realip
模块可以帮助恢复客户端的真实 IP 地址。
server-rewrite阶段
server-rewrite
阶段用于在匹配location
之前修改请求的URI。这个阶段可以在不改变请求的实际内容的情况下,改变请求的内部表示。
在这个阶段可以使用诸如rewrite
、set
、if
等指令来修改请求URI,或者在请求被进一步处理之前设置变量。
这个阶段主要用于执行那些需要在请求被路由到具体的 location 之前的操作,例如修改请求的 URI,从而影响后续的 location 匹配过程。
配置示例:
server {
listen 80;
server_name cengxuyuan.cn;
set $my_var "initial value"; # 设置变量,可用于后续阶段
rewrite ^/oldpage$ /newpage break; # 重写请求URI,从/oldpage到/newpage
...
}
在这个配置中,所有对/oldpage
的请求都会被重写为对/newpage
的请求。break
标志告诉Nginx停止进一步的重写处理。
find-config阶段
find-config
阶段的主要任务是根据请求的 URI 匹配相应的 location
配置块。一旦请求被绑定到一个 location
块,后续的处理将根据该 location
块中的配置来执行。
Nginx 会尝试将请求的 URI 与 server
块中定义的 location
规则进行匹配。匹配规则包括精确匹配、前缀匹配、正则表达式匹配等。
这个阶段完全由Nginx的HTTP Core模块(ngx_http_core_module
)处理,不允许可插拔模块注册自己的处理程序。
配置示例:
server {
listen 80;
server_name cengxuyuan.cn;
location / {
# 默认location,匹配所有请求
}
location /images/ {
# 匹配以/images/开头的请求
}
}
在这个例子中,如果请求URI是/images/logo.png
,则find-config
阶段会将其匹配到/images/
的location
块。
rewrite阶段
rewrite
阶段允许使用模块化的指令来修改请求的URI,这是Nginx处理请求中非常灵活的一部分。它通常用于URL重写、访问控制、条件判断等。
在这个阶段,可以执行ngx_rewrite
模块提供的指令,如set
、if
、rewrite
等。这些指令可以基于请求信息、配置变量或其他条件来修改请求的URI。
除了ngx_rewrite
模块外,其他模块也可以在这个阶段注册自己的处理程序,例如ngx_lua
模块的rewrite_by_lua
指令。
配置示例:
server {
listen 80;
server_name cengxuyuan.cn;
location /old-site/ {
rewrite ^/old-site/(.*)$ /new-site/$1 permanent; # 永久重定向到新URL
}
location / {
if ($request_method = POST) {
return 405; # 如果是POST请求,则返回405 Method Not Allowed
}
set $my_var "some value"; # 设置变量
}
}
在这个例子中,对/old-site/
的请求会被永久重定向到/new-site/
,同时保持URI的其余部分不变。另外,如果请求方法是POST,则返回405错误。
post-rewrite阶段
post-rewrite
阶段紧随 rewrite
阶段之后,主要用于防止请求 URI 重写过程中可能出现的死循环问题。
在 rewrite
阶段中,Nginx 可能会对请求的 URI 进行多次重写。如果没有适当的控制机制,可能会导致无限循环的情况。post-rewrite
阶段就是为了检测和避免这种死循环。
Nginx 通过限制重写次数来防止死循环。默认情况下,Nginx 允许最多重写 10 次。如果超过这个次数,Nginx 将认为发生了死循环,并终止重写过程。
Nginx 会维护一个重写计数器,每次重写 URI 时都会增加计数器的值。如果计数器超过了最大重写次数(默认为 10),Nginx 将停止重写并继续后续处理。
这个阶段由HTTP Core模块(ngx_http_core_module
)处理,不允许可插拔模块注册自己的处理程序。
配置示例:
server {
listen 80;
server_name cengxuyuan.cn;
location / {
rewrite ^/loop/(.*) /loop/$1 break; # 可能导致死循环的重写
}
}
在这个例子中,如果URI包含/loop/
并且后面跟着任何字符,它会被重写为相同的模式。如果没有post-rewrite
阶段,这可能会形成一个无限循环。
preaccess阶段
preaccess
阶段是访问权限检查前的准备阶段。在这个阶段,Nginx执行一些任务,这些任务需要在检查访问权限之前完成,例如限制请求的频率和并发度,以减少 access
阶段的工作负载,并提高整体性能。
可以使用如ngx_limit_req
模块来限制请求的频率,防止服务器被过度请求,从而保护服务器资源。
ngx_limit_conn
模块可以用来限制每个客户端IP的并发连接数,以防止单个用户占用过多资源。
配置示例:
http {
limit_req_zone $binary_remote_addr zone=req_zone:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_zone:10m;
server {
listen 80;
server_name cengxuyuan.cn;
location / {
limit_req zone=req_zone burst=20; # 限制请求频率
limit_conn conn_zone 5; # 限制并发连接数
}
}
}
在这个配置中,limit_req_zone
定义了一个请求频率限制区域,limit_conn_zone
定义了一个并发连接限制区域。在location
块中,limit_req
和limit_conn
指令分别用于实施这些限制。
access阶段
access
阶段是Nginx处理HTTP请求中的一个重要阶段,用于执行访问控制。在这个阶段,Nginx会检查请求是否符合服务器上的访问控制规则,并决定是否允许请求继续。
常用的访问控制指令包括 deny
和 allow
,这些指令用于根据客户端的 IP 地址或其他条件来允许或拒绝请求。
除了标准模块 ngx_http_access_module
,还可以使用第三方模块如 ngx_auth_request
和 ngx_lua
模块来进行更复杂的访问控制。
配置示例:
server {
listen 80;
server_name cengxuyuan.cn;
# 拒绝所有请求
location = /denyall {
deny all;
}
# 允许特定 IP 地址的请求
location = /allowsome {
allow 192.168.0.0/24;
allow 127.0.0.1;
deny all;
echo "You are authorized!";
}
}
在这个例子中,location = /denyall
明确拒绝所有请求,而 location = /allowsome
则允许来自特定 IP 地址范围(192.168.0.0/24)或特定 IP 地址(127.0.0.1)的请求,其余所有请求均被拒绝。
post-access阶段
post-access
阶段用于处理访问控制的结果。如果请求在access
阶段被拒绝,Nginx会在这个阶段决定如何响应。
如果请求在access
阶段被拒绝,Nginx会在post-access
阶段决定如何向客户端发送错误响应。这可能是一个简单的HTTP错误响应,如403 Forbidden或404 Not Found。
如果请求在 access
阶段有多个模块参与访问控制,Nginx会使用satisfy
指令来决定如何响应。satisfy
指令可以设置为“all”或“any”,以确定所有模块的指令都必须通过,还是只要有一个模块的指令通过即可。
配置示例:
server {
listen 80;
server_name cengxuyuan.cn;
location / {
satisfy all;
allow 192.168.0.0/24;
deny all;
}
}
在这个配置中,如果请求的源IP地址不在192.168.0.0/24
网段,请求会被拒绝,并且satisfy all
指令意味着所有模块的指令都必须通过才能允许请求。
try-files阶段
try-files
阶段用于尝试不同的文件路径,以找到存在的文件并返回。这个阶段通常用于处理静态文件请求,它允许Nginx尝试多个文件路径,直到找到一个存在的文件为止。
Nginx会按顺序尝试try-files
指令中列出的文件路径。每个路径都可以是一个文件或目录,Nginx会检查它们是否存在。
如果找到一个存在的文件,Nginx会立即将请求重定向到该文件,而不需要生成响应内容。
如果在所有尝试的路径中都没有找到存在的文件,请求将进入后续的处理阶段。
配置示例:
location /static/ {
try_files $uri $uri/ @fallback;
}
location @fallback {
# 处理静态文件不存在的情况
return 404;
}
在这个配置中,Nginx会尝试以$uri
和$uri/
的形式查找静态文件。如果找到文件,它将直接返回该文件;如果没有找到,请求将重定向到@fallback
位置,在那里可以处理静态文件不存在的情况。
content阶段
content
阶段是请求处理中的一个重要阶段,用于生成响应内容。在这个阶段,Nginx执行所有类型的响应内容生成,包括从文件系统中读取文件、执行脚本、生成动态内容等。
Nginx会执行注册在content
阶段的处理器,如ngx_echo
模块的echo
指令,来生成响应内容。
除了标准模块外,第三方模块也可以在content
阶段注册自己的处理器,例如使用ngx_lua
模块的content_by_lua
指令。
配置示例:
location / {
content_by_lua_block {
ngx.say("Hello, world!")
}
}
在这个配置中,当请求到达/
时,Nginx会执行一个Lua块来生成响应内容。
log阶段
log
阶段是 Nginx 请求处理流程的最后一个阶段,用于记录访问日志。
Nginx会使用配置的日志格式和日志文件来记录请求的信息。这通常包括时间戳、服务器名称、请求行、响应状态码、处理时间等。
记录完日志后,Nginx认为请求处理已经完成,并将请求从事件循环中移除。
配置示例:
http {
log_format combined '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log combined;
}
在这个配置中,Nginx使用combined
日志格式来记录访问日志,并指定日志文件的位置。