facebookgo/grace的sigusr2不生效,主因是signal.notify被提前占用或主goroutine被阻塞;多端口需用servemulti而非serve;子进程bind失败多因go版本<1.8或日志重定向干扰fd继承;旧连接rst源于handler未设超时导致shutdown卡住。

facebookgo/grace 启动后 SIGUSR2 不生效?检查信号注册和进程模型
gracehttp.Serve 默认只响应 SIGUSR2,但如果你在 main 中提前启用了自定义 signal.Notify(比如监听了 SIGINT 或 SIGTERM),就可能阻塞或覆盖 grace 内部的信号处理 goroutine。它不是“注册多个 handler”,而是依赖一个干净的、未被占用的 signal channel。
常见错误现象:kill -USR2 $PID 后无反应,日志无重启提示,子进程未启动。
- 确保没调用
signal.Notify拦截SIGUSR2;grace 自己会做 - 不要在
main()里起 long-running goroutine 并阻塞主 goroutine —— grace 需要主 goroutine 空闲来接收信号 - 若用 systemd 托管,确认
KillMode=control-group(而非process),否则发送信号时可能打到子进程而非父 grace 进程
监听多个端口时 panic: "only one listener per address"?必须用 ServeMulti
直接把多个 *http.Server 传给 gracehttp.Serve 会触发 panic,因为该函数只接受单个 server 实例,且内部对 Addr 做了唯一性校验。多端口(如 HTTP + HTTPS + /metrics)必须显式使用 gracehttp.ServeMulti。
常见错误现象:启动时报 listen tcp :8080: bind: address already in use 或 panic 信息含 only one listener。
- 每个
*http.Server必须绑定不同地址(哪怕都是localhost,端口也得不同) - 用
gracehttp.ServeMulti(servers...)替代gracehttp.Serve(server) - 避免在多个 server 间复用同一个
http.Handler时误改全局状态(如中间件里修改了http.DefaultServeMux)
子进程启动后立刻报 bind 失败?Go 版本和 FD 继承是硬门槛
grace 依赖 Go 1.8+ 的 net.Listener.File() 导出 socket fd,并在子进程中用 net.FileListener() 恢复。低于 1.8 会导致 fd 无效,子进程 fallback 到重新 net.Listen,从而触发端口冲突。
常见错误现象:子进程日志出现 listen tcp :8080: bind: address already in use,父进程却已关闭 listener。
- 确认 Go 版本 ≥
1.8(推荐 ≥1.16,避免旧版File()在某些 syscall 下返回 nil) - 不要在
main()中提前调用log.SetOutput(os.Stderr)或重定向os.Stdout—— 子进程从父进程继承 fd 时,若 stderr 已被替换,可能导致 fd 传递失败 - macOS 上测试时注意:虽然 grace 支持 macOS,但若父进程被 launchd 管理,需额外配置
EnableTransactions和KeepAlive,否则子进程可能无法读取 stdin 中的 fd
旧连接被 RST 中断?Shutdown 超时与 handler 长耗时不匹配
grace 的父进程 shutdown 流程依赖 http.Server.Shutdown(),而后者默认等待所有活跃连接 close。但如果你的 handler 里有未受控的 time.Sleep、阻塞 I/O 或无超时的数据库查询,Shutdown() 就会卡住,最终被系统 kill -9 强制终止,导致连接被 RST。
常见错误现象:curl 请求中途断开,TCP 层看到 RST 包;日志里没有 “shutting down” 完整流程。
- 务必为每个 handler 设置 context 超时,例如
r.Context().Done()配合select检查中断 - 避免在 handler 中直接调用无 timeout 的
http.Get、database/sql.QueryRow等 - 可在
gracehttp.Serve前设置全局http.DefaultClient.Timeout,但更推荐 handler 级细粒度控制
真正难的从来不是“怎么让新进程起来”,而是“怎么让旧连接不感知重启”。fd 传递只是起点,handler 里的每一条阻塞路径,都可能是 RST 的源头。











