跳到正文

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_processes1发行版常配 auto(等于核数)
worker_connections512代理场景客户端和上游各占一个连接
keepalive_timeout75s客户端长连接空闲上限
client_max_body_size1m超过返回 413,不是 502
proxy_connect_timeout60s通常不能超过 75s
proxy_read_timeout60s两次读之间,不是整个响应时长
proxy_send_timeout60s两次写之间
proxy_buffering / proxy_request_bufferingon / on流式接口要关 buffering
max_fails / fail_timeout1 / 10s被动健康检查,失败即摘除
proxy_next_upstreamerror timeout默认不重试上游返回的 502
limit_req_status / limit_conn_status503 / 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_* 变量分清阶段,再决定修后端还是调超时。

把一门技术讲透到既懂也会用