安装、升级与目录结构

这是《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-alpine 1.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)。回滚只要两条:给老 master HUP(用内存里的旧配置重启 worker,不读磁盘),再给新 master QUIT;不需要重装、不需要断连接。

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 或跨了大版本,.so ABI 不兼容(worker 起不来);④真的没编这个模块。排查顺序:nginx -V 比参数 → ls 模块目录 → -t 看报错位置。

Q5. HUP、USR1、TERM 分别用在什么场景?为什么 reload 不掉连接而 stop 会?

答案要点:HUP = 改配置生效(master 不变、worker 换代,存量连接由老 worker 服务完);USR1 = 重开日志(logrotate 每天一次的标准动作,不做就会把磁盘写满);TERM = 快速停机,直接断连接。reload 不掉连接,是因为老 worker 进入"停接新连接、服务完存量再退"的优雅退出状态,stop 没有这一步。


7. 常见坑

  1. nginx -t 通过、reload 后行为没变 → 根因:校验的不是正在跑的那份配置(漏 -c,或改的是另一个目录里的同名文件)。正解:ps -o pid,ppid,args 看进程实际用的 -c,再用 nginx -T 确认最终生效内容。
  2. 从 apt 环境抄了 root /usr/share/nginx/html;,访问 403/404 → 根因:Debian 包站点根是 /var/www/html(见 2.5)。正解:grep -r "root " /etc/nginx/sites-enabled/ 对一遍,或统一用 nginx -T 看生效值。
  3. 机器上装了两份 nginx(apt 一份 + 源码 --prefix=/usr/local 一份),-t 报错在 A 文件、服务表现却是 B 配置 → 根因:nginx 命令命中的是另一个二进制、另一个 --conf-path。正解:which -a nginx + nginx -V 对 --conf-path,只留一个;容器化后这类问题消失。
  4. 热升级后 worker 起来就崩,日志里是 [emerg] 或段错误 → 根因:动态模块与新二进制不是同一版本源码,或主程序未带 --with-compat,ABI 不匹配。正解:同大版本内升级,.so 与主程序一起重编;升级前先起临时实例打流量验证(实验 5 的临时容器就是模板)。
  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*。
  6. 容器里配了 user nginx; 启动失败 [emerg] getpwnam("nginx") failed → 根因:rootless 镜像里没有该用户(uid 101),非 root 也无权 setuid。正解:rootless 场景删掉 user,把 pid、*_temp、日志指到可写目录(如 /tmp)。
  7. tail -f /var/log/nginx/access.log 卡住、日志"没了" → 根因:官方镜像该文件是指向 /dev/stdout 的软链,被 mv/logrotate 之后 tail 跟丢了 inode。正解:本书写真实文件 lab_access.log;需要切割时 mv 之后必须 nginx -s reopen,否则 fd 还指着被移走的 inode,磁盘只涨不落(实验 4 已实测)。
  8. 把 mainline 当 stable 装进生产 → 根因:只记得"官方源默认给最新"。正解:apt-cache policy nginx / dnf repoquery --show-duplicates nginx 看候选版本,稳定站锁 stable 源;升级窗口与 Ch14 的 -T 配置快照 diff 一起做。

8. 延伸阅读

  1. 各平台官方安装包(deb/rpm 源,stable 与 mainline 的取舍):nginx.org/en/linux_pa...
  2. 源码编译与依赖说明:nginx.org/en/docs/ins...
  3. 命令行参数(-t / -T / -V / -s / -g):nginx.org/en/docs/swi...
  4. 运行期控制:全部信号与热升级完整步骤:nginx.org/en/docs/con...
  5. ./configure 全部参数(--with-compat、--add-dynamic-module 等):nginx.org/en/docs/con...
  6. core 模块参考(load_module、user、pid、daemon、worker_*):nginx.org/en/docs/ngx...
  7. 官方 Docker 镜像与 rootless 变体:github.com/nginx/docke...、github.com/nginxinc/do...

相关章节:Ch1 · Ch3 · Ch5 · Ch11 · Ch12 · Ch14


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

相关推荐
皮皮林55130 分钟前
万能通用!权限系统就该这么设计
后端
GoAI2 小时前
DeepSeek Harness Memory (一)主流记忆插件总结
后端·算法·面试
橙子家2 小时前
RESTful 接口:它并非标准或协议,而是一种【架构风格】
后端
小兔子3 小时前
dict.get 的默认值每次都会求值,哪怕键本来就在
后端
斯维赤3 小时前
限流、熔断、降级,你真的分得清吗?
java·后端·架构
肌肉娃子3 小时前
红队攻防测试报告 — wechat-e.cfd
后端
陈随易3 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
海螺_AIHCC4 小时前
SSE 流式返回中文变问号,问题不在编码
后端
panbook4 小时前
从零实现一个时序数据库(一):用 Rust、Arrow 和 Parquet 跑通写入与 SQL 查询
后端