LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

Nginx 深度原理解析

admin
2026年11月10日 21:12 本文热度 32

本系列回答一个问题:Nginx 为什么能用极少的资源扛住海量流量,以及在生产环境中如何用好它、查清它的故障。
版本基准:1.30.5 stable(生产推荐),1.31.x mainline 的新特性会单独标注。资料来源为 nginx.org 官方文档、CHANGES 和官方博客,截至 2026 年 9 月。

阅读路径

本系列采用「总—分」结构,共 9 篇,可以按顺序阅读,也可以按需跳读:

  • • **先读总览(00):**建立全局认知,包括定位、价值、竞品对比、总体架构图、关键概念和一次请求的完整流程。
  • • **再读原理(01~06):**逐个展开架构图中的关键组件,从进程、事件,到请求处理、代理、内存与缓存,再到协议栈。
  • • **最后读实践(07~08):**把原理落到配置、调优和线上排障。

篇章
主题
适合谁先读
00 总览
定位、价值、竞品对比、总体架构
所有人
01 进程模型
Master / Worker、reload、热升级、Control API
运维、SRE(Site Reliability Engineering,站点可靠性工程)
02 事件驱动
epoll、事件循环、定时器、连接分配
希望理解高性能原理的开发者
03 请求处理
模块、11 个阶段、location 匹配、过滤器链
写配置的开发者和运维
04 反向代理与负载均衡
upstream
、负载均衡算法、重试、keepalive
网关和中间件的负责人
05 内存、缓冲与缓存
内存池、缓冲链、sendfile、proxy_cache
性能优化相关的同学
06 协议栈
TLS、HTTP/2、HTTP/3(QUIC)
负责接入层和安全的同学
07 使用实践
配置组织、场景模板、调优清单、常见陷阱
日常使用 Nginx 的所有人
08 线上故障诊断
定界方法、错误速查、六个典型案例
值班和排障人员

1.29~1.31 版本中值得关注的变化

版本
变化
影响
详见
1.29.4
proxy 支持使用 HTTP/2 连接后端
可以在一条后端连接上多路复用
04 篇
1.29.6
开源版支持 sticky 会话保持
不再需要第三方模块
04 篇
1.29.7
upstream
 默认 keepalive 32,proxy 默认使用 HTTP/1.1
升级后要检查两端的 keepalive 超时是否匹配
04、08 篇
1.31.0
开源版支持 least_time,新增 tunnel 模块
更多的负载均衡和代理选择
04 篇
1.31.5
Control API、谓词 location、JSON 模块
可以通过 HTTP 管理 reload,按请求内容路由
01、03 篇

00 总览:Nginx 的定位、价值与总体架构

一句话结论:Nginx 用「一个 Master 进程管理 + 多个单线程 Worker 进程事件驱动」的架构,以极低的内存和 CPU(Central Processing Unit,中央处理器)开销同时处理数十万连接;它是当今互联网流量入口的事实标准,但在动态配置、主动健康检查和可观测性方面需要借助外部能力补齐。
版本基准:本系列以 1.30.5 stable(稳定版) 为生产基准,1.31.x mainline(主线版) 的新特性会单独标注。Nginx 版本号中次版本号为偶数的是 stable、奇数的是 mainline。

一、定位:Nginx 是什么

Nginx(读作 engine x)由 Igor Sysoev 于 2004 年发布,初衷是解决 C10K 问题(Concurrent 10,000 connections,单机同时处理一万个连接)。它采用 2-clause BSD(Berkeley Software Distribution)许可证开源,2019 年起由 F5 公司维护。

今天的 Nginx 是一个多协议流量处理引擎,同一个二进制可以承担以下角色:

角色
典型场景
关键模块
Web 服务器
托管前端静态资源、下载站
ngx_http_static_module
、ngx_http_gzip_module
反向代理
为 Java / Go / Python 应用做统一入口
ngx_http_proxy_module
、ngx_http_grpc_module
负载均衡器
将流量分摊到多台后端
ngx_http_upstream_module
内容缓存
缓存后端响应,降低源站压力
proxy_cache
 系列指令
四层代理
代理 MySQL、Redis、DNS(Domain Name System,域名系统)等 TCP / UDP 流量
ngx_stream_
* 模块
安全网关
TLS 卸载、限流、鉴权、防盗链
ngx_http_ssl_module
、limit_req、auth_request

其中 HTTP 是 HyperText Transfer Protocol(超文本传输协议),TCP 是 Transmission Control Protocol(传输控制协议),UDP 是 User Datagram Protocol(用户数据报协议),TLS 是 Transport Layer Security(传输层安全协议);HTTPS(HTTP Secure)即基于 TLS 加密的 HTTP。

二、价值:为什么是 Nginx

Nginx 的核心价值可以归结为三点:快、稳、可扩展。

  1. 1. **快:**高并发、低资源。每个 Worker 进程是一个单线程的事件循环,使用 epoll / kqueue 等 I/O(Input/Output,输入/输出)多路复用机制,一个线程就能同时管理数万个连接,不需要「一个连接一个线程」。官方数据是:一万个空闲的 HTTP 长连接只占约 2.5 MB 内存。
  2. 2. **稳:**故障隔离、变更不中断。
  • • Master 只负责管理,不处理请求。Worker 崩溃后,Master 会立即拉起新的 Worker。
  • • 修改配置后执行 nginx -s reload,新旧 Worker 会平滑交接,已有连接不会中断。
  • • 升级二进制时可以「热升级」,服务不停止。
  1. 3. **可扩展:**模块化 + 声明式配置。所有功能都以模块的形式实现,包括核心的 HTTP 处理。常见需求只要写配置就能完成;复杂需求可以用 njs(NGINX JavaScript)脚本,或者自己开发 C 模块。

三、竞品对比:Nginx 的优势与短板

产品
架构模型
最擅长
动态配置
主要短板
Nginx
多进程 + 单线程事件循环
静态资源、反向代理、缓存,场景通用
弱:改配置要 reload
开源版可观测性弱,没有主动健康检查
Apache httpd
可选的多进程 / 多线程 MPM(Multi-Processing Module,多处理模块)
.htaccess 目录级配置、传统 PHP
弱
高并发时内存占用高
HAProxy
单进程多线程事件循环
四层 / 七层负载均衡,健康检查丰富
中:有运行时 API(Application Programming Interface,应用程序编程接口)
不适合做 Web 服务器,缓存能力弱
Envoy
多线程事件循环(C++)
服务网格、xDS(x Discovery Service,发现服务协议族)全动态配置
强
配置复杂,资源开销较大
Caddy
Go 协程
自动 HTTPS、配置简单
强:有 Admin API
极致性能和生态不如 Nginx

Nginx 的优势:性能和资源效率处于第一梯队;功能覆盖面最广,从静态资源到四层代理一个产品就能搞定;生态最成熟,OpenResty、Kubernetes Ingress、各大 CDN(Content Delivery Network,内容分发网络)都以它为基础;配置和排障经验资料极其丰富。

Nginx 的短板:

  • • **动态性不足:**增删后端、修改路由都要 reload。1.31.5 引入的 Control API 让 reload 可以通过 HTTP 调用触发,并同步返回结果,但本质仍是 reload。
  • • **主动健康检查需付费:**开源版只有被动检查(请求失败后才摘除节点),主动探测(health_check)属于商业版。
  • • **可观测性弱:**开源版的 stub_status 只有 7 个全局指标,细粒度监控需要借助日志分析或第三方模块。
  • • Worker 内不能有阻塞:任何阻塞调用都会卡住整个 Worker 上的所有连接,这是事件驱动模型固有的约束。

✅ **选型建议:**做通用的 Web 入口、静态资源服务、反向代理和缓存,Nginx 是默认首选;对全动态配置要求高的服务网格场景,优先考虑 Envoy;纯粹做四层 / 七层负载均衡、需要丰富健康检查时,可以考虑 HAProxy。

四、总体架构

Nginx 总体架构图:Master 管理多个单线程 Worker
架构图从外到内分为四层:客户端和上游服务、Nginx 进程、进程间共享资源、操作系统内核。各组件的职责如下:
组件
职责定位
关键点
Master 进程
管理者:解析配置、绑定监听端口、创建和监控 Worker
通常以 root 运行,不处理任何请求;reload 和热升级都由它编排
Worker 进程
执行者:处理所有客户端连接和请求
数量一般等于 CPU 核数;彼此独立、互不通信;每个都是单线程事件循环
Cache Loader / Manager
缓存管家:加载和淘汰磁盘缓存
只有配置了 proxy_cache_path 才会启动
共享内存(Zone)
跨 Worker 共享状态
限流计数、SSL(Secure Sockets Layer,安全套接层,TLS 的前身)会话、缓存索引等必须放在这里,才能在多个 Worker 之间保持一致
线程池
卸载阻塞的磁盘 I/O
用 aio threads 开启,防止大文件读取卡住事件循环
模块体系
实现所有具体功能
core、event、http、stream、mail 五大类,按需编译或动态加载

组件之间如何协同:

  • • Master 先创建监听 Socket,再 fork 出 Worker,Worker 继承这些 Socket,共同接收连接。
  • • Master 通过信号和进程间通道(channel)向 Worker 下发 reload、退出等指令。
  • • Worker 之间不直接通信,需要共享的状态都写在共享内存里,通过原子操作或互斥锁协调。
  • • Worker 内部,请求依次经过「接入解析 → 11 个处理阶段 → 内容处理器 → 过滤器链 → 写回」这条流水线。需要访问后端时,由 upstream 模块负责选择节点和复用连接。

五、关键概念

理解后续篇章,需要先建立以下概念:

概念
含义
详见
事件驱动 / 非阻塞 I/O
Worker 从不等待 I/O:数据未就绪就先登记事件,转去处理别的连接,就绪后再回来继续
02 篇
连接与请求
连接(ngx_connection_t)对应一个 TCP 连接;请求(ngx_http_request_t)对应一次 HTTP 交互;一个连接上可以有多个请求(keepalive、HTTP/2 多路复用)
02、03 篇
配置上下文
main、events、http、server、location、upstream 层层嵌套,内层继承外层配置
03、07 篇
处理阶段(Phase)
HTTP 请求处理被切成 11 个固定阶段,模块在各阶段注册回调
03 篇
Handler 与 Filter
Handler 负责生成响应内容(每个 location 只能有一个);Filter 负责加工响应(可以串联多个)
03 篇
Upstream
描述一组后端服务器,以及选择后端的负载均衡算法
04 篇
变量
$host
、$request_uri 等,按需求值(惰性计算),是配置表达能力的核心
03 篇
内存池与缓冲链
按请求分配、整体释放的内存管理方式;用链表串联数据块,避免拷贝
05 篇

六、总体流程:一次 HTTPS 请求的完整生命周期

启动阶段(只发生一次):Master 解析 nginx.conf → 初始化各模块、创建共享内存 → 绑定并监听端口 → fork 出 N 个 Worker → Worker 各自初始化事件循环,开始等待连接。

请求阶段(每个请求都会经过):

  1. 1. **建立连接:**内核完成 TCP 三次握手,把连接放进 accept 队列,通过 epoll 唤醒某个 Worker。Worker 调用 accept(),从连接池取出一个 ngx_connection_t。
  2. 2. TLS 握手:根据 SNI(Server Name Indication,服务器名称指示)选择证书,完成握手;如果命中会话缓存,就走简化握手。
  3. 3. **解析请求:**读取请求行和请求头,创建 ngx_http_request_t,按 Host 选中 server 块。
  4. 4. **阶段处理:**依次执行 11 个阶段:匹配 location、rewrite 重写、limit_req 限流、allow / auth 鉴权等。
  5. 5. **生成内容:**location 中配置了 proxy_pass,就由 proxy 模块接管:选择后端节点、复用 keepalive 连接、转发请求。
  6. 6. **加工响应:**后端响应依次流经 header filter 和 body filter,完成 gzip 压缩、添加响应头、chunked 编码等处理。
  7. 7. **写回客户端:**通过 writev / sendfile 发送数据,然后在 log 阶段写访问日志。
  8. 8. **连接复用:**连接进入 keepalive 状态等待下一个请求;超时后关闭,并把连接对象归还连接池。

💡 **关键理解:**以上每一步只要遇到「数据还没到」或「缓冲区写满了」,Worker 都会立即返回事件循环去处理其他连接,等事件就绪后再从断点继续。这就是 Nginx 单线程也能支撑海量并发的根本原因。

七、系列导读

篇章
回答的核心问题
对应架构图中的部分
01 进程模型
Master / Worker 如何分工?reload 和热升级为什么不断流?
Master、Worker、Cache 进程
02 事件驱动
单线程如何管理数万连接?epoll 事件循环如何运转?
Worker 顶部的事件循环、内核
03 请求处理
11 个阶段各做什么?location 如何匹配?模块如何协作?
流水线步骤 ①~④
04 反向代理与负载均衡
请求如何转发到后端?负载均衡和失败重试的原理是什么?
upstream
 模块、上游服务
05 内存、缓冲与缓存
内存池、缓冲链、零拷贝、proxy_cache 如何工作?
Worker 内部资源、共享内存、磁盘
06 协议栈
TLS、HTTP/2、HTTP/3(基于 QUIC,即 Quick UDP Internet Connections 协议)在 Nginx 中如何实现?
流水线步骤 ①
07 使用实践
如何组织配置?有哪些关键调优参数?
全局
08 线上故障诊断
502、504、499 等问题如何快速定位?
全局

01 进程模型:Master / Worker、平滑重载与热升级

核心结论:Nginx 把「管理」和「干活」拆给两类进程:Master 负责配置、监听端口和 Worker 的生命周期;Worker 负责处理所有请求。Worker 之间完全独立,因此不需要锁,还能天然利用多核;Master 始终在线,因此 reload、热升级和 Worker 崩溃都不会导致服务中断。

一、为什么选择「多进程 + 单线程」

常见的服务器并发模型有三种,Nginx 选择了第三种:

模型
代表
优点
代价
每连接一个进程
Apache prefork
隔离性好,实现简单
进程重、上下文切换多,难以支撑万级并发
每连接一个线程
Apache worker、传统 Java BIO(Blocking I/O,阻塞式 I/O)
比进程轻
线程栈占内存;共享数据要加锁;一个线程出错可能拖垮整个进程
少量进程 + 事件驱动
Nginx
进程数固定(≈ CPU 核数),没有锁竞争,上下文切换极少
代码必须全程非阻塞,开发复杂度高

这个选择带来三个直接收益:

  • • **无锁:**每个 Worker 只处理自己接收的连接,绝大部分数据结构都是进程私有的,只有共享内存需要同步。
  • • **多核扩展:**Worker 数量等于 CPU 核数,还可以用 worker_cpu_affinity 把 Worker 绑定到固定的核上,减少缓存失效。
  • • **故障隔离:**某个 Worker 崩溃,只会影响它自己正在处理的连接,Master 会立即补上一个新的 Worker。

二、进程角色一览

在一台运行中的 Nginx 服务器上执行 ps,通常能看到以下进程:

root 1001 1 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
nginx 1002 1001 nginx: worker process
nginx 1003 1001 nginx: worker process
nginx 1004 1001 nginx: cache manager process
nginx 1005 1001 nginx: cache loader process

进程
数量
运行身份
职责
Master
1
root
读取并校验配置;绑定 80 / 443 等特权端口;fork 并监控子进程;响应信号
Worker
worker_processes
 个
user 指令指定的普通用户
处理客户端连接、代理请求、读写缓存
Cache Loader
1(仅配置缓存时)
普通用户
启动后分批扫描缓存目录,把缓存元数据加载进共享内存,完成后退出
Cache Manager
1(仅配置缓存时)
普通用户
周期性检查缓存大小和过期时间,淘汰超出 max_size 或长期未访问(inactive)的缓存文件

💡 **安全设计:**只有 Master 以 root 身份运行,并且它不解析任何请求数据。处理外部输入的 Worker 以普通用户运行,即使 Worker 被攻破,攻击者也拿不到 root 权限。

三、启动流程:从 nginx 命令到开始服务

Nginx 启动时会创建一个核心结构 ngx_cycle_t,它代表「一代配置」,包含配置树、监听 Socket 列表、共享内存列表、打开的文件等。之后的每次 reload,都会生成新的一代 cycle。

  1. 1. 解析配置:ngx_init_cycle() 读取 nginx.conf,逐条执行指令对应的回调函数,生成各模块的配置结构体。
  2. 2. **创建共享内存:**为 limit_req_zone、proxy_cache_path、ssl_session_cache 等声明的 zone 分配共享内存。
  3. 3. 打开监听 Socket:对所有 listen 地址执行 socket()、bind()、listen()。
  4. 4. 进入 Master 循环:ngx_master_process_cycle() 为每个 Worker 创建一对 socketpair 作为通信通道(channel),然后 fork。
  5. 5. Worker 初始化:降权为普通用户、设置 CPU 亲和性、调用各模块的 init_process 回调,然后进入事件循环 ngx_process_events_and_timers(),开始接收连接。

由于监听 Socket 是在 fork 之前创建的,所有 Worker 继承的是同一组文件描述符,它们会共同竞争这些 Socket 上的新连接(具体如何分配见 02 篇)。

四、Master 如何控制 Worker

Master 启动后,大部分时间阻塞在 sigsuspend() 上等待信号,收到信号后再执行对应的动作:

信号 / 命令
含义
Master 的动作
HUP
 / nginx -s reload
重新加载配置
执行平滑重载(见第五节)
QUIT
 / nginx -s quit
优雅退出
通知 Worker 处理完当前请求后退出,然后自己退出
TERM
、INT / nginx -s stop
快速退出
通知 Worker 立即退出
USR1
 / nginx -s reopen
重新打开日志文件
重开日志并通知 Worker 重开,用于日志切割
USR2
升级二进制
启动新版本的 Master(见第六节)
WINCH
优雅关闭所有 Worker
只关闭 Worker,保留 Master,用于热升级
CHLD
(系统自动发送)
子进程退出
如果 Worker 是异常退出,就立即重新 fork 一个

Master 向 Worker 下发指令有两条路径:一是通过 channel 发送 NGX_CMD_QUIT、NGX_CMD_TERMINATE、NGX_CMD_REOPEN 等命令;二是直接给 Worker 发信号。Worker 收到后只设置一个标志位,等回到事件循环的安全点再执行,避免在处理请求的中途被打断。

Control API:用 HTTP 管理 Master(1.31.5+,mainline)

信号机制有一个老问题:nginx -s reload 发出信号就立即返回,无法得知新配置到底有没有加载成功,只能事后去翻 error.log。1.31.5 把原来商业版独有的 Control API 开放到了开源版:

# 启动时开启 Control API,建议只监听本地 UNIX Socket
nginx -l unix:/run/nginx/control.sock

# 查看 Master 内存中实际生效的配置(含所有 include 文件)

curl --unix-socket /run/nginx/control.sock http://localhost/1/control/config

# 触发 reload,同步返回结果:成功返回 200,配置错误返回 422 和错误日志

curl --unix-socket /run/nginx/control.sock -X PATCH http://localhost/1/control/config

Control API 由 Master 进程直接提供,另外还有 /1/control/processes(查看 Worker 进程信息和退出状态)和 /1/nginx(查看版本和编译信息)两个端点。它特别适合容器和自动化发布平台:发布脚本可以直接根据 HTTP 状态码判断变更是否成功。

⚠️ 安全提示:Control API 能读取完整配置、触发 reload,务必只监听受限权限的 UNIX Socket,不要暴露在网络上。

五、平滑重载(reload):为什么不断流

执行 nginx -s reload 后,Master 按以下步骤完成「新旧交接」:

  1. 1. **校验与回滚:**Master 用新配置调用 ngx_init_cycle() 构建新一代 cycle。如果语法错误,或者端口、日志文件打不开,就直接丢弃新配置,继续用旧配置运行。
  2. 2. **复用资源:**监听地址没有变化的 Socket 直接沿用,不会关闭重开,所以不会出现端口的空窗期。名称和大小都没变的共享内存 zone 也会复用,限流计数、缓存索引不会丢失。
  3. 3. 启动新 Worker:用新配置 fork 出一批新的 Worker,立即开始接收新连接。
  4. 4. 优雅退出旧 Worker:通知旧 Worker 进入「shutting down」状态:关闭监听 Socket,不再接收新连接;关闭空闲的 keepalive 连接;把正在处理的请求处理完之后退出。

reload 期间,ps 里会同时出现新旧两批 Worker:

nginx: worker process is shutting down ← 旧 Worker,处理完存量请求后退出
nginx: worker process ← 新 Worker,已开始服务

⚠️ 常见问题:如果存在 WebSocket、gRPC 流、大文件下载这类长连接,旧 Worker 可能迟迟不退出。频繁 reload 会导致进程数和内存持续翻倍。可以用 worker_shutdown_timeout 30s; 限定优雅退出的最长时间,超时后强制关闭剩余连接。

六、二进制热升级:不停机更换版本

升级 Nginx 版本(或者加装静态编译的模块)需要更换可执行文件。热升级的核心思路是:新旧两代 Master 并存,共享同一组监听 Socket。

  1. 1. 用新二进制文件替换旧文件,然后向旧 Master 发送 USR2。
  2. 2. 旧 Master 把 pid 文件重命名为 nginx.pid.oldbin,然后 fork 并 exec 新的二进制文件。监听 Socket 的文件描述符通过环境变量 NGINX 传给新 Master,新 Master 直接继承使用,不需要重新 bind。
  3. 3. 新 Master 启动自己的 Worker。此时新旧两批 Worker 同时接收请求。
  4. 4. 向旧 Master 发送 WINCH:旧 Worker 优雅退出,旧 Master 本身保留。
  5. 5. 观察新版本运行是否正常:
  • • **正常:**向旧 Master 发送 QUIT,升级完成。
  • • **异常:**向旧 Master 发送 HUP,它会重新拉起旧版本的 Worker;再向新 Master 发送 QUIT,回滚完成。

💡 **适用范围:**在容器化部署中,通常直接滚动替换 Pod 或容器,很少使用二进制热升级;它主要用于物理机和虚拟机上长期运行的 Nginx 实例。

七、进程相关的关键配置

