一、引言
Nginx 几乎是每个后端、SRE、平台工程师都绕不开的基础组件。它轻量、高性能、配置直观,却也因为「配置太灵活」而容易在线上踩坑。本文不讲安装手册式的内容,而是从架构原理入手,讲清楚它为什么能扛高并发,再落到反向代理 / 负载均衡 / 容器化部署的实战,最后用一整章讲生产环境最常见的排错套路------502、504、403、too many open files、重定向循环、证书握手失败等。
典型适用场景:
- 反向代理:把客户端请求转发到后端应用(Tomcat / Node / 各种 API 服务),隐藏真实后端、统一入口。
- 负载均衡:在多个后端实例间分发流量,配合健康检查做故障转移。
- 静态资源服务:直接 serve HTML / 图片 / 前端构建产物,吞吐极高。
- API 网关 / 边缘入口:SSL 终止、限流、gzip、缓存、重写。
- K8s Ingress Controller:作为集群南北向流量的统一入口(与我之前写的 Ingress/Gateway 博客一脉相承)。
和老牌 Apache 的「一个连接一个进程/线程」模型不同,Nginx 从设计之初就是事件驱动 + 异步非阻塞,单 worker 进程可以用单线程同时hold住成千上万条连接,这是它高并发能力的根本来源。
二、原理解析
2.1 进程架构:Master + Worker
Nginx 启动后是一个多进程模型:
master process (读取配置、绑定端口、管理 worker、接收信号)
├── worker process (实际处理请求,数量通常 = CPU 核数)
├── worker process
└── worker process - Master 进程:以 root 启动(要绑定 80/443 等特权端口),负责读取并校验配置、绑定监听端口、fork 出 worker、接收运维信号(reload、stop、热升级)。它自己不处理业务请求。
- Worker 进程:以非特权用户(如 nginx)运行,每个 worker 是单线程、事件循环模型,独立处理分配到自己的连接。worker 之间不共享连接状态,靠共享内存(如 zone)做限流/健康检查等协同。
热升级(零停机发布) 是 Nginx 的招牌能力:
- master 收到 USR2 信号,启动新版本的 master + worker(新旧并存)。
- 向旧 master 发 WINCH,让它优雅关闭旧 worker(处理完存量连接再退出)。
- 确认新版本正常后,向旧 master 发 QUIT 彻底退出。
- 若新版本有问题,可反向操作回滚。
这正是 Nginx 能做到「改配置 / 升版本不丢连接」的原理,reload 不等于重启进程,旧连接由旧 worker 优雅收尾。
2.2 事件驱动与 I/O 多路复用
Worker 内部是一个事件循环(event loop):
- Linux 上用 epoll,BSD 上用 kqueue,Solaris 上用 /dev/poll------都是 OS 提供的 I/O 多路复用机制。
- 每个 worker 用一个线程 + 一个 epoll 实例,就能同时监视数万条 socket 的「可读 / 可写」事件。
- 配合非阻塞 socket + 异步回调:当某条连接可读/可写时才去处理,没有连接在「傻等」时线程被阻塞。
对比 Apache prefork/worker 模型(每连接占用一个进程或线程、被阻塞就空耗栈内存),Nginx 在大量空闲/长连接(如 keep-alive、WebSocket) 场景下内存和 CPU 优势极其明显。
2.3 请求处理的 11 个阶段(phase)
理解 Nginx 的 rewrite、access、proxy_pass 为什么「有时不生效」,关键是它把请求拆成了有序的 11 个处理阶段,各模块挂载在不同阶段:
-
POST_READ ------ 读取完请求头后(如 real_ip 在这里拿真实客户端 IP)
-
SERVER_REWRITE ------ server 块内的 rewrite
-
FIND_CONFIG ------ 根据 URI 匹配 location(不可介入)
-
REWRITE ------ location 内的 rewrite
-
POST_REWRITE ------ rewrite 后检查是否要重新匹配
-
PREACCESS ------ 预处理访问(如 limit_req、limit_conn 限流在这里生效)
-
ACCESS ------ 访问控制(allow/deny、auth_basic、auth_request)
-
POST_ACCESS ------ 访问阶段收尾
-
PRECONTENT ------ 内容生成前(try_files 在这里)
-
CONTENT ------ 真正生成响应内容(proxy_pass、fastcgi_pass、root/static 在这里)
-
LOG ------ 记录访问日志(即使出错也会走到这里)
实战启示:proxy_pass 属于 CONTENT 阶段,所以 if 里不能混用 proxy_pass(if 是 REWRITE 阶段,阶段错配是 Nginx 经典坑,见排错章节)。
2.4 反向代理与负载均衡
upstream 块定义一组后端,proxy_pass 把流量转过去:
upstream api_backend {
默认 round-robin(加权轮询)
server 10.0.1.11:8080 weight=3;
server 10.0.1.12:8080;
server 10.0.1.13:8080 backup; # 仅当其余全部不可用时启用
被动健康检查:30s 内失败 3 次则摘除 30s
(开源版没有 active health_check 指令,这是标准做法)
server ... max_fails=3 fail_timeout=30s;
}
server {
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;
}
}
负载均衡算法(开源版):
算法
指令
适用场景
加权轮询
默认 / server ... weight=
后端性能不均时按权重分流
最少连接
least_conn;
请求耗时差异大,避免压垮慢节点
IP 哈希
ip_hash;
需要会话保持(同 IP 落同节点,但节点增减会重映射)
一致性哈希
hash $request_uri consistent;
缓存场景,节点变化时最小化映射抖动
随机
random two least_conn;
大规模后端,结合最少连接随机选
健康检查:开源 Nginx 仅支持被动检查------max_fails + fail_timeout 把连续失败的后端暂时摘出,配合 proxy_next_upstream 在单请求内向另一个后端重试。主动探测(health_check 指令)是 NGINX Plus 能力;开源若要主动探活,可借助第三方模块(如 nginx-upstream-check-module),或在 K8s 场景下直接用 Pod 的 readinessProbe 把不健康的实例从 Endpoints 摘掉(这也是 Ingress Controller 天然免费的「健康检查」)。
2.5 配置层级与关键参数
配置层级:main(全局) → events → http → server → location。
几个决定性能与稳定性的核心参数:
-
worker_processes auto; ------ 通常设为 CPU 核数(Nginx 会自动取)。
-
worker_connections 10240; ------ 单个 worker 最大并发连接数;理论总并发 ≈ worker_processes × worker_connections。
-
worker_rlimit_nofile 65535; ------ worker 进程能打开的文件描述符上限(连接、日志、静态文件都吃 fd)。
-
keepalive_timeout 65; / keepalive 32;(upstream 内)------ 复用后端 TCP 连接,降低握手开销。
-
client_max_body_size 20m; ------ 上传体积上限,否则大文件 413。
-
proxy_read_timeout / proxy_send_timeout / proxy_connect_timeout ------ 反向代理三件套超时,直接关联 504。
三、实战演示
场景一:容器化部署 Nginx(反向代理 + 静态)
项目结构:
nginx-demo/
├── Dockerfile
├── conf/
│ └── default.conf
└── static/
└── index.html
Dockerfile(用官方镜像,挂载自定义配置):
FROM nginx:1.27-alpine
COPY conf/default.conf /etc/nginx/conf.d/default.conf
COPY static/ /usr/share/nginx/html/
EXPOSE 80
conf/default.conf:
server {
listen 80;
server_name demo.local;
静态前端
location / {
root /usr/share/nginx/html;
index index.html;
try_files uri uri/ /index.html; # SPA 兜底
}
反向代理 API
location /api/ {
proxy_pass http://backend-svc:8080/; # 注意结尾的 / 会剥离 /api/ 前缀
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
docker run 验证:
docker build -t nginx-demo .
docker run -d -p 8080:80 nginx-demo
curl -i http://localhost:8080/ # 返回静态页
curl -i http://localhost:8080/api/health # 转发到后端
场景二:作为 K8s Ingress Controller(串联已有博客)
在 K8s 里,Nginx 通常以 Deployment + hostNetwork 或 LoadBalancer Service 形式运行,监听 80/443,根据 Ingress 资源把流量路由到对应 Service。它的本质是「把 K8s 的 Ingress 规则翻译成 Nginx 配置,并 watch 变化动态 reload」。
细节(包括 Gateway API 的演进、路径/主机路由、TLS 终止)我已在《K8s 南北向流量治理(Ingress + Gateway API)》中展开,这里只强调一点工程经验:Ingress Controller 是「免费的被动健康检查 + 限流 + 重试」入口,因为后端 Pod 的 readiness 状态直接反映在 Endpoints 上,Nginx 不会把流量打到未就绪的 Pod。
场景三:加权 + 最少连接 + 故障转移
upstream order_svc {
least_conn;
server 10.0.2.21:8080 weight=2 max_fails=3 fail_timeout=15s;
server 10.0.2.22:8080 weight=2 max_fails=3 fail_timeout=15s;
server 10.0.2.23:8080 weight=1 max_fails=3 fail_timeout=15s;
}
server {
location /order/ {
proxy_pass http://order_svc;
proxy_next_upstream error timeout http_502 http_503 http_504;
proxy_next_upstream_tries 2;
proxy_connect_timeout 3s;
proxy_read_timeout 10s;
}
}
要点:proxy_next_upstream 让单次请求在后端报错/超时时自动换一个节点重试(最多 2 次),前提是幂等接口;非幂等写操作要谨慎开启,避免重复下单。
四、排错实战(重点)
4.1 502 Bad Gateway
现象:前端正常返回,但代理接口报 502。
根因(按出现频率):
- 后端服务根本没起来 / 端口不对 ------ 先 curl http://后端IP:端口/health 直连验证。
- proxy_pass 指向的 upstream DNS 在 Nginx 启动时已解析、之后后端 IP 变了(容器环境常见)------ 用 resolver 指令 + 变量形式 proxy_pass http://$upstream_host; 解决,或依赖 K8s Service 的 ClusterIP(稳定)。
- 后端是 FastCGI / uWSGI 但用了 proxy_pass(应该用 fastcgi_pass / uwsgi_pass)。
- 后端响应超大 header(如过长的 Set-Cookie),触发 upstream sent too big header ------ 调大 proxy_buffer_size / proxy_buffers。
定位:看 error.log 里具体行,例如:
2026/10/30 10:12:01 error 11#11: *5 connect() failed (111: Connection refused) while connecting to upstream
4.2 504 Gateway Timeout
现象:请求一直转圈,最终 504。
根因:后端处理慢或 Nginx 等待超时。
- 调大反向代理超时:proxy_connect_timeout 5s; proxy_read_timeout 30s; proxy_send_timeout 30s;(注意:调大只是「容忍更慢」,要同时从后端查慢查询 / 锁 / 下游依赖)。
- 确认后端链路:DB 慢查询、第三方 API 阻塞、GC 停顿。
- 若后端是长任务(报表导出),考虑异步化(先返回任务 ID,前端轮询),而不是无限拉长超时。
4.3 403 Forbidden
根因:
- 文件权限:Nginx worker 用户(如 nginx)对 root 目录或其上级没有执行(x)权限,无法 traverse 路径。典型:chmod 755 目录,确保每一级目录对 worker 用户可执行。
- index 缺失 + 没开 autoindex:location 下找不到 index 文件且未配置 autoindex on; → 403。
- SELinux:开启了 enforcing 且 httpd 上下文没设,用 setsebool -P httpd_can_network_connect 1 或 chcon -R -t httpd_sys_content_t /path。
- allow/deny 规则误伤。
4.4 too many open files / worker_connections 耗尽
现象:日志刷 socket() failed (24: Too many open files) 或 worker_connections are not enough。
排查与修复:
看 worker 实际能开多少
cat /proc/$(pgrep -o nginx)/limits | grep 'Max open files'
Nginx 配置兜底
worker_rlimit_nofile 65535;
events { worker_connections 10240; }
系统/容器层面:宿主机或容器 ulimit 也要放开
docker run --ulimit nofile=65535:65535 ...
或 systemd: Service LimitNOFILE=65535
内核参数也要配套(尤其是作为高并发入口):
/etc/sysctl.conf
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.ip_local_port_range = 1024 65535
sysctl -p
4.5 配置 reload 不生效 / 端口被占
nginx -t # 先校验语法,避免 reload 失败
nginx -s reload # 平滑重载(master 重 fork worker,不中断连接)
nginx -s stop / quit # 快速停止 / 优雅停止
坑:
- reload 后长连接 / 被代理的 keep-alive 后端连接可能仍由旧 worker 持有,旧 worker 要等连接自然结束才退出;若怀疑没生效,看 ps 里是否还有老 master/worker。
| | |
| --- | --- | - 端口被占导致新 worker 起不来:nginx: emerg bind() to 0.0.0.0:80 failed (98: Address already in use) ------ 多半是前一个实例没退出,或别的进程(如 Apache)占了端口,ss -lntp | grep :80 查占用方。
- 容器里 nginx -s reload 需要 master 的 PID 1 支持信号;用 docker kill -s HUP <容器> 或 kubectl exec 里 nginx -s reload。
4.6 重定向循环(Too Many Redirects)
典型场景:Nginx 前面还有一层 LB / CDN 做了 HTTPS 终止,后端收到的是 HTTP,应用又强制跳 HTTPS,于是无限跳。
解法:正确传递协议,并据此判断:
proxy_set_header X-Forwarded-Proto $scheme;
或者在最外层统一处理:
map http_x_forwarded_proto real_scheme {
default KaTeX parse error: Expected 'EOF', got '}' at position 26: ... https https; }̲ 若是「Nginx 自己 ht...host$request_uri;。
4.7 HTTPS / 证书握手失败
- 证书链不完整:只配了服务端证书没配中间 CA,ssl_certificate 应是「服务器证书 + 中间证书」拼接文件。客户端(尤其老系统 / Java)会因不信任中间 CA 握手失败。
- 协议 / 套件不匹配:ssl_protocols TLSv1.2 TLSv1.3; 关掉 TLSv1.0/1.1;ssl_ciphers 用现代套件。
- SNI 问题:多域名共用 443 时确保 server_name 正确,且证书覆盖该域名。
- OCSP / 证书过期:openssl s_client -connect host:443 -servername host 直接看证书信息与有效期。
listen 443 ssl;
ssl_certificate /etc/nginx/tls/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
五、总结
把 Nginx 用稳,记住这几条:
- 理解模型再改配置:Master/Worker + 事件驱动决定了它「高并发、配置热加载、单线程无锁」的特质;11 个处理阶段解释了 rewrite / proxy_pass / if 的种种「玄学」行为。
- 超时三件套是排错入口:proxy_connect/read/send_timeout 直接决定 504;后端慢优先查链路而非无限拉超时。
- fd 是隐性天花板:worker_rlimit_nofile + worker_connections + 宿主机 ulimit + 内核 somaxconn 要配套放开,否则高并发下静默丢连接。
- 502 先直连后端:绝大多数 502 是后端本身的问题(没起、端口错、DNS 缓存),Nginx 只是「如实上报」。
- 健康检查分层做:开源版用 max_fails/fail_timeout + proxy_next_upstream 被动摘除;K8s 场景直接吃 Pod readiness,免费又及时。
- 协议头传到位:X-Forwarded-For / Proto / Host 不传,后端的「真实 IP、是否 HTTPS、重定向」全会乱,进而引发重定向循环等连锁问题。
值得监控的指标:活跃连接数(ngx_http_stub_status 的 Active connections、Reading/Writing/Waiting)、请求速率、 upstream 响应 5xx 比例、TLS 握手成功率、worker 的 fd 使用率。把这些接进 Prometheus,Nginx 的线上行为就完全透明了。