11692 words
58 minutes
Nginx 从进程模型到健康检查完全指南

本文整合了 Nginx 工作原理、HTTP 处理机制、11 个处理阶段、Gzip 压缩、健康检查等核心知识,从底层进程模型一路讲到上层配置实战,帮你建立对 Nginx 的完整心智模型。

一、Nginx 是什么#

Nginx(engine-x)是一款轻量级、高并发的 Web 服务器 / 反向代理服务器,由 Igor Sysoev 于 2002 年用 C 编写。它最显著的特征是:单机十万级并发连接、极低的内存占用、事件驱动的非阻塞架构。它能同时承担静态资源服务、反向代理、负载均衡、HTTP 缓存、TLS 终结、API 网关等多种角色,几乎成为现代 Web 架构的事实标准。

Nginx 的强大并非源于某种”魔法”,而是它对几个核心问题的极致回答:

问题Nginx 的答案
如何高效处理海量连接?Master/Worker 进程模型 + IO 多路复用(epoll/kqueue)
如何把请求分发给正确的处理逻辑?状态机解析 HTTP 头 + 11 阶段管线
如何避免雪崩、滥用?limit_conn / limit_req(漏桶算法)
如何减少带宽、加速响应?Gzip 压缩 + proxy_cache 缓存
如何剔除故障后端?upstream_check_module 健康检查

下面逐层展开。


二、Nginx 架构与工作模式#

2.1 Nginx 的整体架构#

Nginx 从架构上看是一个事件驱动的多进程模型,它并不像传统 Web 服务器(如 Apache 的 prefork 模式)那样为每个连接分配一个进程/线程,而是用少量 worker 进程配合 IO 多路复用来处理海量连接。这种设计避免了进程切换、锁竞争的代价,是 Nginx 高并发的根本来源。

从职责上,Nginx 的角色可以拆成三层:

层次职责实现方式
接入层接收 TCP 连接、TLS 握手、保持长连接master 预先 listen,worker 通过 epoll/kqueue accept
HTTP 层解析请求行/Header、location 匹配、URL 改写、限流、鉴权、生成响应11 个 phase 串起来的模块管线
代理层转发请求到后端、负载均衡、健康检查、缓存upstream + proxy_pass + proxy_cache

Nginx 架构图#

上图展示 Nginx 的整体架构:Master 管理 worker,每个 worker 通过 epoll/kqueue 处理海量连接,请求经状态机解析后流入 11 阶段管线,管线沿途访问内存池、共享内存、缓存,最终通过 upstream 转发到后端。

2.2 整体工作模式#

Nginx 启动后会发生下面几件事:

  1. master 主进程诞生:Nginx 启动后产生一个 master 主进程,它执行一系列初始化工作(读配置、建立 listen socket、设置信号处理等),然后 fork 出一个或多个 worker 工作进程;
  2. 动态请求走代理:对于动态站点请求,Nginx 将收到的 Web 请求通过代理转发至后端服务器(FastCGI/uwsgi/HTTP 等),由后端完成数据处理与组织,Nginx 只负责”搬运”;
  3. 缓存机制:Nginx 采用缓存机制,将历史应答数据缓存到本地(proxy_cache 模块),命中时直接返回缓存副本,提升响应效率并降低网络与后端压力;
  4. 事件循环:每个 worker 在一个事件循环里通过 IO 多路复用同时处理数千上万条连接,单条连接的所有阶段(读取、解析、转发、响应)都在同一个 worker 的同一个事件循环中推进。

这种”少数 worker + 多路复用 + 共享内存”的组合,使得 Nginx 在 1G 内存的单机上轻松跑到 10 万并发——而传统基于线程的模型要做到这点,光线程栈就吃掉几 GB 内存。


2.3 进程模型:master 与 worker#

Master 进程#

master 进程是”管理者”,它不处理任何网络事件,不负责业务执行。它的核心职责是:

  • 接收来自外界的信号:如 kill -HUPkill -USR2nginx -s reload 等;
  • 向各 worker 进程发送信号:把外界的控制意图传递下去;
  • 监控 worker 运行状态:当 worker 异常退出时,master 会自动重新启动一个新 worker,保证 worker 数量稳定。

控制 Nginx 只需通过 kill 向 master 发送信号即可——这就是为什么不需要停服就能 reload、滚日志、热升级。

经典的”从容重启”流程kill -HUP <master_pid>nginx -s reload):

  1. master 收到 HUP 信号后,重新加载配置文件
  2. master 启动新的 worker 进程(用新配置);
  3. master 向老 worker 发信号令其退出——但老 worker 不会立刻退出:
    • 老 worker 不再接受新的连接(不再 accept);
    • 已经在处理的请求继续处理完毕
    • 处理完所有请求后,老 worker 才真正退出。

整个过程中已建立的连接不中断、用户的请求不丢失,这就是 Nginx “zero downtime reload” 的原理。

Worker 进程#

worker 是真正干活的进程,主要功能包括:

  • 接收客户端请求:通过 epoll/kqueue 拿到连接,读取字节流;
  • 请求过滤处理:将请求依次送入各功能模块(11 个 phase)做过滤处理;
  • 与后端服务器通信:通过 proxy_passfastcgi_pass 等把请求转发给后端,并接收后端处理结果;
  • 数据缓存:通过 proxy_cache 模块把后端响应缓存到本地;
  • 响应客户端:把响应数据写回 TCP 连接。

核心原则:一个请求完全由一个 worker 进程处理,且只在一个 worker 进程中处理。

这条原则意味着:请求从建立连接、读取 Header、解析、转发后端、接收响应、写回客户端,整个生命周期都在同一个 worker 里完成,不会在 worker 之间迁移。这样的好处是:① 没有 worker 之间的锁竞争;② 请求上下文可以放在 worker 私有的内存池里,不需要跨进程同步。

Worker 之间如何”公平”地接受连接?#

worker 进程之间是平等的,都由 master 进程 fork 而来。master 在 fork 之前就已经建立了需要 listen 的 socket(listenfd),所以 fork 之后所有 worker 都继承了同一个 listenfd。当新连接到来时,所有 worker 的 listenfd 都会变为”可读”。

如果所有 worker 都去 accept(),会出现”惊群”问题——多个 worker 被唤醒去抢同一个连接,最终只有一个成功,其他被白白唤醒。为避免这种情况,Nginx 让所有 worker 在注册 listenfd 读事件前抢占 accept_mutex 互斥锁

  • 抢到锁的 worker:注册 listenfd 读事件,在读事件中调用 accept() 接受连接。之后该 worker 完成读取请求、解析、处理、返回数据、断开连接的全流程;
  • 没抢到锁的 worker:这次没机会接受新连接,等下一轮 epoll_wait 再尝试抢锁。

