深入理解GlusterFS之POSIX介面
(TaoCloud團隊原創 作者:林世躍@TaoCloud)
FUSE是使用者空間的檔案系統介面,FUSE核心模組為普通應用程式與核心虛擬檔案系統VFS的互動提供了一個橋樑。基於FUSE使用者空間模組,開發人員可以不必瞭解VFS核心機制就能快速便捷地開發POSIX相容的檔案系統互動介面。
本文主要介紹GlusterFS基於FUSE的POSIX檔案系統介面的實現機制和工作原理,給出通過修改FUSE讀寫資料塊大小提升大I/O頻寬效能的具體方法,並在分析FUSE瓶頸的基礎上提出進一步的優化思路。
FUSE專案簡介
FUSE(File system in User Space)是一個使用者空間的檔案系統框架,通過FUSE開發人員可以在使用者態實現檔案系統,並且不需要特權使用者的支援。使用 FUSE,可以像可執行二進位制檔案一樣來開發檔案系統,它們需要連結到 FUSE 庫上。換而言之,這個檔案系統框架並不需要您瞭解檔案系統的內幕和核心模組程式設計的知識。
FUSE作為類UNIX系統平臺上的使用者空間檔案系統就是為了非特權開發者能夠在使用者層開發一套功能完備檔案系統。2.8版本之前,所有的模組是在使用者態,高於2.8版本的FUSE核心模組已經移植進去作業系統的核心。如果使用2.8版本以上則需要從新編譯Linux核心程式碼。對於讀寫虛擬檔案系統來說,FUSE是個很好的選擇。
使用FUSE可以開發功能完備的檔案系統,其具有簡單的 API 庫,可以被非特權使用者訪問,並可以安全的實施。更重要的是,FUSE以往的表現充分證明了其穩定性。在FUSE基礎之上,使用者空間的檔案系統設計就被極大簡化了。基於FUSE的檔案系統的實現例項包括GlusterFS,MooseFS,SSHFS,FTPFS,GmailFS 等著名專案。
(1)FUSE特點
1.1. 庫檔案簡單,安裝簡便;
1.2. 模組化,可重構某個模組;
1.3. 執行安全,系統使用穩定;
1.4. 使用者態和核心態介面高效;
1.5. 支援C/C++/JavaTM 繫結;
(2)FUSE模組組成
FUSE可以分成3個模組:FUSE檔案系統模組、FUSE裝置驅動模組、FUSE使用者態模組。FUSE裝置驅動模組主要是作為FUSE檔案系統模組與FUSE使用者態模組的通訊,交換資料作用。這裡的FUSE裝置驅動相當於一個代理。當用戶使用FUSE掛載了一個客戶端,每次的請求會寫入連結串列,這時候FUSE的使用者態模組監控到資料,讀取解析之後,執行相應的操作。操作結束返回到FUSE裝置驅動,最後返回掛載點應答。FUSE檔案系統核心模組實現與VFS的互動和處理,而具體的檔案系統I/O則由使用者空間程式處理,基於FUSE提供的使用者態lib庫可以實現POSIX相容互動介面。
(3)FUSE工作流程
使用FUSE掛載一個客戶端,應用程式執行呼叫系統函式write寫資料,write呼叫vfs_write,vfs虛擬檔案系統在接受到上層的寫請求會根據fuse的註冊的函式呼叫fuse_file_aio_write寫入到request pending queue,然後進入睡眠等待應答。使用者態檔案系統會啟動一個守護程序去輪詢fuse裝置,最後會呼叫fuse_kern_chan_receive讀取request pending queue的資料,解析request pending queue資料執行相應的write操作。當用戶態執行完相應的寫操作完成之後,這時候會呼叫fuse_reply_write應答回到fuse裝置。最後返回掛載點處的應用程式。
下圖顯示GlusterFS使用FUSE裝置於掛載點應用程式的通訊過程。雖然FUSE提供的使用者態程式碼實現了和FUSE裝置驅動的互動,而GlusterFS重構了部分程式碼。所以下圖的應答回FUSE裝置是不一樣的函式。
圖中省略了核心機制,只是呈現了write的一個流程以及使用者態部分GlusterFS實現,應用在掛載點寫一個塊經過vfs,vfs呼叫FUSE註冊的相應函式。而GlusterFS在這裡相當於fuse模組中的fuse使用者態模組。掛載點之上的應用和glusterfs經過了fuse裝置的通訊。
(4)VFS/FUSE/GlusterFS關係
Linux中檔案系統是一個很重要的子系統,vfs作為了Linux一個抽象的檔案系統,提供了統一的介面,遮蔽了所有的底層的磁碟檔案系統的型別提供了一套統一的介面,這樣在使用者態的使用者需要檔案系統程式設計的時候,只需要關注vfs提供的API(posix介面)。在Linux平臺有眾多的磁碟檔案系統,比如xfs、ext4、btrfs,磁碟檔案系統在格式化初始化時候都會在vfs註冊自己的資訊以及相關的ops操作,這樣在掛載之後上層應用操作檔案時候,vfs則能夠在註冊資訊裡面選擇相應的磁碟檔案系統,磁碟檔案則會選擇出相應的操作執行。而fuse不同於磁碟檔案系統,fuse作為一個使用者態的檔案系統,在呼叫register_filesystem(struct
file_system_type *)函式在vfs虛擬檔案系統中註冊資訊以及fuse的ops操作之後把如果實現檔案的讀寫等操作留給了開發人員。這樣在用fuse的掛載目錄下執行檔案系統類操作,經過vfs層,vfs會選擇相應的fuse註冊函式執行。這時候fuse會把請求寫入到等待隊列當中,進入睡眠等待上層應用處理。glusterfs檔案作為一個分散式檔案系統(使用者態類)這時候會啟動執行緒去輪詢讀fuse的裝置,得出請求的ops型別,執行結束之後返回fuse裝置。解析執行操作,具體講解在下一節。
(5)Dokan專案簡介
Dokan(https://dokan-dev.github.io)是一個開源的Windows平臺下的使用者態檔案系統,它的作用功能和FUSE一樣,被稱為Windows平臺下的FUSE。Dokan為windows平臺開發者提供了一個檔案系統的開發模組,開發者能夠在windows下方便快捷實現檔案系統客戶端,可以不必使用CIFS協議掛載,從而獲得更高的安全性和高效能。Dokan在2003年後有一段時間停止了程式碼更新,而且有開發人員發現記憶體洩漏和穩定性等問題。目前Dokan已經重新開始更新,開源社群活躍度有了很大提升,keybase/seafile等專案已在使用。
GlusterFS POSIX介面實現
GlusterFS是如何利用FUSE來實現分散式檔案系統介面呢?在介紹GlusterFS的fuse層之前,首先從整體介紹一下GlusterFS的堆疊式模組化設計思想和主要函式的呼叫。
GlusterFS實現了副本/條帶/糾刪碼等檔案儲存模式,為在不同的應用場景提供了不同的解決方案,並且實現了很多效能層功能來提升效能,比如io-cache、預讀、回寫等。GlusterFS採用了堆疊式設計,這樣的設計模式一方面流程清晰簡潔,另一方面功能模組之間互不影響,使得使用者能在特定環境下可以移除不必要的功能,也有利於開發者實現自己實現的功能模組。
在程式碼級別層上,GlusterFS使用了xlator結構體定義儲存了每一層資訊,也就是說每一個功能都有自己的一個xlator結構體。每一層都會定義相同型別的operations函式以及相應的回撥函式。glusterfs定義了STACK_WIND函式從某一層下發到另一個層,當執行結束則會呼叫STACK_UNWIND回撥到上一層的函式。
要使用FUSE來建立一個檔案系統,首先需要定義 fuse_operations 型別的結構變數。Glusterfs定義的fuse_operations結構體。
static fuse_handler_t *fuse_std_ops[FUSE_OP_HIGH] = {
[FUSE_LOOKUP] = fuse_lookup,
[FUSE_RMDIR] = fuse_rmdir,
[FUSE_RENAME] = fuse_rename,
[FUSE_LINK] = fuse_link,
[FUSE_OPEN] = fuse_open,
[FUSE_READ] = fuse_readv,
[FUSE_WRITE] = fuse_write,
[FUSE_STATFS] = fuse_statfs,
[FUSE_SETXATTR] = fuse_setxattr,
[FUSE_GETXATTR] = fuse_getxattr,
[FUSE_ACCESS] = fuse_access,
[FUSE_CREATE] = fuse_create,
..........................
};
這裡只是顯示部分的fuse_operation函式,而這些函式在glusterfs中可以簡稱為fop,所以本文後面會把所有的fuse_operation函式稱為fop類函式或者ops操作。
glusterfs定義了fop類函式來處理從fuse驅動裝置讀取出來的資訊,因為glusterfs用的是堆疊式設計,所以glusterfs還會針對於fop函式類一一實現對應的回撥函式。glusterfs從/dev/fuse 裝置讀取資料出來則交由fop函式去處理,處理結束之後則呼叫回撥函式去處理最後傳送回去fuse裝置。
glusterfs程式碼是堆疊式結構,定義xlator結構體去實現每一個功能層,在glusterfs程式碼中,每一個xlator代表一個功能。init()函式實現了每個功能層是初始化以及該層的配置等設定。
現在具體看glusterfs用fuse實現的檔案系統流程。fuse層是glusterfs posix介面的開始的層,這裡首先先看看glusterfs fuse客戶端在主函式裡面的基本設定,以及fuse層的掛載,初始化引數等。使用者在終端上執行mount掛載glusterfs客戶端posix客戶端,則會啟動glusterfs程序,開始在主函式執行,首先會解析傳入的引數確定執行的函式流,執行是客戶端還是brick程序或者是glusterd守護程序。
1、在使用者執行mount -t glusterfs ip:volume /mountpoint之後,啟動了glusterfs客戶端程序,在主函式首先是設定全域性變數,配置iobuf儲存池的大小,設定每一頁儲存池大小為128K。設定事件池的大小,frame,stack等池大小。接著檢測傳入系統引數,確定為客戶端程序,啟動協程池。解析卷模式等資訊。
2、執行fuse的init函式,在init函式中,配置acl,selinux等一些引數。設定掛載點的引數,許可權設定,讀寫塊大小等。
3、開啟/dev/fuse裝置。儲存fd到私有結構體,最後mount掛載點,程序切換為守護程序。
4、當這些工作結束之後,這時候會下發首次的lookup操作,檢測brick程序是否線上,如果這是本次卷的首次掛載還會對目錄進行分割槽區間的操作。這時候會啟動執行緒fuse_thread_proc(),這個程序輪詢的去讀取/dev/fuse裝置傳輸過來的資料,解析資料,選擇相應的fop操作。
到了這裡glusterfs客戶端已經掛載到了一個目錄上,準備工作做完可以開始工作了。
初始化結束之後,客戶端掛載到了遠端的volume。這時就可以在目錄下進行正常的讀寫了,應用程式在讀寫檔案時候是怎麼一個流程?即IO流程,簡單來說就是應用程式在掛載點讀寫,經過vfs層。vfs層在接受會呼叫fuse的註冊函式,相應的讀寫函式會放在讀寫連結串列當中,這時候glusterfs啟動的執行緒fuse_thread_proc()函式會輪詢讀取/dev/fuse裝置,從裝置讀出資料流,解析具體操作,呼叫相應的fop函式,一層一層下發到client層,client運用了rpc技術,資料傳輸到brick,當執行結束之後就會呼叫對應的fop的回撥函式返回。最後寫回fuse裝置。
執行完主函式設定好基本的環境變數,配置好引數,目錄掛載。這時候就可以正常使用glusterfs檔案系統了。這裡主要是講glusterfs從/dev/fuse讀取資料之後的處理部分,也就是fuse實現的使用者態檔案系統提供服務部分,讀寫顯示等操作。這裡可以也可以理解成fuse的使用者態部分。為了解釋glusterfs是如何從fuse裝置傳入資料,這裡首先解釋幾個結構體。
struct iovec {
void *iov_base;
size_t iov_len;
};
這是一個集合讀寫的操作集合,iov_base儲存資料,iov_len儲存了資料的長度。在fuse層中會定義兩個iovec,一個存放操作型別,許可權,使用者,檔名等。一個用來存讀寫的資料。
struct iobuf {
union {
struct list_head list;
struct {
struct iobuf *next;
struct iobuf *prev;
};
};
struct iobuf_arena *iobuf_arena;
gf_lock_t lock;
int ref;
void *ptr;
void
};
iobuf結構是glusterfs中用來做io操作的io記憶體管理。每次做io操作,glusterfs都會從iobuf記憶體池中申請空間,glusterfs的iobuf儲存池其實有幾個結構體組成,池,域,buf三個級別。這裡只關注buf,也就是一次io申請的記憶體大小。每一次有資料讀寫操作會申請一個iobuf。iobuf的預設值為128Kb,這個是為了對應fuse的io大小一致。從fuse裝置讀取出資料塊儲存到這個iobuf中。
struct fuse_in_header {
__u32 len;
__u32 opcode;
__u64 unique;
__u64 nodeid;
__u32 uid;
__u32 gid;
__u32 pid;
__u32 padding;
};
fuse_in_header結構儲存著從fuse裝置讀取到的資料,從fuse讀取資料一般分為兩類,一類是fop操作型別,一類既是應用讀寫的資料流。fuse_in_header主要幾個成員:opcode代表的是指向哪個fop函式,len是本次的資料長度,uid,gid,pid既是使用者id,組id,程序id。利用這個函式,則可以知道呼叫哪個函式,已及能夠識別使用者,程序,操作的檔案從而不會出現資料流混亂。
static fuse_handler_t *fuse_std_ops[FUSE_OP_HIGH] = {
[FUSE_LOOKUP] = fuse_lookup,
[FUSE_FORGET] = fuse_forget,
[FUSE_GETATTR] = fuse_getattr,
[FUSE_SETATTR] = fuse_setattr,
[FUSE_READLINK] = fuse_readlink,
[FUSE_SYMLINK] = fuse_symlink,
[FUSE_MKNOD] = fuse_mknod,
[FUSE_MKDIR] = fuse_mkdir,
[FUSE_UNLINK] = fuse_unlink,
......
[FUSE_LSEEK] = fuse_lseek,
}
fuse_handler_t *fuse_std_ops結構體定義了所有的fop操作,這是與fuse的客戶端一樣的檔案操作函式。從fuse裝置讀取出資料,根據fuse_in_header結構體的opcode選中fuse_std_ops相應的函式執行。
首先glusterfs呼叫readv()函式從/dev/fuse裝置讀取出兩iovec 型別資料,強制轉化型別為fuse_in_header_t結構,fuse_in_header_t成員opcode儲存了相應的fop型別,根據opcaode型別選擇fuse_std_ops中的函式執行。到這裡可以確定了檔案的fop操作型別,但是還沒有確定操作的檔案,檔案路徑以及glusterfs所需要的gfid。這時候glusterfs會執行一個fuse_resolve_and_resume()函式,從iovec 第二個結構中解析出檔名,解析inode id,path,父目錄的路徑,gfid。當這些執行結束下發下一層處理。從這裡可以瞭解glusterfs fuse主要功能:與fuse裝置通訊,獲取檔案操作的fop,解析出檔案的路徑,檔名等必要資訊。當這些工作結束,fuse也就下發到下一層處理。而下一層可能是預讀層,快取層。從這裡也可以看出glusteefs設計的堆疊的優秀性。每一層有各自的工作,各自的處理方式。這樣可以很方便的增加一個功能層。比如glusterfs自身帶的io-cache,read-ahead等。對應開發人員來說,在一些特殊的場景,這時候需要增加新的功能,只需要確定在堆疊的位置,利用xlator定義一個自身的結構體。定義一樣的fop類函式,編寫makefile檔案,在glusterd程式碼加入所在層的編譯。而這些工作在已有的程式碼上做簡單複製則可以實現。比如在很多媒體,醫院的應用場景,對許可權有更多的要求,這時候利用nfs,cifs等無法實現的時候,只需要在增加一層許可權層,對相應許可權在每個fop中進行控制。而這樣的控制也就忽略了上層所用的客戶端,不管是nfs,cifs,還是利用API編寫的函式,實現了許可權在檔案系統上的管理。
現在重點來看fuse層是如何處理從/dev/fuse裝置讀取資料,然後處理這些資料的。也就是從fuse_thread_proc()函式開始的流程。
到這裡已經掛載到了一個目錄上面,這時候glusterfs已經可以給上層應用提供服務。 對於glusterfs客戶端來說,只要能保證fuse層,dht層,client層,卷模式層就可以正常的工作,現在假設我們建立了一個2+1的糾刪碼卷。瞭解glusterfs的整個IO流程,從掛載點到brick的客戶端。這裡以ops的寫writev為例。
資料流在檔案和作業系統和fuse驅動裝置之後,glusterfs接受解析之後處理之後通過網路傳送到服務端。
服務端在接受到客戶端的請求開始執行brick上的工作。
圖中忽略了很多功能層,如io-cache,write-behind,read-behind。brick節點上也忽略了一些io-stat等層只保留了基本的serve和posix層。雖然忽略掉這些層,但是事實上有這些層已經可以工作了。省略這部分主要是為了更清晰的看出glusterfs是如何借用fuse來實現和掛載點上的應用進行讀寫操作。
1. 對應用來說,glusterfs只是提供一個目錄,應用該目錄下的檔案讀寫,當應用呼叫Linux系統呼叫write寫入一個128Kb的資料時候,writev是vfs層提供,這時候vfs接受到應用的writev,根據fuse註冊是函式呼叫fuse_file_aio_write,將寫請求放入fuse connection的request pending queue, 隨後進入睡眠等待應用程式reply。
2. glusterfs是Linux使用者態的檔案系統,會啟動一個fuse_proc_fuse()輪詢的讀取/dev/fuse裝置,glusterfs從/dev/fuse讀取的資料出來儲存在兩個iov_in結構體當中,一個儲存fop型別,一個儲存寫入的資料。解析資料結構的操作型別,根據fuse_ops函式結構體選擇對應的fop函式執行,如果本次操作為寫操作,那麼會從第二個iov_in讀取出資料,在相應的fop函式當中會執行解析檔名,gfid,構建inode等資訊。準備好這些資訊,呼叫STACK_WIND下發到下一個xlator,下發到dht層,dht是一個檔案定位,檔案遷移,分層功能實現的xlator。這裡只解釋檔案定位,檔案會根據檔名獲得一個32位數值,根據父目錄確實能夠檔案所在brick。當檔案定位了遠端伺服器哪個brick之後,dht層開始下發寫到client,下發到client層時候。Client利用rpc技術呼叫遠端的brick函式,隨後進入ping等待。
3. 當服務端的brick接受到客戶端的呼叫,下發經過到posix,posix會在確定絕對路徑之後對檔案進行讀寫。而glusterfs支援檔案IO方式,阻塞,非阻塞,非同步AIO等方式。
glusterfs為使用者提供了fuse這個基本檔案系統的介面。從上面的流程可以知道,使用fuse客戶端需要經過vfs,fuse,最後還會通過/dev/fuse裝置回到使用者態的glusterfs操作。使用fuse需要先使用者態,經過核心態,最後又回到使用者態。所以glusterfs又提供了一種API訪問模式,API訪問方式需要使用者自己去編寫相應的函式。API的方式拋棄了fuse裝置使使用者直接對gluster操作,也不必要去掛載一個目錄,這樣的話在使用者到glusterfs中減少了經過vfs,fuse。也就不必要先使用者態到核心態最後再到使用者態。API的方式直接在使用者態完成了操作。雖然API方式縮減了流程,但是把編碼技巧和效能的提升問題留給了使用者,這樣對於一些沒有學習過大併發高效能的程式設計人員還會覺得API方式效能低下不可用。
目前利用API開發的cifs方式在效能方面還是可以滿足非編視訊的應用,在fio測試軟體中也設計了glusterfs API方式測試,可以利用fio軟體測試對比API和fuse的方式區別。
修改FUSE資料塊提升效能
現在瞭解了資料塊是怎麼從應用到落盤等整個流程。假設應用需要寫100GB容量的檔案,100GB容量的情況可以分成兩種情況,1.一個檔案容量為一個100GB。2. 262144個4KB的檔案。這兩個情況相當於大檔案的讀寫問題,小檔案的讀寫問題。
為了理清這兩個問題,首先了解一個檔案在glusterfs是如何從檔案頭讀取完一個檔案的,因為glusterfs對目錄深度需要一步一步的查詢,然後定位到檔案。而且在分散式叢集當中,客戶端會跟多個brick去通訊。所以這裡假設環境是一個客戶端一個brick,檔案落在根節點上。
1、首先下發fop函式lookup檢視檔案父目錄時候完整,這裡是根目錄,而且只有一個brick,所以不會出現檢測父目錄的問題。這裡只執行了一個lookup操作。當然,在大規模的叢集當中,這裡會出現可能父目錄的檢測,修復等問題。
2、當父目錄在確定的情況下,下發lookup函式去檢視這個檔案是否存在。當確定檔案存在,會呼叫stat函式去獲取檔案的狀態和屬性。因為這裡沒有別的brick,所以也不會出現T檔案情況,不會再次發生lookup,stata等情況。
3、檔案確定下來之後,執行open函式操作,在brick上真實的開啟檔案(系統的fd),在客戶端上會儲存一個虛擬fd。
4、接著呼叫writev/readv函式,這裡用了writev集合寫加快寫的速度。一次讀寫的最大塊是128KB。
5、當讀寫完成之後,呼叫release關閉這個檔案描述符。
從1和2可以知道,在檔案定位和檢測目錄有和可能導致操作函式變多,這裡規定了環境情況,所以這些函式不會執行過多。假設這些函式執行的速率是一致的(事實上,writev/readv這些函式執行的時間遠大於其他,而且這些函式還會受到磁碟,磁碟檔案系統的影響)。
假設一個100GB的檔案,每次讀寫的塊大小為128KB。Writev/Readv這需要819200函式的呼叫,則一次完整的讀寫則大概需要819207次fop函式的執行。如果把每次讀寫的塊大小提升到1MB,一次完整的讀寫則回變成102407次。函式執行速度一致的情況下,這裡相當於縮短了8倍的時間,事實上,在很多應用上的環境,速率會受很多方面的影響,磁碟,網路,應用的讀寫方式等等。而這裡確實看出了當每次讀寫的塊變大,對於大檔案來說,讀寫速率會增加。
現在說一下如何修改fuse的最大塊改為1MB從而提升效能。這裡以fuse2.8版本為例,fuse-2.8版本以上,fuse的模組移入到作業系統核心裡面。
1、獲取作業系統的版本uname -a
2、登入官網下載Linux作業系統版本核心原始碼
3、修改FUSE核心程式碼部分
../fs/fuse/fuse_i.h
#define FUSE_MAX_PAGES_PER_REQ 32
=======》#define FUSE_MAX_PAGES_PER_REQ 256
提高每次讀寫分配的頁數
../include/linux/mm.h
#define VM_MAX_READAHEAD 1024
fc->bdi.ra_pages = VM_MAX_READAHEAD/ PAGE_CACHE_SIZE;
這裡需要調整VM_MAX_READAHEAD為1024X1024,但是這個是在記憶體模組,修改這部分可能導致系統出錯,所以這裡修改fc->bdi.ra_pages = 1024*1024/ PAGE_CACHE_SIZE
修改FUSE的使用者態模組:
/lib/fuse_kern_chan.c
#define MIN_BUFSIZE 0x100000
這部分同時修改利用了fuse裝置的塊部分。
修改glusterfs程式碼模組:
xlators/mount/fuse/src/fuse-bridge.c
fuse_init()
fino.max_readahead = 1 << 20;
fino.max_write = 1 << 20;
Init():
gf_asprintf (&mnt_args, "%s%s%sallow_other,max_read=1048576",
priv->acl ? "" : "default_permissions,",
priv->fuse_mountopts ? priv->fuse_mountopts : "",
priv->fuse_mountopts ? "," : "");
修改glusterfs fuse部分,掛載點的引數等:
glusterfsd/src/glusterfsd.c
ctx->page_size = 1024 * GF_UNIT_KB;
修改預設讀取分配的iobuf塊大小
libglusterfs/src/iobuf.c
struct iobuf_init_config gf_iobuf_init_config[] 這裡的結構體需要修改成最大2Mb每一個頁,分配的頁面和頁大小需要根據系統的記憶體分配,這裡最大的頁面2Mb.
iobuf_pool_new ():
iobuf_pool->default_page_size = 1024 * GF_UNIT_KB;
關於iobuf分配,如果沒有找到對應的頁面大小,會獲取預設頁面大小。
xlators/performance/io-cache/src/io-cache.c
#define IOC_PAGE_SIZE (1024 * 1024)
Io cache層的cache頁大小,這部分如果cache不匹配會導致io cache頁面變多甚至出現頁面miss情況。
xlators/performance/write-behind/src/write-behind.c
#define WB_AGGREGATE_SIZE 1048576
回寫功能模組,在glusterfs很多功能層當中預設的最大塊是128KB,而且glusterfs客戶端有api。nfs,cifs客戶端形式,這時候需要去檢測每一層每一層讀寫塊的大小,根據glusterfs配置檔案每一層的writev/readv可以檢測每一層的讀寫塊大小。
通過調整FUSE資料塊大小為1MB,目前在單客戶端單節點/萬兆網路/brick磁碟沒有限制情況下,GlusterFS posix客戶端讀寫能都把萬兆頻寬跑滿。
GlusterFS POSIX深度優化
在上面的IO流程中,只是一個ops操作,而一個完整的檔案讀寫需要呼叫多個ops,首先需要lookup定位檔案,接著會呼叫stat獲取檔案的屬性狀態,而在一些情況下還會呼叫getfattr獲取檔案的擴充套件屬性,隨後open檔案,這時候為了資料的安全性,glusterfs還會增加一個檔案鎖,而在副本,糾刪碼這種模式下,檔案分佈到不同的機器的brick上,這時候還需要多個brick加鎖。就緒之後呼叫writev()開始讀寫檔案。每一次最大的讀寫塊為128KB。當檔案讀寫結束之後,釋放鎖,隨後呼叫release()釋放fd。這樣一次的檔案操作結束。
在glusterfs當中定義了一個io_stat層,這個層主要是用來收集每個io的呼叫次數,對於開發人員,如果想要知道檔案ops呼叫次數。開啟這個功能選項可以觀察到每一個ops操作的次數和ops的延遲。現在簡單畫出glusterfs io的一個檔案的讀寫流程。
根據上面的ops流程圖,可以發現在讀寫一個檔案時候需要呼叫多個ops,而像lookup,stat等操作很多時候只是為了驗證檔案存在與否,隨後讀寫檔案。在大檔案的情況下,這些ops在整個操作流程佔用比例很少,操作的時間大部分消耗在磁碟的讀寫操作上和網路通訊上。但是小檔案情況下,假如一個檔案為4KB,檔案基本就是lookup一次,stat一次,open一次,readv/writev一次(不考慮異常處理方式)。大量的4KB檔案情況下,比如百萬/千萬級別,這時候會發現檔案ops中的讀寫只是佔了極少的部分,大部分時間都花在檔案定位,獲取檔案的屬性等方面,而真正的讀寫時間特別短。這時候應該儘量縮短這些定位獲得屬性的操作,如構建元資料伺服器,合併ops操作,合併檔案為大檔案。在glusterfs中有一個功能quick_read,開啟這個功能,如果檔案小於64MB情況下,在lookup時候就會把檔案讀取出來,這樣減少了ops操作。
簡化整個過程分成三個模組: 應用的操作,fuse的ops請求佇列,glusterfs的ops處理。這時候可以發現,整個效能提升的核心在glustefs,當glusterfs能夠快速處理完成,則能夠快速的應答。根據上面的流程會發現,fuse_proc_fuse()讀取之後需要先處理完成才會從新寫回到fuse,在沒有處理完成,fuse一直在等待。這時候可以併發ops的操作,這樣能夠同時的完成多個ops。
對於這種小檔案情況,glusterfs主要從兩個方面去優化。一,減少ops運算元,如quick_read。二,快速定位檔案,如元資料伺服器。對於小檔案的儲存讀取在這裡就不多說。
檔案的io是使用者程式發起,fuse裝置為中介,glusterfs最後實現了整個檔案的落盤和操作。從這裡得出要是優化的話可以分成3部分: 使用者的程式,fuse裝置,gluster檔案系統。
對於使用者程式來說,可以使用API模式去繞過了fuse裝置,簡化了流程,但是這樣的話會把所有的快取機制,併發機制,buf等提高效能方面的工作留給了使用者,而且在使用者層這方面,更多使用者是利用已有的軟體去對檔案讀寫,所以這方面優化來說不是很有針對性。對於fuse裝置來說,根據上面的修改塊方式,其實相當於擴大的頁面數,在Linux作業系統裡面IO都是以page頁為單位,核心會將寫入的請求按照PAGE_SIZE劃分成多個page,然後再對page進行操作,簡潔而優美,而glusterfs也是借用了核心的這種結構,快取,iobuf等。這也是glusterfs作為一個檔案系統的優勢,簡潔易上手。事實上上面修改塊的大小是可以換成修改核心page的大小方式。在這裡擴充套件說一下fuse裝置一些優化的方式。
1、延長元資料的有效時間
元資料一般指檔案的路徑,檔案的大小,檔名......在應用層能夠在使用stat函式去獲取這些屬性,在一些分散式的檔案系統中會快取這些元資料,這樣在定位檔案的時候能夠快速的獲得檔案的元資料。fuse儲存元素結構是struct dentry和struct inode,這兩個結構體檔案系統的基礎,所有的檔案的操作都是先要填充著兩個結構體再往後走。為什麼說延長元資料的必要性?根據上面的圖7,我們假設一下一個路徑為 /taocloud-xdfs/glusterfs/app/file查詢的流程和在glusterfs的ops操作。
首先呼叫look和stat ops操作去獲取每個目錄的元資料,先taocloud-xdfs,接著glusterfs,app,file。這樣每個檔案目錄在fuse都會需要去填充struct dentry,struct inode。最後才會readv/writev。從這裡面可以看見目錄的深度以及每個目錄都需要去準備這些元資料。雖然這裡用了glusterfs的ops操作。不過這不影響理解fuse的操作,事實上在檔案系統,inode,dentry,ops操作這些都是相通的。再假設一下,如果有成千上萬的檔案。假設n表示ops的操作延遲(即函式的時間),m表示ops的操作次數,k表示檔案的個數。
file_time = m * lookup(n) + m * stat(n) + open(n) + m * readv(n)/writev(n) + release(n)
lookup stat的m表示目錄的深度,readv/writev的m表示讀寫次數,就像fuse修改塊的大小就是減少了m的次數,當然我們brick上做的raid就是為了減少n的時間,在減少n的時間上,Linux的AIO,非阻塞等也是一種方式。
當大檔案時候,10個,100個這個樣數量的大檔案lookup,stat等這些檔案是的m是極少的,但是如果是十萬個百萬個4k的檔案呢?很明顯,這是readv/writev已經不是很嚴重的問題,lookup,stat這時候已經大量的增加了,大部分時間都是消耗在這兩個函式中,從這兩個檔案去獲取元資料。如果這時候在fuse增加了快取有效時間,獲取的時候會直接在記憶體,記憶體的讀寫是磁碟的數量級。這樣會效能有幫助,但是百萬級別的檔案這樣的快取是根本不行,一個是記憶體沒那麼大,一個是這時候可能導致快取miss的情況加重。
像這樣的情況,glusterfs提供了一種qiuck_read的功能,這種方式是減少ops次數。,在市面上的分散式儲存很多會採用元資料伺服器方式,檔案合併方式。如ceph,利用幾個(奇數)伺服器去快取元資料,這樣每次檔案去讀寫,先伺服器然後到儲存的osd中讀取檔案的內容。這樣確實增加了小檔案的效能,但是也暴露了問題,元資料出現錯誤時候這時候就會導致檔案的出錯而且維護這些元資料伺服器也增加了運維的負擔。這是一種不錯的辦法,但是需要不斷的優化。瞭解ceph發展歷史,ceph的fs模組是最開始出現的,而正真發展的物件和塊。而在最近fs模組才慢慢去產品化。而另一種方式,合併各種小檔案為大檔案,這樣操作時候就能夠減少定位。只需要根據偏移量和大小去讀寫,這種方式類似視訊檔案,先佔一大塊幾T,然後才慢慢填充檔案,但是這樣方式有需要預判檔案的大小,當往這個檔案增加內容時候,超出了自己分配的大小怎麼辦,或者最後自己獨立成為一個大檔案。這些無形中增加了檔案的塊遷移。
各種方式都有自己的優點缺點,這些方式都是得根據自己的應用,儲存的資料去應用。在glusterfs中我們知道有一個分層功能tier,如果在這個功能之上,開發一個元資料伺服器或者定義一個規則,小檔案遷移到這個檔案。這樣能夠glusterfs小檔案的優化,而也縮短的小檔案的路徑。
2、增加資料塊大小,增加每次讀寫資料流。根據上1中的方式,這種方式只能對大檔案起到效果。
3、開啟核心讀快取,Linux檔案系統充分利用記憶體快取檔案資料,這樣很多使用者在讀寫檔案時候根本不需要去對磁碟進行io,根據圖8,也就是必要經過glusterfs。但是這樣很可能讀當髒資料。這時候在使用者態的程式很難控制這種行為。在fuse中,我們可以在fuse掛載時候加上–o kernel_cache –o auto_cache來開啟這個功能。
4、使用DirectIO取代BufferIO。這種方式也是fuse掛載一種引數,但是這種方式在順序寫時候會提升效能,其他應該情況則就下降。畢竟大部分應用都是為了儲存資料,到最後更多是讀。這樣會導致最後不可以接受。
5、fuse裝置request pending queue佇列的優化,在fuse中每次客戶端的讀寫都會放到一個等待隊列當中,隨後等待,使用了AIO的非同步讀寫方式。這一塊如果能夠借鑑記憶體的排程方式,多個佇列,佇列的優先級別也是對效能有幫助。
6、在glusterfs層的優化,也既是圖8中的應用檔案系統優化,一種在glustefs產家種比較常見的方法:執行緒池的併發,一般這種方式是在glustefs的fuse層上開發,這種方式就是定義一個執行緒池,初始化多個執行緒等待工作佇列,在這種方式,會首先在讀取/dev/fuse的ops放入工作佇列,隨後執行緒讀取工作佇列去執行ops操作。但是從圖7中可以看出其實檔案操作的ops是有一個順序流程。這時候怎麼處理好這個流程是一個問題。而且glusterfs中ec卷模式幾乎每次寫都需要去讀一次(糾刪碼的寫損耗),這時候如果出現下一個塊比前一個塊先寫入,這時候能否保證檔案的資料安全性也是需要去驗證。