原文:https://www.cnblogs.com/eaglet/archive/2012/09/13/2682951.html
写文件后调用 FileStream.Close; FileStream.Flush; 或者 using (FileStream fs = new FileStream(…)) {} ,文件是否被实际写入了磁盘?可能大多数人都会说肯定会写入磁盘,但我要告诉你,不一定!
背景
我所在的公司有上千台的计算机在同时运行我们的系统,在实际运行过程中,我们发现有时候我们写入的文件会出现全0或者部分全0的情况,但程序中可以肯定的是我们已经关闭了文件句柄。这个问题困扰了我很久。它的发生概率大概在几千分之一,而且大部分是出现在机器重启时,也就是我们更新软件后要求机器自动重启,结果起来后发现更新的软件中有部分文件的大小是对的,但数据全是0。
问题分析
首先考虑的是不是程序的bug,但分析下来,程序没有任何问题,我们甚至用了 File.WriteAllBytes 这样的静态函数来写文件,依然会出现这个问题。
其次考虑的是不是磁盘缓存造成?因为windows操作系统每个磁盘上都可以设置 Enable write caching on the device. 如果打开这个开关,写入操作将先写入磁盘的缓存,然后在到达大小或时间门限时才写入物理磁盘。如果内容还没有完全写入磁盘就重启计算机,就会造成数据丢失。设置如下图所示:
于是把这个功能取消,结果发现问题依旧。
这到底是怎么回事?
放狗搜索后发现 windows 除了在磁盘的硬件级别上可以提供缓存外,在操作系统层面也有一个文件缓存
这个功能叫 File Caching, 是 windows 2000 以后提供的功能。如下图所示:
当进程写磁盘时,文件会根据一定的策略缓存到系统的文件缓存中,达到一定门限后才会写入物理磁盘。由于这个系统文件缓存对应用程序是透明的,我们在应用程序中调用 文件的 Close, Flush 只能保证文件已经被写入了操作系统的文件缓存,但无法保证文件实际被写入了磁盘。这个机制虽然提供了较好的写入性能,但却增加了丢失数据的风险。从应用角度,我们从逻辑上认为写入已经成功,但实际上并没有写入到实际的磁盘,也就是说写入是否真的成功了,软件无从知道,这样带来很多逻辑上的混乱。特别是一些服务进程利用文件锁来控制多个进程锁定的,比如 lucene.net, mongodb 等,就经常出现重启后文件锁锁定出问题的情况,估计也和这个机制的作用有关。
那么这个机制的优点到底在哪里呢?
微软提供这个机制当然是有原因的,他的最大优点是大大提高了读取的性能。我们可以做如下的实验:
当我们打开一个大文件,并顺序读取这个文件,我们发现系统开机后,第一次读取的速度是非常慢的,这个速度主要取决于磁盘的读取速度,因为第一次读取是没有缓存的。但当我们关闭进程,再重新运行进程读取这个大文件时,无论是顺序读取还是随机读取,都比原来快上百倍,这就是因为这个操作系统缓存在里面起了作用,数据是从内存读取的。由于这个缓存是全局的,进程退出后,文件的缓存并没有被清空。我所做的开源全文索引项目 HubbleDotNet 的较新版本就充分利用了这个机制,大大提高了机器重启后首次读取索引的速度。
关于windows操作系统的文件缓存机制以及如何优化不在本文的讨论范围内,我将在以后的文章中专门来讲述这个机制是如何工作的。
解决方案
那么回到这个问题
我们有没有办法关闭这个文件缓存呢?答案是否定的。但幸运的是 windows 为应用提供了一个标志叫 FILE_FLAG_WRITE_THROUGH, 这个标志可以让应用在写入缓存的同时直接写入磁盘。
用 C# 实现的代码如下:
using (System.IO.FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None,
8192, FileOptions.WriteThrough))
在程序中做了如上更改后,2000多台机器运行了半年,没有发现一次文件数据丢失的问题(原来几乎每个月会出现几次),基本可以证明这个机制有效。
深入问题:
1. 采用 WriteThrough 后没有关闭磁盘缓存,会不会造成数据丢失?
我们再看一下本文最上面的那个图,磁盘缓存有两个 CheckBox, 第一个是是否打开磁盘缓存,第二个是是否关闭windows 的文件写缓存刷新磁盘。如果第二个选中,则有可能会出现数据丢失,如果没有选中则不会。
默认设置,这个地方是不选中的。从实际测试来看,磁盘缓存也确实不影响数据丢失的问题。
2. 采用 WriteThrough 后是否会降低写入磁盘的性能。
我认为如果是随机写,可能是会有影响的,但如果是顺序写,FileStream 这个类已经提供了缓存功能,并不会有太大的影响。除非你直接调用windows 的文件API去写入文件并且每次写入文件的内容都较小,这时确实会有影响。因为每次写入都会触发一次物理的磁盘写。