1,File Systems Unfit as Distributed Storage Backends: Lessons from 10 Years of Ceph Evolution
作者首先總結了在分布式文件系統Ceph多年開發過程中的經驗教訓,然后介紹了團隊開發的新型用戶態存儲后端BlueStore——直接訪問存儲設備上的數據,並將元數據儲存在Key-Value Store中。目前,Ceph已經使用BlueStore作為默認的存儲后端,並且相較於使用傳統的POSIX文件系統有着顯著的性能提升。
具體來說,使用傳統的POSIX文件系統作為分布式文件系統中的存儲后端主要會遇到三個方面的挑戰:提供高效的事務支持、實現快速的元數據操作以及兼容新型硬件接口。以下會逐一介紹。
事務 事務接口可以將多個應用的動作整合為一個原子的操作,從而簡化應用程序的開發過程。一種方案是使用文件系統內部的事務機制,然而其有限的語義只能保證文件系統內部狀態的一致,而非向用戶提供應用層面的一致性保障(例如缺少回滾操作),因此這種方案實用性不高。另一種解決方案是在用戶態實現Write-Ahead-Log(WAL)來支持事務,但是這會導致頻繁調用fsync以持久化WAL和數據、同時也要處理非冪等操作等問題,使用Key-Value Store存儲元數據可以一定程度解決這些問題,但保證Key-Value Store與文件系統中元數據的一致性又引入了新的開銷。
元數據相關操作 在Ceph中常常需要對特定目錄下的文件進行遍歷(readdir),而這一操作的性能會隨着目錄文件數量的增長而下降,同時只返回無序的結果。利用傳統文件系統中解決這一問題,需要文件盡量均勻地分布在各個目錄中,即當文件數量超過閾值時對目錄進行分裂操作。然而這樣又會引入新的問題:inode數量增多不僅降低dentry cache的效率,也會增加小型I/O操作的次數,甚至使得數據分布更加零散,降低了空間局部性。
存儲硬件接口 作者提到近年來硬件廠商提出了諸如Shingled Magnetic Recording(SMR)和Zoned Namespace(ZNS)等新技術,這類技術可以增加存儲容量並且提升訪問性能,但需要在Zoned Interface下才可以更好地發揮效果。而傳統文件系統仍然使用的Block Interface並不能與其兼容,同時二者的更新方式(Overwrite和Copy-On-Write)也存在沖突。
以上的原因使作者團隊決定開發一個新型的存儲后端代替傳統文件系統,即BlueStore:一個在用戶態實現的直接與底層存儲設備交互的存儲后端。
如上圖所示,BlueStore將數據直接保存在存儲設備中,而元數據則先保存在RocksDB中,再通過給RocksDB搭配的輕量級定制文件系統BlueFS以將數據持久化至存儲設備中。首先,這樣的設計使得元數據只存在於RocksDB中,因此無需再試圖保證Key-Value Store與文件系統中元數據的一致,可以高效地支持事務。其次BlueStore中通過元數據的鍵值前綴將其組織成不同的Namespace,例如將元數據鍵值前K位相同的文件定義成屬於同一個文件夾,則通過改變K的值可以快速實現文件夾分裂操作。最后,由於BlueStore擁有對I/O棧的完全控制,其可以自由地決定使用何種硬件接口,同時由於使用Copy-On-Write的更新方式,BlueStore可以很好地兼容Zoned Interface。此外BlueStore還提供了一系列諸如高效Checksum、透明壓縮的特性。
從以上的測試結果可以看出,BlueStore在不同的I/O Size下的吞吐量都要顯著高於(50%-100%)原來的FileStore,並且性能更加穩定。
2,Aegean: Replication beyond the client-server model
當服務模型由傳統的Client-Server模式向如今的Microservice式轉變時,諸如Primary-Backup、Paxos和PBFT等容錯機制是否仍然適用?如何在保證容錯正確性的同時提升應用性能?密西根大學團隊帶來的Aegean回答了這兩個問題。
不同於傳統的Client-Server模式,如今的Server端大都采用多個服務互相交互的方式來響應客戶請求。考慮一個簡單的模型:來自於Client的請求首先到達Middle Service,然后Middle Service需要向Backend Service發起一個Nested請求,並根據該請求的結果來響應用戶。
然而,當Middle Service由多個備份構成時,傳統的容錯機制並不能正確地處理其中的Failure。作者以Primary-Backup協議舉了一個例子:若Primary在向Backend發出Nested請求之后崩潰,由於無法獲取之前Nested請求的返回值甚至無法得知之前向Backend發出了何種請求,Backup成為新的Primary之后也不能恢復至之前Primary的狀態。作者指出,這種情形的根本原因在於,與只有Client這一個外部觀察者的Client-Server模式不同,Backend也對於Middle Service來說也是一個觀察者。因此,每一層Service在向Client和其他Service發出請求之前,都必須在內部的備份之間達到一致的狀態。
作者提出了以下三種機制來達到這一要求:
- Service Shim 為了將Middle和Backend Service解耦開來,Aegean在每個備份中加入了一層Service Shim抽象:每當一個備份收到請求時,Service Shim會檢查其他備份中是否有大多數也收到了這一請求,只有檢查成功后,才會將請求交給備份執行。向Client(包括上層服務)返回結果時,Shim會向其所有備份發送結果,同時會將最近的返回值保存在Cache中。
- Response Durability 在傳統的容錯機制中,會在處理請求之前將輸入進行持久化以便之后恢復。然而在多服務交互的場景中,輸入不僅包括來自Client的請求,也包括來自Backend Service的返回值,因此這類返回值也應當在使用前被持久化。每當Middle Service的備份接收Backend的返回結果時,會向其他備份發出一份ACK,而只有當一個備份收到大多數的來自其他備份的ACK之后,才會使用其返回值。
- Taming Speculation 在Client-Server模式中往往采用推測執行的方式來提升性能:各個備份不需要事先就請求的執行順序達成共識,而是各自推測一個順序然后執行,最后在向Client返回之前,如果備份的狀態不一致再通過Rollback-Replay統一各個備份的狀態。然而在多任務交互的場景下,推測執行的過程中Middle Service往往會向Backend發出Nested請求,這樣一來最后僅僅統一Middle Service的各個備份便不足以保證一致性。因此,本文提出在推測執行的過程中,每當備份向外界發起任何請求之前,都需要插入Barrier來確保各個備份的狀態已經達到了一致。
除此之外,Aegean還實現了Request Pipeling機制以隱藏等待Backend返回結果的延時,同時不再保證Linearizability,而是保證會產生與沒有備份的服務產生一樣的結果,從而提升性能。
3,Taiji: Managing Global User Traffic for Large-Scale Internet Services at the Edge
背景、動機及系統概要: 在大型網絡服務中,數據中心直接與邊緣結點連接。數據中心負責絕大多數計算和存儲,而邊緣結點較數據中心更小,更靠近端用戶。其主要職責是引導用戶連接到最近的ISP,以及緩存和分發靜態的數據。但是當需要動態的內容時,用戶必須經過邊緣結點連接到數據中心。本工作提出的Taiji系統核心目標是在的大規模網絡服務中,1)平衡數據中心的利用率,2) 降低用戶請求的延遲。
Taiji的主要思路是通過將關聯程度高的用戶路由到同一個數據中心,來增強用戶數據的本地性以增加數據中心的緩存命中率,減少shard的遷移率,最終提升服務器的性能。
Taiji系統主要分為兩個部分:一個是Runtime,其負責根據數據中心的實時情況以及邊緣結點與數據中心的連接情況,生成某一段用戶數據應該發到哪個數據中心;一個是Traffic pipeline,其負責在邊緣結點上根據關聯度(connection-aware)生成具體的路由條目。Taiji已經在facebook中使用了超過四年。
Taiji系統設計: 傳統邊緣結點到數據中心使用的是靜態的映射,但是當數據規模擴大,靜態的映射很難維護。另外,在實際場景中,不同的邊結點在一天中的流量變化很大,且峰值流量與正常流量差距很大。因此靜態的映射不再使用。Taiji的提出就是為了解決上述問題。
Taiji的系統架構如上圖所示,其主要分為兩個部分:Runtime和Trafic Pipeline。
-
Runtime
Runtime是用來決定某一段流量應該根據預定的策略,要發送到哪個數據中心中。實質上是一個分配問題,其其最終會生成一個路由表。影響其決定的兩方面因素:1)數據中的狀況,如容量,使用量等,2)動態數據包括實際邊結點到數據中心的時延以及邊的流量。同時其會根據實際情況持續更新路由表。
-
Traffic pipeline
Pipeline是根據路由表,根據具體應用的關聯度來生成每個Edge LB的路由配置。其會利用social hashing將高度相關用戶會分到同一個bucket里面,而這個bucket里面的用戶會被路由到相同的數據中心內。Edge LB的作用就是按照用戶的請求的不同,將用戶放進合適的bucket中,然后將用戶的請求轉發到路由表中指定的數據中心內。其包含兩個部分:離線的的user to bucket的分配 (用的social hashing 分成不同的bucket),以及在線的bucket to datacenter的分配。
除此之外,Taiji的動態路由還有以下優勢:
- 適應不同產品不同的服務類型,如有的交互式的需要與數據中心的連接更穩定。
- 適應硬件異構,新的硬件可能動態地更新。
- 容錯,可以動態的調整路由。
Taiji性能測試: 圖(a)當DC1出現failure的情況下,使用Taiji依然能保持穩定且平均的數據中心利用率。圖(b)使用了不同資源的DC,而Taiji可以根據不同DC的實際資源分配合適的流量。圖(c)與靜態的路由配置(static circa 2015)相比,使用Taiji擁有更低的RTT。Lower Bound是假設鄰近的數據中心有無限的容量而直接路由到最近的數據中心得到的數據。
4,KVell: the Design and Implementation of a Fast Persistent Key-Value Store
這是本session的第一篇文章。文章提出了一個聽起來反直覺的想法:持久鍵值存儲系統,不應該繼續追求設備的順序訪問。之所以說反直覺,因為在傳統的存儲設備(如磁盤)上,順序訪問的性能要遠遠高於隨機訪問。如何保證存儲I/O是順序的,一直是存儲研究的一個重要問題。
然而這篇論文指出,隨着技術的不斷發展,現代的NVMe SSD除了擁有了更高的帶寬之外,其隨機訪問的速度已經和順序訪問相近。這種硬件性能的改變,也使得現有的存儲設計無法完全發揮出現有NVMe SSD存儲的性能。
論文主要關注在持久鍵值存儲系統(Persistent key-value stores)上。為了符合傳統存儲的順序訪問特性,現有的持久鍵值存儲系統使用 LSM(Log-structured Merge)或者B樹保存鍵值對。這兩種數據結構的設計盡量避免隨機訪問,保證磁盤上數據是有序的。而大量的計算被耗費在了維護這種有序和進行數據同步上,使得CPU成為了整個系統的瓶頸。在下圖中可以看出,無論是在LSM還是在B樹上,鍵值存儲的I/O帶寬還遠未達到設備瓶頸,然而CPU卻已經基本占滿。
除了CPU問題之外,另外一個問題是LSM和B樹都會產生嚴重的性能波動。從下圖中可以看出,在波動時,系統的性能最低不足1萬操作/秒,而系統的最高性能要超過12萬操作/秒。不管是LSM還是B樹,造成性能抖動的根本原因在於數據結構中的維護操作,如LSM中的compaction操作。
因此,論文提出了一種新的設計,KVell。與此前的設計不同,KVell的設計不再一味強調順序訪問,而是從整個系統的瓶頸角度試圖降低CPU的計算負擔。KVell提出的設計遵循以下四個原則:
- shared-nothing,通過將數據結構在多個CPU上進行划分,避免由於數據共享造成的同步開銷。換句話說,每個工作線程負責一個鍵的子集,且每個工作線程維護自己的一份數據結構來管理自己所負責的鍵。
- 磁盤上數據無需保證有序,數據直接無序的持久化在其最終的存儲位置上,避免昂貴的排序操作。同時,通過使用內存中的有序索引來提供高效的scan操作。
- 不再強行保證順序訪問,但是依然將I/O進行批處理(batch),減少系統調用帶來的開銷。
- 不適用提交日志(commit log),每次更新將數據直接持久化在它們最終存儲的位置,避免不必要的I/O操作。
通過遵守這些原則,可以減輕CPU的計算工作量,提升鍵值存儲系統的性能。同時,這些原則可以降低性能抖動,保證操作在吞吐量和延遲上的可預測性。但這些原則也並非只有好處,比如shared-nothing的設計,會帶來負載不均衡的問題。又比如磁盤上無序的數據存儲,當讀取一個鍵值對時,需要把整個數據塊都讀取到內存,會影響到scan的性能。
在測試部分, 這篇論文首先使用YCSB以及其中的標准負載進行測試(如上圖)。其中可以看出隨着CPU的計算瓶頸被解決,Kvell的性能提升對於其他系統是十分明顯的。在YCSB-E這個負載中,有大量的scan操作,因而kvell的性能會有些影響。在uniform的分布下,KVell的性能略遜於RocksDB。而在Zipf的分布下,由於熱點數據大多被緩存,KVell在磁盤上的scan操作較少,KVell在其他設計方面的優勢便發揮了出來。於是其性能依舊領先於其他系統。
下圖給出了KVell在存儲I/O帶寬和CPU占用率方面的情況。從圖中可以明顯地看出設備的最大I/O帶寬被占滿,且CPU的占用率被減低到了不足40%。
下圖則給出了KVell在性能波動方面的情況。從圖中可以看出,KVell的性能波動和其他系統相比非常之小。這說明KVell可以持續的提供穩定的吞吐和延遲保證。
5,Recipe: Converting Concurrent DRAM Indexes to Persistent-Memory Indexes
非易失性內存(NVM)是一種新型存儲介質。它同時結合了存儲和內存的性質。具有接近DRAM內存一樣的性能,可以像DRAM一樣被以CPU以字節粒度進行訪問,同時可以像磁盤、固態硬盤等存儲設備一樣具有比較大的容量、能保證寫入其中的數據在失去電力之后是持久存在的。
NVM的出現使得存儲系統的設計可以變得非常不同,因而有很多不同的索引結構為了NVM進行了重新設計,其中包括哈希表、B+樹、Trie、Radix樹等。這些索引結構在NVM出現之前,都曾經有為DRAM而設計的實現,那么有沒有一種方法可以直接將這些為DRAM設計的數據結構轉化成為NVM上的數據結構呢?這篇文章便是回答了這個問題。
由於NVM上的數據可以持久保存,為NVM設計的數據結構需要考慮崩潰一致性。這其中最大的問題來自於CPU緩存。在使用傳統存儲設備時,我們往往會使用DRAM作為傳統設備的緩存。然而,NVM直接被放入內存插槽中,CPU可以像訪問DRAM一樣訪問NVM。CPU緩存便成為了CPU和NVM之間的緩存。然而由於CPU緩存並非是非易失的。這意味着,當電力中斷的時候,雖然NVM中的數據是持久保存的,但CPU緩存中的數據會丟失。同時由於CPU緩存的換出機制(eviction)由硬件來管理,CPU發出的兩個順序寫操作(先寫A,再寫B),在被CPU緩存之后,到達NVM的順序可能是相反的(B比A先到達NVM)。若在兩次操作中間產生了斷電,則會造成數據的不一致。
這篇文章的思路,來自於發現某些特定的並行DRAM索引結構中隔離性(isolation)可以通過很小的修改轉變為相同結構的崩潰一致性(crash consistency)。如為了保證DRAM索引結構的高性能,索引結構的讀者(readers)經常被設計成非阻塞的。為了保證讀者不被同時發生的寫者(writer)所影響,讀者往往可以檢測和容忍一些臨時的非一致性(如讀者可以自動忽略其看到的重復的鍵值對)。此外,寫者往往也可以檢測到這些非一致性,並進行相應的修正。
因此,本文提出Recipe,通過分析DRAM索引結構中的隔離性設計,針對三種情況提出了三條方法,可以幫助開發人員將DRAM上的索引結構轉變為NVM上具有崩潰一致性保證的數據結構。
方法1:若更新操作是通過單個的原子寫進行,可直接在每個寫的后面加上flush(緩存刷除)操作,並通過fence將原子寫操作與其他操作隔離開,保證這些寫操作完成的順序(如下圖)。通過這種轉化,便可在索引結構從DRAM轉化到NVM時,保證崩潰一致性。
方法2:這個方法需要的條件有些復雜:1) 讀者和寫者均是非阻塞的,這意味着它們均使用原子指令,而非被鎖保護。2)索引結構中的寫操作,需要由一系列有順序的原子寫組成。3) 當讀者可以觀察到不一致狀態的時候(比如一系列的原子寫中,只有前幾個原子寫被完成了),可以檢測到並容忍這些不一致。4) 當(另外一個)寫者檢測到不一致狀態的時候,可以通過幫助機制(helping mechanism)來修復不一致狀態(比如需檢測到這一系列原子寫被完成到了哪一步,並可以從這一步開始,將之后的一系列原子寫操作繼續做完)。
在方法2中,當DRAM索引滿足了上述條件,便可以簡單的在每個store之后加入clush和fence操作,來將DRAM索引轉變為NVM索引(如下圖)。之所以可以簡單的加入flush和fence操作,主要因為在這種條件下,DRAM數據結構中的讀者和寫者均已經具備了一定的檢測和容忍、恢復不一致狀態的性能。
方法3:方法3和方法2的條件類似,不同之處在於,寫者由鎖進行保護。在被鎖保護的情況下,DRAM索引中的寫者是沒有機會看到不一致的狀態的。因此,這種條件下的寫者在DRAM實現中並沒有修復不一致性的能力。
在方法3中,除了需要在每個store操作之后加入flush和fence之外,還需要開發人員手動添加一些機制,來檢測到不一致性,並實現一些幫助機制,來對不一致性進行修復。
方法3所涉及到的修改量是最大的。
除了轉換方法外,論文還提供了一種新的方法用於檢測NVM上的索引在crash之后是否正確地進行恢復,以消除不一致性。有興趣的讀者可以關注一下論文的這一部分,此處便不再贅述。
通過上述的方法,論文對一些DRAM上的索引進行了修改,並進行了評測,下圖展示了論文中所設計到的DRAM中的索引,以及每個索引所應用到的轉換方法。
除了性能之外,本工作的另外一個重要關注點在於進行轉換的代價,下圖展示了轉換不同索引結構所涉及到的代碼修改量。我們可以看出,通過Recipe方法進行轉換,最復雜的修改是針對masstree,修改量在200行左右,占原代碼總量的9%。
在性能方面,通過Recipe進行轉換過的代碼,其性能甚至優於專門為NVM設計的結構(如下圖)。
6,SplitFS: Reducing Software Overhead in File Systems for Persistent Memory
這篇文章同樣是在文件系統上的工作。但是卻沒有上面我們的工作那么激進。同樣是用戶態NVM文件系統的設計,在這篇文章中。其將文件的數據操作和元數據操作進行分離。將數據操作部分由用戶態文件系統庫來完成。而元數據部分依然由內核中的文件系統完成。這種職責的分離,也是SplitFS名字的由來。
下圖是SplitFS的高層設計圖。從圖中可以比較明顯的看出,所有的文件讀寫操作由用戶態直接訪問文件,而元數據操作則通過內核中的Ext4-DAX文件系統來進行。在實際操作中,所有的文件均是保存在Ext4-DAX文件系統中。U-Split通過mmap的方式,將Ext4-DAX中的文件映射到用戶態空間,此后任何的文件讀寫操作均可以在U-Split中完成。元數據操作則需要交給Ext4-DAX進行處理。
看起來這樣就結束啦?
事實上並沒有。通過上面講的方法,有一些情況是比較難處理的。比如:假如文件通過append變大了怎么辦?要知道mmap操作是只能映射文件現有大小的,對於一個文件的append操作,是不能通過mmap來進行的。
為了處理append操作(以及后面會提到的原子寫操作),文章提出了staging file的概念。此處staging表明這文件里面的寫是已經被發出,但是還未被提交的。staging file是預先被分配,並被預先mmap到用戶態的一些文件。
當應用程序發出對一個文件的append操作之后,U-Split會將append的數據寫入到staging file對應的mmap區域中,並在U-Split中進行記錄,以保證后續的讀能夠讀到這些append的數據。然而數據並不能一直存放在staging file中。當調用fsync時,staging file中的數據需要被寫入到其對應的原文件中。在此過程中,為了避免數據搬移,SplitFS提出了一個relink的操作。
relink的設計的初衷,是為了快速的將staging file中的data block移交到原文件中。relink的接口是這樣的:relink(file1, offset1, file2, offset2, size),意味着其作用是將file1中的offset1開始的size個字節,挪到file2中的offset2開始的位置。注意,此操作涉及到Ext4-DAX中文件的存儲,因此只能在kernel中進行。為了避免對Ext4-DAX的侵入式修改,SplitFS通過ioctl的方法給Ext4-DAX增加了relink的功能。
上圖中展示了staging和relink的用法過程。最初,U-Split中有一些空閑的staging file,當目標文件進行append的時候,會找到一個空閑staging file並將其分配給此目標文件。后續的append均寫入到此staging file已經mmap好的區域中。對這些append的讀,同樣會被重定向到staging file。在fsync的時候,通過relink將數據從staging file中移動到目標文件上。但是mmap的區域依然保留,因此在U-Split中可以直接將mmap區域的邏輯對應關系進行修改即可。
論文中還對文件系統的語義進行了討論。U-Split的實現可以滿足三種不同的語義,如下圖,包括POSIX、sync和strict。
POSIX模式是最弱的語義,與Ext4-DAX相同,數據和元數據的更新均是異步的,意味着數據和元數據在fsync之后才能保證持久化,文件系統在崩潰后根據元數據將文件系統恢復到一個一致的狀態。
在sync模式下,所有的修改都是同步的,意味着一旦數據操作返回到了應用程序,這個元數據的修改已經被持久化了。這種情況下,應用程序無需再調用fsync操作。但是此時的數據更新依然無法保證原子性。這意味着,當向文件中寫入5K的數據,在寫入操作返回之前發生崩潰,則無法保證這5K中有多少數據被持久化。
在strict模式下,則保證了數據寫入的原子性。通過使用staging file和relink操作,SplitFS在strict模式下可以保證每個文件寫操作都是原子地被持久化的。
論文中有大量的測試來展示SplitFS的性能提升。在下圖中給出了SplitFS中各個關鍵技術所帶來的性能收益。可以看出用戶態的U-Split、staging file和relink均帶來了不同程度的性能收益。更多的性能評測,可以參看論文中的測試部分。
本文內容轉載自: