Windows fsync(FlushFileBuffers)性能与大文件

从确保数据在磁盘上的信息( http://winntfs.com/2012/11/29/windows-write-caching-part-2-an-overview-for-application-developers/ ),甚至在例如停电,看起来在Windows平台上,您需要依靠其“fsync”版本FlushFileBuffers来最好地保证缓冲区实际上从磁盘设备caching刷新到存储介质本身。 FILE_FLAG_NO_BUFFERINGFILE_FLAG_WRITE_THROUGH的组合不能确保刷新设备高速caching,而只是对文件系统高速caching产生影响(如果此信息正确)。

鉴于我将处理相当大的文件,需要更新“事务”,这意味着在事务提交结束时执行“fsync”。 所以我创build了一个小应用程序来testing这样做的performance。 它基本上使用8次写入来执行一批8个内存页大小的随机字节的顺序写入,然后刷新。 批处理循环重复,每写完这么多页面,它就会logging下性能。 另外它有两个可configuration的选项:fsync在flush上,以及是否在开始页面写入之前写一个字节到文件的最后位置。

 // Code updated to reflect new results as discussed in answer below. // 26/Aug/2013: Code updated again to reflect results as discussed in follow up question. // 28/Aug/2012: Increased file stream buffer to ensure 8 page flushes. class Program { static void Main(string[] args) { BenchSequentialWrites(reuseExistingFile:false); } public static void BenchSequentialWrites(bool reuseExistingFile = false) { Tuple<string, bool, bool, bool, bool>[] scenarios = new Tuple<string, bool, bool, bool, bool>[] { // output csv, fsync?, fill end?, write through?, mem map? Tuple.Create("timing FS-EBF.csv", true, false, false, false), Tuple.Create("timing NS-EBF.csv", false, false, false, false), Tuple.Create("timing FS-LB-BF.csv", true, true, false, false), Tuple.Create("timing NS-LB-BF.csv", false, true, false, false), Tuple.Create("timing FS-E-WT-F.csv", true, false, true, false), Tuple.Create("timing NS-E-WT-F.csv", false, false, true, false), Tuple.Create("timing FS-LB-WT-F.csv", true, true, true, false), Tuple.Create("timing NS-LB-WT-F.csv", false, true, true, false), Tuple.Create("timing FS-EB-MM.csv", true, false, false, true), Tuple.Create("timing NS-EB-MM.csv", false, false, false, true), Tuple.Create("timing FS-LB-B-MM.csv", true, true, false, true), Tuple.Create("timing NS-LB-B-MM.csv", false, true, false, true), Tuple.Create("timing FS-E-WT-MM.csv", true, false, true, true), Tuple.Create("timing NS-E-WT-MM.csv", false, false, true, true), Tuple.Create("timing FS-LB-WT-MM.csv", true, true, true, true), Tuple.Create("timing NS-LB-WT-MM.csv", false, true, true, true), }; foreach (var scenario in scenarios) { Console.WriteLine("{0,-12} {1,-16} {2,-16} {3,-16} {4:F2}", "Total pages", "Interval pages", "Total time", "Interval time", "MB/s"); CollectGarbage(); var timingResults = SequentialWriteTest("test.data", !reuseExistingFile, fillEnd: scenario.Item3, nPages: 200 * 1000, fSync: scenario.Item2, writeThrough: scenario.Item4, writeToMemMap: scenario.Item5); using (var report = File.CreateText(scenario.Item1)) { report.WriteLine("Total pages,Interval pages,Total bytes,Interval bytes,Total time,Interval time,MB/s"); foreach (var entry in timingResults) { Console.WriteLine("{0,-12} {1,-16} {2,-16} {3,-16} {4:F2}", entry.Item1, entry.Item2, entry.Item5, entry.Item6, entry.Item7); report.WriteLine("{0},{1},{2},{3},{4},{5},{6}", entry.Item1, entry.Item2, entry.Item3, entry.Item4, entry.Item5.TotalSeconds, entry.Item6.TotalSeconds, entry.Item7); } } } } public unsafe static IEnumerable<Tuple<long, long, long, long, TimeSpan, TimeSpan, double>> SequentialWriteTest( string fileName, bool createNewFile, bool fillEnd, long nPages, bool fSync = true, bool writeThrough = false, bool writeToMemMap = false, long pageSize = 4096) { // create or open file and if requested fill in its last byte. var fileMode = createNewFile ? FileMode.Create : FileMode.OpenOrCreate; using (var tmpFile = new FileStream(fileName, fileMode, FileAccess.ReadWrite, FileShare.ReadWrite, (int)pageSize)) { Console.WriteLine("Opening temp file with mode {0}{1}", fileMode, fillEnd ? " and writing last byte." : "."); tmpFile.SetLength(nPages * pageSize); if (fillEnd) { tmpFile.Position = tmpFile.Length - 1; tmpFile.WriteByte(1); tmpFile.Position = 0; tmpFile.Flush(true); } } // Make sure any flushing / activity has completed System.Threading.Thread.Sleep(TimeSpan.FromMinutes(1)); System.Threading.Thread.SpinWait(50); // warm up. var buf = new byte[pageSize]; new Random().NextBytes(buf); var ms = new System.IO.MemoryStream(buf); var stopwatch = new System.Diagnostics.Stopwatch(); var timings = new List<Tuple<long, long, long, long, TimeSpan, TimeSpan, double>>(); var pageTimingInterval = 8 * 2000; var prevPages = 0L; var prevElapsed = TimeSpan.FromMilliseconds(0); // Open file const FileOptions NoBuffering = ((FileOptions)0x20000000); var options = writeThrough ? (FileOptions.WriteThrough | NoBuffering) : FileOptions.None; using (var file = new FileStream(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, (int)(16 *pageSize), options)) { stopwatch.Start(); if (writeToMemMap) { // write pages through memory map. using (var mmf = MemoryMappedFile.CreateFromFile(file, Guid.NewGuid().ToString(), file.Length, MemoryMappedFileAccess.ReadWrite, null, HandleInheritability.None, true)) using (var accessor = mmf.CreateViewAccessor(0, file.Length, MemoryMappedFileAccess.ReadWrite)) { byte* base_ptr = null; accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref base_ptr); var offset = 0L; for (long i = 0; i < nPages / 8; i++) { using (var memStream = new UnmanagedMemoryStream(base_ptr + offset, 8 * pageSize, 8 * pageSize, FileAccess.ReadWrite)) { for (int j = 0; j < 8; j++) { ms.CopyTo(memStream); ms.Position = 0; } } FlushViewOfFile((IntPtr)(base_ptr + offset), (int)(8 * pageSize)); offset += 8 * pageSize; if (fSync) FlushFileBuffers(file.SafeFileHandle); if (((i + 1) * 8) % pageTimingInterval == 0) timings.Add(Report(stopwatch.Elapsed, ref prevElapsed, (i + 1) * 8, ref prevPages, pageSize)); } accessor.SafeMemoryMappedViewHandle.ReleasePointer(); } } else { for (long i = 0; i < nPages / 8; i++) { for (int j = 0; j < 8; j++) { ms.CopyTo(file); ms.Position = 0; } file.Flush(fSync); if (((i + 1) * 8) % pageTimingInterval == 0) timings.Add(Report(stopwatch.Elapsed, ref prevElapsed, (i + 1) * 8, ref prevPages, pageSize)); } } } timings.Add(Report(stopwatch.Elapsed, ref prevElapsed, nPages, ref prevPages, pageSize)); return timings; } private static Tuple<long, long, long, long, TimeSpan, TimeSpan, double> Report(TimeSpan elapsed, ref TimeSpan prevElapsed, long curPages, ref long prevPages, long pageSize) { var intervalPages = curPages - prevPages; var intervalElapsed = elapsed - prevElapsed; var intervalPageSize = intervalPages * pageSize; var mbps = (intervalPageSize / (1024.0 * 1024.0)) / intervalElapsed.TotalSeconds; prevElapsed = elapsed; prevPages = curPages; return Tuple.Create(curPages, intervalPages, curPages * pageSize, intervalPageSize, elapsed, intervalElapsed, mbps); } private static void CollectGarbage() { GC.Collect(); GC.WaitForPendingFinalizers(); System.Threading.Thread.Sleep(200); GC.Collect(); GC.WaitForPendingFinalizers(); System.Threading.Thread.SpinWait(10); } [DllImport("kernel32.dll", SetLastError = true)] static extern bool FlushViewOfFile( IntPtr lpBaseAddress, int dwNumBytesToFlush); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] static extern bool FlushFileBuffers(SafeFileHandle hFile); } 

我所获得的性能结果(64位Win 7,慢主轴磁盘)并不令人鼓舞。 看起来,“fsync”的性能很大程度上取决于正在刷新的文件的大小,这样就占据了时间,而不是“脏”的数据被刷新的数量。 下面的图表显示了小基准应用程序的4个不同设置选项的结果。

4个场景的基准时间

正如你所看到的,随着文件的增长,“fsync”的性能会呈指数下降(直到几GB时才会停止)。 另外,磁盘本身似乎没有做太多(即资源监视器显示其活动时间仅为百分之几,其磁盘队列大部分时间大部分为空)。

我明显地预料到“fsync”的性能比做普通的缓冲刷新要差一些,但是我期望它的性能差不多和文件大小无关。 像这样,似乎表明它不能与单个大文件结合使用。

有人有解释,不同的经验或不同的解决scheme,可以确保数据在磁盘上,并有一个或多或less不变,可预测的性能?

更新在下面的答案中看到新的信息。

您的测试显示同步运行速度呈指数下降,因为您每次都在重新创建文件。 在这种情况下,它不再是纯粹的顺序写入 – 每一次写入也会增长文件,这需要多次查找来更新文件系统中的文件元数据。 如果您使用预先存在的完全分配的文件运行所有这些作业,则会看到更快的结果,因为这些元数据更新都不会造成干扰。

我在Linux上运行了一个类似的测试。 每次重新创建文件时的结果:

 mmap direct last sync time 0 0 0 0 0.882293s 0 0 0 1 27.050636s 0 0 1 0 0.832495s 0 0 1 1 26.966625s 0 1 0 0 5.775266s 0 1 0 1 22.063392s 0 1 1 0 5.265739s 0 1 1 1 24.203251s 1 0 0 0 1.031684s 1 0 0 1 28.244678s 1 0 1 0 1.031888s 1 0 1 1 29.540660s 1 1 0 0 1.032883s 1 1 0 1 29.408005s 1 1 1 0 1.035110s 1 1 1 1 28.948555s 

使用预先存在的文件的结果(显然,last_byte情况在这里是不相关的,而且,第一个结果也必须创建文件):

 mmap direct last sync time 0 0 0 0 1.199310s 0 0 0 1 7.858803s 0 0 1 0 0.184925s 0 0 1 1 8.320572s 0 1 0 0 4.047780s 0 1 0 1 4.066993s 0 1 1 0 4.042564s 0 1 1 1 4.307159s 1 0 0 0 3.596712s 1 0 0 1 8.284428s 1 0 1 0 0.242584s 1 0 1 1 8.070947s 1 1 0 0 0.240500s 1 1 0 1 8.213450s 1 1 1 0 0.240922s 1 1 1 1 8.265024s 

(请注意,我只使用了10000块而不是25000块,所以这只使用ext2文件系统写入320MB。我没有更大的ext2fs方便,我更大的fs是XFS,它拒绝允许mmap +直接I / O 。)

这里是代码,如果你感兴趣:

 #define _GNU_SOURCE 1 #include <malloc.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <sys/types.h> #include <sys/time.h> #include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #define USE_MMAP 8 #define USE_DIRECT 4 #define USE_LAST 2 #define USE_SYNC 1 #define PAGE 4096 #define CHUNK (8*PAGE) #define NCHUNKS 10000 #define STATI 1000 #define FSIZE (NCHUNKS*CHUNK) main() { int i, j, fd, rc, stc; char *data = valloc(CHUNK); char *map, *dst; char sfname[8]; struct timeval start, end, stats[NCHUNKS/STATI+1]; FILE *sfile; printf("mmap\tdirect\tlast\tsync\ttime\n"); for (i=0; i<16; i++) { int oflag = O_CREAT|O_RDWR|O_TRUNC; if (i & USE_DIRECT) oflag |= O_DIRECT; fd = open("dummy", oflag, 0666); ftruncate(fd, FSIZE); if (i & USE_LAST) { lseek(fd, 0, SEEK_END); write(fd, data, 1); lseek(fd, 0, SEEK_SET); } if (i & USE_MMAP) { map = mmap(NULL, FSIZE, PROT_WRITE, MAP_SHARED, fd, 0); if (map == (char *)-1L) { perror("mmap"); exit(1); } dst = map; } sprintf(sfname, "%x.csv", i); sfile = fopen(sfname, "w"); stc = 1; printf("%d\t%d\t%d\t%d\t", (i&USE_MMAP)!=0, (i&USE_DIRECT)!=0, (i&USE_LAST)!=0, i&USE_SYNC); fflush(stdout); gettimeofday(&start, NULL); stats[0] = start; for (j = 1; j<=NCHUNKS; j++) { if (i & USE_MMAP) { memcpy(dst, data, CHUNK); if (i & USE_SYNC) msync(dst, CHUNK, MS_SYNC); dst += CHUNK; } else { write(fd, data, CHUNK); if (i & USE_SYNC) fdatasync(fd); } if (!(j % STATI)) { gettimeofday(&end, NULL); stats[stc++] = end; } } end.tv_usec -= start.tv_usec; if (end.tv_usec < 0) { end.tv_sec--; end.tv_usec += 1000000; } end.tv_sec -= start.tv_sec; printf(" %d.%06ds\n", (int)end.tv_sec, (int)end.tv_usec); if (i & USE_MMAP) munmap(map, FSIZE); close(fd); for (j=NCHUNKS/STATI; j>0; j--) { stats[j].tv_usec -= stats[j-1].tv_usec; if (stats[j].tv_usec < 0) { stats[j].tv_sec--; stats[j].tv_usec+= 1000000; } stats[j].tv_sec -= stats[j-1].tv_sec; } for (j=1; j<=NCHUNKS/STATI; j++) fprintf(sfile, "%d\t%d.%06d\n", j*STATI*CHUNK, (int)stats[j].tv_sec, (int)stats[j].tv_usec); fclose(sfile); } } 

这里是我的synctest代码的Windows版本。 我只在VirtualBox vm中运行它,所以我不认为我有任何有用的数字比较,但你可以给它一个镜头来比较你的机器上的C#号码。 我传递OPEN_ALWAYS到CreateFile,所以它会重用现有的文件。 如果每次都想用空文件再次测试,请将该标志更改为CREATE_ALWAYS。

有一件事我注意到,第一次运行这个程序的结果要快得多。 也许NTFS在覆盖现有数据方面效率不高,后续运行时会出现文件碎片效应。

 #include <windows.h> #include <stdio.h> #define USE_MMAP 8 #define USE_DIRECT 4 #define USE_LAST 2 #define USE_SYNC 1 #define PAGE 4096 #define CHUNK (8*PAGE) #define NCHUNKS 10000 #define STATI 1000 #define FSIZE (NCHUNKS*CHUNK) static LARGE_INTEGER cFreq; int gettimeofday(struct timeval *tv, void *unused) { LARGE_INTEGER count; if (!cFreq.QuadPart) { QueryPerformanceFrequency(&cFreq); } QueryPerformanceCounter(&count); tv->tv_sec = count.QuadPart / cFreq.QuadPart; count.QuadPart %= cFreq.QuadPart; count.QuadPart *= 1000000; tv->tv_usec = count.QuadPart / cFreq.QuadPart; return 0; } main() { int i, j, rc, stc; HANDLE fd; char *data = _aligned_malloc(CHUNK, PAGE); char *map, *dst; char sfname[8]; struct timeval start, end, stats[NCHUNKS/STATI+1]; FILE *sfile; DWORD len; printf("mmap\tdirect\tlast\tsync\ttime\n"); for (i=0; i<16; i++) { int oflag = FILE_ATTRIBUTE_NORMAL; if (i & USE_DIRECT) oflag |= FILE_FLAG_NO_BUFFERING|FILE_FLAG_WRITE_THROUGH; fd = CreateFile("dummy", GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, oflag, NULL); SetFilePointer(fd, FSIZE, NULL, FILE_BEGIN); SetEndOfFile(fd); if (i & USE_LAST) WriteFile(fd, data, 1, &len, NULL); SetFilePointer(fd, 0, NULL, FILE_BEGIN); if (i & USE_MMAP) { HANDLE mh; mh = CreateFileMapping(fd, NULL, PAGE_READWRITE, 0, FSIZE, NULL); map = MapViewOfFile(mh, FILE_MAP_WRITE, 0, 0, FSIZE); CloseHandle(mh); dst = map; } sprintf(sfname, "%x.csv", i); sfile = fopen(sfname, "w"); stc = 1; printf("%d\t%d\t%d\t%d\t", (i&USE_MMAP)!=0, (i&USE_DIRECT)!=0, (i&USE_LAST)!=0, i&USE_SYNC); fflush(stdout); gettimeofday(&start, NULL); stats[0] = start; for (j = 1; j<=NCHUNKS; j++) { if (i & USE_MMAP) { memcpy(dst, data, CHUNK); FlushViewOfFile(dst, CHUNK); dst += CHUNK; } else { WriteFile(fd, data, CHUNK, &len, NULL); } if (i & USE_SYNC) FlushFileBuffers(fd); if (!(j % STATI)) { gettimeofday(&end, NULL); stats[stc++] = end; } } end.tv_usec -= start.tv_usec; if (end.tv_usec < 0) { end.tv_sec--; end.tv_usec += 1000000; } end.tv_sec -= start.tv_sec; printf(" %d.%06ds\n", (int)end.tv_sec, (int)end.tv_usec); if (i & USE_MMAP) UnmapViewOfFile(map); CloseHandle(fd); for (j=NCHUNKS/STATI; j>0; j--) { stats[j].tv_usec -= stats[j-1].tv_usec; if (stats[j].tv_usec < 0) { stats[j].tv_sec--; stats[j].tv_usec+= 1000000; } stats[j].tv_sec -= stats[j-1].tv_sec; } for (j=1; j<=NCHUNKS/STATI; j++) fprintf(sfile, "%d\t%d.%06d\n", j*STATI*CHUNK, (int)stats[j].tv_sec, (int)stats[j].tv_usec); fclose(sfile); } } 

我已经做了更多的尝试,并找到了一个可能被我接受的解决方案(尽管目前我只测试了顺序写入)。 在这个过程中,我发现了一些意想不到的行为,引发了一些新的问题。 我将发布一个新的SO问题( 解释/所寻求的信息:Windows为“fsync”(FlushFileBuffers)写入I / O性能 )。

我在基准测试中增加了以下两个选项:

  • 使用无缓冲/写入写入(即指定FILE_FLAG_NO_BUFFERINGFILE_FLAG_WRITE_THROUGH标志)
  • 通过内存映射文件间接写入文件。

这给我提供了一些意想不到的结果,其中一个给了我一个或多或少的可接受的解决方案,我的问题。 当“fsyncing”结合无缓冲/写入I / O时,我不会观察到写入速度的指数衰减。 因此(虽然速度不是很快),这为我提供了一个解决方案,可以确保数据在磁盘上,并且具有不受文件大小影响的可预测的性能。

其他一些意想不到的结果如下:

  • 如果一个字节写入文件的最后一个位置,在执行使用“fsync”和“unbuffered / writeethrough”选项的页写入之前,写入吞吐量几乎翻番。
  • 使用或不使用fsync的未缓冲/写入的性能几乎完全相同,除非将一个字节写入文件的最后一个位置。 空文件上没有“fsync”的“无缓冲/写”场景的写入吞吐量大约为12.5 MB / s,而在同一场景中,在文件的最后一个位置写入字节的情况下,吞吐量为三倍于37 MB / s。
  • 即使在文件上设置了“无缓冲/写入”,通过内存映射文件与“fsync”结合间接写入文件也会显示与直接写入文件的缓冲写入相同的指数吞吐量下降。

我已将用于基准测试的更新代码添加到我的原始问题中。

下图显示了一些额外的新结果。

吞吐量为不同的选项组合

[错误; 看评论。]

我相信你所引用的文章是不正确的说明FlushFileBuffers对未缓冲的I / O有任何有用的影响。 它指的是微软的一篇论文,但是这篇论文没有提到这样的说法。

根据文档,使用未缓冲的I / O与每次写入后调用FlushFileBuffer效果相同,但效率更高。 所以实际的解决方案是使用无缓冲的I / O而不是使用FlushFileBuffer。

但是请注意,使用内存映射文件会破坏缓冲设置。 如果您尝试尽快将数据推送到磁盘,我不建议使用内存映射文件。