worker_processes auto; # Worker 数量,auto 表示等于 CPU 核数
worker_cpu_affinity
 auto; # 把 Worker 绑定到固定 CPU 核(Linux / FreeBSD)
worker_rlimit_nofile
 200000; # 每个 Worker 能打开的最大文件描述符数
worker_shutdown_timeout
 30s; # reload / quit 时旧 Worker 优雅退出的上限
events
 {
    worker_connections
 65535; # 每个 Worker 的最大连接数(含到后端的连接)
}

✅ 经验法则:worker_rlimit_nofile 要大于 worker_connections。做反向代理时,每个客户端请求通常还要占用一个到后端的连接,所以一个 Worker 能同时服务的客户端数约为 worker_connections / 2。

02 事件驱动:epoll、事件循环与连接管理

核心结论:Worker 的全部工作都在一个循环里完成:「等待事件 → 处理就绪的读写事件 → 处理到期的定时器 → 再次等待」。所有 I/O 都是非阻塞的:数据没准备好就立即返回、登记事件,转去处理别的连接。因此一个线程就能同时推进数万个连接,前提是任何一步都不能阻塞。

一、从阻塞 I/O 到 I/O 多路复用

阻塞模型的问题:调用 read() 时如果数据还没到,线程就被挂起等待。要同时服务 1 万个连接,就需要 1 万个线程,内存和上下文切换的开销都会成为瓶颈。

I/O 多路复用的思路:把所有 Socket 都设置为非阻塞,然后交给内核统一监视,只处理已经就绪的那些。Linux 上先后出现过三种实现:

机制
如何传递监视集合
如何找出就绪连接
连接数上限
复杂度
select
每次调用都把 fd 位图完整拷贝进内核
遍历全部 fd
1024(FD_SETSIZE)
O(n)
poll
每次调用都拷贝 pollfd 数组
遍历全部 fd
无硬性上限
O(n)
epoll
用 epoll_ctl 注册一次,内核长期保存
内核直接返回就绪列表
只受内存和文件描述符数限制
O(就绪数)

其中 fd 即 file descriptor(文件描述符)。Nginx 会自动选择平台上最高效的机制:Linux 用 epoll,FreeBSD / macOS 用 kqueue,Solaris 用 eventport。

二、epoll 的工作原理

epoll 只有三个系统调用:

  • • epoll_create():在内核中创建一个 epoll 实例。它包含一棵红黑树(存放所有被监视的 fd)和一个就绪链表。
  • • epoll_ctl():往红黑树里添加、修改或删除 fd,同时在该 fd 的等待队列上挂一个回调。
  • • epoll_wait():如果就绪链表为空,就休眠等待;网卡收到数据、协议栈处理完成后,会触发回调,把对应的 fd 加入就绪链表,并唤醒等待者。

所以 epoll 的开销和「连接总数」无关,只和「就绪的连接数」有关,这正是它能支撑海量空闲长连接的原因。

触发模式:epoll 有两种通知方式:

  • • 水平触发(LT,Level Triggered):只要缓冲区里还有数据,每次 epoll_wait 都会报告。
  • • 边缘触发(ET,Edge Triggered):只在状态变化时(例如新数据到达)报告一次。

Nginx 对连接使用边缘触发。代价是每次都必须把数据读到返回 EAGAIN 为止,否则剩下的数据不会再被通知;好处是避免了重复通知带来的无效唤醒。

三、Nginx 的事件抽象

为了屏蔽不同平台的差异,Nginx 定义了统一的事件接口 ngx_event_actions(包含 add、del、process_events 等操作),epoll、kqueue 等各自实现这组接口。上层代码只和三个核心结构打交道:

结构
含义
关键字段
ngx_connection_t
一个连接(客户端连接或者到后端的连接)
fd、read、write、pool、data
ngx_event_t
连接上的一个读事件或写事件
handler(回调函数)、ready、active、timedout、timer
ngx_cycle_t
当前这一代配置及其运行时资源
connections、free_connections、listening

关键设计:回调驱动的状态机。每个事件都绑定一个 handler。一个连接在处理的不同阶段,只需要替换 handler:例如等待请求头时,handler 是「解析请求头」;转发到后端后,handler 就换成「读取后端响应」。事件就绪时直接调用当前的 handler,处理逻辑从上次中断的地方继续。这样就用单线程实现了成千上万个「可暂停、可恢复」的执行流。

四、事件循环:Worker 的心跳

每个 Worker 都在无限循环调用 ngx_process_events_and_timers(),一轮循环大致如下:

for ( ;; ) {
    timer = ngx_event_find_timer(); // 1. 计算距离最近一个定时器到期还有多久
    if
 (accept_mutex) ngx_trylock_accept_mutex(); // 2. 可选:抢 accept 锁
    ngx_process_events(cycle, timer); // 3. epoll_wait(timeout = timer)
    ngx_time_update(); // 更新缓存的当前时间
    ngx_event_process_posted(accept_events); // 4. 先处理新连接事件
    ngx_shmtx_unlock(accept_mutex); // 尽快释放 accept 锁
    ngx_event_expire_timers(); // 5. 处理所有已到期的定时器
    ngx_event_process_posted(posted_events); // 6. 处理延后执行的普通读写事件
}

几个要点:

  • • 超时时间由最近的定时器决定:epoll_wait 最多只休眠到下一个定时器到期,保证超时能被及时处理。
  • • **时间缓存:**Nginx 在每轮循环里只获取一次系统时间,并缓存在 ngx_current_msec 中,避免频繁调用 gettimeofday()。可以用 timer_resolution 进一步降低更新频率。
  • • posted 队列:持有 accept 锁时,读写事件会先放进 posted 队列,等锁释放后再处理,这样可以缩短持锁时间。

五、定时器:超时如何实现

Nginx 的所有超时,包括 client_header_timeout、proxy_read_timeout、keepalive_timeout 等,都是基于同一棵红黑树实现的:

  • • 以「到期时间」为键,把事件插入红黑树(ngx_event_add_timer)。
  • • 每轮循环取出最左边(即最早到期)的节点,计算 epoll_wait 的超时时间。
  • • 到期后把事件的 timedout 置为 1 并调用它的 handler,由 handler 负责返回 408、504 等状态码,或者关闭连接。
  • • 事件在超时之前就绪了,就删除这个定时器。

💡 **理解超时语义:**Nginx 的大多数读写超时是指「两次连续读写操作之间」的间隔,而不是整个请求的总时长。比如 proxy_read_timeout 60s 表示后端连续 60 秒没有发送任何数据才判定超时;如果后端每 59 秒吐出一个字节,这个请求可以持续很久。

六、新连接如何分配给 Worker

所有 Worker 共享同一个监听 Socket。一个新连接到来时,内核唤醒哪个 Worker?Nginx 先后演进出以下几种方案:

方案
原理
优点
缺点
不做处理
所有 Worker 都监听同一个 Socket,被同时唤醒
简单
惊群效应(Thundering Herd):只有一个 Worker 能 accept 成功,其余被白白唤醒
accept_mutex
各 Worker 竞争一把共享内存锁,只有持锁者注册监听事件
避免惊群
负载可能不均;有抢锁延迟。1.11.3 起默认关闭
EPOLLEXCLUSIVE
Linux 4.5+ 的内核标志:每次只唤醒一个等待者
没有锁开销,Nginx 自动启用
分配不保证均衡
reuseport
listen 80 reuseport;:每个 Worker 拥有独立的监听 Socket,由内核按四元组哈希分配连接
分配均衡;不会惊群;accept 没有竞争
某个 Worker 卡顿时,分给它的连接会一起变慢

此外,Nginx 还有一个自我调节机制:当 Worker 的空闲连接数少于总数的 1/8 时,变量 ngx_accept_disabled 会变为正数,这个 Worker 会主动放弃若干轮 accept,把新连接让给更空闲的 Worker。

✅ **实践建议:**高并发的 Linux 场景下推荐开启 reuseport,同时确保 Worker 内没有阻塞操作。

七、连接池:worker_connections 的真实含义

每个 Worker 启动时,会一次性预分配 worker_connections 个 ngx_connection_t,以及同样数量的读事件和写事件,用空闲链表管理:

  • • 新连接到来时,从 free_connections 链表头取出一个;连接关闭后放回链表。整个过程没有 malloc,是 O(1) 操作。
  • • **这个数量包括所有连接:**客户端连接、到后端的连接、到 DNS 解析器的连接,都会占用连接池中的位置。
  • • 连接池耗尽后,error.log 会出现 worker_connections are not enough,新连接将被拒绝。

最大并发连接数的估算:

角色
最大并发客户端数
静态文件服务器
worker_processes
 × worker_connections
反向代理
约 worker_processes × worker_connections / 2(每个请求还要占用一个上游连接)

同时要满足 worker_rlimit_nofile ≥ worker_connections,否则连接池还没用完,就会先遇到 Too many open files 错误。

八、非阻塞的边界:哪些操作会拖垮 Worker

事件驱动模型只有一个致命弱点:Worker 里任何一次阻塞,都会冻结它上面所有的连接。常见的阻塞来源和应对方式如下:

阻塞来源
现象
应对方式
磁盘读取大文件(页缓存未命中)
静态文件服务的延迟毛刺
aio
 threads; 把磁盘读操作卸载到线程池;配合 directio 读取超大文件
复杂的正则表达式
单核 CPU 打满
简化正则;开启 pcre_jit on;
同步调用的第三方模块 / Lua 代码
全局延迟上升
只使用非阻塞 API(如 OpenResty 的 cosocket)
高压缩级别的 gzip
CPU 占用高
gzip_comp_level
 取 4~6;静态资源预压缩后用 gzip_static
同步写日志
磁盘繁忙时整体变慢
access_log
 ... buffer=64k flush=5s;

💡 **排查线索:**如果 $request_time 普遍偏高,而 $upstream_response_time 并不高,同时某个 Worker 的 CPU 持续 100%,基本可以判定是 Worker 内部出现了阻塞或者 CPU 密集型操作。具体方法见 08 篇。

03 请求处理:模块体系、11 个阶段与 location 匹配

核心结论:Nginx 把一次 HTTP 请求的处理拆成三段流水线:11 个处理阶段决定「由谁来处理、是否放行」,内容处理器(Handler)负责生成响应,过滤器链(Filter)负责加工响应。所有功能都是模块在这些环节上注册的回调,配置文件的本质就是「告诉每个模块在哪个作用域做什么」。

一、模块体系:一切皆模块

Nginx 的核心代码只提供最基础的框架,所有具体能力都由模块实现。模块按层次可以分为五类:

类型
作用
代表
core
框架核心:进程、日志、配置解析
ngx_core_module
、ngx_errlog_module
event
事件机制
ngx_events_module
、ngx_epoll_module
http
七层 HTTP 处理
ngx_http_core_module
、ngx_http_proxy_module
stream
四层 TCP / UDP 代理
ngx_stream_proxy_module
mail
邮件协议代理
ngx_mail_smtp_module

每个模块都用一个 ngx_module_t 结构体描述,其中最重要的是两部分:

  • • **指令表(commands):**声明模块支持哪些配置指令、允许出现在哪些上下文,以及解析时调用哪个函数。
  • • **上下文(ctx):*对 HTTP 模块来说,就是 create_main_conf、create_srv_conf、create_loc_conf 和对应的 merge_ 函数,决定了配置如何在各层之间继承。

