Go HTTP服务平滑重启需旧进程调用Shutdown等待连接处理完、新进程复用监听fd接管端口;必须用显式http.Server实例并设超时,配合systemd Type=forking与信号监听,杜绝goroutine泄漏。

Go HTTP 服务如何不丢请求地重启
平滑重启的本质不是“不中断”,而是“旧连接继续处理完,新连接交给新进程”。http.Server 本身不支持热替换,必须靠外部信号(如 SIGHUP)触发老进程优雅关闭、新进程启动并接管端口。
常见错误是直接 os.Exit(0) 或 kill -9,导致正在读 body、写 response 的连接被硬断开,前端收到 connection reset 或超时;另一种是新进程启动后立刻监听同一端口,但老进程还没关,报错 address already in use。
- 用
net.Listener复用文件描述符:通过syscall.Unshare或第三方库(如facebookgo/grace)传递监听套接字给子进程,避免端口争抢 - 老进程收到
SIGUSR2(非SIGHUP,后者常被 systemd 拦截)后,调用srv.Shutdown(),并设srv.ReadTimeout和srv.WriteTimeout防止长连接卡死 - 新进程启动前,检查环境变量(如
LISTEN_FD)是否含已继承的 listener fd,有则直接net.FileListener复用,无则正常net.Listen
ListenAndServe 为什么不能直接用于生产
http.ListenAndServe 是便利封装,底层调用 http.Server.ListenAndServe,但它没暴露 Server 实例,无法控制超时、TLS 配置、连接生命周期,更没法调用 Shutdown —— 平滑重启第一步就卡死。
典型表现:改了代码重新 go run main.go,旧请求突然 502;或加了 log.Println("shutting down") 却发现日志根本没打出来,因为进程已被强杀。
立即学习“go语言免费学习笔记(深入)”;
- 永远用显式
http.Server实例:定义srv := &http.Server{Addr: ":8080", Handler: mux} - 必须设
ReadTimeout、WriteTimeout、IdleTimeout,否则Shutdown可能无限等待空闲连接 - 别依赖
http.ListenAndServeTLS的快捷方式,它同样不返回Server,要用srv.ListenAndServeTLS
如何让 systemd 管理 Go 服务并支持平滑重启
systemd 默认用 kill -TERM 停止服务,而 Go 程序若没注册 os.Signal 监听 os.Interrupt 或 syscall.SIGTERM,就会直接退出,跳过 Shutdown 流程。
另一个坑是 Type=simple 模式下,systemd 认为进程 fork 后就启动完成,但平滑重启需要父子进程协作,必须用 Type=forking 并配 PIDFile,否则 systemctl reload 会误判服务已死。
- 在 main 中注册信号:
signal.Notify(sigChan, syscall.SIGUSR2, syscall.SIGTERM),收到SIGUSR2触发重启,SIGTERM触发优雅退出 - service 文件里写
Type=forking,启动后由子进程写PIDFile,父进程退出;同时加Restart=on-failure防止子进程崩溃后没人拉起 - 禁用
StartLimitIntervalSec或调大,否则频繁重启会被 systemd 拉黑
goroutine 泄漏会让 Shutdown 卡住
srv.Shutdown() 会等所有活跃连接关闭,但如果业务代码启了没回收的 goroutine(比如忘了 defer rows.Close()、HTTP client 没设 Timeout、或用了全局 channel 但没 close),这些 goroutine 可能持续持有连接或阻塞在 I/O,导致 Shutdown 永远不返回。
现象是:发 SIGUSR2 后,lsof -i :8080 显示端口还在监听,ps aux | grep yourapp 看到两个进程,但新进程不处理请求,老进程也不退出。
- 所有 HTTP handler 内部启动的 goroutine,必须绑定 context 并在
ctx.Done()时退出 - 数据库连接池、HTTP client、Redis client 等,统一用带 timeout 的配置,避免底层连接 hang 住
- 用
runtime.NumGoroutine()在健康检查接口中暴露当前 goroutine 数,突增就是泄漏信号
平滑重启真正难的不是信号怎么发、端口怎么传,而是整个请求链路上每个环节都得配合超时和取消 —— 少一个 context.WithTimeout,就可能让一次重启拖十几秒。










