crawlab的golang後端內存分析及優化-基於go pprof

1. 背景

Crawlab發佈幾個月以來,其中經歷過屢次迭代,在使用者們的積極反饋下,crawlab爬蟲平臺逐漸穩定,可是最近有用戶報出crawlab啓動一段時間後,主節點機器會出現內存佔用太高的問題,一臺4G內存的機器在運行crawlab後居然能佔用3.5G以上,幾乎能夠確定後端服務的某個接口因爲代碼不規範致使內存佔用,因而決定對crawlab進行一次內存分析。git

2. 分析

分析內存光靠手撕代碼是比較困難的,總要藉助一些工具。Golang pprof是Go官方的profiling工具,很是強大,使用起來也很方便。github

首先,咱們在crawlab項目中嵌入以下幾行代碼:算法

import _ "net/http/pprof"
go func() {
		http.ListenAndServe("0.0.0.0:8888", nil)
	}()
複製代碼

將crawlab後端服務啓動後,瀏覽器中輸入http://ip:8899/debug/pprof/就能夠看到一個彙總分析頁面,顯示以下信息:後端

/debug/pprof/

profiles:
0    block
32    goroutine
552    heap
0    mutex
51    threadcreate

full goroutine stack dump
複製代碼

點擊heap,在彙總分析頁面的最上方能夠看到以下圖所示,紅色箭頭所指的就是當前已經使用的堆內存是25M,amazing!在我只上傳一個爬蟲文件的狀況下,一個後端服務所用的內存居然能達到25M瀏覽器

mYnJuF.jpg

接下來咱們須要藉助go tool pprof來分析:app

go tool pprof -inuse_space http://本機Ip:8888/debug/pprof/heap
複製代碼

這個命令進入後,是一個相似gdb的交互式界面,輸入top命令能夠前10大的內存分配,flat是堆棧中當前層的inuse內存值,cum是堆棧中本層級的累計inuse內存值(包括調用的函數的inuse內存值,上面的層級)函數

mYMF91.png

能夠看到,bytes.makeSlice這個內置方法居然使用了24M內存,繼續往下看,能夠看到ReadFrom這個方法,搜了一下,發現 ioutil.ReadAll() 裏會調用 bytes.Buffer.ReadFrom, 而 bytes.Buffer.ReadFrom 會進行 makeSlice。再回頭看一下io/ioutil.readAll的代碼實現,工具

func readAll(r io.Reader, capacity int64) (b []byte, err error) {
    buf := bytes.NewBuffer(make([]byte, 0, capacity))
    defer func() {
        e := recover()
        if e == nil {
            return
        }
        if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
            err = panicErr
        } else {
            panic(e)
        }
    }()
    _, err = buf.ReadFrom(r)
    return buf.Bytes(), err
}

// bytes.MinRead = 512
func ReadAll(r io.Reader) ([]byte, error) {
    return readAll(r, bytes.MinRead)
}

複製代碼

能夠看到,ioutil.ReadAll 每次都會分配初始化一個大小爲 bytes.MinRead 的 buffer ,bytes.MinRead 在 Golang 裏是一個常量,值爲 512 。就是說每次調用 ioutil.ReadAll 都會分配一塊大小爲 512 字節的內存,目前看起來是正常的,但咱們再看一下ReadFrom的實現,優化

// ReadFrom reads data from r until EOF and appends it to the buffer, growing
// the buffer as needed. The return value n is the number of bytes read. Any
// error except io.EOF encountered during the read is also returned. If the
// buffer becomes too large, ReadFrom will panic with ErrTooLarge.
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
    b.lastRead = opInvalid
    // If buffer is empty, reset to recover space.
    if b.off >= len(b.buf) {
        b.Truncate(0)
    }
    for {
        if free := cap(b.buf) - len(b.buf); free < MinRead {
            // not enough space at end
            newBuf := b.buf
            if b.off+free < MinRead {
                // not enough space using beginning of buffer;
                // double buffer capacity
                newBuf = makeSlice(2*cap(b.buf) + MinRead)
            }
            copy(newBuf, b.buf[b.off:])
            b.buf = newBuf[:len(b.buf)-b.off]
            b.off = 0
        }
        m, e := r.Read(b.buf[len(b.buf):cap(b.buf)])
        b.buf = b.buf[0 : len(b.buf)+m]
        n += int64(m)
        if e == io.EOF {
            break
        }
        if e != nil {
            return n, e
        }
    }
    return n, nil // err is EOF, so return nil explicitly
}
複製代碼

這個函數主要做用就是從 io.Reader 裏讀取的數據放入 buffer 中,若是 buffer 空間不夠,就按照每次 2x + MinRead 的算法遞增,這裏 MinRead 的大小也是 512 Bytes ,也就是說若是咱們一次性讀取的文件過大,就會致使所使用的內存倍增,假設咱們的爬蟲文件總共有500M,那麼所用的內存就有500M * 2 + 512B,何況爬蟲文件中還帶了那麼多log文件,那看看crawlab源碼中是哪一段使用ioutil.ReadAll讀了爬蟲文件,定位到了這裏:ui

mYQTWn.jpg

這裏直接將所有的文件內容,以二進制的形式讀了進來,致使內存倍增,使人窒息的操做。

其實在讀大文件的時候,把文件內容所有讀到內存,直接就翻車了,正確是處理方法有兩種,一種是流式處理:

func ReadFile(filePath string, handle func(string)) error {
    f, err := os.Open(filePath)
    defer f.Close()
    if err != nil {
        return err
    }
    buf := bufio.NewReader(f)

    for {
        line, err := buf.ReadLine("\n")
        line = strings.TrimSpace(line)
        handle(line)
        if err != nil {
            if err == io.EOF{
                return nil
            }
            return err
        }
        return nil
    }
}
複製代碼

第二種方案就是分片處理,當讀取的是二進制文件,沒有換行符的時候,使用這種方案比較合適:

func ReadBigFile(fileName string, handle func([]byte)) error {
    f, err := os.Open(fileName)
    if err != nil {
        fmt.Println("can't opened this file")
        return err
    }
    defer f.Close()
    s := make([]byte, 4096)
    for {
        switch nr, err := f.Read(s[:]); true {
        case nr < 0:
            fmt.Fprintf(os.Stderr, "cat: error reading: %s\n", err.Error())
            os.Exit(1)
        case nr == 0: // EOF
            return nil
        case nr > 0:
            handle(s[0:nr])
        }
    }
    return nil
}
複製代碼

咱們這裏採用的第二種方式來優化,優化後再來看下內存分析:

mYltYj.png

佔用1M內存,這纔是一個正常後端服務該有的內存大小,源碼已push到crawlab,能夠在GitHub項目源碼中閱讀。

最後附上項目連接,github.com/tikazyq/cra… 爲crawlab打電話!歡迎你們一塊兒貢獻,讓crawlab變得更好用!

相關文章
相關標籤/搜索