天天看點

70%讀寫性能提升!基于UCloud對象存儲US3的使用者态檔案系統設計

作者:UCloud雲計算

前言

為了解決在資料備份場景中的可靠性、容量、成本問題,越來越多的使用者傾向于使用對象存儲來進行備份。然而,有些場景下通過對象存儲US3來備份還是不夠友善,甚至不适用。比如在資料庫備份場景下,如果直接使用對象存儲備份,可能需要先把資料庫通過mysqldump做邏輯備份,或者采用xtrabackup做實體備份到本地,然後使用基于對象存儲的SDK的工具把備份檔案上傳到對象存儲,備份過程繁瑣。再例如服務的日志歸檔備份,為降低成本可以将日志存儲到對象存儲US3中,通過SDK或者工具來操作,不僅需要編寫備份代碼,而且管理複雜。如果能提供一種以POSIX接口遠端通路對象存儲的方式,就可以很好地解決上述問題。

開源方案實踐

已經有一些開源的項目将對象存儲中的bucket映射為檔案系統,如s3fs和goofys等,在使用這些開源方案的時候,我們發現了一些問題。

s3fs

s3fs通過FUSE将s3和支援s3協定的對象存儲的bucket挂載到本地(FUSE的介紹詳見下文)。通過對s3fs進行測試後,我們發現其在大檔案的寫入方面性能特别差,研究其實作過程後,我們發現s3fs在寫入時會優先寫入本地臨時檔案,然後以分片上傳的方式将并發的将資料寫入到對象存儲。如果空間不足,則會以同步的方式将分片上傳,代碼如下:

