原文:ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs——>io——>device)

1024水一篇文。。。。

最近在做ZNS-ssd 相關的測試,因為 rocksdb 社群有一個 Hans 提的 Zenfs的PR,引起了很多大佬的討論。雖然還是沒有合入主線(其僅僅是作為了一個檔案系統的backend,而且需要特殊的硬體支援,並且合入需要單獨維護幾千行程式碼,對於社群來說代價還是有點高,大家建議Hans 單獨拉一個子專案,作為rocksdb的plug),但是在討論過程中大家對Zenfs的接入還是比較歡迎的。

本文將基於ZNS-SSD 論文 : ZNS: Avoiding the Block Interface Tax for Flash-based SSDs 的介紹 以及 實際測試過程中對 libzbd 和 zenfs 程式碼的瞭解 來深入分析ZNS-ssd的 實現機制。

術語表(方便大家理解下文)

ZNS: zoned namespace

page: ssd內部的最小的寫單位,一般為4K

block: ssd內部的最小的擦除單位,一般為32K,在其之上還有plane, die, package

OP: over providing,ssd內部過度供應的空間,是各個廠商為GC預留的空間,對使用者不可見

NMOS:浮柵電晶體, 現在的大多數 NAND ssd的底層儲存單元;對應的3D XPoint的底層儲存單元是PCM(相變記憶體)

OC-SSD: open channel ssd

WP : zone 內部用來排程寫入的 write pointer,表示當前zone 可寫的下一個LBA 地址

1 背景

ZNS-SSD 出現的起源肯定是因為傳統的block interface ssd 也就是 Flash-ssd 有一些無法很好解決的問題。

1。1 Block Interface SSD 的問題

1。1。1 效能問題

Flash-SSD 底層大多數是NMOS 儲存單元,為了保證NMOS 儲存單元的0/1可以穩態體現,寫之前需要先擦除,更底層的原理可以參考 從NMOS 和 PCM 底層儲存單元 來看NAND和3D XPoint的本質區別。因為這個問題,管理 Flash-SSD 的 FTL (Flash-Transfer-Layer) 需要做很多的取捨。為了儘可能高效得擦除儲存單元,FTL 將擦除的最小粒度設定成了block 32K(不同的SSD應該是不一樣的),而寫入則是按照 page 4K 粒度進行。

除此之外,FTL 更多的工作是 管理 LBAs(logical block address, 將作業系統的塊驅動層下發的請求的邏輯偏移地址轉化為Flash-ssd的物理地址)、回收過期/刪除的資料、排程使用者的隨機寫等。因為 NMOS 的寫特性, 隨著 SSD 逐漸接近滿態,FTL查詢可用空閒 page 或者 GC 過期 block 都會讓整個 SSD 的效能不可預估。可能 SSD 剛開始的時候擁有較高的吞吐和較低的長尾,而隨著寫入的增加, 吞吐會有一定程度的下降,長尾則更是不可預期。

1。1。2 空間放大問題

因為FTL的GC需求,因為當想要擦除一個block的時候,這個block中的部分page還是有未過期的資料的,這一些資料需要移動到新的page中。則就需要一部分空閒來作為GC過程中的未過期資料的儲存,而這一些空間則不會體現在SSD實際的可用容量中。而且這一些空間佔用總容量的比例在不同廠商生產的SSD下最高能夠達到28%,也就是實際1T的SSD,使用者可用的也就720G,這一些OP(over providing)導致的空間放大會體現在使用者的成本之上,無形中增加了使用者的儲存成本。

關於GC引入的空間放大,用下圖舉個例子:

這是我們現在的SSD內部block 和 page的分佈,可以看到4不同的檔案(用顏色代替了屬於不同檔案的page)的page是隨機分佈在整個SSD的 塊空間中。 每一個小方塊,代表的是一個ssd 的page, 5個小方塊屬於一個block,這個block也是擦除的最小粒度。最下面的則是OP空間,SSD廠商專門為GC 預留的空間,對使用者不可見。

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

當我們刪除其中一個檔案的時候,比如我們刪除 File C,那麼SSD內部屬於File C的 這一些 page 就是過期的,需要被清理。 但是,我們想要擦除這一些過期較多的block的時候需要將其中未過期的page 移動到 OP 空間內。

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

當我們將可用的block中的page移動到OP空間之後,整個block 就可以被GC,從而能夠用作其他資料的儲存,下圖中灰色的兩個block後續就可以被擦除掉。

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

可以看到GC的整個過程需要涉及到資料的遷移和OP空間的佔用,而且頻繁的GC還會導致NMOC的壽命不斷降低,因為NMOC的的兩個氧化層在頻繁的寫/擦除之下會不斷得被消耗。

1。2 業界嘗試過的解決方案

1。2。1 Stream SSDs

