认识 Nginx

这是《Nginx 从入门到精通》系列的第 1 篇(全系列 15 篇 + 4 个实战项目)。目标:说清 Nginx 在系统里的位置、进程模型和性能来源,能回答「为什么 1 万并发下它比 Apache prefork 省内存」;并养成两个习惯------动手前先对版本,动脑前先想边界。

前置 :Linux 常用命令(ps、netstat、tail -f)、HTTP 报文与状态码基础、能装 Docker,不需要 Nginx 经验。 本文所有数字与输出均为 nginx 1.30.6(stable)实测,文末附自测 5 题与延伸阅读。


1. 先跑起来(5 分钟实验)

四条命令拿到「版本 + 进程树 + 响应头」,不用任何配置文件:

bash 复制代码
docker run -d --rm --name hello-nginx -p 8080:80 nginx:stable-alpine
docker exec hello-nginx nginx -v
docker exec hello-nginx ps -o pid,ppid,args
curl -si http://127.0.0.1:8080/ | head -5

预期输出(本机实测,worker 行按 CPU 数展开,此处截断):

text 复制代码
nginx version: nginx/1.30.6
    1     0 nginx: master process nginx -g daemon off;
   30     1 nginx: worker process      # 共 11 个(本机 11 核),此处截断
HTTP/1.1 200 OK
Server: nginx/1.30.6

对着输出读三件事,这就是本章的全部骨架:

  1. PPID 全是 1:worker 都由 master fork 出来,worker 之间不通信、不共享普通内存(要共享只能显式开 zone,见 2.4);
  2. worker 数 = CPU 数 :官方镜像的 nginx.conf 默认 worker_processes auto,本机 11 核就 11 个 worker;
  3. Server: 头带版本号 :默认 server_tokens on 会暴露版本,生产一般在接入层关掉(详见 第 10 篇)。

端口冲突:配套实验环境 lab/00-env 也占 8080。若已 docker compose up,把 -p 8080:80 改成 -p 8081:80 并 curl 8081。实验完 docker rm -f hello-nginx。

2. 核心概念

2.1 进程模型:一个 master,N 个 worker

进程 干什么 绝对不干什么 出事时的表现
master(1 个) 读配置并校验、绑端口、fork worker、收信号、拉起 cache manager/loader 不 accept 连接、不处理请求 它死了全站死;但 reload 时配置写错它拒绝切换,旧 worker 继续服务
worker(nproc 个) accept 连接、跑状态机、读上游/磁盘、写日志 不存业务状态、不互相通知 单个 worker 崩溃 master 会补一个新的;连接数达到 worker_connections 上限则新连接排队或被拒
cache manager(1 个) 周期扫描磁盘缓存,删过期的、删超量的 只在配置了 *_cache_path 时才存在 缓存目录无限增长、磁盘打满
cache loader(1 个) 启动时把磁盘上已有缓存的元数据装进共享内存 zone 同上,只在有 cache 配置时存在 重启后缓存"看起来全空了"(文件还在,索引没加载完)

涉及指令:worker_processes、worker_connections、worker_rlimit_nofile。

为什么这样设计 :master 只做"换人"这件事------reload 换 worker(第 3 篇 讲配置、第 2 篇 讲信号与热升级)、热升级换 master 与二进制。因此 Nginx 能在不断连接的前提下换掉全部配置、甚至换掉运行中的程序,这是"配置静态但仍可运维"的根基。

2.2 单线程事件循环:性能到底省在哪

模型 每连接的资源成本 1 万空闲长连接 上下文切换 扩展方式
Apache prefork 一个进程独占一套内存,实测常在 10MB 以上量级(看模块集) 100GB 级,做不到 重(进程切换) 加机器/加内存
Apache worker(线程) 一线程 + 栈(默认栈常为 MB 级) 受线程栈与调度限制,通常千级吃力 中(线程切换) 调线程池
Nginx worker(epoll) 一个 fd + 一个连接结构体 + 缓冲区 万级可行,几十 MB 到百 MB 级 几乎为零(单线程无切换) 加 worker(≈加核)
bash 复制代码
# 终端 A:60 个并发慢请求,每个占住 2 秒(lab/00-env 的 gateway,1 个 worker)
seq 1 60 | xargs -P 60 -I{} curl -s -m 10 -o /dev/null http://127.0.0.1:8080/slow/
# 终端 B:看 worker 的常驻内存与连接数
docker compose exec -T gateway sh -c 'W=$(ps -o pid,args | grep "worker process" | grep -v grep | awk "{print \$1}"); grep VmRSS /proc/$W/status; wget -qO- http://127.0.0.1/nginx_status | head -1'

