go平滑升级必须由老进程通过sigusr2移交监听器fd给新进程,配合http.server.shutdown()等待请求结束;单靠shutdown()会中断服务,k8s中应由deployment滚动更新而非自fork。

Go 语言没有“优雅重载”这回事——所谓重载(函数名相同、参数不同)在 Go 中编译直接报错;所谓热重载(不重启进程更新代码)在 Go 中原生不支持。你真正需要的,是「平滑升级」:老进程交出监听器、新进程接棒、连接不丢、状态可控。
为什么 http.Server.Shutdown() 单独调用就中断服务
http.Server.Shutdown() 只做两件事:停止接受新连接、等待活跃请求结束。它不启动新进程,不传递 net.Listener,也不释放端口供新进程复用。如果你只调它然后 os.Exit(0),结果必然是:address already in use(新进程 bind 失败)或 502(反向代理连不上)。
- 超时 context 必须显式传入,例如
ctx, _ := context.WithTimeout(context.Background(), 45*time.Second);设太短会强制断开 WebSocket 或大文件上传 - Shutdown() 返回后,
net.Listener已关闭,但log.Writer、*sql.DB、redis.Client等资源仍持有 FD,必须手动 Close(),否则进程卡住不退出 - 它对长连接无感知:HTTP/2 流、WebSocket 连接可能还在读写,但 Shutdown() 已返回,你以为“完了”,其实连接还在跑
SIGUSR2 + ExtraFiles 是 Linux 下唯一可靠监听器传递方式
老进程收到 SIGUSR2 后,必须立刻调 listener.(*net.TCPListener).File() 获取一个可继承的 *os.File,再通过 exec.Cmd.ExtraFiles 传给子进程。不是写临时文件,不是读环境变量,不是 os.Stdin.Fd()。
- 子进程启动时,FD 编号固定为 3(因 0/1/2 是 stdin/stdout/stderr),用
os.NewFile(3, "")拿到句柄,再喂给net.FileListener() - 子进程的
http.Server.Addr必须设为空字符串"",否则server.Serve()会尝试重新net.Listen(),导致绑定随机端口 - 务必验证
fileListener.Addr().String()是否与原始监听地址一致,尤其当原始监听用":8080"(未指定 IP)时,复原后可能变成"127.0.0.1:34567" - 若监听多个端口(如 :80 和 :443),需分别调
.File(),按顺序塞进ExtraFiles切片,子进程按索引 3、4… 恢复
Kubernetes 场景下别自己 fork,preStop 发 SIGUSR2 很危险
容器内进程几乎从不等于 PID 1,kill -USR2 $PID 极大概率被 sh 或 init 拦截,子进程拿不到 FD,net.FileListener() 直接返回 invalid argument。硬编码自 fork 在 K8s 中基本不可用。
- 正确做法是:更新
Deployment.spec.template.spec.containers[].image,由 kubelet 拉起全新 Pod -
readinessProbe必须真实反映监听器就绪——比如 HTTP GET/healthz返回 200,且该 handler 只在server.Serve()开始 accept 后才成功 - 如果坚持用 preStop,应发
SIGTERM而非SIGUSR2,让老进程走Shutdown()退出,靠滚动更新自然切换 - 旧 Pod 的
terminationGracePeriodSeconds至少设为 60s,确保有足够时间处理长连接和清理资源
WebSocket 断连无法避免,但能控制在秒级恢复
gorilla/websocket.Upgrader.Upgrade() 是一次性协议切换,底层 TCP 连接绑定在老进程上,操作系统不支持跨进程迁移。新进程启动后,旧连接不会自动“续上”。
- 关键不在“不断”,而在“断得可控”:负载均衡器(如 Nginx)必须开启 draining,停止转发新请求,但允许已有连接完成生命周期
- 服务端要设
conn.SetReadDeadline()(建议为心跳间隔的 2 倍)+conn.SetPongHandler()(每次收到 Pong 就刷新读超时),双重保活 - 客户端必须实现指数退避重连:1s → 2s → 4s → 8s,上限 30s;重连前检查
WebSocket.CLOSED状态,避免并发 connect - 反向代理必须透传
Upgrade和Connection: upgrade头,Nginx 配置里漏掉proxy_set_header Upgrade $http_upgrade就会导致升级阶段直接断连
最容易被忽略的不是监听器怎么传,而是子进程是否真的接管了所有上下文:日志文件是否重新打开、DB 连接池是否重建、gRPC client 是否重连、Redis pub/sub 是否重新 subscribe——这些都不是 FD 传递能解决的,必须在新进程启动后主动初始化。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











