0

当我想编写用于将文本写入文件的 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 File I/O 的隐藏宝石”?几乎任何事情都会发生,除了,就像我说的,使用其他日志框架。基本LoggerAPI 需要类似于:

public class Logger {
    private File logFile;

    public Logger(File logFile) {
        super();

        setFile(logFile);
    }

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

提前致谢!

更新:如果我Logger使用 aByteOutputStream而不是 a FileWriter,那么如何正确同步我的log(String) : void方法?

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.
    }
}
4

3 回答 3

1

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

于 2013-07-08T09:54:59.993 回答
1

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

于 2013-07-08T03:10:29.433 回答
0

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

您将需要缓冲写入磁盘。使用无缓冲的 I/O 编写大量的小片段会产生一大堆开销:

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

操作系统可能会在内部缓冲您的写入。它可能不会。这取决于操作系统和底层文件系统的特性。您通常可以强制它不缓冲您的写入。您通常还可以刷新操作系统的缓冲区。这样做会产生磁盘寻道和写入的成本。估计磁盘寻道时间约为 8 毫秒,写入速度在 100MB/s 和 1GB/s 之间。如果您使用的是 RAM 磁盘或闪存或类似的东西,则将磁盘查找开销抛到窗外——延迟通常要低得多。

如果可能的话,你想要避免的真正大的成本是磁盘寻道成本。编写 100 多个字节的日志消息时,8 毫秒的等待时间实在是太长了。您将需要在用户和后备存储之间进行某种缓冲,无论是由操作系统提供还是由日志接口隐藏。

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

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

Buffered如果您保留一个Strings 队列,一旦它达到一定大小就刷新它,您可以做得比类更好。这节省了内存副本,但我相当怀疑这是否值得。

关于缓冲区大小,我建议大约 4MB 或 8MB。此范围内的缓冲区大小很好地覆盖了大多数典型现代硬件上的磁盘寻道延迟。你的南桥可以推大约 1GB/s,一个典型的磁盘可以推大约 100MB/s。最大化你的南桥,然后,一个 8MB 的写入将需要大约 8 毫秒——大约与磁盘搜索一样长。对于单个“典型的现代磁盘”,进行 8MB 随机写入所花费的时间中有 90% 用于写入。

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

于 2013-07-08T18:45:05.607 回答