Go守护进程平滑重启需父进程传递监听套接字文件描述符给子进程,通过SIGHUP触发:父进程关闭listener、启动子进程并等待活跃连接结束;SIGTERM用于优雅关闭,须调用http.Server.Shutdown()配合context超时,并正确处理数据库等外部依赖。

Go守护进程如何用信号触发平滑重启
平滑重启的关键不是杀死进程再拉起,而是让新进程接管监听套接字、旧进程处理完已有连接后再退出。Go本身不直接支持socket传递,得靠os/exec配合net.Listener的File()方法导出文件描述符,再由子进程用net.FileListener重建监听器。
常见错误是父进程在exec.Command启动子进程后立刻os.Exit(0),导致子进程继承的fd被关闭;或没正确设置syscall.SIGHUP的os.Signal监听,导致信号被忽略。
- 主进程启动时需调用
syscall.Setenv("LISTEN_FDS", "1")(若只监听一个端口),并把listener.(*net.TCPListener).File()传给子进程 - 子进程启动后立即读取环境变量
LISTEN_FDS和LISTEN_PID,用os.NewFile(3, "")(fd从3开始)构造*os.File,再转为net.Listener - 父进程收到
SIGHUP后,先listener.Close(),再启动子进程,最后等待所有活跃连接完成(可用sync.WaitGroup计数)
为什么os.Interrupt和syscall.SIGTERM要分开处理
os.Interrupt对应Ctrl+C(即SIGINT),常用于开发调试;而生产环境的优雅关闭应响应SIGTERM——容器编排系统(如Kubernetes)默认发送它。混用会导致本地测试行为与线上不一致。
更隐蔽的问题是:若仅监听os.Interrupt,在systemd服务中会被忽略,因为systemd发的是SIGTERM;若未设置signal.Ignore(syscall.SIGPIPE),某些I/O操作可能意外panic。
立即学习“go语言免费学习笔记(深入)”;
- 用
signal.Notify(c, syscall.SIGTERM, os.Interrupt)同时捕获两者,但内部逻辑要区分:SIGTERM走完整关闭流程,os.Interrupt可跳过资源清理直接退出 - 关闭前必须调用
http.Server.Shutdown()(如果用了http.Serve),它会阻塞直到所有HTTP请求完成,超时需显式设置context.WithTimeout - 数据库连接池、消息队列消费者等外部依赖,应在
Shutdown()之后单独调用Close()或Stop(),且需设超时避免卡死
http.Server.Shutdown()不生效的三个典型原因
现象是进程收到SIGTERM后立即退出,正在处理的HTTP请求被中断,返回502或连接重置。根本原因往往不在Shutdown()调用本身,而在上下文管理或监听器生命周期。
- 传给
Shutdown()的context.Context超时太短(如time.Second),而长轮询或大文件上传尚未完成 -
http.Server的Handler里启了goroutine但没用context.WithCancel关联主请求ctx,导致Shutdown无法感知子goroutine是否就绪 - 监听器被
http.ListenAndServe()独占启动,未分离net.Listener对象——必须改用server.Serve(listener)才能在Shutdown前手动listener.Close()
示例关键片段:
srv := &http.Server{Addr: ":8080", Handler: myHandler}
l, _ := net.Listen("tcp", srv.Addr)
go func() {
if err := srv.Serve(l); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// 收到信号后:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx) // 此时l仍有效,Shutdown会通知Serve退出
systemd服务配置中容易漏掉的两个信号选项
Go进程默认不响应SIGHUP,systemd也不会自动转发。若想用systemctl reload触发平滑重启,必须显式配置KillSignal=SIGTERM和ReloadSignal=SIGHUP,否则reload只是杀掉再启动,完全不平滑。
另一个坑是RestartSec=5这类参数——若进程因Shutdown超时退出,systemd可能在旧进程还没彻底结束时就拉起新实例,造成端口冲突。必须配合StartLimitIntervalSec=0禁用启动频率限制,或确保Shutdown()超时小于RestartSec。
- 服务文件里加:
KillSignal=SIGTERM、ReloadSignal=SIGHUP、SendSIGKILL=yes(确保超时后强制终止) - Go代码中对
SIGHUP的处理必须包含子进程启动逻辑,且子进程启动失败时要记录日志并保持父进程存活,否则systemd会反复重启 - 所有监听端口必须绑定
SO_REUSEPORT(Go 1.11+默认开启),否则子进程启动时会报address already in use
真正麻烦的从来不是写几行signal.Notify,而是让每个组件——HTTP服务器、数据库连接、后台goroutine、systemd配置——在信号到达那一刻达成状态共识。少一个环节,平滑就变成假象。


















