以下服务器代码:
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
这样做会在后台分配更小(更少)的缓冲区