Skip to content

Nginx进程模型、事件循环与高并发原理

“Nginx性能高,因为它是异步非阻塞”只是一句结论。要真正理解,必须回答:谁监听端口、master和worker分别做什么、一个worker为什么能同时管理大量连接、epoll通知的是什么、读不到数据时为什么不需要创建一个线程等待、反向代理为什么同时占用客户端和上游连接,以及慢磁盘、DNS或第三方模块为什么仍可能把worker卡住。

学习目标

学完后应能:

  1. 区分master、worker、cache loader和cache manager等进程角色。
  2. 解释阻塞IO、非阻塞IO、IO多路复用和异步IO的区别。
  3. 画出accept、read、解析、upstream、write和keep-alive全过程。
  4. 解释epoll返回“就绪事件”而不是直接返回完整业务响应。
  5. 说明一个worker如何用事件和状态机管理大量连接。
  6. 解释 worker_processesworker_connections、FD和代理双连接的容量关系。
  7. 说明长连接、慢客户端、大响应和Buffer如何影响内存与连接数。
  8. 解释reload为什么能让新旧worker平滑交接。
  9. 定位CPU高、连接满、worker阻塞和上游慢等生产问题。

一、先看请求完整经过哪些角色

mermaid
flowchart TD
    A["客户端建立TCP连接"] --> B["操作系统监听队列"]
    B --> C["Nginx worker接受连接"]
    C --> D["事件循环等待socket就绪"]
    D --> E["读取并解析HTTP请求"]
    E --> F{"处理方式"}
    F -- "静态资源" --> G["读取文件并发送"]
    F -- "反向代理" --> H["连接upstream"]
    H --> I["发送上游请求"]
    I --> J["读取上游响应"]
    J --> K["Buffer或流式转发给客户端"]
    G --> L["请求完成或进入keep-alive"]
    K --> L
    L --> D

这个流程中不存在“每来一个连接就永久分配一个Nginx线程”。worker把大量socket注册到事件通知机制,只有某个socket可读、可写、连接完成或超时时,才处理该连接当前阶段。

二、master和worker分别做什么

mermaid
flowchart TD
    A["master进程"] --> B["读取与校验配置"]
    A --> C["打开日志和监听socket"]
    A --> D["创建与管理worker"]
    A --> E["接收TERM、QUIT、HUP、USR信号"]
    D --> F["worker 1事件循环"]
    D --> G["worker 2事件循环"]
    D --> H["worker N事件循环"]
    F --> I["客户端与上游连接"]
    G --> I
    H --> I

2.1 master主要负责控制面

  • 读取配置并建立运行环境。
  • 打开监听端口和日志文件。
  • fork worker。
  • 监控worker退出并按需要拉起。
  • 接收reload、优雅退出、重新打开日志等信号。
  • reload时创建新一代worker,并通知旧worker优雅退出。

master通常不直接执行普通HTTP业务请求。

2.2 worker主要负责数据面

  • 接受客户端连接。
  • TLS握手。
  • 读取和解析HTTP请求。
  • 执行rewrite、access、content、filter等阶段处理。
  • 读取静态文件或访问upstream。
  • 发送响应和维护keep-alive。
  • 执行定时器和超时处理。

2.3 为什么使用多进程

  • 可以利用多核CPU。
  • worker地址空间相对隔离,一个worker异常不必直接破坏所有worker。
  • 每个worker内部仍保持事件驱动,避免每连接一线程的高切换成本。
  • reload时可以让新旧worker并存,实现平滑切换。

多进程不代表所有状态天然共享。限流区、连接区和缓存元数据需要共享内存区,普通模块内存变量通常只属于当前worker。

三、阻塞IO为什么会限制并发

假设一个线程执行阻塞读取:

text
调用read
→ 当前没有数据
→ 线程睡眠等待
→ 数据到达后被唤醒
→ 继续处理

如果使用“一个连接一个线程”,一万个慢连接可能需要大量线程。线程会消耗:

  • 线程栈内存。
  • 内核调度结构。
  • 上下文切换CPU。
  • 锁和共享状态协调成本。

线程模型不是绝对低性能,但对于大量I/O等待连接,创建同数量线程通常不经济。

