Производительность Windows fsync (FlushFileBuffers) с большими файлами
Из информации о том, что данные находятся на диске ( http://winntfs.com/2012/11/29/windows-write-caching-part-2-an-overview-for-application-developers/), даже в случае например, при отключении питания, кажется, что на платформах Windows вам нужно полагаться на его версию "fsync" FlushFileBuffers
иметь лучшую гарантию того, что буферы фактически сбрасываются из кэшей дисковых устройств на сам носитель данных. Сочетание FILE_FLAG_NO_BUFFERING
с FILE_FLAG_WRITE_THROUGH
не гарантирует очистку кеша устройства, а просто влияет на кеш файловой системы, если эта информация верна.
Учитывая тот факт, что я буду работать с довольно большими файлами, которые необходимо обновить "транзакционно", это означает выполнение "fsync" в конце фиксации транзакции. Поэтому я создал крошечное приложение, чтобы проверить производительность при этом. Он в основном выполняет последовательную запись пакета из 8 случайных байтов размером страницы памяти, используя 8 записей, а затем сбрасывает. Пакет повторяется в цикле, и после каждого такого количества записанных страниц записывается производительность. Кроме того, у него есть два настраиваемых параметра: выполнить fsync при сбросе и записать ли байт в последнюю позицию файла перед началом записи страницы.
// 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-E-B-F.csv", true, false, false, false),
Tuple.Create("timing NS-E-B-F.csv", false, false, false, false),
Tuple.Create("timing FS-LB-B-F.csv", true, true, false, false),
Tuple.Create("timing NS-LB-B-F.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-E-B-MM.csv", true, false, false, true),
Tuple.Create("timing NS-E-B-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 различных параметров настройки небольшого тестового приложения.
Как вы можете видеть, производительность "fsync" экспоненциально уменьшается по мере роста файла (пока на нескольких ГБ он действительно не остановится). Кроме того, сам диск, кажется, не делает много (т.е. монитор ресурсов показывает свое активное время всего лишь около нескольких процентов, а его очередь на диске в большинстве случаев пуста).
Я, очевидно, ожидал, что производительность "fsync" будет немного хуже, чем при выполнении обычных буферизованных сбросов, но я ожидал, что она будет более или менее постоянной и независимой от размера файла. Таким образом, можно предположить, что его нельзя использовать в сочетании с одним большим файлом.
Есть ли у кого-то объяснение, другой опыт или другое решение, которое позволяет гарантировать, что данные находятся на диске и которое имеет более или менее постоянную, предсказуемую производительность?
ОБНОВЛЕНО Смотрите новую информацию в ответе ниже.
4 ответа
Ваш тест показывает экспоненциальное снижение скорости при выполнении синхронизации, потому что вы каждый раз воссоздаете файл. В этом случае это больше не чисто последовательная запись - каждая запись также увеличивает файл, что требует многократных попыток обновления метаданных файла в файловой системе. Если бы вы выполнили все эти задания с использованием уже существующего полностью выделенного файла, вы бы увидели намного более быстрый результат, потому что ни одно из этих обновлений метаданных не будет мешать.
Я запустил аналогичный тест на моем компьютере с 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 чанков, а не 25 000 чанков, так что это только запись 320 МБ с использованием файловой системы ext2. У меня не было под рукой большего ext2fs, мой больший fs - XFS, и он отказался разрешить mmap+ прямой ввод / вывод.)
Вот код, если вам интересно:
#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);
}
}
Вот версия моего кода синхронизации для 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)) для тех.
Я добавил следующие два дополнительных параметра в свой тест:
- Используйте небуферизованные / записывающие записи (т.е. указав
FILE_FLAG_NO_BUFFERING
а такжеFILE_FLAG_WRITE_THROUGH
флаги) - Напишите в файл косвенно, через отображенный файл памяти.
Это дало мне некоторые неожиданные результаты, один из которых дает мне более или менее приемлемое решение моей проблемы. Когда "fsyncing" в сочетании с небуферизованным / записывающим вводом / выводом, я не наблюдаю экспоненциального затухания скорости записи. Таким образом (хотя это и не очень быстро) это дает мне решение, которое позволяет гарантировать, что данные находятся на диске, и которое имеет постоянную предсказуемую производительность, на которую не влияет размер файла.
Несколько других неожиданных результатов были следующие:
- Если байт записан в последнюю позицию в файле, перед выполнением записи страницы с опциями "fsync" и "unbuffered/writeethrough" пропускная способность записи почти удваивается.
- Производительность небуферизованного / перезаписи с или без fsync практически идентична, за исключением случаев, когда байт был записан в последнюю позицию в файле. Пропускная способность записи сценария "без буферизации / записи" без "fsync" в пустом файле составляет около 12,5 МБ / с, тогда как в том же сценарии для файла, в котором в последней позиции файла записан байт, пропускная способность равна трем. раз выше при 37 МБ / с.
- Непосредственная запись в файл через отображенный в память файл в сочетании с "fsync" демонстрирует такое же экспоненциальное снижение пропускной способности, которое наблюдается при буферизованной записи непосредственно в файл, даже если для файла задано "unbuffered/writeethrough".
Я добавил обновленный код, который использовал для теста, к своему первоначальному вопросу.
На графике ниже показаны некоторые дополнительные новые результаты.
[Неправильно; смотрите комментарии.]
Я считаю, что статья, на которую вы ссылаетесь, неверна, утверждая, что FlushFileBuffers оказывает какое-либо полезное влияние на небуферизованный ввод-вывод. Это относится к документу Microsoft, но данный документ не претендует на такую претензию.
Согласно документации, использование небуферизованного ввода-вывода имеет тот же эффект, что и более эффективно, чем вызов FlushFileBuffer после каждой записи. Поэтому практическим решением является использование небуферизованного ввода-вывода, а не FlushFileBuffer.
Тем не менее, обратите внимание, что использование отображенного в память файла нарушает настройки буферизации. Я бы не рекомендовал использовать файл с отображенной памятью, если вы пытаетесь записать данные на диск как можно скорее.