facebookgo/grace 更适合生产环境,因其通过 gracehttp.Serve 封装 socket 传递、子进程启动与父进程等待退出全流程,显式继承监听 fd 避免 bind 失败,且默认用 SIGUSR2 触发、无需手动处理 SO_REUSEPORT 或 FD_CLOEXEC。
go 服务要实现真正可用的平滑重启,核心不是“能不能重启”,而是“老连接是否被强制断开”“新请求是否被丢弃”“子进程能否可靠继承 socket”。
和
是目前最常被选中的两个方案,但它们行为差异明显,选错会导致 sigusr2 不生效、子进程 bind 失败、或旧连接被 rst 中断。
为什么
的
更适合生产环境
它把监听 socket 的传递、子进程启动、父进程等待退出全封装进一个函数里,不需要手动处理
或
标志。关键点在于:
默认使用
触发重启,且子进程通过
启动时显式继承了父进程的监听文件描述符(fd),避免了重新 bind 端口失败的问题。
常见错误现象:直接用
+ 自己 fork 子进程,却没调用
清除父进程 fd 的 close-on-exec 标志,导致子进程启动后监听 fd 被自动关闭,报错
。
实操建议:
确保 Go 版本 ≥ 1.8(
的
方法才稳定支持 fd 导出)
不要在
中提前调用
或修改
,否则子进程可能无法正确重定向日志
若服务监听多个端口(如 HTTP + HTTPS + metrics),需用
,不能只传单个
的
在什么场景下会卡住
它依赖
信号触发 fork,并在子进程中调用
重新监听。这意味着它实际是“双监听”模式:新旧进程同时 listen 同一地址(靠
支持)。问题就出在这里——macOS 不支持
,Linux 上若内核 SO_REUSEADDR,导致新请求被随机分发到旧进程,而旧进程在收到
后已停止 accept,出现 connection refused。
另一个坑是:它默认在子进程启动后立刻向父进程发送
,但不等待父进程完成正在处理的请求。如果你的 handler 里有长耗时操作(比如
),父进程可能在请求中途就被 kill -9 强制终止。
实操建议:
仅限 Linux 生产环境使用,且确认内核版本 ≥ 3.9
必须配合
/
设置超时,否则父进程可能无限 hang 在
避免在
中做阻塞初始化(如连 DB、读大配置文件),因为 fork 后子进程会重复执行,易引发资源竞争
自研平滑重启时,
比裸写
少踩哪些坑
把“PID 文件管理”“子进程信号同步”“socket fd 安全传递”都做了抽象。比如它会在子进程启动前先写 PID 文件,再通过
复制监听 fd 并设置
,最后才 fork;父进程退出前还会等子进程发出 readiness 信号(通过管道),确保新服务已 ready 才真正 shutdown。
裸写
最容易漏的是:没调用
传入 fd,或者忘了在子进程中用
重建 listener,结果子进程根本没监听任何端口。
实操建议:
必须指定
,否则重启后无法定位新进程 PID,监控和运维脚本会失效
不要在子进程中调用
,
依赖子进程正常 exit 来判断升级完成
若服务用
做代理,需额外将上游连接池的
放到
钩子里,防止旧连接复用到已关闭的后端
SIGUSR2 信号没反应?先检查这三件事
不是代码没写信号监听,而是信号被中间层吞掉了。常见原因:
第一,进程被 systemd 管理时,默认
,发给主进程的
会被转发给整个 cgroup,包括子进程,导致多个进程同时尝试重启,互相干扰。
第二,Docker 容器中未加
参数,容器内没有 init 进程接管子进程,
可能被 PID 1 的 shell 截获,根本没传给 Go 进程。
第三,程序运行在 k8s 中,且用了
的 exec 方式健康检查,probe 脚本里若用了
,但
返回多个 PID(比如有 zombie 进程残留),信号会发错目标。
实操建议:
systemd service 文件里加
,并用
Docker run 加
,或用
作为入口命令
k8s 中改用
健康检查,避免 exec 场景下的 PID 不确定性
真正难的不是让新进程起来,而是让旧连接在退出前完成所有 I/O、中间件中间状态清理、以及上下文取消传播到位。很多“看似成功”的平滑重启,其实只是没压测出长连接场景下的 race condition。
facebookgo/gracefvbock/endlessfacebookgo/gracegracehttp.ServeSO_REUSEPORTFD_CLOEXECgracehttp.ServeSIGUSR2os.StartProcesshttp.ListenAndServesyscall.CloseOnExeclisten tcp :8080: bind: address already in usenet.ListenerFile()main()log.SetOutputos.Stderrgracehttp.ServeMulti*http.Serverfvbock/endlessListenAndServeSIGHUPnet.Listen("tcp", addr)SO_REUSEPORTSO_REUSEPORTSIGTERMSIGTERMtime.Sleep(10 * time.Second)endless.WithReadTimeoutWithWriteTimeoutAcceptinit()tableflipexec.Commandcloudflare/tableflipunix.DupCLOEXEC=falseexec.Commandcmd.ExtraFilesos.NewFileOptions.PIDFileos.Exit(0)tableflipnet/http/httputil.ReverseProxyCloseIdleConnectionsPreStartProcessKillMode=control-groupSIGUSR2--initSIGUSR2livenessProbekill -USR2 $(pidof myapp)pidofKillMode=processExecReload=/bin/kill -USR2 $MAINPID--inittinihttpGet