四、非阻塞IO解决了什么

把socket设为非阻塞后:

text
调用read
→ 有数据:立即读到部分或全部数据
→ 暂无数据:立即返回EAGAIN/EWOULDBLOCK

线程不会因为一个socket没数据而睡眠,但如果不断轮询一万个socket:

text
for each socket:
    read(socket)

绝大多数调用可能都返回EAGAIN,CPU会被无效扫描浪费。因此还需要IO多路复用通知“哪些socket现在值得处理”。

五、IO多路复用和epoll

Linux常用epoll。概念流程:

mermaid
flowchart TD
    A["worker把多个FD注册到epoll"] --> B["worker调用epoll_wait"]
    B --> C{"是否有事件或定时器到期"}
    C -- "没有" --> B
    C -- "有" --> D["返回就绪事件集合"]
    D --> E["调用对应连接的事件处理函数"]
    E --> F["非阻塞read、write或connect检查"]
    F --> G["更新连接状态与关注事件"]
    G --> B

epoll返回的是类似“这个FD当前可读”“那个FD当前可写”的就绪通知,不会替Nginx完成HTTP解析、业务路由和上游转发。

5.1 Reactor式理解

可以把worker理解成:

text
事件收集器
→ 事件分发器
→ 每种事件的处理函数
→ 连接状态机

同一个worker在很短时间片中轮流推进不同连接:

text
连接A:读取请求头
连接B:等待上游connect完成
连接C:读取上游响应
连接D:客户端socket可写,发送Buffer
连接E:keep-alive超时,关闭

它不是同时在一条CPU指令上处理所有连接,而是在连接发生就绪事件时快速切换处理对象。

六、事件就绪不等于一次读完

TCP是字节流。一次read可能只获得半个HTTP请求头,也可能同时获得请求头和部分请求体。

mermaid
flowchart TD
    A["socket可读"] --> B["执行非阻塞read"]
    B --> C{"数据是否足够完成当前解析阶段"}
    C -- "否" --> D["保存已解析位置和部分Buffer"]
    D --> E["重新关注可读事件"]
    C -- "是" --> F["进入下一处理阶段"]

因此Nginx需要为每个连接保存状态,例如:

  • 已读取多少字节。
  • 请求行解析到哪里。
  • Header是否完整。
  • 请求体是否需要继续读取或落临时文件。
  • upstream是否连接成功。
  • 响应已经发送多少。
  • 当前定时器和超时边界。

这就是事件驱动必须配合状态机的原因。

七、一个HTTP请求的阶段化处理

简化理解:

mermaid
flowchart TD
    A["读取请求行与Header"] --> B["选择server和location"]
    B --> C["rewrite阶段"]
    C --> D["访问控制与限流阶段"]
    D --> E["content阶段生成内容或代理"]
    E --> F["Header Filter"]
    F --> G["Body Filter"]
    G --> H["写回客户端"]
    H --> I["日志阶段"]

模块在不同阶段注册处理函数。配置指令不只是文本替换,它们通常在配置解析期生成模块配置,在请求运行期参与对应阶段。

location、rewrite和proxy_pass的详细匹配过程见 Nginx配置解析与请求匹配

八、反向代理为什么至少涉及两端连接

mermaid
flowchart TD
    A["客户端连接"] --> B["Nginx worker"]
    B --> C["上游连接"]
    C --> D["Spring Boot或其他后端"]
    D --> C
    C --> B
    B --> A

一个代理请求通常同时占用:

  • 客户端到Nginx的连接。
  • Nginx到upstream的连接。

如果使用upstream keepalive,上游连接还可能在请求结束后留在连接池中复用。因此:

text
worker_processes × worker_connections

不能直接等同于最大客户端数量,更不能直接等同于QPS。

还要扣除:

  • 上游连接。
  • 监听socket。
  • 日志、文件和临时文件FD。
  • 管理连接。
  • WebSocket等长连接。

九、worker_connections与文件描述符

配置:

nginx
worker_processes auto;

events {
    worker_connections 4096;
}

理论连接槽位还受到进程FD限制:

bash
nginx -T
cat /proc/<worker-pid>/limits
ls /proc/<worker-pid>/fd | wc -l

容量估算不能只改Nginx配置,还要检查:

  • worker_rlimit_nofile
  • systemd LimitNOFILE
  • 宿主机文件描述符上限。
  • 容器ulimit。
  • 每请求连接放大倍数。
  • 内存与Buffer。
  • 后端容量。

把FD上限调得很大,不会自动让后端数据库和Java线程池承受更多并发。

十、worker_processes应该设置多少

nginx
worker_processes auto;

常见起点是接近可用CPU核数,因为worker主要采用单线程事件循环。实际还要考虑:

  • 容器CPU quota与cpuset。
  • TLS握手、gzip等CPU工作。
  • 磁盘IO和静态文件访问。
  • 模块是否创建线程池或执行阻塞任务。
  • NUMA与CPU亲和性。

worker过少可能无法利用CPU,过多可能增加调度和缓存抖动。必须在目标cgroup和真实流量下压测,不根据宿主机物理核数机械填写。

十一、accept如何分配给多个worker

多个worker可能共享监听socket。连接到达时需要决定由谁accept。不同平台、版本和配置可能使用:

  • 内核唤醒与调度机制。
  • accept_mutex等协调机制。
  • reuseport让每个worker拥有独立监听socket并由内核分流。

目标是避免所有worker被无效唤醒,也避免连接长期集中到少数worker。不要只凭旧文章永久开启某个开关;应结合当前版本默认行为、操作系统和连接分布验证。

观察worker负载可以结合:

bash
ps -o pid,ppid,psr,pcpu,pmem,cmd -C nginx
ss -lntp

容器环境还应看cgroup CPU throttling和容器可见CPU。

十二、keep-alive为什么既提升性能又占连接

HTTP keep-alive复用TCP连接,减少:

  • TCP握手。
  • TLS握手。
  • 慢启动和网络往返。

但空闲keep-alive也占用:

  • 连接对象。
  • 文件描述符。
  • 少量内存。
  • 定时器。

超时时间过短会频繁重连,过长会让大量低价值空闲连接占住容量。应根据客户端类型、网关层级和连接规模配置,而不是永久复制 keepalive_timeout 65

十三、Buffer为什么能隔离快慢两端

反向代理常见两端速度不同:

text
后端快速产生响应
客户端移动网络读取很慢

如果Nginx适当缓冲,可以让后端更快释放连接,再按客户端速度发送。但Buffer不是无限的:

  • 内存Buffer会占worker内存。
  • 响应过大可能写临时文件并产生磁盘IO。
  • 关闭buffering可能让慢客户端长期占用上游连接。
  • SSE和流式响应通常需要低延迟转发,不能照搬普通API缓冲策略。
mermaid
flowchart TD
    A["upstream响应"] --> B["Nginx proxy Buffer"]
    B --> C{"Buffer是否足够"}
    C -- "是" --> D["按客户端可写速度发送"]
    C -- "否" --> E["按配置写临时文件或施加背压"]
    E --> D

十四、哪些操作会阻塞worker

事件驱动不等于worker永远不会阻塞。以下操作可能造成长时间占用:

  • 第三方模块执行阻塞系统调用。
  • 同步DNS解析或外部库调用方式不当。
  • 大文件操作和磁盘严重抖动。
  • 复杂正则和极端rewrite规则消耗CPU。
  • Lua或脚本模块执行重CPU逻辑。
  • 压缩大响应占用CPU。
  • TLS握手洪峰。
  • 单次处理循环做过多工作,无法及时返回事件循环。

一个worker被阻塞时,它管理的许多连接都可能延迟升高。这也是为什么不能把复杂业务逻辑塞进入口代理层。

十五、sendfile、AIO和线程池边界

sendfile

静态文件发送时,sendfile 可以减少用户态数据复制路径,让内核更高效地从文件向socket发送。但是否收益明显取决于操作系统、文件系统、TLS和缓存命中。

AIO与线程池

磁盘IO并非在所有场景都能像socket一样完全依靠epoll解决。Nginx可以按平台和模块能力使用AIO或线程池,把可能阻塞的文件操作移出主事件循环。

