Dalle informazioni sull'assicurazione dei dati sul disco (http://winntfs.com/2012/11/29/windows-write-caching-part-2-an-overview-for-application-developers/), anche nel caso ad es. un'interruzione dell'alimentazione, sembra che su piattaforme Windows è necessario fare affidamento sulla sua versione "fsync" FlushFileBuffers
per avere la migliore garanzia che i buffer siano effettivamente scaricati dalle cache del dispositivo disco sul supporto di memorizzazione stesso. La combinazione di FILE_FLAG_NO_BUFFERING
con FILE_FLAG_WRITE_THROUGH
non garantisce lo svuotamento della cache del dispositivo, ma ha semplicemente un effetto sulla cache del file system, se queste informazioni sono corrette.Prestazioni di Windows fsync (FlushFileBuffers) con file di grandi dimensioni
Dato che lavorerò con file piuttosto grandi, che devono essere aggiornati "a livello transazionale", ciò significa eseguire un "fsync" alla fine di un commit di transazione. Così ho creato una piccola app per testare le prestazioni nel farlo. Esegue fondamentalmente scritture sequenziali di un lotto di 8 byte casuali di dimensioni di pagina di memoria utilizzando 8 scritture e quindi esegue il flushing. Il batch viene ripetuto in un ciclo e, dopo ogni numero di pagine scritte, registra le prestazioni. Inoltre ha due opzioni configurabili: fsync su un flush e se scrivere un byte nell'ultima posizione del file, prima di iniziare la scrittura della pagina.
// 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);
}
I risultati prestazionali sto ottenendo (64 bit Windows 7, disco a rotazione lenta) non sono molto incoraggianti. Sembra che le prestazioni di "fsync" dipendano molto dalle dimensioni del file che viene svuotato, in modo tale da dominare il tempo e non dalla quantità di dati "sporchi" da svuotare. Il grafico sottostante mostra i risultati per le 4 diverse opzioni di impostazione della piccola applicazione benchmark.
Come si può vedere, le prestazioni di "fsync" in modo esponenziale diminuisce il file cresce (fino a qualche GB macina davvero una battuta d'arresto). Inoltre, il disco stesso sembra non occuparsi molto (ad esempio, il monitor delle risorse mostra il suo tempo attivo solo attorno a qualche percento, e la sua coda di dischi è quasi sempre vuota per la maggior parte del tempo).
Mi aspettavo ovviamente che le prestazioni di "fsync" fossero un po 'peggiori rispetto ai normali flush buffer, ma mi aspettavo che fosse più o meno costante e indipendente dalla dimensione del file. In questo modo sembrerebbe suggerire che non è utilizzabile in combinazione con un singolo file di grandi dimensioni.
Qualcuno ha una spiegazione, esperienze diverse o una soluzione diversa che consente di garantire che i dati siano su disco e che abbiano una prestazione più o meno costante e prevedibile?
AGGIORNATO Vedere le nuove informazioni nella risposta di seguito.
Da quello che ho capito l'articolo è corretto. Anche confermato ad es. qui: http://support.microsoft.com/kb/332023 (vedere la sezione "Ulteriori informazioni"). 'FlushFileBuffers' si traduce nel comando' SYNCHRONIZE CACHE' per i dispositivi SCSI e nel comando 'FLUSH CACHE' per i dispositivi IDE/ATAPI. 'Write Through' è rilasciato come' ForceUnitAccess', che apparentemente non è onorato dai dispositivi IDE/ITAPI. Credo che questo sia anche ciò che viene menzionato nell'articolo di riferimento ftp://ftp.research.microsoft.com/pub/tr/TR-2008-36.pdf. – Alex
@alex questi articoli parlano di una cache all'interno del disco/controller, non del livello del SO. Quando si usa writethrough, dovrebbe averlo fatto su "nella cache del disco", ma in realtà non è "sul disco". Per test come la tua Q, questa cache potrebbe spiegare il declino delle aspettative al crescere del tuo volume di IO. Questa cache è un grosso problema per i database quando queste cache "nel disco" falliscono in condizioni powerfail prima di essere scritte in un settore, vedere http://support.microsoft.com/kb/234656 – rlb
Non riesco a trovare nulla in quel carta che dice qualcosa sull'API Win32. Tuttavia l'articolo KB certamente lo fa! –