這種型別的ssd 透過 SSD主控 來對寫入的請求進行標記,請求進入到流式SSD內部的時候根據預定義好的流式ssd標記來將帶有不同標記的輸入command 分配儲存到不同的擦除塊之內。這樣,能夠極大得節省因為FTL 排程GC導致的效能以及ssd壽命的損失。但是這種流式SSD 對主控也就是 host的要求比較高,需要host能夠快速的區分接收到的寫入指令,如果主控不能很好得區分寫入指令的stream hint 型別,那仍然就像block interface 的ssd一樣,GC需要由FTL進行排程。

也就是stream ssd想要保證自己有足夠的資源和效率來區分寫入的command,那就需要消耗更多的記憶體(儲存未區分的狀態) 以及效能更好的主控晶片,然而,對這一些資源的需求並不能降低SSD本身的成本。

而具體stream ssds 的效能能有多好,是不是能彌補成本提升所帶來的損失,按照現在的業界硬體儲存發展來看,應該是沒戲了,不過ZNS的論文中做了測試,後續會提到。

1。2。2 Open-Channel SSDs

這種型別的SSD之前業界還是有很多探索的,而且百度已經將一種 OC- SSD SDF型號 大規模部署在了自己的基礎架構的集群裡 。OC-SSD 它允許SSD主控和SSD共同管理 一個 連續的LAB 塊的集合,並且對外暴露的操作指令是擦除塊對齊的。這種按照擦除塊對齊的訪問模型能夠消除 SSD 內部GC的代價,並且降低對OP空間的消耗(不需要預留一部分空間用作擦出塊內部的正常資料page的遷移)以及 DRAM的消耗(DRAM 需要儲存LBA,因為OC是按照擦除塊對齊的,擦除塊相比於page大一些,所以需要的LBA 空間就少一些)。

總的來說 OC-ssd 能夠提供以下幾個特性:

將SSD內部的可以並行操作的特性暴露給了使用者

。每一個chunk 可以看作是一個channel,也就是一塊獨立的儲存區域,類似/dev/sda這種像是傳統SSD的塊裝置。一個SDF 的OC裝置最多能夠提供43個channel,那它這一個ssd 就能作為/dev/sda1 ——- /dev/sda43 這麼多個塊裝置供使用者使用。

提供了非對稱的I/O介面。即讀取的最小單元(page,在SDF 中page 是8K)和寫入的最小單元(erase block, SDF 中是 2M)可以不同,在 SDF 中寫入是擦除塊對齊的,所以能夠最大程度得減少甚至消除GC引入的寫放大。

erase 操作作為了一個 裝置指令,並且暴露給了使用者。erase操作在傳統的 block interface 的ssd中成本非常高,相比於讀寫操作來說延時非常長,在SDF 中erase 一個2M 的block 延時甚至能夠達到 3ms。所以,即使是在 open-channel 內部去排程erase操作,也會引入較高的延時。OC聰明的一點是將排程 erase 指令的許可權暴露給了使用者,使用者自己去選擇什麼時候排程OC內部的erase,自己去考慮怎麼解決 erase 延時的問題。

為 OC-ssd 設計的一個短鏈路的I/O棧。os 的 block layer 的請求排程是極為複雜的,並且 os的傳統I/O棧成為很多高效能硬體儲存的瓶頸(NVM-PMEM 這樣的儲存硬體完全不用os 的 塊層儲存棧),所以 在SDF 的實現中,為了降低對OC 的訪問延時,by-pass了大部分的I/O棧,上次應用(比如leveldb 的sst)的訪問透過

ioctl

直接與底層的 driver 進行互動,這樣能夠極大得降低訪問延時。在SDF 中,leveldb 的 sst 訪問延時能夠降低到 2-4us,這個量級 對於 走PCIe+NVMe 的 ssd 來說確實很低了。

所以OC-ssd 能夠提升SSD的效能 以及 儲存單元的壽命。但是,對LBA的管理 以及 erase block的回收和壞塊管理就都得由host來做了。並且,host 需要相容不同的SSD內部實現機制,並且對暴露統一的訪問模式。想要讓OC-ssd 通用,就需要不斷得對host 部分的軟體進行維護,維護成本較為高昂。並且OC還提供了自己的I/O棧,想要使用OC的高效能特性,就無法走作業系統的I/O棧,這對儲存軟體來說,還得自己實現一個塊驅動層來 在OS內部管理 OC-ssd的儲存空間才行,對於不同核心驅動/塊層實現細節的 儲存研發工程師來說接入成本實在是過於高昂。

貼一個百度架構 和高校合作 LevelDB + SDF(OC-SSD)的架構圖,對OC-SSD 的大體使用形態就一目瞭然了:

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

可以看到 不論是 Stream-SSD 還是 OC-SSD 都能在一定程度緩解 甚至 解決 block-interface ssd的問題,但是要不就是硬體成本太高,要不就是接入成本太高,這對於做儲存系統以及儲存引擎的公司來說都是需要投入較長的時間以及人力成本才能看到收益。

所以,ZNS社群 很聰明得抓住了儲存軟體研發人員的 “痛點”,並吸取了前兩者在設計上的優點以及解決了前兩者的痛點,從而展現出了一個全新的、更容易被儲存系統研發人員接受的 新硬體形態。

