持久數據存儲和 Linux 文件 API

我,研究雲系統中數據存儲的穩定性,決定測試自己,以確保我了解基本的東西。 我 從閱讀 NVMe 規範開始 為了理解有關數據持久性的保證(即保證數據在系統故障後可用),請給我們 NMVe 磁盤。 我得出了以下主要結論:您需要考慮從給出數據寫入命令的那一刻起,直到將它們寫入存儲介質的那一刻起,損壞的數據。 然而,在大多數程序中,系統調用用於寫入數據是相當安全的。

在本文中,我探討了 Linux 文件 API 提供的持久性機制。 看來這裡一切都應該很簡單:程序調用命令 write(), 該命令運行完成後,數據將安全地存儲在磁盤上。 但 write() 僅將應用程序數據複製到位於 RAM 中的內核緩存中。 為了強制系統將數據寫入磁盤,必須使用一些額外的機制。

持久數據存儲和 Linux 文件 API

總的來說,這份材料是一組筆記,與我在我感興趣的主題上學到的知識有關。 如果我們非常簡短地談論最重要的,事實證明,為了組織可持續的數據存儲,您需要使用命令 fdatasync() 或打開帶有標誌的文件 O_DSYNC. 如果您有興趣了解更多關於數據在從代碼到磁盤的過程中發生的情況,請查看 文章。

使用 write() 函數的特點

系統調用 write() 標準中定義 IEEE POSIX 作為將數據寫入文件描述符的嘗試。 工作順利完成後 write() 數據讀取操作必須準確返回先前寫入的字節,即使正在從其他進程或線程訪問數據(這裡 POSIX 標準的相應部分)。 這裡,在關於線程與正常文件操作的交互的部分中,有一條註釋說如果兩個線程各自調用這些函數,那麼每個調用都必須看到執行另一個調用導致的所有指示結果,或者看不到任何後果。 這導致了所有文件 I/O 操作必須鎖定正在處理的資源的結論。

這是否意味著該操作 write() 是原子的? 從技術角度來看,是的。 數據讀取操作必須返回全部或不返回任何寫入的內容 write(). 但是手術 write(),按照標準,不必結束,讓她寫下的都寫下了。 只允許寫入部分數據。 例如,我們可能有兩個流,每個流將 1024 字節附加到由相同文件描述符描述的文件。 從標準的角度來看,當每個寫操作只能向文件追加一個字節時,結果是可以接受的。 這些操作將保持原子性,但在它們完成後,它們寫入文件的數據將變得混亂。 這裡 Stack Overflow 上關於此主題的非常有趣的討論。

fsync() 和 fdatasync() 函數

將數據刷新到磁盤的最簡單方法是調用函數 fsync(). 此函數要求操作系統將所有修改的塊從緩存移動到磁盤。 這包括文件的所有元數據(訪問時間、文件修改時間等)。 我相信很少需要這個元數據,所以如果你知道它對你不重要,你可以使用這個功能 fdatasync()。 在 幫助fdatasync() 它說,在這個函數的運行過程中,如此大量的元數據被保存到磁盤,這是“正確執行以下數據讀取操作所必需的”。 而這正是大多數應用程序所關心的。

這裡可能出現的一個問題是這些機制不能保證在可能出現故障後可以找到該文件。 特別是,當創建一個新文件時,應該調用 fsync() 對於包含它的目錄。 否則,崩潰後,可能會發現該文件不存在。 之所以會這樣,是因為在UNIX下,由於使用了硬鏈接,一個文件可以存在於多個目錄中。 因此,調用時 fsync() 文件無法知道哪個目錄數據也應該刷新到磁盤(這裡 您可以閱讀更多相關信息)。 看起來 ext4 文件系統能夠 自動 申請 fsync() 到包含相應文件的目錄,但其他文件系統可能並非如此。

這種機制可以在不同的文件系統中以不同的方式實現。 我用了 黑道 了解 ext4 和 XFS 文件系統中使用了哪些磁盤操作。 兩者都針對文件內容和文件系統日誌向磁盤發出通常的寫入命令,刷新緩存並通過執行 FUA(強制單元訪問,將數據直接寫入磁盤,繞過緩存)寫入日誌退出。 他們這樣做可能只是為了確認交易的事實。 在不支持 FUA 的驅動器上,這會導致兩次緩存刷新。 我的實驗表明 fdatasync() 快一點 fsync(). 公用事業 blktrace 表示 fdatasync() 通常將較少的數據寫入磁盤(在 ext4 中 fsync() 寫入 20 KiB,並且 fdatasync() - 16 KiB)。 另外,我發現 XFS 比 ext4 稍快。 在這裡得到幫助 blktrace 能夠發現 fdatasync() 將較少的數據刷新到磁盤(XFS 中為 4 KiB)。

使用 fsync() 時的模棱兩可的情況

