我的golang程序(URL监视器)发生内存泄漏,最终被内核(oom)杀死。
环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ go version
go version go1.0.3

$ go env
GOARCH="amd64"
GOBIN=""
GOCHAR="6"
GOEXE=""
GOGCCFLAGS="-g -O2 -fPIC -m64 -pthread"
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/data/apps/go"
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
CGO_ENABLED="1"

码:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
package main

import (
   "bytes"
   "database/sql"
   "flag"
   "fmt"
    _"github.com/Go-SQL-Driver/MySQL"
   "ijinshan.com/cfg"
   "log"
   "net"
   "net/http"
   "net/smtp"
   "os"
   "strconv"
   "strings"
   "sync"
   "time"
)

var (
    Log           *log.Logger
    Conf          cfg.KVConfig
    Debug         bool
    CpuCore       int
    HttpTransport = &http.Transport{
        Dial: func(netw, addr string) (net.Conn, error) {
            deadline := time.Now().Add(30 * time.Second)
            c, err := net.DialTimeout(netw, addr, 20*time.Second)
            if err != nil {
                return nil, err
            }

            c.SetDeadline(deadline)
            return c, nil
        },
        DisableKeepAlives: true,
    }
    HttpClient = &http.Client{
        Transport: HttpTransport,
    }
    WG            sync.WaitGroup
)

const (
    LogFileFlag   = os.O_WRONLY | os.O_CREATE | os.O_APPEND
    LogFileMode   = 0644
    LogFlag       = log.LstdFlags | log.Lshortfile
    GET_VIDEO_SQL = `SELECT B.Name, A.TSID, A.Chapter, A.ChapterNum,
    IFNULL(A.Website, ''), IFNULL(A.Descr, ''),
    IFNULL(A.VideoId, ''), IFNULL(AndroidWebURL, ''), IFNULL(IOSWebURL, ''),
    IFNULL(AndroidURL, ''), IFNULL(AndroidURL2, ''), IFNULL(IOSURL, '')
    FROM Video A INNER JOIN TVS B ON A.TSID = B.ID LIMIT 200`

    HtmlHead = `<table border=1 width=100% height=100%><tr><td>节目名
    </td><td>tsid</td><td>章节</td><td>章节号</td><td>描述
    </td><td>videoid</td><td>网站</td><td>地址</td></tr>`
    HtmlTail ="</table>"
)

type videoInfo struct {
    name          string
    tsid          uint
    chapter       string
    chapterNum    uint
    descr         string
    videoId       string
    website       string
    androidWebUrl string
    iosWebUrl     string
    androidUrl    string
    androidUrl2   string
    iosUrl        string
}

func init() {
    var (
        confFile string
        err      error
    )

    // parse command argument:w
    flag.StringVar(&confFile,"c","./vsmonitor.conf"," set config file path")
    flag.Parse()
    // read config
    if Conf, err = cfg.Read(confFile); err != nil {
        panic(fmt.Sprintf("Read config file "%s" failed (%s)",
            confFile, err.Error()))
    }
    // open log file
    file, err := os.OpenFile(Conf["log.file"], LogFileFlag, LogFileMode)
    if err != nil {
        panic(fmt.Sprintf("OpenFile "%s" failed (%s)", Conf["log.file"],
            err.Error()))
    }
    // init LOG
    Log = log.New(file,"", LogFlag)
    Debug = false
    i, err := strconv.ParseInt(Conf["cpucore.num"], 10, 32)
    if err != nil {
        panic(fmt.Sprintf("ParseInt "%s" failed (%s)", Conf["cpucore.num"],
            err.Error()))
    }

    CpuCore = int(i)
}

func getHttpStatusCode(url string) int {
    if url =="" {
        return 200
    }

    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return 0
    }

    req.Header.Add("User-Agent","Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17")
    req.Header.Add("Connection","close")
    resp, err := HttpClient.Do(req)
    if err != nil {
        return 0
    }

    defer resp.Body.Close()
    return resp.StatusCode
}