模块可以静态编译进二进制,也可以编译成动态模块(.so),用 load_module 按需加载。注意:动态模块必须和 Nginx 主程序用相同的版本和编译参数构建,否则会因为二进制不兼容而拒绝加载。

二、配置的继承与合并

HTTP 配置是一棵层层嵌套的树:http → server → location(location 还可以继续嵌套)。每一层的每个模块都有一份自己的配置结构体。解析完成后,框架自顶向下调用 merge_* 函数:内层没有设置的值,就继承外层的值。

但是有一类指令是例外:数组型指令,比如 add_header、proxy_set_header,遵循「要么全部继承,要么完全不继承」的规则:

server {
    add_header
 X-Frame-Options DENY;
    location
 /api/ {
        add_header
 X-Request-Id $request_id;
        # 结果:/api/ 下只有 X-Request-Id,X-Frame-Options 丢失了!

    }
}

只要内层写了一条 add_header,外层所有的 add_header 就都不再生效。这是线上最常见的配置陷阱之一。从 1.29.3 开始(1.30 stable 已包含),可以用 add_header_inherit merge; 改为「追加合并」:

http {
    add_header_inherit
 merge; # 在顶层声明一次,递归作用于所有下层
}

三、请求解析与 server 选择

  1. 1. **读取与解析:**Worker 从连接上读取数据,用手写的状态机逐字节解析请求行和请求头,速度很快,而且不依赖正则表达式。请求头先放在 client_header_buffer_size(默认 1k)大小的缓冲区里;超出后,再从 large_client_header_buffers(默认 4 个 8k)中分配。单个请求头超出一个大缓冲区时返回 400;请求行超出时返回 414。
  2. 2. 选择 server:先按 listen 的 IP(Internet Protocol,网际协议)地址和端口圈出候选的 server 块,再用 Host 头(HTTPS 时优先用 SNI)按以下顺序匹配 server_name:
  3. 3. 精确名称,如 www.example.com
  4. 4. 以通配符开头的最长名称,如 *.example.com
  5. 5. 以通配符结尾的最长名称,如 www.example.*
  6. 6. 按配置顺序匹配正则表达式,如 ~^api\d+\.example\.com$
  7. 7. 都没有匹配时,使用该监听地址上的 default_server;没有显式指定 default_server 的话,就用第一个 server 块

四、11 个处理阶段

请求头解析完成后,就进入阶段引擎。Nginx 在启动时把各模块注册的回调扁平化为一个数组,每个元素带一个 checker 函数,由它决定下一步是执行下一个回调、跳到某个阶段,还是结束请求。ngx_http_core_run_phases() 只需要沿着这个数组往下走。

阶段
作用
典型模块 / 指令
可以注册自定义模块吗
POST_READ
读取请求头后最先执行
realip(还原真实客户端 IP)
可以
SERVER_REWRITE
server 级别的重写
server 块中的 rewrite、set、return
可以
FIND_CONFIG
根据 URI(Uniform Resource Identifier,统一资源标识符)匹配 location
框架内置,见第五节
不可以
REWRITE
location 级别的重写
location 中的 rewrite、set
可以
POST_REWRITE
检查 URI 是否被重写
被重写就跳回 FIND_CONFIG,最多重复 10 次
不可以
PREACCESS
访问控制之前的限制
limit_req
、limit_conn
可以
ACCESS
访问控制
allow / deny、auth_basic、auth_request
可以
POST_ACCESS
汇总 ACCESS 阶段的结果
satisfy any / all
不可以
PRECONTENT
生成内容之前
try_files
、mirror
可以
CONTENT
生成响应内容
proxy_pass
、fastcgi_pass、index、static
可以
LOG
请求结束时记录日志
access_log
可以

💡 **理解执行顺序:**配置文件中指令的书写顺序不代表执行顺序,执行顺序由阶段决定。例如,无论 limit_req 写在 rewrite 前面还是后面,都一定是 rewrite 先执行。这也解释了为什么 return 会让同一 location 里的 allow / deny 失效:return 属于 REWRITE 阶段,在 ACCESS 阶段之前就结束了请求。

五、location 匹配规则

FIND_CONFIG 阶段按下面的优先级为请求选择 location:

  1. 1. 精确匹配 =:完全相等时立即选中,停止搜索。
  2. 2. **前缀匹配:**找出匹配的最长前缀并记下。如果它带有 ^~ 修饰符,就直接选中,停止搜索。
  3. 3. 正则匹配 ~(区分大小写)/ ~*(不区分大小写):按配置文件中出现的顺序逐个检查,第一个匹配的就被选中。
  4. 4. 谓词匹配$变量(1.31.5+,mainline):正则都没有匹配时,按顺序检查谓词 location,变量值非空且不为 "0" 就算匹配。
  5. 5. 以上都不匹配时,使用第 2 步记下的最长前缀 location。

示例:

配置
/
/images/a.png
/static/a.png
/api/v1
location = / {}
✅



location ^~ /static/ {}


✅

location ~* .(png|jpg)$ {}

✅


location /api/ {}



✅

⚠️ 注意:/images/a.png 即使另外配置了 location /images/ {},也会被正则 location 抢走,因为普通前缀匹配只是「记下」,正则仍然会参与匹配。如果想让前缀优先,请使用 ^~。

谓词 location:按请求内容路由(mainline 新特性)。过去 location 只能按 URI 匹配,按请求头或请求体路由只能借助 if 或脚本。1.31.5 起,任何变量都可以作为 location 的匹配条件,并且可以配合 client_body_early_read 和 ngx_http_json_module,在选择 location 之前就解析 JSON(JavaScript Object Notation,JavaScript 对象表示法)格式的请求体:

client_body_early_read on; # 在 location 匹配之前读取请求体
json_set
 $vip $request_body user.params.vip; # 从 JSON 中提取字段作为变量