这种”惊群避免”机制使得:① 只有一个 worker 被唤醒处理新连接;② worker 之间负载天然均衡(每个 worker 抢锁的机会均等)。

现代 Nginx 在大多数 Linux 上默认 accept_mutex off 也能工作得很好(内核已不再向所有进程派发同一连接),但在某些场景下 accept_mutex on 仍能减少唤醒次数。可通过 events { accept_mutex on; } 显式开启。


三、IO 多路复用模型#

Nginx 之所以能用少量 worker 处理海量连接,关键在于 IO 多路复用:让内核替进程监视多个文件描述符(socket),哪一个就绪了就通知进程去处理。Nginx 支持 selectpollepollkqueue,编译时根据平台自动选最优实现。

核心理念:一个进程可以同时监视多个描述符(socket),一旦某个 IO 条件就绪(如可读、可写),内核立即通知进程。这样就不需要为每个连接开一个线程,单进程就能处理上万连接。

3.1 select#

原理:select 监视的文件描述符分为三类:writefds(可写集)、readfds(可读集)、exceptfds(异常集)。调用 select 后进程阻塞,直到有描述符就绪或超时。返回后,进程需要遍历整个 fdset 找到就绪的描述符。

优点:几乎所有的平台都支持,是可移植性最好的多路复用机制。

缺点

  • 单个进程打开的 FD 数量受 FD_SETSIZE 限制:默认 1024(32 位机),64 位机默认 2048。这直接限死了 select 能监听的连接数上限;
  • 对 socket 是线性扫描(轮询):随着 fd 数量增加,每次 select 返回都要遍历整个集合,效率急剧下降;
  • 用户空间与内核空间之间传递 fd 数据结构时复制开销大:每次调用都要把整个 fdset 从用户态拷到内核态,返回时再拷回来,不论这些 fd 是否就绪。

3.2 poll#

原理:与 select 本质相同,但用链表(而非位图)存储 fd,因此没有最大连接数限制。调用时把用户传入的 fd 数组拷贝到内核空间,遍历查询每个 fd 的设备状态;若遍历完没有就绪设备则挂起当前进程,直到就绪或超时;被唤醒后再次遍历。

优点:基于链表存储,突破了 FD_SETSIZE 的限制。

缺点

  • 大量 fd 数组在用户态与内核态间整体复制:每次调用都要拷贝整个数组,不论是否有意义;
  • 水平触发(LT):如果 fd 被报告就绪后未被处理,下一次 poll 会再次报告它,可能导致重复唤醒;
  • 仍然是轮询:性能仍然随 fd 数量增加而下降,没有从根本上解决问题。

3.3 epoll(Linux 2.6+,Nginx 在 Linux 上的首选)#

epoll 是 select 和 poll 的增强版本,由 Linux 2.6 内核引入,是 Nginx 在 Linux 平台上的默认选择。

原理:epoll 使用一个文件描述符管理多个描述符,把用户关心的 fd 事件存入内核事件表(而不是每次调用都重新传入),用户空间与内核空间之间仅需一次 copy。注册时通过 epoll_ctl 把 fd 加入事件表;fd 就绪时,内核采用类似 callback 的回调机制激活该 fd,epoll_wait 即可收到通知。这样就不需要像 select/poll 那样遍历整个集合——内核只把就绪的 fd 通知给你。

两种触发模式

模式特点应用注意
LT(水平触发,默认)事件通知后应用程序可暂不处理,下次 epoll_wait再次通知编程简单,但若不处理会反复唤醒
ET(边缘触发)事件通知后应用程序必须立即处理,否则下次不再通知,且只通知一次必须配合非阻塞 IO 一次性读完数据,编程复杂但效率最高

优点

  • 无最大并发连接限制:1G 内存大约可监听 10 万个端口;
  • 非轮询方式:只处理活跃连接,效率不会随 fd 数目增加而下降(select/poll 是 O(n),epoll 是 O(1));
  • 利用 mmap() 内存映射减少内核空间与用户空间的消息传递开销——fd 事件表在内核与用户态之间共享同一段物理内存,不需要拷贝。

Nginx 与 epoll 的配合:Nginx worker 在 epoll_wait 上阻塞,内核一旦有 fd 就绪就唤醒 worker;worker 处理完一批就绪事件后再回到 epoll_wait。这就是单 worker 能处理上万连接的全部秘密——绝大多数 fd 在任意时刻都是空闲的,epoll 只把”有事可做”的那些 fd 通知给 worker。

3.4 kqueue#

kqueue 与 epoll 思路类似,由 Jonathan Lemon 在 2000 年于 FreeBSD 上开发,是 FreeBSD/macOS 上的高性能事件通知接口。注册 socket 描述符后,状态变化时 kqueue 一次性通知哪些描述符可读、可写或出错。Nginx 在 FreeBSD/macOS 平台默认使用 kqueue。它的可移植性比 epoll 差(适应平台较少),但在 FreeBSD 上的性能与 epoll 在 Linux 上的性能相当。

3.5 四种模型对比#

模型平台最大连接触发模式性能随 fd 增长复制开销
select几乎全平台FD_SETSIZE(1024/2048)LTO(n) 下降整体拷贝
poll几乎全平台无限制LTO(n) 下降整体拷贝
epollLinux 2.6+无限制(约 10 万/GB)LT/ETO(1) 不下降mmap 共享
kqueueFreeBSD/macOS无限制LT/ETO(1) 不下降mmap 共享

四、HTTP 头部处理机制与内存池#

理解了”worker 怎么接收连接”,下一步要回答”worker 怎么把字节流变成结构化的 HTTP 请求”。Nginx 在处理 HTTP 请求前,先与客户端建立连接,然后接收请求行(方法、URL 等),再接收所有 Header,根据 Header 信息决定由哪些 HTTP 模块处理请求。

4.1 整体处理流程#

  1. 三次握手完成:内核完成 TCP 连接建立后,通过负载均衡算法(在 Linux 上是内核层面的 SO_REUSEPORT 或 Nginx 的 accept_mutex)选出一个 worker 进程;
  2. worker 接受连接:该 worker 进程通过 epoll_wait 获取到连接句柄,调用 accept() 方法建立连接,并为该连接分配连接内存池
  3. 设置回调:HTTP 模块启动时调用 ngx_http_init_connection 设置回调方法,将新连接的读事件通过 epoll_ctl 加入 epoll,并加上超时定时器(防止客户端建了连接不发数据);
  4. 读取请求字节流:用户请求到达后,事件模块的 epoll_wait 获取到该请求,调用回调方法 ngx_http_wait_request_handler 把数据从内核态读到用户态;
  5. 状态机解析:Nginx 用状态机依次解析请求行所有 Header,根据 Header 信息确定由哪个 server 块、哪个 location 处理;
  6. 进入 11 阶段处理:解析完成后进入核心的 11 个阶段 HTTP 请求处理管线(见第五章)。

