Golang Web应用优雅重启方案_基于Signal信号的平滑升级

5次阅读

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

Golang Web应用优雅重启方案_基于Signal信号的平滑升级

Go 服务如何监听 SIGUSR2 实现平滑重启

Go 原生不支持 fork 子进程式热更,但可以用 SIGUSR2 触发主进程优雅关闭旧 listener、启动新 listener,再交出连接句柄。关键是不能直接 os.Exit(),也不能让新老 goroutine 同时 accept 同一端口。

  • SIGUSR2linux/unix 下约定俗成的“重载配置/重启服务”信号,systemdsupervisord 都支持发送它
  • 必须在 http.Server 启动前注册 signal handler,否则可能漏掉第一个信号
  • 旧 server 调用 srv.Shutdown() 时,要给足够时间处理完正在读写的连接(比如设 5s timeout),否则会强制断连
  • 新 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_FDSLISTEN_FD_NAMES
  • systemd 启动时默认不开启 FileDescriptorStoreMax=,需在 service 文件里显式加 FileDescriptorStoreMax=1

为什么不用 graceful 类第三方库

github.com/tylerb/gracefulgithub.com/soheilhy/cmux 看似省事,但它们要么已停止维护,要么只解决多协议复用,不解决跨进程 fd 传递。

立即学习go语言免费学习笔记(深入)”;

  • 标准库 http.Server.Shutdown() 自 Go 1.8 起就稳定可用,没必要引入额外依赖
  • 真正难的是 signal 协调和 fd 传递,这部分必须自己写清楚逻辑,封装成库反而掩盖细节,出问题更难 debug
  • 如果你用 net/http 之外的框架(如 ginecho),它们底层仍是 http.Server,只需把 shutdown 逻辑套进去,别被中间层迷惑

信号处理和 fd 传递这两步一旦写错,重启时就会出现连接拒绝或请求丢失,而且问题往往只在线上高并发下暴露。本地测通不等于线上安全,务必在 staging 环境用真实流量压测 shutdown 流程。

text=ZqhQzanResources