2 ZNS 架構

2。1 ZNS storage 模型演進

先大體看一下 ZNS-ssd 的內部資料分佈:

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

ZNS-ssd 像 OC 那種 channel,將整個SSD內部的儲存區域分為多個zone 空間,每一段zone空間管理一段LBA(物理page和邏輯地址之間的對映),不同的zone 空間之間的資料可以是獨立的。最主要的是,每一個zone 內部的寫入 只允許順序寫,可以隨機讀。因為 zone 內部的順序寫特性,基本可以消除 SSD GC的開銷。為了保證zone 內部的順序寫,在ZNS內部想要覆蓋寫一段LBA地址的話需要先reset(清理當前地址的資料),才能重新順序寫這一段邏輯地址空間。

同時一個完整的 ZNS裝置是支援完整的儲存棧,包括底層的塊驅動到上層的檔案系統都是已經實現好的。這個完整的儲存棧就是相比於OC-ssd的優勢,能夠對外遮蔽複雜的接入功能,緊抓使用者的使用習慣,使用者軟體可以做到基本零成本接入。

當然,僅限於底層是順序寫的儲存應用。rocksdb / ceph的bluestore 等。

整個儲存棧的演進是一個過程,並不是直接就推出了ZNS的設計,下文將會詳細介紹,從ZNS 的演進過程我們也能看到ZNS 可應用場景的一些限制。

2。1。1 zone storage model

最開始的時候西數推出了zone 形態的儲存模型,也就是上圖中SSD內部的儲存空間按照zone 進行了邏輯劃分,而且只允許順序寫,覆蓋寫的前提是reset 之前的LBA。

這個時候的zone storage model 內部實現 以及一些約束可以從以下幾個方面來看:

1 Per-zone state machine

如果想要確定一個zone 是否能夠寫,需要透過一系列狀態進行判斷,下圖是zone 的內部狀態變化。

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

這裡關注的主要是幾個重要的外部狀態:

Empty : 所有zone 未使用前的開始狀態。

Open : 內部有兩種變遷狀態,想要寫入的話先從 Empty轉化為Open 狀態,隨後該zone 就是可寫的。

Full:持續寫入,達到一個zone 空間的上限之後會進入Full 狀態。

Closed: 如果 zone ssd 有最大zone 個數限制,當處於Open的zone 個數達到了limit 限制,還想Open新的zone, 這一些限制中的某一個zone 需要切換到Close狀態。Closed 狀態的zone還是可寫的,但是需要先進入Open 狀態才寫。

2 write pointer ,後續簡稱為WP

write pointer 表示一個可寫的zone 內部 可寫的下一個LBA地址,當前zone可寫的狀態是Empty和 Open。當zone 的狀態是Full 的時候,任何從當前WP 開始 的寫指令都會失敗,當 一個 zone 排程了 reset 指令,就會清理當前zone 上所有的資料,並將當前zone 的狀態置為 Empty,同時WP 的LBA 也會移動到zone 最開始的 LBA。

下圖的WP 指向的是當前writable zone 中的下一個可寫的LBA地址,在WP 指向的LBA 之前的LBA都是已經寫過資料的了。

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

3 writable zone capacity

這個特性是 zone storage 以及 後續的 ZNS 為了相容 行業規範(論文中說的是SMR HDD 的

power-of-two zone size industry norm

,沒有找到這個規範的出處 )推出的。個人絕對最重要的特性還是直觀得告訴使用者當前zone 的可用容量,論文中這樣圖解釋的比較清楚:

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

一個zone 會被分為是那個區域

Written LBAs

Unwritten LBAs

Unmapped LBAs

這樣,zone size 就是三個容量之和;而

zone capacity

則是 written + Unwritten 之和,也就是 WP 可用的LBAs 的容量。對於 Unmmapped LBAs 因為沒有建立這一部分空間的邏輯地址和物理地址的對映,那肯定就不可用了。

有了

zone capacity

,就能很好得控制 LBA 對映表的大小,不像interface ssd一樣,一開始就對整個儲存空間建立一個 LBA 對映表,如果磁碟足夠大,那對記憶體的消耗就比較大了;有了Capacity, 那當capacity 快要被消耗光的時候再建立LBA,就能少一部分的記憶體消耗了。

4 active zone limit

這是對於 NAND 儲存介質的磁碟的一種限制。如果儲存介質是NAND,則因為電源異常的時候需要透過 附加電源電容器確保將 其內部NMOS 儲存單元的電子歸位(奇偶校驗),當前伺服器附加電容器因為個數有限,內部的電容只能支援部分zone 空間的奇偶校驗,所以 zone storage 為了保證資料一致性,會限制最大同時可寫(處於 Open 和 Closed 狀態的zone)的 zone 的個數。然而這個限制 在機械硬碟的儲存介質(磁儲存)下就不會有了,因為不需要額外的 附加電源電容器。

現在的ZN540 zns-ssd 最大允許 14個 active zones,當然實際的效能並不會說因為有active zone limit 個數的限制而下降,當前的active zones 還是能夠發揮出磁碟本身的效能上限的。