4.2 接收请求事件模块:连接建立与连接内存池#

连接内存池#

每个新连接都会分配一个连接内存池,初始大小由 connection_pool_size 控制。这个内存池是 Nginx 内存管理的核心——所有该连接相关的内存分配都从池里取,连接关闭时整池释放,避免了 malloc/free 的碎片开销。

配置指令默认值说明
connection_pool_size512 字节连接内存池初始大小
client_header_buffer_size1k默认从连接内存池中分配;即使用户只发 1 字节,也会分配 1k 内存
client_header_timeout60s若 60 秒内未收到客户端请求,则断开连接

注意 client_header_buffer_size 的”按块分配”特性:哪怕请求只有 100 字节,Nginx 也会从内存池中切出整块 1k。这种”宁多勿少”的策略是为了避免反复 realloc,也方便后续 Header 解析状态机连续推进。

回调设置#

HTTP 模块启动时调用 ngx_http_init_connection 完成回调方法的设置:

  • 将新连接的读事件通过 epoll_ctl 加入 epoll 监听;
  • 同时给该连接加一个超时定时器(默认 60s)。

如果客户端建了连接但在 60 秒内没发任何数据,定时器触发,Nginx 主动关闭连接,防止空连接占用资源。

4.3 接收请求 HTTP 模块:请求内存池与大 Header 缓冲#

当请求字节流到达后,worker 调用 ngx_http_wait_request_handler 把数据读到用户态,进入 HTTP 模块的处理范围。这时需要分配新的内存池来处理请求上下文。

请求内存池#

配置指令默认值说明
request_pool_size4k专门用于处理请求的上下文分析

请求内存池和连接内存池是分开的:连接内存池用于 socket 层面的资源,请求内存池用于 HTTP 处理层面的资源。一条连接可以复用(HTTP keep-alive),但每个请求都有自己的请求内存池。

大 Header 缓冲:large_client_header_buffers#

正常情况下 1k 的 client_header_buffer_size 够用,但如果请求行特别长(比如超长 URL)或 Header 特别多(比如带大量 Cookie),1k 就不够了。这时启用大 Header 缓冲:

配置指令默认值说明
large_client_header_buffers4 8k最多分配 4 个 8k 缓冲区,请求行和 Header 共用这 4×8k=32k 的配额

分配策略的关键点:Nginx 不是一次性分配 32k,而是按需渐进分配

  1. 先用 1k 的 client_header_buffer_size 解析请求行;
  2. 不够 → 分配第一个 8k 大缓冲区,继续解析;
  3. 还不够 → 分配第二个 8k;
  4. 最多到 4 个 8k(32k)为止,超过就返回 414 Request-URI Too Large 或 400 Bad Request。

重要约束:请求行和 Header 共用这 4 个 8k 的配额——不是请求行单独 32k、Header 又有 32k。这意味着如果你有一个超长 URL,留给 Header 的空间就会变小。

4.4 状态机工作流#

Nginx 用状态机逐字节解析 HTTP 请求,整个过程:

  1. 分配请求内存池request_pool_size: 4k
  2. 解析请求行:状态机开始逐字节解析请求行(method、URI、version);
  3. URL 超长则扩容:若 URL 超过 4k(请求内存池装不下),从连接内存池分配 large_client_header_buffers 指定的大缓冲区;
  4. 标识 URI:解析完成后,Nginx 用指针引用 URI 各部分(零拷贝——不复制字符串,只记指针和长度,效率极高);
  5. 解析 Header:继续逐行解析 Header,若内存不够则继续从大缓冲区分配(但受限于 4×8k 的总上限);
  6. 标识 Header,确定 server 块:根据 Host Header 等信息确定由哪个 server 块处理请求;
  7. 移除超时定时器:请求已就绪,不再需要 client_header_timeout 的保护;
  8. 进入 11 阶段处理:把结构化后的请求交给 HTTP 处理管线。

4.5 关键内存配置总结#

池/缓冲大小用途
连接内存池初始 512Bsocket 层面资源,每连接一个
client_header_buffer_size1k(从连接池分配)第一块请求行/Header 解析缓冲
large_client_header_buffers4×8k(从连接池分配)大请求行/Header 的扩展缓冲
请求内存池4kHTTP 处理上下文,每请求一个

关键认知:Nginx 对内存极其”吝啬”——所有解析都在小内存池里以状态机方式推进,能用指针就不复制字符串,能按需扩容就不预分配。这种”省着用”的设计是它单机十万并发的根因之一:内存占用极低,每个空闲连接几乎不耗内存,所以能同时挂着十几万条连接。


五、HTTP 请求处理的 11 个阶段#

这是 Nginx 最核心、也是最容易”知其然而不知其所以然”的部分。请求进入后,并不会一股脑丢给某个 handler,而是依次流过 11 个阶段(phase),每个阶段挂载若干模块。这种”管线化”设计的好处是:① 模块职责单一、可插拔;② 阶段顺序固定,便于排查问题;③ 模块之间通过请求上下文传递数据,解耦清晰。

11 阶段流程图#

上图按 4 个泳道展示 11 个阶段的执行顺序:1-3 接入与匹配 → 4-5 URL 改写 → 6-8 限流鉴权 → 9-11 内容生成与日志。每个阶段的模块按源码顺序执行,某模块不向下传递则后续模块收不到请求。

5.0 宏观处理流程(7 步)#

在展开 11 个阶段之前,先看 Nginx 处理一个 HTTP 请求的 7 个宏观步骤,11 个阶段是这 7 步的精细化拆分:

  1. Read Request Headers(解析请求头):从 TCP 流中读取并解析请求行 + 所有 Header,由第四章的状态机完成;
  2. Identify Configuration Block(识别配置块):根据 Host + URI 匹配到唯一的 server 块和 location 块,决定后续由谁处理;
  3. Apply Rate Limits(应用限速):判断是否触发限速,包括并发连接数限制(limit_conn)和单 IP QPS 限制(limit_req);
  4. Perform Authentication(执行鉴权):连接控制与权限验证,如 IP 黑白名单、HTTP Basic 认证、Referer 防盗链、子请求鉴权等;
  5. Generate Content(生成响应):生成响应内容,可能是读静态文件、列目录、转发到后端(proxy_pass)、子请求(subrequest)或重定向;
  6. Response Filters(响应过滤):对响应做后处理,如 Gzip 压缩、图片处理、Header 改写等;
  7. Log(记日志):把这次请求记录到 access_log,便于后续审计与分析。

理解了这 7 步,11 个阶段就是它的”展开版”——把每一步拆得更细,方便模块挂载。

阶段总览#

