news 2026/10/27 13:22:49

Red Hat系Linux源码编译安装nginx:参数与平滑升级

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Red Hat系Linux源码编译安装nginx:参数与平滑升级

1. 为什么这套方案值得亲手折腾一次

源码编译安装 nginx,这件事在 Linux 运维圈里属于"入门级动作、高手级细节"。标题说的是在 Red Hat 系的企业级 Linux 上用源码编译安装 nginx,但真正决定成败的从来不是./configure && make && make install这三行命令能不能跑通——这套流程十分钟就能背下来——而是编译参数怎么定、依赖走哪条路、装完之后怎么托管成系统服务、以及日后升级怎么不把线上业务搞崩。我见过太多机器上/usr/local/nginx目录存在、二进制也能启动,但 SSL 模块没编进去、HTTP/2 用不了、日志切分靠手写脚本、升级一次停机二十分钟的情况。

这篇内容面向三类人:一是刚接手 Linux 服务器、需要把 nginx 装在自己指定路径下的新手;二是被软件包里 nginx 版本老、模块不全折腾过、想拿回编译主动权的中级运维;三是需要给业务方一个"可解释、可复现、可回滚"的部署方案、却不想每次都翻官方文档的负责人。我会把参数选择背后的理由、并行编译的机器资源换算、systemd 托管的几个易错字段、平滑升级的信号机制,以及那些文档里不会写的坑,全部摊开讲一遍。

先说清楚一件事:源码编译不是"更高级",它只是"更可控"。包管理装出来的 nginx 是别人替你做的决策集合,源码编译是你自己做决策,代价是每个决策都得自己担责任。理解这一点,后面的所有选择才有意义。

1.1 源码编译和包管理的真实差距在哪

很多人以为源码编译的唯一好处是"版本新",其实版本只是最表层的东西。我更看重的是三件事:模块的确定性、路径的确定性、以及升级路径的确定性。

第一是模块确定性。发行版仓库里的 nginx 为了通用性,往往把模块拆成一堆 subpackage,比如某个-mod-http-*包、某个 stream 相关的独立包。你要用--with-stream_ssl_preread_module做四层 TLS 前置识别,或者用--with-http_auth_request_module做子请求鉴权,经常会遇到"这个模块在这个发行版版本里根本没打包"的窘境。源码编译时,模块是你写在configure参数里的,跑完nginx -V能完整打印出编译时的全部开关,这种"我说了算"的确定感,在排查线上诡异问题时价值极高。

第二是路径确定性。包管理版本默认把配置放在/etc/nginx、日志放在/var/log/nginx、二进制放在/usr/sbin,并且这些路径被写死在编译期。当你需要和某个既有目录规范对齐,或者需要把 nginx 装到一个独立分区、独立用户下做隔离时,包管理版本会让你很别扭。源码编译用--prefix一套参数就能把二进制、配置、日志、pid 文件、锁文件全部收拢到一个目录树里,迁移和备份都是整目录拷贝,干净利落。

第三是升级路径确定性。包管理升级是"换二进制 + 保留配置",但你没法控制旧进程什么时候退出,也没法在一个进程里同时跑两套配置做灰度。源码编译的平滑升级机制(USR2 + WINCH 那套信号)允许你在不中断连接的前提下把新二进制换上去,发现有问题再用老 master 进程回滚——这套能力在关键业务窗口期是真能救命的。

对比维度发行版包管理安装源码编译安装
版本获取速度跟随发行版生命周期,通常滞后可以精确到某个次版本
模块可选性受限于官方打包清单写什么参数就有什么模块
安装路径分散在/etc、/var、/usr统一收敛到--prefix目录
升级方式替换二进制,重启生效支持信号驱动的平滑升级和回滚
运维成本低,dnf update一把梭高,需要自己写作 unit、日志切分
安全补丁跟进发行版负责回溯移植需要自己盯上游 changelog

看这张表就知道,源码编译不是无脑更优,它是一个"用运维投入换控制力"的交易。如果你只有三台边缘小机器、跑的是纯静态资源,包管理版本完全够用,别为了折腾而折腾。但如果你的机器承载着网关、鉴权、四层转发这类不能随便重启的角色,那这套折腾就非常值得。

1.2 我在什么场景下会毫不犹豫选源码

判断标准其实很简单:当你需要的东西在包管理版本里"刚好缺那么一点"的时候,就是转向源码的时机。我总结了几种典型情况。

需要特定三方模块或非默认内置模块。比如要给静态资源加 br 压缩、要做 Lua 级别的动态逻辑、要用特定的地理位置库做区域判断,这些通常不在发行版默认打包范围内。即便有,版本耦合也常常让人抓狂。

需要固定一个跨机器完全一致的编译产物。批量部署场景下,几百台机器的 nginx 必须是同一个二进制、同一套模块、同一份编译参数,否则出问题时你连"是不是编译差异导致的"这个变量都排除不了。源码编译加统一打包分发,是解决这个问题的标准做法。

需要跑比发行版新两个大版本的特性。举个具体的例子,HTTP/2 的指令写法在较新版本里从listen 443 ssl http2改成了独立的http2 on,早期版本还要求 OpenSSL 1.0.2 以上才支持 ALPN。如果你在新老版本混用的环境里想统一配置写法,编译时的 OpenSSL 版本就成了硬约束,包管理版本给不了你这个自由度。

提示:不要因为"源码编译显得专业"就上源码。先问自己一句——包管理版本缺的到底是哪个模块、哪个版本、哪个路径?如果答不上来,说明你暂时不需要源码编译。


2. 动手前的环境盘点与依赖准备

编译这活儿,八成的时间浪费在依赖上。Red Hat 系现在的版本谱系跨度不小,7 系用yum,8 系和 9 系用dnf,而 8 系之后又引入了模块化仓库(AppStream/BaseOS 分离),dnf groupinstall "Development Tools"装出来的东西和 7 系的yum groupinstall不完全一致。我建议先花两分钟把环境摸清楚,别上来就敲命令。

先用cat /etc/os-release确认发行版主版本号,用uname -r看内核版本,用getconf LONG_BIT确认是 64 位,再用df -h /usr/local看看目标分区还有多少空间——源码目录加上编译中间产物,一个完整编译过程吃掉 300MB 到 500MB 是很正常的,如果你还要把 OpenSSL、PCRE2、zlib 的源码一起编进去,留 2GB 更保险。最后用free -h看内存,这个数字直接决定后面make -j能开多大并行度。

2.1 编译工具链与三类库的取舍

nginx 本体只依赖 C 编译器,但你要的绝大多数功能都挂在外部队库上。核心是三样:PCRE2(正则,location里的正则匹配、rewrite全靠它)、zlib(gzip 压缩)、OpenSSL(HTTPS)。此外还有一些可选依赖:libxslt用于 XSLT 模块,gd用于图片过滤模块,perl用于嵌入式 Perl。

安装基础工具链的命令,8 系和 9 系这样写:

dnf install -y gcc gcc-c++ make automake autoconf libtool dnf install -y pcre2-devel zlib-devel openssl-devel dnf install -y libxslt-devel gd-devel perl perl-devel perl-ExtUtils-Embed

7 系把dnf换成yum,把pcre2-devel换成pcre-devel即可。这里有个细节值得说一下:nginx 从 1.21 版本之后默认走 PCRE2,早期版本走 PCRE1,如果你编译的是老版本 nginx 却装了pcre2-devel,configure 阶段会报找不到 pcre 库,这个坑我第一次踩的时候排查了半小时。

然后是几个"看起来是工具、实际是必需"的包。redhat-rpm-config提供了一堆编译宏定义,某些版本上缺了它会在 configure 阶段报奇怪的错误;libuuid-devel在你要用--with-http_dav_module之类的模块时可能被间接引用。稳妥起见,我习惯在编译前跑一次dnf groupinstall -y "Development Tools",把整个开发组装上,反正是编译机,多装点没坏处。

注意:不要在生产容器里装完整开发工具链。常规做法是在一台专门的构建机上编译好,把二进制打包成 rpm 或者 tar 包分发到目标机器,目标机器只需要运行时库。这样既安全又省资源。

2.2 目录规划与专用账号

这一步很多人跳过,然后一年后回来看发现/usr/local/nginx下面堆着历史版本的备份、残留的 pid 文件、手动切的日志。我的习惯是先定好目录骨架再动手。

mkdir -p /usr/local/nginx/{conf,sbin,logs,client_body_temp,proxy_temp,fastcgi_temp,uwsgi_temp,scgi_temp} groupadd -r nginx 2>/dev/null useradd -r -g nginx -s /sbin/nologin -M -d /usr/local/nginx nginx 2>/dev/null chown -R nginx:nginx /usr/local/nginx/logs

这里几个参数都值得解释。-r表示创建系统账号,UID 落在系统账号区间内,不会和业务用户冲突;-s /sbin/nologin是关键,nginx 进程被攻破后攻击者拿到的是一个不能登录的 shell,这是最小权限原则的落地;-M不创建家目录,因为 nginx 用户不需要家目录;-d /usr/local/nginx把家目录字段指向安装目录,某些场景下(比如~nginx路径展开)会用到。

临时目录那五个(client_body_temp、proxy_temp、fastcgi_temp、uwsgi_temp、scgi_temp)是 nginx 处理大请求体、代理响应时的落盘位置。如果你不预建,nginx 启动时会自己在 prefix 下创建,但权限是按启动用户来的,一旦你后续调整了运行用户,就会出现"临时目录写不进去导致 413 或者 502"的问题。提前建好并统一 owner,能省掉一次深夜排查。

