在Java中真正强制文件同步/刷新

42
如何使用Java将写入文件的数据真正刷新/同步到块设备上?
我尝试了使用NIO的以下代码:
FileOutputStream s = new FileOutputStream(filename)
Channel c = s.getChannel()
while(xyz)
    c.write(buffer)
c.force(true)
s.getFD().sync()
c.close()

我认为c.force(true)和s.getFD().sync()应该足够了,因为force的文档说明如下:
强制将此通道文件的任何更新写入包含它的存储设备。
如果此通道文件驻留在本地存储设备上,则当此方法返回时,保证自创建此通道以来或自上次调用此方法以来对文件所做的所有更改都已写入该设备。这对于确保在系统崩溃时不会丢失关键信息非常有用。 sync的文档说明如下:
强制所有系统缓冲区与底层设备同步。此方法在所有已修改的数据和此FileDescriptor的属性已写入相关设备后返回。特别是,如果此FileDescriptor引用物理存储介质(例如文件系统中的文件),则只有在与此FileDescriptor关联的所有内存中修改的缓冲区副本都被写入物理介质后,sync才会返回。sync适用于需要将物理存储(如文件)置于已知状态的代码。

这两个调用足够了吗?我猜它们不够。

背景:我使用C / Java进行小型性能比较(2 GB,顺序写入),Java版本比C版本快两倍,并且可能比硬件更快(单个HD上的120 MB / s)。我还尝试使用Runtime.getRuntime()。exec(“sync”)执行命令行工具sync,但这并没有改变行为。

导致70 MB / s的C代码是(使用低级API(open,write,close)并不会改变太多):

FILE* fp = fopen(filename, "w");
while(xyz) {
    fwrite(buffer, 1, BLOCK_SIZE, fp);
}
fflush(fp);
fclose(fp);
sync();

没有对sync的最后一次调用时,我得到了不切实际的值(超过1 GB即主存储性能)。
为什么C和Java之间有这么大的差异? 有两种可能性:在Java中,我没有正确地同步数据,或者C代码出于某些原因是次优的。
更新: 我使用“strace -cfT cmd”运行了strace。以下是结果:
C(低级API): MB / s 67.389782
%时间秒usecs / call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 87.21 0.200012 200012 1 fdatasync 11.05 0.025345 1 32772 write 1.74 0.004000 4000 1 sync
C(高级API): MB / s 61.796458
%时间秒usecs / call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 73.19 0.144009 144009 1 sync 26.81 0.052739 1 65539 write Java (1.6 SUN JRE, java.io API): MB/s 128.6755466197537
% 时间 秒数 每次使用微秒 调用次数 错误系统调用 ------ ----------- ----------- --------- --------- ---------------- 80.07 105.387609 3215 32776 写入 2.58 3.390060 3201 1059 读取 0.62 0.815251 815251 1 同步文件
Java (1.6 SUN JRE, java.nio API): MB/s 127.45830221558376
5.52 0.980061 490031 2 同步文件 1.60 0.284752 9 32774 写入 0.00 0.000000 0 80 关闭
时间值似乎只是系统时间,因此毫无意义。
更新2: 我换了另一台服务器,重新启动,并使用新格式化的ext3。现在Java和C之间只有4%的差异。我不知道出了什么问题。有时候事情很奇怪。在写这个问题之前,我应该尝试在另一个系统上进行测量。抱歉。
更新3: 总结答案如下:
  • 对于Java NIO,请使用c.force(true)后跟s.getFD()。sync(),对于Java的流API,请使用s.flush()和s.getFD()。sync()。对于C中的高级API,请不要忘记同步。fflush将数据提交给操作系统,但不会将您的数据带到块设备。
  • 使用strace分析命令执行的系统调用
  • 在发布问题之前,请交叉检查您的结果。
更新4: 请注意以下后续问题 question

1
我真的很想仅使用第2节功能来查看吞吐量。 - Charlie Martin
你在使用什么 BLOCK_SIZE?它与你在Java中的缓冲区大小相同吗?在当今,512 尺寸会非常次优。你可能至少想要 4096(x86 上的页面大小)或者可能更高。我在某些机器上看到了可测量的改进,最高可达 32k。哦,当然,如果你的缓冲区与页面对齐,将为内核提供更多优化的空间。 - aij
另一个可能的问题是你发布的代码没有使用“低级API(open,write,close)”。它正在使用更高级别的可移植stdio API(fopen,fwrite,fclose),默认情况下会添加额外的缓冲层。你是否在发布的代码之外明确关闭了缓冲? - aij
5个回答

12

实际上,在C语言中,您只需要在一个文件描述符上调用fsync(),而不是调用sync()(或者"同步"命令),后者会向内核发出信号,强制将所有缓冲区刷新到整个磁盘系统。

如果使用strace(这里限制在Linux平台),可以观察到JVM在您的输出文件上执行的fsync()fdatasync()系统调用。我期望这就是getFD().sync()调用所要做的事情。我认为c.force(true) 仅仅是告诉NIO在每次写入后都调用fsync()。可能是因为您正在使用的JVM实际上并没有实现sync()调用?

我不确定为什么您在调用"同步"命令时没有看到任何差异:但显然,在第一次调用之后,后续的调用通常会快得多。同样,我倾向于将strace(在Solaris上为truss)作为“这里实际发生了什么?”工具进行分析。


跟踪系统调用的想法很好。我明天会去做。 - dmeister
1
force() 调用 fsync 或 fdatasync(取决于元数据标志)。但是,它不会设置状态以在每次调用后直接调用 fsync/fdatasync。我在 OpenJDK 源代码中查找了它。 - dmeister

