优化大容量日志文件的Java文件I / O.

时间:2013-07-08 02:40:34

标签: java performance optimization logging file-io

当我想编写用于将文本写入文件的Java代码时,它通常看起来像这样:

File logFile = new File("/home/someUser/app.log");
FileWriter writer;

try {
    writer = new FileWriter(logFile, true);

    writer.write("Some text.");

    writer.close();
} catch (IOException e) {
    e.printStackTrace();
}

我现在正在编写一个Logger,将由内部报告工具广泛使用。由于在这个问题的上下文之外的原因,我不能使用传统的日志框架之一(SLF4J,Log4j,Logback,JUL,JCL等)。所以我必须做一些本土的东西。

此日志记录系统将是简单的,不可配置的,但必须能够处理大量(每秒可能有数百个日志操作或更多)。

所以我问:我如何优化上面的普通文件I / O模板,以处理高吞吐量的日志记录?我可以在这里利用什么“隐藏的Java文件I / O 宝石”?几乎任何事情都有,除了像我说的那样,使用其他日志框架。基本Logger API需要类似于:

public class Logger {
    private File logFile;

    public Logger(File logFile) {
        super();

        setFile(logFile);
    }

    public void log(String message) {
        ???
    }
}

提前致谢!

更新:如果我的Logger使用的是ByteOutputStream而不是FileWriter,那么我该如何正确同步log(String) : void方法?< / p>

public class Logger {
    private File logFile;

    // Constructor, getters/setters, etc.

    public void synchronized log(String message) {
        FileOutputStream foutStream = new FileOutputStream(logFile);
        ByteOutputStream boutStream = new BytesOutputStream(foutStream);

        boutStream.write(message.getBytes(Charset.forName("utf-8")));

        // etc.
    }
}

3 个答案:

答案 0 :(得分:1)

如果要实现日志记录操作的最大吞吐量,则应使用队列和单独的日志写入线程将消息的日志记录与将其写入文件系统分离。

答案 1 :(得分:1)

日志记录系统的目的不仅仅是实现最大吞吐量。它是审计跟踪所必需的。必须做出业务决策,以确定在发生崩溃时可以容忍多少数据丢失(如果有的话)。在承诺使用任何特定的技术解决方案之前,您需要首先进行调查。

答案 2 :(得分:0)

我这里只讨论吞吐量而不是工程或可靠性问题,因为这个问题只涉及性能。

您需要缓冲写入磁盘。使用无保险的I / O编写大量小小块会产生大量开销:

  • 本机方法调用的成本; JVM必须进行大量的簿记,包括知道哪些线程正在运行本机方法,哪些不是,为了工作。在现代平台上,这大约是几十或几百纳秒。
  • 通过魔术JNI调用将数据从Java堆复制到本机内存。有一个内存副本占用时间与数据长度成正比,但也有一堆JVM簿记。在大约几百纳秒的时间内,簿记开销。
  • write()或类似操作系统调用的成本。 Ballpark的开销大约2微秒。 (还有其他成本;您的缓存和TLB可能在返回时被刷新。)write()还需要将数据从用户空间复制到内核空间。

操作系统可以在内部缓冲您的写入。它可能不会。这取决于操作系统和底层文件系统的特性。您通常可以强制它不要缓冲您的写入。您通常也可以刷新操作系统的缓冲区。这样做会导致磁盘搜索和写入的成本。 Ballpark磁盘寻求大约8ms,写入在100MB / s和1GB / s之间。如果您正在使用RAM磁盘或闪存存储器或类似的东西,请将磁盘搜索开销扔出窗口 - 通常会有更低的延迟。

如果可能,您想要避免的巨大成本是磁盘寻道成本。写入100多字节的日志消息时,等待8毫秒是一段很长的时间。您需要在用户和后备存储之间进行某种缓冲,无论是由操作系统提供还是由日志记录界面隐藏。

来自JVM的系统调用的开销也很重要,尽管它比磁盘搜索的成本低大约1000倍。您需要花费两到三微秒来告诉内核缓冲100多字节的写入。几乎所有这些两到三微秒都用于处理与将日志消息写入文件完全无关的各种簿记任务。这就是您希望缓冲在用户空间中发生的原因,最好是在Java代码而不是本机代码中。 (但是,工程问题可能会使这变得不可能。)

Java已经提供了嵌入式缓冲解决方案--- BufferedWriterBufferedOutputStream。事实证明,这些是内部同步的。您将需要使用BufferedOutputStream,以便字符串到字节的转换发生在锁外而不是内部。

如果你保留一个Buffered的队列,你可以做一个比String更好的队列。这节省了内存副本,但我怀疑这是值得的。

关于缓冲区大小,我建议大约4MB或8MB。此范围内的缓冲区大小可以弥补磁盘在大多数典型现代硬件上寻找的延迟。您的南桥可以推动大约1GB / s,典型的磁盘可以推动大约100MB / s。那么,在你的南桥最大化的情况下,8MB写入将需要大约8毫秒 - 大约与磁盘搜索一样长。使用单个“典型的现代磁盘”,花费8MB随机写入的时间花费在写入上。

同样,如果需要将日志消息可靠地写入后备存储,则无法在Java内部进行缓冲。在这种情况下,您需要信任内核,并且为此付出了速度。