西數的SSD 效能還是略差一些,單GB價格相近的NVMe ssd相比於 Intel 同等價格的寫效能差不少,單讀效能又比較接近,可能與SSD 主控效能關係比較大吧。。。

zone storage 的思想還是可借鑑的。

2。1。2 Evolving to ZNS

下文將介紹 對於 ZNS-ssd 的推出 可能影響其效能的一些 硬體限制和主控軟體的設計。

1 硬體限制

Zone sizing

首先傳統的interface ssd 的擦除單位會比一般的寫請求的最小粒度大不少。所以 ZNS-ssd 會考慮再擦除效率和寫入自由度之間的平衡,在保證最大效能的前提下選擇合適的 擦除塊大小。ZN540 的擦除塊是2M。

Mapping table

這個之前說過,就是傳統的block interface 的ssd 會對整個儲存空間的物理地址建立對應的邏輯地址對映,也就是 LBAs 和 物理地址的對映表。當然,對整個SSD 進行這樣的對映是為了得最大程度得提升GC效率, GC的時候查詢空閒塊的話只需要訪問記憶體中的這一張對映表就能知道哪一些 LBAs 對應的 物理塊是空閒的。存在的問題也很明顯,就是平均1TB 的磁碟需要1G的 記憶體空間來儲存這個對映表。如果磁碟以及容量都增加的話,LBA tables 消耗的記憶體對於使用者來說就很難接受了。

這個問題,ZNS中因為每一個 zone space 都只能順序寫,這樣對於ZNS 裝置來說,只需要維護擦除塊這種極為粗粒度的對映表了,能夠極大得節省對記憶體空間的消耗。

2 主控軟體的設計

這裡主要是ZNS-ssd 為了讓自己的易用性更強,提供的一些外部應用可接入的介面。

Host- Side FTL (HFTL)

HFTL 的主要作用是 在 外部應用如果排程inplace-upate/隨機寫 則透過HFTL 轉化為 ZNS-ssd 支援的 寫語義。畢竟,inplace-update 的應用還是佔據了市場的很大一部分(資料庫儲存領域的 mysql),對於這種應用 ZNS-ssd 不能說不支援,只能說相比於 LSM-tree 來說這種效能沒有那麼好罷了。

HFTL 這裡就像是傳統的 ssd FTL,排程GC 並且 管理 LBA 對映表,同時也要管理 ZNS 裝置用到的CPU和記憶體資源。如果使用者排程的隨機寫場景 ,那ZNS 裝置的執行機制就像是傳統的磁碟裝置了。

現在已經支援 HFLT 管理的對映機制包括:dm-zone (SMR-HDD), dm-zap(ZNS-SSD,到目前為止還在開發中),pblk 和 SPDK‘FTL 都顯示了 HFTL 的可行性和適用性。

File System

為了支援更多的應用來訪問ZNS 裝置,檔案系統會作為一個更高層的儲存介面,就像是POSIX API 一樣為應用提供檔案語義。現有的檔案系統包括(f2fs, btrfs, zfs) 都能夠支援順序寫模式,但是這一些檔案系統的元資料的更新並非是順序寫,比如 log-structured write (檔案系統的WAL),supperblock 的更新等 都不是順序寫。所以想要為ZNS 做一個更上層的應用,並且排程ZNS 內部的 各個zone 的狀態 使用現有的這一些檔案系統其實都不是很友好(過於複雜,且改造成本太高),雖然在前期的 zone storage 演進的過程中也在f2fs 這種檔案系統上做了適配,但也僅限於 在ZAC/ZBC 這樣的裝置中使用(hdd),不適用於ZNS;所以後續ZNS 單獨設計了適配於ZNS 的檔案系統來為應用提供檔案語義。

End-To-End Data Placement

端到端的資料儲存。傳統的磁碟I/O棧 需要透過 核心檔案系統,通用塊層,I/O排程層,塊裝置驅動層 這樣的一系列I/O子系統才能達到磁碟的主控。這一些漫長的鏈路無形中無拖慢資料的儲存效率,間接降低磁碟吞吐,增加了請求的延時。然而 ZNS 的推出,ZNS 內部的設計本身能夠極大的降低SSD 內部的寫放大,提升寫吞吐,同時少了很多的指令和對LBA的管理操作(erase block的粒度大,減少了 對LBA的需求),

ZNS 做了一個非常大膽的挑戰

,開發了支援檔案系統語義的端到端的訪問形態的檔案系統

Zenfs

,資料的儲存繞開像原本 block interface 一樣需要龐大的I/O棧,直接與ZNS-SSD 進行互動。

有很多應用廣泛的系統一直在 端到端的資料儲存上進行探索,而且這一些系統能夠支援順序寫模式,對於ZNS 的推出也是極大的支援,包括但不限於:LSM-tree 儲存引擎 Rocksdb, 基於cache 的儲存 CacheLib, 物件儲存 Ceph 的seastore。後續會給出Zenfs 作為Rocksdb 的一個檔案系統 backend 的效能測試資料。