5

使用同步的I/O数据完整性完成是个好主意。但是你的C示例程序使用了错误的方法。你使用了sync(),它用于同步整个操作系统。

如果你想将单个文件的块写入磁盘,你需要在C中使用fsync(2)fdatasync(2)。顺便说一句:当你在C中使用带缓冲的stdio(或Java中的BufferedOutputStream或某个Writer)时,你需要先刷新缓冲区再进行同步。

fdatasync()变体比较高效,如果该文件自同步以来未更改名称或大小。但是它可能也无法保留所有元数据。如果你想编写自己的事务安全数据库系统,你需要注意一些其他问题(如同步父目录)。


1

您需要告诉我们更多关于硬件和操作系统的信息,以及具体的Java版本。您是如何测量吞吐量的?

您的确是正确的,force / sync应该将数据强制输出到物理介质。


这是一份原始版本的拷贝。在Intel Mac上使用gcc 4.0编译,应该很干净。
/* rawcopy -- pure C, system calls only, copy argv[1] to argv[2] */

/* This is a test program which simply copies from file to file using
 * only system calls (section 2 of the manual.)
 *
 * Compile:
 *
 *      gcc -Wall -DBUFSIZ=1024 -o rawcopy rawcopy.c
 *
 * If DIRTY is defined, then errors are interpreted with perror(3).
 * This is ifdef'd so that the CLEAN version is free of stdio.  For
 * convenience I'm using BUFSIZ from stdio.h; to compile CLEAN just
 * use the value from your stdio.h in place of 1024 above.
 *
 * Compile DIRTY:
 *
 *      gcc -DDIRTY -Wall -o rawcopy rawcopy.c
 *
 */
#include <fcntl.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <stdlib.h>
#include <unistd.h>
#if defined(DIRTY)
#   if defined(BUFSIZ)
#       error "Don't define your own BUFSIZ when DIRTY"
#   endif
#   include <stdio.h>
#   define PERROR perror(argv[0])
#else
#   define CLEAN
#   define PERROR
#   if ! defined(BUFSIZ)
#       error "You must define your own BUFSIZ with -DBUFSIZ=<number>"
#   endif
#endif

char * buffer[BUFSIZ];          /* by definition stdio BUFSIZ should
                                   be optimal size for read/write */

extern int errno ;              /* I/O errors */

int main(int argc, char * argv[]) {
    int fdi, fdo ;              /* Input/output file descriptors */
    ssize_t len ;               /* length to read/write */
    if(argc != 3){
        PERROR;
        exit(errno);
    }

    /* Open the files, returning perror errno as the exit value if fails. */
    if((fdi = open(argv[1],O_RDONLY)) == -1){
        PERROR;
        exit(errno);
    }
    if((fdo = open(argv[2], O_WRONLY|O_CREAT)) == -1){
        PERROR;
        exit(errno);
    }

    /* copy BUFSIZ bytes (or total read on last block) fast as you
       can. */
    while((len = read(fdi, (void *) buffer, BUFSIZ)) > -1){
        if(len == -1){
            PERROR;
            exit(errno);
        }
        if(write(fdo, (void*)buffer, len) == -1){
            PERROR;
            exit(errno);
        }
    }
    /* close and fsync the files */
    if(fsync(fdo) ==-1){
        PERROR;
        exit(errno);
    }
    if(close(fdo) == -1){
        PERROR;
        exit(errno);
    }
    if(close(fdi) == -1){
        PERROR;
        exit(errno);
    }

    /* if it survived to here, all worked. */
    exit(0);
}

IcedTea OpenJDK 1.6 Java,openSUSE 11 Linux,4 Core-CPU,4 GB,1 SATA-HD通过FiberChannel从JBOD传输。 - dmeister
我使用相同的随机数据,以64K块的大小写了一个4 GB的文件,并测量了文件打开和关闭之间的时间(如果进行同步,则也测量同步时间)。 - dmeister
还有其他的工作量吗?这个C是用GCC > 4编译的吗?这个配置和我在STK(已经过时)尝试过的一个相似,120 MB/s听起来很可信。 - Charlie Martin
是的,GCC 4.3.2。我计划接下来评估随机 IO,并将 Python 和 Erlang 添加到已评估语言的列表中。 - dmeister
我已经切换到Sun的JRE 1.6.0,但行为非常相似。 - dmeister

0

我知道这是一个非常晚的回复,但我在做谷歌搜索时遇到了这个线程,这也可能是你来到这里的原因。

在Java中,您调用单个文件描述符上的sync(),因此只有与该文件相关的缓冲区才会刷新到磁盘。

在C和命令行中,您正在对整个操作系统调用sync() - 因此,每个文件缓冲区都会被刷新到磁盘,适用于您的操作系统正在执行的所有内容。

为了进行比较,C调用应该是syncfs(fp);

来自Linux手册页面:

   sync() causes all buffered modifications to file metadata and data to
   be written to the underlying file systems.

   syncfs() is like sync(), but synchronizes just the file system contain‐
   ing file referred to by the open file descriptor fd.

2
syncfs()并不比sync()更好,两者都是错误的。fdatasync()调用是Java使用的调用,也是您想在C中使用的调用。 - eckes

-1

这段 C 代码可能不够优化,因为它使用了 stdio 而不是原始的操作系统 write()。但是,Java 可能更加优化,因为它分配了更大的缓冲区?

无论如何,你只能相信 APIDOC。其他的超出了你的职责范围。


不,编程并不仅仅是遵循文档并对其他所有事情说“哦,这不是我的工作”。 - Glenn Maynard

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