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

Posted

技术标签:

【中文标题】在 Java 中真正强制文件同步/刷新【英文标题】:Really force file sync/flush in Java 【发布时间】:2010-10-18 08:21:05 【问题描述】:

写入文件的数据真正如何通过 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 引用物理存储介质,例如文件系统中的文件,则在与此 FileDesecriptor 关联的缓冲区的所有内存中修改副本都已写入物理介质之前,同步将不会返回。同步旨在供需要物理存储(例如文件)处于已知状态的代码使用。

这两个调用应该足够了。是吗?我猜他们不是。

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

导致 70 MB/s 的 C 代码是(使用低级 API(打开、写入、关闭)没有太大变化):

FILE* fp = fopen(filename, "w");
while(xyz) 
    fwrite(buffer, 1, BLOCK_SIZE, fp);

fflush(fp);
fclose(fp);
sync();

没有最后的同步调用;我得到了不切实际的值(超过 1 GB 又名主内存性能)。

为什么 C 和 Java 之间有这么大的区别?有两种可能性:我没有在 Java 中正确同步数据,或者 C 代码由于某种原因不是最理想的。

更新: 我已经使用“strace -cfT cmd”完成了 strace 运行。结果如下:

C(低级 API): MB/s 67.389782

% time seconds usecs/call 调用错误 syscall ------ ----------- ------------ --------- --------- ---- ------------ 87.21 0.200012 200012 1 fdatasync 11.05 0.025345 1 32772 写 1.74 0.004000 4000 1 同步

C(高级 API): MB/s 61.796458

% time seconds usecs/call 调用错误 syscall ------ ----------- ------------ --------- --------- ---- ------------ 73.19 0.144009 144009 1 同步 26.81 0.052739 1 65539 ​​写

Java(1.6 SUN JRE,java.io API): MB/s 128.6755466197537

% time seconds usecs/call 调用错误 syscall ------ ----------- ------------ --------- --------- ---- ------------ 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。

【问题讨论】:

我真的很想看看只使用第 2 节函数的吞吐量。 BLOCK_SIZE 使用的是什么?它与 Java 中的缓冲区大小相同吗?这些天,512 将非常不理想。您可能需要至少 4096(x86 上的页面大小)或更高。我在某些机器上看到了高达 32k 的可衡量的改进。哦,当然,如果你的缓冲区是页面对齐的,它会给内核更多的优化空间。 另一个可能的问题是您发布的代码没有使用“低级 API(打开、写入、关闭)”。它使用更高级别的可移植 stdio API(fopen、fwrite、fclose),默认情况下会添加额外的缓冲层。您是否在您发布的代码之外的某处明确关闭了缓冲? 【参考方案1】:

实际上,在 C 语言中,您只想在一个文件描述符上调用 fsync(),而不是 sync()(或“同步”命令)向内核发送信号 flush 所有缓冲区到磁盘系统范围内。

如果您使用strace(在此处获取特定于 Linux)的 JVM,您应该能够观察到对您的输出文件进行的 fsync()fdatasync() 系统调用。这就是我希望getFD().sync() 调用所做的事情。我假设c.force(true) 只是向NIO 标记fsync() 应该在每次写入后调用。可能只是因为您使用的 JVM 实际上没有实现 sync() 调用?

我不确定为什么在调用“sync”作为命令时没有看到任何区别:但显然,在第一次同步调用之后,后续调用通常要快得多。同样,我倾向于将strace(Solaris 上的桁架)作为“这里实际发生的事情?”工具。

【讨论】:

跟踪系统调用的想法很好。我明天会做。 force() 调用 fsync 或 fdatasync(取决于元数据标志)。但是,它没有设置在每次调用后直接调用 fsync/fdatasync 的状态。我在 OpenJDK 源代码中查到了。【参考方案2】:

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

如果要将单个文件的块写入磁盘,则需要在 C 中使用 fsync(2)fdatasync(2)。顺便说一句:当您在 C 中使用缓冲 stdio 时(或 BufferedOutputStream 或 Java 中的某些 Writer)在同步之前,您需要先刷新两者。

如果文件在您同步后没有更改名称或大小,则fdatasync() 变体会更有效。但它也可能不会保留所有元数据。如果你想编写自己的事务安全数据库系统,你需要观察更多的东西(比如 fsyncing 父目录)。

【讨论】:

【参考方案3】:

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

您是正确的,强制/同步应该将数据强制输出到物理媒体。


这是原始版本的副本。在 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 over FiberChannel from a JBOD。 我使用 64K 块相同的随机数据编写了一个 4 GB 的文件,并测量了文件打开和文件关闭之间的时间(如果完成则同步)。 还有其他工作负载吗? C 与 GCC > 4?该配置类似于我在 STK (RIP) 上尝试过的配置,120 MB/s 听起来很合理。 是的,GCC 4.3.2。我计划接下来评估随机 io 并将 python 和 Erlang 添加到评估语言列表中。 我切换到 Suns JRE 1.6.0,但行为非常相似。【参考方案4】:

(我知道这是一个很晚的回复,但我在谷歌搜索时遇到了这个帖子,这可能也是你最终来到这里的原因。)

您在 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.

【讨论】:

syncfs() 并不比 sync() 好,两者都是错误的。 fdatasync() 调用是 java 使用的调用,也是您要在 C 中使用的调用。【参考方案5】:

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

无论如何,您只能信任 APIDOC。其余的不在你的职责范围内。

【讨论】:

不,编程不仅仅是遵循文档并对其他所有内容说“哦,好吧,不是我的工作”。

以上是关于在 Java 中真正强制文件同步/刷新的主要内容,如果未能解决你的问题,请参考以下文章

避免“资源与文件系统不同步”

JavaWeb 项目,更改本地文件需刷新才有效问题 (tomcat相关)

强制刷新gradle依赖缓存的方法

文件系统不同步

关于线程同步的一些问题

游戏开发中——垂直同步绘制效率显示器刷新频率与帧率