如何實現一個高效的本地日誌收集程序

客戶端在請求資源時,請求會發送到服務端的業務程序,而後業務程序負責把資源返回給客戶端。在這個過程當中,若是咱們要對服務端的程序進行優化,那麼分析服務端的日誌是必不可少的。而分析服務端的日誌,首先就須要把服務端的日誌收集起來。那麼如何實現一個高效的本地日誌收集程序,是本文要討論的內容。java

"高效" 應該怎麼理解呢?本文把"高效"定義爲:在保證讀取日誌吞吐量的同時,儘量少地佔用服務器資源。更具體的說,"高效"包含的指標有:CPU消耗、內存佔用、可靠性、耗時、吞吐量等因素。git

本文將介紹使用 Java 編程語言實現的讀取本地文件的幾種方式,並分析每種方式的優缺點。最後給出筆者實踐獲得的最高效的本地日誌收集程序,供讀者參考。另外因爲筆者水平有限,文中有誤之處,歡迎指正。github

1、BufferedReader 樸素方式

讀取本地文件,咱們最經常使用的方式是構建一個 BufferedReader 類,經過它的 readLine() 方法讀取每一行日誌。編程

若是咱們要實現可靠的文件傳輸功能,就須要定時保存文件的當前讀取位置。 這樣能夠保證,程序即便在讀文件過程當中中止,程序重啓後,依然能夠從文件上次讀取的位置繼續消費日誌,保障日誌不會被重複或遺漏消費。代碼以下:數組

BufferedReader reader =  new BufferedReader(new InputStreamReader(new FileInputStream(filePath)));
while ((line = reader.readLine()) != null){
  recordPosition();   // 記錄讀取位置
  process(line);      // 處理每一行的內容
}

BufferedReader方式的優勢是:讀取日誌消耗的內存和CPU比較小,吞吐量高。緩存

  • 因爲使用了緩存,程序會申請一塊固定大小的內存做爲中轉,不會把整個文件讀到內存,這樣內存佔用會比較小,申請的緩存默認大小爲 8192個字節。
  • 一樣因爲使用到了緩存,讀取本地文件不會逐個字節讀取,逐個字節讀取的方式會頻繁地中斷CPU,而是每次讀取一個緩存塊的數據,這樣會下降中斷CPU的次數,CPU消耗會很低。
  • 因爲使用緩存,相比逐個字節地從文件讀取內容,以塊方式讀取文件內容,能大大提升日誌讀取的吞吐量。

BufferedReader方式的缺點是:不支持隨機讀取,在一些場景下耗時比較高。服務器

  • 考慮這樣的場景,程序在文件讀取過程當中異常中止,程序重啓後,BufferedReader方式會從頭開始掃描文件,直到找到上次文件讀取的位置,在繼續消費日誌。而查找文件某個位置的時間複雜度爲O(n),這樣若是文件很大(超過1GB),且重啓操做比較頻繁,那麼程序會消耗不少無用的操做在掃描日誌上,從而增長日誌處理的耗時。

2、RandomAccessFile 隨機讀取方式

基於上述BufferedReader樸素方式的缺點,咱們但願實現隨機讀取日誌的方式。所以咱們考慮使用 RandomAccessFile 類,經過它的 readLine() 方法來讀取每一行日誌。網絡

一樣,要實現高可靠的文件傳輸的功能,也須要定時保存文件的當前讀取位置。 實現代碼以下:app

RandomAccessFile raf = new RandomAccessFile(file, "r");
raf.seek(position);   // 定位到文件的讀取位置
while ((line = raf.readLine()) != null) {
  process(line);      // 處理每一行的內容
}

RandomAccessFile 方式的優勢是:支持隨機讀取,讀取日誌消耗內存少。dom

  • 這種方式可以快速定位到文件的讀取位置,定位到文件讀取位置的時間複雜度爲 O(1)
  • 該方式讀取本地文件,會逐個字節讀取文件中內容,且不使用緩存,內存佔用極低。

RandomAccessFile 方式的缺點是:CPU佔用高、吞吐量低。

  • 它的內部實現是經過一個字節一個字節地讀取文件內容,因爲每讀一個字節都會中斷一次CPU,相對於使用緩存方式讀取一批數據中斷一次CPU,這種方式中斷CPU次數會更頻繁,形成CPU佔用高。
  • 另外相對於緩存按照塊方式讀取文件內容,這種逐個字節讀取文件內容的方式,明顯會下降文件讀取的吞吐量,文件讀取效率很低。

3、MappedByteBuffer 內存映射文件方式

RandomAccessFile 隨機讀取方式須要按字節讀取文件,這樣讀取文件的吞吐量會很低。而 BufferedReader 的數據塊緩存機制能提升文件的讀取吞吐量,所以考慮爲 RandomAccessFile 添加緩存。調研發現 MappedByteBuffer 內存映射文件方式提供了緩存機制。

一樣,要實現可靠的文件傳輸的功能,也須要定時保存文件的當前讀取位置。下面代碼展現了核心的讀文件處理流程,考慮到更清晰地展現核心處理流程,去掉了保存文件的當前讀取位置的邏輯。實現代碼以下:

RandomAccessFile raf = new RandomAccessFile(file, "r");
FileChannel channel = raf.getChannel();
MappedByteBuffer out = channel.map(FileChannel.MapMode.READ_ONLY, 0, file.length());
byte[] buf = new byte[count];   // buf 數組用來存儲每一行