我可以想到三種模棱兩可的情況 fsync()我在實踐中遇到過。

第一次此類事件發生在 2008 年。 當時,如果將大量文件寫入磁盤,Firefox 3 界面會“凍結”。 問題在於接口的實現使用 SQLite 數據庫來存儲有關其狀態的信息。 在界面中發生每次更改後,都會調用該函數 fsync(),為穩定的數據存儲提供了良好的保障。 在當時使用的ext3文件系統中,函數 fsync() 將系統中的所有“臟”頁面刷新到磁盤,而不僅僅是那些與相應文件相關的頁面。 這意味著在 Firefox 中單擊一個按鈕可能會導致數兆字節的數據寫入磁盤,這可能需要很多秒。 據我了解,問題的解決方案 材料,是將與數據庫的工作轉移到異步後台任務。 這意味著 Firefox 過去常常實施比真正必要的更嚴格的存儲持久性要求,而 ext3 文件系統功能只會加劇這個問題。

第二個問題發生在2009年。 然後,在系統崩潰後,新的 ext4 文件系統的用戶發現許多新創建的文件長度為零,但舊的 ext3 文件系統不會發生這種情況。 在上一段中,我談到了 ext3 如何將過多的數據轉儲到磁盤上,這大大降低了速度。 fsync(). 為了改善這種情況,ext4 只刷新那些與特定文件相關的“臟”頁面。 與 ext3 相比,其他文件的數據在內存中保留的時間要長得多。 這樣做是為了提高性能(默認情況下,數據保持這種狀態 30 秒,您可以使用 dirty_expire_centisecs; 這裡 你可以找到更多關於這個的信息)。 這意味著大量數據在崩潰後可能無法挽回地丟失。 這個問題的解決方案是使用 fsync() 在需要提供穩定數據存儲並儘可能保護它們免受故障後果影響的應用程序中。 功能 fsync() 使用 ext4 比使用 ext3 更有效。 這種方法的缺點是,它的使用和以前一樣會減慢某些操作的速度,例如安裝程序。 查看詳情 這裡 и 這裡.

第三個問題關於 fsync(),起源於2018年。 然後,在PostgreSQL項目的框架內,發現如果函數 fsync() 遇到錯誤,它將“臟”頁面標記為“乾淨”。 結果,以下調用 fsync() 對此類頁面不執行任何操作。 因此,修改後的頁面存儲在內存中,永遠不會寫入磁盤。 這是一場真正的災難,因為應用程序會認為某些數據已寫入磁盤,但實際上並不會。 此類故障 fsync() 很少見,在這種情況下的應用程序幾乎無法解決問題。 如今,一旦發生這種情況,PostgreSQL 和其他應用程序就會崩潰。 這裡,在文章“Can Applications Recover from fsync Failures?”中,詳細探討了這個問題。 目前解決這個問題的最好方法是使用帶標誌的直接 I/O O_SYNC 或帶有旗幟 O_DSYNC. 使用這種方法,系統將報告執行特定數據寫入操作時可能發生的錯誤,但這種方法需要應用程序自己管理緩衝區。 閱讀更多相關信息 這裡 и 這裡.

使用 O_SYNC 和 O_DSYNC 標誌打開文件

讓我們回到對提供持久數據存儲的 Linux 機制的討論。 即,我們正在談論使用標誌 O_SYNC 或標誌 O_DSYNC 使用系統調用打開文件時 打開(). 使用這種方法,每個數據寫入操作就像在每個命令之後一樣執行 write() 系統分別給出命令 fsync() и fdatasync()。 在 POSIX規範 這稱為“同步 I/O 文件完整性完成”和“數據完整性完成”。 這種方法的主要優點是只需要執行一個系統調用來確保數據完整性,而不是兩個(例如 - write() и fdatasync()). 這種方法的主要缺點是所有使用相應文件描述符的寫操作都將同步,這會限制構建應用程序代碼的能力。

使用帶 O_DIRECT 標誌的直接 I/O

系統調用 open() 支持國旗 O_DIRECT,其設計目的是繞過操作系統緩存,執行 I/O 操作,直接與磁盤交互。 在許多情況下,這意味著程序發出的寫入命令將直接轉換為旨在使用磁盤的命令。 但是,總的來說,這種機制並不能替代功能 fsync()fdatasync(). 事實是磁盤本身可以 延遲或緩存 用於寫入數據的適當命令。 而且,更糟糕的是,在某些特殊情況下,使用標誌時執行的 I/O 操作 O_DIRECT, 播送 進入傳統的緩衝操作。 解決這個問題最簡單的方法是使用標誌打開文件 O_DSYNC,這意味著每個寫操作之後都會有一個調用 fdatasync().

