这是《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
对着输出读三件事,这就是本章的全部骨架:
- PPID 全是 1:worker 都由 master fork 出来,worker 之间不通信、不共享普通内存(要共享只能显式开 zone,见 2.4);
- worker 数 = CPU 数 :官方镜像的
nginx.conf默认worker_processes auto,本机 11 核就 11 个 worker; 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. 常见坑
-
把
worker_connections当"总并发上限" 。现象:压测到几百并发就报worker_connections are not enough。根因:它是单 worker 的连接数,且代理场景一请求占两个连接。正解:按并发请求 × 2 ÷ worker 数反推,并同步抬高worker_rlimit_nofile与容器ulimits.nofile。 -
把
reload当"重建连接" 。现象:reload 后ps里旧 worker 赖着不走,内存短时接近翻倍,甚至新老 worker 同时写日志。根因:旧 worker 要服务完存量长连接/慢请求才退。正解:看ps里的is shutting down,配worker_shutdown_timeout限时;长连接场景别期望 reload 立即释放内存。 -
改完配置以为生效了,其实改的不是加载的那份 。现象:改
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)。 -
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)。 -
[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)。 -
团队里
nginx -v各不相同,指令有时候认识有时候不认识 。现象:http2 on;本机过、CI 挂。根因:本地用nginx:alpine(mainline 1.31.x),服务器用老稳定版 1.24。正解:固定镜像 tag(本教程一律nginx:stable-alpine),CI 里跑nginx -t;用人人都能跑的那条命令对齐版本(nginx -V),而不是凭记忆。 -
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。 -
对着
-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 篇故障案例库。