2。2 ZNS 的架構實現

先看看 支援zone 儲存的 SMR HDD 以及 支援 zonefs 的 nvme ssd 的整個儲存棧形態

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

其中對於ceph 這樣的應用來說 bluestore或者Seastore 這樣的後端引擎是直接管理裸裝置的,所以不需要檔案系統支援。當然如果需要,也可以透過一個核心支援的小型檔案系統zonefs 來進行資料訪問。

但是這個小型檔案系統過去簡單,它將每一個zone空間暴露為一個檔案,使用LBA0 來儲存superblock,並沒有複雜的inode/dentry 這種元資料的管理機制,在這個檔案系統上建立/刪除/重新命名都是不允許的。針對資料的寫還是類似zone storage的要求,即透過一個WP來進行寫,如果這個zone 空間對應的檔案被寫滿了,則WP 無法寫入直到 對這個zone 執行了reset ,才會將WP 重新移動到LBA0,從而可寫。

它對於Rocksdb 來說功能還不足,而且Rocksdb 只需要一個檔案 使用者態的 backend,雖然如是說,但是如何在rocksdb排程寫的時候分配一個最優的zone,如何選擇合適的時機刪除sst檔案(重置zone空間)都需要精心的設計在裡面。

後來,Hans 主導設計了 Zenfs 來作為Rocksdb 的Backend 來進行端到端的請求排程,且選擇最優的資料儲存方式 並且在降低寫放大(LSM-tree)、SSD 的磨損均衡、降低讀長尾 等都做了較多的探索。

大體架構如下:

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

接下來我們仔細看看 ZNS 內部的一些實現的特性 以及 Zenfs 的詳細設計實現。

2。2。1 ZNS 實現過程中的一些PR

ZNS的特性 需要核心支援,所以開發了ZBD(zoned block device) 核心子系統 來提供通用的塊層訪問介面。除了支援核心透過ZBD 訪問ZNS之外,還提供了使用者API ioctl進行一些基礎資料的訪問,包括:當前環境 zone 裝置的列舉,展示已有的zone的資訊 ,管理某一個具體的zone(比如reset)。

在 FIO 內部支援了 對 ZBD的壓測。

在近期,為了更友好得評估ZNS-ssd的效能,在ZBD 上支援了暴露 per zone capacity 和 active zones limit。

Zenfs 的設計 並 作為 rocksdb 的一個檔案系統backend。

這裡是對應修改程式碼行數的概覽:

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

從程式碼行數上來看,可以說是非常得輕量了。

2。2。2 Zenfs 的設計實現

2。2。2。1 為什麼zenfs如此看重rocksdb(LSM-tree 架構)

之所以ZNS 社群對Rocksdb 這麼看重,程式碼行數上的貢獻上可以說 在Rocksdb 上投入的精力遠超其他方面。

從LSM-tree原理上,我們可以看到幾點:

LSM-tree 的寫入是append 順序寫,這適配 ZNS 的 zone 架構來說簡直再合適不過。

LSM-tree 的compaction 也是順序寫一批資料,然後再集中刪除,這也符合 ZNS 的空間回收方式(每一個zone 狀態是Full的時候,想要重新寫,只有reset了)。在好的配置下相當於 SSD內部的GC 和 rocksdb的compaction 完美結合了。

LSM-tree on 傳統 ssd 的痛點比較明顯。讀方面:LSM-tree分層軟體架構對讀效能不友好(長尾較為嚴重),再加上ssd 的FTL GC會間接 讓長尾不可預估;引以為傲的順序寫優勢也因為SSD 內部的FTL 頻繁GC 導致寫效能抖動且相比於空載時的下降。這一些痛點在ZNS 下都能夠被很好的避免甚至完全解決。

拋開 LSM-tree 本身on ssd 的劣勢 之外,Rocksdb 則有一些自身特有的優勢,值得 ZNS 社群持續投入:

k/v 儲存領域裡應用廣泛,適合用於高速儲存介質(NVMe-ssd)

開源 且 擁有活躍的社群,社群也在持續跟進新的儲存技術。包括:io_uring / spdk 等

可插拔的儲存後端設計,實現一個fs backend,移植就非常容易(將zenfs 編譯到rocksdb 程式碼中就可以看出來)。

2。2。2。2 zenfs 詳細設計

先看一下總體Zenfs的系統架構概覽,這個圖是論文中的圖,更簡潔一些:

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

因為它要作為Rocksdb 的fs backend,負責和zoned block devcei 進行互動,那其繼承自

FileSystemWrapper

類的基本介面肯定是都實現了。

主要的元件如下幾個:

Journaling and Data.

Zenfs 定義了兩種型別的zones: journal 和 data。 程式碼中

ZonedBlockDevice

類管理的也就是兩個vector ,

meta_zones

io_zones

,下文統一稱為

Journal Zones

Data Zones

其中

Journal Zones

用來管理檔案系統的元資料,包括異常時恢復檔案系統的一致性狀態,維護檔案系統的superblock 以及 對映 wal 和 資料檔案到 zone中。

