go服务长连接不中断的关键是旧进程用shutdown()处理完请求、新进程通过fd继承复用监听socket、sigusr2触发切换三者协同;直接kill或os.exit()会因未调shutdown、goroutine未响应ctx、端口被占导致rst中断、5xx暴增和close_wait堆积。

Go 服务重启时长连接不中断,关键不是“不让旧连接断”,而是让旧进程继续服务完它们、新进程用同一监听 socket 接管新连接——靠 http.Server.Shutdown() + 文件描述符继承 + SIGUSR2 协同,三者缺一不可。
为什么直接 kill 或 os.Exit() 会断长连接
现象包括:curl 突然报 connection reset by peer、监控里 5xx 暴增、lsof -i :8080 看到大量 CLOSE_WAIT。根本原因有三个:
- 没调
srv.Shutdown(ctx),只调了srv.Close()或什么都没调就退出 ——Close()是立即关闭 listener,不等正在处理的请求 - goroutine 没响应
ctx.Done(),比如用了time.Sleep(10 * time.Second)却没 select 监听上下文,导致 Shutdown 等超时后强制杀掉 - 新进程启动时直接
http.ListenAndServe(":8080", h),报bind: address already in use—— 因为旧进程的 listener 还占着端口,又没把 fd 传过去
如何正确传递监听 socket 的文件描述符
Linux 下 socket 是文件,复用它的本质是让子进程继承父进程打开的 fd。Go 标准库支持,但步骤不能错:
- 父进程收到
SIGUSR2后,先调ln.(*net.TCPListener).File()拿到*os.File,记下它的file.Fd()(比如是3) - 启动子进程时,用
os.StartProcess(不用exec.Command),在sys.ProcAttr.Files里显式填入[]uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), 3}—— 缺少3,子进程就拿不到 listener - 子进程启动后第一件事:读环境变量(如
os.Getenv("LISTEN_FD")),用os.NewFile(3, "")恢复*os.File,再调net.FileListener(file)得到可监听的net.Listener
验证是否成功:lsof -i :8080 应同时看到两个 PID 在监听同一端口;若只看到一个,说明 fd 传递失败或子进程没走对恢复流程。
Shutdown() 的 context 怎么设才不卡住
超时时间不是越长越好,context 本身也不能带 cancel:
- 别写
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()——defer cancel()会在Shutdown()返回前就触发,导致 ctx 提前取消 - handler 内部必须用传入的 ctx 控制所有阻塞操作:比如
db.QueryContext(ctx, ...)、select { case - 推荐超时 30–60 秒:太短会强杀长请求(如文件上传),太长拖慢发布节奏;HTTP server 建议配
ReadTimeout/WriteTimeout防慢连接拖死 shutdown
真正容易被忽略的是:新进程拿到 listener 后不能立刻 Accept(),得等父进程完成 Shutdown() 并释放 accept 队列;否则可能因 TIME_WAIT 或内核状态未切换,导致新进程 accept 失败或连接被丢弃。











