RSS 持续增长和疑似内存泄漏
top 里的常驻集大小(resident set size,RSS)一直上涨,并不自动等于内存泄漏。先判断增长发生在 Go 对象、未关闭的 ImageRef、libvips 缓存,还是进程使用的原生内存;这四种情况需要不同的处理方式。
- 请求结束后,
vips.OpenImageRefs()的计数仍持续上升。 - Go 堆趋于稳定,但进程 RSS 在相同负载下仍不断抬高。
- libvips 缓存达到设定上限后,RSS 仍没有进入稳定区间。
- 服务最终被内存限制或 OOM killer 终止。
先看哪组指标
Section titled “先看哪组指标”| 观测结果 | 更可能说明什么 | 下一步 |
|---|---|---|
OpenImageRefs() 持续增加 |
某条成功或错误路径没有调用 Close() |
审计每个创建 ImageRef 的分支;在测试中加入 AssertNoLeaks(t) |
OpenImageRefs() 回到基线,Go 堆增加 |
Go 对象仍被引用,或垃圾回收尚未运行 | 使用 Go 的 heap profile 定位保留对象 |
| 引用数与 Go 堆稳定,缓存仍在增长 | libvips 操作缓存尚未达到上限 | 固定 vips.Config 的缓存上限后重新测量 |
| 上述三项稳定,RSS 仍持续增长 | 原生分配器碎片、解码器内存或 C 层泄漏仍待区分 | 在可重复负载下采集原生内存证据,再缩小到最小复现 |
OpenImageRefs() 只统计 govips 当前追踪的未关闭引用。它能发现常见的生命周期错误,但不能单独证明进程没有其他原生内存问题。
先把线上处理链缩成“加载一张图并立即关闭”。下面的程序在 govips v2.19.0 下记录启动后的基线,并在循环结束后检查引用数是否回到该基线:
package main
import ( "fmt" "log" "os"
"github.com/davidbyttow/govips/v2/vips")
func main() { if len(os.Args) != 2 { log.Fatalf("usage: %s INPUT", os.Args[0]) } if err := vips.Startup(nil); err != nil { log.Fatal(err) } defer vips.Shutdown()
baseline := vips.OpenImageRefs() for i := 0; i < 1000; i++ { image, err := vips.NewImageFromFile(os.Args[1]) if err != nil { log.Fatalf("iteration %d: %v", i, err) } image.Close() }
open := vips.OpenImageRefs() fmt.Printf("open ImageRef instances: baseline=%d after=%d\n", baseline, open) if open != baseline { log.Fatalf("ImageRef count did not return to baseline") }}如果这个程序稳定,而完整处理链的计数上涨,就逐段加回读取、变换和导出步骤。这样可以定位第一个没有回到基线的阶段,而不是靠重启或手动触发 GC 猜测原因。
1. 先修正 ImageRef 的所有权
Section titled “1. 先修正 ImageRef 的所有权”每个成功创建的 ImageRef 都应由明确的代码路径负责 Close()。不仅要检查正常返回,也要检查解码失败、参数校验失败、导出失败和请求取消等提前返回路径。不要依赖 finalizer 代替显式释放。
测试代码可使用 defer vips.AssertNoLeaks(t) 检查用例结束时是否仍有未关闭引用。这个辅助函数适合发现回归,不适合替代生产监控。
2. 确认进程级生命周期
Section titled “2. 确认进程级生命周期”vips.Startup(...) 应在进程初始化时调用一次,并检查返回错误;vips.Shutdown() 应在不再有处理任务时调用。不要在每条请求中反复启动和关闭 libvips。
3. 固定缓存边界后再比较 RSS
Section titled “3. 固定缓存边界后再比较 RSS”govips v2.19.0 通过 vips.Config 暴露 MaxCacheSize、MaxCacheMem 和 MaxCacheFiles。先为测试设置明确上限,再在同一负载下观察 RSS 是否进入稳定区间。vips.PrintCache() 可辅助查看缓存,vips.ClearCache() 只适合诊断;频繁清空缓存会改变正常性能特征。
4. 分开记录 Go 堆与进程 RSS
Section titled “4. 分开记录 Go 堆与进程 RSS”Go heap profile 只覆盖 Go 管理的对象,RSS 还包含 libvips、解码器、线程栈、映射文件和原生分配器保留的页面。若引用数、Go 堆和 libvips 缓存都已稳定,但 RSS 仍在同一负载下无界增长,才有足够依据继续调查原生分配器或 C 层泄漏。
受影响版本与边界
Section titled “受影响版本与边界”- 本页针对 govips v2.19.0,并以仓库当前的 libvips 8.17.3 基线复核。
OpenImageRefs()与AssertNoLeaks(t)从 govips v2.18.0 起可用;更早版本不能直接照搬这一步。- sharp、pyvips 与 vips CLI 的对象生命周期和诊断接口不同,不能把 govips 的引用计数方法原样套用。