关键是logs目录的属主。很多人把整个 prefix 目录chown -R给 nginx,这在只用 80 端口时没问题;但当你监听 443 时,master 进程是以 root 启动再降权到 nginx 的,日志目录必须让 nginx 用户可写。所以正确做法是:prefix 根目录保持 root 所有,只有logs和五个 temp 目录给 nginx 用户。

目录/文件属主权限说明
/usr/local/nginxroot:root755安装根目录
/usr/local/nginx/sbinroot:root755二进制目录,只读
/usr/local/nginx/confroot:root750配置文件,普通用户不可读
/usr/local/nginx/logsnginx:nginx750日志与 pid,需可写
五个*_temp目录nginx:nginx700临时落盘区

3. configure 参数怎么定:一次讲透每个开关

configure是整个编译过程里唯一真正需要动脑的环节,make只是体力活。我把参数分成四类:路径类、身份类、模块类、三方库类。路径类决定装在哪,身份类决定以谁的身份跑,模块类决定能干什么,三方库类决定依赖从哪来。

3.1 路径与身份参数:把安装产物收拢到一处

路径参数的原则只有一个——全部指到同一个 prefix 下,避免二进制在sbin、配置在conf、日志在/var/log这种散落状态。至于为什么不直接用--prefix让它自动推导,原因是推导出来的相对路径在某些打包和迁移场景下会出问题,显式写死更稳。

./configure \ --prefix=/usr/local/nginx \ --sbin-path=/usr/local/nginx/sbin/nginx \ --conf-path=/usr/local/nginx/conf/nginx.conf \ --error-log-path=/usr/local/nginx/logs/error.log \ --http-log-path=/usr/local/nginx/logs/access.log \ --pid-path=/usr/local/nginx/logs/nginx.pid \ --lock-path=/usr/local/nginx/logs/nginx.lock \ --http-client-body-temp-path=/usr/local/nginx/client_body_temp \ --http-proxy-temp-path=/usr/local/nginx/proxy_temp \ --http-fastcgi-temp-path=/usr/local/nginx/fastcgi_temp \ --http-uwsgi-temp-path=/usr/local/nginx/uwsgi_temp \ --http-scgi-temp-path=/usr/local/nginx/scgi_temp \ --user=nginx \ --group=nginx

--lock-path容易被忽略。老版本的 nginx 在某些文件系统上会用它做启动互斥,虽然新版本用得少了,但写上无害,而且能避免默认落到/var/lock目录导致的权限麻烦。

--user和--group是编译期默认值,也就是说nginx.conf里如果没有写user指令,就采用这个值。这里有个常见误区:有人以为编译时写了--user=nginx就万事大吉,结果配置里又写了user nobody;,实际生效的是配置文件的。编译参数只提供默认值,配置文件优先级更高。

提示:--with-cc-opt和--with-ld-opt可以追加编译器和链接器参数。我通常加--with-cc-opt="-O2 -fstack-protector-strong"开启栈保护,这对线上服务是一层低成本的安全加固。

3.2 模块取舍:少即是多,但别砍掉将来要用的

模块参数这里最容易犯两种错误:一是"能加的都加上",编出一堆业务根本用不到的东西,攻击面变大、编译变慢;二是砍太狠,过三个月业务提了个需求,发现得重编。

我的取舍原则是:HTTP 核心功能全开,安全相关全开,性能相关按需,第三方扩展一律不加。

必装的几个,我列一下理由:

  • --with-http_ssl_module:HTTPS 基础,不加这个listen 443 ssl直接报错。这个没得商量。
  • --with-http_v2_module:HTTP/2 支持,现代浏览器的并发优势全在这。注意较新版本里还需要在listen行或单独指令里启用。它的前提是 OpenSSL 版本足够新,能支持 ALPN。
  • --with-http_realip_module:放在负载均衡后面必须要有它,否则$remote_addr全是前置代理的地址,日志分析和限流全都失真。这是最容易被遗漏、又最难事后补救的模块之一。
  • --with-http_stub_status_module:给监控系统提供/status端点,暴露活跃连接数、累计请求数。没它你根本不知道 nginx 现在扛了多少。
  • --with-http_gzip_static_module:允许直接返回预压缩好的.gz文件,省掉实时压缩的 CPU 开销。
  • --with-http_sub_module:响应内容替换,做域名改写、简单的 HTML 注入时有用。
  • --with-threads:开启线程池,处理大文件静态资源时不阻塞事件循环,磁盘慢的场景下提升明显。
  • --with-stream和--with-stream_ssl_module:四层转发能力,做数据库代理、TCP 负载均衡必须要有。这一块很多人不装,等到要给数据库做前置代理时才发现得重编。