func sendMail(host, user, pwd, from, to, subject, body, mailType string) error {
    auth := smtp.PlainAuth("", user, pwd, strings.Split(host,":")[0])
    cntType := fmt.Sprintf("Content-Type: text/%s;charset=UTF-8", mailType)
    msg := fmt.Sprintf("To: %s

From: %s<%s>

Subject: %s

%s



%s",
        to, from, user, subject, cntType, body)

    return smtp.SendMail(host, auth, user, strings.Split(to,","), []byte(msg))
}

func getVideos(videoChan chan *videoInfo, htmlBuf *bytes.Buffer) error {
    defer HttpTransport.CloseIdleConnections()
    db, err := sql.Open("mysql", Conf["weikan.mysql"])
    if err != nil {
        return err
    }

    rows, err := db.Query(GET_VIDEO_SQL)
    if err != nil {
        db.Close()
        return err
    }

    for rows.Next() {
        video := &videoInfo{}
        err = rows.Scan(&video.name, &video.tsid, &video.chapter,
            &video.chapterNum,
            &video.website, &video.descr, &video.videoId, &video.androidWebUrl,
            &video.iosWebUrl, &video.androidUrl, &video.androidUrl2,
            &video.iosUrl)
        if err != nil {
            db.Close()
            return err
        }

        videoChan <- video
        WG.Add(1)
    }

    db.Close()
    // wait check url finish
    WG.Wait()
    // send mail
    for {
        if htmlBuf.Len() == 0 {
            Log.Print("no error found!!!!!!!!")
            break
        }

        Log.Print("found error !!!!!!!!")
        /*
        err := sendMail("smtp.gmail.com:587","xxxx",
           "xxx","xxx <xxx>",
            Conf["mail.to"],"xxxxx",
            HtmlHead+htmlBuf.String()+HtmlTail,"html")
        if err != nil {
            Log.Printf("sendMail failed (%s)", err.Error())
            time.Sleep(10 * time.Second)
            continue
        }
        */

        Log.Print("send mail")
        break
    }

    Log.Print("reset buf")
    htmlBuf.Reset()
    return nil
}

func checkUrl(videoChan chan *videoInfo, errChan chan string) {
    defer func() {
        if err := recover(); err != nil {
            Log.Print("rouintes failed :", err)
        }
    }()

    for {
        video := <-videoChan
        ok := true
        errUrl :=""

        if code := getHttpStatusCode(video.androidWebUrl); code >= 400 {
            errUrl += fmt.Sprintf("%s (%d)<br />",
                video.androidWebUrl, code)
            ok = false
        }

        if code := getHttpStatusCode(video.iosWebUrl); code >= 400 {
            errUrl += fmt.Sprintf("%s (%d)<br />",
                video.iosWebUrl, code)
            ok = false
        }

        if code := getHttpStatusCode(video.androidUrl); code >= 400 {
            errUrl += fmt.Sprintf("%s (%d)<br />",
                video.androidUrl, code)
            ok = false
        }

        if code := getHttpStatusCode(video.androidUrl2); code >= 400 {
            errUrl += fmt.Sprintf("%s (%d)<br />",
                video.androidUrl2, code)
            ok = false
        }

        if code := getHttpStatusCode(video.iosUrl); code >= 400 {
            errUrl += fmt.Sprintf("%s (%d)<br />",
                video.iosUrl, code)
            ok = false
        }

        if !ok {
            errChan <- fmt.Sprintf(`<tr><td>%s</td><td>%d</td><td>%s</td>
            <td>%d</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>`,
                video.name, video.tsid, video.chapter, video.chapterNum,
                video.descr, video.videoId,
                video.website, errUrl)
            Log.Printf(""%s" (%s) —— "%s" checkurl err", video.name,
                video.chapter, video.descr)
        } else {
            Log.Printf(""%s" (%s) —— "%s" checkurl ok", video.name,
                video.chapter, video.descr)
            WG.Done()
        }
    }
}

