【问题标题】:how to read a large log file which other process current write如何读取其他进程当前写入的大型日志文件
【发布时间】:2020-12-08 13:29:49
【问题描述】:

每天创建日志文件,一个文件约400MB,JVM内存约2GB。 让一个进程使用 'a' 模式写入一个大日志文件。 我想读取这个文件并能实现一些功能:

  1. 追加读取新写入的数据
  2. jvm重启后我会存储偏移量来恢复读取

这是我的简单实现,不知道时间和内存消耗好不好。我想知道是否有更好的方法来解决这个问题

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);
        }
    }
}

【问题讨论】:

    标签: java file io


    【解决方案1】:

    不幸的是,事实并非如此。

    这段代码有两个主要问题。首先我会解决简单的问题,但最重要的是第二点。

    编码问题

    String line = randomAccessFile.readLine();

    这一行将字节隐式转换为字符,这通常是个坏主意,因为字节不是字符,从一个字节转换为另一个字节需要字符集编码。

    这种方法(来自 RAF 的readLine())是一个奇怪的案例 - 可能是因为 RandomAccessFile 是非常古老的 API。使用此方法将应用一些 bizarro 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 个字符并一口气读取它们要慢 100 倍。

    解决方案

    您可能想完全放弃 RandomAccessFile,它是相当旧的 API。

    缓冲的一个主要问题是,除非您事先知道要读取多少字节,否则缓冲要困难得多。在这里,您不知道:您想继续阅读,直到遇到换行符为止,但是您不知道要多久才能到达那里。此外,缓冲 API 往往只返回方便的内容,因此可能读取的字节数比我们要求的要少(但它总是至少读取 1,除非我们到达文件末尾)。因此,我们需要编写代码来重复读取整个块的数据,分析块中是否有换行符,如果不存在,请继续阅读。

    此外,打开渠道等费用昂贵。所以,如果你想挖掘所有的日志行,编写每次都打开一个新通道的代码是次优的。

    这个怎么样,使用来自java.nio.file的较新的文件API:

    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 对象已关闭,因此,请使用 try-with-resources。

    【讨论】:

    • 谢谢你,完美解决了我的问题。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-05-19
    • 1970-01-01
    • 2015-06-04
    • 2020-04-01
    • 2019-11-13
    • 1970-01-01
    • 2011-08-15
    相关资源
    最近更新 更多