不建议无脑加的:--with-http_dav_module(WebDAV 有安全隐患,除非业务明确需要)、--with-http_perl_module(嵌入 Perl,维护成本高、性能不如 FastCGI)、--with-debug(会显著降低性能,只在排查问题时临时编一个)。另外地理位置库类模块在新版本里已经从 MaxMind 老库迁移到别的方案,参数写法变了,按你用的 nginx 版本对应文档走,别抄老教程。

3.3 三方库的三条路线:系统库、源码目录、静态链接

这是最体现功力的一环。nginx 对 PCRE2 / zlib / OpenSSL 的引用有两条路:一是链接系统已安装的.so,二是让 nginx 自己把这三个库编进二进制。

路线的选择标准是:要不要特定的 OpenSSL 版本。PCRE2 和 zlib 走系统库完全够用,版本差异影响很小。但 OpenSSL 不一样。如果你的系统发行版自带的 OpenSSL 版本偏老,不支持 TLS 1.3,或者你想开启某些特定算法,那就必须用--with-openssl=指定源码目录,让 nginx 编译时把它们一起编进去。

# 准备源码目录 cd /usr/local/src tar -xzf openssl-3.x.tar.gz tar -xzf pcre2-10.x.tar.gz tar -xzf zlib-1.3.x.tar.gz # 在 nginx configure 中指定 ./configure \ ... \ --with-openssl=/usr/local/src/openssl-3.x \ --with-openssl-opt="enable-tls1_3 no-shared" \ --with-pcre=/usr/local/src/pcre2-10.x \ --with-zlib=/usr/local/src/zlib-1.3.x

这里--with-openssl-opt里的no-shared值得说一句:它让 OpenSSL 只产出静态库,nginx 会把静态库链接进去。好处是编译出来的 nginx 二进制自包含,换机器不用担心中间库版本不一致;代价是二进制体积变大,而且 OpenSSL 出安全公告时必须重新编译 nginx 才能修补。这是一个明确的权衡——追求部署一致性就选静态,追求补丁响应速度就选动态链接系统库。

用--with-*指定源码目录还有个副作用要注意:nginx 是用你指定的目录去编译,而不是去装一个库到系统里。所以目录名必须是解压后的顶层目录,且里面要能找到Configure(OpenSSL)或configure(PCRE2/zlib)脚本。如果目录名写错了或者解压层级多套了一层,configure 阶段会报找不到文件,错误信息还相当晦涩。


4. 编译、安装与验证的完整流水线

4.1 并行度与内存的换算关系

make -j N的 N 不是随便拍的。每个 gcc 编译进程编译 nginx 的单个源文件时,峰值内存大约在 100MB 到 200MB 之间,OpenSSL 的部分源文件吃内存更多,能到 250MB 上下。所以并行度的上限大致是:可用内存(GB)除以 0.3,再和 CPU 核数取较小值。

举两个具体例子。一台 2 核 2GB 的机器,2 ÷ 0.3 约等于 6,跟核数取小是 2,所以-j 2最稳;如果开-j 4,编译 OpenSSL 那几个大文件时极容易触发 OOM Killer,表现出来是gcc: Killed signal terminated program cc1,很多人以为是代码问题,其实是内存爆了。一台 8 核 16GB 的机器,16 ÷ 0.3 约 53,跟核数取小是 8,-j 8完全没问题。

# 稳妥的写法:自动取核数,但不超过 8 JOBS=$(nproc) [ "$JOBS" -gt 8 ] && JOBS=8 make -j"$JOBS" make install

编译时间参考:4 核机器上,不含 OpenSSL 源码时大约 1 到 2 分钟;带上 OpenSSL 源码大约 4 到 6 分钟。如果超过十分钟还没进展,多半是卡在某个依赖探测上,看一眼objs/autoconf.err能知道详情。

还有个小技巧:如果你只是改配置、不换版本,不需要重新编译。只有模块增减、编译参数变化、版本升级才需要重来。日常调整nginx.conf用nginx -s reload就够了,很多人以为改了配置就得重编,那是误解。

4.2 装完必须做的五项自检

make install跑完 banner 打印出来不代表成功。我有一套固定的自检清单,五项都过才算落地。

第一项,看版本和编译参数。/usr/local/nginx/sbin/nginx -V会打印nginx version、built by、configure arguments三行。重点核对 configure arguments 里该有的模块是不是都在,特别是我上面强调的 realip、stub_status、ssl、v2、stream。

第二项,测配置文件语法。/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf。-t会做语法检查和文件可达性检查,输出syntax is ok和test is successful。注意这一步会尝试打开日志文件,如果 logs 目录权限不对,此时就会暴露出来。

