Go优雅重启需Shutdown()等待请求结束、fd继承复用监听socket、SIGUSR2触发新进程三者协同;直接exit会导致RST、goroutine泄漏、数据丢失。

Go 语言做优雅重启,核心不是“不中断”,而是“旧进程等完请求、新进程接住新连接”——靠 http.Server.Shutdown() + 文件描述符继承 + SIGUSR2 信号协同完成,缺一不可。
为什么不能直接 os.Exit(0) 或杀进程?
直接退出会触发 TCP RST,客户端立刻收到 connection reset by peer 或超时;更隐蔽的问题是:goroutine 没被 cancel、数据库连接没释放、日志缓冲没 flush,导致内存泄漏或数据丢失。
- 现象:curl 请求中途断开、监控显示 5xx 突增、
lsof -i :8080发现大量CLOSE_WAIT - 根本原因:没调
Shutdown(),只调了Close()或什么都没调就 exit - Go 1.8+ 必须用
Shutdown(ctx),它会等活跃请求自然结束;Close()是立即关闭 listener,不等正在处理的请求
怎么让新进程复用老进程的监听 socket?
关键在文件描述符(fd)传递:父进程调 ln.(*net.TCPListener).File() 拿到 fd,通过环境变量(如 LISTENER_FD=3)传给子进程,子进程用 os.NewFile(3, "") 恢复成 *os.File,再转成 net.Listener。
- 错误做法:新进程直接
http.ListenAndServe(":8080", handler)→ 报bind: address already in use - Linux 下 fd 默认不继承,但 Go 的
File()已自动设CloseOnExec=false,不用手动干预 - 验证是否成功:
lsof -i :8080应同时看到两个 PID;若只看到一个,说明 fd 传递失败或新进程没正确调net.FileListener()
http.Server.Shutdown() 怎么配 context 才不卡住?
超时时间不是越长越好,context 要干净——parent 不能带 cancel,否则可能提前终止 shutdown 流程;且 handler 内部必须用传入的 ctx 控制所有阻塞操作(比如 db.QueryContext(ctx, ...))。
立即学习“go语言免费学习笔记(深入)”;
- 常见卡住原因:handler 里用了
time.Sleep(10 * time.Second)却没检查ctx.Done(),导致 Shutdown 等满超时才强制断开 - 推荐超时:30–60 秒,太短会强杀长请求(如文件上传),太长拖慢发布节奏
- 别写
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()——defer cancel()会在 Shutdown 返回前就触发,导致 ctx 提前取消
要不要用第三方库(比如 endless 或 graceful)?
不推荐。这些库大多停止维护,且多数不兼容 Go 1.8+ 原生 Shutdown();endless 虽仍有人用,但它内部封装了 fork 和信号逻辑,出问题难调试,还隐式依赖 SIGHUP,和 systemd 等现代 init 系统冲突。
- 手写更可控:只依赖标准库,逻辑透明,signal 处理时机、listener 传递方式、超时策略全由你定
- 最小必要组件就三个:
signal.Notify(quit, syscall.SIGUSR2)、exec.Command(os.Args[0], os.Args[1:]...)、http.Server.Shutdown() - 真正容易被忽略的点:新进程启动后,不能立刻
listener.Accept(),要等父进程调完ln.Close()(通常在 Shutdown 返回后);否则可能因 socket 处于 TIME_WAIT 或内核状态未切换,导致 accept 失败


