Data zones

則主要用於儲存 sst 這樣的資料檔案。

Extents.

Rocksdb 的資料檔案會被對映 寫入到一個extents 集合中

std::vector extents_

。其中一個extent 時一個變長但block對齊的連續LBA地址,而且會拿著一個標識當前sst的資訊 順序寫入到一段zone空間中,會用

ZoneFile

這個資料結構標識檔案以及屬於這個檔案的extents 陣列。每一個 zone空間能夠儲存多個extents,但是一個extent 不會跨越多個zone而存在。

Extent 的分配和釋放都是一個記憶體資料結構來管理的,當一個檔案變比或者這個extent的資料要持久化到磁碟 呼叫

Fsync/Append

時,記憶體的資料結構也會對應持久化到journal_zone之中。而且記憶體中這個資料結構會持續跟蹤extents的分配情況,當一個zone內的所有extents 所屬的檔案都被刪除,這個zone就可以被reset了,方便後續的 reuse。

Superblock.

Superblock 主要用來初始化Zenfs 或者 從磁碟異常恢復Zenfs 的狀態。Superblock 會透過unique id, 魔數和使用者選項 來標識屬於當前磁碟的Zenfs。這個唯一標識 是 UUID(unique identifier),允許使用者識別對應磁碟上的檔案系統,即當磁碟的碟符重啟或者外部插拔髮生變化的時候仍然能夠識別到這上面的檔案系統。

Journal.

Journal的主要工作是維護 superblock 和 WAL 以及 sst 和 儲存於zone中的extents的對映。 Journal的資料主要儲存在上圖中的 Journal Zones中,也就是 程式碼中的

meat_zones

,而且journal zone 是位於整個儲存裝置上的前三個永遠不會offline的 zone

ZENFS_META_ZONES

。其中任何時刻,總會有一個zone是處於active的, 也就是必須可寫的,不然這兩個zone 被closed 的話就無法跟蹤元資料了的更新了。

其中最開始的那個active zone 會有一個header,包含:sequence number(每當有一個journal zone被初始化的時候都會自增),superblock 資料結構,當前journal 狀態的一個snapshot。初始化的時候,header被持久化完成,整個zone剩下的capacity就可以開始接受新的data 資料更新了。

我們從一個 ZNS 磁碟初始化一個Zenfs的過程需要執行:

。plugin/zenfs/util/zenfs mkfs ——zbd=$DEV ——aux_path=/tmp/$AUXPATH ——finish_threshold=10 ——force

它會先執行

1

Zenfs::MkFS

所有的meta zone都會reset,並且在第一個meta zone上建立一個zenfs檔案系統,執行如下內容

寫一個superblock 的資料結構,包括sequence 的初始化 並持久化

初始化一個空的snapshot,並持久化。

2

Zenfs::Mount

從磁碟 Recovery 一個已經存在的zenfs 的幾個步驟如下:

現在是三個journal zones,最開始的時候需要先讀取三個 journal zones 的第一個LBA內容,從而確定每一個zone 的sequence,其中seq 最大的是當前的active zone(擁有最全的元資料新的zone)。

讀取active zone的header 內容,並且初始化 superblock 和 jourace state。

對 journal 的更新都會同步到到 header 的snapshot 中。

這兩步操作基本就構建好了一個完整的Zenfs 狀態,後續就會持續接受使用者的寫入。

寫入過程中 sst 的資料儲存是透過 儲存著extent 並由extent持久化到對應的zone空間中,那 如何選擇一個Data zone 來作為儲存當前檔案資料的呢? 因為不同的zone 在實際接受資料儲存時其 capacity 的容量是變化的。如果一個sst 檔案的儲存是跨zone的,那最後對一個zone的 reset 還需要考慮這個檔案 是否被刪除。

Best-Effort Alogthrim for Zone Selection

Zenfs 這裡開發了 Best-effort 演算法來選擇zone 作為 rocksdb sst 檔案的儲存。Rocksdb 透過對 WAL 和 不同 level 的 sst 設定不同的 write_hint 來表示這一些檔案的生命週期。

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

選擇哪一種 write_hint, 則透過如下邏輯進行:

Env

::

WriteLifeTimeHint

ColumnFamilyData

::

CalculateSSTWriteHint

int

level

{

if

initial_cf_options_

compaction_style

!=

kCompactionStyleLevel

{

return

Env

::

WLTH_NOT_SET

}

if

level

==

0

{

return

Env

::

WLTH_MEDIUM

}

int

base_level

=

current_

->

storage_info

()

->

base_level

();

// L1: medium, L2: long, 。。。

if

level

-

base_level

>=

2

{

return

Env

::

WLTH_EXTREME

}

else

if

level

<

base_level

{

// There is no restriction which prevents level passed in to be smaller

// than base_level。

return

Env

::

WLTH_MEDIUM

}

return

static_cast

<

Env

::

WriteLifeTimeHint

>

level

-

base_level

+

static_cast

<

int

>

Env

::

WLTH_MEDIUM

));

}

