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 扩展时才不会空中楼阁。二、环境说明项目配置CPU8 核General Purpose Processor 2.0GHz内存16G磁盘40G系统盘剩余约 35G系统Ubuntu 24.04.4 LTS内核 6.8.0-106-generic公网 IP119.3..已打码真实日志中保留原样编译器gcc 13.3.0 / GNU Make 4.3 / OpenSSL 3.0.13Nginx 版本源码编译 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 (pidA) ├─ worker1 ┐ ├─ worker2 │ 收到 -s reload └─ worker8 ┘ │ ▼ 新 master (pidB, 由旧 master fork 并 re-exec) ├─ worker1 ← 用【新】配置启动 ├─ worker2 ← 用【新】配置启动 └─ worker8 ← 用【新】配置启动 │ 旧 worker 继续服务【在途连接】处理完即退出 worker_shutdown_timeout 兜底超时强制退出四、逐步实操命令 真实回显4.1 环境准备安装编译依赖编译 Nginx 需要 C 编译器、PCRE正则、zlib压缩、OpenSSLHTTPS。用一条apt搞定apt-getupdate-qqDEBIAN_FRONTENDnoninteractiveapt-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/srccd/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.2timemake-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 更稳yesNginx 静态资源服务器实战这是一段用于演示 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/svgxml; # 访问日志额外记录请求处理耗时 $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 auto10gzip 效果实测重点关键问题是开启 gzip 后传输体积到底小了多少用curl分别测不压缩和压缩两种下载量# 不压缩客户端不发送 Accept-Encodingcurl-s--no-compressed-o/dev/null-wuncompressed_bytes%{size_download}\nhttp://127.0.0.1/big.txt# 压缩发送 Accept-Encoding: gzipcurl-s-HAccept-Encoding: gzip-o/dev/null-wcompressed_bytes%{size_download}\nhttp://127.0.0.1/big.txt真实回显uncompressed_bytes11700000 compressed_bytes4561512MB → 45,615 字节压缩比约 256:1再看响应头对比curl-s-Ihttp://127.0.0.1/big.txt|grep-icontent-lengthcurl-s-I-HAccept-Encoding: gziphttp://127.0.0.1/big.txt|grep-iEcontent-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让我们能提前把文件压成.gzNginx 直接发预压缩包省去运行时压缩开销echogzip_static 预压缩演示文件内容。/data/www/dl/sample.txtgzip-k-f/data/www/dl/sample.txt# 同时保留 sample.txt 和 sample.txt.gzcurl-s-I-HAccept-Encoding: gziphttp://127.0.0.1/dl/sample.txt|grep-iEcontent-encoding|content-length真实回显Content-Length: 77 Content-Encoding: gzipautoindex 目录浏览autoindex on让/dl/目录可直接列出来真实回显curl-shttp://127.0.0.1/dl/|grep-iEsample|href|headh1Index of /dl//h1hrprea href../..//a a hrefsample.txtsample.txt/a 24-Jul-2026 04:37 43 a hrefsample.txt.gzsample.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-ix-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 --forestps-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然后立刻psOLD_PID$(cat/usr/local/nginx/logs/nginx.pid)curl-s--limit-rate 1M-o/dev/null http://127.0.0.1/big.txt# 慢速下载约 12ssleep1/usr/local/nginx/sbin/nginx-sreloadsleep2ps-ef--forest|grep[n]ginxwait# 等慢下载结束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 pid12512 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.htmlechoold access.log.1 size:$(stat-c%s /usr/local/nginx/logs/access.log.1)bytesechonew 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 rt0.000 127.0.0.1 - - [24/Jul/2026:12:41:20 0800] GET /about.html HTTP/1.1 200 262 - curl/8.5.0 rt0.000注意rt0.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_FRONTENDnoninteractiveapt-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-Es/ rt[0-9.]$///usr/local/nginx/logs/access_combined.log生成 HTML 报表并验证可访问goaccess /usr/local/nginx/logs/access_combined.log --log-formatCOMBINED-o/data/www/report.htmlcurl-s-o/dev/null-wHTTP%{http_code} type%{content_type}\nhttp://127.0.0.1/report.html真实回显report size: 560K HTTP200 typetext/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看热门 URLstatic_requests单独统计静态资源not_found直接列出 404 最多的路径本例就是nope.html对排查死链极有用status_codes汇总各状态码占比。上面 CSV 摘要里bandwidth 425593字节、约 416KB正是这 1002 次请求的总流量unique_visitors 1则因为所有请求都来自本机127.0.0.1这一个来源。把report.html放到站点目录后直接用浏览器打开即可交互式下钻比肉眼grep日志高效太多。五、踩坑清单真实遇到的问题#现象 / 报错原因解决方案1cmdfile里写多行cat EOF ... EOF时脚本报错ValueError: not enough values to unpacksshrun.py按每行一条TAG:::命令解析heredoc 的续行被当成独立命令缺少:::多行配置文件改用 paramikoSFTP 上传agentA_put.py远程命令只写单行2SFTP 上传脚本偶发无输出 退出码 1Python 输出缓冲 服务器 SSH 偶发握手抖动加-u无缓冲运行遇到抖动直接重试一次即可3kill -USR1后ls access.log*报 “cannot access”上一条命令cd改了工作目录后续命令用了相对路径在新会话里找不到文件日志相关操作一律用绝对路径/usr/local/nginx/logs/access.log*4头请求curl -I -H Accept-Encoding: gzip只返回Content-Encoding: gzip而没有 Content-LengthNginx 对 HEAD 请求在 gzip 下采用分块传输不预先给出长度改用curl -w %{size_download}实测下载字节数得到 45615更能说明压缩效果5goaccess 直接解析带rt...的日志时统计异常/不识别我们的log_format在 COMBINED 之后追加了自定义字段超出标准格式用sed去掉行尾rt...还原为标准 COMBINED 再喂给 goaccess6goaccess 在管道里输出的是 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全部命令输出取自真实环境。