go平滑升级需sigusr2信号移交监听器fd、新进程用net.filelistener恢复监听,并配合http.server.shutdown()等待请求结束;k8s中应由deployment管理而非自行fork。

Go 程序本身不支持热重载,所谓“平滑升级”必须靠信号协作 + 监听器传递 + 连接等待三者配合;单靠 http.Server.Shutdown() 只能优雅关闭,不能实现零停机——新进程没拿到监听器,流量就断了。
用 SIGUSR2 触发监听器移交
Linux 下最通用的做法是用 SIGUSR2 通知老进程交出 net.Listener。这不是可选项,而是必须项:只有通过文件描述符(FD)传递,新进程才能复用同一端口,避免 address already in use 错误。
- 老进程收到
SIGUSR2后调用listener.(*net.TCPListener).File()获取 FD,并通过exec.Cmd.ExtraFiles传给子进程(不推荐写临时文件,易权限/清理问题) - 新进程启动时从
os.Stdin.Fd()或os.Getenv("LISTEN_FD")读取 FD,再用net.FileListener(fd)恢复监听器 - 务必检查
listener.Addr()是否与原地址一致,否则可能绑定到随机端口 - 若监听 Unix socket,需额外处理 socket 文件权限和残留问题
http.Server.Shutdown() 不等于平滑升级
http.Server.Shutdown() 只负责停止接收新连接并等待活跃请求结束,它不参与监听器移交,也不启动新进程。把它当“滚动更新”的全部实现,结果就是服务短暂不可用。
- 调用
Shutdown()前,必须确保新进程已成功监听并 ready,否则老进程退出后无服务可用 - 超时时间要设合理:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second),太短会强制中断长连接,太长拖慢发布节奏 -
Shutdown()返回后,仍需显式关闭日志句柄、DB 连接池、Redis client 等资源,否则进程无法真正退出
Kubernetes 中别自己 fork 子进程
在容器环境里硬编码 exec.Command(os.Args[0], os.Args[1:]...) 启动子进程,大概率失败:容器内进程通常不是 PID 1,SIGUSR2 会被 shell 拦截;且 FD 传递在容器命名空间中受限,FileListener 极易返回 invalid argument。
- 正确做法是让 Deployment 控制器管理生命周期:更新镜像字段,由 kubelet 拉起全新 Pod
- 配置
readinessProbe必须真实反映 HTTP 监听器就绪状态(比如 GET /healthz 返回 200),否则流量切早了,新 Pod 还没 bind 完成 - preStop hook 中执行
kill -USR2 $PID仅在你确认进程是 PID 1 且未被 sh 包裹时才安全;更稳的方式是直接发SIGTERM,靠Shutdown()退出,让 K8s 重建替代“热替换”
最容易被忽略的不是监听器传递,而是子进程继承的资源句柄——日志文件、数据库连接、gRPC client 的底层连接池,如果新进程没主动接管或重连,它们会沿用老进程的旧 FD,导致后续写入失败或连接泄漏。平滑升级不是“换二进制”,是“换上下文”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











