你如何在 Java 中写入磁盘(使用刷新)并保持性能?

Posted

技术标签:

【中文标题】你如何在 Java 中写入磁盘(使用刷新)并保持性能?【英文标题】:How do you write to disk (with flushing) in Java and maintain performance? 【发布时间】:2012-10-29 10:50:41 【问题描述】:

使用以下代码作为基准,系统可以在几分之一秒内将 10,000 行写入磁盘:

void withSync() 
    int f = open( "/tmp/t8" , O_RDWR | O_CREAT );
    lseek (f, 0, SEEK_SET );
    int records = 10*1000;
    clock_t ustart = clock();
    for(int i = 0; i < records; i++) 
        write(f, "012345678901234567890123456789" , 30);
        fsync(f);
    
    clock_t uend = clock();
    close (f);
    printf("   sync() seconds:%lf   writes per second:%lf\n", ((double)(uend-ustart))/(CLOCKS_PER_SEC), ((double)records)/((double)(uend-ustart))/(CLOCKS_PER_SEC));

在上面的代码中,10,000 条记录可以在几分之一秒内写入并刷新到磁盘,输出如下:

sync() seconds:0.006268   writes per second:0.000002

在 Java 版本中,写入 10,000 条记录需要 4 秒以上。这只是 Java 的限制,还是我遗漏了什么?

public void testFileChannel() throws IOException 
    RandomAccessFile raf = new RandomAccessFile(new File("/tmp/t5"),"rw");
    FileChannel c = raf.getChannel();
    c.force(true);
    ByteBuffer b = ByteBuffer.allocateDirect(64*1024);
    long s = System.currentTimeMillis();
    for(int i=0;i<10000;i++)            
        b.clear();
        b.put("012345678901234567890123456789".getBytes());
        b.flip();
        c.write(b);
                    c.force(false);
    
    long e=System.currentTimeMillis();
    raf.close();
    System.out.println("With flush "+(e-s));


返回这个:

With flush 4263

请帮助我了解用 Java 将记录写入磁盘的正确/最快方法是什么。

注意:我将RandomAccessFile 类与ByteBuffer 结合使用,因为最终我们需要对该文件进行随机读/写访问。

【问题讨论】:

您的比较不公平。您正在使用 ByteBuffer 并在 Java 版本中调用 .getBytes() 。如果您的想法是测试应用程序的性能,那么这没关系。但是与 C 相比,这是不公平的,因为您在做不同的事情。 这很公平。使用 ByteBuffer 和 .getBytes 实际上比在 Java 中以任何其他方式执行它更快(至少在我的机器上的测试中)。如果您对如何在 Java 中进行随机访问有其他建议,我很乐意听取他们的意见。谢谢! 【参考方案1】:

实际上,我很惊讶测试并没有变慢。 force 的行为取决于操作系统,但从广义上讲,它会将数据强制写入磁盘。如果您有 SSD,您可能会实现每秒 40K 的写入,但使用 HDD 则不会。在 C 示例中,它显然没有将数据提交到磁盘,因为即使是最快的 SSD 也无法执行超过 235K IOPS(制造商保证它不会比这更快:D)

如果您每次都需要将数据提交到磁盘,您可以预期它会很慢并且完全取决于您的硬件速度。如果您只需要将数据刷新到操作系统,并且如果程序崩溃但操作系统没有,您不会丢失任何数据,您可以无需强制写入数据。更快的选择是使用内存映射文件。这将为您提供随机访问,而无需对每条记录进行系统调用。

我有一个库Java Chronicle,它每秒可以读取/写入 5-20 百万条记录,延迟为 80 ns,采用随机访问的文本或二进制格式,并且可以在进程之间共享。这只工作这么快,因为它不会在每条记录上将数据提交到磁盘,但您可以测试如果 JVM 在任何时候崩溃,写入编年史的数据不会丢失。

【讨论】:

我希望刷新将缓冲区推送到操作系统。如果没有脸红,它可能会一次缓冲几行。 您的建议很有意义!我的极客想找到一种方法来肯定地确认这一点......也许一些涉及断开电源线的测试(: 尝试轮询文件大小,看看它增长了多少倍。 如果必须将数据提交到磁盘,那么您需要同步系统调用:linux.die.net/man/2/sync 来自 OS/X fsync(2) man page:“对于需要更严格保证其数据完整性的应用程序,Mac OS X 提供了 F_FULLFSYNC fcntl。F_FULLFSYNC fcntl 要求驱动器将所有缓冲数据刷新到永久存储。需要严格的写入顺序的应用程序(例如数据库)应使用 F_FULLF-SYNC 以确保其数据按预期顺序写入。有关更多详细信息,请参阅 fcntl(2)。 【参考方案2】:

这段代码更类似于您用 C 编写的代码。在我的机器上只需要 5 毫秒。如果您确实需要在每次写入后刷新,则大约需要 60 毫秒。您的原始代码在这台机器上花费了大约 11 秒。顺便说一句,关闭输出流也会刷新。

public static void testFileOutputStream() throws IOException 
  OutputStream os = new BufferedOutputStream( new FileOutputStream( "/tmp/fos" ) );
  byte[] bytes = "012345678901234567890123456789".getBytes();
  long s = System.nanoTime();
  for ( int i = 0; i < 10000; i++ ) 
    os.write( bytes );
  
  long e = System.nanoTime();
  os.close();
  System.out.println( "outputstream " + ( e - s ) / 1e6 );

【讨论】:

关闭刷新会使上面的代码在我的机器上执行大约0.15秒(:我们权利的软件需要能够保证当它说数据被保存时,它真的被保存了。跨度> 所以,在刷新的情况下,它仍然只有 60 毫秒...顺便说一句,fflush 实际上并没有写入磁盘。 C 版本的 fsync 时间如何?当您删除 BufferedOutputStream 装饰时,fsync 类似于 os.getFD().sync()。不过同步确实很慢:测试需要 6 秒。 无论如何,此方法不支持随机文件访问。使用 fsync 不会显着降低 C 代码的速度。 @Jacob 来自 fsync 的手册页:Note that while fsync() will flush all data from the host to the drive (i.e. the "permanent storage device"), the drive itself may not physically write the data to the platters for quite some time and it may be written in an out-of-order sequence. 您需要使用 F_FULLFSYNC 调用 fcntl 才能确定。【参考方案3】:

fputs 的 Java 等价物是 file.write("012345678901234567890123456789"); ,您正在调用 4 个函数,而 C 中只有 1 个,延迟似乎很明显

【讨论】:

这不是它慢 5 个数量级的原因。还有其他原因导致速度大幅下降 感谢您的回复,但我的测试表明使用write() 然后flush() 或其他DirectFileAccess 方法稍微慢一些。无论哪种方式,我们都在谈论受磁盘限制而不是 CPU 限制的东西。我找不到比这更快的 java 代码了。 @dave: 虚拟机 vs 编译 ;) @DavidRF:好吧,如果这是您的观点,请将其放入您的答案中。尽管我仍然不认为您的平均 JVM 是减速 5 个数量级的原因。 Java 程序比 C 慢 10,000 倍?快告诉全世界不要再写一行Java了!!!要么,要么你错了。我会选择后者(因为奥卡姆剃刀)。 但实际上,将 10,000 条记录写入随机访问文件需要 4 秒与 0.001 秒?慢了 4,000 倍! java真的那么差吗?【参考方案4】:

我认为这与您的 C 版本最相似。我认为您的 java 示例中的直接缓冲区导致的缓冲区副本比 C 版本多得多。这在我的(旧)盒子上大约需要 2.2 秒。

  public static void testFileChannelSimple() throws IOException 
    RandomAccessFile raf = new RandomAccessFile(new File("/tmp/t5"),"rw");
    FileChannel c = raf.getChannel();
    c.force(true);
    byte[] bytes = "012345678901234567890123456789".getBytes();
    long s = System.currentTimeMillis();
    for(int i=0;i<10000;i++)
      raf.write(bytes);
      c.force(true);
    
    long e=System.currentTimeMillis();
    raf.close();
    System.out.println("With flush "+(e-s));
  

【讨论】:

以上是关于你如何在 Java 中写入磁盘(使用刷新)并保持性能?的主要内容,如果未能解决你的问题,请参考以下文章

从 Windows CLI 刷新磁盘写入缓存

刷新磁盘写缓存

如何定期将 c# FileStream 刷新到磁盘?

File.WriteAllText 不将数据刷新到磁盘

C 语言文件操作 ( fflush 函数 | 刷新缓冲区示例代码 )

如何强制事件跟踪会话更频繁地刷新数据?