深入理解GlusterFS之POSIX接口

(作者:林世躍@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的文件系統的實現實例包括GluterFS、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設備是不一樣的函數。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

        圖中省略了內核機制,只是呈現了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守護進程。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

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爲例。
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

數據流在文件和操作系統和fuse驅動設備之後,glusterfs接受解析之後處理之後通過網絡發送到服務端。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

服務端在接受到客戶端的請求開始執行brick上的工作。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

圖中忽略了很多功能層,如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的一個文件的讀寫流程。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

        根據上面的ops流程圖,可以發現在讀寫一個文件時候需要調用多個ops,而像lookup,stat等操作很多時候只是爲了驗證文件存在與否,隨後讀寫文件。在大文件的情況下,這些ops在整個操作流程佔用比例很少,操作的時間大部分消耗在磁盤的讀寫操作上和網絡通信上。但是小文件情況下,假如一個文件爲4KB,文件基本就是lookup一次,stat一次,open一次,readv/writev一次(不考慮異常處理方式)。大量的4KB文件情況下,比如百萬/千萬級別,這時候會發現文件ops中的讀寫只是佔了極少的部分,大部分時間都花在文件定位,獲取文件的屬性等方面,而真正的讀寫時間特別短。這時候應該儘量縮短這些定位獲得屬性的操作,如構建元數據服務器,合併ops操作,合併文件爲大文件。在glusterfs中有一個功能quick_read,開啓這個功能,如果文件小於64MB情況下,在lookup時候就會把文件讀取出來,這樣減少了ops操作。

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=

        簡化整個過程分成三個模塊: 應用的操作,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卷模式幾乎每次寫都需要去讀一次(糾刪碼的寫損耗),這時候如果出現下一個塊比前一個塊先寫入,這時候能否保證文件的數據安全性也是需要去驗證。


參考文獻
1.FUSE源碼剖析 http://blog.sae.sina.com.cn/archives/2308 
2.使用 FUSE 開發自己的文件系統https://www.ibm.com/developerworks/cn/linux/l-fuse
3.基於fuse文件系統優化方法總結 https://baijia.baidu.com/s?old_id=493750 


源地址:http://mp.weixin.qq.com/s/hVTfhJVvm9svTpi3C6V_Bg


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章