#阶段名主要模块职责
1POST_READrealip获取客户端真实 IP
2SERVER_REWRITErewriteserver 块内的 URL 改写
3FIND_CONFIG(内部)location 匹配
4REWRITErewritelocation 内的 URL 改写
5POST_REWRITE(内部)改写后跳转判断
6PREACCESSlimit_conn、limit_req并发/QPS 限流
7ACCESSaccess、auth_basic、auth_request鉴权
8POST_ACCESS(内部)配合 satisfy
9PRECONTENTtry_files、mirror内容前处理
10CONTENTstatic、index、autoindex、concat生成响应
11LOGlog记日志,不可禁用

5.1 POST_READ 阶段:realip 模块#

刚读完 Header、尚未做任何处理时执行。核心模块是 ngx_http_realip_module默认不编译进 Nginx,需 --with-http_realip_module 启用

为什么需要 realip?#

当 Nginx 前面有 CDN、负载均衡器或上层代理时,TCP 连接的对端是上层代理而不是终端用户,$remote_addr 取到的是代理的 IP,而不是用户真实 IP。这会导致两个问题:① 限流(limit_req/limit_conn)会针对代理 IP 而非用户 IP,等于把所有用户当成一个人;② 日志里看不到真实用户 IP,审计和风控失效。

realip 模块就是为了解决这个问题:从 HTTP Header 中取出真实 IP,改写 $remote_addr 变量,让后续所有阶段都拿到的是用户真实 IP。

两个相关 Header 字段#

  • X-Forwarded-For:标准的”代理链”字段,格式为 client, proxy1, proxy2, ...,逗号分隔;每一跳代理会把自己的上一跳 IP 追加到末尾;
  • X-Real-IP:Nginx 独有的字段,非 RFC 规范,只记录单一真实 IP。如果中间有非 Nginx 代理,可能取不到这个 Header。

指令#

set_real_ip_from 192.168.0.108; # 信任的上游代理地址
real_ip_header X-Forwarded-For; # 从哪个 Header 取真实 IP
real_ip_recursive on; # 是否从尾部递归过滤可信地址

set_real_ip_from 指定哪些地址是可信的上游代理——只有可信代理发来的 Header 才会被采信,否则任意用户伪造一个 X-Forwarded-For: 1.1.1.1 就能骗过 Nginx。

real_ip_recursive 控制过滤策略:开启后从 X-Forwarded-For尾部开始,逐个剔除可信地址,剩下的最后一个就是真实 IP——这能正确处理多层代理链的场景。

关键变量#

$realip_remote_addr / $realip_remote_port 保存的是原始 TCP 连接的对端地址,即使 $remote_addr 被改写过,也能通过这俩变量拿到原始代理 IP,用于审计。

示例:

server {
listen 80;
server_name ziyang.realip.com;
set_real_ip_from 192.168.0.108;
real_ip_recursive off;
real_ip_header X-Forwarded-For;
location / {
return 200 "Client real ip: $remote_addr\n";
}
}

测试:

Terminal window
curl -H 'X-Forwarded-For: 1.1.1.1,192.168.0.108' ziyang.realip.com
# → Client real ip: 192.168.0.108

real_ip_recursive 改为 on 后:

Terminal window
curl -H 'X-Forwarded-For: 1.1.1.1,2.2.2.2,192.168.0.108' ziyang.realip.com
# → Client real ip: 2.2.2.2 (从尾部过滤掉可信的 192.168.0.108,取剩下的最后一个)

5.2 SERVER_REWRITE 与 5.5 POST_REWRITE#

涉及 rewrite 模块,前者在 server 块内执行,后者用于改写后判断是否要重新跳到 FIND_CONFIG。一般不写第三方模块挂这两个阶段。


5.3 FIND_CONFIG 阶段:location 匹配#

这是内部阶段,做 location 匹配——把请求 URI 映射到具体的 location 块。location 匹配是 Nginx 配置中最容易出错的部分,因为它涉及前缀匹配、正则匹配、精确匹配三种策略的优先级组合。

location 指令语法#

location [ = | ~ | ~* | ^~ ] uri { ... }
location @name { ... }

三类匹配规则#

  1. 前缀字符串匹配:常规匹配(无修饰符)、=(精确匹配,URI 必须完全相等)、^~(匹配后不再走正则,优先于正则);
  2. 正则表达式匹配~(大小写敏感)、~*(大小写不敏感),按配置文件中的出现顺序依次匹配;
  3. 命名 location@name,不参与外部匹配,仅用于内部跳转(如 error_pagetry_files 跳转)。

匹配优先级流程(关键)#

Nginx 的 location 匹配顺序是反直觉的——不是”先匹配到先赢”,而是:

  1. 先遍历所有前缀字符串,选出最长匹配的那一个;若该最长前缀带有 =^~ 修饰符,则直接使用并终止后续匹配;
  2. 否则记住最长前缀 location(作为后备),按 nginx.conf 配置顺序依次匹配正则表达式;
  3. 正则一旦命中即使用,不再继续往下匹配
  4. 所有正则都没命中,回退到第 2 步记住的最长前缀 location。

这个流程的关键点在于:① 前缀匹配选最长的,与配置顺序无关;② 正则匹配按配置顺序,先到先得;③ =^~ 能”截断”正则匹配流程。

实战配置#

server {
listen 80;
server_name location.ziyang.com;
merge_slashes off;
location ~ /Test1/$ {
return 200 'first regular expressions match!\n';
}
location ~* /Test1/(\w+)$ {
return 200 'longest regular expressions match!\n';
}
location ^~ /Test1/ {
return 200 'stop regular expressions match!\n';
}
location /Test1/Test2 {
return 200 'longest prefix string match!\n';
}
location /Test1 {
return 200 'prefix string match!\n';
}
location = /Test1 {
return 200 'exact match!\n';
}
}
请求 URL响应命中 location
/Test1exact match!= /Test1(精确优先)
/Test1/stop regular expressions match!^~ /Test1/(最长前缀且 ^~,跳过正则)
/Test1/Test2longest regular expressions match!~* /Test1/(\w+)$(前缀最长但非 =/^~,正则命中)
/Test1/Test2/longest prefix string match!/Test1/Test2(正则都不命中,回退最长前缀)
/Test1/Test3stop regular expressions match!^~ /Test1/(同上,跳过正则)

merge_slashes 默认 on,会把 URL 中连续的 / 合并成一个;改 off 可保留路径中的双斜杠语义。


5.4 REWRITE 阶段:rewrite 模块#

5.4.1 return 指令#

return code [text];
return code URL;
return URL;

return 指令直接终止当前请求处理,返回指定状态码或重定向。可返回的状态码类型:

状态码含义协议
444Nginx 自定义,立即关闭连接,不给客户端任何响应Nginx 独有
301永久重定向,浏览器会缓存,下次直接跳转HTTP/1.0
302临时重定向,禁止缓存,浏览器可能改方法(POST→GET)HTTP/1.0
303临时重定向,允许改方法(POST→GET),禁止缓存HTTP/1.1
307临时重定向,不允许改方法(保持 POST)HTTP/1.1
308永久重定向,不允许改方法(保持 POST)HTTP/1.1