第三项,前台试跑。/usr/local/nginx/sbin/nginx -g "daemon off;"让它在终端里跑,能看到启动日志实时输出。按 Ctrl+C 停掉。这一步是为了确认端口能绑上、pid 文件能写、用户能切换。如果 80 端口被占用(比如系统自带的 httpd 或者已经有一个 nginx 在跑),会报bind() to 0.0.0.0:80 failed (98: Address already in use),用ss -lntp | grep :80定位占用进程。

第四项,验证模块真的可用。光看-V输出不够,得让配置真正引用来验证。比如写一个临时配置片段启用stub_status,curl一下看有没有返回数据;比如listen 443 ssl后nginx -t不报错,说明 SSL 模块确实编进去了。

第五项,检查动态依赖。ldd /usr/local/nginx/sbin/nginx会列出它依赖的所有.so。如果里面有not found,说明运行环境缺库,这在你把二进制拷到另一台机器时尤其常见。这也是我前面建议静态链接 OpenSSL 的原因——少一个依赖就少一个出问题的点。

/usr/local/nginx/sbin/nginx -V /usr/local/nginx/sbin/nginx -t /usr/local/nginx/sbin/nginx -g "daemon off;" # 前台跑一下,Ctrl+C 退出 ldd /usr/local/nginx/sbin/nginx | grep -i "not found"

自检清单过完,基本可以确认编译本身的正确性。接下来才是真正影响长期运维体验的部分:怎么让它以系统服务的方式被管理。


5. 让服务真正可用:systemd 托管与日志治理

5.1 手写 unit 文件时那几个不能错的字段

源码安装最大的缺口就是没有 service 文件。systemctl start nginx报Unit nginx.service not found,这个体验对新手很不友好。自己写一个 unit 文件,内容不多,但几个字段写错就会出各种诡异问题。

[Unit] Description=nginx - high performance web server Documentation=https://nginx.org/en/docs/ After=network-online.target remote-fs.target nss-lookup.target Wants=network-online.target [Service] Type=forking PIDFile=/usr/local/nginx/logs/nginx.pid ExecStartPre=/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf ExecReload=/bin/kill -s HUP $MAINPID ExecStop=/bin/kill -s QUIT $MAINPID TimeoutStopSec=5 KillMode=mixed PrivateTmp=true LimitNOFILE=65535 Restart=on-failure RestartSec=3 [Install] WantedBy=multi-user.target

逐条说几个关键点。

Type=forking配合PIDFile是必须的。nginx 默认以守护进程方式启动,master 进程 fork 之后父进程就退出了,systemd 需要靠 PIDFile 才能追踪到真正的 master。如果你把Type写成simple,systemd 会认为启动命令一返回服务就"退出"了,然后不停重启。

ExecStartPre里的-t是保险丝。有它在,配置文件语法错误时systemctl start会直接失败并在journalctl -u nginx里显示原因,而不是启动到一半进程没了。这个设计我强烈建议保留,能让故障暴露在启动阶段而不是运行阶段。

ExecReload=/bin/kill -s HUP $MAINPID让systemctl reload nginx等价于优雅重载配置,不中断现有连接。注意要写绝对路径/bin/kill,因为 systemd 的 PATH 环境和 shell 里不一样,只写kill可能找不到。

KillMode=mixed是个细节但重要。默认的control-group会在停止服务时把整个 cgroup 里的进程全部 SIGTERM,包括 master 和所有 worker,可能导致正在处理的请求被硬中断。mixed模式下 SIGTERM 只发给主进程(master),由 master 自己去优雅关闭 worker,而 SIGKILL 才会发给整个组。配合TimeoutStopSec=5,超过 5 秒还没优雅退出就强制杀。

LimitNOFILE=65535解决文件描述符上限问题。系统默认nofile常常是 1024,nginx 在并发上来之后会出现worker_connections are not enough或者Too many open files。这里的数值和后面worker_connections的乘积关系要在脑子里有数。

注意:worker_processes * worker_connections是理论上限的并发连接数,但实际受制于两个东西:一是单个 worker 进程的 fd 上限(LimitNOFILE),二是系统全局的fs.file-max。如果worker_connections 10240而LimitNOFILE只有 1024,那实际只能开 1024 个,配置写了也白写。这是我见过的最高频的"配了没用"现象。

配置写完执行三步走:

systemctl daemon-reload systemctl enable --now nginx systemctl status nginx --no-pager

5.2 端口放行、SELinux 与日志切分

服务起来了不代表外部能访问。Red Hat 系有两个门禁:防火墙和 SELinux,前者拦网络包,后者拦进程行为,两个都会让你对着一个"服务明明在跑但就是连不上"的页面发呆。

防火墙这边,先确认firewalld是否在运行,然后按需放行:

firewall-cmd --permanent --add-service=http firewall-cmd --permanent --add-service=https firewall-cmd --reload firewall-cmd --list-all