这不意味着开启线程池就能修复慢磁盘。底层IOPS不足、网络存储抖动和大文件并发仍需容量治理。

十六、超时本质是状态机的失败边界

常见超时:

超时约束哪个阶段
client header/body timeout客户端发送请求过慢
proxy connect timeout建立上游连接过慢
proxy send timeout向上游发送数据长期无进展
proxy read timeout从上游连续读取长期无进展
keepalive timeout空闲持久连接保留时间

超时通常不是“整个请求必须在N秒完成”的简单总时长,而是特定操作阶段长时间没有进展的边界,具体语义以指令文档为准。

无限调大超时会让失败连接更久占用资源;设置过短则会误杀合理慢请求。应同时治理后端慢SQL、线程池、连接池和下游调用。

十七、reload为什么可以平滑

mermaid
flowchart TD
    A["master收到HUP"] --> B["重新读取并校验配置"]
    B --> C{"配置是否有效"}
    C -- "否" --> D["记录错误,旧worker继续"]
    C -- "是" --> E["打开新日志和监听资源"]
    E --> F["启动新一代worker"]
    F --> G["通知旧worker优雅退出"]
    G --> H["旧worker停止接受新连接"]
    H --> I["完成已有请求后退出"]

因此reload前应先:

bash
nginx -t
nginx -s reload

平滑不等于瞬间结束所有旧worker。WebSocket、长下载和长轮询可能让旧worker长期存在。需要观察worker代际和连接,并为最长连接设计关闭策略。

十八、停止信号的区别

常见控制意图:

  • 快速停止:可能直接终止worker,影响在途请求。
  • 优雅退出:停止接新连接,等待当前请求结束。
  • reload:加载新配置并切换worker代际。
  • 重新打开日志:配合外部日志轮转。

容器中Nginx master通常是PID 1,Docker/Kubernetes停止信号和宽限期必须与Nginx优雅退出匹配。完整容器边界见 Nginx容器化与生产安全

十九、HTTP/2与HTTP/3带来的变化

HTTP/1.1中一个TCP连接同时处理请求的方式有限,HTTP/2可以在一个连接上多路复用多个stream。此时:

  • 连接数不再直接等于并发请求数。
  • 单连接可能承载大量请求。
  • 流控、Header压缩和优先级带来新状态。
  • TCP丢包仍可能影响同连接多个stream。

HTTP/3基于QUIC/UDP,连接和流管理进一步变化。不能拿HTTP/1.1的“连接数=并发请求”模型直接套用。具体支持和配置以Nginx版本、构建模块和TLS库为准。

二十、最小原理验证配置

nginx
worker_processes auto;
error_log /var/log/nginx/error.log notice;

events {
    worker_connections 4096;
}

http {
    access_log /var/log/nginx/access.log;
    keepalive_timeout 30s;
    sendfile on;

    upstream order_api {
        server 127.0.0.1:8080;
        keepalive 32;
    }

    server {
        listen 8081;

        location = /health {
            access_log off;
            return 200 "ok\n";
        }

        location /api/ {
            proxy_pass http://order_api/;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_connect_timeout 3s;
            proxy_read_timeout 15s;
        }
    }
}

验证:

bash
nginx -t
nginx -T
ps -ef | grep '[n]ginx'
curl -i http://127.0.0.1:8081/health

没有后端时访问 /api/ 预期产生上游连接错误,这可用于观察error log,但生产不能通过反复请求制造无意义告警。

二十一、生产排查:CPU高

先判断CPU在哪个进程:

bash
ps -o pid,ppid,psr,pcpu,pmem,cmd -C nginx
top -H -p <worker-pid>

再关联:

  • QPS和连接数是否上涨。
  • TLS握手是否异常。
  • gzip压缩量是否变大。
  • 是否出现恶意请求或复杂正则。
  • access log中某URI是否集中。
  • upstream失败是否造成重试放大。
  • 容器是否CPU throttling。

不能只增加worker数量;CPU已经饱和时增加worker可能加重调度竞争。

二十二、生产排查:连接数满

证据:

bash
ss -s
ss -antp
cat /proc/<worker-pid>/limits
ls /proc/<worker-pid>/fd | wc -l

