【问题标题】:Optimizing Java File I/O for High-Volume Log Files为大容量日志文件优化 Java 文件 I/O
【发布时间】:2013-07-08 02:40:34
【问题描述】:

当我想编写将文本写入文件的 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 的隐藏宝石”中的哪个孩子?几乎任何事情都会发生,除了,就像我说的,使用其他日志框架。基本的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 方法?

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

【问题讨论】:

  • 也许尝试使用 BufferedOutputStream 而不是 FileWriter。您可能希望将写入同步到 BufferedOutputStream。也许将缓冲区大小设置为 8MB 左右,以将磁盘搜索对吞吐量的影响降至最低。
  • 感谢@tmyklebu (+1) - 我对您的声明非常感兴趣“也许将缓冲区大小设置为 8MB 左右,以将磁盘搜索对吞吐量的影响降至最低。" 你能详细说明一下吗?我想说到目前为止这个问题已经得到的所有输入,你在这里的评论是最有帮助的。如果您要做出正式的回答,我可能会给您绿色检查;-)。再次感谢!
  • 观察这里的数字:磁盘寻道大约需要 8 毫秒。您的南桥可能可以将大约 1GB/s 的速度推送到您的磁盘阵列。您的磁盘阵列可能比这慢,它可能有更好的事情要做;特别是,如果您谈论的是单个磁盘,您将获得大约 100MB/s。 8ms * 1GB/s 告诉您进行磁盘查找所花费的时间可能已经花费在写入 8MB 的内容上,或者用于单个磁盘的 800k。如果您在 LAN 上使用闪存或阵列,那么这种眼光就完全消失了,因为延迟和吞吐量数字是不同的。
  • 再次感谢@tmyklebu(我希望我能给你更多的支持!) - 请看看我的代码更新 - 我应该如何同步日志方法(使用ByteOutputStream),或者有没有更好的办法?此外,ByteOutputStream 似乎已经是线程安全的(每种方法) - 所以我什至需要尝试在其上添加自己的同步吗?为什么/为什么不?再次感谢!
  • 最大的胜利是从无缓冲到有缓冲的 I/O。提前进行 char -> byte 转换比较小。我不熟悉你的 ByteOutputStream。

标签: java performance optimization logging file-io


【解决方案1】:

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

【讨论】:

  • 那么您对应该使用多大的缓冲区以及类似的东西有疑问。由于额外的线程争用 CPU 时间,进行各种不必要的处理器间通信,以及可能在糟糕的时间进行 I/O,因此您必须平衡此处感知的吞吐量增益与系统中其他任何地方的吞吐量损失。 (尽管它可能很诱人,但你不能忽视无限的内存消耗问题。因为当程序耗尽内存并死掉时,它永远不会好。)
【解决方案2】:

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

【讨论】:

  • 砰的一声。但最终,您需要同时满足系统的功能要求和性能要求。有时这意味着您必须在其中一项上做出妥协,或者激怒您的用户。
【解决方案3】:

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

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

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

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

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

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

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

如果您保留Strings 的队列,一旦达到一定大小,您可以比Buffered 类做得更好。这样可以节省内存副本,但我很怀疑这样做是否值得。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-04-12
    • 2018-05-20
    • 1970-01-01
    • 2017-02-27
    • 1970-01-01
    • 1970-01-01
    • 2013-12-16
    相关资源
    最近更新 更多