跳转到内容

RSS 持续增长和疑似内存泄漏

复制本页

top 里的常驻集大小(resident set size,RSS)一直上涨,并不自动等于内存泄漏。先判断增长发生在 Go 对象、未关闭的 ImageRef、libvips 缓存,还是进程使用的原生内存;这四种情况需要不同的处理方式。

  • 请求结束后,vips.OpenImageRefs() 的计数仍持续上升。
  • Go 堆趋于稳定,但进程 RSS 在相同负载下仍不断抬高。
  • libvips 缓存达到设定上限后,RSS 仍没有进入稳定区间。
  • 服务最终被内存限制或 OOM killer 终止。
观测结果 更可能说明什么 下一步
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 猜测原因。

每个成功创建的 ImageRef 都应由明确的代码路径负责 Close()。不仅要检查正常返回,也要检查解码失败、参数校验失败、导出失败和请求取消等提前返回路径。不要依赖 finalizer 代替显式释放。

测试代码可使用 defer vips.AssertNoLeaks(t) 检查用例结束时是否仍有未关闭引用。这个辅助函数适合发现回归,不适合替代生产监控。

vips.Startup(...) 应在进程初始化时调用一次,并检查返回错误;vips.Shutdown() 应在不再有处理任务时调用。不要在每条请求中反复启动和关闭 libvips。

govips v2.19.0 通过 vips.Config 暴露 MaxCacheSize、MaxCacheMem 和 MaxCacheFiles。先为测试设置明确上限,再在同一负载下观察 RSS 是否进入稳定区间。vips.PrintCache() 可辅助查看缓存,vips.ClearCache() 只适合诊断;频繁清空缓存会改变正常性能特征。

Go heap profile 只覆盖 Go 管理的对象,RSS 还包含 libvips、解码器、线程栈、映射文件和原生分配器保留的页面。若引用数、Go 堆和 libvips 缓存都已稳定,但 RSS 仍在同一负载下无界增长,才有足够依据继续调查原生分配器或 C 层泄漏。

  • 本页针对 govips v2.19.0,并以仓库当前的 libvips 8.17.3 基线复核。
  • OpenImageRefs() 与 AssertNoLeaks(t) 从 govips v2.18.0 起可用;更早版本不能直接照搬这一步。
  • sharp、pyvips 与 vips CLI 的对象生命周期和诊断接口不同,不能把 govips 的引用计数方法原样套用。