Ngrok内存泄露源码分析与解决方案

背景

由于本人在做Bullet项目时,底层采用了golang语言开发的Ngrok作为内网穿透的核心。使用 了很长 时间也非常熟悉他的穿透原理。有一次在映射群辉系统做 外网数据下载的时候,出现了卡死的现象,整个Ngrok服务不可用的情况。当时查资料就发现Ngrok1.X的版本虽然是开源版本存在内存泄露的大BUG,平时偶尔使用是不会发现的,一旦下载 大文件(这里测试的4G系统iso)就会出现内存客户端进程内存占用非常大的情况。

这里我正在下载的一个超级大的文件,图下图所示:
image

下载一段时间后,client的内存就爆了
image
但是服务器端的内存非常稳定。

比较恶心 的解决方案

这个没有从源头解决问题的方案,万能的重启。。。

https://www.jianshu.com/p/2d2628aeb4ed

ngrok监控与源码分析

这里我使用了http的一款监控组件:


import _ "net/http/pprof" func main() { go func() { http.ListenAndServe("localhost:6060", nil) }() }

在ngrok源码中添加上面的代码在任意位置,注意去掉main函数

go tool pprof http://localhost:6060/debug/pprof/heap

执行后输入top命令 回车,输出占用内存函数排行情况

marker@markerdeMacBook-Pro / % go tool pprof -alloc_space http://127.0.0.1:8080/debug/pprof/heap
Fetching profile over HTTP from http://127.0.0.1:8080/debug/pprof/heap
Saved profile in /Users/marker/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.030.pb.gz
Type: alloc_space
Time: Dec 26, 2020 at 5:42pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 4GB, 99.86% of 4.01GB total
Dropped 41 nodes (cum <= 0.02GB)
      flat  flat%   sum%        cum   cum%
       4GB 99.86% 99.86%        4GB 99.86%  bytes.makeSlice
         0     0% 99.86%        4GB 99.85%  bytes.(*Buffer).ReadFrom
         0     0% 99.86%        4GB 99.86%  bytes.(*Buffer).grow
         0     0% 99.86%        4GB 99.85%  ngrok/proto.(*Http).readResponses
         0     0% 99.86%        4GB 99.84%  ngrok/proto.extractBody
(pprof) 

这里注意到ngrok/proto.extractBody 函数调用站中makeSlice切片出现占有4G的情况。

找到ngrok/proto/http.go源代码的 readResponses 函数:

func (h *Http) readResponses(tee *conn.Tee, lastTxn chan *HttpTxn) {
    ...... 
    // make sure we read the body of the response so that
    // we don't block the reader
    _, _ = httputil.DumpResponse(resp, true)

    txn.Resp = &HttpResponse{Response: resp}
    // apparently, Body can be nil in some cases
    if resp.Body != nil {
        txn.Resp.BodyBytes, txn.Resp.Body, err = extractBody(resp.Body)
        if err != nil {
            tee.Warn("Failed to extract response body: %v", err)
        }
    } 
    ..... 
}

这段代码做了一件事就是 抓取了响应的body数据。由于我测试的下载文件,所以body的数据很大很大。
经过分析大概代码意思就是抓取这些数据是为了在ngrok客户端通过的UI中通过websocket实时展示,但这个功能在Bullet项目中没有使用。

通过一系列的测试,果然屏蔽读取body并关闭ReadCloser能解决内存占用问题。为啥要关闭?看看下面这篇

https://blog.csdn.net/jeffrey11223/article/details/80456693

具体代码改动,在http.go中找到extractBody函数:

func extractBody(r io.ReadCloser) ([]byte, io.ReadCloser, error) { 
    buf := new(bytes.Buffer) 
    // 万恶的根源在这里
    // buf.ReadFrom(r)
    defer r.Close() // 这个是重点
    // 注释 原来的返回值
    // return buf.Bytes(), ioutil.NopCloser(buf), err
    return buf.Bytes(), nil, nil
}

完美修复结果

image

这里我要说的

商业的内网穿透工具netapp,他们很早就修复了这个bug,但是没有公开细节。。。

https://natapp.cn/news/10

来源: 雨林博客(www.yl-blog.com)