实测结果:空闲 VmRSS: 3400 kB → 60 个并发在飞时 VmRSS: 4056 kB、Active connections: 61 → 请求结束后回落到 3412 kB。即单 worker 用不到 5MB 内存同时扛住 60 个连接,约 10KB/连接的增量主要是请求/响应缓冲区。

根因:epoll 是"就绪事件回调 + 每个连接一个小状态机",连接数增长只增加 fd 与缓冲区,不增加执行流;而 fork/thread 模型里每个连接都要一套栈和调度实体。外推 1 万连接的进程侧内存约百 MB 级(内核 socket buffer 另计),prefork 到 1 万进程是内存与调度双重不可行------这就是"为什么高并发下它更省内存"的可测答案。

推论(也是坑) :单线程意味着任何阻塞调用都会卡住整个 worker 。所以 Nginx 用 sendfile 绕开用户态拷贝、用 aio/thread_pool(≥1.7.11)把阻塞文件 IO 丢给线程池,并严禁在配置里做慢操作------这块归 第 12 篇。

2.3 请求生命周期 6 步

accept → 读请求头 → 选 server → 选 location → 内容处理 → 写响应

步 做什么 可介入的指令 详见
① accept 三次握手完成,从 listen socket 取连接,分配连接结构体 listen(backlog/reuseport/ssl) Ch2、Ch10
② 读请求头 解析请求行与头部、URI 解码、找 Host client_header_timeout、large_client_header_buffers Ch3
③ 选 server 按监听端口 + Host(TLS 下还有 SNI)挑 server 块 server_name、default_server 第 6 篇
④ 选 location 精确 = → 前缀(最长优先)→ 正则,定出处理块 location、root、alias 第 4 篇
⑤ 内容处理 静态文件/代理/return/error_page;rewrite 会回到 ④ 重选 proxy_pass、index、try_files、rewrite Ch4、第 5 篇
⑥ 写响应 过滤器链(gzip、add_header)、缓冲、落 access_log gzip、sendfile、access_log 第 8 篇、第 9 篇

内部实现是 11 个 phase:post_read → server_rewrite → find_config → rewrite → post_rewrite → preaccess → access → post_access → precontent → content → log。不必背全,但要记住三个高频落点:access 阶段 放鉴权与 allow/deny(Ch10)、preaccess 阶段 放 limit_req/limit_conn(Ch10)、precontent 阶段 放 try_files 与 mirror(Ch4)。"某条指令为什么不生效"的排查,八成是它不在你以为的那个阶段。

2.4 共享内存 zone:worker 之间唯一的通道

worker 之间不共享普通内存,要跨 worker 计数或缓存,必须显式声明 zone(匿名 mmap + slab 分配器)。

用途 声明指令 谁在用 详见
缓存索引 proxy_cache_path ... keys_zone=cache_x:10m cache loader/manager + 全部 worker Ch8
限流计数 limit_req_zone $binary_remote_addr zone=req:10m rate=10r/s 各 worker 原子累加 Ch10
连接数计数 limit_conn_zone $binary_remote_addr zone=conn:10m 同上 Ch10
upstream 共享状态 upstream { zone app_pool 64k; }(≥1.9.0) 各 worker 共享权重与失败计数 Ch5、第 7 篇
上游动态解析 server app1:8080 resolve;(≥1.27.3 ,必须与 zone 同用) 后台按 resolver 的 valid= 周期重解析 Ch7

为什么这样设计 :用无锁的 slab + 原子操作换掉互斥锁,代价是容量必须预先声明。注意三件事:size 默认单位是字节(64k = 65536),写小了会在 error_log 里报 zone "xxx" is exhausted;reload 时 zone 会被新老 worker 继承,所以缓存与限流计数不清零;而容器重建/进程重启会清零(匿名内存随进程消失)。

2.5 版本模型:先对版本,再抄配置

通道 版本号规律 本机实测 适合谁
mainline 主线 奇数小版本,如 1.31.x nginx:alpine = 1.31.8 尝鲜新指令、要 HTTP/3 相关修复
stable 稳定 偶数小版本,如 1.30.x nginx:stable-alpine = 1.30.6 生产、教程复盘(本教程全部实验用这个)
legacy 历史 上一代稳定分支 一般只在安全修复时更新 老系统

三条命令解决"我的版本有没有":

