这是《Nginx 从入门到精通》系列的第 2 篇(全系列 15 篇 + 4 个实战项目)。目标:在任意环境(Debian、RHEL、容器、自编译)装好、升级、回滚 Nginx;能独立读懂
nginx -V,并说清「什么时候必须源码编译」。前置 :第 1 篇(认识 Nginx)的 master/worker 模型与信号基础;实验复用
lab/00-env。 本文所有数字与输出均为 nginx 1.30.6(stable)实测,文末附自测题与延伸阅读。
1. 先跑起来(5 分钟实验)
两条命令看清"装的是什么",再起环境。先读编译参数------这是本章后面所有判断的依据:
bash
docker run --rm --entrypoint nginx nginx:stable-alpine -V 2>&1 | head -5
预期输出(版本号随镜像更新,以 https://nginx.org/en/CHANGES 为准;本文实测时 nginx:stable-alpine = 1.30.6、nginx:alpine = 1.31.8):
text
nginx version: nginx/1.30.6
built by gcc 15.2.0 (Alpine 15.2.0)
built with OpenSSL 3.5.8 25 Aug 2026 (running with OpenSSL 3.5.9 29 Sep 2026)
TLS SNI support enabled
configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf ... --with-compat --with-threads --with-http_ssl_module --with-http_v2_module --with-http_v3_module --with-http_stub_status_module --with-stream ...
注意 --entrypoint nginx:官方镜像的 entrypoint 会先跑 /docker-entrypoint.d/ 脚本,不绕过它就只能看到一堆 Launching ... 日志。然后起实验环境:
bash
cd nginx/lab/00-env
docker compose up -d # 5 个容器:gateway + app1/app2/app3
docker compose ps # gateway 立即 running,app 约 10s 后 healthy
curl -s localhost:8080/healthz
预期输出:
text
gateway ok
宿主 8080 → 容器 80;网关主配置是挂载进来的 /etc/nginx/lab/nginx.conf,由 command 的 -c 指定。这条事实贯穿全章:在这个环境里,nginx -t 不带 -c 校验的是镜像自带的默认配置,等于没校验。
2. 核心概念
2.1 四条安装路径怎么选
| 路径 | 典型命令 / 镜像 | 版本 | 升级方式 | 什么时候选 | 代价 |
|---|---|---|---|---|---|
| 发行版仓库 | apt install nginx、dnf install nginx |
滞后(常落后 1--2 个分支) | 包管理器换二进制 + reload |
只想跑静态站 / 反代,跟系统补丁走 | 缺 HTTP/3、第三方模块难加;发行版还改了默认目录与 nginx.conf |
| nginx.org 官方源 | 按 linux_packages.html 配 deb/rpm 源,可选 stable / mainline |
新(实测 stable 1.30.x、mainline 1.31.x) | apt upgrade nginx / dnf update nginx + reload |
生产首选:新版本、官方目录约定、可预测 | 需要自己固定版本、自己管 repo 优先级 |
| 官方 Docker 镜像 | nginx:stable-alpine、nginx:alpine |
同上,随 tag 滚动 | 换镜像 + 重建容器 | 本书实验、不可变基础设施、K8s Ingress 底座 | pid/日志/临时目录都在容器内;user 与权限坑(见坑 6) |
| 源码自编译 | ./configure && make && make install |
完全自选(含打补丁) | USR2 热升级(见 2.4) |
要 HTTP/3、brotli、njs 等官方包未带的模块,或要自定义路径/编译选项 | 自己管编译、自己管 CVE、自己管模块 ABI |
两条附加路线:OpenResty (Nginx + LuaJIT + lua-resty-*,见 Ch11);rootless 镜像 nginxinc/nginx-unprivileged(以非 root uid 101 运行,默认监听 8080,pid 与临时文件挪到可写目录,适配 K8s runAsNonRoot 基线;具体路径以该镜像 README 为准)。
判断口径只有一句:能用包或镜像解决的,别自编译 ;自编译的每一份配置都要进 Git,并把 nginx -V 的输出一起存档------否则半年后没人知道这台机器上的 nginx 还能不能 load_module。
2.2 编译参数:nginx -V 是唯一可信源
nginx -V = 版本 + 全部 ./configure 参数(-v 只给版本)。它回答三个问题:哪些模块是静态编进去的 (--with-*)、动态模块该放哪 (--modules-path)、默认路径是什么 (--prefix、--conf-path、--pid-path、--error-log-path)。
本章要认识的参数:
| 参数 | 作用 | 不写会怎样 |
|---|---|---|
--prefix |
安装根目录(决定默认 logs/、conf/ 位置) |
落到 /usr/local/nginx,与系统包打架(见坑 3) |
--modules-path |
动态模块搜索目录,load_module modules/x.so; 的相对基准 |
load_module 找不到文件(dlopen() ... failed) |
--with-http_ssl_module |
HTTPS 基础,主流版本默认不带,必须显式开 | 配置里出现 ssl_certificate 直接 unknown directive |
--with-http_v2_module |
HTTP/2 | http2 on;(≥1.25.1)报 unknown directive |
--with-http_v3_module |
HTTP/3(listen 443 quic,≥1.25.0 实验性) |
同上 |
--with-stream |
四层(TCP/UDP)代理 | stream { } 块报 unknown directive |
--with-compat |
允许动态模块与主程序按不同选项编译仍可加载 | 第三方 .so 加载时 ABI 不匹配(见坑 4) |
--add-module |
第三方模块静态编入 | --- |
--add-dynamic-module |
第三方模块编成 .so,运行期 load_module |
--- |
实测:
nginx:stable-alpine1.30.6 的-V里已带--with-http_v3_module。规划附录 A 写"官方镜像不含 HTTP/3"是旧信息;判断方法永远是现场跑-V,不要背结论。
2.3 信号:Nginx 的运维接口
Nginx 没有配置 API,一切运行期操作都是给 master 发信号 (nginx -s <sig> 只是读 pid 文件后 kill------所以 -s 不需要 -c,实测在 lab 网关里不带 -c 也能 reload 成功)。
| 信号 | -s 写法 |
语义 | 对存量连接 | 典型用途 |
|---|---|---|---|---|
HUP |
reload |
重新读配置,起新 worker,老 worker 优雅退出 | 不断(老连接由老 worker 服务完) | 改配置生效 |
QUIT |
quit |
优雅停机 | 服务完再退 | 缩容、灰度下线 |
TERM / INT |
stop |
快速停机 | 立即断 | 卡死时的最后手段 |
USR1 |
reopen |
重开日志文件 | 不影响 | 配合 logrotate(见坑 7) |
USR2 |
--- | 启动新二进制的新 master(热升级) | 不影响 | 换二进制 |
WINCH |
--- | 让老 worker 优雅退出、master 保留 | 逐步退 | 热升级第 3 步 |
2.4 平滑升级与回滚(5 步)
顺序不能错,每一步都有可验证的产物:
| # | 动作 | 目的 | 验证 |
|---|---|---|---|
| 0 | 备份 conf + nginx -t -c <你的主配置> |
杜绝"带病升级" | syntax is ok / test is successful |
| 1 | 换二进制,kill -USR2 $(cat /run/nginx.pid) |
老 master 把 pid 改名 nginx.pid.oldbin,fork 出新 master + 新 worker |
ls /run/nginx.pid* 两个文件;ps 里两个 master process |
| 2 | 打流量验证(curl / 慢请求) |
确认新二进制在这套配置下真能干活 | 状态码正常、日志字段正常 |
| 3 | kill -WINCH $(cat /run/nginx.pid.oldbin) |
老 worker 优雅退出,老 master 留着当退路 | ps 里老 worker 消失,服务不中断 |
| 4 | kill -QUIT $(cat /run/nginx.pid.oldbin) |
老 master 退出,nginx.pid.oldbin 被清理 |
只剩一棵进程树 |
两个必须知道的前提(实测得来,见实验 5):① 老的 nginx 进程必须用绝对路径启动 (/usr/sbin/nginx),否则 USR2 里 execve() 找不到 "nginx" 直接失败;② USR2 的目标是 master ,容器里若 nginx 就是 PID 1,最后一步 QUIT 老 master 等于停容器------线上热升级要在"nginx 由 init/systemd 托管"的宿主机进程模型下做。
回滚 :任何一步发现问题 → 给老 master 发 HUP(它用内存里的旧配置重启 worker,不读磁盘),再给新 master 发 QUIT。全程不重启机器、不断连接,这是 Nginx 敢在线上换二进制的底气。
2.5 目录约定:先认路再改配置
| 项目 | Debian/Ubuntu 包 | nginx.org / RHEL 系 | 官方 Docker 镜像(本书) |
|---|---|---|---|
| 主配置 | /etc/nginx/nginx.conf |
/etc/nginx/nginx.conf |
/etc/nginx/nginx.conf(本书被 -c 换成 /etc/nginx/lab/nginx.conf) |
| 站点配置 | conf.d/、sites-available/ + sites-enabled/(软链) |
conf.d/*.conf |
conf.d/*.conf |
| 动态模块 | /usr/lib/nginx/modules + modules-enabled/*.conf |
/usr/lib64/nginx/modules |
/etc/nginx/modules → /usr/lib/nginx/modules(空目录) |
| 站点根 | /var/www/html |
/usr/share/nginx/html |
/usr/share/nginx/html(本书挂载 lab/00-env/static) |
| 日志 | /var/log/nginx/{access,error}.log |
同左 | /var/log/nginx/,本书写 lab_access.log / lab_error.log |
| pid | /run/nginx.pid |
/run/nginx.pid |
/run/nginx.pid(本书配置写 /var/run/nginx.pid,Alpine 上二者是同一文件) |
| 临时目录 | /var/lib/nginx/{client_body,proxy,...} |
/var/cache/nginx/*_temp |
/var/cache/nginx/*_temp |
两个必须记住的差异:(1) 站点根不同 ------从 apt 装的环境把 root /usr/share/nginx/html; 抄过去会 403/404;(2) sites-enabled 是 Debian 的软链约定 ,include 的 glob 按字典序加载,多站点时顺序敏感(Ch3 用 -T 展开 include 顺序)。
2.6 静态模块 vs 动态模块
静态编入(--add-module、--with-*) |
动态模块(--add-dynamic-module → .so) |
|
|---|---|---|
| 配置写法 | 无 | load_module modules/ngx_xxx.so;(main 上下文 ,必须在 events/http 之前) |
| 生效时机 | 编译期 | 每次 reload 都要能被加载 |
| 升级二进制 | 必须整包重编 | 换主程序后 .so 通常可复用(同大版本内) |
忘写 load_module |
不存在此问题 | 该模块的指令报 unknown directive,或 worker 启动即崩 |
| 适用 | 核心功能、追求极简 | 第三方/非核心:geoip、brotli、njs、vts |
3. 指令详解
| 指令 | 默认值 | 上下文 | 说明与坑 |
|---|---|---|---|
load_module |
无 | main |
只在 main;写进 http 里直接 directive is not allowed here。路径相对 --modules-path,也可写绝对路径。升级后漏这一行是最常见的"配置没变却起不来" |
user |
编译参数 --user 决定(镜像 nginx,Debian 包 www-data) |
main |
只影响 worker。rootless 容器里写它反而 [emerg] getpwnam("nginx") failed |
worker_processes |
1 |
main |
auto = CPU 核数。本书实验固定 1:轮询、缓存、日志完全可预测;多 worker 差异在 Ch12 |
worker_connections |
512 |
events |
每 worker 连接上限,不是并发请求数:反代场景一个请求同时占"客户端 + 上游"两个连接 |
pid |
编译参数 --pid-path(/run/nginx.pid) |
main |
-s 与 USR2 都靠它找 master。热升级后同目录出现 nginx.pid.oldbin |
daemon |
on |
main |
容器里必须 off(官方镜像启动时自动加 -g "daemon off;"),否则容器立刻退出 |
master_process |
on |
main |
只在 gdb 单进程调试时设 off;off 后所有信号语义失效(没有 master) |
worker_shutdown_timeout |
无(不限,≥1.11.11) | main |
reload/停机时老 worker 的兜底上限;长连接站不设,老 worker 可能挂几天不退,看起来像"内存泄漏" |
worker_rlimit_nofile |
继承系统限制 | main |
抬 fd 上限(systemd 的 LimitNOFILE 要同步改),只改配置不改 limits 无效 |
include |
--- | 任意 | include /etc/nginx/lab/conf.d/*.conf;;glob 无匹配不报错(静默失效的经典原因),按字典序加载 |
error_log |
logs/error.log(镜像 /var/log/nginx/error.log) |
main,http,mail,stream,server,location |
级别 debug/info/notice/warn/error/crit;debug 需 --with-debug 编译,否则不生效也不报错 |
access_log |
logs/access.log combined |
http,server,location,if,limit_except,stream |
官方镜像的 access.log 是指向 /dev/stdout 的软链,tail -f 会卡住;本书写真实文件 lab_access.log |
4. 完整配置示例
两段文件,与 lab/00-env 的两个文件一一对应。第一段 nginx/nginx.conf:
nginx
# ===== /etc/nginx/lab/nginx.conf(main + http 上下文)=====
# 相对 lab/00-env 只多了三处,均在下方标「新增」
user nginx;
worker_processes 1; # 实验固定 1 worker,行为可预测
# 新增 1:fd 上限(配合容器/宿主 ulimit -n 使用)
worker_rlimit_nofile 65535;
# 新增 2:reload/停机时老 worker 的兜底退出上限(≥1.11.11)
worker_shutdown_timeout 30s;
# 新增 3:显式写 master,为热升级实验留出对照点(默认就是 on)
master_process on;
error_log /var/log/nginx/lab_error.log warn;
pid /var/run/nginx.pid; # 与镜像默认 /run/nginx.pid 是同一文件
# 第三方模块从这里加载(main 上下文,必须早于 events/http):
# load_module /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so;
# 官方镜像的 modules 目录是空的:这一行开着却没有 .so,nginx -t 就报 dlopen() failed
events {
worker_connections 1024; # 单 worker,反代场景约等于 512 并发请求
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
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;
resolver 127.0.0.11 valid=5s ipv6=off; # Docker 内置 DNS
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;
}
include /etc/nginx/lab/conf.d/*.conf;
}
第二段 nginx/conf.d/default.conf 与 lab 一致(宿主 8080 → 容器 80):
nginx
# ===== /etc/nginx/lab/conf.d/default.conf =====
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location = /healthz {
access_log off;
return 200 "gateway ok\n";
}
location /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
location /static/ {
alias /usr/share/nginx/html/;
expires 7d;
add_header Cache-Control "public";
}
location /slow/ {
proxy_pass http://app_pool;
proxy_set_header Host $host;
proxy_read_timeout 5s;
}
location /api/ {
proxy_pass http://app_pool/;
proxy_connect_timeout 2s;
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;
}
location / {
try_files $uri $uri/ /index.html;
}
}
相对 lab/00-env 改了哪几行 :新增 worker_rlimit_nofile 65535;、worker_shutdown_timeout 30s;、master_process on; 三行;load_module 一行是注释示例(装了第三方模块再放开)。其余(8080 映射、app_pool 三成员、lab_access.log、-c /etc/nginx/lab/nginx.conf)与 lab 一字不差;改完照 README 三步走:nginx -t -c ... → nginx -s reload → curl /healthz。
5. 必做实验
实验 1:三条安装路径的 -V 对照
bash
docker run --rm --entrypoint nginx nginx:stable-alpine -V 2>&1 | tr ' ' '\n' | grep -E '^--' | head -6
docker run --rm --entrypoint nginx nginx:alpine -v 2>&1
预期:
text
--prefix=/etc/nginx
--sbin-path=/usr/sbin/nginx
--modules-path=/usr/lib/nginx/modules
--conf-path=/etc/nginx/nginx.conf
--error-log-path=/var/log/nginx/error.log
--http-log-path=/var/log/nginx/access.log
nginx version: nginx/1.31.8
为什么 :stable 用偶数码、mainline 用奇数码(实测同一时刻 1.30.6 与 1.31.8),只有 mainline 第一时间拿到新指令;同一台机器上两份二进制谁在跑,也只能靠 -V 区分。
实验 2:读懂 -t 的两种报错
bash
mkdir -p /tmp/ngxcheck && cat > /tmp/ngxcheck/bad.conf <<'EOF'
events {}
http {
worker_connections 1024;
server {
listen 8080;
brotli on;
location / { return 200 "ok\n"; }
}
}
EOF
docker run --rm -v /tmp/ngxcheck:/c:ro nginx:stable-alpine nginx -t -c /c/bad.conf
预期(报第一个错就停,所以先看到上下文错):
text
nginx: [emerg] "worker_connections" directive is not allowed here in /c/bad.conf:3
nginx: configuration file /c/bad.conf test failed
把 worker_connections 1024; 挪回 events 再跑,第二条错误才出现:
text
nginx: [emerg] unknown directive "brotli" in /c/bad.conf:6
nginx: configuration file /c/bad.conf test failed
为什么 :directive is not allowed here 是放错上下文 (worker_connections 只属于 events),unknown directive 是没编/没加载这个模块 (brotli 不在官方镜像里,正是 2.6 与坑 4 的场景)。两种错指向完全不同的修法,Ch3 会把上下文规则讲透。
实验 3:目录勘察(不进容器也能看清布局)
bash
docker run --rm --entrypoint sh nginx:stable-alpine -c 'ls /etc/nginx; ls -ld /etc/nginx/modules; ls /usr/share/nginx/html'
预期:
text
conf.d fastcgi.conf fastcgi_params mime.types # ls /etc/nginx
modules nginx.conf scgi_params uwsgi_params
lrwxrwxrwx 1 root root 22 Oct 21 23:13 /etc/nginx/modules -> /usr/lib/nginx/modules
50x.html index.html # ls /usr/share/nginx/html
为什么 :modules 是指向 /usr/lib/nginx/modules 的软链、且目录是空的------这解释了为什么动态模块必须自己编译再放进去;/usr/share/nginx/html 里有现成的 index.html/50x.html,所以镜像里 root /usr/share/nginx/html; 能直接出页,而 Debian 包上要改成 /var/www/html。
实验 4:reload 与 reopen 的真实效果
bash
cd nginx/lab/00-env
docker compose exec -T gateway nginx -t -c /etc/nginx/lab/nginx.conf
docker compose exec -T gateway ps -o pid,ppid,args # busybox ps 不支持 -C / cmd
docker compose exec -T gateway nginx -s reload
sleep 1
docker compose exec -T gateway ps -o pid,ppid,args
docker exec lab-gateway sh -c 'mv /var/log/nginx/lab_access.log /var/log/nginx/lab_access.log.bak'
docker compose exec -T gateway nginx -s reopen # 等价 kill -USR1
docker exec lab-gateway ls -l /var/log/nginx/ | grep lab_access
实测输出(reload 前后):
text
nginx: the configuration file /etc/nginx/lab/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/lab/nginx.conf test is successful
1 0 nginx: master process nginx -c /etc/nginx/lab/nginx.conf -g daemon off;
66 1 nginx: worker process
1 0 nginx: master process nginx -c /etc/nginx/lab/nginx.conf -g daemon off;
156 1 nginx: worker process
-rw-r--r-- 1 nginx root 0 Nov 3 04:20 lab_access.log
-rw-r--r-- 1 root root 36258 Nov 3 04:19 lab_access.log.bak
为什么 :master 的 pid(1)不变、worker 的 pid(66 → 156)换代 ------所以内存里的共享内存 zone(app_pool 那 64k)跨 reload 保留,这正是缓存与限流计数能在 Ch8 里活下去的前提。reopen 让 nginx 重新 open() 日志路径、造出 0 字节新文件(属主 nginx),旧文件里 36KB 内容留在 .bak 中------这就是 logrotate 的标准动作。
实验 5:完整热升级 + 回滚(一次性容器,不污染 lab)
bash
docker run -d --name ngxup nginx:stable-alpine sh -c '/usr/sbin/nginx; sleep 3600'
sleep 2
docker exec ngxup ps -o pid,ppid,args | head -4 # master=8,worker=9
# 1) 起新 master
docker exec ngxup sh -c 'kill -USR2 $(cat /run/nginx.pid); sleep 2; ls /run/nginx.pid*; ps -o pid,ppid,args | head -8'
# 2) 老 worker 优雅退出
docker exec ngxup sh -c 'kill -WINCH $(cat /run/nginx.pid.oldbin); sleep 1; ps -o pid,ppid,args | head -6'
# 3) 老 master 退出
docker exec ngxup sh -c 'kill -QUIT $(cat /run/nginx.pid.oldbin); sleep 1; ls /run/nginx.pid*; ps -o pid,ppid,args | head -4'
docker rm -f ngxup
实测关键片段:
text
# USR2 之后:两个 pid 文件、两棵进程树
/run/nginx.pid /run/nginx.pid.oldbin
8 1 nginx: master process /usr/sbin/nginx
34 8 nginx: master process /usr/sbin/nginx # 新 master 是老 master 的子进程
35 34 nginx: worker process
# WINCH 之后:老 master(8) 仍在,老 worker 9..13 全退,只剩新树
8 1 nginx: master process /usr/sbin/nginx
34 8 nginx: master process /usr/sbin/nginx
# QUIT 之后:老 master 变僵尸 [nginx],pid 文件只剩一个
/run/nginx.pid # 8 -> [nginx],34 被 init 接管(PPID 变 1)
为什么 :USR2 只 fork 新 master、不碰老 worker;WINCH 才让老 worker 停接新连接、服务完存量再退------所以升级窗口内请求不会断。回滚练法:在第 1 步之后给老 master 发 kill -HUP 8(用内存旧配置重启 worker),再 kill -QUIT 34,ps 就回到单棵树。注意实测踩到的坑 :把 nginx 写成相对路径启动时,USR2 会失败------[alert] execve() failed while executing new binary process "nginx" (2: No such file or directory),因为 re-exec 用的是 argv0,不做 PATH 查找。
6. 验收自测(5 题)
Q1. 写出平滑升级的 5 个步骤(含信号),并说明某一步出问题怎么回滚。
答案要点:备份 conf →
nginx -t通过 → 换二进制 →USR2(生成nginx.pid.oldbin、新 master 起来)→ 打流量验证 →WINCH老 worker →QUIT老 master(清理 oldbin)。回滚只要两条:给老 masterHUP(用内存里的旧配置重启 worker,不读磁盘),再给新 masterQUIT;不需要重装、不需要断连接。
Q2. 举出 3 个"必须源码编译"的场景,并说明判断依据。
答案要点:①要官方包未带的第三方模块(brotli 压缩、njs、vts 指标)------只能
--add-dynamic-module编出.so再load_module;②要非默认编译选项(某些发行版包没有--with-http_v3_module,没开--with-compat时第三方.so加载失败);③要打补丁或自定义路径(--prefix=/opt/nginx,日志/临时目录不在/var)。判断依据永远是nginx -V:参数里没有的,配置里一定用不了。
Q3. 本实验环境里为什么 nginx -t 必须带 -c /etc/nginx/lab/nginx.conf?不带会发生什么?
答案要点:主配置被
command的-c改到了挂载目录,默认的/etc/nginx/nginx.conf仍然是镜像自带那份。不带-c时校验的是默认配置------同样会打印test is successful,于是你以为校验过了,实际你改的conf.d/default.conf根本没参与校验;等-s reload才在 error.log 里看到[emerg]。这就是 lab README 把它列为头号排障项的原因。
Q4. 升级后配置里出现 unknown directive "brotli",可能原因有哪些?
答案要点:①换二进制后漏了
load_module(模块没加载,指令就成了"未知");②load_module写了但.so不在--modules-path下,此时报dlopen() ... failed;③新二进制没带--with-compat或跨了大版本,.soABI 不兼容(worker 起不来);④真的没编这个模块。排查顺序:nginx -V比参数 →ls模块目录 →-t看报错位置。
Q5. HUP、USR1、TERM 分别用在什么场景?为什么 reload 不掉连接而 stop 会?
答案要点:
HUP= 改配置生效(master 不变、worker 换代,存量连接由老 worker 服务完);USR1= 重开日志(logrotate 每天一次的标准动作,不做就会把磁盘写满);TERM= 快速停机,直接断连接。reload 不掉连接,是因为老 worker 进入"停接新连接、服务完存量再退"的优雅退出状态,stop没有这一步。
7. 常见坑
nginx -t通过、reload后行为没变 → 根因:校验的不是正在跑的那份配置(漏-c,或改的是另一个目录里的同名文件)。正解:ps -o pid,ppid,args看进程实际用的-c,再用nginx -T确认最终生效内容。- 从 apt 环境抄了
root /usr/share/nginx/html;,访问 403/404 → 根因:Debian 包站点根是/var/www/html(见 2.5)。正解:grep -r "root " /etc/nginx/sites-enabled/对一遍,或统一用nginx -T看生效值。 - 机器上装了两份 nginx(apt 一份 + 源码
--prefix=/usr/local一份),-t报错在 A 文件、服务表现却是 B 配置 → 根因:nginx命令命中的是另一个二进制、另一个--conf-path。正解:which -a nginx+nginx -V对--conf-path,只留一个;容器化后这类问题消失。 - 热升级后 worker 起来就崩,日志里是
[emerg]或段错误 → 根因:动态模块与新二进制不是同一版本源码,或主程序未带--with-compat,ABI 不匹配。正解:同大版本内升级,.so与主程序一起重编;升级前先起临时实例打流量验证(实验 5 的临时容器就是模板)。 kill -USR2没反应,或出现两个 master 后忘了收尾 → 根因有三个,实测都能复现:①老进程用相对路径nginx启动,re-exec 报execve() failed;②容器里 nginx 就是 PID 1,QUIT老 master = 停容器;③只做了USR2没做WINCH/QUIT,内存与 fd 双倍占用。正解:用绝对路径启动、在 systemd/init 托管的宿主机进程模型下演练,并把 5 步写成清单,收尾必做ls /run/nginx.pid*。- 容器里配了
user nginx;启动失败[emerg] getpwnam("nginx") failed→ 根因:rootless 镜像里没有该用户(uid 101),非 root 也无权 setuid。正解:rootless 场景删掉user,把pid、*_temp、日志指到可写目录(如/tmp)。 tail -f /var/log/nginx/access.log卡住、日志"没了" → 根因:官方镜像该文件是指向/dev/stdout的软链,被mv/logrotate 之后tail跟丢了 inode。正解:本书写真实文件lab_access.log;需要切割时mv之后必须nginx -s reopen,否则 fd 还指着被移走的 inode,磁盘只涨不落(实验 4 已实测)。- 把 mainline 当 stable 装进生产 → 根因:只记得"官方源默认给最新"。正解:
apt-cache policy nginx/dnf repoquery --show-duplicates nginx看候选版本,稳定站锁stable源;升级窗口与 Ch14 的-T配置快照 diff 一起做。
8. 延伸阅读
- 各平台官方安装包(deb/rpm 源,stable 与 mainline 的取舍):nginx.org/en/linux_pa...
- 源码编译与依赖说明:nginx.org/en/docs/ins...
- 命令行参数(
-t/-T/-V/-s/-g):nginx.org/en/docs/swi... - 运行期控制:全部信号与热升级完整步骤:nginx.org/en/docs/con...
./configure全部参数(--with-compat、--add-dynamic-module等):nginx.org/en/docs/con...- core 模块参考(
load_module、user、pid、daemon、worker_*):nginx.org/en/docs/ngx... - 官方 Docker 镜像与 rootless 变体:github.com/nginx/docke...、github.com/nginxinc/do...
系列导航:第 1 篇进程模型与能力边界 → 第 2 篇(本篇)安装、升级与目录结构 → 第 3 篇配置语法 → 第 4 篇静态站与 location → 第 5 篇反向代理 ... 第 15 篇故障案例库。