Crawlab發佈幾個月以來,其中經歷過屢次迭代,在使用者們的積極反饋下,crawlab爬蟲平臺逐漸穩定,可是最近有用戶報出crawlab啓動一段時間後,主節點機器會出現內存佔用太高的問題,一臺4G內存的機器在運行crawlab後居然能佔用3.5G以上,幾乎能夠確定後端服務的某個接口因爲代碼不規範致使內存佔用,因而決定對crawlab進行一次內存分析。git
分析內存光靠手撕代碼是比較困難的,總要藉助一些工具。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瀏覽器
接下來咱們須要藉助go tool pprof
來分析:app
go tool pprof -inuse_space http://本機Ip:8888/debug/pprof/heap
複製代碼
這個命令進入後,是一個相似gdb
的交互式界面,輸入top
命令能夠前10大的內存分配,flat
是堆棧中當前層的inuse內存值,cum是堆棧中本層級的累計inuse內存值(包括調用的函數的inuse內存值,上面的層級)函數
能夠看到,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
這裏直接將所有的文件內容,以二進制的形式讀了進來,致使內存倍增,使人窒息的操做。
其實在讀大文件的時候,把文件內容所有讀到內存,直接就翻車了,正確是處理方法有兩種,一種是流式處理:
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
}
複製代碼
咱們這裏採用的第二種方式來優化,優化後再來看下內存分析:
佔用1M內存,這纔是一個正常後端服務該有的內存大小,源碼已push到crawlab,能夠在GitHub項目源碼中閱讀。
最後附上項目連接,github.com/tikazyq/cra… 爲crawlab打電話!歡迎你們一塊兒貢獻,讓crawlab變得更好用!