Go微服务平滑重启核心是旧进程用http.Server.Shutdown()等待请求结束、新进程通过文件描述符继承复用监听socket、SIGUSR2信号协同触发,三者缺一不可;直接调用http.ListenAndServe或os.Exit(0)会导致连接强制中断、数据丢失。

Go 微服务要实现真正可用的平滑重启,核心不是“能不能 fork 出新进程”,而是“老连接是否被强制断开”“新请求是否被丢弃”“子进程能否可靠继承监听 socket”。缺一环,就不是优雅,而是抖动或中断。
为什么直接用 http.ListenAndServe 无法平滑重启
它不接受 context.Context,也不暴露 listener 实例,更不会响应信号。你发 SIGUSR2 它完全无视;调 os.Exit(0) 或 server.Close() 会立刻终止所有连接,上传中断、WebSocket 断连、事务未提交——这不是重启,是拍死。
-
http.ListenAndServe是封装好的黑盒,没法在收到信号后先停 accept、再等请求结束 - 必须用
*http.Server实例 +srv.ListenAndServe()启动,才能后续调srv.Shutdown() - 没保存
srv引用,就等于放弃对 listener 的控制权 - 哪怕只加一行
log.Printf("shutting down...")在Shutdown()前,都可能让超时窗口错位,导致部分请求被强杀
如何正确触发并等待 srv.Shutdown()
Shutdown 不是“关服务”,它是“停止接收新连接,但继续服务旧连接直到它们自然结束”。它必须配信号监听和带超时的 context.Context,否则会卡住或忽略退出指令。
- 用
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)——别只监听SIGHUP,systemd 默认发SIGTERM,SIGUSR2才是热重启事实标准 - 超时 context 必须在
Shutdown()调用前创建:ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second);defer cancel()会提前失效,不能加 -
srv.Shutdown(ctx)返回后,才代表旧连接已全部退出;此时再启动新服务,否则bind: address already in use - 忽略
http.ErrServerClosed:这是ListenAndServe()正常退出标志,不是错误
子进程怎么拿到老 listener 的 fd 而不报错
Linux 下没有“复制 socket”这回事,只有“继承 fd”。Go 标准库不自动做这事,必须手动从 listener 提取 fd、传给子进程、再在子进程中恢复 listener。一步出错,就双进程争端口或新进程 bind 失败。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 父进程收到
SIGUSR2后,调ln.(*net.TCPListener).File()获取*os.File,记下file.Fd()(通常是 3,但别硬编码) - 启动子进程时,用
os.StartProcess,在sys.ProcAttr.Files中显式填入该 fd,并通过环境变量如LISTEN_FD=3传递 - 子进程第一行代码必须是:
fd, _ := strconv.Atoi(os.Getenv("LISTEN_FD"))→file := os.NewFile(uintptr(fd), "")→ln, _ := net.FileListener(file) - 验证方式:
lsof -i :8080应同时显示两个 PID;若只看到一个,说明 fd 没传成功,或子进程没调net.FileListener
长连接(WebSocket/SSE)为什么还是断了
Shutdown() 对已升级的连接完全无感知。它只管 HTTP 生命周期,不管底层 TCP 连接是否还在跑。WebSocket 升级后脱离 http.Server 管理,SSE 响应头设了 text/event-stream 也一样。
- handler 里所有阻塞操作必须响应
req.Context().Done():数据库查要用db.QueryContext(ctx, ...),HTTP 调用要用client.Do(req.WithContext(ctx)) - WebSocket 连接需手动存进
sync.Map,并在收到 shutdown 信号后遍历调conn.Close();别指望Shutdown()自动帮你关 - 别用
time.Sleep()等待,改用select { case - 中间件里启的 goroutine(如日志异步上报)必须监听同一
ctx,否则会拖慢整个 shutdown 流程
真正的难点不在写几行 Shutdown(),而在于全链路 ctx 传播、fd 继承的原子性、以及长连接生命周期的手动兜底。任何一环漏掉,重启就只是“看起来没报错”,实际用户已经收到了 connection reset。

















