多部分表单上传+golang中的内存泄漏?
Multipart form uploads + memory leaks in 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 似乎在 darwin/amd64 和 linux/amd64.
上的 Go 1.4.2 中存在某种形式的内存泄漏
当我连接 pprof
时,我看到 bytes.makeSlice
在调用该服务几次后使用了 96MB 的内存(最终在我上面的代码中被 r.FormFile
调用)。
如果我继续调用 curl
,进程的内存使用量会随着时间的推移而变慢,最终似乎会在我的机器上停留在 300MB 左右。
想法?我认为这不是预期的/我做错了什么?
如果内存使用停滞在 "maximum",我不会真正称之为内存泄漏。我宁愿说 GC 不急于和懒惰。或者只是不想在经常重新分配/需要的情况下物理释放内存。如果真的是内存泄漏,使用的内存不会止于 300 MB。
r.FormFile("file")
将导致调用 Request.ParseMultipartForm()
, and 32 MB will be used as the value of maxMemory
parameter (the value of defaultMaxMemory
variable defined in request.go
). Since you upload a larger file (80 MB), a buffer of size 32 MB at least will be created - eventually (this is implemented in multipart.Reader.ReadFrom()
). Since bytes.Buffer
用于读取内容,读取过程将从一个小的或空的缓冲区开始,并在需要更大的缓冲区时重新分配。
缓冲区重新分配的策略和缓冲区大小取决于实现(并且还取决于来自请求的 read/decoded 块的大小),但只是粗略地想象一下,像这样:0 字节、4 KB、16 KB、64 KB、256 KB、1 MB、4 MB、16 MB、64 MB。同样,这只是理论上的,但说明总和甚至可以增长超过 100 MB,只是为了读取内存中文件的前 32 MB,此时将决定它将在文件中 moved/stored。有关详细信息,请参阅 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
这样做会在后台分配更小(和更少)的缓冲区。
以下服务器代码:
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 似乎在 darwin/amd64 和 linux/amd64.
当我连接 pprof
时,我看到 bytes.makeSlice
在调用该服务几次后使用了 96MB 的内存(最终在我上面的代码中被 r.FormFile
调用)。
如果我继续调用 curl
,进程的内存使用量会随着时间的推移而变慢,最终似乎会在我的机器上停留在 300MB 左右。
想法?我认为这不是预期的/我做错了什么?
如果内存使用停滞在 "maximum",我不会真正称之为内存泄漏。我宁愿说 GC 不急于和懒惰。或者只是不想在经常重新分配/需要的情况下物理释放内存。如果真的是内存泄漏,使用的内存不会止于 300 MB。
r.FormFile("file")
将导致调用 Request.ParseMultipartForm()
, and 32 MB will be used as the value of maxMemory
parameter (the value of defaultMaxMemory
variable defined in request.go
). Since you upload a larger file (80 MB), a buffer of size 32 MB at least will be created - eventually (this is implemented in multipart.Reader.ReadFrom()
). Since bytes.Buffer
用于读取内容,读取过程将从一个小的或空的缓冲区开始,并在需要更大的缓冲区时重新分配。
缓冲区重新分配的策略和缓冲区大小取决于实现(并且还取决于来自请求的 read/decoded 块的大小),但只是粗略地想象一下,像这样:0 字节、4 KB、16 KB、64 KB、256 KB、1 MB、4 MB、16 MB、64 MB。同样,这只是理论上的,但说明总和甚至可以增长超过 100 MB,只是为了读取内存中文件的前 32 MB,此时将决定它将在文件中 moved/stored。有关详细信息,请参阅 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
这样做会在后台分配更小(和更少)的缓冲区。