主题
Nginx 核心原理与反向代理
一句话:Nginx 用少量单线程 Worker 加 epoll 事件循环,把「连接数」和「线程数」解耦;代价是任何阻塞操作都会卡住整个 Worker 的事件循环。
适用版本:Nginx 1.24–1.28 开源版。默认值以官方文档为准;listen ... http2 从 1.25.1 起废弃,改用 http2 on;。
为什么需要
一连接一线程的模型在连接数上来后先撞两堵墙:每线程 MB 级栈内存,以及内核上下文切换。异步回调能省线程,但把业务逻辑拆成状态机,超时和错误处理很难写对。
Nginx 把等待变成事件:一个 Worker 用非阻塞 IO 同时盯大量连接,哪个就绪处理哪个。它适合做接入层——反向代理、负载均衡、TLS 终止、静态资源;不适合承载业务逻辑。
核心机制
Master-Worker
- Master 以 root 启动:读配置、绑定端口、fork Worker、处理信号、平滑升级。它不处理请求。
- Worker 以普通用户运行,单线程事件循环,各自独立处理连接。
worker_processes上游默认 1;发行版配置通常写auto,等于 CPU 核数。worker_connections默认 512,发行版常改成 1024 或 768。accept_mutex从 1.11.3 起默认 off;需要避免惊群时可用reuseport。- 信号:
HUP平滑重载、QUIT优雅退出、TERM快速退出、USR1重开日志、USR2平滑升级。
重载时 Master 先校验配置,再启动新 Worker,旧 Worker 处理完在途请求后退出;监听 socket 被继承,所以不会丢新连接。旧 Worker 可能因为长连接存活很久,可用 worker_shutdown_timeout 限制等待时间。
为什么快
- epoll:内核只返回就绪的连接,开销跟「就绪事件数」相关,而不是总连接数;select/poll 每次都要遍历全量 fd。
- 非阻塞 IO:读写返回
EAGAIN就把请求挂起,Worker 转去处理别的连接。 - 每 Worker 单线程:没有锁竞争和线程切换;反过来说,一次阻塞调用会停住这个 Worker 上的所有连接。
- sendfile / 零拷贝:静态文件从页缓存直接送到 socket,避免数据在用户态和内核态之间往返;配合
tcp_nopush、tcp_nodelay控制发送行为。 - 连接池与内存池:减少频繁分配;大文件或特殊文件系统可用线程池(
aio threads)避免阻塞事件循环。
请求怎么匹配与转发
location 优先级:= 精确匹配 → ^~ 最长前缀(命中后不再试正则)→ ~ / ~* 正则(按配置顺序)→ 普通最长前缀。
proxy_pass 的 URI 语义是高频 404 来源:带 URI(如 http://app/)时,匹配到的 location 前缀会被替换;不带 URI(http://app)时,完整原始 URI 原样追加。
upstream 默认平滑加权轮询,还支持 least_conn、ip_hash、hash $key consistent、random two。对上游启用长连接要同时写 keepalive、proxy_http_version 1.1 和 proxy_set_header Connection "",缺一个都会退化成短连接。
最小上手
bash
mkdir -p /tmp/nginx-demo && cd /tmp/nginx-demo
# 把下面的 nginx.conf 保存到当前目录;Windows 挂载时用绝对路径nginx
worker_processes auto;
events { worker_connections 1024; }
http {
upstream app {
server 127.0.0.1:9000;
keepalive 16;
}
server { # 模拟后端
listen 127.0.0.1:9000;
location / { return 200 "backend ok\n"; }
}
server {
listen 80;
location /api/ {
proxy_pass http://app/;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
proxy_send_timeout 30s;
}
}
}bash
docker run --rm -d --name nginx-demo -p 8080:80 \
-v "$PWD/nginx.conf:/etc/nginx/nginx.conf:ro" nginx:1.27-alpine
curl -i http://127.0.0.1:8080/api/ # 期望 200,body 为 backend ok
docker exec nginx-demo nginx -t # 期望 syntax is ok
docker rm -f nginx-demo生产动作
关键参数与默认值(Nginx 1.x)
| 参数 | 默认值 | 要点 |
|---|---|---|
worker_processes | 1 | 发行版常配 auto(等于核数) |
worker_connections | 512 | 代理场景客户端和上游各占一个连接 |
keepalive_timeout | 75s | 客户端长连接空闲上限 |
client_max_body_size | 1m | 超过返回 413,不是 502 |
proxy_connect_timeout | 60s | 通常不能超过 75s |
proxy_read_timeout | 60s | 两次读之间,不是整个响应时长 |
proxy_send_timeout | 60s | 两次写之间 |
proxy_buffering / proxy_request_buffering | on / on | 流式接口要关 buffering |
max_fails / fail_timeout | 1 / 10s | 被动健康检查,失败即摘除 |
proxy_next_upstream | error timeout | 默认不重试上游返回的 502 |
limit_req_status / limit_conn_status | 503 / 503 | 限流拒绝要和上游故障区分 |
limit_req_zone 的 10 MB 共享内存约存 16 万个 IPv4 状态(官方文档口径)。
502 / 504 排查路径
先分清状态码:
- 502 Bad Gateway:Nginx 没拿到合法响应。连接被拒、DNS 失败、上游 reset、上游提前关闭 keepalive 连接、响应格式错误。
- 504 Gateway Timeout:连上了但超时。连接阶段超过
proxy_connect_timeout,读阶段超过proxy_read_timeout,或写阶段超过proxy_send_timeout。 - 499 是 Nginx 自定义:客户端先断开,通常说明客户端等不及或上游太慢。
排查步骤:
bash
# 1. 访问日志至少包含这些变量,才能分清阶段
log_format up '$remote_addr $request $status ua=$upstream_addr us=$upstream_status '
'uct=$upstream_connect_time uht=$upstream_header_time '
'urt=$upstream_response_time rt=$request_time';
# 2. 502:先确认上游还活着
ss -lntp | grep ':9000'
curl -o /dev/null -s -w '%{http_code} %{time_total}\n' http://127.0.0.1:9000/healthz
tail -f /var/log/nginx/error.log # connect() failed / no live upstreams / prematurely closed
# 3. 504:看时间花在哪个阶段
# urt 接近 proxy_read_timeout -> 后端处理慢
# uct 接近 proxy_connect_timeout -> 网络或后端 accept 慢
# rt 远大于 urt -> Nginx 排队、缓冲或客户端慢
# 4. 改完先校验再平滑重载
nginx -t && nginx -s reload判断依据:no live upstreams 说明所有后端都被 max_fails/fail_timeout 摘除;上游 keepalive 连接被对端先关会产生 502,默认的 proxy_next_upstream error timeout 会重试幂等请求,但 POST 不要轻易加 non_idempotent,否则可能重复写。504 优先修后端耗时,调大 proxy_read_timeout 只是把故障推迟;它是「两次读之间」的超时,慢速流式响应只要持续有数据就不会触发。
边界
- 阻塞即全局:同步 DNS、阻塞磁盘 IO、第三方模块里的阻塞调用都会卡住整个 Worker;Nginx 快的前提是没有阻塞点。
if少用:location 内的if行为不符合直觉,能用try_files、map、return就不要用。worker_connections不是客户端上限:代理场景一个请求占客户端和上游两个连接,有效并发要打折。- 开源版没有主动健康检查:只有
max_fails/fail_timeout的被动摘除;主动探测是 Nginx Plus 能力。 ip_hash在 NAT/CDN 后会失衡:同一出口 IP 全落到一台;需要会话保持时用一致性哈希或外部会话存储。proxy_read_timeout不限制总时长:要限制整体耗时得在客户端或应用层做,不要误以为调它就能兜住慢请求。
讲清楚
Nginx 的核心是 Master-Worker 加事件驱动:Master 管配置、端口、Worker 和信号,Worker 以普通用户单线程跑 epoll,一个连接等待时不占线程,所以少量 Worker 能撑大量连接。它快在 epoll 只处理就绪事件、非阻塞 IO 不占线程、sendfile 减少拷贝;代价是 Worker 里任何阻塞调用都会拖住它负责的所有连接。
做反向代理时最该记的是三件事:proxy_pass 带不带 URI 决定路径替换;三处代理超时默认都是 60 秒,且 proxy_read_timeout 只管两次读之间;502 是拿不到合法响应,504 是等超时。排查先看 $upstream_* 变量分清阶段,再决定修后端还是调超时。