目录

Go 图片压缩为什么吃掉 200MB 内存?一次从标准库到 bimg 的优化实录

最近在用 Go + Gin 写一个服务端图片压缩接口,功能跑通了,结果一上压测就傻眼:一张 20MB 的图片,处理过程内存峰值居然冲到 200MB 以上。

几个并发直接把容器干 OOM 了。

这篇就记录下我怎么一步步把它从 200MB 砍到 40MB 以内的。

一开始的写法,错在哪?

最初的接口设计图省事,前端把图片转成 base64 塞进 JSON body,后端解析 base64 还原成文件,再用标准库 image/jpeg 压缩,最后返回下载链接。

代码大概长这样:

func compress(c *gin.Context) {
    var req struct {
        Image string `json:"image"` // 整张图的 base64
    }
    c.BindJSON(&req)

    // 坑 1:整段 base64 一次性解码进内存
    raw _ := base64.StdEncoding.DecodeString(req.Image)

    // 坑 2:标准库解码会把整张图解成未压缩位图
    img _ _ := image.Decode(bytes.NewReader(raw))

    out _ := os.Create("out.jpg")
    defer out.Close()
    jpeg.Encode(out img &jpeg.Options{Quality: 80})

    c.JSON(200 gin.H{"url": "/out.jpg"})
}

看着没毛病,对吧?我一开始也这么觉得。

直到 pprof 把真相摆我面前。

用 pprof 把内存吃在哪看清楚

光猜没用,得测。我在服务里挂了 net/http/pprof,再配一个进程级的内存采样,单请求跑一张 20MB 的图,盯着 heap profile 看。

import _ "net/http/pprof"

func main() {
    go http.ListenAndServe("localhost:6060" nil) // pprof 端口
    // ... gin 路由
}

抓一次堆快照:

go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap

结果很直观,内存几乎全卡在两个地方。

第一处是 base64 解码。

JSON 里的 base64 字符串本身就比原图大约 33%,一张 20MB 的图,body 文本就有 27MB 左右。

DecodeString 又复制出一份 20MB 的字节切片。

光这一步,两份数据躺在内存里。

第二处更狠,是 image.Decode

标准库解码 JPEG 时,会把压缩过的图还原成 RGBA 未压缩位图。

一张 4000×3000 的图,位图大小是 4000 × 3000 × 4 = 48MB,跟它本身压没压缩半毛钱关系。

再加上编码时的中间缓冲,几个 40MB 叠一块儿,200MB 就是这么来的。

说白了,问题压根不是压缩算法慢,是数据在内存里被反复复制 + 解成了巨大的位图。

解决思路:别让整张图躺内存里

定位清楚后,优化方向就两条。

第一,干掉 base64,改用 multipart 文件流。

文件上传走 multipart/form-data,Gin 能直接拿到一个流,边读边写到临时文件,内存里永远只留一小段缓冲。

func upload(c *gin.Context) {
    file _ := c.FormFile("image")
    // SaveUploadedFile 内部是 io.Copy,按 32KB 缓冲流式落盘
    dst := filepath.Join(os.TempDir() file.Filename)
    c.SaveUploadedFile(file dst)
    // 后续对 dst 这个临时文件做压缩
}

这一步下来,27MB 的 base64 文本和那份解码副本就全没了。

如果你确实是需要传输 base64,也可以用流式解析的方式,具体可以让 AI 帮你实现,流式解析可以极大降低内存占用。

第二,换掉标准库,用 bimg。

标准库那套"解码成位图再编码"的路子,内存天然下不来。

我对比了几个库,最后选了 bimg,它底层是 C 写的 libvips

libvips 用的是流式 + 分块处理,不会把整张图一次性解成位图,所以内存占用低得离谱。

import "github.com/h2non/bimg"

func compressFile(path string) error {
    buf err := bimg.Read(path) // 读临时文件
    if err != nil {
        return err
    }
    newImg err := bimg.NewImage(buf).Process(bimg.Options{
        Quality: 80
        Type:    bimg.JPEG
    })
    if err != nil {
        return err
    }
    return bimg.Write("out.jpg" newImg)
}

改完再压测,同样那张 20MB 的图,单请求内存峰值掉到 40MB 以内,差不多是原来的五分之一。并发跑起来容器也稳了。

数据对比

方案 20MB 图片单请求内存峰值 关键问题
base64 + image/jpeg 标准库 200MB+ 整体解码 + 位图膨胀
multipart 流式 + 标准库 120MB 左右 位图膨胀还在
multipart 流式 + bimg ≤ 40MB 基本没有大块复制

两步优化里,换 bimg 是主力,流式上传是辅助。

两个一起上效果最好。

bimg 怎么装?这步劝退了不少人

bimg 不是纯 Go 库,它依赖 libvips,所以必须开 CGO,还得在机器上装好 vips。

macOS 上直接:

brew install vips

Ubuntu/Debian:

apt-get install -y libvips-dev

linux:

dnf install -y libvips-dev

这里简化了 vips 的安装,实际安装还挺麻烦,请自行找安装教程或咨询 AI。

编译时记得开 CGO(默认就是开的,但 Docker 多阶段构建里容易忘):

CGO_ENABLED=1 go build .

我第一次在 Alpine 镜像里构建踩了个坑:Alpine 用的是 musl libc,装 vips 还得 apk add vips-dev,而且最终运行镜像里也要带上 vips 运行时库,不然启动直接报 error while loading shared libraries: libvips.so

我是在宿主机编译,复制到容器内执行才会出现,建议直接用 docker 进行编译

折腾了快一个小时才整明白。

如果你用 Docker,建议直接找带 vips 的基础镜像省事。

常见问题

bimg 一定比标准库快很多吗?

不一定快"很多",但内存优势是碾压级的。

bimg 底层 libvips 处理大图时内存占用通常只有 ImageMagick 或标准库的几分之一。

速度上,小图差距不明显,图越大、批量越多,libvips 的流式优势越突出。

不想引入 CGO 和 vips,有纯 Go 方案吗?

有,但要权衡。

纯 Go 的库比如 disintegration/imagingnfnt/resize 部署简单,可内存模型跟标准库一样,大图照样会解成位图。

如果你的图普遍不大(几百 KB 以内),纯 Go 够用;一旦要处理几十 MB 的大图,bimg 这类基于 libvips 的方案更稳。

base64 上传就一定不行吗?

也不是绝对不行。

如果图都很小(比如头像、缩略图,几十 KB),base64 那点开销无所谓,前端集成还方便。后端解析采用流式解析一样可以减少内存占用。

临时文件会不会堆满磁盘?

会,所以一定要清。

压缩完该删的删,可以用 defer os.Remove(tmpPath),或者跑个定时任务清理 os.TempDir() 下的过期文件。

我线上是处理完立刻删源文件,压缩结果走对象存储,本地不留。

写在最后

整件事的核心其实就一句话:别让一张完整的大图以"未压缩"的形态在内存里待着。

base64 整体解码和标准库的位图膨胀,是两个最容易踩的坑。

换成流式上传加 libvips,内存问题基本就解决了。

如果你也在做服务端图片处理,或者对 bimg、libvips 的配置有疑问,欢迎在评论区交流~~~

版权声明

未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!

本文原文链接: https://fiveyoboy.com/articles/go-image-compression-memory-optimization/

备用原文链接: https://blog.fiveyoboy.com/articles/go-image-compression-memory-optimization/