server
 {
    location
 $vip { proxy_pass http://vip_backend; } # vip 字段为真值时命中
    location
 / { proxy_pass http://normal_backend; }
}

六、内容处理器与过滤器链

内容处理器(Handler):每个请求最终只能由一个 Handler 生成响应。如果 location 中配置了 proxy_pass、fastcgi_pass、grpc_pass 等指令,对应模块就会把自己设置为这个 location 的专属 Handler;否则,就按顺序尝试 CONTENT 阶段注册的通用 Handler:index、autoindex、static(静态文件)。

过滤器链(Filter):Handler 生成的响应还要经过一串过滤器加工,分为两条链:

  • • header filter 链:处理响应状态行和响应头,例如 not_modified(处理 304)、headers(add_header、expires)、gzip(修改 Content-Encoding)。
  • • body filter 链:处理响应体,例如 sub(文本替换)、gzip(压缩)、chunked(分块编码),最后由 write filter 真正写入 Socket。

过滤器链是一个单向链表:每个 filter 模块初始化时,都把自己插到链表头部,并保存原来的链表头作为「下一个」。因此执行顺序和注册顺序相反,这个顺序在编译时由 configure 生成的模块列表决定。

💡 **流式处理:**body filter 处理的是缓冲链(ngx_chain_t)而不是完整的响应体。后端返回一块数据,就立即流经整条过滤链并发出去,不需要等待完整的响应,所以内存占用与响应大小无关(详见 05 篇)。

七、变量:配置的表达能力

$host、$remote_addr、$upstream_status 等变量是 Nginx 配置灵活性的来源,它的实现有两个关键特征:

  • • **惰性求值:**变量只在真正被使用时才计算。例如,$request_body 只有被引用时才会读取请求体。
  • • **索引化缓存:**配置解析阶段,每个被引用的变量都会分配一个索引。请求中第一次求值后,结果缓存在 r->variables[index] 中,后续访问直接读取缓存。

map 是构造变量的主要工具,同样是惰性求值的:

map $http_user_agent $is_mobile {
    default
 0;
    ~*(android|iphone) 1;
}

八、内部跳转与子请求

内部跳转(Internal Redirect):rewrite ... last、try_files 的最后一个参数、error_page,都会让请求带着新 URI 回到 FIND_CONFIG 阶段重新匹配 location。这个过程不会产生新的 TCP 连接,客户端也感知不到。为了防止死循环,最多允许跳转 10 次,超过后返回 500。

子请求(Subrequest):在一个主请求内部,再发起一个或多个请求,并复用同一个连接。典型用法有:

  • • auth_request:先向鉴权服务发送子请求,根据它返回的 2xx 或 401 / 403 决定是否放行主请求。
  • • **mirror:**把请求复制一份发往测试环境,用于流量回放,镜像请求的响应会被丢弃。
  • • SSI(Server Side Includes,服务端包含):把多个子请求的响应拼接成一个页面。多个子请求的输出由 postpone filter 保证按正确的顺序输出。

04 反向代理与负载均衡:upstream 原理

核心结论:反向代理就是 Worker 替客户端向后端发起一个非阻塞的请求,再把响应流式转发回去。决定其表现的是三件事:选谁(负载均衡算法)、怎么连(连接复用与超时)、出错怎么办(被动健康检查与重试)。从 1.29.7 起(1.30 stable 已包含),Nginx 默认启用到后端的 HTTP/1.1 长连接,这是近年对代理行为影响最大的一次变化。

一、一次代理请求的完整流程

proxy、fastcgi、grpc、uwsgi 等模块都建立在同一个 upstream 框架(ngx_http_upstream_t)之上。各协议模块只需要实现「如何构造请求」「如何解析响应头」等几个回调,连接管理、超时、重试、缓冲都由框架统一处理。

  1. 1. **读取请求体:**默认(proxy_request_buffering on)先把客户端请求体完整读完,放在内存或临时文件里,再开始连接后端。这样可以避免慢速客户端长时间占用后端。
  2. 2. **选择后端:**调用负载均衡模块的 get 回调选出一个节点(见第三节)。
  3. 3. **建立连接:**优先从 keepalive 连接池里取一个空闲连接;没有空闲连接,才发起非阻塞的 connect(),同时启动 proxy_connect_timeout 定时器。
  4. 4. **发送请求:**写入请求行、请求头(按 proxy_set_header 改写)和请求体,受 proxy_send_timeout 约束。
  5. 5. **读取响应头:**等待后端响应,受 proxy_read_timeout 约束。解析状态行和响应头,交给 header filter 链处理。
  6. 6. **转发响应体:**按缓冲模式把后端数据转发给客户端(见第二节)。
  7. 7. **收尾:*释放后端连接:如果可以复用,就放回 keepalive 池,否则关闭。记录 $upstream_ 系列变量,供日志使用。

二、缓冲:Nginx 如何隔离快慢两端

客户端通常是「慢」的(移动网络、跨地域),后端通常是「快」而「贵」的(每个连接都占用应用线程)。缓冲的目的是让后端尽快处理完、尽快释放连接,由 Nginx 慢慢把数据喂给客户端。

方向
指令
开启时(默认)
关闭时
请求
proxy_request_buffering
先读完整个请求体,再发给后端
边读边发,适合大文件上传;但出错后无法重试
响应
proxy_buffering
用 proxy_buffers 在内存中缓冲后端响应;装不下就写入临时文件(上限 proxy_max_temp_file_size)
收到多少就同步转发多少,只使用一个 proxy_buffer_size 大小的缓冲区;适合流式接口(SSE(Server-Sent Events,服务器推送事件)、长轮询)

开启响应缓冲时,Nginx 用一个叫 event pipe(ngx_event_pipe_t)的组件同时处理「从后端读」和「向客户端写」两个方向的事件:两端各自独立推进,由缓冲区在中间调节速度差。

⚠️ 注意:proxy_buffer_size 同时决定了能接收的响应头最大值。后端返回很大的 Cookie 或头部时,会出现 upstream sent too big header,并返回 502。遇到这种情况,要调大的是 proxy_buffer_size,而不是 proxy_buffers。

三、负载均衡算法

算法
指令
原理
适用场景
加权轮询
默认
平滑加权轮询(见下文)
后端同构、请求耗时相近
最少连接
least_conn
选择「活跃连接数 / 权重」最小的节点
请求耗时差异大,例如长轮询
一致性哈希
hash $key consistent
ketama 算法:每个节点按权重在哈希环上放置 160 个虚拟点
需要会话粘滞或缓存亲和,节点增减时只影响少量 key
IP 哈希
ip_hash
对 IPv4 地址的前三段(或 IPv6 完整地址)做哈希
简单的会话保持(同一网段会落到同一节点)
随机
random two least_conn
随机选两个节点,再取连接数较少的一个
多个 Nginx 实例共同代理同一组后端,各自的连接计数都不完整时
最短响应时间
least_time
 header | last_byte
综合平均响应时间和活跃连接数选择节点;1.31.0 起进入开源版
后端性能不均,例如混合了新旧机型
会话保持
sticky
 cookie | route | learn
用 Cookie 或路由变量把同一客户端固定到同一节点;1.29.6 起进入开源版
有状态的应用

平滑加权轮询(Smooth Weighted Round-Robin)是默认算法,它解决的问题是:权重 5:1:1 的三个节点 a、b、c,朴素的做法会连续发 5 个请求给 a,造成瞬时压力集中。Nginx 的算法如下:

  1. 1. 每轮选择时,每个节点的 current_weight 加上它自己的权重。
  2. 2. 选出 current_weight 最大的节点。
  3. 3. 被选中的节点,current_weight 减去所有节点的权重总和。

轮次
选择前 current_weight(a, b, c)
选中
选中后 current_weight
1
5, 1, 1
a
-2, 1, 1
2
3, 2, 2
a
-4, 2, 2
3
1, 3, 3
b
1, -4, 3
4
6, -3, 4
a
-1, -3, 4
5
4, -2, 5
c
4, -2, -2
6
9, -1, -1
a
2, -1, -1
7
7, 0, 0
a
0, 0, 0

7 次选择得到的序列是 a a b a c a a:比例严格等于 5:1:1,而且 b、c 被均匀地穿插进去了。7 轮之后所有值都回到 0,开始新的周期。

💡 多 Worker 的影响:默认情况下,每个 Worker 各自维护一份轮询状态和失败计数。配置了 zone backend 64k; 后,这些状态才会放到共享内存里,由所有 Worker 共享。建议所有 upstream 都配置 zone。

四、被动健康检查与失败重试

开源版 Nginx 通过真实请求的失败来判断节点是否健康,称为被动健康检查:

  • • **判定:**在 fail_timeout(默认 10s)时间窗内,失败次数达到 max_fails(默认 1),这个节点就被标记为不可用,时长同样是 fail_timeout。
  • • **恢复:**不可用时间结束后,放行一个请求去试探。成功就恢复正常,失败就继续标记为不可用。
  • • **什么算失败:**连接或读写出错(error)、超时(timeout)、响应头不合法(invalid_header)总是计为失败;http_500、http_502、http_503、http_504、http_429 只有在 proxy_next_upstream 中显式列出,才计为失败;403 和 404 永远不计为失败。

失败重试(next upstream):当前节点失败时,只要还没有向客户端发送任何数据,就会换一个节点重试:

proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries
 2; # 最多尝试 2 个节点(含第一次)
proxy_next_upstream_timeout
 5s; # 重试的总时间上限

⚠️ 幂等性保护:从 1.9.13 起,POST、LOCK、PATCH 等非幂等请求默认不会被重试,以免重复下单这类问题;除非显式加上 non_idempotent 参数。所有节点都不可用时,error.log 会记录 no live upstreams,并返回 502。
主动健康检查(health_check,定期主动探测后端)属于商业版功能,开源版一般通过外部探测配合服务发现来实现。

五、到后端的连接复用:keepalive

为什么重要:如果每个请求都新建一个到后端的 TCP 连接,除了握手带来的延迟,Nginx 这一侧还会积累大量 TIME_WAIT 状态的连接,最终耗尽本地端口,出现 Cannot assign requested address。

1.29.7 带来的默认值变化(1.30 stable 已包含):

项目
1.29.7 之前
1.29.7 及之后
upstreamkeepalive
默认关闭,必须显式配置
默认 keepalive 32 local
proxy_http_version
默认 1.0(不支持长连接)
默认 1.1
Connection 请求头
默认发送 close,必须用 proxy_set_header Connection "" 清空
默认不再发送

其中 local 参数表示:缓存的连接只在同一个 location 内复用,不跨 location 共享,避免不同 location 设置的请求头在同一条连接上互相干扰。32 是每个 Worker 为每个 upstream 最多保留的空闲连接数,不是连接总数的上限。

相关参数:

upstream backend {
    zone
 backend 64k;
    server
 10.0.0.1:8080;
    server
 10.0.0.2:8080;
    keepalive
 64; # 每个 Worker 最多保留的空闲连接数
    keepalive_timeout
 50s; # 空闲连接的保留时长(默认 60s)
    keepalive_requests
 1000; # 单个连接最多承载的请求数
}

⚠️ 升级注意:从旧版本升级到 1.30 后,原本「每个请求一个短连接」的服务会自动变成长连接。如果后端的空闲超时比 Nginx 的 keepalive_timeout 还短,后端会先关闭连接,而 Nginx 恰好在这时复用了这条连接,就会出现偶发的 upstream prematurely closed connection 和 502。原则:Nginx 的 keepalive_timeout 要小于后端的空闲超时。详见 08 篇案例一。

六、服务发现:后端地址会变怎么办

server backend.svc:8080; 这种写法中的域名,只在启动或 reload 时解析一次。后端 IP 变化后,Nginx 仍然会连接旧的地址。有两种解决办法:

  • • resolve 参数(开源版从 1.27.3 起支持):upstream 需要配置 zone,同时配置 resolver,Nginx 会按 DNS 的 TTL(Time To Live,生存时间)周期性地重新解析,自动增删节点。
  • • 在 proxy_pass 中使用变量:proxy_pass http://$backend_host; 每次请求都会通过 resolver 异步解析,但这种方式无法使用 upstream 的负载均衡和 keepalive 能力。
resolver 10.0.0.2 valid=10s;
upstream
 backend {
    zone
 backend 64k;
    server
 backend.default.svc.cluster.local:8080 resolve;
}

七、后端协议:HTTP/2、gRPC 与更多

  • • HTTP/2 后端(1.29.4+):proxy_http_version 2; 可以让 proxy 模块用 HTTP/2 连接后端,在一条连接上多路复用多个请求。
  • • gRPC(Google Remote Procedure Call,谷歌远程过程调用框架):grpc_passgrpc://backend;,基于 HTTP/2,支持流式调用。
  • • FastCGI / uWSGI / SCGI:分别用于对接 PHP-FPM(FastCGI Process Manager,FastCGI 进程管理器)和 Python 等应用服务器。
  • • CONNECT 隧道(1.31.0+,mainline):ngx_http_tunnel_module 处理 HTTP CONNECT 请求,可以让 Nginx 充当正向代理。

八、四层代理:stream 模块

stream 模块用和 HTTP 相同的事件模型和 upstream 框架代理 TCP / UDP 流量,只是没有 HTTP 解析,处理阶段也更简单:Post-accept → Pre-access → Access → SSL → Preread → Content → Log。

  • • ssl_preread:只读取 TLS 握手中的 SNI,不解密流量,就能按域名把连接分发到不同后端,可以实现「TLS 透传」。
  • • proxy_protocol:在转发时附带客户端的真实地址,1.31.4 起在 stream 和 mail 模块中支持 PROXY protocol v2 版本。
  • • 支持的负载均衡算法、max_fails 被动健康检查、resolve 动态解析,都与 HTTP upstream 一致。

05 内存、缓冲与缓存:内存池、零拷贝与 proxy_cache

核心结论:Nginx 的高效,一半来自事件模型,另一半来自「少分配、少拷贝、少读盘」:内存池让内存按生命周期批量释放;缓冲链让数据在各模块之间传递时只传指针;sendfile 让文件数据不经过用户态;proxy_cache 让重复的请求不再访问后端。

一、内存池:按生命周期管理内存

C 语言服务器最常见的两类问题是内存泄漏和内存碎片。Nginx 的对策是 ngx_pool_t 内存池:只分配、不单独释放,生命周期结束时整体销毁。

内存池的结构:

  • • **小块内存:**从池中预先申请的内存块里,通过移动指针的方式分配,速度接近栈分配。当前内存块不够用时,再申请一个新块,用链表串起来。
  • • **大块内存:**超过阈值(接近一个内存页大小)的申请,直接调用 malloc 分配,挂在 large 链表上,可以单独释放。
  • • **清理回调(cleanup):**需要在销毁时执行的动作(例如关闭文件、释放锁)以回调的形式注册,池销毁时统一执行。

三种典型的生命周期:

内存池
创建时机
销毁时机
存放内容
cycle 池
启动或 reload
这一代配置被废弃时
配置结构体、共享内存描述
连接池
接受连接时(connection_pool_size)
连接关闭时
连接级别的数据,例如 TLS 状态
请求池
创建请求时(request_pool_size)
请求结束时
请求头、变量值、缓冲区

这种设计让模块开发者不必关心 free:只要从正确的池里分配,内存就一定会在正确的时间被回收。

二、缓冲与缓冲链:传指针,不传数据

Nginx 在各模块之间传递数据时,统一使用两个结构:

  • • ngx_buf_t(缓冲区):描述「一段数据在哪里」。它既可以指向内存(pos 到 last),也可以指向文件的某个区间(file_pos 到 file_last)。标志位 last_buf 表示响应结束,flush 表示需要立即发送。
  • • ngx_chain_t(缓冲链):把多个 ngx_buf_t 串成链表。

过滤器处理数据时,传递的是缓冲链,不是数据本身。例如,给响应追加内容时,只需要在链表中插入一个新节点;如果缓冲区指向的是文件,数据从头到尾都不会被读进用户态内存,最终直接交给 sendfile 发送。

缓冲区复用:upstream 等模块会维护 free(空闲)和 busy(数据还在发送中)两条链。一块缓冲区的数据被内核完全接收后,它就从 busy 链移回 free 链,接着装下一批数据。所以即使转发 1 GB 的响应,内存占用也始终只有配置的若干个缓冲区大小。

三、零拷贝:sendfile

发送一个静态文件时,传统方式与 sendfile 的对比如下:

方式
系统调用
数据拷贝
用户态 / 内核态切换
read() + write()
2 次
4 次:磁盘 → 页缓存 → 用户缓冲区 → Socket 缓冲区 → 网卡
4 次
sendfile
()
1 次
2 次 DMA(Direct Memory Access,直接内存访问)拷贝,外加最多 1 次内核内拷贝,数据不经过用户态
2 次

相关指令的配合:

sendfile on; # 开启零拷贝发送
sendfile_max_chunk
 2m; # 单次 sendfile 的上限,防止一个大文件独占 Worker(1.21.4 起默认 2m)
tcp_nopush
 on; # 开启 TCP_CORK:先把响应头和文件开头拼成满包再发送
tcp_nodelay
 on; # keepalive 连接上关闭 Nagle 算法,小包立即发送

⚠️ sendfile 不适用的场景:启用了 gzip 压缩或 SSI 这类需要修改响应体的过滤器、启用了 TLS(数据需要在用户态加密)时,数据必须读进内存处理,sendfile 自动失效。Linux 内核的 kTLS(Kernel TLS,内核态 TLS)可以缓解这个问题,Nginx 通过 ssl_conf_command Options KTLS; 启用,需要 OpenSSL 3.0+ 和内核的支持。

大文件的另一条路径:页缓存命中率低的超大文件(例如视频),可以用 directio 4m; 绕过页缓存,再配合 aio threads; 用线程池异步读取,避免 Worker 阻塞在磁盘 I/O 上。

四、共享内存与 slab 分配器

多个 Worker 需要共享的数据,比如限流计数、SSL 会话、缓存索引、upstream 状态,都存放在共享内存 zone 中。

  • • **创建:**Master 在 fork 之前用 mmap 创建匿名共享内存,Worker 继承后就能直接访问同一块物理内存。
  • • **分配:**zone 内部用 slab 分配器管理:先按内存页划分,再按 2 的幂次划分为不同规格的槽位(8 字节、16 字节、32 字节……)。同规格的对象放在一起,避免产生碎片。
  • • **同步:**每个 zone 有一把互斥锁(ngx_shmtx_t),优先用原子操作加自旋实现;自旋一段时间仍拿不到锁,再退化为信号量等待。
  • • reload 时复用:名称、大小都没变的 zone 会被新一代配置直接复用,其中的数据不会丢失。

⚠️ 容量规划:zone 装满后,新的条目会分配失败。例如 limit_req_zone 满了之后,会淘汰最久未使用的条目;实在淘汰不出空间,就会拒绝请求并返回 503,同时 error.log 中出现 could not allocate node。官方给出的参考容量:1 MB 的 ssl_session_cache 约能存放 4000 个会话;1 MB 的缓存 keys_zone 约能存放 8000 个 key。

五、proxy_cache:把后端响应缓存在本地

配置示例:

proxy_cache_path /data/cache levels=1:2 keys_zone=api_cache:100m
                 max_size=50g inactive=7d use_temp_path=off;
server
 {
    location
 /api/ {
        proxy_cache
 api_cache;
        proxy_cache_key
 host$request_uri;
        proxy_cache_valid
 200 302 10m;
        proxy_cache_lock
 on; # 防止缓存击穿
        proxy_cache_use_stale
 error timeout updating; # 后端故障时返回旧缓存
        proxy_cache_background_update
 on; # 在后台刷新过期缓存
        add_header
 X-Cache-Status $upstream_cache_status;
        proxy_pass
 http://backend;
    }
}

存储结构:两级存储。

  • • 内存(keys_zone):存放索引,用红黑树按 key 的 MD5(Message-Digest Algorithm 5,消息摘要算法第 5 版)值查找,同时维护一个 LRU(Least Recently Used,最近最少使用)队列,用于淘汰。
  • • **磁盘:**存放完整的响应(响应头 + 响应体),文件名就是 MD5 值,levels=1:2 表示按 MD5 值的末尾字符建立两级子目录,避免单个目录下的文件过多。

一次请求的处理流程:

  1. 1. 按 proxy_cache_key 计算 MD5,在共享内存中查找索引。
  2. 2. **命中且未过期(HIT):**直接读取磁盘文件返回(通常命中页缓存,并且可以使用 sendfile),完全不访问后端。
  3. 3. 未命中(MISS)或已过期(EXPIRED):向后端请求。响应一边返回给客户端,一边写入临时文件,完成后原子地 rename 为正式的缓存文件。
  4. 4. **缓存击穿保护:**开启 proxy_cache_lock 后,同一个 key 同时只有一个请求访问后端,其他请求等待它写完缓存后,直接读缓存。
  5. 5. **过期处理:**开启 proxy_cache_use_stale updating 和 background_update 后,过期内容先返回旧版本(UPDATING / STALE),同时在后台发起子请求刷新缓存,用户感受不到延迟。

$upstream_cache_status 的常见取值有:HIT、MISS、EXPIRED、STALE、UPDATING、REVALIDATED、BYPASS。在日志中记录这个变量,是分析缓存命中率最直接的办法。

缓存有效期由谁决定:后端返回的 Cache-Control、Expires 响应头优先;没有这些头时,才使用 proxy_cache_valid。后端返回 Set-Cookie 或 Cache-Control: private 的响应默认不会被缓存,这一点经常导致「缓存配了却不生效」。

六、open_file_cache:缓存文件元数据

静态文件服务器每处理一个请求,都要执行 open()、fstat() 等系统调用。open_file_cache 在 Worker 本地缓存文件描述符、文件大小、修改时间,以及「文件不存在」这类查找结果:

open_file_cache max=10000 inactive=60s;
open_file_cache_valid
 30s; # 每 30 秒重新校验一次缓存项
open_file_cache_errors
 on; # 同时缓存「文件不存在」等错误结果

💡 **代价:**文件更新后,最多要等 open_file_cache_valid 这么长的时间才会生效。发布频繁的静态站点,要结合发布方式(例如带版本号的文件名)来权衡。

06 协议栈:TLS、HTTP/2 与 HTTP/3(QUIC)

核心结论:三种协议在 Nginx 中都是事件驱动的状态机,握手或读写无法立即完成时就让出 Worker,与普通请求一样不会阻塞。它们解决的问题层层递进:TLS 解决安全问题;HTTP/2 在一条 TCP 连接上多路复用,解决 HTTP 层的队头阻塞;HTTP/3 把传输层换成基于 UDP 的 QUIC,进一步消除 TCP 层的队头阻塞,并支持连接迁移。

一、三代 HTTP 协议对比

维度
HTTP/1.1
HTTP/2
HTTP/3
传输层
TCP
TCP
QUIC(基于 UDP)
报文格式
文本
二进制帧
二进制帧
并发方式
每个连接同时只能处理一个请求,浏览器靠开多个连接实现并发
一个连接上多路复用多个流(Stream)
同 HTTP/2,而且流与流之间在传输层也相互独立
队头阻塞
HTTP 层和 TCP 层都有
解决了 HTTP 层;TCP 丢一个包,所有流都要等待重传
基本消除:一个流丢包不影响其他流
头部压缩
无
HPACK
QPACK
建连耗时
TCP 握手 + TLS 握手,约 2~3 RTT(Round-Trip Time,往返时延)
同 HTTP/1.1
1 RTT;会话恢复时可以 0-RTT
连接迁移
不支持
不支持
支持:手机从 Wi-Fi 切到 4G 后,连接不中断

二、TLS:安全与性能的平衡

1. 非阻塞握手

TLS 握手需要多次往返。Nginx 调用 OpenSSL 的 SSL_do_handshake(),如果返回 SSL_ERROR_WANT_READ 或 SSL_ERROR_WANT_WRITE,就把连接的 handler 设置为「继续握手」,然后返回事件循环;数据到达后再接着握手。握手完成后,读写操作通过 SSL_read() 和 SSL_write() 进行,处理方式相同。

2. 握手的 CPU 开销与优化

完整握手中最耗 CPU 的是证书私钥签名运算。常见的优化手段:

优化
做法
效果
会话缓存
ssl_session_cache
 shared:SSL:50m;
会话信息存放在共享内存中,所有 Worker 都能复用;1 MB 约可存放 4000 个会话
会话票据
ssl_session_tickets
 on;(默认开启)
会话状态加密后交给客户端保存,服务端不需要存储
ECDSA 证书
同时配置 RSA(Rivest–Shamir–Adleman,一种公钥算法)和 ECDSA 两套证书
ECDSA(Elliptic Curve Digital Signature Algorithm,椭圆曲线数字签名算法)签名比 RSA 快得多;老客户端自动回退到 RSA
TLS 1.3
ssl_protocols
 TLSv1.2 TLSv1.3;
完整握手只需 1 RTT
0-RTT
ssl_early_data
 on;
恢复会话时,客户端可以随第一个包就发送请求
证书缓存
ssl_certificate_cache
、ssl_object_cache_inheritable(1.27.4+)
reload 时复用已经加载的证书,减少大量证书场景下的 reload 耗时

⚠️ 两个安全细节:

3. 近期 TLS 相关演进

  • • **证书压缩(1.29.1):**TLS 1.3 的证书压缩改为默认关闭,可以用 ssl_certificate_compression on; 显式开启。
  • • ECH(Encrypted ClientHello,加密客户端问候,1.29.4):加密握手中的 SNI,防止网络中间设备看到访问的域名。需要使用 OpenSSL 的 ECH 特性分支构建,用 ssl_ech_file 配置。
  • • **加密库:**除了 OpenSSL,也支持 BoringSSL、LibreSSL;1.29.2 起支持 AWS-LC(Amazon Web Services libcrypto,亚马逊维护的加密库),1.29.8 起兼容 OpenSSL 4.0。1.29.0 起可以通过 OpenSSL provider 从硬件令牌加载私钥。
  • • **证书自动化:**官方提供 ngx_http_acme_module 动态模块,通过 ACME(Automatic Certificate Management Environment,自动证书管理环境)协议自动申请和续期证书,不再需要依赖外部的 certbot 脚本。
  • • OCSP 装订:ssl_stapling on; 由服务端代替客户端查询 OCSP(Online Certificate Status Protocol,在线证书状态协议)状态。注意,部分 CA(Certificate Authority,证书颁发机构)已经停止提供 OCSP 服务,例如 Let's Encrypt,对这类证书开启装订没有意义。

三、HTTP/2:一个连接,多个请求

1. 开启 HTTP/2

从 1.25.1 起,使用独立的 http2 指令开启,原来的 listen ... http2 写法已被废弃:

server {
    listen
 443 ssl;
    http2
 on;
}

同一版本还移除了 Server Push。主流浏览器已经放弃这个特性,替代方案是 103 Early Hints:1.29.0 起,early_hints 指令可以把后端返回的 103 响应转发给客户端,让浏览器提前加载关键资源。

2. HTTP/2 在 Nginx 中的实现

  • • **连接与流:**一条 TCP 连接对应一个 ngx_http_v2_connection_t。每个流(Stream)创建一个独立的 ngx_http_request_t,同时配一个「伪连接」,所以流在进入 11 个阶段之后,处理方式和 HTTP/1.1 请求完全一样,上层模块不需要感知 HTTP/2。
  • • **帧解析:**用状态机逐帧解析 HEADERS、DATA、SETTINGS、WINDOW_UPDATE 等帧;请求头经过 HPACK 解码后,还原成普通的请求头结构。
  • • **输出:**各个流产生的数据封装成帧,放入连接级别的发送队列,发送时同时受流级和连接级两层流控窗口约束。

3. 关键限制参数

指令
默认值
作用
http2_max_concurrent_streams
128
单个连接上同时存在的最大流数
keepalive_requests
1000
单个连接上允许的最大请求数,同样适用于 HTTP/2
http2_recv_buffer_size
256k
每个 Worker 的 HTTP/2 读缓冲区大小
large_client_header_buffers
4 8k
同样限制 HTTP/2 解压之后的请求头大小

💡 **安全意义:**这些限制是抵御 HTTP/2 协议层攻击的第一道防线,例如 2023 年的 Rapid Reset(CVE-2023-44487,CVE 即 Common Vulnerabilities and Exposures,通用漏洞披露编号)攻击,攻击者就是通过大量快速创建并取消流来耗尽服务端资源。不建议随意把这些参数调大。

四、HTTP/3 与 QUIC:把传输层搬到用户态

1. 开启 HTTP/3

server {
    listen
 443 ssl; # TCP:HTTP/1.1 和 HTTP/2
    listen
 443 quic reuseport; # UDP:HTTP/3
    http2
 on;
    http3
 on;
    quic_retry
 on; # 验证客户端地址,抵御伪造源地址的攻击
    add_header
 Alt-Svc 'h3=":443"; ma=86400'; # 告诉浏览器这个站点支持 HTTP/3
}

浏览器第一次通常用 TCP 访问,看到 Alt-Svc 响应头后,下一次才会尝试 HTTP/3。所以 TCP 监听必须保留,UDP 443 端口也要在防火墙和安全组上放行。

2. QUIC 在 Nginx 中的实现

QUIC 本身包含了可靠传输、拥塞控制、流量控制和 TLS 1.3 加密,这些在 TCP 中是由内核实现的,在 Nginx 中全部由 Worker 在用户态实现(ngx_event_quic 模块):

  • • **收包:**Worker 从 UDP Socket 读取数据报,根据包头中的连接 ID(Connection ID)找到对应的 QUIC 连接,而不是依赖 IP 地址和端口组成的四元组。
  • • **加解密:**每个包单独加密,使用 TLS 1.3 协商出的密钥。0-RTT 需要 OpenSSL 3.5.1 及以上版本(1.29.1 起支持),也可以使用 BoringSSL、QuicTLS 等库。
  • • **可靠性:**Worker 自己维护确认(ACK)、丢包检测、重传和拥塞控制(1.27.5 起支持 CUBIC 算法)。
  • • **流:**每个 HTTP/3 请求对应一个 QUIC 流,头部使用 QPACK 压缩,之后与 HTTP/2 一样创建 ngx_http_request_t,进入 11 个阶段处理。

3. 关键难点:数据包必须送到正确的 Worker

TCP 连接一旦建立,就固定属于 accept 它的那个 Worker。UDP 没有连接的概念,reuseport 按四元组哈希把数据包分给 Worker。问题在于,客户端的 IP 或端口一旦变化,例如 NAT(Network Address Translation,网络地址转换)重新绑定或者网络切换,数据包就可能被分到另一个 Worker,而那个 Worker 上并没有这条连接的状态。

Nginx 的解决方案是 quic_bpf on;(仅 Linux):在内核中加载一段 eBPF(extended Berkeley Packet Filter,扩展伯克利包过滤器)程序,按连接 ID 把数据包路由到正确的 Worker,从而真正支持连接迁移。

⚠️ 生产建议:HTTP/3 的实现仍在快速演进,近两年的版本中频繁修复与 QUIC 相关的安全问题,例如 1.31.0、1.31.2、1.31.6 都包含 HTTP/3 的 CVE 修复。启用 HTTP/3 时,务必跟进最新的 stable 版本,并且使用较新的 OpenSSL(3.5.1+)。

07 使用实践:配置组织、典型场景与性能调优

核心结论:用好 Nginx 的关键不在于记住多少指令,而在于三件事:配置可维护(分层组织、变更前校验)、场景有模板(静态资源、API 代理、WebSocket、限流、HTTPS 各有一套经过验证的写法)、调优有依据(先看指标和日志定位瓶颈,再改参数)。

一、配置的组织方式

推荐的目录结构:

/etc/nginx/
├── nginx.conf # 全局配置:进程、events、http 公共参数
├── conf.d/
│   ├── upstreams.conf # 所有 upstream 定义
│   └── maps.conf # 公共 map,例如 WebSocket 升级头
├── sites/
│   ├── api.example.com.conf
│   └── www.example.com.conf
└── snippets/
    ├── ssl-common.conf # TLS 公共参数
    └── proxy-common.conf # proxy_set_header、超时等公共参数

变更的基本规范:

  1. 1. 先校验,再生效:nginx -t 检查语法和文件引用,通过后再执行 reload。在 1.31.5+ 上,可以直接用 Control API 发起 reload,然后根据返回的状态码判断是否成功。
  2. 2. 查看最终生效的配置:nginx -T 会输出展开所有 include 之后的完整配置,排查「到底哪条配置生效了」时非常有用。
  3. 3. **配置纳入版本管理:**配置文件放进代码仓库,通过 MR(Merge Request,合并请求)评审后再发布,并保留回滚能力。

二、典型场景配置模板

场景 1:前端静态资源 + 单页应用

server {
    listen
 443 ssl;
    http2
 on;
    server_name
 www.example.com;
    root
 /var/www/app/dist;

    location
 / {
        try_files
 $uri $uri/ /index.html; # 前端路由统一回落到 index.html
    }
    location
 ~* .(js|css|png|jpg|svg|woff2)$ {
        expires
 30d; # 带哈希文件名的资源可以长期缓存
        add_header
 Cache-Control "public, immutable";
    }
    location
 = /index.html {
        add_header
 Cache-Control "no-cache"; # 入口文件不缓存,保证发布后立即生效
    }
}

场景 2:API 反向代理

upstream api_backend {
    zone
 api_backend 64k;
    least_conn;
    server
 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
    server
 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
    keepalive
 64;
    keepalive_timeout
 50s; # 小于后端的空闲超时
}
server
 {
    listen
 443 ssl;
    server_name
 api.example.com;
    client_max_body_size
 10m;

    location
 /api/ {
        proxy_pass
 http://api_backend;
        proxy_set_header
 Host $host;
        proxy_set_header
 X-Real-IP $remote_addr;
        proxy_set_header
 X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header
 X-Forwarded-Proto $scheme;
        proxy_connect_timeout
 3s;
        proxy_read_timeout
 30s;
        proxy_next_upstream
 error timeout;
        proxy_next_upstream_tries
 2;
    }
}

⚠️ proxy_pass 结尾的斜杠:proxy_pass http://api_backend; 会把原始 URI 原样转发给后端;proxy_pass http://api_backend/; 则会把匹配到的 location 前缀替换成 /,例如 /api/users 会变成 /users。这是最常见的「404 找不到接口」原因之一。

场景 3:WebSocket

map $http_upgrade $connection_upgrade {
    default
 upgrade;
    '' close;
}
location
 /ws/ {
    proxy_pass
 http://ws_backend;
    proxy_set_header
 Upgrade $http_upgrade;
    proxy_set_header
 Connection $connection_upgrade;
    proxy_read_timeout
 1h; # 长连接,放宽读超时;建议客户端定期发心跳
}

场景 4:限流

limit_req_zone $binary_remote_addr zone=per_ip:20m rate=20r/s;
limit_conn_zone
 $binary_remote_addr zone=conn_ip:10m;
server
 {
    location
 /api/ {
        limit_req
 zone=per_ip burst=40 nodelay; # 允许短时突发 40 个请求,超出的立即拒绝
        limit_req_status
 429; # 默认返回 503,改为语义更准确的 429
        limit_conn
 conn_ip 50;
        proxy_pass
 http://api_backend;
    }
}

limit_req 基于漏桶算法:rate 是漏水的速度,burst 是桶的容量。加上 nodelay 后,桶内的请求立即处理,不排队等待,只占用桶的额度;不加 nodelay 的话,突发请求会被按 rate 匀速放行,造成额外的延迟。

💡 真实客户端 IP:Nginx 前面还有 SLB(Server Load Balancer,负载均衡器)或 CDN 时,$remote_addr 拿到的是它们的地址,限流会误伤所有用户。需要先用 realip 模块还原真实 IP:set_real_ip_from 10.0.0.0/8; real_ip_header X-Forwarded-For; real_ip_recursive on;。

场景 5:HTTPS 公共参数

ssl_protocols TLSv1.2 TLSv1.3;
ssl_certificate
 /etc/nginx/certs/example.com.ecdsa.crt;
ssl_certificate_key
 /etc/nginx/certs/example.com.ecdsa.key;
ssl_certificate
 /etc/nginx/certs/example.com.rsa.crt; # 兼容老客户端
ssl_certificate_key
 /etc/nginx/certs/example.com.rsa.key;
ssl_session_cache
 shared:SSL:50m;
ssl_session_timeout
 1d;
add_header
 Strict-Transport-Security "max-age=31536000" always; # HSTS

其中 HSTS 是 HTTP Strict Transport Security(HTTP 严格传输安全),它要求浏览器在有效期内只通过 HTTPS 访问这个站点。

三、性能调优清单

层面
参数
推荐值
说明
进程
worker_processes
auto
等于 CPU 核数
进程
worker_rlimit_nofile
200000
要大于 worker_connections
连接
worker_connections
65535
包括到后端的连接
连接
listen ... reuseport
开启
连接在 Worker 之间分配更均衡
传输
sendfile
 / tcp_nopush / tcp_nodelay
全部 on
零拷贝,并减少小包
长连接
keepalive_timeout
30~65s
太长会占用连接,太短会增加握手
压缩
gzip_comp_level
4~6
再高收益递减,而 CPU 消耗明显上升
日志
access_log
 ... buffer=64k flush=5s
开启缓冲
减少磁盘写入次数
文件
open_file_cache
按需开启
适用于静态文件较多的场景
内核
net.core.somaxconn、net.ipv4.tcp_max_syn_backlog
65535
需要配合 listen ... backlog=65535
内核
net.ipv4.ip_local_port_range
1024 65535
扩大连接后端时可用的本地端口
内核
fs.file-max、ulimit -n
足够大
否则会出现 Too many open files

✅ **调优原则:**每次只改一个参数,改之前记录基线,改完之后用压测或线上指标验证效果。多数性能问题的根因在后端或者配置错误,不在 Nginx 的参数本身。

四、常见配置陷阱

陷阱
现象
正确做法
add_header
 在内层被覆盖
安全响应头在部分 location 丢失
在内层重复声明,或者使用 add_header_inherit merge;(1.29.3+)
add_header
 对错误响应不生效
4xx / 5xx 响应里没有 CORS(Cross-Origin Resource Sharing,跨域资源共享)头
加上 always 参数
root 与 alias 混用
返回 404,或者文件路径拼接错误
alias 会用路径替换 location 前缀,root 则是把 URI 追加在路径后面
在 location 中使用 if
除 return、rewrite 外的指令行为难以预测(官方文档称之为「If is Evil」)
if 里只放 return 和 rewrite ... last;其他需求改用 map 或谓词 location
$host
 与 $http_host
后端拿到的 Host 不带端口,或者为空
$host
 始终有值且不带端口;$http_host 是原始请求头
在 proxy_pass 中使用域名变量
报 no resolver defined 错误
必须同时配置 resolver
没有配置 client_max_body_size
上传文件时返回 413
默认只有 1m,需要按业务调整

五、可观测性:日志与指标

  1. 1. **结构化访问日志:**访问日志是定位问题的第一手资料,建议统一使用 JSON 格式,并记录后端相关的时间字段:
log_format json escape=json '{'
    '"time":"$remote_addr","host":"$host",'

    '"request":"$request","status":$status,"bytes":$body_bytes_sent,'

    '"request_time":$request_time,"upstream_addr":"$upstream_addr",'

    '"upstream_status":"$upstream_status",'

    '"upstream_connect_time":"$upstream_connect_time",'

    '"upstream_header_time":"$upstream_header_time",'

    '"upstream_response_time":"$upstream_response_time",'

    '"request_id":"$request_id","upstream_cache_status":"$upstream_cache_status"}'
;
access_log
 /var/log/nginx/access.log json buffer=64k flush=5s;
  1. 2. 基础指标(stub_status):

指标
含义
Active connections
当前的客户端连接总数,等于 Reading + Writing + Waiting
accepts / handled
累计接受的连接数和累计处理的连接数。两者不相等,说明连接池耗尽后有连接被丢弃
requests
累计请求数
Reading
正在读取请求头的连接数
Writing
正在处理请求或发送响应的连接数,持续偏高通常意味着后端变慢
Waiting
空闲的 keepalive 连接数

  1. 3. **链路追踪:**官方的 ngx_otel_module 支持 OpenTelemetry(开放遥测标准),可以把 Nginx 纳入全链路追踪体系。配合 $request_id 透传给后端,就能用同一个 ID 串起「网关 → 服务 → 数据库」的完整调用链。

08 线上故障诊断:方法论、错误速查与典型案例

核心结论:Nginx 的故障诊断遵循「先定界,再定位」:先根据状态码和日志中的时间字段,判断问题出在客户端 ↔ Nginx、Nginx 自身,还是 Nginx ↔ 后端这三段中的哪一段;再到对应的一段里用 error.log、系统指标和工具找到根因。绝大多数 5xx 问题的根因都在后端或者两端的超时配置不匹配,而不在 Nginx 本身。

一、诊断方法论:四步定界法

  1. 1. **看现象:**是哪个状态码?是全部请求还是部分请求(某个域名、某个 location、某个后端节点)?是持续出现还是偶发?从什么时间开始?期间有没有发布或者配置变更?
  2. 2. **看时间:**用访问日志中的时间字段,判断时间花在了哪一段(见下图)。
  3. 3. **看错误:**在 error.log 中按时间和 request_id 找到对应的错误信息,错误信息通常会直接说明原因。
  4. 4. **看系统:**检查 CPU、内存、连接数、文件描述符、网络丢包和重传,确认是否存在资源瓶颈。

请求处理各阶段耗时分解示意图

💡 **前提条件:**以上分析依赖日志中记录了 $request_time、$upstream_connect_time、$upstream_header_time、$upstream_response_time、$upstream_addr、$upstream_status。如果还没有记录这些字段,请先按照 07 篇第五节的模板补上。

二、状态码与错误日志速查

状态码
含义
error.log 中的典型信息
常见根因
499
客户端在收到响应之前主动断开(Nginx 自定义状态码)
client prematurely closed connection
客户端超时时间比后端处理时间短;用户主动取消;上一层代理超时
502
后端无法正常响应
connect() failed (111: Connection refused)
后端进程挂了,或者端口配置错误


upstream
 prematurely closed connection
后端崩溃;keepalive 超时时间不匹配(案例一)


no live upstreams
所有节点都被 max_fails 标记为不可用


upstream
 sent too big header
响应头超过 proxy_buffer_size
503
服务不可用
limiting requests, excess: ...
触发了 limit_req / limit_conn(默认状态码为 503)
504
等待后端超时
upstream
 timed out (110: Connection timed out) while reading response header
后端处理超过 proxy_read_timeout
413
请求体过大
client intended to send too large body
超过 client_max_body_size
400 / 414
请求头或 URI 过长
client sent too long URI 等
超过 large_client_header_buffers
—
连接被拒绝或丢弃
worker_connections
 are not enough
连接池耗尽
—
同上
accept4() failed (24: Too many open files)
文件描述符上限太低
—
连接后端失败
connect() ... (99: Cannot assign requested address)
本地端口耗尽,通常是因为没有使用 keepalive

⚠️ 注意日志中的「while ...」:error.log 的每条错误后面都带有 while connecting to upstream、while reading response header from upstream、while sending to client 等上下文,它直接说明了错误发生在哪一步,是定界最快的线索。

三、典型案例

案例一:升级到 1.30 后,偶发 502「upstream prematurely closed connection」

  • • **现象:**升级前一切正常,升级后出现低比例的 502,并且集中在流量低谷期。
  • • **分析:**1.29.7 起,upstream 默认启用 keepalive,Nginx 会复用空闲连接。后端(例如 Node.js 的默认空闲超时是 5 秒)先于 Nginx(默认 60 秒)关闭了空闲连接,Nginx 恰好在这个时刻把请求写入了一条「后端刚刚关闭」的连接。流量低时,连接空闲的时间更长,所以低谷期更容易出现。
  • • **解决:**在 upstream 中设置 keepalive_timeout,让它小于后端的空闲超时,例如后端是 5 秒,Nginx 就设为 4 秒;或者调大后端的空闲超时。对幂等请求,proxy_next_upstream error 默认会自动换一条连接重试,可以进一步掩盖这类偶发错误。

案例二:504 大量出现

  • • **现象:**某个接口大量返回 504,$upstream_header_time 接近 proxy_read_timeout 的值(例如 60 秒)。
  • • **分析:**时间消耗在「后端处理」这一段,Nginx 只是如实地等待,然后超时。应该沿着后端继续排查:线程池是否满了、数据库有没有慢查询、下游依赖是否超时。
  • • **解决:**先修复后端的问题。不要一上来就调大 proxy_read_timeout:超时时间越长,慢请求占用的连接越多,还可能引发雪崩。对确实需要长时间处理的接口,应该改成异步的方式,例如提交任务后轮询结果。

案例三:499 突然增多

  • • **现象:**499 的比例突然上升,同时 $request_time 普遍偏高。
  • • **分析:**499 是客户端「等不及」主动断开的结果,本质仍然是响应变慢,常见的情况有两种:一是后端变慢,但还没有慢到触发 Nginx 的超时;二是 Nginx 前面还有一层代理(SLB、网关、客户端 SDK(Software Development Kit,软件开发工具包)),它的超时时间比 Nginx 短。
  • • **解决:**按照请求链路对齐各层的超时,原则是越靠近客户端,超时越长。如果希望客户端断开后仍然让后端把请求处理完(例如支付回调),可以使用 proxy_ignore_client_abort on;。

案例四:reload 之后内存和进程数持续上涨

  • • **现象:**每次 reload 之后,ps 中都残留 worker process is shutting down,内存越来越高。
  • • **分析:**旧的 Worker 上还有 WebSocket、gRPC 流或者大文件下载这样的长连接,处理完之前不会退出;频繁 reload 就会不断叠加新旧两代 Worker。
  • • **解决:**配置 worker_shutdown_timeout 30s;,限制旧 Worker 的最长存活时间;降低 reload 的频率,例如通过 resolve 动态解析来代替「后端变化就 reload」;客户端需要实现断线重连。

案例五:某个 Worker 的 CPU 100%,整体延迟升高

  • • **现象:**top 中只有一两个 Worker 的 CPU 接近 100%,其他 Worker 比较空闲;延迟毛刺明显。
  • • **分析:**没有开启 reuseport 时,连接在 Worker 之间可能分配不均;或者某些请求触发了 CPU 密集型的操作(复杂正则、高级别 gzip、大量 SSL 握手)。
  • • **定位:**用 perf top -p 查看热点函数:热点集中在 pcre 就是正则的问题,集中在 deflate 就是 gzip 的问题,集中在 RSA / EC 就是握手的问题。
  • • **解决:**开启 reuseport;优化正则并开启 pcre_jit;调低压缩级别;提高 TLS 会话复用率。

案例六:连接数打满,新连接超时

  • • **现象:**客户端连接超时,error.log 出现 worker_connections are not enough,或者 Too many open files。
  • • **分析:**连接池或者文件描述符耗尽。常见原因有:后端变慢导致连接堆积;没有使用 keepalive 导致大量 TIME_WAIT;ulimit 没有生效(例如 systemd 服务需要在 unit 文件中单独配置 LimitNOFILE)。
  • • **定位:**ss -s 查看各状态的连接数;ss -lnt 查看监听 Socket 的 Recv-Q,如果它持续接近 Send-Q,说明 accept 队列已满;nstat -az | grep -i listen 查看队列溢出的计数;cat /proc//limits 确认实际生效的文件描述符上限。

四、诊断工具箱

工具
用途
示例
nginx -T
查看最终生效的完整配置
nginx -T
 | grep -n proxy_read_timeout
stub_status
实时连接状态
curl http://127.0.0.1/nginx_status
Control API(1.31.5+)
查看 Worker 进程状态和实际加载的配置
curl --unix-socket ... /1/control/processes
debug_connection
只对指定的客户端 IP 输出 debug 日志
events { debug_connection 10.0.0.5; },需要用 --with-debug 编译
ss / nstat
连接状态、队列溢出、重传
ss -tanp state time-wait | wc -l
strace
查看 Worker 卡在哪个系统调用上
strace -p -T -e trace=network,read,write
perf
定位 CPU 热点
perf top -p
curl -w
从客户端测量各阶段的耗时
curl -w '%{time_connect} %{time_starttransfer}\n' -o /dev/null -s URL

⚠️ 合规提示:在生产环境使用 tcpdump 抓包、strace / perf 跟踪进程,或者开启 debug 日志之前,应该按照公司流程申请授权,并评估这些操作对性能的影响(例如 strace 会显著拖慢被跟踪的进程)。抓包和日志中可能包含用户的敏感数据,不得外传。

五、预防:把故障挡在发生之前

  • • **监控告警:**对 5xx 比例、499 比例、$upstream_response_time 的 P99(99 分位)、Active connections、accepts 与 handled 的差值设置告警。
  • • **变更规范:**所有配置变更都要经过 nginx -t 校验和评审;先灰度到一台机器上观察,再全量发布;每次发布都保留一键回滚的能力。
  • • 容量冗余:worker_connections、文件描述符、共享内存 zone 的大小,都按峰值的 2 倍规划。
  • • **版本跟进:**关注官方的安全公告。近两年 HTTP/2、HTTP/3、rewrite 模块都出现过高危漏洞,stable 版本要及时升级。


阅读原文:原文链接​


该文章在 2026/11/11 11:19:37 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-1  粤公网安备44030602007207号