bash 复制代码
nginx -v                       # 一行版本号
nginx -V 2>&1 | head -3        # 版本 + 编译器 + OpenSSL(含运行时实际版本)
nginx -V 2>&1 | tr ' ' '\n' | grep -E '^--(with|add)' | sort   # 编译参数/静态模块清单

本机 nginx:stable-alpine 1.30.6 的清单里能看到 --with-http_v2_module、--with-http_v3_module、--with-stream、--with-compat、--with-http_stub_status_module、--with-threads。要点:

  • --with-* = 编译期已经静态编入,运行时不用 load_module;--add-dynamic-module 才是 .so;
  • --with-compat 是动态模块的 ABI 前提,自己编模块前先确认有它;
  • --with-cc-opt / --with-ld-opt 反映编译与链接选项,--with-openssl= 表示用了自定义 OpenSSL(此时以 nginx -V 里 "running with OpenSSL" 那行为准);
  • 编译进去了 ≠ 打开了 :有 --with-http_v3_module 还得写 listen 443 quic 才启用 HTTP/3(Ch10);
  • 如果某篇指南说"官方镜像不含 HTTP/3",那说的是 1.25 之前的老镜像------以你自己 nginx -V 的输出为准。这条习惯比记住任何版本号都值钱。

版本下限速查(完整表见系列附录 A):http2 on; ≥1.25.1 (更早只能写 listen 443 ssl http2;)、upstream resolve ≥1.27.3 、worker_shutdown_timeout ≥1.11.11 、reuseport ≥1.9.1 、keepalive_requests 默认 1.19.10 起为 1000(此前 100)。拿不准时以 nginx.org 的 changelog 为准。第 5 节实验 E5 会用两个镜像把这条差异当场跑出来。

2.6 生态位:什么时候用 Nginx,什么时候别用

软件 强项 短板 什么时候选它
Nginx(OSS) 静态文件 + 反向代理 + 七层路由,单机吞吐与内存效率极佳,运维资料最多 无动态配置 API、无熔断、无主动健康检查、无 WAF 静态站、常规反代、API 网关基座、K8s Ingress 底座
Apache httpd .htaccess 免重启改配置、mod_rewrite 表达能力极强、模块生态老而全 prefork/worker 模型在高并发下内存开销大 遗留 PHP/共享主机、重度依赖 .htaccess 的项目
HAProxy 四/七层负载均衡、健康检查与统计报表最专业 不做静态资源、复杂内容改写弱 纯负载均衡层,尤其是 TCP 层
Envoy xDS 动态配置、熔断与重试预算、可观测性原生 学习曲线陡,YAML/xDS 心智负担大 服务网格数据面、强动态配置需求
Caddy 默认自动 HTTPS(ACME),配置极简 高并发场景资料与调优经验少于 Nginx 个人站、内网小站
Traefik 容器/K8s 服务发现、自动路由 复杂七层改写不如 Nginx 灵活 K8s/容器边缘入口
APISIX / Kong 插件化网关(JWT、限流、灰度开箱) 多一层运维与延迟 需要网关级插件与动态配置

一句话选型:Nginx 是"静态 + 反代 + 七层路由"的稳定基座;要服务发现、熔断、动态配置,用它的上层(Ingress/APISIX)或换数据面(Envoy)。完整对照见系列附录 B。

2.7 典型拓扑位置

位置 常见抓手 详见
公网接入层(七层入口) listen/server_name/rewrite/TLS Ch4、Ch6、Ch10
静态资源层(图片/CSS/JS/前端包) root/alias/expires/gzip/sendfile Ch4、Ch8
TLS 终结 ssl_certificate/http2 on/OCSP Ch10
API 网关(路由 + 限流 + 鉴权) location + proxy_pass + limit_req + auth_request Ch5、Ch10
四层负载均衡(TCP/UDP) stream { } + proxy_pass + ssl_preread 第 13 篇
K8s Ingress ingress-nginx(Nginx 的 K8s 封装) Ch13

2.8 边界感:它不能做什么

开源 Nginx 没有 业务逻辑:没有 session、没有熔断器、没有主动健康检查、没有跨节点共享计数、没有原生 JWT/OIDC、没有 WAF、没有动态配置 API(配置只能整份改完 reload)。这不是缺陷而是设计取舍------它是数据面而非控制面。先建立这个边界感,后面才不会在每一章都问"为什么它不能动态"。这些能力的现实解法(auth_request、njs、Lua、vts、Ingress、Envoy)散落在 Ch10--Ch14,汇总在系列附录 B;第 15 篇 里也能看到"以为它能做、结果它不能"的典型事故。

3. 指令详解