事實證明,XFS 文件系統最近為 O_DIRECT|O_DSYNC-數據記錄。 如果塊被覆蓋使用 O_DIRECT|O_DSYNC,然後 XFS 將執行 FUA 寫入命令(如果設備支持),而不是刷新緩存。 我使用該實用程序驗證了這一點 blktrace 在 Linux 5.4/Ubuntu 20.04 系統上。 這種方法應該更有效,因為它將最少量的數據寫入磁盤並使用一次操作,而不是兩次(寫入和刷新緩存)。 我找到了一個鏈接 修補 2018內核實現了這個機制。 有一些關於將此優化應用於其他文件系統的討論,但據我所知,XFS 是迄今為止唯一支持它的文件系統。

sync_file_range() 函數

Linux有一個系統調用 同步文件範圍(),它允許您僅將文件的一部分刷新到磁盤,而不是整個文件。 此調用啟動異步刷新並且不等待它完成。 但在參考 sync_file_range() 據說這個命令“非常危險”。 不建議使用它。 特點和危險 sync_file_range() 很好地描述了 材料。 特別是,這個調用似乎使用 RocksDB 來控制內核何時將“臟”數據刷新到磁盤。 但同時那裡,為了保證穩定的數據存儲,還使用 fdatasync()。 在 程式碼 RocksDB 對此主題有一些有趣的評論。 例如,它看起來像調用 sync_file_range() 使用 ZFS 時不會將數據刷新到磁盤。 經驗告訴我,很少使用的代碼可能包含錯誤。 因此,除非絕對必要,否則我建議不要使用此系統調用。

有助於確保數據持久性的系統調用

我得出的結論是,可以使用三種方法來執行持久性 I/O 操作。 它們都需要一個函數調用 fsync() 對於創建文件的目錄。 這些是方法:

  1. 呼叫函數 fdatasync()fsync() 功能後 write() (更好地使用 fdatasync()).
  2. 使用帶有標誌打開的文件描述符 O_DSYNCO_SYNC (更好 - 有一面旗幟 O_DSYNC).
  3. 命令用法 pwritev2() 有旗幟的 RWF_DSYNCRWF_SYNC (最好有旗幟 RWF_DSYNC).

性能說明

我沒有仔細衡量我調查的各種機制的性能。 我注意到他們工作速度的差異非常小。 這意味著我可能是錯的,在其他條件下,同樣的事情可能會顯示不同的結果。 首先,我將討論對性能影響較大的因素,然後再討論對性能影響較小的因素。

  1. 覆蓋文件數據比將數據附加到文件更快(性能提升可以達到 2-100%)。 將數據附加到文件需要對文件的元數據進行額外更改,即使在系統調用之後也是如此 fallocate(), 但這種影響的大小可能會有所不同。 為了獲得最佳性能,我建議致電 fallocate() 預先分配所需的空間。 那麼這個空間必須明確地用零填充並稱為 fsync(). 這將導致文件系統中相應的塊被標記為“已分配”而不是“未分配”。 這提供了一個小的(大約 2%)性能改進。 此外,某些磁盤的第一個塊訪問操作可能比其他磁盤慢。 這意味著用零填充空間可以顯著(大約 100%)性能提升。 特別是,這可能發生在磁盤上。 亞馬遜電子服務系統 (這是非官方數據,我無法證實)。 存儲也是如此。 GCP 永久性磁盤 (這已經是官方信息,已通過測試確認)。 其他專家也做了同樣的事情 觀察與不同的磁盤有關。
  2. 系統調用越少,性能越高(增益可以達到 5% 左右)。 好像是來電 open() 有旗幟的 O_DSYNC 或致電 pwritev2() 有旗幟的 RWF_SYNC 更快的通話 fdatasync(). 我懷疑這裡的重點是,使用這種方法,解決相同任務所需執行的系統調用更少(一次調用而不是兩次)這一事實發揮了作用。 但性能差異非常小,因此您可以輕鬆忽略它並在應用程序中使用不會導致其邏輯複雜化的東西。

如果您對可持續數據存儲感興趣,這裡有一些有用的材料:

  • I/O 訪問方法 — 輸入/輸出機制的基礎概述。
  • 確保數據到達磁盤 - 一個關於數據在從應用程序到磁盤的過程中發生了什麼的故事。
  • 什麼時候應該 fsync 包含的目錄 - 何時申請的問題的答案 fsync() 對於目錄。 簡而言之,事實證明您需要在創建新文件時執行此操作,此建議的原因是在 Linux 中可以有多個對同一文件的引用。
  • Linux 上的 SQL Server:FUA 內部 - 這裡描述瞭如何在Linux平台上的SQL Server中實現持久化數據存儲。 這裡有一些 Windows 和 Linux 系統調用之間的有趣比較。 我幾乎可以肯定,正是由於這份資料,我才了解了 XFS 的 FUA 優化。

您曾經丟失過您認為安全地存儲在磁盤上的數據嗎?

持久數據存儲和 Linux 文件 API

持久數據存儲和 Linux 文件 API

來源: www.habr.com