深蓝黑背景封面:右侧大标题「换发动机,不用降落」,副标题「Nginx 配置热加载与二进制热升级的平滑交接」;左侧原创示意图——两组 worker 方块一旧一新,旧的逐渐淡出变虚线,新的亮起实线,上方 master 进程发出一枚 reload 信号箭头,存量连接箭头从旧 worker 缓缓收尾;左上标注「面试经典 · 系统设计」,右上标注「Day 07 · 10-25」 图 1:本文封面,换发动机不用降落。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。题目通常这样问:改完 nginx.conf 为什么要 reload 而不是重启?线上要换新版本的 Nginx 二进制,怎么做到不断连接?

先把结论立住:**热更新不丢连接的本质,是监听套接字由 master 进程持有,worker 可以成批替换——新 worker 先起来接管新连接,旧 worker 不再接新连接、把手头存量连接服务完再退出。监听端口全程有人守着,所以外部看服务没有中断。**下面分两个场景拆开讲。

**场景一:配置热加载(reload)。**流程由 master 主导,对应官方文档里 HUP 信号的语义:master 收到 HUP 后先做一件事——检查新配置语法是否合法,并尝试应用它(打开新的日志文件、新的监听套接字)。如果这一步失败,master 回滚改动、继续用旧配置工作,等于这次 reload 被安全地拒绝;如果成功,master 用新配置拉起一批新 worker,同时向旧 worker 发出优雅退出指令。旧 worker 收到指令后关闭自己的监听套接字——不再接受任何新连接——但继续把已经建立的客户连接服务完,全部处理完才退出。过程中 ps 能看到「worker process is shutting down」这种过渡状态,新旧 worker 短暂并存是正常现象。

一个容易忽略的细节:reload 不是「刷新」,而是「换一批人」。配置是随 worker 进程生效的,新连接全部落到新 worker 上,自然用新配置;旧连接由旧 worker 负责到底,用的还是它启动时的旧配置。两种配置在同一个端口上短暂共存,互不干扰。

**场景二:二进制热升级。**要换的是可执行文件本身,流程在热加载之前多两步。第一步,把新二进制文件替换到位,然后向旧 master 发 USR2 信号:旧 master 先把自己的 pid 文件改名为 .oldbin 后缀,再启动新可执行文件——新文件拉起一个新 master,新 master 再拉起自己的一批新 worker。此刻系统里有两套 master 加 worker 并存,而且都在接受请求。第二步,确认新版本工作正常后,向旧 master 发 WINCH 信号,它的旧 worker 开始优雅退出;等旧 worker 全部退完,再向旧 master 发 QUIT,升级完成,只剩新的一套。

这中间的回滚通道是设计里最值钱的部分:旧 master 在升级全程没有关闭自己的监听套接字,如果新版本表现异常,给旧 master 发 HUP 就能让它重新拉起旧 worker 接管,再让新的一套退出即可回到升级前;甚至直接 TERM 掉新 master,旧 master 也会自动重新拉起 worker。升级不是一个不可回头的切换动作,而是一段「双保险并存期」。

双流程图:上方是配置热加载四步——master 收到 reload 信号,先用新配置拉起一批新 worker,新 worker 立即接管新连接,旧 worker 不再接受新连接、把手头存量连接处理完后自行退出,全程监听端口不断线。下方是二进制热升级的扩展步骤——在热加载之前多两步:替换可执行文件后向旧 master 发信号拉起一个「新 master」,新 master 再拉新 worker,此时新旧两套并存,确认无误后让旧 master 的旧 worker 全部退完再结束旧 master,出问题则可回滚让旧 master 重新接管。右侧提示框:热更新不丢连接的本质是「监听套接字由 master 持有、worker 成批替换」,存量连接由旧 worker 负责到底,长连接场景要给旧 worker 设退出宽限 图 2:热加载四步与热升级的并存、回滚设计。

**长连接场景要补一个动作。**优雅退出意味着旧 worker 会等存量连接全部结束——如果业务里有长连接(推送、流式响应),旧 worker 可能拖很久不退。官方从 1.11.11 版本起提供 worker_shutdown_timeout 指令:给优雅退出设一个宽限期,超时后强制关闭该 worker 上的连接,避免旧进程无限期赖着。设多长取决于业务能接受的最长连接时长,这是个需要拍板的取舍,不是默认值能替你决定的。

落到操作上,三条习惯能避开大多数坑:reload 之前先跑一次配置预检,把语法错误挡在信号发出之前(master 自己也会检,但预检能让你在改动现场就知道结果);升级二进制时盯住错误日志,确认新 worker 正常接管之后再让旧的一套退场;全程给监控打上时间点标记,并存期持续了多久、旧 worker 何时退完,都要看得见,而不是靠感觉。

**常见误区。**一是以为 reload 会断一下连接,实际上监听套接字从未易手;二是交接期嫌旧 worker 碍事直接 kill -9,存量连接被当场截断,把优雅退出生生做成事故;三是热升级时忘了 .oldbin 和旧 master 的存在,以为升级是原子切换,出问题时不知道回滚通道一直开着;四是在长连接业务里不设退出宽限,升级完成后旧 worker 挂几个小时,监控上一看以为出了故障。

这套「先起新的、再退旧的、全程留回滚」的交接思路不只属于 Nginx:模型网关更新路由与限流配置、推理实例滚动升级、模型版本灰度,面对的都是同一个问题——替换干活的进程,但别让正在进行的请求掉地上。你的服务在做版本升级时,存量连接是怎么收场的?欢迎分享。