Linux 服务平滑重启的实现思路

7次阅读

平滑重启是在不中断现有连接、不丢弃处理中请求的前提下完成新旧进程切换,要求客户端无感知、TCP连接不断、长连接持续有效,依赖SO_REUSEPORT内核支持与进程协作机制。

Linux 服务平滑重启的实现思路

什么是平滑重启(graceful restart)

平滑重启不是 kill -HUP 或 systemctl reload 的简单代名词,而是指服务在不中断已有连接、不丢弃正在处理请求的前提下完成新旧进程切换。关键判断标准是:客户端无感知,TCP 连接不断,http 长连接、websocket、上传中请求等持续有效。

实现依赖两个核心能力:SO_REUSEPORT(内核支持)和进程间协作(如父进程监听 socket、子进程接管)。nginxopenrestygunicorn 等成熟服务已内置该逻辑;自研服务需手动实现。

linux 内核层面的关键支持:SO_REUSEPORT

没有 SO_REUSEPORT,多个进程无法同时 bind 同一端口,平滑重启只能靠「先启后停」或「socket 传递」。启用它后,新旧进程可共存并行接收新连接,旧进程只处理已有连接直至自然退出。

  • 要求内核 ≥ 3.9(主流发行版默认开启)
  • 需在 socket 创建时显式设置:setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on))
  • 注意:SO_REUSEADDR 不等价于 SO_REUSEPORT,后者才支持负载分发与平滑切换
  • glibc 2.22+ 才完全支持该选项的可靠行为,低版本可能 fallback 到单进程绑定

应用层如何安全关闭旧进程

新进程启动后,旧进程不能立刻退出,必须等待所有活跃连接完成处理。常见做法是监听信号(如 SIGUSR2SIGHUP),进入「拒绝新连接 + 等待存量连接关闭」状态。

  • 监听 socket 应设为非阻塞,并在收到信号后 close() 它,阻止 accept 新连接
  • 对每个已建立连接,设置合理的 idle timeout(如 30s),超时后主动 close
  • 若使用 epoll/kqueue,需从事件循环中移除监听 fd,但保留 client fd 直至其自然断开或超时
  • 避免用 shutdown(SHUT_RDWR) 强制终止连接——这会破坏 HTTP/2 流或 WebSocket 帧完整性

systemd 下的配合要点

直接用 systemctl reload 不一定触发平滑重启,取决于服务是否实现了 reload 逻辑。systemd 本身不理解「graceful」,它只负责发信号、等待进程退出。

  • 服务 unit 文件中应配置:Type=notify + Notifyaccess=all,允许进程通过 sd_notify("RELOADING=1") 告知 systemd 正在重载
  • Restart=on-failure 不能替代平滑重启——它是崩溃恢复机制,会中断连接
  • 避免在 ExecReload 中写 kill -HUP $MAINPID 就完事;要确保目标进程真正支持该信号语义
  • 调试时可用 journalctl -u your-service -f 观察 sd_notify 日志,确认 reload 是否被正确识别

实际最难的部分往往不在代码,而在连接生命周期的边界判断:比如长轮询响应未发出、HTTP/2 push stream 正在传输、TLS 握手半途而废……这些场景下,仅靠超时不够,需要结合协议层状态跟踪。

text=ZqhQzanResources