平滑重启本质是进程替换而非单进程内重启,需通过外部机制启动新进程并优雅终止旧进程,依赖操作系统级socket继承或信号协作;gin自身不提供该能力,须借助endless等第三方库或手写fork+exec流程实现。

平滑重启的本质是进程替换,不是单进程内“重启”
Gin 本身不提供平滑重启能力——它只是一个 HTTP 路由框架,没有进程管理逻辑。所谓“Gin 平滑重启”,实际是用外部机制启动新进程、优雅终止旧进程,中间靠操作系统级的 socket 继承或信号协作完成切换。误以为调用某个 router.Restart() 就能实现,会直接卡在原地。
常见错误现象包括:发送 SIGHUP 后服务无响应、旧进程没退出导致端口占用、新请求被拒绝、正在处理的长耗时请求(如 time.Sleep(5 * time.Second))被强制中断。
- Go 1.8+ 原生
http.Server.Shutdown()只负责单进程优雅关机,不涉及 fork 新进程 - 第三方库(如
fvbock/endless)才是真正在做父子进程协作和文件描述符传递 - 必须确保监听 socket 是通过
os.NewFile(uintptr(fd), "listener")在子进程中重建,否则新进程无法接收连接
endless.ListenAndServe 是最轻量且稳定的方案
fvbock/endless 库至今仍被大量生产项目使用(2026 年最新 commit 在 2025 年),它封装了 fork + socket 传递 + 信号转发全套逻辑,无需手动处理 LISTENER_FD 或环境变量。
关键行为:
- 收到
SIGHUP→ fork 子进程,把监听 socket 文件描述符传过去 → 父进程进入等待状态 - 收到
SIGINT或SIGTERM→ 触发Shutdown(),等所有活跃连接关闭后退出 - 子进程启动后,父进程不会立即退出,而是持续监听连接关闭状态,直到无活跃连接才真正退出
示例代码中这一行必须保留:if err := endless.ListenAndServe(":8080", router); err != nil。不要替换成 router.Run(),后者完全绕过平滑逻辑。
自建方案要注意文件描述符泄漏和信号屏蔽
若因合规或安全要求不能用第三方库,需手写 fork+exec 流程,最容易出错的是文件描述符未正确继承或关闭。
典型坑点:
- 父进程调用
syscall.Close()关闭 listener 前,必须先用syscall.Dup()复制 fd 并传给子进程,否则子进程拿到的是无效 fd - 子进程启动后,父进程要调用
syscall.Setpgid(0, 0)避免信号被子进程继承干扰 - 必须显式设置
cmd.ExtraFiles = []*os.File{file},否则LISTENER_FD=3在子进程中不可见 - 子进程里重建 listener 后,要立刻
file.Close(),否则父进程退出时该 fd 会被系统回收,导致新进程监听失效
验证是否真平滑:别只看日志,要看连接生命周期
光看控制台输出 Received SIGINT 和 Waiting for connections to finish 不代表成功。真正验证点只有两个:
- 在发送 kill 信号前发起一个长请求(如
curl http://localhost:8080/ping且 handler 中有time.Sleep(10 * time.Second)),确认该请求仍能完整返回 200,而不是超时或 connection reset - 用
lsof -i :8080观察:旧进程的 LISTEN 状态应持续到所有 ESTABLISHED 连接关闭后才消失;新进程的 LISTENER 出现在旧进程退出前
很多“看似平滑”的实现,其实只是把 Shutdown 延迟了几秒,但没做进程替换——这意味着部署新版本时仍需停服。真正的平滑重启,必须有两个进程同时存在过短暂重叠期。











