Go net.Pipe 关闭后堆仍增长:先清除两端 deadline
Go 1.27.3 的 net.Pipe.Close 未停止 deadline 定时器,远期期限可让已关闭 pipe 状态继续保留。仅在拥有两端且结束读写的收尾路径,先清零两端期限再关闭,并按 Go 堆核验。
大量短生命周期的 net.Pipe 已经两端关闭,GC 后 Go 堆仍随创建数量增长;每条连接之前设置过很远的 deadline。Go 1.27.3 下,要检查关闭时是否还留着 deadline 定时器,而不只数活跃 goroutine。
这条表现涉及内存中的 net.Pipe,不是 TCP 连接池或操作系统文件描述符泄漏。保留状态直到期限到达,是有时间边界的滞留;不能只凭进程 RSS 没下降就认定符合本题。
Close 没有同时停止两个方向的定时器
1.27.3 的 pipe 为读、写分别保存 deadline。未来期限通过 time.AfterFunc 创建回调,回调持有对应的 deadline 状态;Close() 则只关闭本端完成通道,没有清除这些 timer。即使应用已不再引用连接,未来回调仍可能使相关状态保留到到期。
把期限设为零,会进入停止并移除现有 timer 的分支。不过 SetDeadline 会先检查本端和对端是否已关闭;任一端已经关闭时,它返回 io.ErrClosedPipe,不会再帮你清理。这决定了临时处理的顺序。
先保留 Go 版本、创建及关闭连接的位置、deadline 长度与堆分析。确认增长对象来自这条 pipe 路径,再比较相同生命周期下有没有设置远期 deadline。单独强制 GC 或降低 GOGC,无法取消仍由定时器引用的状态。
在任一端关闭前清除两端 deadline
如果应用拥有这对 pipe 的完整生命周期,可以在终止读写、确认没有其他 goroutine 继续设置期限后,先对两端清零,再关闭两端。下面是清理位置的片段,a、b 必须是同一次 net.Pipe() 返回且归当前组件所有的两端:
errA := a.SetDeadline(time.Time{})
errB := b.SetDeadline(time.Time{})
closeA := a.Close()
closeB := b.Close()
err := errors.Join(errA, errB, closeA, closeB)
片段使用 time、errors,最后的 err 要交回原有错误处理流程。两次 SetDeadline 必须都在第一次 Close 前;把清零操作写进每端各自的关闭函数,调用第一端后再处理第二端,仍可能让第二次清零被拒绝。
这不是让所有网络连接都在业务读写期间取消超时。只在本组件负责的 pipe 结束阶段执行,保留原来的取消、退出和等待机制;有并发读写时,先确保清零不会让尚未结束的操作失去超时保护。也不能替调用方关闭共享连接或截断另一组件仍在使用的数据。
已经关闭的 pipe 不能靠事后 SetDeadline 恢复这条清理路径。应修正后续创建对象的收尾顺序,保留当前诊断材料,不要用全局清理其他连接来追求一次内存下降。
用同一批连接生命周期比较堆存活
在项目隔离环境,用相同数量、相同 deadline 和相同工作负载比较清理前后,观察 GC 后的 Go 堆及对象,而不是直接把它当 RSS 对照。再覆盖读写取消、期限已到以及提前关闭的分支,确认原本需要的错误仍能传出,等待中的操作能够结束。
后续升级 Go 时,核对实际版本是否已在 Close 路径处理定时器,再评估移除临时顺序要求。不能把开发中的修改当作正在运行的 1.27.3 已有的行为。