301 vs 308 / 302 vs 307 的区别:HTTP/1.0 时代的 301/302 允许浏览器把 POST 改成 GET 重发,这在表单提交场景会导致”重复提交”问题;HTTP/1.1 引入 307/308 强制保持原方法,更安全。现代配置推荐用 308/307 替代 301/302,避免方法被改写。

5.4.2 error_page 指令#

error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
error_page 404 =200 /empty.gif; # 把 404 改写为 200 响应
error_page 404 = /404.php;
error_page 404 = @fallback; # 跳到命名 location
error_page 403 http://example.com/forbidden.html;
error_page 404 =301 http://example.com/notfound.html;

关键结论:server 块的 error_page 与 location 块的 return 不会合并,谁先执行看阶段;location 里的 return 属于 REWRITE 阶段,会先于 ACCESS 等后续阶段生效。

5.4.3 rewrite 指令#

rewrite regex replacement [flag];

将 regex 匹配的 URL 替换为 replacement;replacement 以 http://https://$schema 开头时直接返回 302。

flag 参数:

Flag含义
last用 replacement URL 进行新的 location 匹配
break停止当前脚本指令执行(不再走 rewrite)
redirect返回 302
permanent返回 301

实战:

server {
listen 80;
server_name rewrite.ziyang.com;
rewrite_log on;
error_log logs/rewrite_error.log notice;
location /first {
rewrite /first(.*) /second$1 last;
return 200 'first!\n';
}
location /second {
rewrite /second(.*) /third$1; # 无 flag
return 200 'second!\n';
}
location /third {
return 200 'third!\n';
}
location /redirect1 {
rewrite /redirect1(.*) $1 permanent; # → 301
}
location /redirect2 {
rewrite /redirect2(.*) $1 redirect; # → 302
}
location /redirect3 {
rewrite /redirect3(.*) http://rewrite.ziyang.com$1; # → 302
}
location /redirect4 {
rewrite /redirect4(.*) http://rewrite.ziyang.com$1 permanent; # → 301
}
}

测试 curl rewrite.ziyang.com/first/3.txt

  • 默认(/second 中 rewrite 无 flag)→ 返回 second!
  • /second 的 rewrite 加 break → 返回 test3(继续走完 break 后命中 /third)。

5.4.4 if 指令#

if (condition) { ... }

condition 支持的判断:

  1. 变量为空或为 0;
  2. = / != 字符串匹配;
  3. ~ / !~(大小写敏感)、~* / !~*(大小写不敏感)正则匹配;
  4. -f / !-f 文件存在;
  5. -d / !-d 目录存在;
  6. -e / !-e 文件/目录/软链接存在;
  7. -x / !-x 可执行文件。

示例:

if ($http_user_agent ~ MSIE) {
rewrite ^(.*)$ /msie/$1 break;
}
if ($http_cookie ~* "id=([^;]+)(?:;|$)") {
set $id $1;
}
if ($request_method = POST) {
return 405;
}
if ($invalid_referer) {
return 403;
}

5.4.5 rewrite_log#

rewrite_log on | off;

开启后 rewrite 日志写入 logs/rewrite_error.log,调试神器。


5.6 PREACCESS 阶段:限流#

PREACCESS 阶段在 ACCESS 之前执行,做两件事:限制并发连接数和限制请求速率。这两个限制都基于共享内存——所有 worker 进程共享同一份计数器,所以限流是跨 worker 全局生效的,不会因为请求落到不同 worker 而失效。

5.6.1 limit_conn 模块#

限制并发连接数。基于共享内存,对所有 worker 生效。它的有效性依赖于前置的 realip 模块——如果 realip 没启用,$binary_remote_addr 取到的是上层代理 IP,限流就成了针对代理 IP 的限流,所有用户被当成一个人。

limit_conn 适合防止”瞬时大量连接打爆后端”的场景,比如恶意爬虫并发开几十条连接拉取页面。

limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
location / {
limit_conn_status 500; # 超限返回码,默认 503
limit_conn_log_level warn; # 超限日志级别,默认 error
limit_conn addr 1; # 每 IP 并发连接上限 1
limit_rate 50; # 每秒 50 字节
}
}

5.6.2 limit_req 模块#

基于漏桶算法(Leaky Bucket):想象一个固定容量的桶,请求像水一样倒入桶中,桶底以恒定速率漏出(被处理);如果倒水速度超过漏水速度,多余的水会溢出(被拒绝)。漏桶算法的特点是输出速率恒定,不管输入多突发,后端收到的请求速率永远是匀速的。

这与”令牌桶”算法不同——令牌桶允许突发(桶里存了令牌就可以瞬间处理一批),漏桶不允许突发,只能匀速输出。

rate=2r/m 表示每分钟 2 个请求的漏水速率(即每 30 秒处理 1 个)。burst 参数控制桶的容量——超出速率的请求可以排队等待,最多排 burst 个;nodelay 让排队的请求立即处理而不是等待。

limit_req_zone $binary_remote_addr zone=one:10m rate=2r/m;

rate=2r/m 表示每分钟 2 个请求的速率。

location / {
limit_req zone=one; # 严格按速率,超出立即 503
limit_req zone=one burst=3; # 允许 3 个排队等待
limit_req zone=one burst=3 nodelay; # 3 个立即处理,超出 503
}

三种行为的差异:

配置行为
limit_req zone=one超出速率立即 503
limit_req zone=one burst=3突发 3 个请求排队等待处理
limit_req zone=one burst=3 nodelay突发 3 个立即处理,超出再 503

重要limit_req 阶段在 limit_conn 之前执行,所以两个一起配时,req 优先级更高。


5.7 ACCESS 阶段:鉴权#

ACCESS 阶段解决”用户能否访问”的问题。Nginx 在这一阶段挂了三个模块:access(IP 黑白名单)、auth_basic(HTTP Basic 认证)、auth_request(子请求鉴权)。这三个模块的执行顺序由源码决定(access → auth_basic → auth_request),与配置文件里的指令顺序无关——这是一个容易踩坑的点。

5.7.1 access 模块(IP 黑白名单)#

location / {
deny 192.168.1.1;
allow 192.168.1.0/24;
allow 10.1.1.0/16;
allow 2001:0db8::/32;
deny all;
}

顺序执行,命中一条就停止。

5.7.2 auth_basic 模块(HTTP Basic 认证)#

基于 RFC2617 的 HTTP Basic Authentication 协议:浏览器弹框让用户输入用户名密码,浏览器把它们以 user:pass 形式 Base64 编码后放在 Authorization: Basic ... Header 里发给服务器。Nginx 解码后与本地密码文件比对,匹配则放行,不匹配返回 401 + WWW-Authenticate 头让浏览器再弹框。

