net.listener能平滑接管的核心是操作系统fd继承机制:父进程需清除监听socket的fd_cloexec标志,通过extrafiles或scm_rights传递fd,子进程用net.filelistener包装;必须先srv.shutdown再exec,严禁先close或依赖so_reuseport替代fd传递。

为什么 net.Listener 能在不中断连接的情况下被新进程接管
核心在于操作系统层面的文件描述符(fd)继承机制,而非 Go 语言本身有多神奇。当父进程用 fork 启动子进程时,若提前将监听 socket 的 fd 设置为 CLOEXEC=0(即关闭 FD_CLOEXEC 标志),该 fd 就会自动出现在子进程的 fd 表里。
Go 的 exec.Command 默认不会清除 CLOEXEC,所以必须手动用 syscall.Syscall 或第三方库(如 golang.org/x/sys/unix)调用 fcntl(fd, syscall.F_SETFD, 0) 解除限制;否则子进程 os.NewFile 会失败,报错 bad file descriptor。
- 监听 socket 必须在
exec前以SO_REUSEPORT或父子共享方式打开(常见做法是父进程先listen,再传 fd) - Go 标准库
net/http.Server.Serve不支持直接注入已有net.Listener的 fd,需用net.FileListener包装*os.File - 别依赖
os.StartProcess的Env传递 fd 编号——不同平台 fd 分配策略不同,应通过ExtraFiles字段显式传递
如何用 syscall.UnixCredentials 在 Unix domain socket 上安全传 fd
这是 Linux/FreeBSD 上跨进程传递 socket fd 的标准方式:发送方把 fd 编号写进 SCM_RIGHTS 控制消息,接收方从 recvmsg 的 ancillary data 中提取。Go 没有直接封装,得靠 golang.org/x/sys/unix 手动构造。
典型错误是忽略控制消息长度校验或缓冲区对齐——unix.Sendmsg 要求控制消息 buffer 大小至少为 unix.CmsgLen(4)(一个 int32 fd),且起始地址需按 unix.SizeofCmsghdr 对齐,否则内核返回 EINVAL。
立即学习“go语言免费学习笔记(深入)”;
- 发送端:用
unix.UnixRights(fd)生成 control bytes,传给unix.Sendmsg - 接收端:调用
unix.Recvmsg后,用unix.ParseSocketControlMessage解析,再用unix.GetsockoptInt提取 fd - 接收方拿到 fd 后必须立即用
os.NewFile封装,否则进程退出时 fd 会被内核回收
http.Server.Shutdown 和 Close 在平滑升级中到底该谁先停
必须先调用 srv.Shutdown,等它返回后再让父进程退出;绝不能先 srv.Close ——它会立刻关闭 listener 并中断所有未完成请求,违背“平滑”本意。
Shutdown 的作用是拒绝新连接、等待活跃请求结束(默认无超时),但它不碰底层 listener fd。这个 fd 正是你留给子进程接续的关键资源。如果父进程在 Shutdown 返回前就 exec,子进程可能拿到一个已被 kernel 标记为 “close-on-exec” 的 fd,导致后续 accept 失败。
-
Shutdown超时建议设为 30s 左右,太短丢请求,太长卡升级流程 - 不要依赖
context.WithTimeout简单包一层——要确保 timeout 触发后仍能安全释放 listener fd - 子进程启动成功、完成 fd 接管后,父进程才能调用
Shutdown;顺序错了就变成“先断网再换人”
为什么用 SO_REUSEPORT 无法替代 fd 传递
SO_REUSEPORT 允许多个进程绑定同一地址端口,但它是内核负载均衡行为,不是连接继承。老进程关闭后,其已建立的 TCP 连接(ESTABLISHED 状态)不会迁移,客户端会收到 RST;而平滑升级要求这些连接持续服务到自然结束。
换句话说:SO_REUSEPORT 解决的是“新连接分发”,fd 传递解决的是“旧连接延续”。两者定位完全不同。强行混用会导致连接抖动、TIME_WAIT 爆增,甚至触发某些 LB 的健康检查误判。
- 启用
SO_REUSEPORT需在net.ListenConfig中设置Control函数调用setsockopt - 即使开了
SO_REUSEPORT,父进程仍需调用Shutdown等待存量请求,否则新进程可能因连接数突增被打垮 - macOS 不完全支持
SO_REUSEPORT的进程级复用,生产环境优先走 fd 传递路径
真正难的从来不是传一个 fd,而是确保父子进程在 fd 生效窗口期、连接状态同步、信号处理时机这三者严丝合缝。漏掉任意一环,用户看到的就是 502 或连接重置。










