如何在Java中写入磁盘(并刷新),同时保持性能?

6
使用以下代码作为基准,系统可以在瞬间将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相结合,最终需要在这个文件上进行随机读/写访问。

你的比较不公平。在Java版本中,你使用了ByteBuffer并调用了.getBytes()方法。如果你的想法是为了测试应用程序的性能,那么这是可以的。但是与C语言进行比较是不公平的,因为你正在做不同的事情。 - dave
1
这是非常公平的。在我的机器上,使用ByteBuffer和.getBytes实际上比以任何其他方式在Java中执行更快(至少在我的测试中)。如果您对如何在Java中进行随机访问有其他建议,我非常乐意听取。谢谢! - Jay
4个回答

5

实际上,我很惊讶测试结果并没有变慢。force的行为取决于操作系统,但它通常会强制将数据写入磁盘。如果你使用的是SSD硬盘,你可能可以实现每秒4万次写入,但是如果使用的是HDD硬盘,就无法实现这个速度。在C语言示例中,显然没有将数据提交到磁盘上,因为即使是最快的SSD也不能执行超过235K IOPS操作(厂商保证不会更快:D)。

如果你需要每次都将数据写入磁盘,那么速度会很慢,完全取决于硬件的速度。如果你只需要将数据刷新到操作系统中,并且程序崩溃而操作系统没有崩溃,你就不会失去任何数据,你可以写入数据而不使用force。更快的选项是使用内存映射文件。这将给你随机访问,而不需要为每个记录调用系统调用。

我有一个库Java Chronicle,可以以文本或二进制格式读写500万到2000万条记录,具有80纳秒的延迟和随机访问,并且可在多个进程之间共享。这之所以能够如此快速,是因为它不会在每个记录上都将数据提交到磁盘,但是你可以测试JVM的崩溃点,如果发生崩溃,Chronicle中写入的任何数据都不会丢失。


1
我期望flush将缓冲区推送到操作系统。如果没有刷新,它可能会一次缓冲几行。 - Peter Lawrey
1
你的建议非常有道理!内心的极客想要找到一种确定的方法...也许可以进行一些涉及断开电源线的测试 (: - Jay
1
尝试轮询文件大小并查看它以何种倍数增长。 - Peter Lawrey
2
如果数据必须提交到磁盘,则需要使用同步系统调用:http://linux.die.net/man/2/sync - dave
2
从OS/X fsync(2) man page中得知:“对于需要更严格保证数据完整性的应用程序,Mac OS X提供了F_FULLFSYNC fcntl。 F_FULLFSYNC fcntl要求驱动器将所有缓冲数据刷新到永久存储器。 需要严格写入顺序的应用程序(例如数据库)应使用F_FULLFSYNC以确保其数据按照预期顺序编写。 有关详细信息,请参见fcntl(2)。” - David Moles
显示剩余4条评论

1

这段代码更类似于您用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秒。我们编写的软件需要能够确保当它说数据已保存时,确实已保存。 - Jay
所以,使用flushing后,仍然只有60毫秒...顺便说一下,fflush实际上并没有写入磁盘。对于C版本,您使用fsync的时间是多少?当您删除BufferedOutputStream修饰时,fsync类似于os.getFD().sync()。但同步非常慢:在这里测试需要6秒钟。 - jackrabbit
无论哪种方式,这种方法都不支持随机文件访问。使用fsync不会显著减慢C代码的速度。 - Jay
@Jacob 从fsync的manpage中可以看到:请注意,虽然fsync()将刷新主机上的所有数据到驱动器(即“永久存储设备”),但驱动器本身可能不会立即将数据物理写入盘片,并且可能以无序序列进行写入。 您需要调用带有F_FULLFSYNC的fcntl来确保。 - jackrabbit

0

我认为这个版本最接近你的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));
  }

0
Java 中 fputs 的等效方法是 file.write("012345678901234567890123456789");,你调用了 4 个函数而 C 中只需调用 1 个,延迟显而易见。

这不是它慢了五个数量级的原因。还有其他的因素导致了巨大的减速。 - dave
感谢您回复,但我的测试表明使用write()然后是 flush() 或其他 DirectFileAccess 方法稍微慢一些。不管怎样,我们讨论的是磁盘绑定而非 CPU 绑定的内容。我找不到比这更快的 Java 代码了。 - Jay
1
@dave:虚拟机 vs 编译 - David Ranieri
1
@DavidRF:如果这是你的观点,请在你的答案中表达出来。尽管我仍然认为你平均的JVM不是导致5个数量级的减速的原因。Java程序比C慢10,000倍?快告诉全世界不要再写Java了!!!要么你错了,要么我错了。我选择后者(因为奥卡姆剃刀原理)。 - dave
但是,将10,000条记录写入随机访问文件需要4秒钟与0.001秒钟的时间,这真的相差如此之大吗?这慢了4,000倍!Java真的那么糟糕吗? - Jay
我提问的原因是希望被证明是错误的。我希望能够使用Java编写一类依赖于快速磁盘性能的应用程序。我只是有点震惊于性能差异如此之大。 - Jay

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接