ssize_t FdEntity::Write(const char* bytes, off_t start, size_t size){ // no enough disk space if(0 != (result = NoCachePreMultipartPost())){ S3FS_PRN_ERR("failed to switch multipart uploading with no cache(errno=%d)", result); return static_cast(result); } // start multipart uploading if(0 != (result = NoCacheLoadAndPost(0, start))){ S3FS_PRN_ERR("failed to load uninitialized area and multipart uploading it(errno=%d)", result); return static_cast(result); }}

由于我們的主要使用場景為大檔案的備份,基于雲主機硬碟成本等方面的考慮,我們決定放棄這一方案。

goofys

goofys是用go實作的将s3以及部分非s3協定的對象存儲挂載到linux的檔案系統,測試後,我們發現goofys主要有三個問題:

寫入沒有進行并發控制。在大檔案的寫入場景下,goofys同樣将檔案進行分片,然後每個分片開一個協程寫入到後端存儲。對象存儲一般通過HTTP協定進行通信,由于請求是同步的方式,在不限制并發數的情況下會有大量的連接配接,消耗大量的記憶體等資源。

讀取采用同步方式,性能很差。FUSE有兩種讀取模式async和sync,通過挂載時的設定去選擇,goofys強制使用了sync模式,并且預讀的實作為亂序讀取超過三次後停止預讀,代碼如下:

if !fs.flags.Cheap && fh.seqReadAmount >= uint64(READAHEAD_CHUNK) && fh.numOOORead < 3 { ... err = fh.readAhead(uint64(offset), len(buf)) ...}

fh.numOOORead為亂序讀取的次數,FUSE子產品會對超過128k的IO進行拆分,以128k對齊。簡單介紹一下FUSE的同步讀取和異步讀取模式的差別。核心的讀取一般入口是在底層檔案系統的read_iter函數,然後調用VFS層的generic_file_read_iter,該函數内部實作會通過調用readpages進行預讀。如果預讀後沒有對應的page則會調用readpage讀取單頁。由于goofys不支援該設定,我們通過對s3fs設定不同的配置來測試,然後抓取讀取時的調用棧對比其中的差別。設定了異步讀取模式的讀堆棧如下所示:

fuse_readpages+0x5/0x110 [fuse]read_pages+0x6b/0x190__do_page_cache_readahead+0x1c1/0x1e0ondemand_readahead+0x1f9/0x2c0? pagecache_get_page+0x30/0x2d0generic_file_buffered_read+0x5a50xb10? mem_cgroup_try_charge+0x8b/0x1a0? mem_cgroup_throttle_swaprate+0x17/0x10efuse_file_read_iter+0x10d/0x130 [fuse]? __handle_mm_fault+0x662/0x6a0new_sync_read+0x121/0x170vfs_read+0x91/0x140

其中vfs_read是系統調用到vfs層的入口函數。之後會調用到readpages進行多頁的讀取。fuse_readpages将讀請求發給使用者态檔案系統,進而完整整個讀取流程。同步讀取模式的堆棧如下所示:

fuse_readpage+0x5/0x60 [fuse] generic_file_buffered_read+0x61a/0xb10 ? mem_cgroup_try_charge+0x8b/0x1a0 ? mem_cgroup_throttle_swaprate+0x17/0x10e fuse_file_read_iter+0x10d/0x130 [fuse] ? __handle_mm_fault+0x662/0x6a0 new_sync_read+0x121/0x170vfs_read+0x91/0x140

和異步流程相同,依然是在generic_file_read_iter中進行讀取,當讀取之後沒有對應的頁,會嘗試讀取單頁。相關代碼如下,核心版本基于4.14:

no_cached_page: /* * Ok, it wasn't cached, so we need to create a new * page.. */ page = page_cache_alloc_cold(mapping); if (!page) { error = -ENOMEM; goto out; } error = add_to_page_cache_lru(page, mapping, index, mapping_gfp_constraint(mapping, GFP_KERNEL)); if (error) { put_page(page); if (error == -EEXIST) { error = 0; goto find_page; } goto out; } goto readpage;

如果設定了同步方式進行讀取,FUSE子產品會無效核心的預讀,轉而進入到no_cached_page讀取單頁。是以同步模式下落到使用者态檔案系統的讀IO有大塊的readpagesIO和readpage的4K單頁IO,由于offset存在相同,goofys會判斷為亂序的讀取,超過3次後停止預讀,由于每次和US3的互動都是4K的GET請求,性能會比較差,難以滿足使用者的需求。

分片上傳的大小不固定,無法适配US3 。US3目前的分片大小固定為4M,而goofys的分片大小需要動态的去計算,并手動修改進行适配,代碼如下:

func (fh *FileHandle) partSize() uint64 { var size uint64

if fh.lastPartId < 1000 { size = 5 * 1024 * 1024 } else if fh.lastPartId < 2000 { size = 25 * 1024 * 1024 } else { size = 125 * 1024 * 1024 }

...

}

同時,s3協定本身沒有rename的的接口,s3fs和goofys的rename都是通過将源檔案内容複制到目标檔案,然後删除源檔案實作的。

而US3内部支援直接修改檔案名,US3FS通過使用相關的接口實作rename操作,相比s3fs和goofys性能更好。同時s3fs和goofys挂載US3的bucket都需要走代理進行協定的轉換,使用US3FS則減少了這一IO路徑,性能上更有優勢。

通過對s3fs和goofys的實踐,我們發現兩者在US3的備份場景上的性能有一些問題,同時适配的工作量也比較大,基于此,我們決定開發一款能夠滿足使用者在資料備份場景需求的,依托對象存儲作為後端的檔案系統。

US3FS設計概述

US3FS通過FUSE實作部分POSIX API。在介紹US3FS實作之前,先簡單介紹一下Linux的VFS機制和FUSE實作(有這部分基礎的朋友可直接跳過)。

VFS

VFS,全稱Virtual File System,是linux核心中一個承上啟下的虛拟層,隸屬于IO子系統。對上,為使用者态應用提供了檔案系統接口;對下,将具體的實作抽象為同一個函數指針供底層檔案系統實作。

linux檔案系統中的中繼資料分為dentry(directory entry)和inode,我們知道,檔案名并不屬于檔案的中繼資料,為了優化查詢,vfs在記憶體中建立dentry以緩存檔案名和inode的映射以及目錄樹的實作。單機檔案系統的實作,dentry隻存在于記憶體中,不會落盤,當查找某個檔案時記憶體沒有對應的dentry,vfs會調用具體的檔案系統實作來查找對應的檔案,并建立起對應的資料結構。inode緩存了一個檔案的中繼資料,如大小,修改時間等,會持久化到硬碟中,資料的讀寫通過位址空間找到對應的page和block device進行讀寫。

FUSE

FUSE,全稱Filesystem in Userspace,使用者态檔案系統,我們知道,一般直接在核心态實作某個特性是比較痛苦的事情,通常核心的debug比較困難,而且稍不注意就會陷入到核心的各種細節而無法自拔。FUSE就是為了簡化程式員的工作,将核心的細節隐藏起來,提供一套使用者态的接口用于實作自己的檔案系統,使用者隻需要實作對應的接口即可。核心态的FUSE子產品和使用者态的FUSE庫的互動通過/dev/fuse進行通信,然後調用使用者自己的實作。當然,缺點就是增加了IO路徑以及核心态/使用者态的切換,對性能有一定影響。

70%讀寫性能提升!基于UCloud對象存儲US3的使用者态檔案系統設計

中繼資料設計

US3FS通過實作FUSE的接口,将US3中bucket的對象映射為檔案,和分布式檔案系統不同,沒有mds(metadata server)維護檔案中繼資料,需要通過HTTP向us3擷取。當檔案較多時,大量的請求會瞬間發出,性能很差。為了解決這一點,US3FS在記憶體中維護了bucket的目錄樹,并設定檔案中繼資料的有效時間,避免頻繁和US3互動。

這也帶來了一緻性的問題,當多個client修改同一bucket中的檔案,其中的緩存一緻性無法保證,需要使用者自己取舍。為了提升檢索的性能,檔案并沒有像對象存儲以平鋪的方式放在整個目錄中,而是采用了傳統檔案系統類似的方式,為每一個目錄建構相關資料結構來儲存其中的檔案,同時inode的設計也盡量簡潔,隻儲存必要字段,減少記憶體的占用。

目前Inode中儲存的字段有uid,gid,size,mtime等,通過US3的中繼資料功能在對象中持久化。例如下圖所示,在US3的bucket中有一個名為"a/b/c/f1"的對象,在檔案系統中,會将每一個“/"劃分的字首映射為目錄,進而實作左邊的目錄樹。

70%讀寫性能提升!基于UCloud對象存儲US3的使用者态檔案系統設計

IO流程設計

對于資料的寫入,US3支援大檔案的分片上傳。利用這一特性,US3FS通過将資料寫入cache,在背景将資料以分片上傳的方式,将資料以4MB的chunk寫入到後端存儲中。分片上傳的流程如下圖所示,通過令牌桶限制整個系統的寫入并發數。每個分片寫入的線程都會擷取令牌後寫入,通過當檔案close時寫入最後一個分片,完成整個上傳流程。

檔案的讀取通過在US3FS的cache實作預讀來提升性能。kernel-fuse自身對資料的讀寫進行了分片,在不修改核心的情況下,IO最大為128K。而大檔案的讀取場景一般為連續的大IO,這種場景下IO會被切成128K的片,不做預讀的話,無法很好的利用網絡帶寬。US3FS的預讀算法如下所示:

70%讀寫性能提升!基于UCloud對象存儲US3的使用者态檔案系統設計

如圖所示,第一次同步讀取完成後,會往後進行目前長度的預讀,并将預讀的中點設定為下次觸發預讀的trigger。之後的讀取如果不連續,則清空之前的狀态,進行新的預讀,如果連續,則判斷目前讀取的結束位置是否不小于觸發預讀的偏移,如果觸發預讀,則将預讀視窗的大小擴大為2倍,直到達到設定的門檻值。之後以新的視窗進行預讀。如果未觸發,則不進行預讀。預讀對順序讀的性能有很大提升。鑒于US3FS使用場景多為大檔案的場景,US3FS本身不對資料進行任何緩存。在US3FS之上有核心的pagecache,當使用者重複讀取同一檔案時pagecache能夠很好的起作用。

資料一緻性

由于對象存儲的實作機制原因,目前大檔案的寫入,在完成所有的分片上傳之前,資料是不可見的,是以對于US3FS的寫入,在close之前,寫入的資料都是不可讀的,當close後,US3FS會發送結束分片的請求,結束整個寫入流程,此時資料對使用者可見。

對比測試

在并發度為64,IO大小為4M測試模型下,40G檔案的順序寫和順序讀進行多次測試,平均結果如下:

70%讀寫性能提升!基于UCloud對象存儲US3的使用者态檔案系統設計

測試過程中,goofys的記憶體占用比較高,峰值約3.3G,而US3FS比較平穩,峰值約305M,節省了90%記憶體空間。s3fs表現相對較好,因為使用本地臨時檔案做緩存,是以記憶體占用比較少,但是寫入檔案比較大,硬碟空間不足時,性能會下降到表格中的資料。

在順序讀的測試中,測試結果可以驗證我們的分析,goofys由于本身設計的原因,在這種場景下性能無法滿足我們的要求。另外在測試移動1G檔案的場景中,對比結果如下:

70%讀寫性能提升!基于UCloud對象存儲US3的使用者态檔案系統設計

可見在移動需求場景下,特别是大檔案居多的場景,通過US3FS能提升上百倍的性能。

總結

總而言之,s3fs和goofys在大檔案的讀寫場景下各有優劣,相比之下,US3自研的 US3FS 無論是讀還是寫都有更好的性能,而且和US3的适配性更強,更易于拓展。

繼續閱讀