注意 Base64 不是加密,等于明文传输,必须配合 HTTPS 使用,否则密码会被中间人嗅探。默认编译进 Nginx,可通过 --without-http_auth_basic_module 禁用。

location /auth_basic {
satisfy any;
auth_basic "test auth_basic";
auth_basic_user_file example/auth.pass;
deny all;
}

用 htpasswd 生成密码文件(依赖 httpd-tools):

Terminal window
htpasswd -bc auth.pass ziyang 123456

文件格式:

name1:password1
name2:password2:comment

5.7.3 auth_request 模块(子请求鉴权)#

上游服务转发请求做鉴权:Nginx 生成一个子请求发到指定的内部 location,该 location 通过 proxy_pass 转发到上游鉴权服务;上游返回 2xx 则原请求继续执行,返回 401/403 则把该状态响应给客户端。

这种模式的好处是:把鉴权逻辑完全交给后端服务实现,Nginx 只做”转发 + 判断状态码”,可以做复杂的权限校验(如 JWT 验证、API token 校验、多租户权限检查)。默认不编译,需 --with-http_auth_request_module

location / {
auth_request /test_auth;
}
location = /test_auth {
internal;
proxy_pass http://127.0.0.1:8090/auth_upstream;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}

5.7.4 satisfy 指令#

satisfy all | any;
  • all:所有 ACCESS 阶段模块必须全部通过;
  • any:任一通过即可。

对 access、auth_basic、auth_request 三个模块都生效。

几个关键认知:

  1. return 属于 REWRITE 阶段,先于 ACCESS,因此 location 里写了 return 后 ACCESS 阶段不再执行;
  2. 多个 ACCESS 模块的执行顺序由源码 ngx_module_names 数组决定,与配置文件中的指令顺序无关
  3. satisfy any + auth_basic + deny all:输对密码即可访问;
  4. allow all 会使 auth_basic 没机会执行(access 模块先于 auth_basic)。

5.9 PRECONTENT 阶段#

PRECONTENT 阶段在生成响应(CONTENT)之前执行,做”内容前处理”。两个核心模块:try_files(试文件)和 mirror(流量镜像)。

5.9.1 try_files 模块#

依次尝试一组文件,命中则返回,都不命中则跳到最后一个 URI(命名 location 或内部跳转)或返回指定 code。常用于:① 维护页面切换——把 /system/maintenance.html 放在第一位,维护时上传该文件即可一键切流;② 静态资源 fallback——先找 $uri,找不到再找 $uri/index.html$uri.html,都不存在再返回 404 或转给后端。

location /first {
try_files /system/maintenance.html
$uri $uri/index.html $uri.html
@lasturl;
}
location @lasturl {
return 200 'lasturl!\n';
}
location /second {
try_files $uri $uri/index.html $uri.html =404;
}

5.9.2 mirror 模块#

流量镜像:处理真实请求的同时,生成子请求复制到其他服务,对子请求返回值不做处理。常用于线上流量预演、新版本灰度验证。

location / {
mirror /mirror;
mirror_request_body off;
}
location = /mirror {
internal;
proxy_pass http://127.0.0.1:10020$request_uri;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}

5.10 CONTENT 阶段#

CONTENT 阶段是模块最多的阶段,负责生成响应内容。挂载的模块有 static(读静态文件)、index(目录默认页)、autoindex(列目录)、concat(合并文件)。当某个模块命中并生成响应后,后续模块不再执行——比如 index 命中了 index.html,autoindex 就不会再列目录。

5.10.1 static 模块:root 与 alias#

alias path; # location 后的 URI 映射到 path
root path; # 完整 URI 映射到 path

两者的差别:root 把完整 URL 拼到 path 后;alias 只把 location 之后的 URL 拼到 path 后。这是 Nginx 配置最容易踩的坑之一:

  • root 是”在 path 末尾追加完整 URI”——location /root { root html; } 访问 /root/1.txt 会去找 html/root/1.txt
  • alias 是”把 location 部分替换成 path”——location /alias { alias html; } 访问 /alias/1.txt 会去找 html/1.txt/alias 被替换成 html)。

经验法则:location 与文件路径有”前缀替换”关系时用 alias;location 就是文件路径的一部分时用 root。正则 location 里几乎总是用 alias,因为正则匹配的 URI 需要被替换掉。

location /root {
root html; # /root/1.txt → html/root/1.txt(不存在)
}
location /alias {
alias html; # /alias/1.txt → html/1.txt(存在)
}
location ~ /root/(\w+\.txt) {
root html/first/$1; # /root/1.txt → html/first/1.txt/root/1.txt(不存在)
}
location ~ /alias/(\w+\.txt) {
alias html/first/$1; # /alias/1.txt → html/first/1.txt(存在)
}

5.10.2 三个相关变量#

变量含义
$request_filename待访问文件的完整路径
$document_root由 URI 和 root/alias 生成的文件夹路径(可能含软链接)
$realpath_root$document_root 中的软链接替换为真实路径

5.10.3 目录重定向相关#

访问目录不带 / 时,static 模块返回 301 加 /

server_name_in_redirect on; # 用 server_name 拼跳转地址
port_in_redirect on; # 拼端口
absolute_redirect on; # 拼成绝对 URL

5.10.4 index 与 autoindex#

index index.html;
autoindex on; # index 不存在时列出目录
autoindex_exact_size on; # 显示精确字节数
autoindex_format html; # html / xml / json / jsonp
autoindex_localtime on; # 用本地时间

index 模块先于 autoindex 执行:index 文件存在 → 显示文件;不存在 → 走 autoindex 列目录。

5.10.5 concat 模块(Tengine)#

阿里巴巴开发,合并多个小文件请求,减少 HTTP 请求数。需 --add-module=../nginx-http-concat/ 编译。

server {
concat on;
concat_max_files 20;
concat_types text/plain;
concat_unique on;
concat_delimiter ':::';
concat_ignore_file_error on;
}

请求形如 /??a.js,b.js?ver=1 可一次拉回合并后的内容。


5.11 LOG 阶段#

对应 ngx_http_log_module无法禁用

默认 combined 格式#

log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';

access_log 配置#

access_log path [format [buffer=size] [gzip[=level]] [flush=time] [if=condition]];
access_log off;

特性:

  • path 可含变量,但每条日志都需打开/关闭文件,性能差;
  • if 通过变量值控制是否记录;
  • 日志缓存:批量写盘,触发条件为缓存满、flush 到期、worker reopen 或关闭;
  • 日志压缩:buffer 默认 64KB,gzip 级别默认 1(1 最快压缩率最低,9 最慢压缩率最高),开启压缩自动启用缓存。

含变量日志路径的优化#

open_log_file_cache max=1000 inactive=20s min_uses=2 valid=1m;
  • max:缓存最大文件句柄数,超出 LRU 淘汰;
  • inactive:多久未访问则关闭,默认 10s;
  • min_uses:inactive 内使用次数达到此值才留在内存;
  • valid:多久校验文件是否还存在,默认 60s。

模块执行顺序的源码视角#

同一阶段内多个模块的执行顺序,由源码 ngx_module.cngx_module_names 数组的逆序决定。关键模块顺序(从早到晚):

ngx_http_realip_module
ngx_http_limit_req_module
ngx_http_limit_conn_module
ngx_http_access_module
ngx_http_auth_basic_module
ngx_http_auth_request_module
ngx_http_try_files_module
ngx_http_mirror_module
ngx_http_index_module
ngx_http_autoindex_module
ngx_http_static_module
ngx_http_concat_module

这就是为什么 limit_req 先于 limit_connaccess 先于 auth_basic。某模块不向下传递时,后续模块都收不到请求——例如 index 命中后 autoindex 就不会执行。


六、Gzip 压缩配置#

带宽是钱,压缩是省钱。Nginx 开启 Gzip 压缩后,对 css、js、xml、html 等静态文本资源在传输前先做压缩,页面大小可缩减至原来的 30% 甚至更小,从而节约大量出口带宽、提高传输效率,同时提升用户感知体验(小文件下载更快)。代价是压缩过程会消耗一定 CPU 资源,但现代服务器 CPU 富余、网络紧张,整体收益显著。

注意:图片(jpg/png/gif)、视频、大文件等本身已有压缩的资源不应再启用 Gzip——压缩前后大小变化不大,反而白白浪费 CPU;尤其是大文件,压缩开销大、效果差,得不偿失。

6.1 完整参数表#

参数说明
gzip on;开启 gzip(on/off)
gzip_min_length允许压缩的最小字节数,建议 >1k
gzip_buffers块大小倍数申请内存,如 4 16k 表示以 16k 为单位申请 4 倍
gzip_http_version识别 HTTP 协议版本(早期浏览器不支持自解压会乱码)
gzip_comp_level压缩等级 1-9,越低越快压缩比越小,越高越慢压缩比越大
gzip_types需要压缩的 MIME 类型
gzip_vary on;应答头加 Vary: Accept-Encoding,便于前端缓存服务器缓存压缩版
gzip_proxied反向代理时启用压缩的条件(off/expired/no-cache/no-store/private/no_last_modified/no_etag/auth)
gzip_disable不压缩的浏览器,支持正则匹配 User-Agent

6.2 基础配置#

gzip on;
gzip_min_length 10k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 2;
gzip_types text/css text/xml application/javascript;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;

6.3 线上生产配置#

gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 9;
gzip_types text/plain
application/x-javascript
text/css
application/xml
text/javascript
application/x-httpd-php
application/javascript
application/json;
gzip_disable "MSIE [1-6]\.";
gzip_vary on;

配置可放在 http {} 全局,也可放在 serverlocation 下做精细控制。

6.4 验证#

Terminal window
curl -I -H "Accept-Encoding: gzip, deflate" "http://域名/资源路径"

响应头出现 Content-Encoding: gzip 即生效。浏览器 F12 看响应头同样可确认。

6.5 不该压缩的资源#

类型原因
图片(jpg/png/gif)已有压缩,gzip 收益微乎其微,反而浪费 CPU
视频与大文件压缩开销大、效果差,得不偿失

七、负载均衡与健康检查#

7.1 upstream 基础与原生缺陷#

upstream web {
server 172.16.0.1;
server 172.16.0.186;
}
server {
listen 80;
server_name 172.16.0.132;
location / {
proxy_pass http://web;
}
}

Nginx 原生 upstream 的负载均衡策略有轮询(默认)、weight、ip_hash、least_conn、random 等,调度逻辑本身够用。但健康检查是它的短板——原生 upstream 只能做被动健康检查:只有当请求转发到某节点失败(连接超时、5xx 等)时,才会通过 max_fails / fail_timeout 把该节点临时标为 down。这意味着:

  • 没有请求时不会探活,节点宕了 Nginx 不知道;
  • 第一个探到故障节点的请求会被”牺牲”掉——用户体验差;
  • 节点恢复后也只在 fail_timeout 时间过后才重新尝试。

要补上主动健康检查这一刀,需要第三方模块 upstream_check_module

7.2 upstream_check_module 简介#

GitHub:yaoweibin/nginx_upstream_check_module第三方模块,非 Nginx 官方。它能周期性主动探活后端:每隔 N 毫秒发一个探测请求,连续失败 fall 次后把节点标为 down,连续成功 rise 次后恢复。被标为 down 的节点不再参与调度,请求不会转发过去——这就是”主动健康检查”与原生”被动失败计数”的本质区别。

适用场景:对可用性要求高的反向代理 / 负载均衡场景,确保流量只被转发到健康的后端服务器,避免请求被发送到故障节点。

7.3 安装步骤#

依赖#

Terminal window
yum install -y pcre pcre-devel openssl openssl-devel patch \
libxml2 libxml2-dev libxslt-devel gd gd-devel \
perl-devel perl-ExtUtils-Embed gperftools

拉源码与打补丁#

Terminal window
git clone https://github.com/yaoweibin/nginx_upstream_check_module.git
wget http://nginx.org/download/nginx-1.7.5.tar.gz
tar zxf nginx-1.7.5.tar.gz
cd nginx-1.7.5
# 选与 Nginx 版本匹配的补丁
patch -p1 < ../nginx_upstream_check_module/check_1.7.5+.patch

编译#

Terminal window
./configure --prefix=/usr/local/nginx \
--add-module=/path/to/nginx_upstream_check_module \
--with-http_ssl_module \
--with-http_gzip_static_module \
--with-http_stub_status_module
make
make install # 全新安装
# 已安装过:仅替换二进制
cp /usr/local/nginx/sbin/nginx{,.bak}
cp objs/nginx /usr/local/nginx/sbin/nginx

7.4 配置与参数详解#

upstream web {
server 172.16.0.1;
server 172.16.0.186;
check interval=3000 rise=2 fall=3 timeout=1000
type=http default_down=true port=80;
}
server {
listen 80;
server_name 172.16.0.132;
location / {
proxy_pass http://web;
}
location /status {
check_status; # 健康检查状态页
}
}

check 指令参数表#

参数含义
interval=3000探测间隔,毫秒
rise=2连续成功 N 次认为恢复
fall=3连续失败 N 次认为宕机
timeout=1000探测请求超时,毫秒
default_down=true初始状态;为 true 时默认 down,需等 rise 达标才认为正常
port=80健康检查端口(可与业务端口不同)
type=http探测协议类型

支持的协议类型#

不同后端服务用不同的应用层协议,健康检查也要”说同一种语言”。upstream_check_module 支持 5 种协议探测:

类型原理适用后端
tcp简单的 TCP 连接,连接成功即认为后端正常任意 TCP 服务(最轻量)
ssl_hello发送初始 SSL Hello 包并接收服务器的 SSL Hello 包HTTPS / TLS 服务
http发送 HTTP 请求,通过后端返回的状态码判断是否存活HTTP / HTTPS Web 服务(最常用)
mysql向 MySQL 服务器发起连接,通过接收 greeting 包判断后端是否存活MySQL 数据库
ajp向后端发送 AJP 协议的 Cping 包,通过接收 Cpong 包判断后端是否存活Tomcat AJP

7.5 状态页与重载#

Terminal window
/usr/local/nginx/sbin/nginx -s reload

访问 /status 即可在浏览器看到后端节点的健康状态。当某台 Nginx 关停后,状态页对应行会切换为不可用,请求不会再被转发过去。


八、把所有东西串起来:一个请求的全生命周期#

把前面所有知识点串起来,看一个 HTTP 请求是怎么被 Nginx 处理的:

  1. TCP 三次握手 → 内核负载均衡选 worker;
  2. worker epoll_wait 收到连接accept() → 分配 512B 连接内存池;
  3. 读取请求字节流 → 状态机解析请求行(URL 超 4k 时申请 large_client_header_buffers);
  4. 解析 Header → 标识 URI/Header,确定 server 块,移除超时定时器;
  5. POST_READ → realip 模块修正 $remote_addr
  6. SERVER_REWRITE / FIND_CONFIG / REWRITE / POST_REWRITE → location 匹配 + URL 改写(可能跳转回 FIND_CONFIG);
  7. PREACCESSlimit_req(漏桶)→ limit_conn
  8. ACCESS → access → auth_basic → auth_request,受 satisfy 调度;
  9. PRECONTENTtry_files 试文件 → mirror 镜像子请求;
  10. CONTENT → index → autoindex → static,或 proxy_pass 转发到 upstream;
  11. upstream 内的 server 由 upstream_check_module 周期性探活,故障节点自动剔除;
  12. 响应回客户端 → 走 Response Filters(如 gzip 压缩、图片处理);
  13. LOG → access_log 写盘,受 open_log_file_cache 优化。

整条链路上:worker 单线程跑 epoll,每个请求自始至终在同一个 worker;内存走池化、零拷贝指针引用;限流走共享内存跨 worker 生效;健康检查走第三方模块主动探活。这就是 Nginx 十万并发的全部秘密。


九、生产配置速查模板#

把常用配置浓缩成一份模板,按需复制:

user nginx;
worker_processes auto;
worker_rlimit_nofile 100000;
events {
use epoll; # Linux 选 epoll,FreeBSD 选 kqueue
worker_connections 10240;
multi_accept on; # 一次 accept 多个连接
}
http {
# ---------- 编码与 MIME ----------
charset utf-8;
include mime.types;
default_type application/octet-stream;
# ---------- 日志 ----------
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time urt=$upstream_response_time';
access_log /var/log/nginx/access.log main buffer=64k flush=5s;
open_log_file_cache max=1000 inactive=20s min_uses=2 valid=1m;
# ---------- 头部与内存 ----------
client_header_timeout 60s;
client_body_timeout 60s;
client_max_body_size 20m;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
connection_pool_size 512;
request_pool_size 4k;
# ---------- 长连接 ----------
keepalive_timeout 65s;
keepalive_requests 1000;
# ---------- Gzip ----------
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/javascript
application/json application/x-javascript;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
# ---------- 限流 ----------
limit_conn_zone $binary_remote_addr zone=conn_zone:10m;
limit_req_zone $binary_remote_addr zone=req_zone:10m rate=10r/s;
# ---------- upstream + 健康检查 ----------
upstream backend {
server 172.16.0.1:8080 max_fails=3 fail_timeout=30s;
server 172.16.0.186:8080 max_fails=3 fail_timeout=30s;
check interval=3000 rise=2 fall=3 timeout=1000
type=http default_down=true port=8080;
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com.pem;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# realip(前置 CDN/LB 时启用)
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# 限流
location /api/ {
limit_req zone=req_zone burst=20 nodelay;
limit_conn conn_zone 10;
proxy_pass http://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_http_version 1.1;
proxy_set_header Connection "";
}
# 静态资源
location /static/ {
root /var/www;
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
# 健康检查状态页(生产建议加 IP 白名单或 Basic Auth)
location /nginx_status {
check_status;
allow 10.0.0.0/8;
deny all;
}
}
}

十、运维命令速查#

Terminal window
# 启动 / 停止 / 重载
nginx # 启动
nginx -s reload # 平滑重载(master 重新读配置,老 worker 处理完后退出)
nginx -s stop # 优雅停止
nginx -s quit # 等待请求处理完退出
nginx -t # 检查配置语法
nginx -T # 打印完整配置(含 include)
nginx -V # 查看编译参数与已启用模块
# 信号
kill -HUP <master_pid> # 等同 reload
kill -USR2 <master_pid> # 热升级(启动新 master 与老 master 并存)
kill -WINCH <master_pid> # 优雅关闭老 worker
kill -QUIT <master_pid> # 优雅退出
kill -TERM <master_pid> # 快速退出
# 日志切割
mv access.log access.log.$(date +%F)
nginx -s reopen # 重新打开日志文件

十一、参考资料#


写在最后#

Nginx 不是”会用配置”就够了,要写出正确、安全、高性能的配置,必须理解:

  • 进程模型:知道 worker 是单线程跑 epoll,所以不能让某个 worker 卡死;
  • 多路复用:知道为什么 Nginx 在 Linux 上必须用 epoll,ET 模式为什么必须立刻处理;
  • 头部处理:知道 large_client_header_buffers 上限是 4×8k,过长 URL 怎么调;
  • 11 阶段:知道 return 在 ACCESS 之前、limit_reqlimit_conn 之前、模块执行顺序与配置顺序无关;
  • Gzip:知道图片视频不该压,等级不是越高越好;
  • 健康检查:知道原生 upstream 不主动探活,必要时上 upstream_check_module

把这些底层认知串起来,你就能在故障现场快速判断:“这是 worker 数不够?是 limit_req 漏桶满了?是 location 正则没命中?是 upstream 没做健康检查?“——而这是线上救火最值钱的能力。

祝你早日写出十万并发的 Nginx 配置。

Nginx 从进程模型到健康检查完全指南
https://sgjki547.top/posts/2026-07-06-nginx从进程模型到健康检查完全指南/
Author
SGJki
Published at
2026-07-06
License
CC BY-NC-SA 4.0