func mergeErr(errChan chan string, htmlBuf *bytes.Buffer) {
    defer func() {
        if err := recover(); err != nil {
            Log.Print("rouintes failed :", err)
        }
    }()

    for {
        html := <-errChan
        _, err := htmlBuf.WriteString(html)
        if err != nil {
            Log.Printf("htmlBuf WriteString "%s" failed (%s)", html,
                err.Error())
            panic(err)
        }

        WG.Done()
    }
}

func main() {
    videoChan := make(chan *videoInfo, 100000)
    errChan := make(chan string, 100000)
    htmlBuf := &bytes.Buffer{}
    defer func() {
        if err := recover(); err != nil {
            Log.Print("rouintes failed :", err)
        }
    }()

    // check url
    for i := 0; i < CpuCore; i++ {
        go checkUrl(videoChan, errChan)
    }
    // merge error string then send mail
    go mergeErr(errChan, htmlBuf)

    for {
        // get Video and LiveSrc video source
        if err := getVideos(videoChan, htmlBuf); err != nil {
            Log.Printf("getVideos failed (%s)", err.Error())
            time.Sleep(10 * time.Second)
            continue
        }

        // time.Sleep(1 * time.Hour)
    }

    Log.Print("exit...")
}

该代码具有四个funcs

getHttpStatusCode

免费资源使用resp.Body.Close()

sendMail

我不需要手动释放资源

mergeErr

通过使用htmlBuf(* bytes.Buffer)来连接err字符串

getVideos

首先,它获取视频URL,然后将其发送给videoChan,然后等待所有例程完成其检查工作。
然后sendmail并重置htmlBuf。

我找不到任何需要免费的资源,但是。

$ top

显示:

1
2
PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                                                      
6451 root      20   0 3946m 115m 2808 S  0.7  0.2   6:11.20 vsmonitor

VIRT和RES将会增长...

内存配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
(pprof) top
Total: 10.8 MB
2.3  21.2%  21.2%      2.3  21.2% main.main
2.0  18.5%  39.8%      2.0  18.5% bufio.NewWriterSize
1.5  13.9%  53.7%      1.5  13.9% bufio.NewReaderSize
1.5  13.9%  67.6%      1.5  13.9% compress/flate.NewReader
0.5   4.6%  72.2%      0.5   4.6% net.newFD
0.5   4.6%  76.8%      0.5   4.6% net.sockaddrToTCP
0.5   4.6%  81.5%      4.5  41.7% net/http.(*Transport).getConn
0.5   4.6%  86.1%      2.5  23.2% net/http.(*persistConn).readLoop
0.5   4.6%  90.7%      0.5   4.6% net/textproto.(*Reader).ReadMIMEHeader
0.5   4.6%  95.4%      0.5   4.6% net/url.(*URL).ResolveReference
  • 递归最终可能会占用大量堆空间,因为go使用的拆分堆栈可能会增长,而不是成为固定的内存区域。 寻找无尽的(足够大的)递归。
  • 但我的代码只包含循环
  • 介意让配置文件运行更长一点,直到看到实际的增长?
  • @nemo好吧,我会...
  • net / http错误,如果不阅读所有响应,resp.Body.Close()将无法关闭tcp连接!

在您的程序中添加一个选项非常容易,这样它就可以记录内存的使用位置。 在您的程序中,没有什么比我错得厉害的了。 您下载的文件是否很大? 您可以代替HEAD请求吗? 我不知道这是否有帮助; 如果您的请求量很大,也许会。

Go博客上有一篇(陈旧的)文章,有关内存分析,网址为http://blog.golang.org/2011/06/profiling-go-programs.html,文档为http://golang.org/ pkg / runtime / pprof /和http://golang.org/pkg/net/http/pprof/