检查:

  • 活跃客户端连接和keep-alive空闲连接。
  • upstream连接。
  • TIME_WAIT、SYN_RECV和CLOSE_WAIT分布。
  • WebSocket和长轮询。
  • worker_connections与FD限制。
  • 慢客户端、后端慢和超时设置。

连接满可能是容量不足,也可能是请求迟迟无法完成。仅提高上限会把压力继续传给后端。

二十三、生产排查:延迟高但CPU不高

可能原因:

  • upstream慢SQL、线程池或连接池等待。
  • DNS或建连延迟。
  • 磁盘和临时文件IO。
  • 慢客户端占用上游连接。
  • 网络丢包和重传。
  • worker被少量阻塞操作卡住。
  • 超时和重试叠加。

应对齐Nginx访问日志中的总请求时间与upstream时间,再查看后端APM、数据库和系统指标。后续日志专题会详细拆解这些变量。

二十四、商业场景:促销时扩Nginx仍然504

现象:入口QPS上涨,增加Nginx实例后短暂改善,随后504继续增加。

根因链:

text
更多Nginx容量
→ 允许更多请求并发进入后端
→ Java线程池和数据库连接池排队
→ SQL响应变慢
→ Nginx proxy_read_timeout触发
→ 504增加

Nginx扩容只扩大入口,不会自动扩大后端和数据库容量。正确做法是同时检查限流、缓存、异步化、Java线程池、连接池、慢SQL和降级,并用端到端压测确定系统瓶颈。

二十五、面试标准回答

Nginx为什么能支持高并发

Nginx采用多进程加事件驱动模型。master负责配置、监听资源、信号和worker生命周期;worker把大量非阻塞socket注册到epoll等事件机制,只在连接可读、可写、建连完成或超时时推进对应状态机,不需要为每个连接创建一个线程,因此减少线程栈和上下文切换。但worker仍可能被阻塞模块、CPU密集压缩、TLS或慢磁盘影响。

epoll返回的是什么

epoll返回已注册FD的就绪事件集合,例如某socket当前可读或可写。它不直接返回完整HTTP请求,也不替Nginx处理业务。worker收到事件后执行非阻塞read/write,可能只处理部分数据,再保存解析状态并重新等待事件。

worker_connections乘worker_processes就是最大QPS吗

不是。它近似表达连接槽位上限的一部分,还受FD、内存和系统限制。反向代理请求同时占客户端和上游连接,keep-alive、WebSocket、监听socket和文件也占FD;QPS还取决于请求耗时、CPU、网络和后端容量,所以连接数不能直接换算成QPS。

Nginx reload为什么不中断大多数请求

master校验新配置后启动新worker处理新连接,再通知旧worker停止accept并完成已有请求后退出;配置失败时旧worker继续运行。但WebSocket和长下载可能让旧worker长期存在,所以平滑reload仍需观察worker代际和最长连接。

异步非阻塞是否表示Nginx永远不会阻塞

不是。socket主路径使用非阻塞事件模型,但第三方模块、同步外部调用、磁盘抖动、复杂正则、压缩、TLS和CPU密集脚本都可能长时间占用worker。单个worker被卡住会影响它管理的很多连接,因此入口层不应承载复杂业务逻辑。

二十六、学习验收

  1. 画出master、worker、监听socket、客户端和upstream关系。
  2. 对比阻塞IO、非阻塞轮询和epoll事件通知。
  3. 解释一次可读事件为什么不保证读到完整HTTP请求。
  4. 画出请求阶段和模块处理链。
  5. 说明代理请求为什么通常至少占两个连接。
  6. 根据FD、worker_connections、长连接和上游连接估算容量边界。
  7. 解释keep-alive的收益和资源成本。
  8. 说明Buffer如何隔离快后端和慢客户端,以及何时不适合缓冲。
  9. 找出至少五种会阻塞或长期占用worker的操作。
  10. 演示配置成功与失败时reload的worker代际变化。
  11. 分别给出CPU高、连接满和低CPU高延迟的排查证据。
  12. 解释为什么只扩Nginx可能把数据库打得更慢。

关联知识点