http.Server.Shutdown仅优雅关闭旧进程,不启动新进程;真正优雅重启需父子进程协作传递监听socket的fd,并用context控制请求生命周期。

用 http.Server.Shutdown 做关机,不是重启
很多人以为调用 srv.Shutdown() 就算“优雅重启”,其实它只负责关——旧进程停掉,新进程得你手动拉起。真正的重启必须父子进程协作:旧进程把监听 socket 的文件描述符(fd)传给新进程,新进程用它继续 accept(),才能不丢连接。
-
Shutdown()只等待已有请求完成或超时,它不启动新进程,也不传递 fd - 直接
exec.Command(os.Args[0], os.Args[1:]...)启动新进程?不行——新进程没监听端口,会报address already in use - 必须用
ln.(*net.TCPListener).File()拿到 fd,再通过环境变量(如LISTEN_FD=3)或Files字段传给子进程 - 新进程要调用
os.NewFile(3, "")恢复 listener,再用net.FileListener()包装成可监听对象
为什么 endless 或 graceful 库现在不推荐直接用
这些老牌库(如 github.com/fvbock/endless)封装了 fork + fd 传递逻辑,但维护已停滞:endless 最后更新是 2021 年,不兼容 Go 1.21+ 的 syscall 变更;graceful 已归档,且内部仍用过时的 syscall.ForkExec,在 macOS 和新版 Linux 上容易出错。
- 它们默认监听
SIGHUP触发重启,但现代容器编排(如 Kubernetes)发的是SIGTERM,不是SIGHUP,信号没注册就收不到 - 底层依赖
syscall直接操作 fd,Go 1.19 起syscall被标记为 deprecated,Go 1.22+ 可能彻底移除 - 没有显式控制新旧进程共存时间,容易出现双进程争抢 accept,或旧进程提前退出导致连接断开
- 建议改用标准库组合:自己写
signal.Notify+os.StartProcess+net.FileListener,可控性高、无隐藏依赖
os.StartProcess 传 fd 的关键三步不能漏
fd 传递不是“复制文件句柄”,而是让子进程继承父进程打开的 socket。Linux 下靠 Files 字段实现,Windows 不支持,所以该方案仅限 Unix-like 系统。
- 父进程:调用
ln.(*net.TCPListener).File()得到*os.File,记下它的file.Fd()(比如是3) - 启动子进程时,
os.StartProcess的sys.ProcAttr.Files必须包含[]uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), 3}—— 缺少3,子进程就拿不到 listener - 子进程:读取环境变量(如
os.Getenv("LISTEN_FD")),用os.NewFile(3, "")恢复*os.File,再用net.FileListener(file)得到可监听的net.Listener - 务必验证:用
lsof -i :8080查看,重启中应同时看到两个 PID 在监听同一端口;若只看到一个,说明 fd 传递失败或子进程没正确读取环境变量
handler 里不用 ctx,Shutdown 就会卡住
http.Server.Shutdown 返回的前提是所有活跃请求结束。如果 handler 里用了阻塞操作(比如没带 context 的数据库查询、time.Sleep、第三方 SDK 同步调用),它就会等满超时时间才强制断连,用户请求实际被截断,不是“优雅”。
立即学习“go语言免费学习笔记(深入)”;
- 所有 I/O 操作必须用带
ctx的变体:db.QueryContext(ctx, ...)、http.DefaultClient.Do(req.WithContext(ctx))、c.Request.Context()传下去 - 中间件里启动的 goroutine(如日志上报、指标打点)必须监听
ctx.Done(),否则它们会长期存活,拖慢 Shutdown -
http.Server.ReadTimeout/WriteTimeout对 Shutdown 无效——它们只管单次读写,不管整个请求生命周期 - 测试方法:启一个长耗时接口(如
time.Sleep(10 * time.Second)),发请求后立刻kill -TERM $PID,观察日志里是否真等满 10 秒才退出
fd 传递和 context 控制是两套正交机制:前者解决“连接不丢”,后者解决“请求不杀”。漏掉任意一个,都不算真正优雅。尤其注意新进程启动后,旧进程不能立刻退出——得等新进程成功监听并 ready 后,再由新进程发信号通知旧进程 shutdown,这个协同逻辑最容易被手写脚本忽略。


