也就是從當前總層數開始,倒數兩層的sst 檔案擁有最長的生命週期,level0 擁有

WITH_MEDIUM

的生命週期,WAL 則擁有最短的生命週期

WITH_SHORT

回到Zenfs 選擇zone 的過程,總的來說就是讓 life_time 小的檔案儘量存放在和它 life_time接近的zone中,這樣更大機率得統一對整個zone 進行 reset :

(1) 對於新的寫入,直接分配一個新的zone

(2) 優先從 active zones 中進行分配,如果能夠找到合適的zone,則直接Reset 這個zone,並作為當前檔案的儲存。合適的zone 的條件是:如果當前檔案的lifetime 比 active zone 中最老的資料 還小,則當前zone 比較合適作為當前檔案的儲存;如果有多個active zone滿足這個條件,則選擇一個最近比較的active zone。

(3) 如果從active zone中沒有找到合適的zone,那直接分配一個新的zone。當然,分配的過程也就意味著判斷active zone個數有沒有超過

max_nr_active_io_zones_

,超過了則需要關閉一個 active zone,然後才能分配一個新的zone。

邏輯如下:

Zone

*

ZonedBlockDevice

::

AllocateZone

Env

::

WriteLifeTimeHint

file_lifetime

{

。。。

// best effort 演算法的邏輯

for

const

auto

z

io_zones

{

if

((

z

->

open_for_write_

&&

z

->

used_capacity_

>

0

&&

z

->

IsFull

())

{

// 主要就是拿著當前 zone 的lifetime 和當前檔案的file_lifetime (也就是write_hint)進行對比

// 如果檔案的life_time小,則當前zone 滿足儲存需求。

unsigned

int

diff

=

GetLifeTimeDiff

z

->

lifetime_

file_lifetime

);

if

diff

<=

best_diff

{

allocated_zone

=

z

best_diff

=

diff

}

}

}

。。。

// 如果從沒有為當前檔案找到找到合適的zone,那就得分配一個新的了

if

best_diff

>=

LIFETIME_DIFF_NOT_GOOD

{

/* If we at the active io zone limit, finish an open zone(if available) with

* least capacity left */

if

active_io_zones_

load

()

==

max_nr_active_io_zones_

&&

finish_victim

!=

nullptr

{

s

=

finish_victim

->

Finish

();

if

s

ok

())

{

Debug

logger_

“Failed finishing zone”

);

}

active_io_zones_

——

}

if

active_io_zones_

load

()

<

max_nr_active_io_zones_

{

for

const

auto

z

io_zones

{

if

((

z

->

open_for_write_

&&

z

->

IsEmpty

())

{

z

->

lifetime_

=

file_lifetime

allocated_zone

=

z

active_io_zones_

++

new_zone

=

1

break

}

}

}

}

。。。

}

AllocateZone 完成之後就可以 更新當前檔案在 分配的zone 中的extent(主要存放偏移地址和length),透過

IOStatus Zone::Append

進行檔案資料的實際寫入了。

總的來說,Zenfs 透過 Best-effort 演算法,根據 Rocksdb 配置的

write_hint_

儲存 data檔案和zone 接近的生命週期 來加速過期zone的回收,極大得減少了 ZNS 的空間放大問題,根據論文中的資料,說能夠保持空間放大在10% 左右(可以說是整個LSM-tree + SSD 的空間放大,資料沒問題的話已經很了不起了)。

當然,想要有這樣的測試資料,需要對rocksdb 的引數配置進行調整,可以透過執行 Zenfs下的一個指令碼來達到這個目的:

。/zenfs/tests/get_good_db_bench_params_for_zenfs。sh nvme2n1

可以獲取到官方推薦的一個配置,建議讓 target_file_size 和 zone 配置的大小對齊。

Zenfs 也有active_zone_limits 的限制,即我們在AllocateZone 函式中可以看到,分配一個新的zone 的話如果當前active zone 的個數達到了max_nr_active_io_zones_ ,需要先關閉之前的一個zone才行,也就是在 Rocksdb 中也會有 active zone個數的限制。當然這方面 Zenfs 也做了對應的測試,發現 active zone 的個數小於6的話 會對寫效能有影響, 但是達到12的話後面再增加對寫效能沒有太大的影響。

3 效能測試

3。1 環境搭建

1 需要有一塊西數提供的 ZNS-ssd

2 核心版本大多數功能是 5。9 才支援的,建議直接升級核心到5。12

3 依賴的安裝包:linux-util (blkzone) ,libzbd (負責和核心的ZBD 透過ioctl 進行互動),nvme-cli (檢視/升級 ZNS 韌體), Cmake, gflags

4 Zenfs 和 Rocksdb 的編譯 直接參考:

https://

github。com/westerndigit

alcorporation/zenfs/tree/master/

3。2 原始裝置的效能

在測試 ZNS-ssd 和擁有 7% op空間 以及 28% op空間的 block ssd 的穩態吞吐和延時的對比,測試是對擁有相同儲存介質對外暴露不同儲存介面 硬體進行的。

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

