长连接中断的根本原因是旧进程未完成请求就强制关闭 listener 或退出,需 http.Server.Shutdown()、fd 继承和信号控制三者协同。

Go 服务重启时长连接中断,本质不是“连接没保住”,而是旧进程在未完成请求前就强行关闭 listener 或退出 —— http.Server.Shutdown() 必须配对 fd 继承和信号控制,三者缺一不可。
为什么长连接在重启时会断开?
常见现象是客户端收到 connection reset by peer 或超时,监控看到大量 CLOSE_WAIT。根本原因有三个:
- 旧进程调了
listener.Close()或直接os.Exit(0):TCP 层发 RST,连接立刻失效 - 只调
srv.Shutdown(ctx)但没传给 handler 用:db.Query()、time.Sleep()等阻塞操作不响应ctx.Done(),导致 shutdown 卡满超时才强制杀 goroutine - 新进程没复用老 listener 的 fd,而是自己
http.ListenAndServe():报bind: address already in use,根本起不来
必须用 http.Server.Shutdown() 配合干净的 context
Go 1.8+ 后,srv.Close() 已废弃,它不等活跃请求;srv.Shutdown() 才是唯一正确入口。但 context 构造极易出错:
- 别写
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()——defer会在Shutdown()返回前就触发,ctx 提前取消 - handler 内所有阻塞点必须显式检查
ctx.Done():比如db.QueryContext(ctx, ...)、http.NewRequestWithContext(ctx, ...)、select { case - 超时设 30–60 秒较稳妥:太短会强杀文件上传类长请求;太长拖慢发布节奏,且可能掩盖 handler 中未响应 ctx 的 bug
新进程怎么拿到老 listener 的 socket?
关键在文件描述符(fd)传递,仅限 Unix-like 系统:
- 父进程收到
SIGUSR2后,先调ln.(*net.TCPListener).File()拿到*os.File,记下file.Fd()(如 3) - 启动子进程时,用
os.StartProcess并在sys.ProcAttr.Files中显式加入该 fd:[]uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), 3} - 子进程启动后第一件事:读环境变量(如
os.Getenv("LISTEN_FD")),用os.NewFile(3, "")恢复*os.File,再用net.FileListener(file)转成net.Listener - 验证是否成功:
lsof -i :8080应同时看到两个 PID;若只看到一个,说明 fd 传递失败或子进程没调net.FileListener
最容易被忽略的时序陷阱
新进程拿到 listener 后不能立刻 Accept(),旧进程也不能一收到信号就 shutdown —— 二者存在竞态:
- 父进程必须在 fork 子进程 *之后*、发送信号 *之前*,调用
listener.SetDeadline(time.Now().Add(-1 * time.Second))或类似手段,让Accept()快速返回并跳出循环,否则仍会抢新连接 - 子进程必须确认
net.FileListener()成功且能Accept()后,再启动 HTTP server;否则可能因 socket 处于 TIME_WAIT 或父进程尚未释放而失败 - 旧进程
Shutdown()返回后才真正退出,此时所有活跃请求已自然结束或超时被 cancel —— 这个“等待完成”的窗口期,才是长连接得以保全的唯一时间窗口


















