dm-vdo 設計¶
dm-vdo(虛擬資料最佳化器)目標提供線上重複資料刪除、壓縮、零塊消除和精簡配置。dm-vdo 目標可由高達 256TB 的儲存支援,並可呈現高達 4PB 的邏輯大小。該目標最初由 Permabit Technology Corp. 於 2009 年開始開發。它於 2013 年首次釋出,此後一直用於生產環境。Permabit 被 Red Hat 收購後,於 2017 年將其開源。本文件描述了 dm-vdo 的設計。有關用法,請參閱與此檔案位於同一目錄中的 dm-vdo。
由於重複資料刪除率隨著塊大小的增加而大幅下降,vdo 目標的最大塊大小為 4K。但是,它能實現 254:1 的重複資料刪除率,即一個給定的 4K 塊最多可以有 254 份副本引用一個 4K 的實際儲存空間。它能實現 14:1 的壓縮率。所有零塊完全不佔用儲存空間。
操作原理¶
dm-vdo 的設計基於重複資料刪除是一個兩部分問題。首先是識別重複資料。其次是避免儲存這些重複資料的多個副本。因此,dm-vdo 有兩個主要部分:一個用於發現重複資料的重複資料刪除索引(稱為 UDS),以及一個包含引用計數塊對映的資料儲存,該對映將邏輯塊地址對映到資料的實際儲存位置。
區域和執行緒¶
由於資料最佳化的複雜性,在 vdo 目標上執行單個寫入操作所涉及的元資料結構數量多於大多數其他目標。此外,由於 vdo 必須在小塊大小上操作才能實現良好的重複資料刪除率,因此只有透過並行性才能實現可接受的效能。因此,vdo 的設計旨在實現無鎖。
vdo 的大多數主要資料結構都被設計成易於劃分為“區域”,以便任何給定的 bio 只能訪問任何區域結構的一個區域。透過確保在正常操作期間,每個區域分配給一個特定執行緒,並且只有該執行緒才能訪問該區域中資料結構的部分,從而實現最小鎖定的安全性。每個執行緒都關聯一個工作佇列。每個 bio 都關聯一個請求物件(“data_vio”),當其操作的下一階段需要訪問與該佇列關聯的區域中的結構時,該物件將被新增到工作佇列中。
另一種思考這種安排的方式是,每個區域的工作佇列對其管理的所有操作的結構都具有隱式鎖,因為 vdo 保證沒有其他執行緒會更改這些結構。
儘管每個結構都被劃分為區域,但這種劃分並未反映在每個資料結構的磁碟表示中。因此,每次啟動 vdo 目標時,可以重新配置每個結構的區域數量,從而重新配置執行緒數量。
重複資料刪除索引¶
為了有效識別重複資料,vdo 的設計旨在利用重複資料的一些共同特徵。根據經驗觀察,我們得出了兩個關鍵見解。首先是,在大多數包含大量重複資料集的資料集中,重複資料傾向於具有時間區域性性。當出現重複資料時,更可能檢測到其他重複資料,並且這些重複資料大約在同一時間寫入。這就是索引按時間順序儲存記錄的原因。第二個見解是,新資料更有可能重複最近的資料,而不是重複舊資料,並且通常情況下,回溯時間越長,收益越遞減。因此,當索引滿時,它應該淘汰其最舊的記錄以為新記錄騰出空間。索引設計背後的另一個重要思想是,重複資料刪除的最終目標是降低儲存成本。由於儲存節省與實現這些節省所消耗的資源之間存在權衡,vdo 不會嘗試找到每一個重複塊。找到並消除大部分冗餘就足夠了。
每個資料塊都經過雜湊處理以生成一個 16 位元組的塊名。索引記錄由該塊名與其在底層儲存上推定的資料位置配對組成。然而,無法保證索引的準確性。在最常見的情況下,這是因為當塊被覆蓋或丟棄時更新索引的成本太高。這樣做需要將塊名與塊一起儲存(這在基於塊的儲存中難以高效完成),或者在覆蓋每個塊之前讀取並重新雜湊它。不準確性也可能源於雜湊衝突,即兩個不同的塊具有相同的名稱。實際上,這種情況極不可能發生,但由於 vdo 不使用加密雜湊,因此可以構造惡意工作負載。由於這些不準確性,vdo 將索引中的位置視為提示,並在將現有塊與新塊共享之前,讀取每個指示的塊以驗證它確實是重複的。
記錄被收集到稱為“章節”的組中。新記錄被新增到最新的章節,稱為開放章節。此章節以最佳化新增和修改記錄的格式儲存,並且開放章節的內容在用完新記錄空間之前不會最終確定。當開放章節填滿時,它會被關閉並建立一個新的開放章節來收集新記錄。
關閉章節會將其轉換為另一種針對讀取最佳化的格式。記錄根據接收順序寫入一系列記錄頁面。這意味著具有時間區域性性的記錄應該位於少量頁面上,從而減少檢索它們所需的 I/O。章節還編譯了一個索引,指示哪個記錄頁面包含任何給定名稱。此索引意味著對某個名稱的請求可以準確確定哪個記錄頁面可能包含該記錄,而無需從儲存中載入整個章節。此索引僅使用塊名稱的一個子集作為其鍵,因此無法保證索引條目引用所需的塊名稱。它只能保證,如果存在此名稱的記錄,它將位於指示的頁面上。已關閉的章節是隻讀結構,其內容絕不會以任何方式更改。
一旦寫入足夠的記錄以填滿所有可用索引空間,最舊的章節將被移除,為新章節騰出空間。每當請求在索引中找到匹配記錄時,該記錄都會被複制到開放章節中。這確保了有用的塊名稱在索引中保持可用,而未引用的塊名稱則會隨著時間被遺忘。
為了在舊章節中查詢記錄,索引還維護一個更高級別的結構,稱為卷索引,其中包含將每個塊名對映到包含其最新記錄的章節的條目。當塊名的記錄被複制或更新時,此對映也會更新,從而確保只能找到給定塊名的最新記錄。塊名的舊記錄將不再被找到,即使它尚未從其章節中刪除。與章節索引一樣,卷索引僅使用塊名的一個子集作為其鍵,並且不能明確地說某個名稱存在記錄。它只能說明如果記錄存在,哪個章節會包含該記錄。卷索引完全儲存在記憶體中,並且僅在 vdo 目標關閉時才儲存到儲存中。
從特定塊名請求的角度來看,它將首先在卷索引中查詢該名稱。此搜尋將指示該名稱是新的,或者要搜尋哪個章節。如果返回一個章節,請求將在章節索引中查詢其名稱。這將指示該名稱是新的,或者要搜尋哪個記錄頁。最後,如果不是新的,請求將在指示的記錄頁中查詢其名稱。此過程每次請求可能需要多達兩次頁面讀取(一次用於章節索引頁,一次用於請求頁)。然而,最近訪問的頁面會被快取,因此這些頁面讀取可以分攤到許多塊名請求中。
卷索引和章節索引是使用一種記憶體高效的結構實現的,稱為差分索引。它不儲存每個條目的完整塊名(鍵),而是按名稱對條目進行排序,並且只儲存相鄰鍵之間的差值(差分)。由於我們期望雜湊值隨機分佈,差分的大小遵循指數分佈。由於這種分佈,差分使用霍夫曼編碼表示,以佔用更少的空間。整個排序的鍵列表稱為差分列表。這種結構使得索引每個條目使用的位元組數遠少於傳統雜湊表,但查詢條目的成本略高,因為請求必須讀取差分列表中的每個條目,以累加差分才能找到所需的記錄。差分索引透過將其鍵空間分成許多子列表來降低這種查詢成本,每個子列表都從一個固定的鍵值開始,從而使每個單獨的列表都很短。
預設索引大小可以容納 6400 萬條記錄,相當於大約 256GB 的資料。這意味著如果原始資料是在最近 256GB 的寫入中寫入的,索引就可以識別重複資料。這個範圍被稱為重複資料刪除視窗。如果新寫入的資料重複了比這更舊的資料,索引將無法找到它,因為舊資料的記錄已被刪除。這意味著如果一個應用程式將一個 200GB 的檔案寫入 vdo 目標,然後立即再次寫入,這兩個副本將完美地進行重複資料刪除。對一個 500GB 的檔案進行相同的操作將導致沒有重複資料刪除,因為在第二次寫入開始時,檔案的開頭將不再在索引中(假設檔案本身沒有重複資料)。
如果應用程式預計資料工作負載的有效重複資料刪除將超出 256GB 的閾值,則可以將 vdo 配置為使用更大的索引,並相應地增加重複資料刪除視窗。(此配置只能在建立目標時設定,不能在以後更改。在配置 vdo 目標之前,考慮預期工作負載非常重要。)有兩種方法可以做到這一點。
一種方法是增加索引的記憶體大小,這也會增加所需的後端儲存量。將索引大小加倍將以儲存大小和記憶體需求加倍為代價,使重複資料刪除視窗的長度加倍。
另一種選擇是啟用稀疏索引。稀疏索引會將重複資料刪除視窗擴大 10 倍,代價是儲存大小也增加 10 倍。但是,使用稀疏索引時,記憶體需求不會增加。權衡是每次請求的計算量略有增加,以及檢測到的重複資料刪除量略有減少。對於大多數具有大量重複資料的工作負載,稀疏索引將檢測到標準索引所能檢測到的 97-99% 的重複資料刪除量。
vio 和 data_vio 結構¶
vio(Vdo I/O 的縮寫)在概念上類似於 bio,但包含附加欄位和資料以跟蹤 vdo 特定的資訊。struct vio 維護一個指向 bio 的指標,但也跟蹤與 vdo 操作相關的其他欄位。vio 與其相關的 bio 分開儲存,因為在許多情況下,vdo 完成 bio 後,仍必須繼續執行與重複資料刪除或壓縮相關的工作。
元資料讀寫以及源自 vdo 內部的其他寫入直接使用 struct vio。應用程式讀寫使用一個更大的結構,稱為 data_vio,以跟蹤它們的進度資訊。一個 struct data_vio 包含一個 struct vio,並且還包括與重複資料刪除和其他 vdo 功能相關的幾個其他欄位。data_vio 是 vdo 中應用程式工作的主要單元。每個 data_vio 都會經過一系列步驟來處理應用程式資料,之後它會被重置並返回到 data_vio 池中以便重用。
有一個固定的 2048 個 data_vios 池。選擇這個數字是為了限制從崩潰中恢復所需的工作量。此外,基準測試表明增加池的大小並不能顯著提高效能。
資料儲存¶
資料儲存由三個主要資料結構實現,所有這些結構協同工作,以減少或分攤儘可能多的資料寫入操作中的元資料更新。
Slab 儲存區
vdo 卷的大部分屬於 slab 儲存區。該儲存區包含一系列 slab。每個 slab 最大可達 32GB,並分為三個部分。一個 slab 的大部分由 4K 塊的線性序列組成。這些塊用於儲存資料,或用於儲存塊對映的一部分(參見下文)。除了資料塊之外,每個 slab 都有一組引用計數器,每個資料塊使用 1 位元組。最後,每個 slab 都帶有一個日誌。
引用更新寫入 slab 日誌。Slab 日誌塊在寫滿時,或恢復日誌請求時寫出,以便主恢復日誌(參見下文)能夠釋放空間。Slab 日誌用於確保主恢復日誌能夠定期釋放空間,並分攤更新單個引用塊的成本。引用計數器儲存在記憶體中,並僅在需要回收 slab 日誌空間時,按塊(以最舊的髒塊順序)寫出。寫入操作根據需要以後臺方式執行,因此它們不會增加特定 I/O 操作的延遲。
每個 slab 都獨立於其他 slab。它們以輪詢方式分配到“物理區域”。如果有 P 個物理區域,則 slab n 將分配給區域 n mod P。
slab 儲存區維護一個額外的、較小的資料結構,即“slab 摘要”,用於減少崩潰後恢復上線所需的工作量。slab 摘要為每個 slab 維護一個條目,指示該 slab 是否曾被使用、其所有引用計數更新是否已持久化到儲存中,以及其大致的滿載程度。在恢復期間,每個物理區域將嘗試恢復至少一個 slab,一旦恢復到一個有一些空閒塊的 slab 就會停止。一旦每個區域都有一些空間,或者確定沒有可用空間,目標就可以在降級模式下恢復正常執行。讀寫請求可以得到服務,儘管效能可能會下降,而其餘的髒 slab 則在後臺恢復。
塊對映¶
塊對映包含邏輯到物理的對映。它可以被認為是一個數組,每個邏輯地址有一個條目。每個條目為 5 位元組,其中 36 位包含儲存給定邏輯地址資料的物理塊號。其餘 4 位用於指示對映的性質。在 16 種可能的狀態中,一種表示未對映的邏輯地址(即從未寫入或已丟棄),一種表示未壓縮塊,而其他 14 種狀態用於指示對映的資料已壓縮,以及壓縮塊中的哪個壓縮槽包含此邏輯地址的資料。
實際上,對映條目陣列被劃分為“塊對映頁”,每個頁都適合單個 4K 塊。每個塊對映頁由一個頁頭和 812 個對映條目組成。每個對映頁實際上是一個基數樹的葉子,該基數樹在每個級別都由塊對映頁組成。有 60 棵基數樹以輪詢方式分配給“邏輯區域”。(如果有 L 個邏輯區域,則樹 n 將屬於區域 n mod L。)在每個級別上,這些樹是交錯的,因此邏輯地址 0-811 屬於樹 0,邏輯地址 812-1623 屬於樹 1,依此類推。這種交錯一直保持到 60 個根節點。選擇 60 棵樹可以在大量的可能邏輯區域計數下,使每個區域的樹數量均勻分佈。60 個樹根的儲存在格式化時分配。所有其他塊對映頁都根據需要從 slab 中分配。這種靈活的分配避免了為整個邏輯對映集預分配空間的需求,並且也使得增加 vdo 的邏輯大小相對容易。
在操作中,塊對映維護兩個快取。將樹的整個葉子級別儲存在記憶體中是不可行的,因此每個邏輯區域都維護自己的葉子頁面快取。此快取的大小可在目標啟動時配置。第二個快取在啟動時分配,並且足夠大以容納整個塊對映的所有非葉子頁面。此快取會根據需要填充頁面。
恢復日誌¶
恢復日誌用於分攤塊對映和 slab 儲存區的更新。每個寫入請求都會導致在日誌中建立一個條目。條目可以是“資料重新對映”或“塊對映重新對映”。對於資料重新對映,日誌記錄受影響的邏輯地址及其舊的和新的物理對映。對於塊對映重新對映,日誌記錄塊對映頁碼和為其分配的物理塊。塊對映頁永不回收或重新利用,因此舊對映始終為 0。
每個日誌條目都是一個意圖記錄,彙總了 data_vio 所需的元資料更新。恢復日誌在每次日誌塊寫入之前都會發出一個重新整理操作,以確保該塊中新塊對映的物理資料在儲存上是穩定的,並且所有日誌塊寫入都設定了 FUA 位,以確保恢復日誌條目本身是穩定的。日誌條目及其所代表的資料寫入必須在磁碟上穩定後,其他元資料結構才能更新以反映該操作。這些條目允許 vdo 裝置在意外中斷(例如斷電)後重建邏輯到物理的對映。
寫入路徑¶
所有對 vdo 的寫入 I/O 都是非同步的。一旦 vdo 完成了足夠的工作以保證最終可以完成寫入,每個 bio 就會被確認。通常,已確認但未重新整理的寫入 I/O 資料可以被視為已快取在記憶體中。如果應用程式要求資料在儲存上穩定,它必須像任何其他非同步 I/O 一樣,發出一個重新整理或將資料寫入時設定 FUA 位。關閉 vdo 目標也將重新整理所有剩餘的 I/O。
應用程式寫入 bios 遵循以下步驟。
從 data_vio 池中獲取一個 data_vio 並將其與應用程式 bio 關聯。如果沒有可用的 data_vio,傳入的 bio 將阻塞直到有 data_vio 可用。這為應用程式提供了背壓。data_vio 池受自旋鎖保護。
新獲取的 data_vio 被重置,如果它是寫入操作且資料不全為零,則 bio 的資料會複製到 data_vio 中。資料必須被複制,因為應用程式 bio 可以在 data_vio 處理完成之前被確認,這意味著後續的處理步驟將無法再訪問應用程式 bio。應用程式 bio 也可能小於 4K,在這種情況下,data_vio 將已經讀取了底層塊,資料則會複製到更大塊的相關部分上。
data_vio 對 bio 的邏輯地址施加一個宣告(“邏輯鎖”)。防止同時修改相同的邏輯地址至關重要,因為重複資料刪除涉及共享塊。此宣告作為雜湊表中的一個條目實現,其中鍵是邏輯地址,值是指向當前處理該地址的 data_vio 的指標。
如果一個 data_vio 在雜湊表中查詢並發現另一個 data_vio 已經在該邏輯地址上操作,它會等待直到上一個操作完成。它還會發送一條訊息通知當前鎖的持有者它正在等待。最值得注意的是,一個新的等待邏輯鎖的 data_vio 將會把之前的鎖持有者從壓縮打包器(步驟 8d)中刷新出來,而不是允許它繼續等待被打包。
此階段要求 data_vio 在適當的邏輯區域上獲得一個隱式鎖,以防止雜湊表的併發修改。這種隱式鎖定由上面描述的區域劃分處理。
data_vio 遍歷塊對映樹,透過嘗試查詢其邏輯地址的葉子頁,以確保所有必要的內部樹節點都已分配。如果任何內部樹頁缺失,則此時會從用於儲存應用程式資料的相同物理儲存池中分配它。
如果樹中的任何頁面節點尚未分配,則必須在寫入繼續之前對其進行分配。此步驟要求 data_vio 鎖定需要分配的頁面節點。此鎖與步驟 2 中的邏輯塊鎖一樣,是一個雜湊表條目,它會導致其他 data_vios 等待分配過程完成。
在分配發生時,隱式邏輯區域鎖被釋放,以允許同一邏輯區域中的其他操作繼續進行。分配的詳細資訊與步驟 4 相同。一旦新節點被分配,該節點就會以類似於新增新資料塊對映的過程新增到樹中。data_vio 記錄了將新節點新增到塊對映樹的意圖(步驟 10),更新新塊的引用計數(步驟 11),並重新獲取隱式邏輯區域鎖以將新對映新增到父樹節點(步驟 12)。一旦樹更新完成,data_vio 會沿著樹向下執行。任何等待此分配的其他 data_vio 也將繼續執行。
在穩態情況下,塊對映樹節點將已被分配,因此 data_vio 只需遍歷樹,直到找到所需的葉節點。對映的位置(“塊對映槽”)記錄在 data_vio 中,以便後續步驟無需再次遍歷樹。然後 data_vio 釋放隱式邏輯區域鎖。
如果塊是零塊,則跳到步驟 9。否則,將嘗試分配一個空閒資料塊。此分配確保即使無法進行重複資料刪除和壓縮,data_vio 也可以將其資料寫入某個位置。此階段在物理區域上獲得一個隱式鎖,以在該區域內搜尋空閒空間。
data_vio 將在區域中的每個 slab 中搜索,直到找到空閒塊或確定沒有空閒塊。如果第一個區域沒有空閒空間,它將透過獲取該區域的隱式鎖並釋放前一個鎖來繼續搜尋下一個物理區域,直到找到空閒塊或耗盡可搜尋的區域。data_vio 將在空閒塊上獲取一個 struct pbn_lock(“物理塊鎖”)。struct pbn_lock 還有幾個欄位用於記錄 data_vio 對物理塊可以擁有的各種宣告。pbn_lock 被新增到雜湊表中,類似於步驟 2 中的邏輯塊鎖。此雜湊表也受隱式物理區域鎖的保護。空閒塊的引用計數被更新,以防止任何其他 data_vio 認為它是空閒的。引用計數器是 slab 的一個子元件,因此也受隱式物理區域鎖的保護。
如果獲得分配,data_vio 將擁有完成寫入所需的所有資源。此時可以安全地確認應用程式 bio。確認操作在單獨的執行緒上進行,以防止應用程式回撥阻塞其他 data_vio 操作。
如果無法獲得分配,data_vio 將繼續嘗試對資料進行重複資料刪除或壓縮,但由於 vdo 裝置可能空間不足,因此 bio 未被確認。
此時,vdo 必須確定應用程式資料的儲存位置。data_vio 的資料被雜湊處理,並且雜湊值(“記錄名稱”)被記錄在 data_vio 中。
data_vio 保留或加入一個 struct hash_lock,該結構管理所有當前寫入相同資料的 data_vio。活動的雜湊鎖在雜湊表中進行跟蹤,類似於步驟 2 中跟蹤邏輯塊鎖的方式。此雜湊表受雜湊區域上的隱式鎖保護。
如果此 data_vio 的 record_name 沒有現有雜湊鎖,則 data_vio 從池中獲取一個雜湊鎖,將其新增到雜湊表,並將自身設定為新雜湊鎖的“代理”。hash_lock 池也受隱式雜湊區域鎖的保護。雜湊鎖代理將完成所有決定應用程式資料寫入位置的工作。如果 data_vio 的 record_name 的雜湊鎖已經存在,並且 data_vio 的資料與代理的資料相同,則新的 data_vio 將等待代理完成其工作,然後共享其結果。
在極少數情況下,如果存在 data_vio 雜湊的雜湊鎖,但資料與雜湊鎖的代理不匹配,則 data_vio 會跳到步驟 8h 並嘗試直接寫入其資料。例如,當兩個不同的資料塊生成相同的雜湊值時,可能會發生這種情況。
雜湊鎖代理透過以下步驟嘗試對其資料進行重複資料刪除或壓縮。
代理初始化並將其嵌入式重複資料刪除請求(struct uds_request)傳送到重複資料刪除索引。這不需要 data_vio 獲取任何鎖,因為索引元件管理自己的鎖。data_vio 等待,直到它從索引獲取響應或超時。
如果重複資料刪除索引返回建議,data_vio 將嘗試獲取指示物理地址上的物理塊鎖,以便讀取資料並驗證其與 data_vio 的資料相同,並且可以接受更多引用。如果物理地址已被另一個 data_vio 鎖定,則該地址的資料可能很快被覆蓋,因此使用該地址進行重複資料刪除是不安全的。
如果資料匹配且物理塊可以新增引用,則代理和任何其他等待它的 data_vios 將把此物理塊記錄為其新的物理地址,然後繼續執行步驟 9 以記錄其新對映。如果雜湊鎖中的 data_vios 數量多於可用引用數量,則剩餘的 data_vios 之一將成為新的代理,並繼續執行步驟 8d,如同未返回有效建議一樣。
如果未找到可用的重複塊,代理首先檢查它是否有一個已分配的物理塊(來自步驟 3)可以寫入。如果代理沒有分配,雜湊鎖中其他具有分配的 data_vio 將接替代理。如果所有 data_vios 都沒有已分配的物理塊,則這些寫入操作將因空間不足而失敗,因此它們將繼續執行步驟 13 進行清理。
代理嘗試壓縮其資料。如果資料無法壓縮,data_vio 將繼續執行步驟 8h 以直接寫入其資料。
如果壓縮大小足夠小,代理將釋放隱式雜湊區域鎖,並轉到打包器(struct packer),在那裡它將與其他 data_vios 一起放入一個 bin(struct packer_bin)中。所有壓縮操作都需要打包器區域上的隱式鎖。
打包器可以在單個 4K 資料塊中組合多達 14 個壓縮塊。只有當 vdo 可以在單個數據塊中打包至少 2 個 data_vios 時,壓縮才有用。這意味著一個 data_vio 可能會在打包器中等待任意長的時間,直到其他 data_vios 填充壓縮塊。vdo 有一種機制,可以在繼續等待會導致問題時驅逐等待中的 data_vios。導致驅逐的情況包括應用程式重新整理、裝置關閉,或後續的 data_vio 嘗試覆蓋相同的邏輯塊地址。如果一個 data_vio 在更多可壓縮塊需要使用其 bin 之前無法與任何其他壓縮塊配對,它也可能被從打包器中驅逐。被驅逐的 data_vio 將繼續執行步驟 8h 以直接寫入其資料。
如果代理填滿了打包器 bin,無論是由於其所有 14 個槽位都已使用,還是因為它沒有剩餘空間,它都會使用其一個 data_vios 的已分配物理塊進行寫入。步驟 8d 已確保分配可用。
每個 data_vio 將壓縮塊設定為其新的物理地址。data_vio 在物理區域上獲得一個隱式鎖,並獲取壓縮塊的 struct pbn_lock,該鎖被修改為共享鎖。然後它釋放隱式物理區域鎖並繼續執行步驟 8i。
任何從打包器中被驅逐的 data_vio 都將擁有步驟 3 中的分配。它會將資料寫入該已分配的物理塊。
資料寫入後,如果 data_vio 是雜湊鎖的代理,它將重新獲取隱式雜湊區域鎖,並將其物理地址儘可能多地共享給雜湊鎖中的其他 data_vios。然後每個 data_vio 將繼續執行步驟 9 以記錄其新對映。
如果代理確實寫入了新資料(無論是否壓縮),則會更新重複資料刪除索引以反映新資料的位置。然後代理釋放隱式雜湊區域鎖。
data_vio 確定邏輯地址的先前對映。存在一個塊對映葉頁面快取(“塊對映快取”),因為通常有太多的塊對映葉節點無法完全儲存在記憶體中。如果所需的葉頁面不在快取中,data_vio 將在快取中保留一個槽位並將所需的頁面載入到其中,可能會驅逐較舊的快取頁面。然後 data_vio 找到此邏輯地址的當前物理地址(“舊物理對映”)(如果有的話),並記錄下來。此步驟需要對塊對映快取結構加鎖,該鎖由隱式邏輯區域鎖覆蓋。
data_vio 在恢復日誌中建立一個條目,其中包含邏輯塊地址、舊物理對映和新物理對映。建立此日誌條目需要持有隱式恢復日誌鎖。data_vio 將在日誌中等待,直到包含其條目的所有恢復塊都被寫入並重新整理,以確保事務在儲存上穩定。
一旦恢復日誌條目穩定,data_vio 會建立兩個 slab 日誌條目:一個用於新對映的增量條目,一個用於舊對映的減量條目。這兩個操作都需要持有受影響物理 slab 的鎖,該鎖由其隱式物理區域鎖覆蓋。為了在恢復期間的正確性,任何給定 slab 日誌中的 slab 日誌條目必須與相應的恢復日誌條目順序相同。因此,如果這兩個條目位於不同的區域,它們會併發建立;如果它們位於同一區域,則增量條目始終在減量條目之前建立,以避免下溢。在記憶體中建立每個 slab 日誌條目後,相關的引用計數也會在記憶體中更新。
一旦兩個引用計數更新完成,data_vio 會獲取隱式邏輯區域鎖,並更新塊對映中的邏輯到物理對映,使其指向新的物理塊。至此,寫入操作完成。
如果 data_vio 擁有雜湊鎖,它將獲取隱式雜湊區域鎖,並將其雜湊鎖釋放回池中。
然後 data_vio 獲取隱式物理區域鎖,並釋放其持有的已分配塊的 struct pbn_lock。如果它有未使用的分配,它還會將該塊的引用計數設定回零,以便供後續的 data_vio 使用。
然後 data_vio 獲取隱式邏輯區域鎖,並釋放步驟 2 中獲得的邏輯塊鎖。
如果應用程式 bio 尚未被確認,則此時會被確認,並且 data_vio 返回到池中。
讀取路徑¶
應用程式讀取 bio 遵循一組更簡單的步驟。它執行寫入路徑中的步驟 1 和 2,以獲取 data_vio 並鎖定其邏輯地址。如果該邏輯地址已存在正在進行且保證會完成的寫入 data_vio,則讀取 data_vio 將從寫入 data_vio 複製資料並返回。否則,它將像步驟 3 中那樣遍歷塊對映樹來查詢邏輯到物理的對映,然後讀取並可能解壓縮指定物理塊地址處的指示資料。如果缺少塊對映樹節點,讀取 data_vio 將不會分配它們。如果內部塊對映節點尚不存在,則邏輯塊對映地址仍必須處於未對映狀態,並且讀取 data_vio 將返回所有零。讀取 data_vio 按照步驟 13 處理清理和確認,儘管它只需要釋放邏輯鎖並將其自身返回到池中。
小寫入¶
vdo 內的所有儲存都以 4KB 塊管理,但它可以接受小至 512 位元組的寫入。處理小於 4K 的寫入需要進行讀-修改-寫操作,該操作會讀取相關的 4K 塊,將新資料複製到塊的相應扇區上,然後啟動對修改後的資料塊的寫入操作。此操作的讀寫階段與正常的讀寫操作幾乎相同,並且整個操作過程中都使用單個 data_vio。
恢復¶
當 vdo 在崩潰後重啟時,它將嘗試從恢復日誌中恢復。在下次啟動的預恢復階段,會讀取恢復日誌。有效條目的增量部分會應用到塊對映中。接下來,有效條目會按照要求,順序地應用到 slab 日誌中。最後,每個物理區域會嘗試重播至少一個 slab 日誌,以重建一個 slab 的引用計數。一旦每個區域都有一些空閒空間(或者確定沒有),vdo 就會上線,而其餘的 slab 日誌則在後臺用於重建其餘的引用計數。
只讀重建¶
如果 vdo 遇到不可恢復錯誤,它將進入只讀模式。此模式表明某些先前已確認的資料可能已丟失。可以指示 vdo 儘可能進行重建,以恢復到可寫狀態。但是,由於資料可能丟失,因此這絕不會自動進行。在只讀重建期間,塊對映像以前一樣從恢復日誌中恢復。但是,引用計數不會從 slab 日誌中重建。相反,引用計數被清零,遍歷整個塊對映,並從塊對映中更新引用計數。雖然這可能會丟失一些資料,但它確保了塊對映和引用計數彼此一致。這允許 vdo 恢復正常操作並接受進一步的寫入。