go服务平滑重启必须同时满足信号协调、文件描述符继承和上下文传播三者协同,仅调用http.server.shutdown()会导致连接中断、address already in use或502错误,因其不传递socket fd、不启动新进程、不释放端口。

Go 服务平滑重启不是“热更新”,而是靠新旧进程交接监听 socket —— 不传 fd,就必然中断;只调 http.Server.Shutdown(),等于没做。
为什么 http.Server.Shutdown() 单独调用会丢请求
它只停止 Accept() 新连接、等待已有 handler 返回,但不会:
- 监听任何信号(
SIGUSR2或SIGTERM需手动注册) - 把当前
net.Listener的文件描述符交给新进程 - 感知长连接(如 WebSocket、HTTP/2 流)是否真正空闲
常见后果:ListenAndServe() 返回 http.ErrServerClosed 后你立刻 os.Exit(0),handler 还在往响应体写数据,客户端收到 connection reset by peer;或者新进程启动时 bind 失败,日志报 listen tcp :8080: bind: address already in use。
Linux 下必须用 ExtraFiles 传递 listener fd
跨进程复用监听 socket 的唯一可靠方式是父进程调 ln.(*net.TCPListener).File() 拿到 *os.File,通过 exec.Cmd.ExtraFiles 传给子进程。
- 不是临时文件,不是环境变量,不是
os.Stdin.Fd() - 子进程启动后,fd 编号固定为 3(因 0/1/2 是 stdin/stdout/stderr),用
os.NewFile(3, "")恢复句柄 - 再用
net.FileListener(file)转成net.Listener,喂给http.Server.Serve() -
http.Server.Addr必须设为空字符串"",否则Serve()会尝试重新net.Listen(),触发冲突
Kubernetes 场景下别自己 fork 子进程
容器内进程几乎从不等于 PID 1。kill -USR2 $PID 极大概率被 sh 或 init 拦截,子进程拿不到 fd,net.FileListener() 直接返回 invalid argument。
- 硬编码自 fork 在 K8s 中基本不可用
- 正确做法是更新
Deployment.spec.template,配合preStop生命周期钩子发SIGTERM给主进程(用于优雅关闭),而非SIGUSR2 - 若需配置热重载,应走
ConfigMap+fsnotify,而不是进程级重启
systemd 环境需显式配置 FileDescriptorStoreMax=1
否则 LISTEN_FDS 不生效。而 systemd 默认发 SIGTERM,不是 SIGHUP 或 SIGUSR2 —— 若没注册 syscall.SIGTERM,进程直接退出,跳过 Shutdown() 流程。
-
http.Server实例必须显式创建,不能用http.ListenAndServe()这类封装,否则无法控制超时、TLS、生命周期 -
ReadTimeout、WriteTimeout、IdleTimeout必须设,否则Shutdown()可能无限等待空闲连接 - 所有阻塞调用(DB 查询、HTTP 调用、
time.Sleep())必须用带ctx的版本,中间件里启的 goroutine 也得监听同一ctx,否则拖慢 shutdown
最容易被忽略的是:子进程恢复 listener 后,必须验证 fileListener.Addr().String() 是否与原始地址一致 —— 原始监听用 ":8080" 时,复原后可能变成 "127.0.0.1:34567",导致健康检查失败或路由错乱。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