如果你把 nginx 监听在 8080 这类非标准端口,不能只加端口,还得处理 SELinux。SELinux 默认只允许 web 进程绑定带http_port_t标签的端口(通常是 80、443、8080 之外的一批)。监听新端口会报Permission denied,error.log里能看到bind() to 0.0.0.0:8888 failed (13: Permission denied)。解决办法是给端口打上正确的标签:

semanage port -a -t http_port_t -p tcp 8888 semanage port -l | grep http_port_t

如果 nginx 需要主动往外发请求(做反向代理到后端),还要开一个布尔值:setsebool -P httpd_can_network_connect 1。不开的话,反向代理会 502,错误日志里是connect() failed (13: Permission denied) while connecting to upstream。这个报错文案非常容易误导人去查后端服务,实际上是本机 SELinux 拦的。

日志切分这件事,我自己定过规矩:源码安装的 nginx 必须配 logrotate,不允许出现单个 access.log 长到几个 G 才发现的情况。包管理版本自带/etc/logrotate.d/nginx,源码版本得自己写。

/usr/local/nginx/logs/*.log { daily rotate 30 missingok notifempty compress delaycompress sharedscripts postrotate /bin/kill -USR1 $(cat /usr/local/nginx/logs/nginx.pid 2>/dev/null) 2>/dev/null || true endscript }

这里的关键是postrotate里的USR1信号。它的语义是"重新打开日志文件",nginx 收到后会关掉旧 fd、按配置路径重新打开,这样 logrotate 改名后的旧文件才能真正被释放,新日志写到新文件里。如果你用copytruncate模式(先拷再清空),在超高并发下可能丢掉清空瞬间写入的日志,所以我偏好信号重开的方式。

rotate 30是保留 30 天,compress开启 gzip 压缩归档,delaycompress让最近一份归档先不压(避免和正在写的进程抢文件)。missingok和notifempty是两个安全性开关,防止日志文件不存在时误报错、防止空日志占位置。


6. 平滑升级与回滚:源码安装的最大红利

6.1 信号驱动的热升级到底怎么走

这是源码编译相对包管理最有价值的能力,也是最多人不敢碰的部分。其实流程拆开看就五步,理解了每一步的信号语义,心里就有底了。

前置动作是记录当前编译参数,一定要记,因为它决定了新二进制能不能和老进程共存:

/usr/local/nginx/sbin/nginx -V 2>&1 | tee /usr/local/nginx/backup/nginx-V-$(date +%F).txt

然后编译新版本,不要执行 make install,因为 install 会覆盖正在运行的二进制。只把objs/nginx拿出来放到旁边:

cd /usr/local/src/nginx-<新版本> ./configure <照抄上面记录的全部参数> make -j"$(nproc)" cp objs/nginx /usr/local/nginx/sbin/nginx.new

验证新二进制能正常解析现有配置:

/usr/local/nginx/sbin/nginx.new -t -c /usr/local/nginx/conf/nginx.conf

通过后开始热替换。第一步,给正在运行的 master 发USR2信号。master 收到后会做三件事:把老 pid 文件重命名为nginx.pid.oldbin,然后用新二进制启动一个新的 master 和一个新的 worker 组。此时新老两套进程同时存在,都在监听同一个端口(因为 listen 的 fd 被子进程继承),新连接会由内核分配到两套进程上。

kill -USR2 $(cat /usr/local/nginx/logs/nginx.pid) sleep 2 ps -ef | grep "nginx:" | grep -v grep

第二步,确认新进程正常后,给老 master 发WINCH信号。老 master 收到后会让自己的 worker 优雅退出(处理完当前请求再退),但老 master 本身还留着。这一步之后,所有流量都由新进程处理。

kill -WINCH $(cat /usr/local/nginx/logs/nginx.pid.oldbin)

第三步,观察一段时间(我一般看五分钟到半小时,视业务量而定),确认新版本没问题,给老 master 发QUIT,彻底退出。

kill -QUIT $(cat /usr/local/nginx/logs/nginx.pid.oldbin)

回滚路径正好相反,这也是为什么要留着老 master 不立刻杀。如果新版本出问题,给老 master 发HUP让它重新拉起 worker,再给新 master 发QUIT让它退出。整个过程中连接不中断,因为端口 fd 一直在。

# 回滚:先唤醒老 master 的 worker,再让新 master 退出 kill -HUP $(cat /usr/local/nginx/logs/nginx.pid.oldbin) kill -QUIT $(cat /usr/local/nginx/logs/nginx.pid)

提示:热升级前必须确认新二进制的编译参数和老的一致,特别是--prefix、--pid-path、--lock-path这几个路径参数。如果路径变了,新旧进程会各自操作不同的 pid 文件,信号就发错了对象,那时候场面会比较难看。

6.2 这套机制背后的原理,值得多想一层

为什么能做到连接不中断?核心在于 Unix 的fork语义和文件描述符继承。master 进程在启动时已经bind并listen了端口,得到的是一个监听 socket 的 fd。当 masterfork出 worker 时,worker 继承了这个 fd,于是多个进程可以同时accept同一个监听 socket,内核负责唤醒哪个进程来处理新连接。热升级时,新 master 是从二进制文件重新 exec 出来的,但它并没有自己 bind,而是通过环境变量接收老 master 传过来的监听 fd。所以端口从未被释放,也就没有"断开连接"这个时刻。

这也解释了几个看起来很怪的现象。比如为什么新老进程能同时监听 80 端口——因为没有第二次 bind,而是共享同一个已存在的 socket。再比如为什么升级期间ss -lntp会看到两个进程都持有同一个端口——这是正常的,不是配置错误。

还有一个实践中的坑:如果你在升级前手动systemctl stop nginx,那 master 是被 QUIT 掉的,监听 fd 随之释放,就没法做无缝接管了,只能重启。热升级的前提是"老 master 保持存活"。所以操作顺序上,永远是先起新的、再退老的。


7. 常见问题速查与踩坑实录

7.1 编译阶段的典型报错

编译报错的文案通常很吓人,但绝大多数集中在依赖缺失和路径写错两类。我整理了一张速查表,基本覆盖了日常能遇到的九成情况。

报错关键词根本原因处理方式
the HTTP rewrite module requires the PCRE libraryPCRE2 开发包缺失,或版本不匹配装pcre2-devel;老版本 nginx 装pcre-devel
the HTTP gzip module requires the zlib libraryzlib 开发包缺失dnf install zlib-devel
SSL modules require the OpenSSL libraryOpenSSL 开发包缺失,或--with-openssl路径不对装openssl-devel,检查路径是否为解压顶层目录
C compiler cc is not foundgcc 未装,或 PATH 异常装 gcc,确认which cc有输出
./configure: error: invalid option "--with-xxx"使用的 nginx 版本不支持该模块对照该版本文档查模块名,老版本某些模块名不同
cc1: Killed signal terminated program并行度过高导致 OOM降低-j数值,或加 swap
undefined reference to ...链接参数缺失,通常是--with-ld-opt没写全补-L路径或减少静态链接
PCRE2 library is not found in ...用--with-pcre指了目录但目录内无有效源码用--with-pcre时目录必须是源码根目录

表里最后一条我印象特别深。当时我图省事,把 PCRE2 的 tar 包解压到了/usr/local/src/pcre2-10.42,但里面还套了一层同名目录,导致--with-pcre=/usr/local/src/pcre2-10.42找不到configure脚本。报错信息只说"不在该目录",没说是层级问题,我盯着路径对了三遍才反应过来。后来养成的习惯是解压完先ls一下这个目录,确认能看到configure、Makefile.in这些文件再往下走。

7.2 运行阶段那些"看着不像 nginx 问题"的问题

运行期的问题更有意思,因为很多报错信息指向的地方和真实原因完全无关。

现象一:systemctl status显示服务 active,但浏览器访问超时。大概率是防火墙没放行,或者云主机还有一层安全组。先本机curl -I 127.0.0.1验证服务本身,再从外部访问验证链路。本机通外部不通,问题一定在网络层,别去翻 nginx 配置。

现象二:反向代理返回 502,错误日志写Permission denied。Red Hat 系上八成是 SELinux。用getenforce确认状态,ausearch -m avc -ts recent能看到被拒绝的具体操作。前面提到的httpd_can_network_connect布尔值是最常见的一条,httpd_can_network_relay在某些场景也需要。

现象三:nginx -t报open() "/usr/local/nginx/logs/error.log" failed (13: Permission denied)。这是权限问题,但要注意 nginx 的检查机制:-t是用当前用户执行的,如果当前是普通用户而日志目录只有 nginx 用户可写,就会报这个。解决办法不是给日志 777,而是用 root 执行-t,或者临时chmod后立刻改回。给日志目录777是个坏习惯,会让同机其他用户能篡改日志。

现象四:业务量上来后error.log刷worker_connections are not enough。需要同时调三处:worker_connections值、systemd 的LimitNOFILE、系统fs.file-max。只改第一处是最常见的无效操作。另外worker_connections的数值是"每个 worker 的连接数",而一个反向代理场景下,一个客户端连接会占用前后各一个 fd(客户端一个、上游一个),所以实际能撑的客户端数是总连接数的一半左右。

现象五:修改配置reload后新配置不生效。先看nginx -t是否通过,再看ps -ef里 master 的启动时间有没有变(如果没变说明 reload 没成功)。还有一种是配置被 include 覆盖了,nginx -T(大写的 T)能打印出所有生效配置,包括 include 进来的,比-t更好用,排查"配置到底先读哪个"的问题非常有效。

现象六:二进制拷到新机器启动报error while loading shared libraries。ldd一看某个.so是not found。这就是动态链接的代价。要么在新机器装齐依赖,要么重新用静态链接 OpenSSL 方式编译。生产上我倾向于后者,一致性问题一次性解决。

7.3 我自己总结的几条经验

第一条,编译参数一定要存档。每台机器上/usr/local/nginx/backup/放一份nginx -V的输出,文件名叫带日期。升级时直接打开抄,不用靠记忆。这个习惯帮我避免过好几次参数漏写导致的模块缺失。

第二条,先小流量验证再全量切。热升级看起来无感,但新版本的配置兼容性、模块行为变化仍然可能出问题。我的做法是先在一台权重低的机器上升级,观察半小时,没问题再滚动到其余机器。

第三条,不要在生产机上长期保留编译源码目录。/usr/local/src/nginx-x.y.z编完就可以清掉,留着除了占空间没别的用。真要回溯,用存档的编译参数加上当前版本号,十分钟就能重编一个一模一样的出来。

第四条,把 nginx 的监控端点纳入基础监控。stub_status输出就三个核心数字:Active connections、accepts/handled/requests。handled比accepts小说明有连接被丢弃,通常是连接数打满了;requests减去handled能看出长连接复用率。这几个数字配上阈值告警,比等业务方打电话过来要有用得多。

说到最后,我个人在实际操作中的体会是:源码编译安装 nginx 这件事,真正的门槛不在编译本身,而在"装完之后你打算怎么运营它"。编译十分钟,运营三年。那十分钟里多花的力气,全都会在之后的每一次升级、每一次排查、每一次扩容里慢慢还回来。我现在给团队定的规范是:凡是承担流量入口角色的机器,一律源码编译,参数存档、systemd 托管、logrotate 配齐、热升级流程演练过一遍——这四条是底线,缺一条就不算装完。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 13:21:43

UDP通信实战:Windows客户端与终端聊天室

1. 先把这个标题拆开&#xff1a;UDP通信、Windows客户端、聊天室、终端重定向1.1 标题里藏着四件事&#xff0c;别混在一起做第一次看到“基于UDP协议的网络通信&#xff08;Windows客户端版简易聊天室版&#xff09;&#xff0c;重定向到终端”这个描述&#xff0c;很多人会本…

作者头像 李华
网站建设 2026/10/27 13:20:22

企业AI落地三大难题与JVS-AI解决方案

1. 企业AI落地的"水土不服"现象解析 第一次在企业内部部署AI系统时&#xff0c;我遇到了一个典型场景&#xff1a;市场部同事兴奋地展示了一个能自动生成营销文案的AI工具&#xff0c;结果生成的文案中出现了完全不适用于本地市场的节日促销方案——这个工具训练数据…

作者头像 李华
网站建设 2026/10/27 13:19:49

Pulsar消息路由全链路:Topic寻址、分区选择与订阅分发

做消息中间件这块时间长了&#xff0c;你会发现大部分线上问题最后都指向同一个地方——消息到底被送到哪儿去了&#xff0c;又是从哪儿被取走的。Pulsar消息路由这个话题&#xff0c;表面上看就是"生产者发到哪个分区、消费者从哪个分区读"&#xff0c;但真正扒开来…

作者头像 李华
网站建设 2026/10/27 13:19:48

Delphi/BCB电子表格源码深度解析与工业级调优指南

简介&#xff1a;本资源为DevExpress ExpressSpreadSheet v1.38版本的完整Delphi与C Builder&#xff08;BCB&#xff09;源代码包&#xff0c;面向中高级桌面应用开发者&#xff0c;解决电子表格功能深度集成、性能调优及UI样式定制等核心开发需求。包内共382个文件&#xff0…

作者头像 李华
网站建设 2026/10/27 13:19:36

Science Slop Index:用AI灌水检测与LaTeX源码分析量化论文质量

1. 从“Science Slop Index”说起&#xff1a;一个被AI灌水逼出来的评价维度 第一次看到“Science Slop Index”这个说法&#xff0c;我的反应是&#xff1a;终于有人把大家私下吐槽的事情摆到台面上了。所谓 Science Slop&#xff0c;直译过来就是“科研泔水”或者“学术灌水”…

作者头像 李华
网站建设 2026/10/27 13:19:12

Faster-Whisper工程实践:GPU加速语音转录与中文流式字幕实现

简介&#xff1a;本资源是基于CTranslate2加速实现的高效语音识别模型Faster-Whisper开源项目&#xff0c;面向AI工程师、语音处理开发者及Transformer模型实践者&#xff0c;解决传统Whisper推理内存占用高、速度慢的问题&#xff0c;适用于实时转录、边缘部署与批量音频处理等…

作者头像 李华