指令 默认值 上下文 说明与坑
worker_processes 1 main 生产写 auto=可见 CPU 数;官方镜像默认已是 auto(本机 11 核 → 11 worker)。容器里跟随 CPU 亲和性(--cpuset-cpus=0-1 实测 auto=2 个 worker)。设太小单核打满、设太大加剧 worker 间分布不均(Ch12)
worker_connections 512 events 单个 worker 的连接上限,不是"并发请求数";同时受 worker_rlimit_nofile 与系统 ulimit -n 制约。代理场景每条请求占 2 个连接(客户端 + 上游),1024 实际只够约 500 并发请求
worker_rlimit_nofile 未设置(不改) main 让 Nginx 自己 setrlimit,省掉改 systemd LimitNOFILE 或容器 ulimits;建议 ≥ 2 × worker_connections
use 平台自动选最优 events Linux 上自动就是 epoll,不要手写;写错平台值会导致启动失败或退化
multi_accept off events on = 一次唤醒取多个新连接,突发流量下减少 epoll 唤醒次数;与 reuseport 组合的效果必须实测(Ch12)
error_log logs/error.log error main/http/server/location 级别 debug→info→notice→warn→error→crit;生产 warn,排障临时 info 后记得降回来(Ch9)
pid 编译期 --pid-path(官方包 /run/nginx.pid) main nginx -s reload 靠它定位 master,路径与启动时不一致会报 invalid PID number(见坑 7.4)
include 无 任意 通配符匹配不到文件不报错 ,非通配符文件缺失直接 [emerg]------用 include conf.d/*.conf; 做可选加载时别踩反了(Ch3)
upstream 的 zone 无 upstream ≥1.9.0 ;跨 worker 共享权重与失败计数;server ... resolve 强制依赖它(≥1.27.3)
resolver 无 http/server/location 不配则上游域名只在启动/reload 时解析一次,容器 IP 变化后一直打旧地址;lab 里指向 Docker 内置 DNS 127.0.0.11
worker_shutdown_timeout 无(不限) main ≥1.11.11 ;reload/停机时给"正在处理的请求"设上限,否则慢请求会让旧 worker 长期驻留
keepalive_timeout 75s http/server/location 空闲长连接存活时间;调大省握手但 fd 占用上升,配合 keepalive_requests 一起看(Ch12)
server_tokens on http/server/location 关掉后 Server: 只回 nginx,不暴露版本号
sendfile off http/server/location 静态文件零拷贝;打开后留意 sendfile_max_chunk,大文件场景别一次拷到底(Ch12)

4. 完整配置示例

下面是本章配套的完整主配置:把 lab/00-env(配套实验环境) 的 nginx/nginx.conf 与 nginx/conf.d/default.conf 合并成一个文件 ,其余保持对齐(容器内监听 80,宿主用 8080 映射;上游组 app_pool;日志 lab_access.log;主配置路径 /etc/nginx/lab/nginx.conf)。

nginx 复制代码
# /etc/nginx/lab/nginx.conf ------ Ch1 版(单文件,便于对照 nginx -T 输出)
user  nginx;
# 实验环境固定 1 个 worker:轮询/缓存/日志/连接数的行为完全可预测。
# 生产写 auto(= 可见 CPU 数);多 worker 下轮询计数各自独立、连接分布不均的问题在 Ch12 讲。
worker_processes  1;

# worker_connections 1024 是"单 worker 连接数",代理场景每条请求占 2 个连接,
# 所以这个 rlimit 至少要给到 2 × 1024,留出上游连接和日志文件的 fd。
worker_rlimit_nofile  2048;

# ≥1.11.11:reload/停机时给正在处理的请求设上限,避免旧 worker 因慢请求长期驻留
worker_shutdown_timeout  10s;
error_log  /var/log/nginx/lab_error.log warn;
pid        /var/run/nginx.pid;

events {
    # use epoll;        # Linux 上自动选择,写死反而容易在别的平台起不来
    # multi_accept off; # 默认值,突发流量场景可试 on(Ch12 用 wrk 验证)
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    server_tokens off;            # 不在响应头暴露版本号(Ch10 还会补安全头)

    # 关键字段:$request_time 总耗时;$upstream_response_time 可多值;
    # $upstream_addr 多值对应重试链;$http_x_forwarded_for 看真实客户端
    log_format  main  '$remote_addr - $remote_user "$request" '
                      '$status $body_bytes_sent rt=$request_time '
                      'urt=$upstream_response_time up=$upstream_addr '
                      'xff="$http_x_forwarded_for"';

    access_log  /var/log/nginx/lab_access.log  main;

    sendfile           on;
    keepalive_timeout  65;

    gzip on;
    gzip_types text/css application/javascript application/json text/plain;

    # 容器/云的 IP 会变,upstream 域名默认只在 reload 时解析。
    # 配 zone + resolve + resolver 后,Nginx 会按 valid 周期自己重解析(Ch7)。
    resolver 127.0.0.11 valid=5s ipv6=off;   # 127.0.0.11 = Docker 内置 DNS

    # zone ≥1.9.0:跨 worker 共享权重与失败计数;server ... resolve 需要它(≥1.27.3)
    upstream app_pool {
        zone app_pool 64k;
        server app1:8080 resolve max_fails=3 fail_timeout=10s;
        server app2:8080 resolve max_fails=3 fail_timeout=10s;
        server app3:8080 resolve max_fails=3 fail_timeout=10s;
    }

    server {
        listen      80;          # 宿主 8080 由 compose 的 ports: "8080:80" 映射
        server_name _;

        root  /usr/share/nginx/html;
        index index.html;

        # 本机可达状态页(生产必须 allow 内网):本章实验 E4 用它读连接数
        location = /healthz {
            access_log off;
            return 200 "gateway ok\n";
        }

        location /nginx_status {
            stub_status;
            allow 127.0.0.1;
            deny  all;
        }

        # 静态资源长缓存(alias + expires 的更多组合见 Ch4)
        location /static/ {
            alias /usr/share/nginx/html/;
            expires 7d;
            add_header Cache-Control "public";
        }

        # proxy_pass 不带 URI ------ 原始路径透传(/slow/x 原样给后端)
        location /slow/ {
            proxy_pass http://app_pool;
            proxy_set_header Host $host;
            proxy_read_timeout 5s;      # 后端 sleep 2s,故意留余量
        }

        # proxy_pass 带 URI(结尾 /)------ 前缀被替换(/api/json → /json)
        location /api/ {
            proxy_pass http://app_pool/;
            proxy_connect_timeout 2s;   # 连不上上游时快速失败(默认 60s 才 502/504)
            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;
        }

        # SPA 回退(/api、/static 在上面已优先命中)
        location / {
            try_files $uri $uri/ /index.html;
        }
    }
}

相对 lab/00-env 只改了这 4 处,别的一行没动:

改动 位置 为什么
把 conf.d/default.conf 合并进主配置 整个 server 块 本章只讲架构,单文件便于和 nginx -T 的实际生效结果对照;Ch3 起恢复 include 拆分
新增 worker_rlimit_nofile 2048; main 给 worker_connections 1024 留出上游连接与日志 fd 的余量
新增 worker_shutdown_timeout 10s; main ≥1.11.11;配合实验 E2 观察旧 worker 退出,给慢请求设上限
新增 server_tokens off; http 呼应第 1 节的 Server: 头,Ch10 继续补安全头

其余(user、worker_processes 1、resolver 127.0.0.11、upstream app_pool 三成员、/api/ /slow/ /static/ /nginx_status、日志路径)与 lab 完全一致。校验方式见本章末尾的命令块。

5. 必做实验

E1 读版本与编译参数,判断"能不能用某个指令"

bash 复制代码
docker compose -f lab/00-env/docker-compose.yml exec -T gateway nginx -v
docker compose -f lab/00-env/docker-compose.yml exec -T gateway sh -c "nginx -V 2>&1 | tr ' ' '\n' | grep -E '^--(with|add)' | sort"

预期:第一行 nginx version: nginx/1.30.6;第二行输出含 --with-http_v2_module、--with-http_v3_module、--with-stream、--with-compat、--with-http_stub_status_module 等约 25 项。

为什么是这个结果:-V 打印的是 configure 参数 ,即"编译期决定了哪些模块在二进制里"。stub_status 出现在清单里,实验 E4 的 /nginx_status 才能工作;若清单里没有 --with-http_auth_request_module,Ch10 的 auth_request 一写就是 unknown directive。

E2 master/worker 关系与 reload 的退出顺序

bash 复制代码
# 终端 A:占住一个 2 秒的慢请求
curl -s http://127.0.0.1:8080/slow/ &
# 终端 B:立刻 reload 并看进程树
docker compose -f lab/00-env/docker-compose.yml exec -T gateway nginx -s reload -c /etc/nginx/lab/nginx.conf
docker compose -f lab/00-env/docker-compose.yml exec -T gateway ps -o pid,ppid,args
sleep 3
docker compose -f lab/00-env/docker-compose.yml exec -T gateway ps -o pid,ppid,args

预期(本机实测):

text 复制代码
    1     0 nginx: master process nginx -c /etc/nginx/lab/nginx.conf -g daemon off;
   30     1 nginx: worker process is shutting down   # 老 worker,服务完存量连接才退
   66     1 nginx: worker process                    # 新 worker,已开始 accept
# sleep 3 之后:只剩 master(1) + 新 worker(66),老 worker 30 已消失

为什么是这个结果:reload 只是给 master 发 HUP。master 校验新配置 → fork 新 worker(66)并让它开始 accept → 给老 worker(30)发 QUIT,老 worker 服务完存量连接 才退出,所以 2 秒内它以 is shutting down 挂着;请求一结束它就消失。master 的 PID 始终是 1------reload 不重启进程 ,这既是"改配置不断连接"的原因,也是"旧 worker 长期驻留、内存双峰"的原因(worker_shutdown_timeout 就是给它兜底的)。

E3 listen 与端口映射是两件事

bash 复制代码
docker compose -f lab/00-env/docker-compose.yml exec -T gateway netstat -tlnp
lsof -nP -iTCP:8080 -sTCP:LISTEN          # macOS 宿主;Linux 用 ss -tlnp | grep 8080

预期:容器内只有 0.0.0.0:80 在 LISTEN(PID 1 是 master);宿主上 8080 由 Docker 的代理进程持有。容器里永远看不到 8080。

为什么是这个结果:listen 80 是容器网络命名空间内的监听;ports: "8080:80" 是 Docker 在宿主侧做的 DNAT。所以"curl 8080 被拒"时先分清是宿主端口没映射、还是容器内 Nginx 没起------这一条能省掉大量把网络问题误判成 upstream 故障的时间(Ch9 有同类排查)。

E4 worker_connections 1024 ≠ 1024 并发请求

bash 复制代码
# 终端 A:60 个并发慢请求(每个 2 秒)
seq 1 60 | xargs -P 60 -I{} curl -s -m 10 -o /dev/null http://127.0.0.1:8080/slow/
# 终端 B:读状态页
docker compose -f lab/00-env/docker-compose.yml exec -T gateway wget -qO- http://127.0.0.1/nginx_status

预期(本机实测,峰值):

text 复制代码
Active connections: 61
server accepts handled requests
 374 374 374
Reading: 0 Writing: 61 Waiting: 0

为什么是这个结果:61 = 60 条客户端连接 + 读状态页这一条。关键在于这 60 条请求每条还占一个到上游的连接 ,所以此刻单 worker 已用掉约 122 个连接配额------worker_connections 1024 折合只有约 500 并发请求。再加 worker_processes 1,全站连接上限就是 1024。因此 worker_connections 要按「并发请求数 × 2(代理)÷ worker 数」反推,而不是直接当并发数(公式与压测见 Ch12)。

E5 版本门槛:同一个配置,两个镜像,两种结果

bash 复制代码
mkdir -p /tmp/ngxver && cat > /tmp/ngxver/t.conf <<'EOF'
events {}
http { server { listen 8080; http2 on; } }
EOF
docker run --rm -v /tmp/ngxver:/c:ro nginx:1.24-alpine nginx -t -c /c/t.conf
docker run --rm -v /tmp/ngxver:/c:ro nginx:stable-alpine nginx -t -c /c/t.conf

预期(本机实测):

text 复制代码
nginx: [emerg] unknown directive "http2" in /c/t.conf:5      # 1.24 不认识
nginx: the configuration file /c/t.conf syntax is ok
nginx: configuration file /c/t.conf test is successful       # 1.30 通过

为什么是这个结果:http2 on; 是 ≥1.25.1 才有的写法,之前只能写 listen 443 ssl http2;(且 1.25.1 起旧写法会告警)。nginx -t 是最便宜的版本探针:不确定就先 -t,别等 reload 到生产才发现。

6. 验收自测(5 题)

先盖住"答案要点"自己写一遍,再逐条对照;答不上来的回对应小节重读。

Q1. worker_connections 1024 是不是"最多支持 1024 个并发"?在 worker_processes 1 + 反向代理场景下,实际能支撑多少并发请求?

答案要点 :不是。它只是单个 worker 的连接上限 (默认 512),总上限 ≈ worker_processes × worker_connections。反向代理时每条请求占 2 个连接(客户端 + 上游),所以 1 worker + 1024 只够约 500 并发请求;另外还要保证 worker_rlimit_nofile(或系统 ulimit -n)不低于这个数,否则先撞 fd 上限,error_log 里出现 worker_connections are not enough。

Q2. 执行 nginx -s reload 后,旧 worker 什么时候退出?为什么"配置写错时 reload 不会打挂站点"?

答案要点 :reload = 向 master 发 HUP:master 先校验新配置 (等价 nginx -t),失败则整个 reload 放弃、旧 worker 继续服务;成功才 fork 新 worker、给旧 worker 发 QUIT,旧 worker 服务完存量连接 后自动退出(ps 里显示 worker process is shutting down)。master PID 不变,所以不是重启。慢请求会让旧 worker 长期驻留,用 worker_shutdown_timeout(≥1.11.11)兜底。

Q3. 写出请求生命周期 6 步,并给每一步至少一个可介入的指令。

答案要点 :① accept 建连 → listen(reuseport/backlog/ssl);② 读请求头 → client_header_timeout、large_client_header_buffers;③ 选 server → server_name、default_server;④ 选 location → location、root、alias;⑤ 内容处理 → proxy_pass、try_files、return、rewrite(rewrite 会回到 ④ 重选);⑥ 写响应 → gzip、add_header、sendfile、access_log。能补一句"内部是 11 个 phase,access 阶段放鉴权"更佳。

Q4. 为什么 1 万连接下 Nginx 撑得住而 Apache prefork 不行?给出至少两个可测指标。

答案要点 :模型差异:prefork 每连接一个进程(独立内存 + 进程切换),Nginx 是单线程 epoll 事件循环 + 每连接一个状态机。可测指标:① worker 进程 RSS------本机实测 1 worker 空闲 VmRSS 3400 kB,60 并发在飞 4056 kB,结束回落 3412 kB(约 10KB/连接);② ps 里的进程/线程数(Nginx 恒定,prefork 随并发线性增长);③ 上下文切换(vmstat 的 cs);④ stub_status 的 Active connections 与 fd 占用(ls /proc/<pid>/fd | wc -l)。反推 1 万连接约百 MB 级,而 1 万进程是内存与调度双重不可行。

Q5. zone 解决什么问题?举 3 个依赖它的场景;为什么 reload 后缓存还在、而容器重建后缓存没了?

答案要点 :worker 之间不共享普通内存,zone 是显式声明的跨 worker 共享内存区(匿名 mmap + slab + 原子操作)。依赖它的场景:proxy_cache_path ... keys_zone=(缓存索引)、limit_req_zone/limit_conn_zone(限流计数)、upstream { zone app_pool 64k; }(共享权重与失败计数,且 server ... resolve ≥1.27.3 强制要求它)。reload 时 master 把同一块 zone 交给新老 worker 共用,所以缓存与计数不清零;进程/容器重启后匿名内存随进程消失,缓存文件虽在磁盘但索引要由 cache loader 重新加载。

7. 常见坑

  1. 把 worker_connections 当"总并发上限" 。现象:压测到几百并发就报 worker_connections are not enough。根因:它是单 worker 的连接数,且代理场景一请求占两个连接。正解:按 并发请求 × 2 ÷ worker 数 反推,并同步抬高 worker_rlimit_nofile 与容器 ulimits.nofile。

  2. 把 reload 当"重建连接" 。现象:reload 后 ps 里旧 worker 赖着不走,内存短时接近翻倍,甚至新老 worker 同时写日志。根因:旧 worker 要服务完存量长连接/慢请求才退。正解:看 ps 里的 is shutting down,配 worker_shutdown_timeout 限时;长连接场景别期望 reload 立即释放内存。

  3. 改完配置以为生效了,其实改的不是加载的那份 。现象:改 conf.d/default.conf 毫无变化,nginx -T 里看不到自己的改动。根因:容器用 -c /etc/nginx/lab/nginx.conf 启动,而该文件 include /etc/nginx/lab/conf.d/*.conf;改了别的路径(例如镜像默认 /etc/nginx/conf.d/)自然不生效。正解:一律用 nginx -T(会打印合并后的完整配置)确认"谁真的生效了"(Ch3)。

  4. nginx -s reload 报 invalid PID number "" in "/run/nginx.pid" 。现象:命令直接失败,配置并没生效。根因:-s 靠 pid 指令指定的文件找 master,nginx -s reload 没带 -c 时读的是默认配置里的 pid 路径,与实际启动用的配置不一致(lab 主配置里是 /var/run/nginx.pid)。正解:带上同一个 -c,或直接用 docker compose restart/kill -HUP 1(容器里 master 就是 PID 1)。

  5. [emerg] "resolve" parameter requires "zone" directive 。现象:抄来 server app1:8080 resolve; 启动即失败。根因:resolve(≥1.27.3 )必须在声明了 zone 的 upstream 里用,且要配 resolver。正解:upstream 加 zone app_pool 64k;,http 里加 resolver 127.0.0.11 valid=5s;(Docker 环境),版本不够就退回"reload 才重解析"的老行为(Ch7)。

  6. 团队里 nginx -v 各不相同,指令有时候认识有时候不认识 。现象:http2 on; 本机过、CI 挂。根因:本地用 nginx:alpine(mainline 1.31.x),服务器用老稳定版 1.24。正解:固定镜像 tag(本教程一律 nginx:stable-alpine),CI 里跑 nginx -t;用人人都能跑的那条命令对齐版本(nginx -V),而不是凭记忆。

  7. worker_processes auto 在容器里数错了核数 。现象:docker stats 里 Nginx CPU 上不去,但 ps 里十几个 worker。根因:auto 取的是进程可见的 CPU 数;容器没设 cpuset 时就是全部可见核(本机 11 核 → 11 worker,实测 --cpuset-cpus=0-1 时 auto 变 2)。正解:容器里显式写 worker_processes 数字,或给 compose 设 cpuset;调优细节在 Ch12。

  8. 对着 -V 清单想当然 。现象:指南说"官方镜像不含 HTTP/3",于是去自己编译,结果发现镜像里早就有 --with-http_v3_module。根因:镜像的编译参数随版本变化。正解:先跑 nginx -V 看清单,再决定要不要自编译;查不到就"以官方文档/changelog 为准"(附录 A 给的是硬性下限,不是镜像清单)。

8. 延伸阅读

文档页 解决什么问题
Beginner's Guide 官方入门:配置结构、静态站、反向代理的最小例子
Controlling nginx -s reload/stop/quit 与信号语义,配合实验 E2 读
ngx_core_module worker_processes、worker_rlimit_nofile、worker_shutdown_timeout、events/worker_connections 的官方定义
ngx_http_upstream_module upstream zone、server ... resolve、max_fails/fail_timeout
ngx_http_stub_status_module 实验 E4 各字段(Active/Reading/Writing/Waiting)的准确含义
nginx CHANGES 版本下限的唯一权威出处;拿不准某指令几版起有时就看这里

章节依赖 :本章不依赖任何前置章;第 2 篇(安装/信号/热升级)、第 3 篇(上下文与 nginx -T)直接建立在 2.1--2.3 之上;第 5 篇 用到本章的 upstream app_pool 与连接数概念;第 7 篇 承接 2.4 的 zone 与 resolve;第 9 篇 与 第 12 篇 会用本章的观测手法做定量分析。

本章配置的自校验 (提交前必跑,等价于 CI 里的 nginx -t):

bash 复制代码
mkdir -p /tmp/ngxcheck    # 第 4 节配置写入 site.conf,upstream 三成员换成 127.0.0.1:8080
docker run --rm -v /tmp/ngxcheck:/c:ro nginx:stable-alpine nginx -t -c /c/site.conf

预期结尾两行:syntax is ok / test is successful------CI 里把这条当门禁。


系列导航:第 1 篇(本篇)进程模型与能力边界 → 第 2 篇安装升级 → 第 3 篇配置语法 → 第 4 篇静态站与 location → 第 5 篇反向代理 ... 第 15 篇故障案例库。

相关推荐
红尘散仙28 分钟前
谁说终端只能敲键盘?我用 Rust 给小说阅读器加上了鼠标
前端·后端·rust
特立独行的猫A1 小时前
我给女儿写了个打字练习:一边敲键盘,一边把小学英语单词记牢
java·后端·python
陈随易2 小时前
C++26 草案被否,二十八年来头一回
前端·后端·程序员
金銀銅鐵2 小时前
同一个 Hello World,Kotlin 和 Java 在字节码层面差在哪
jvm·后端·kotlin
IT_陈寒5 小时前
Redis内存暴涨,没想到是AOF重写捣的鬼
前端·人工智能·后端
ikoala5 小时前
让 Codex 自己查账:一个省 token 的小技巧
前端·后端·ai编程
wo_jack9 小时前
C++ 入门笔记(十六):第16课 vector 模拟实现十问十答 —— 从三指针到迭代器失效
c++·后端·面试·stl·vector
量化分析码农10 小时前
【Python量化波动率实战 02】波动率会传染:EWMA 指数加权给近期行情更大话语权
后端
GISer_Jing11 小时前
如何学习Agent
后端·langchain