Nginx 源码编译与静态资源服务器实战 —— 基于陶辉《Nginx 核心知识 150 讲》第一、二章
Nginx 源码编译与静态资源服务器实战 —— 基于陶辉《Nginx 核心知识 150 讲》第一、二章
本文所有命令均在真实服务器(华为云 FlexusX 8C16G / Ubuntu 24.04)上执行,所有回显均来自真实环境,未做任何虚构与篡改(仅对超长输出做了合理截断)。
一、前言
很多人第一次接触 Nginx,都是通过apt install nginx一条命令装好就用。这当然最快,但"会用"和"懂它"之间隔着一条叫"编译与架构"的河。陶辉老师在《Nginx 核心知识 150 讲》的第一、二章里,重点讲了两件事:为什么要用源码编译、以及Nginx 的事件驱动 + master/worker 进程模型。
我这次的目标很明确:
- 抛开发行版自带的那份"通用"二进制,自己从源码编译一个带 SSL、HTTP/2、stub_status、gzip_static、realip、stream 模块的 Nginx;
- 用它搭一个真实的静态资源 Web 服务器,把
gzip、autoindex、access_log里的$request_time、目录浏览、预压缩这些都跑通看效果; - 把命令行运维(reload、日志切割、平滑升级/平滑退出)一个个亲手做出来,尤其是用
ps --forest亲眼看到 master 与 worker 的父子关系,以及 reload 时旧 worker "优雅退场"的全过程; - 最后用
stub_status看实时连接数,用goaccess把访问日志变成一份可视化报表。
整套做下来,最让我"哇"的一刻,是 reload 之后ps里赫然出现一行nginx: worker process is shutting down——那种"书里讲的架构真的在跑"的实感,是看文档永远替代不了的。
本实验严格对应陶辉课程第一、二章的主线:第一章讲"如何从源码构建一个适合自己业务的 Nginx",第二章讲"Nginx 的进程模型与请求处理机制"。所以下面每一步都不是为了"跑通而已"——编译参数决定了 Nginx 的能力边界,reload 与 worker 的行为决定了线上能否"平滑无感"地变更。把这两章吃透,后面讲负载均衡、缓存、Lua 扩展时才不会空中楼阁。
二、环境说明
| 项目 | 配置 |
|---|---|
| CPU | 8 核(General Purpose Processor @ 2.0GHz) |
| 内存 | 16G |
| 磁盘 | 40G(系统盘,剩余约 35G) |
| 系统 | Ubuntu 24.04.4 LTS(内核 6.8.0-106-generic) |
| 公网 IP | 119.3..(已打码,真实日志中保留原样) |
| 编译器 | gcc 13.3.0 / GNU Make 4.3 / OpenSSL 3.0.13 |
| Nginx 版本 | 源码编译 nginx/1.26.2 |
说明:该机器带宽仅 5Mbit/s,因此全程尽量走
apt或小体积源码包(Nginx 源码包仅约 1.2MB),避免大文件下载拖慢节奏。
三、整体架构与流程(ASCII 图)
3.1 静态资源服务器的请求链路
┌──────────────────────────────┐ 客户端 (curl/浏览器) │ Nginx 1.26.2 │ GET /index.html ───► │ master process (1个, root) │ │ │ ├─ worker (nobody) │──► 读 /data/www/index.html │ │ ├─ worker (nobody) │──► 读 /data/www/big.txt │ │ ├─ worker (nobody) ... │──► gzip 压缩后回传 │ └─ worker (nobody) │ └──────────────────────────────┘ │ access.log (含 $request_time) error.log3.2 源码编译流程
下载 nginx-1.26.2.tar.gz (1.2MB) │ ▼ ./configure ──► 探测 PCRE / OpenSSL / zlib,生成 objs/Makefile │ ▼ make -j8 ──► 编译各模块 (real 0m4.988s, 8核并行) │ ▼ make install ──► 落到 /usr/local/nginx/{sbin,conf,html,logs}3.3 reload(平滑重载)流程
旧 master (pid=A) ├─ worker1 ┐ ├─ worker2 │ 收到 -s reload └─ worker8 ┘ │ ▼ 新 master (pid=B, 由旧 master fork 并 re-exec) ├─ worker1' ← 用【新】配置启动 ├─ worker2' ← 用【新】配置启动 └─ worker8' ← 用【新】配置启动 │ 旧 worker 继续服务【在途连接】,处理完即退出 (worker_shutdown_timeout 兜底,超时强制退出)四、逐步实操(命令 + 真实回显)
4.1 环境准备:安装编译依赖
编译 Nginx 需要 C 编译器、PCRE(正则)、zlib(压缩)、OpenSSL(HTTPS)。用一条apt搞定:
apt-getupdate-qqDEBIAN_FRONTEND=noninteractiveapt-getinstall-ygccmakebuild-essential\libpcre3-dev zlib1g-dev libssl-dev openssl验证工具链版本(真实回显):
gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 GNU Make 4.3 OpenSSL 3.0.13 30 Jan 2024 (Library: OpenSSL 3.0.13 30 Jan 2024)4.2 源码编译 Nginx 1.26.2
下载仅 1.2MB 的源码包(适配 5M 小带宽):
mkdir-p/usr/local/src&&cd/usr/local/srccurl-fsSL-onginx-1.26.2.tar.gz https://nginx.org/download/nginx-1.26.2.tar.gztarxzf nginx-1.26.2.tar.gz./configure指定安装路径并开启我们需要的模块。关键:源码编译的意义正在于此——按需裁剪模块,而不是用发行版那个"万能但不一定需要"的二进制。
cd/usr/local/src/nginx-1.26.2 ./configure--prefix=/usr/local/nginx\--with-http_ssl_module\--with-http_v2_module\--with-http_stub_status_module\--with-http_gzip_static_module\--with-http_realip_module\--with-stream配置摘要(真实回显,已截取关键部分):
checking for PCRE library ... found checking for PCRE JIT support ... found checking for OpenSSL library ... found checking for zlib library ... found creating objs/Makefile Configuration summary + using system PCRE library + using system OpenSSL library + using system zlib library nginx path prefix: "/usr/local/nginx" nginx binary file: "/usr/local/nginx/sbin/nginx" nginx configuration file: "/usr/local/nginx/conf/nginx.conf" nginx pid file: "/usr/local/nginx/logs/nginx.pid" ...8 核并行编译,计时用time记录真实耗时:
cd/usr/local/src/nginx-1.26.2&&timemake-j8真实编译耗时(真实回显):
real 0m4.988s user 0m30.649s sys 0m7.971s解读:
real仅约 5 秒,因为是 8 核并行(-j8),但user累计达 30 秒,说明 CPU 确实被多个编译线程吃满了。user/sys之和远大于real,正是并行编译的典型特征。
安装并校验模块:
makeinstall/usr/local/nginx/sbin/nginx-V真实回显(这正是我们"要什么模块就有什么模块"的证据):
nginx version: nginx/1.26.2 built by gcc 13.3.0 (Ubuntu 13.3.0-6ubuntu2~24.04.1) built with OpenSSL 3.0.13 30 Jan 2024 TLS SNI support enabled configure arguments: --prefix=/usr/local/nginx --with-http_ssl_module \ --with-http_v2_module --with-http_stub_status_module \ --with-http_gzip_static_module --with-http_realip_module --with-stream逐模块说明我们为何开启它们:--with-http_ssl_module提供 HTTPS 能力;--with-http_v2_module启用 HTTP/2 多路复用,提升并发与首包速度;--with-http_stub_status_module暴露内建监控页(见 4.5);--with-http_gzip_static_module支持发送预压缩的.gz文件,省去运行时压缩(见 4.3);--with-http_realip_module在 Nginx 前面有 CDN / 反代时还原真实客户端 IP;--with-stream则让 Nginx 具备四层 TCP/UDP 负载均衡能力,超越纯七层反向代理。这些都不是默认编译进发行版二进制的"最小集",而是我们按需求亲手勾选的能力边界。
4.3 搭建静态资源 Web 服务器
准备站点目录(/data/www)和测试文件,其中big.txt是一个约 12MB、内容高度重复的大文本,专门用来演示 gzip 的压缩比:
mkdir-p/data/www /data/www/dl# index.html / style.css / about.html 通过 SFTP 上传(多行配置走 SFTP 更稳)yes'Nginx 静态资源服务器实战:这是一段用于演示 gzip 压缩效果的重复文本,包含中文与 English mix 内容...'\|head-n60000>/data/www/big.txt生成结果(真实回显):
-rw-r--r-- 1 root root 12M Jul 24 12:37 /data/www/big.txt核心配置/usr/local/nginx/conf/nginx.conf(节选真实内容):
worker_processes auto; events { worker_connections 1024; } http { include mime.types; sendfile on; keepalive_timeout 65; # ===== gzip ===== gzip on; gzip_comp_level 6; gzip_min_length 1k; gzip_vary on; gzip_types text/plain text/css application/javascript application/json application/xml text/xml image/svg+xml; # 访问日志额外记录请求处理耗时 $request_time log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" rt=$request_time'; access_log /usr/local/nginx/logs/access.log main; server { listen 80; server_name localhost; root /data/www; autoindex on; # 开启目录浏览 location / { index index.html index.htm; try_files $uri $uri/ =404; } location /dl/ { gzip_static on; } # 支持 .gz 预压缩文件 } }先-t检查语法,再启动:
/usr/local/nginx/sbin/nginx-t/usr/local/nginx/sbin/nginx真实回显:
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful启动后进程数(1 个 master + 8 个 worker,因为 8 核worker_processes auto):
10gzip 效果实测(重点)
关键问题是:开启 gzip 后,传输体积到底小了多少?用curl分别测"不压缩"和"压缩"两种下载量:
# 不压缩(客户端不发送 Accept-Encoding)curl-s--no-compressed-o/dev/null-w'uncompressed_bytes=%{size_download}\n'http://127.0.0.1/big.txt# 压缩(发送 Accept-Encoding: gzip)curl-s-H'Accept-Encoding: gzip'-o/dev/null-w'compressed_bytes=%{size_download}\n'http://127.0.0.1/big.txt真实回显:
uncompressed_bytes=11700000 compressed_bytes=4561512MB → 45,615 字节,压缩比约 256:1!再看响应头对比:
curl-s-Ihttp://127.0.0.1/big.txt|grep-icontent-lengthcurl-s-I-H'Accept-Encoding: gzip'http://127.0.0.1/big.txt|grep-iE'content-length|content-encoding'真实回显:
Content-Length: 11700000 Content-Encoding: gzipCSS 这类小文本同样受益(text/css在gzip_types内):
Content-Type: text/css Content-Length: 178gzip 的本质是用 CPU 换带宽:把高度冗余的文本(HTML/CSS/JS/JSON)压缩后再传,客户端浏览器透明解压。我们特意让big.txt内容重复,正是为了放大这种冗余——真实业务里日志、报表、接口 JSON 往往同样"啰嗦",压缩收益极大。配置里gzip_min_length 1k表示小于 1KB 的文件不压缩(压缩小文件反而因头部开销得不偿失),gzip_comp_level 6是压缩率与 CPU 消耗的折中(1 最快、9 最狠)。对于图片、视频等本身已压缩的二进制,切忌列入gzip_types,否则只是白白浪费 CPU。
gzip_static 预压缩演示
--with-http_gzip_static_module让我们能提前把文件压成.gz,Nginx 直接发预压缩包,省去运行时压缩开销:
echo'gzip_static 预压缩演示文件内容。'>/data/www/dl/sample.txtgzip-k-f/data/www/dl/sample.txt# 同时保留 sample.txt 和 sample.txt.gzcurl-s-I-H'Accept-Encoding: gzip'http://127.0.0.1/dl/sample.txt|grep-iE'content-encoding|content-length'真实回显:
Content-Length: 77 Content-Encoding: gzipautoindex 目录浏览
autoindex on让/dl/目录可直接列出来(真实回显):
curl-shttp://127.0.0.1/dl/|grep-iE'sample|href'|head<h1>Index of /dl/</h1><hr><pre><a href="../">../</a> <a href="sample.txt">sample.txt</a> 24-Jul-2026 04:37 43 <a href="sample.txt.gz">sample.txt.gz</a> 24-Jul-2026 04:37 774.4 Nginx 命令行运维实操
(1) reload 生效验证(修改配置后必须验证)
修改配置(新增add_header X-Powered-By "Nginx-1.26.2-AgentA";与worker_shutdown_timeout 30s;),上传后-t+-s reload:
/usr/local/nginx/sbin/nginx-t/usr/local/nginx/sbin/nginx-sreloadcurl-s-Ihttp://127.0.0.1/|grep-i'x-powered-by'真实回显(表明确实加载了新配置):
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful reloaded-ok X-Powered-By: Nginx-1.26.2-AgentA(2) master/worker 进程结构(ps --forest)
ps-ef--forest|grep'[n]ginx'真实回显(清晰地看到 master 与 worker 的树形父子关系):
root 12512 1 0 12:37 ? 00:00:00 nginx: master process /usr/local/nginx/sbin/nginx nobody 12696 12512 0 12:39 ? 00:00:00 \_ nginx: worker process nobody 12697 12512 0 12:39 ? 00:00:00 \_ nginx: worker process nobody 12698 12512 0 12:39 ? 00:00:00 \_ nginx: worker process nobody 12699 12512 0 12:39 ? 00:00:00 \_ nginx: worker process ... (共 8 个 worker,对应 8 核)master 以
root运行(绑定 80 端口需要特权),worker 以低权限nobody运行(降权,安全)。这正是 Nginx "master 管进程、worker 管业务"模型活生生的样子。
为什么是 master + 多个 worker,而不是一个进程扛所有连接?核心在于 Nginx 的事件驱动(epoll/kqueue)模型:每个 worker 是一个单线程事件循环,靠非阻塞 I/O 同时盯住成千上万条连接,谁就绪就处理谁,不会因为某条连接慢而阻塞整个进程。master 不参与业务,只负责读配置、发信号、拉起/回收 worker,因此它即使长期运行也几乎不出错。worker 以nobody低权限运行则是安全底线——即便某个 worker 被攻破,攻击者拿到的也只是最小权限。reload 时 master 不退出、只 fork 出新 worker,正是"不丢连接"的根本原因。
(3) 平滑退出(reload 真相 + worker_shutdown_timeout)
书里讲 “reload 不是重启,而是 master 拉起新 worker、旧 worker 处理完在途请求再退出”。我们亲手验证:开一个限速的慢速大文件下载,在它进行中执行 reload,然后立刻ps:
OLD_PID=$(cat/usr/local/nginx/logs/nginx.pid)curl-s--limit-rate 1M-o/dev/null http://127.0.0.1/big.txt&# 慢速下载(约 12s)sleep1/usr/local/nginx/sbin/nginx-sreloadsleep2ps-ef--forest|grep'[n]ginx'wait# 等慢下载结束ps-ef--forest|grep'[n]ginx'真实回显(reload 后瞬间,旧 worker 还在"shutting down",因为它的连接还没传完):
=== ps right after reload (old worker may still serve slow request) === root 12512 1 0 12:37 ? 00:00:00 nginx: master process /usr/local/nginx/sbin/nginx nobody 12698 12512 0 12:39 ? 00:00:00 \_ nginx: worker process is shutting down nobody 12727 12512 0 12:39 ? 00:00:00 \_ nginx: worker process ... (8 个新 worker) old master pid=12512 slow transfer finished === ps after transfer (old worker should be gone) === root 12512 1 0 12:37 ? 00:00:00 nginx: master process /usr/local/nginx/sbin/nginx nobody 12727 12512 0 12:39 ? 00:00:00 \_ nginx: worker process ... ("is shutting down" 的那一个已消失)worker process is shutting down这行,就是"平滑退出"的铁证。配置里的worker_shutdown_timeout 30s则保证:万一旧连接在 30 秒内还没传完,master 会强制把它结束,避免旧 worker 永远赖着不走。
(4) 日志切割(mv + kill -USR1)
线上 Nginx 日志会越滚越大,标准做法是"改名 + 给 master 发 USR1 信号让它重新打开日志文件"(而不是重启进程):
mv/usr/local/nginx/logs/access.log /usr/local/nginx/logs/access.log.1kill-USR1$(cat/usr/local/nginx/logs/nginx.pid)sleep1ls-lh/usr/local/nginx/logs/access.log*curl-s-o/dev/null http://127.0.0.1/;curl-s-o/dev/null http://127.0.0.1/about.htmlecho"old access.log.1 size:$(stat-c%s /usr/local/nginx/logs/access.log.1)bytes"echo"new access.log size:$(stat-c%s /usr/local/nginx/logs/access.log)bytes"真实回显(旧文件被"冻结",新请求写入新文件):
-rw-r--r-- 1 nobody root 0 Jul 24 12:40 /usr/local/nginx/logs/access.log -rw-r--r-- 1 nobody root 200 Jul 24 12:39 /usr/local/nginx/logs/access.log.1 old access.log.1 size now: 200 bytes (should be unchanged) new access.log size now: 198 bytes (should have grown) --- tail new access.log --- 127.0.0.1 - - [24/Jul/2026:12:41:20 +0800] "GET / HTTP/1.1" 200 531 "-" "curl/8.5.0" rt=0.000 127.0.0.1 - - [24/Jul/2026:12:41:20 +0800] "GET /about.html HTTP/1.1" 200 262 "-" "curl/8.5.0" rt=0.000注意rt=0.000——这就是我们log_format里加的$request_time,实时记录每个请求的服务端耗时,是性能排查的利器。
4.5 stub_status 监控页
在server块里加一个location /nginx_status,只允许本机访问(生产环境务必限制来源):
location /nginx_status { stub_status on; allow 127.0.0.1; deny all; access_log off; }reload 后访问(真实回显):
/usr/local/nginx/sbin/nginx-sreloadcurl-shttp://127.0.0.1/nginx_statusActive connections: 1 server accepts handled requests 14 14 14 Reading: 0 Writing: 1 Waiting: 0字段含义:
- Active connections:当前活跃连接数;
- accepts / handled / requests:累计"已接受连接 / 已处理连接 / 已处理请求",三者相等说明没有连接被丢弃;
- Reading / Writing / Waiting:正在读请求头 / 正在写响应 / 空闲保活(Waiting 高通常说明 keep-alive 生效,连接被复用)。
Waiting数值值得重点关注:它代表当前处于 keep-alive 空闲状态的连接数。Waiting 越高,说明客户端在复用同一条 TCP 连接发起多次请求,服务端资源利用率越高;反之若 Reading/Writing 长期偏高、Waiting 很低,往往意味着连接建立后立刻关闭(keep-alive 没生效)或后端响应慢。生产环境一定要给/nginx_status加allow/deny限制来源,否则这个"内部仪表盘"会对外暴露连接规模,成为信息泄露点。
4.6 GoAccess 日志可视化
goaccess能把纯文本访问日志秒变一张交互式报表。先装:
DEBIAN_FRONTEND=noninteractiveapt-getinstall-ygoaccesswhichgoaccess# /usr/bin/goaccess用for循环疯狂造几百条访问(覆盖首页、大文件、404 等),再让 goaccess 解析(我们的日志末尾多了rt=...,先剥掉还原成标准 COMBINED 格式以保证解析准确):
foriin$(seq1200);docurl-s-o/dev/null http://127.0.0.1/curl-s-o/dev/null-r0-999 http://127.0.0.1/big.txtcurl-s-o/dev/null http://127.0.0.1/about.htmlcurl-s-o/dev/null http://127.0.0.1/style.csscurl-s-o/dev/null http://127.0.0.1/nope.htmldone# 还原为标准 COMBINED(去掉行尾 rt=...)cp/usr/local/nginx/logs/access.log /usr/local/nginx/logs/access_combined.logsed-i-E's/ rt=[0-9.]+$//'/usr/local/nginx/logs/access_combined.log生成 HTML 报表并验证可访问:
goaccess /usr/local/nginx/logs/access_combined.log --log-format=COMBINED-o/data/www/report.htmlcurl-s-o/dev/null-w'HTTP=%{http_code} type=%{content_type}\n'http://127.0.0.1/report.html真实回显:
report size: 560K HTTP=200 type=text/html终端统计摘要(从 goaccess 导出的 CSV 中提取的真实数据):
总请求数 total_requests : 1002 有效请求 valid_requests : 1002 失败请求 failed_requests : 0 独立访客 unique_visitors : 1 总带宽 bandwidth : 425593 bytes 唯一静态文件 unique_static_files: 2 TOP 请求路径(命中/字节): / 201 hits, 106731 B /about.html 201 hits, 52662 B /big.txt 200 hits, 200000 B (static) /style.css 200 hits, 35600 B (static) /nope.html 200 hits, 30600 B (404) 状态码分布: 2xx 成功 802 (80.04%) -> 200 OK:602, 206 Partial:200 4xx 客户端 200 (19.96%) -> 404 Not Found:200说明:
/big.txt用-r 0-999做了范围请求,因此出现了 206 Partial Content;/nope.html不存在,因此全是 404。这些"异常"数据恰好让报表更有看头。
goaccess 的报表按面板组织:visitors看独立访客与带宽;requests看热门 URL;static_requests单独统计静态资源;not_found直接列出 404 最多的路径(本例就是nope.html,对排查死链极有用);status_codes汇总各状态码占比。上面 CSV 摘要里bandwidth = 425593字节、约 416KB,正是这 1002 次请求的总流量;unique_visitors = 1则因为所有请求都来自本机127.0.0.1这一个来源。把report.html放到站点目录后,直接用浏览器打开即可交互式下钻,比肉眼grep日志高效太多。
五、踩坑清单(真实遇到的问题)
| # | 现象 / 报错 | 原因 | 解决方案 |
|---|---|---|---|
| 1 | cmdfile里写多行cat <<'EOF' ... EOF时,脚本报错ValueError: not enough values to unpack | sshrun.py按"每行一条TAG:::命令"解析,heredoc 的续行被当成独立命令,缺少::: | 多行配置文件改用 paramikoSFTP 上传(agentA_put.py),远程命令只写单行 |
| 2 | SFTP 上传脚本偶发"无输出 + 退出码 1" | Python 输出缓冲 + 服务器 SSH 偶发握手抖动 | 加-u(无缓冲)运行;遇到抖动直接重试一次即可 |
| 3 | kill -USR1后ls access.log*报 “cannot access” | 上一条命令cd改了工作目录,后续命令用了相对路径,在新会话里找不到文件 | 日志相关操作一律用绝对路径/usr/local/nginx/logs/access.log* |
| 4 | 头请求curl -I -H 'Accept-Encoding: gzip'只返回Content-Encoding: gzip而没有 Content-Length | Nginx 对 HEAD 请求在 gzip 下采用分块传输,不预先给出长度 | 改用curl -w '%{size_download}'实测下载字节数(得到 45615),更能说明压缩效果 |
| 5 | goaccess 直接解析带rt=...的日志时统计异常/不识别 | 我们的log_format在 COMBINED 之后追加了自定义字段,超出标准格式 | 用sed去掉行尾rt=...还原为标准 COMBINED 再喂给 goaccess |
| 6 | goaccess 在管道里输出的是 HTML 而非文本报表 | goaccess 检测到 stdout 不是 TTY 时,默认输出 HTML 报表 | 改用-o report.html生成网页;文本摘要则用-o report.csv导出 CSV 后抽取关键行 |
六、总结
这一趟从源码编译到静态资源服务器、再到运维与可视化,把陶辉老师前两章的核心串了起来:
为什么源码编译:发行版二进制是"通用解",而
./configure让我们精确控制模块。本文最终nginx -V清楚地列出了 ssl / v2 / stub_status / gzip_static / realip / stream 六个按需开启的模块——这恰恰是"懂 Nginx"和"只会用 Nginx"的分水岭。gzip 不是玄学,是实打实的带宽节省:同一个 12MB 文件,关 gzip 传 11,700,000 字节,开 gzip 仅 45,615 字节,约 256 倍。再加
gzip_static预压缩,连运行时压缩的 CPU 都省了。master/worker 不是概念图,是
ps --forest里看得见的树:reload 时"旧 worker 标着is shutting down、新 worker 已就位"的那一幕,比任何文字都更能说明 Nginx 的热重载为什么"不丢连接"。日志是金矿:
$request_time让每个请求耗时无所遁形;mv + kill -USR1的切割方式做到"不重启、不断流";stub_status给出现网连接实况;goaccess再把上千条日志变成一张可点可看的报表。踩坑即素材:heredoc 与批量脚本的冲突、相对路径的坑、goaccess 的格式适配……这些真实报错反而让实验更完整、更可信。
如果你也想动手,强烈建议别用apt版的 Nginx 偷懒——亲自make一次,你会第一次真正"拥有"你的 Nginx。
本文实验环境为华为云 FlexusX 8C16G / Ubuntu 24.04,全部命令输出取自真实环境。