while(out.remaining()>0){
      // 解析出每一行
      byte by = out.get();
      ch =(char)by;
      switch(ch){
        case '\n':
          flag = true;
          break;
        case '\r':
          flag = true;
          break;
        default:
          buf[j++] = by;
          break;
      }
      // 讀取的字符超過了buf 數組的大小,須要動態擴容
      if(flag ==false && j>=count){
        count = count + extra;
        buf = copyOf(buf,count);
      }
      // 處理每一行並初始化環境
      if(flag==true){
        String line = new String(buf, 0, j, StandardCharsets.UTF_8);
        process(line);  	// 處理每一行
        flag = false;
        count = extra;
        buf = new byte[count];
        j =0;
      }
    }

MappedByteBuffer 內存映射文件方式的優勢:CPU消耗低、吞吐量高、支持隨機讀取。

  • 這種方式在實現上使用了緩存,下降 IO 對 CPU 的中斷次數,這樣 CPU 消耗低,文件讀取的吞吐量高。
  • 而且底層使用了 RandomAccessFile ,支持文件內容的隨機讀取,查找文件讀取位置的時間複雜度爲 O(1).

MappedByteBuffer 方式內存佔用高,且映射的文件有文件大小限制。

  • 這種方式須要把文件內容所有讀入內存,這樣會消耗服務器的大量內存,內存佔用高。

  • 另外這種方式最大映射的文件大小爲 Integer的最大值,即最大支持映射 2GB 的文件,也就是說只能處理2GB如下的文件,沒法處理超過 2GB 的文件。

4、ByteBuffer 數據塊緩存方式

MappedByteBuffer 內存映射文件方式,須要把文件內容所有寫入內存,並且沒法應對傳輸文件大小超過2GB大小的場景。因而可知,MappedByteBuffer方式的核心缺點在於內存佔用的問題。

針對上述缺點,筆者設計了一種 ByteBuffer 數據塊緩存方式的解決方案:申請一個數據塊緩存,把文件相應大小的內容裝入緩存,該數據塊的緩存被消費完後,在往數據塊緩存裝入下一部分的文件內容,而後繼續消費數據塊緩存中的數據;如此循環,直到把文件內容所有讀完爲止。

數據塊緩存

一樣,要實現可靠的文件傳輸的功能,也須要定時保存文件的當前讀取位置。具體實現代碼以下:

RandomAccessFile raf = new RandomAccessFile(filePath, "r");
FileChannel fc = raf.getChannel();

ByteBuffer buffer = ByteBuffer.allocate(bufferSize);   // 讀取一批日誌申請的字節緩存空間大小
ByteBuffer lineBuffer = ByteBuffer.allocate(lineBufferSize); //每行日誌申請的字節緩存空間大小

int bytesRead = fc.read(buffer);
while (bytesRead != -1 && !fileReaderClosed.get()) {
  currPos = fc.position() - bytesRead;

  buffer.flip();      // 切換爲讀模式
  while (buffer.hasRemaining()) {
    byte b = buffer.get();
    currPos++;
    if (b == '\n' || b == '\r') {
      sendLine(lineBuffer);  // 處理日誌
    } else {
      // 若空間不夠則擴容
      if (!lineBuffer.hasRemaining()) {
        lineBuffer = reAllocate(lineBuffer);
      }
      lineBuffer.put(b);
    }
  }
  buffer.clear();    // 清除緩存

  bytesRead = fc.read(buffer);   // 寫入緩存
}

ByteBuffer 數據塊緩存方式的優勢是:支持隨機讀取,CPU消耗少,內存佔用低,吞吐量高。

  • 這種方式底層使用了RandomAccessFile作文件掃描,查找指定位置的字符串時間複雜度O(1)
  • 相對於每讀取一個字節都要中斷一次CPU,經過使用一個字節緩存塊來批量讀取文件內容的方案,能大大下降調用CPU的頻率,減小CPU的消耗。
  • 相對於把整個文件映射到內存,每次把文件的部份內容映射到內存緩衝區,可以有效減低內存佔用,且不受文件大小的限制。
  • 相對於逐個字節讀取文件內容,以緩存塊方式讀取能有效提升吞吐量。

總結

本文由淺入深地介紹了四種讀取本地文件的方式,並分析了每種方式存在的優缺點。經過對每種方式存在的缺點進行探索式改進,最後實現了一種高效的收集本地日誌文件的方案——ByteBuffer 數據塊緩存方式。這四種方式的優缺點對比彙總以下:

吞吐量 CPU消耗 內存佔用 時間複雜度(理論值)
BufferedReader 樸素方式 O(n)
RandomAccessFile 隨機讀取方式 O(1)
MappedByteBuffer 內存映射文件方式 O(1)
ByteBuffer 數據塊緩存方式 O(1)

這裏須要說明一點,文中提到的可靠性是指在正常狀況下的操做,如:啓動、中止操做,日誌消費可作到Exactly-once。可是在異常狀況下,如:網絡抖動或服務被強制kill,日誌消費可能會出現少許的日誌重複或丟失現象。服務還有待向高可靠的方向演進。

ByteBuffer 數據塊緩存方式已應用到本部門的開源項目 Databus 的日誌推送端業務中,其具體的實現爲FileSource ,代碼地址:https://github.com/weibodip/databus/blob/master/src/main/java/com/weibo/dip/databus/source/FileSource.java ,有興趣的同窗能夠查閱。

相關文章
相關標籤/搜索