Nginx进程模型、事件循环与高并发原理
“Nginx性能高,因为它是异步非阻塞”只是一句结论。要真正理解,必须回答:谁监听端口、master和worker分别做什么、一个worker为什么能同时管理大量连接、epoll通知的是什么、读不到数据时为什么不需要创建一个线程等待、反向代理为什么同时占用客户端和上游连接,以及慢磁盘、DNS或第三方模块为什么仍可能把worker卡住。
学习目标
学完后应能:
- 区分master、worker、cache loader和cache manager等进程角色。
- 解释阻塞IO、非阻塞IO、IO多路复用和异步IO的区别。
- 画出accept、read、解析、upstream、write和keep-alive全过程。
- 解释epoll返回“就绪事件”而不是直接返回完整业务响应。
- 说明一个worker如何用事件和状态机管理大量连接。
- 解释
worker_processes、worker_connections、FD和代理双连接的容量关系。 - 说明长连接、慢客户端、大响应和Buffer如何影响内存与连接数。
- 解释reload为什么能让新旧worker平滑交接。
- 定位CPU高、连接满、worker阻塞和上游慢等生产问题。
一、先看请求完整经过哪些角色
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分别做什么
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 --> I2.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为什么会限制并发
假设一个线程执行阻塞读取:
调用read
→ 当前没有数据
→ 线程睡眠等待
→ 数据到达后被唤醒
→ 继续处理如果使用“一个连接一个线程”,一万个慢连接可能需要大量线程。线程会消耗:
- 线程栈内存。
- 内核调度结构。
- 上下文切换CPU。
- 锁和共享状态协调成本。
线程模型不是绝对低性能,但对于大量I/O等待连接,创建同数量线程通常不经济。
四、非阻塞IO解决了什么
把socket设为非阻塞后:
调用read
→ 有数据:立即读到部分或全部数据
→ 暂无数据:立即返回EAGAIN/EWOULDBLOCK线程不会因为一个socket没数据而睡眠,但如果不断轮询一万个socket:
for each socket:
read(socket)绝大多数调用可能都返回EAGAIN,CPU会被无效扫描浪费。因此还需要IO多路复用通知“哪些socket现在值得处理”。
五、IO多路复用和epoll
Linux常用epoll。概念流程:
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 --> Bepoll返回的是类似“这个FD当前可读”“那个FD当前可写”的就绪通知,不会替Nginx完成HTTP解析、业务路由和上游转发。
5.1 Reactor式理解
可以把worker理解成:
事件收集器
→ 事件分发器
→ 每种事件的处理函数
→ 连接状态机同一个worker在很短时间片中轮流推进不同连接:
连接A:读取请求头
连接B:等待上游connect完成
连接C:读取上游响应
连接D:客户端socket可写,发送Buffer
连接E:keep-alive超时,关闭它不是同时在一条CPU指令上处理所有连接,而是在连接发生就绪事件时快速切换处理对象。
六、事件就绪不等于一次读完
TCP是字节流。一次read可能只获得半个HTTP请求头,也可能同时获得请求头和部分请求体。
flowchart TD
A["socket可读"] --> B["执行非阻塞read"]
B --> C{"数据是否足够完成当前解析阶段"}
C -- "否" --> D["保存已解析位置和部分Buffer"]
D --> E["重新关注可读事件"]
C -- "是" --> F["进入下一处理阶段"]因此Nginx需要为每个连接保存状态,例如:
- 已读取多少字节。
- 请求行解析到哪里。
- Header是否完整。
- 请求体是否需要继续读取或落临时文件。
- upstream是否连接成功。
- 响应已经发送多少。
- 当前定时器和超时边界。
这就是事件驱动必须配合状态机的原因。
七、一个HTTP请求的阶段化处理
简化理解:
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配置解析与请求匹配。
八、反向代理为什么至少涉及两端连接
flowchart TD
A["客户端连接"] --> B["Nginx worker"]
B --> C["上游连接"]
C --> D["Spring Boot或其他后端"]
D --> C
C --> B
B --> A一个代理请求通常同时占用:
- 客户端到Nginx的连接。
- Nginx到upstream的连接。
如果使用upstream keepalive,上游连接还可能在请求结束后留在连接池中复用。因此:
worker_processes × worker_connections不能直接等同于最大客户端数量,更不能直接等同于QPS。
还要扣除:
- 上游连接。
- 监听socket。
- 日志、文件和临时文件FD。
- 管理连接。
- WebSocket等长连接。
九、worker_connections与文件描述符
配置:
worker_processes auto;
events {
worker_connections 4096;
}理论连接槽位还受到进程FD限制:
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应该设置多少
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负载可以结合:
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为什么能隔离快慢两端
反向代理常见两端速度不同:
后端快速产生响应
客户端移动网络读取很慢如果Nginx适当缓冲,可以让后端更快释放连接,再按客户端速度发送。但Buffer不是无限的:
- 内存Buffer会占worker内存。
- 响应过大可能写临时文件并产生磁盘IO。
- 关闭buffering可能让慢客户端长期占用上游连接。
- SSE和流式响应通常需要低延迟转发,不能照搬普通API缓冲策略。
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为什么可以平滑
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前应先:
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库为准。
二十、最小原理验证配置
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;
}
}
}验证:
nginx -t
nginx -T
ps -ef | grep '[n]ginx'
curl -i http://127.0.0.1:8081/health没有后端时访问 /api/ 预期产生上游连接错误,这可用于观察error log,但生产不能通过反复请求制造无意义告警。
二十一、生产排查:CPU高
先判断CPU在哪个进程:
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可能加重调度竞争。
二十二、生产排查:连接数满
证据:
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继续增加。
根因链:
更多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被卡住会影响它管理的很多连接,因此入口层不应承载复杂业务逻辑。
二十六、学习验收
- 画出master、worker、监听socket、客户端和upstream关系。
- 对比阻塞IO、非阻塞轮询和epoll事件通知。
- 解释一次可读事件为什么不保证读到完整HTTP请求。
- 画出请求阶段和模块处理链。
- 说明代理请求为什么通常至少占两个连接。
- 根据FD、worker_connections、长连接和上游连接估算容量边界。
- 解释keep-alive的收益和资源成本。
- 说明Buffer如何隔离快后端和慢客户端,以及何时不适合缓冲。
- 找出至少五种会阻塞或长期占用worker的操作。
- 演示配置成功与失败时reload的worker代际变化。
- 分别给出CPU高、连接满和低CPU高延迟的排查证据。
- 解释为什么只扩Nginx可能把数据库打得更慢。
