注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
使用 gin 作为文件下载服务器,内存占用突然从几十 M 到了 10G 以上,导致服务被 kill 重启
复现
server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| func main() { r := gin.Default() r.GET("/download", func(context *gin.Context) { f, err := os.Open("./win7.iso") log.Println(err) defer f.Close() info, _ := f.Stat() b := make([]byte, info.Size()) f.Read(b) context.Data(200, "application/octet-stream", b) }) go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() go func() { for { time.Sleep(time.Second * 30) runtime.GC() log.Println("gc") } }() runtime.MemProfileRate = 16 * 1024 r.Run(":8080") }
|
client.go
1 2 3 4 5 6
| func main() { resp, _ := http.Get("http://localhost:8080/download") resp.Body.Close() log.Println("ok") select {} }
|
使用pprof我们可以发现内存占用高达3GB, 即使我主动调用了 GC 这个内存仍未释放

追溯
通过查看代码我们可以发现请求已经结束,代码并没有其他地方对[]byte引用,一直追溯到最低层也不见其他引用。
但是结束client进程之后会有一个神奇的发现,结束 client 之后这一块内存就可以被 GC 掉
通过这个现象自然而然的就想到可能是 TCP 链接没有断开,导致这一块内存的引用并没有被释放掉
http 是一个本身是短连接,但是为了复用 TCP 连接所以有了keep-alive,但是对于下载服务来说我们其实不用复用 TCP 连接,只需要在文件下载完毕之后主动关闭这个连接即可,所以我分别在 client 加上了一个 header
再次通过pprof查看内存占用发现内存仍未得到释放
原因
通过 rfc 文档,我们可以发现规范并没有规定由谁来关闭链接,Go net/http 希望客户端关闭链接
https://tools.ietf.org/html/rfc2616#page-117
HTTP/1.1 defines the “close” connection option for the sender to signal that the connection will be closed after completion of the response.
解决
- 使用流而不是直接读内存,在
gin中不要直接使用c.Data而是使用c.DataFromReader - 使用 AWS S3 等存储服务下发文件,减轻服务压力
- 尽量不使用官方的
net/http处理文件 - @jiajun 老师 已经给 Go 官方提了一个 PR,等待 PR Merge
参考资料
- https://jiajunhuang.com/articles/2018_11_24-memory_leak_in_net_http.md.html
- https://tools.ietf.org/html/rfc2616
- https://lailin.xyz/post/notes/pprof-go%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90%E5%B7%A5%E5%85%B7/
关注我获取更新
猜你喜欢