Go中热重启必须用SIGUSR2而非SIGHUP,因其为用户自定义信号,避免与Kubernetes或systemd冲突;需通过ln.File()获取fd、ExtraFiles传递、环境变量LISTEN_FD显式指定,并配合http.Server.Shutdown()完成平滑交接。

Go 里用 SIGUSR2 触发重启,不是 SIGHUP
生产环境(尤其是 Kubernetes)默认发的是 SIGTERM 关停,但重启必须用 SIGUSR2 —— 它是用户自定义信号,不会被容器编排系统误杀,也不会和 systemd 的生命周期管理冲突。SIGHUP 在传统守护进程里常用,但 Go 程序若没显式注册,它会被忽略;更麻烦的是,很多云平台(如 AWS ECS、阿里云 ACK)压根不转发 SIGHUP。
实操要点:
- 用
signal.Notify(c, syscall.SIGUSR2)单独监听,别混进os.Interrupt或SIGTERM一起处理 - 收到
SIGUSR2后,立刻调用ln.(*net.TCPListener).File()拿到 fd,别等后续逻辑再取——listener 可能已被关闭 - 子进程启动前,必须设环境变量:
os.Setenv("LISTEN_FD", "3")(假设 fd 是 3),且父进程不能提前os.Exit(),否则子进程继承的 fd 会失效
os.StartProcess 传 fd 的三个硬性条件缺一不可
用 exec.Command 启动新进程时,ExtraFiles 字段只是“告诉子进程哪些 fd 可用”,但真正让子进程拿到并复用 socket,还得靠底层 os.StartProcess 的 Files 参数。漏掉任意一项,lsof 查不到双进程监听同一端口,重启就断连。
必须同时满足:
立即学习“go语言免费学习笔记(深入)”;
- 父进程调用
ln.File().Fd()得到真实 fd 值(通常是 3、4、5,不是 0/1/2) -
os.StartProcess的sys.ProcAttr.Files数组里,要显式包含该 fd(例如[]uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), 3}) - 子进程启动后,立即读取环境变量
os.Getenv("LISTEN_FD"),再用os.NewFile(3, "")构造*os.File,最后转成net.Listener
http.Server.Shutdown() 不等于重启,只负责旧进程收尾
很多人以为调了 srv.Shutdown(ctx) 就算完成优雅重启,其实它只做一件事:等已有 HTTP 请求结束或超时。它不拉新进程、不传 fd、不接管端口。如果没手动启动子进程,服务就直接下线了。
典型错误链:
- 收到
SIGUSR2后,只调srv.Shutdown(),没 exec 新进程 → 端口释放,新连接全失败 - 新进程启动了,但没正确用
net.FileListener(file)恢复 listener → bind: address already in use -
Shutdown()超时设太短(比如 1s),而业务请求平均耗时 3s → 强制中断,返回 502
建议超时至少设为最大请求预期耗时的 2 倍,且 Shutdown() 必须在子进程已成功监听后才调用。
macOS 和 Windows 上没法用这套方案
文件描述符继承依赖 Unix-like 系统的 fork/exec 语义,os.StartProcess 在 macOS 上虽可用,但内核对 fd 继承的支持不稳定(尤其在 SIP 启用时);Windows 根本不支持 socket fd 继承,net.FileListener 会 panic。
如果你的部署目标含 Windows 或需要跨平台,别硬套这套方案。要么改用轮询 reload 配置(不换二进制)、要么上反向代理(如 nginx 做 graceful reload)、要么接受短暂停机窗口。
真正容易被忽略的点:即使在 Linux 上,也要验证 /proc/<pid>/fd/</pid> 目录里,新旧进程是否都列出了同一个 socket fd —— 这是 fd 传递成功的唯一铁证,其他日志或端口监听状态都可能骗人。


















