簡體   English   中英

如何讀取其他進程當前寫入的大型日志文件

[英]how to read a large log file which other process current write

每天創建一個日志文件,一個文件400MB左右,JVM memory 2GB左右。讓一個進程用'a'模式寫一個大日志文件。我想讀這個文件並能實現一些功能:

  1. Append 讀取新寫入的數據
  2. jvm重啟后我會存儲偏移量來恢復讀取

這是我的簡單實現,不知道時間和memory消耗好不好。 我想知道是否有更好的方法來解決這個問題

public static void main(String[] args) throws IOException {
    String filePath = "D://test.log";
    long restoreOffset = resotoreOffset();
    RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r");
    randomAccessFile.seek(restoreOffset);
    while (true) {
        String line = randomAccessFile.readLine();
        if(line != null) {

            // doSomething(line);

            restoreOffset = randomAccessFile.getFilePointer();

            //storeOffset(restoreOffset);
        }
    }
}

不幸的是,事實並非如此。

這段代碼有兩個主要問題。 首先,我將解決簡單的問題,但最重要的是第二點。

編碼問題

String line = randomAccessFile.readLine();

這一行將字節隱式轉換為字符,這通常是個壞主意,因為字節不是字符,從一個字節轉換到另一個字節需要字符集編碼。

這種方法(來自 RAF 的readLine() )是一個奇怪的案例 - 可能是因為 RandomAccessFile 是非常古老的 API。 使用這種方法將應用一些奇怪的 ISO-8859-1 esque 字符集編碼:它通過將每個字節作為一個完整的字符來將字節轉換為字符,假設該字節表示 unicode 字符,這實際上不是一個理智的編碼,只是一個懶惰的程序員。

你的結果是:除非你能保證這個日志文件永遠只包含 ASCII 字符,否則這個代碼會被破壞,並且readLine根本不能使用。 相反,您將不得不做更多的工作:讀取字節直到換行,然后使用new String(byteArray, StandardCharsets.UTF_8)將如此收集的字節轉換為字符串,或者使用ByteBuffer並應用類似的策略。 但是請繼續閱讀,因為解決第二個問題會自動解決這個問題。

緩沖

現代計算機系統傾向於喜歡“打包”。 您不能真正對單個字節進行操作。 以 SSD 為例(盡管這也適用於旋轉盤片):實際的 SSD 硬件無法讀取單個字節。 它只能讀取整個塊的數據。

因此,當您明確向操作系統詢問單個字節時,最終會引發一系列事件,導致 SSD 讀取整個塊,然后將整個塊傳遞給操作系統,然后操作系統將忽略除一個字節之外的所有內容你想要的,然后返回。

如果您的代碼隨后請求下一個字節,我們將再次執行該例程。

因此,如果您從具有 1024 字節塊的 SSD 連續讀取 1024 字節,則通過調用read() 1024 次會導致 SSD 執行 1024 次讀取,而調用read(byteArr)一次,將其傳遞給 1024 字節數組, 使 SSD 執行單次讀取。

是的,這意味着字節數組解決方案實際上快了 1000 倍。

這同樣適用於網絡。 一千次發送 1 字節通常比發送 1000 字節一次慢近 1000 倍; TCP/IP 數據包可以攜帶大約 1800 字節的數據,因此發送少於該數據的數據幾乎不會為您帶來任何好處。

RAF 的readLine()就像第一個(壞的)場景一樣工作:它一次讀取一個字節,直到遇到換行符。 因此,要讀取 100 個字符的字符串,它比只知道需要讀取 100 個字符並在一個 go 中讀取它們要慢 100 倍。

解決方案

您可能想完全放棄 RandomAccessFile,它是相當古老的 API。

緩沖的一個主要問題是,除非您事先知道要讀取多少字節,否則它要困難得多。 在這里,您不知道:您想繼續閱讀,直到遇到換行符,但您不知道要多久才能到達那里。 此外,緩沖 API 往往只返回方便的內容,因此可能讀取的字節數比我們要求的要少(但它總是至少讀取 1,除非我們到達文件末尾)。 所以,我們需要編寫代碼來重復讀取整個塊的數據,分析塊中的換行符,如果不存在,繼續閱讀。

此外,打開通道等是昂貴的。 因此,如果您想挖掘所有日志行,編寫每次都打開一個新通道的代碼是次優的。

這個怎么樣,使用來自java.nio.file

public class LogLineReader implements AutoCloseable {
  private final byte[] buffer = new byte[1024];
  private final ByteBuffer bb = wrap(buffer);
  private final SeekableByteChannel channel;
  private final Charset charset = StandardCharsets.UTF_8;

  public LogLineReader(Path p) {
    channel = Files.newByteChannel(p, StandardOpenOption.READ);
    channel.position(111L); // you seek to pos 111 in your code...
  }

  @Override public void close() throws IOException {
    channel.close();
  }

  // This code buffers: First, our internal buffer is scanned
  // for a new line. If there is no full line in the buffer,
  // we read bytes from the file and check again until we find one.

  public String readLine() {
    int len = 0;
    if (!channel.isOpen()) return null;

    int scanStart = 0;

    while (true) {
      // Scan through the bytes we have buffered for a newline.

      for (int i = scanStart; i < buffer.position(); i++) {
        if (buffer[i] == '\n') {
          // Found it. Take all bytes up to the new line, turn into
          // a string.
          String res = new String(buffer, 0, i, charset);

          // Copy all bytes from _after_ the newline to the front.
          System.arraycopy(buffer, i + 1, buffer, 0, buffer.position() - i - 1);

          // Adjust the position (which represents how many bytes are buffered).
          buffer.position(buffer.position() - i - 1);
          return res;
        }
      }
      scanStart = buffer.position();

      // If we get here, the buffer is empty or contains no newline.

      if (scanStart == buffer.limit()) {
        throw new IOException("Log line too long");
      }

      int read = channel.read(buffer); // let's fetch more bytes!

      if (read == -1) {
        // we've reached the end of the file.

        if (buffer.position() == 0) return null;
        return new String(buffer, 0, buffer.position(), charset);
      }
    }
  }
}

為了效率,這段代碼不能處理長度超過 1024 的日志行; 隨意增加這個數字。 如果您希望能夠讀取無限大小的日志線,那么在某些時候巨大的緩沖區是一個問題。 如果必須,您可以編寫代碼在達到 1024 時調整緩沖區大小,或者您可以更新此代碼以使其繼續讀取,但僅返回前 1024 個字符的截斷字符串。 我會把它留給你作為練習。

注意:我也沒有對此進行測試,但至少它應該為您提供使用 SeekableByteChannel 的一般要點,以及緩沖區的概念。

要使用:

Path p = Paths.get("D://logfile.txt");
try (LogLineReader reader = new LogLineReader(p)) {
  for (String line = reader.readLine(); line != null; line = reader.readLine()) {
    // do something with line
  }
}

您必須確保 LLR object 已關閉,因此,請使用 try-with-resources。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM