Golang 快速讀取處理大日誌檔案工具

linvon發表於2020-08-27

前段時間根據在站內看到的文章,做了翻譯並製作改進了易用工具,用於快速處理大日誌檔案 相關文章連結可以見 文章

工具地址在 這裡


本文翻譯並改進於 此文

當今世界的計算機系統每天會產生海量的日誌,但是這些日誌並不適合存入到資料庫中。因為這些資料都是不可變的,並且只用於資料分析和故常排除。因此大部分情況下這些日誌都會被存放在磁碟的檔案中。往往這些日誌檔案都會增長到 GB 級別,在需要對日誌和打點進行資料分析時,如何快速高效的讀取海量日誌檔案便成為了很影響工作效率的一件事。本文介紹一種使用 Golang 通過非同步 IO 快速讀取大日誌檔案的方法。

相關的工具程式碼在 Github 可以找到

Start

首先我們來開啟檔案。我們使用 Go 標準庫的 os.File 來處理所有檔案的 IO。

f, err := os.Open(fileName)
if err != nil {
   fmt.Println("cannot able to read the file", err)
   return
}
defer file.Close()  //Do not forget to close the file

開啟檔案之後,我們有兩種方式來處理檔案:

  • 一行一行的讀取檔案。這種方式減輕了記憶體的壓力,但是會在檔案 IO 上會費更多的時間
  • 一次性將整個檔案讀入記憶體。這種方式會消耗掉很多記憶體但極大地提高了執行效率

鑑於我們的日誌檔案很大,可能會有幾個 GB 甚至十幾 GB,我們無法將整個檔案讀入到記憶體中,但是第一種方法同樣不太符合我們的需求,因為我們期望能在秒級內處理整個日誌檔案。 好在我們還有第三種方法,我們可以平衡兩種方法,將整個檔案按塊讀取到記憶體中,在 Go 中使用 bufio.NewReader() 就可以做到這一點。

r := bufio.NewReader(f)
for {
buf := make([]byte,4*1024) //the chunk size
n, err := r.Read(buf) //loading chunk into buffer
buf = buf[:n]
if n == 0 {

    if err != nil {
      fmt.Println(err)
      break
    }
    if err == io.EOF {
      break
    }
    return err
 }
}

每當我們讀入了一個塊,我們就啟動一個 Goroutine 來併發的處理檔案塊,這樣上面的程式碼會變成:

//sync pools to reuse the memory and decrease the preassure on //Garbage Collector
linesPool := sync.Pool{New: func() interface{} {
   lines := make([]byte, 500*1024)
   return lines
}}
stringPool := sync.Pool{New: func() interface{} {
   lines := ""
   return lines
}}
slicePool := sync.Pool{New: func() interface{} {
   lines := make([]string, 100)
   return lines
}}
r := bufio.NewReader(f)
var wg sync.WaitGroup //wait group to keep track off all threads
for {

   buf := linesPool.Get().([]byte)
   n, err := r.Read(buf)
   buf = buf[:n]
   if n == 0 {
       if err != nil {
           fmt.Println(err)
           break
       }
       if err == io.EOF {
           break
       }
       return err
   }
   nextUntillNewline, err := r.ReadBytes('\n') //read entire line

   if err != io.EOF {
       buf = append(buf, nextUntillNewline...)
   }

   wg.Add(1)
   go func() {

       //process each chunk concurrently
       //start -> log start time, end -> log end time

       ProcessChunk(buf, &linesPool, &stringPool, &slicePool, start, end)
       wg.Done()

   }()
}
wg.Wait()

上面的程式碼在效率上有兩個優化點:

  • sync.Pool 是一個高效的例項池,可以用於減輕 GC 的壓力。我們可以對不同的切片複用記憶體分配,這大大減少了記憶體上的壓力。
  • 使用了 Go Routines 來併發處理檔案塊,這樣可以進行非同步 IO,提高了 CPU 的利用率,加快了整個檔案的處理速度

最後我們來實現處理函式

//sync pools to reuse the memory and decrease the preassure on //Garbage Collector
linesPool := sync.Pool{New: func() interface{} {
   lines := make([]byte, 500*1024)
   return lines
}}
stringPool := sync.Pool{New: func() interface{} {
   lines := ""
   return lines
}}
slicePool := sync.Pool{New: func() interface{} {
   lines := make([]string, 100)
   return lines
}}
r := bufio.NewReader(f)
var wg sync.WaitGroup //wait group to keep track off all threads
for {

   buf := linesPool.Get().([]byte)
   n, err := r.Read(buf)
   buf = buf[:n]
   if n == 0 {
       if err != nil {
           fmt.Println(err)
           break
       }
       if err == io.EOF {
           break
       }
       return err
   }
   nextUntillNewline, err := r.ReadBytes('\n') //read entire line

   if err != io.EOF {
       buf = append(buf, nextUntillNewline...)
   }

   wg.Add(1)
   go func() {

       //process each chunk concurrently
       //start -> log start time, end -> log end time

       ProcessChunk(buf, &linesPool, &stringPool, &slicePool, start, end)
       wg.Done()

   }()
}
wg.Wait()

上面的程式碼處理 16GB 的檔案耗時大概在 25S 左右

多元化相容處理

在實際的生產環境中,我們肯定不會任由日誌檔案瘋狂增長下去,一般情況下都會將日誌檔案進行切割和壓縮。日誌檔案往往都會按照一定名稱格式來儲存,並且歸檔為 .gz 型別的檔案。因此我在原本的程式碼基礎上增加了一些定製內容,包括

  • 指定檔名稱格式化,支援多檔案處理。(多檔案處理採用了序列方式,實測並行檔案處理在非同步 IO 上優化不大,且會佔用更多的記憶體)
  • 支援 gz 檔案讀取
  • 行處理函式可定製,自由定製輸出格式
更多原創文章乾貨分享,請關注公眾號
  • Golang 快速讀取處理大日誌檔案工具
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章