多部分表单上传+ golang内存泄漏



以下服务器代码:

package main
import (
  "fmt"
  "net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
  file, _, err := r.FormFile("file")
  if err != nil {
    fmt.Fprintln(w, err)
    return
  }
  defer file.Close()
  return
}
func main() {
  http.ListenAndServe(":8081", http.HandlerFunc(handler))
}

被运行,然后调用它:

curl -i -F "file=@./large-file" --form hello=world http://localhost:8081/

large-file大约80MB的地方,在达尔文/amd64和linux/amd64的Go 1.4.2中似乎有某种形式的内存泄漏。

当我连接pprof时,我看到bytes.makeSlice在调用服务几次后使用了96MB的内存(最终在上面的代码中由r.FormFile调用)。

如果我一直调用curl,进程的内存使用随着时间的推移而变慢,最终似乎在我的机器上停留在300MB左右。

想法吗?我想这是意料之外的/我做错了什么?

如果内存使用停滞在"最大值",我真的不会称之为内存泄漏。我宁愿说GC不是很渴望,而是很懒惰。或者只是不想物理释放内存,如果它经常被重新分配/需要。如果这真的是一个内存泄漏,使用的内存不会停止在300 MB。

r.FormFile("file")将导致对Request.ParseMultipartForm()的调用,并且将使用32mb作为maxMemory参数的值(request.go中定义的defaultMaxMemory变量的值)。由于您上传的文件较大(80 MB),因此最终将创建一个大小至少为32 MB的缓冲区(这在multipart.Reader.ReadFrom()中实现)。由于bytes.Buffer用于读取内容,因此读取过程将从一个小缓冲区或空缓冲区开始,并在需要更大缓冲区时重新分配。

缓冲区重新分配的策略和缓冲区大小取决于实现(也取决于从请求中读取/解码的块的大小),但只是有一个粗略的图片,想象一下这样:0字节,4 KB, 16 KB, 64 KB, 256 KB, 1 MB, 4 MB, 16 MB, 64 MB。同样,这只是理论上的,但说明了总和甚至可以超过100 MB,只是为了读取内存中文件的前32 MB,此时将决定将其移动/存储在文件中。详细信息请参见multipart.Reader.ReadFrom()的实现。这合理地解释了96 MB的分配。

这样做几次,如果GC不立即释放分配的缓冲区,您可以很容易地最终使用300 MB。如果有足够的空闲内存,GC就没有压力来匆忙释放内存。你看到它变大的原因是因为后台使用了大缓冲区。如果您在上传1MB的文件时也这样做,您可能不会遇到这种情况。

如果它对你很重要,你也可以用一个较小的maxMemory值手动调用Request.ParseMultipartForm(),例如

r.ParseMultipartForm(2 << 20) // 2 MB
file, _, err := r.FormFile("file")
// ... rest of your handler

这样做会在后台分配更小(更少)的缓冲区

最新更新