作者:eaglet
引用請注明出處
寫文件后調用 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去寫入文件並且每次寫入文件的內容都較小,這時確實會有影響。因為每次寫入都會觸發一次物理的磁盤寫。