從資料中可以得到如下結論

Op 空間的增加 有助於最大寫吞吐的提升 以及 平均延時的降低

Block SSD 在寫入吞吐不斷增加時,後續因為底層的GC 無法達到target 的目標吞吐。對於op 空間越大的ssd,其target 吞吐能更接近實際 吞吐一些。

ZNS-ssd 的吞吐和延時不受 GC的影響,其實際吞吐和延時的增加都會隨著寫入target 的增加而線性增加,而且不需要額外的OP空間,可以說是效能表現極佳了。

測試 ZNS 的 fio指令碼如下:

併發寫多個zone:

[global]

filename=/dev/nvme3n1

group_reporting

bs=2M

rw=randread

norandommap

randrepeat=0

max_open_zones=14

ioengine=libaio

buffered=0

direct=1

runtime=60

time_based

#io_size set to 4 zones for each job (1077 * 2)

io_size=2154m

#size set for 2 zones (2 * 2g)

size=4g

numjobs=8

iodepth=1

zonemode=zbd

[randomzonetest1]

offset=0

[randomzonetest2]

offset=4g

[randomzonetest3]

offset=8g

[randomzonetest4]

offset=12g

[randomzonetest5]

offset=16g

[randomzonetest6]

offset=20g

[randomzonetest7]

offset=24g

[randomzonetest8]

offset=28g

[randomzonetest9]

offset=32g

[randomzonetest10]

offset=36g

[randomzonetest11]

offset=40g

[randomzonetest12]

offset=44g

併發壓測一個zone

[global]

filename=/dev/nvme3n1

zonemode=zbd

max_open_zones=14

direct=1

ioengine=libaio

numjobs=8

iodepth=4

group_reporting

size=100G

time_based

runtime=60s

[seqwrite]

rw=write # or write

bs=4K

3。3 應用測試效能

這裡主要是對Rocksdb 的測試,整個測試是在AMD Epyc 7302P CPU 和 128G 的記憶體上跑的。

對比了xfs on block ssd , f2fs on block ssd , f2fs on zns, zenfs 這四種 場景。Key: 20B, value :800b,帶壓縮。構造資料集是先 fillrandom 3。8 billion的key,再overwrite。

大體的吞吐結果如下:

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

可以看到fill random 時的效能,大家差異並不大,Zenfs 相比於xfs 也就高了10% 左右。但是, 當進行overwrite 有大量compaction進行的時候,Zenfs的排程策略就很有優勢了,其本省的資料檔案和相同生命週期的zone一起排程刪除 以及 ZNS 底層的zone的回收和compaction的結合,使得整個on Zenfs 的rocksdb 寫放大在原本compaction策略不變的情況下降到了最低,

寫吞吐提升了一倍多。

測試了寫效能,還得再看看讀效能。在之前的寫資料集的基礎上排程了讀,底層資料集大於記憶體5倍以上。

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

在資料集的基礎上,分別跑 readrandom, readwhilewriting with 20M/s rate limiter, readwhilewriting with no write limits。

可以看到readrandom 場景,ZNS 讀吞吐並不比其他的檔案系統差,同時如果有寫入且加上rate limiter,讀效能也能夠和其他測試保持接近。

如果不限制ratelimiter,保持讀吞吐的同時 寫吞吐也能夠極大得提升。

再看看對應的讀延時:

ZNS : 解決傳統SSD問題的高效能儲存棧設計(fs-->io-->device)

很明顯 on ZNS 的ssd 讀長尾得到了明顯的緩解,因為 zenfs 以及 F2FS on ZNS 都是端到端的資料訪問, 不會再走核心龐大的I/O排程了邏輯,在有寫入的讀場景,因為傳統的block-ssd 的主控排程無法分開排程讀寫,所以讀長尾會因為寫入的增加而增加。但這一些對ZNS-ssd 來說都沒有這樣的問題,所以ZNS 在讀上因為其本身硬體設計的特性保證了讀吞吐並不受影響的同時讀長尾相比於傳統的block ssd 顯著降低。

當然,這一些測試資料的細節還在本地環境復現中,後續會做一個補充。

不過總的來說,ZNS ssd 能夠從工業界的角度解決 傳統SSD 本身的內部實現上的問題(GC引起的寫放大,需要消耗過多記憶體儲存LBAs和物理地址的對映,需要OP空間進行GC),並且能夠在以前解決方案的基礎上進一步提升易用性,這符合當今高速發展的硬體和其特有的軟體棧相結合 來發揮高效能硬體本省效能的趨勢。不論是 PMEM 的硬體儲存棧 還是 SPDK的軟體儲存棧都希望將 前人(os)的部分 on 新硬體的冗餘設計拋開,從而達到降本增效的目的。

4 參考

[1。 ZNS: Avoiding the Block Interface Tax for Flash-based SSDs]

[2。 An Efficient Design and Implementation of LSM-Tree based Key-Value Store on Open-Channel SSD]

3。 https://github。com/westerndigitalcorporation/zenfs

4。 ZNS-SSD