快速讀寫檔案
written by Alex Stocks on 2019/05/05,版權所有,無授權不得轉載
4月初讀到 PolarDB 開發團隊的一篇文章《how to write file faster》 ,受教頗多,現拾人牙慧成就本文,以示致敬!
1 快速寫檔案
對檔案的操作一般區分為讀寫兩種動作。如果檔案特指為 SATA 磁碟檔案,檔案寫操作其實就是 append 操作,檔案讀操作則有順序和隨機兩種。
個人在2014年時對 7200轉 的 SATA 磁碟檔案的讀寫操作有兩個經驗資料:優化後的寫速度可達 150MB/s,順序讀可達 800 MB/s。一般情況下,隨機讀速度高於寫速度,對檔案寫速度的優化難於對其讀速度的優化。本章主要描述對磁碟檔案寫流程的優化。
1.1 記憶體對齊與雙緩衝
大概五年前吾人寫有《如何快速的把日誌輸出到磁碟上》
一文,其中write faster
的關鍵之處在於:合併寫輸出,待輸出內容為 4096 Bytes 時再呼叫系統 API 以 append 方式輸出至磁碟。正如此文所述,這種方法其實借鑑自 muduo 的 log 系統。
muduo 的 log 系統還給出了進一步的優化:預先申請兩個 buffer,以減少執行緒輸出日誌爭搶 buffer 時的等待【等待系統分配 buffer 記憶體空間】時間,頗類似於早期 VGA 顯示卡加速時採用的雙緩衝技術。
用俗話總結這種優化手段就是:寫檔案時用到的記憶體資源在寫之前預先申請好,不要在輸出內容時有等待時間;log 內容輸出至磁碟時把記憶體與磁碟對齊,且以 append 的方式進行順序輸出。
本節所用到的技巧其實僅僅在於如何高效使用記憶體,並未更進一步地述及如何加快操作磁碟檔案寫流程【根本原因在於當時水平太渣^_^】。
1.2 fdatasync
linux 系統會在核心記憶體空間為磁碟檔案其分配一個核心緩衝區,有人稱其為 “核心態記憶體區”。既然存在檔案的 “核心態” 緩衝區,自然應該有一個 “使用者態” 緩衝區。
記得 2011 年在深圳某家公司幹活時,老大 Randy Ling 給了一本 APUE 作為見面禮,扉頁上有這麼一句話:使用 open 函式開啟 log 檔案時其 flag 引數應該加上O_SYNC | O_DIRECT
,以保證系統掉電時不丟失 log 內容。據說老大之所以加上這麼一句話,是因為當時的華為 SSD 故障率實在是太高了。使用 linux open 函式開啟檔案時,檔案系統只有一個 “核心態” 緩衝區,如果 linux open 函式的 flag 有O_DIRECT
引數,則對檔案進行讀寫時會繞過這個緩衝區,使用者對檔案的讀寫操作會直接作用於磁碟。
如果呼叫 linux fopen 函式開啟檔案,則對檔案的讀寫會經 “使用者態” 緩衝區 和 “核心態” 緩衝區 而後作用於磁碟。考慮如今的硬體系統健壯性與軟體系統穩定性,一般情況下使用 fread/fwrite 之類函式足以保證資料一致性,但是不排除使用者程式有 bug。為減少程式 bug 對檔案資料安全性【資料丟失風險】的影響,一般的程式會在呼叫 fwrite 之後,再呼叫一次 fsync 保證資料被重新整理至磁碟。
linux 系統每個檔案都有一個 inode 區和 data 區,分別儲存檔案的 metadata 和 data,呼叫一次 fsync 會產生兩次寫操作:更新檔案的 metadata 和 data。metadata 的更新內容主要有 size/update time等。
在寫日誌檔案這一場景下,一般都要求每個日誌檔案大小一致,如果不關心檔案的 update time 且預先為 log 檔案提前分配了固定 size 的空間,則不需要在寫 log 時更新檔案的 size,每次呼叫 fsync 對 metadata 進行更新就顯得無意義。針對這種場景,linux 專門提供了 fdatasync api 對檔案的 data 區域進行更新。
fdatasync 的意義即為把 fsync 對檔案的磁碟區域的兩次寫減少為一次寫。
1.3 fallocate
上節述及 fdatasync 時,提到預先為 log 檔案提前分配了固定大小的空間
,linux 的 fallocate api 即可實現這一動作。
fallocate 保證系統預先為檔案分配相應的邏輯磁碟空間,保證寫資料時不會產生 “磁碟空間不足” 這個錯誤,但是並未分配相應的物理磁碟空間,所以呼叫 fallocate 僅產生預先為 log 檔案提前標識了相應 size 的空間(extents)
的效果,在寫磁碟檔案的過程中還是會產生系統中斷:linux 系統在中斷過程中為其分配物理磁碟空間。有中斷便有等待時間,等待時間過後才能繼續 “快速寫”。
誠如《how to write file faster》
所述FALLOC_FL_ZERO_RANGE mode 是在核心3.15 版本才引入
,3.15 版本的 linux 系統給 fallocate api 的 mode 引數添加了一個ALLOC_FL_ZERO_RANGE
選項,其作用是對相應 size 的邏輯磁碟空間進行filling zero
操作,其效果是 linux 提前為磁碟檔案分配相應的磁碟空間,這段磁碟空間對磁碟讀操作不可見,所以有文章稱這段空間為 "hole"[檔案空洞]。檔案空洞的一個好處是避免在寫檔案時因 linux 尚未為邏輯空間分配對應的物理磁碟空間導致的中斷等待,另一個好處是固定檔案 metadata 的 file size,避免寫過程中因為需要更新這個引數而產生的雙寫行為。
這種filling zero
操作頗類似於對 linux bzero api 的效果:為一段邏輯記憶體空間提前分配對應的實體記憶體空間,避免在寫記憶體時產生中斷。
1.4 檔案複用
《how to write file faster》
一文還提到另一個優化通過後臺執行緒提前建立檔案並且filling zero 從而達到高效的寫入
。
linux 系統建立檔案時需要向檔案系統申請檔案資源,如欲實現檔案 “快速寫”,這個等待時間也是很可觀的,所以類似於第一節的寫檔案時用到的記憶體資源在寫之前預先申請好
優化手段,這種行為即是寫檔案時用到的檔案資源在寫之前預先申請好
。
2 快速讀檔案
優化檔案讀取速度的最基本手段即是順序讀
,其原理在於 linux 系統讀取檔案資料時會提前對檔案進行預讀,減少讀資料時的缺頁中斷。
linux 系統有預讀行為,但預讀資料量則是使用者所不知道的。linux 提供了一個叫做 readahead 的 api,使用者通過這個 api 可以控制系統的預讀行為。
具體實踐中,readahead 可以配合 mmap 函式一起使用以加快資料讀取速度。
參考文件
扒糞者-於雨氏
2019/05/05,於雨氏,於 G44,初作此文。