Go服务通过监听SIGUSR2信号实现平滑重启:先优雅关闭旧listener(Shutdown()设合理timeout),再启动新listener,关键在于父子进程间安全传递socket fd,需手动处理signal协调与fd继承,而非依赖第三方库。

Go 服务如何监听 SIGUSR2 实现平滑重启
Go 原生不支持 fork 子进程式热更,但可以用 SIGUSR2 触发主进程优雅关闭旧 listener、启动新 listener,再交出连接句柄。关键是不能直接 os.Exit(),也不能让新老 goroutine 同时 accept 同一端口。
-
SIGUSR2是 Linux/Unix 下约定俗成的“重载配置/重启服务”信号,systemd和supervisord都支持发送它 - 必须在
http.Server启动前注册 signal handler,否则可能漏掉第一个信号 - 旧 server 调用
srv.Shutdown()时,要给足够时间处理完正在读写的连接(比如设5stimeout),否则会强制断连 - 新 server 必须等旧 server 完全退出 listener(即
Shutdown()返回)后再ListenAndServe(),否则报address already in use
http.Server.Shutdown() 的 timeout 设置多长才合理
太短会丢请求,太长会拖慢重启节奏。实际取决于你最长的业务处理耗时 + 网络 RTT,不是拍脑袋定的。
- 如果接口里有调第三方 HTTP、DB 查询或文件 IO,timeout 至少要比它们的超时总和多
1–2s - 线上建议用
context.WithTimeout(ctx, 10*time.Second),比硬写数字更可控 - 注意:
Shutdown()只停止接收新连接,已建立的连接仍可继续读写,直到 handler 自己返回或 context 被 cancel - 别忽略
srv.Close()的 error 检查——如果 listener 已被关掉,Shutdown()会返回http.ErrServerClosed,这是正常路径
子进程接管 listener 文件描述符的坑在哪
真正平滑的关键不是 Go 代码,而是父子进程间传递 socket fd。Go 标准库不直接暴露 fd 传递逻辑,得靠 syscall + os/exec 配合环境变量或 Unix domain socket 中转。
- 父进程需用
syscall.RawConn.Control()获取 listener 的 fd,并通过cmd.ExtraFiles传给子进程 - 子进程启动时检查
os.Getenv("LISTEN_FDS") == "1"和os.Getenv("LISTEN_PID") == strconv.Itoa(os.Getppid()),防止误启动 - fd 编号从
3开始(0/1/2 是 stdin/stdout/stderr),别直接用3,要读LISTEN_FDS和LISTEN_FD_NAMES - systemd 启动时默认不开启
FileDescriptorStoreMax=,需在 service 文件里显式加FileDescriptorStoreMax=1
为什么不用 graceful 类第三方库
像 github.com/tylerb/graceful 或 github.com/soheilhy/cmux 看似省事,但它们要么已停止维护,要么只解决多协议复用,不解决跨进程 fd 传递。
立即学习“go语言免费学习笔记(深入)”;
- 标准库
http.Server.Shutdown()自 Go 1.8 起就稳定可用,没必要引入额外依赖 - 真正难的是 signal 协调和 fd 传递,这部分必须自己写清楚逻辑,封装成库反而掩盖细节,出问题更难 debug
- 如果你用
net/http之外的框架(如gin、echo),它们底层仍是http.Server,只需把 shutdown 逻辑套进去,别被中间层迷惑
信号处理和 fd 传递这两步一旦写错,重启时就会出现连接拒绝或请求丢失,而且问题往往只在线上高并发下暴露。本地测通不等于线上安全,务必在 staging 环境用真实流量压测 shutdown 流程。










