NFS-Ganesha源代碼分析
1. NFSv4簡(jiǎn)要概述
NFS這個(gè)協(xié)議(NFSv2)最初由Sun Microsystems在1984年設(shè)計(jì)提出,由于存在一些不足,因此在隨后由幾家公司聯(lián)合推出了NFSv3。到了NFSv4時(shí),開發(fā)完全由IETF主導(dǎo),設(shè)計(jì)目標(biāo)是:
- – 提高互聯(lián)下的NFS訪問和性能
- – 提供安全性
- – 更強(qiáng)的跨平臺(tái)操作
- – 方便后期擴(kuò)展
我們可以看到NFSv4在緩存能力、擴(kuò)展性、高可用性方面取得了很大的突破,放棄了之前版本的無狀態(tài)性,采用了強(qiáng)狀態(tài)機(jī)制,客戶端和服務(wù)端采用了復(fù)雜的方式交互,由此保證了服務(wù)器端的負(fù)載均衡,減少了客戶端或服務(wù)端的RTO。
在安全性方面,NFSv4采用了面向連接的協(xié)議,強(qiáng)制使用RPCSEC_GSS并且提供基于RPC的安全機(jī)制。放棄了之前版本中采用的UDP,采用了TCP。NFSv4支持通過次要版本進(jìn)行擴(kuò)展,我們可以看到在NFSv4.1支持了RDMA、pNFS范式以及目錄委派等功能。
2. NFS-Ganesha的四大優(yōu)勢(shì)
2007年左右,CEA的大型計(jì)算機(jī)中心每天都會(huì)產(chǎn)生10TB左右的新數(shù)據(jù),CEA將這些數(shù)據(jù)放在由HSM組成的HPSS中,而這些HSM本身提供了NFS接口。但是開發(fā)者在生產(chǎn)環(huán)境中發(fā)現(xiàn)HSM和NFS的橋接仍舊有不少問題,因此開發(fā)者決心寫一個(gè)新的NFS Daemon來讓NFS接口更好的配合HPSS。
這個(gè)項(xiàng)目需要解決以上的問題之外,開發(fā)團(tuán)隊(duì)還指定了其他目標(biāo):
- – 可以管理百萬級(jí)別的數(shù)據(jù)緩存,從而來避免底層文件系統(tǒng)阻塞
- – 除了可以對(duì)接HPSS以外,還可以對(duì)接其他文件系統(tǒng)
- – 支持NFSv4,實(shí)現(xiàn)易適配(adaptability),易擴(kuò)展,安全等特性
- – 從根本上解決軟件所帶來的性能瓶頸
- – 開源
- – 支持Unix系統(tǒng)
由此NFS-Ganesha應(yīng)運(yùn)而生,它并不是用來替代內(nèi)核版本的NFSv4,相反,NFS Ganesha是一個(gè)全新的程序,可能對(duì)比kernel版本的NFSv4,Ganesha的性能有所欠缺,但是基于user-space的方法會(huì)帶來更多有意思的功能。
靈活的內(nèi)存分配
首先,user-space的程序可以分配大量的內(nèi)存讓程序使用,這些內(nèi)存可以用來建立軟件內(nèi)部緩存,經(jīng)過測(cè)試,我們只需要4GB就可以實(shí)現(xiàn)百萬級(jí)別的數(shù)據(jù)緩存。在一些x86_64平臺(tái)的機(jī)器上,我們甚至可以分配更大的內(nèi)存(16 32GB),來實(shí)現(xiàn)千萬級(jí)別的數(shù)據(jù)緩存。
更強(qiáng)的可移植性
如果NFS Ganesha是kernel-space的話,那樣NFS Ganesha的內(nèi)部結(jié)構(gòu)只能適應(yīng)一款特定的OS,而很難移植到別的OS上。另外考慮的是代碼本身:在不同的平臺(tái)上編譯和運(yùn)行的產(chǎn)品比在一個(gè)單一平臺(tái)上開發(fā)的產(chǎn)品更安全。 我們開發(fā)人員的經(jīng)驗(yàn)表明,只在單一平臺(tái)上開發(fā)會(huì)讓開發(fā)后期困難重重; 它通常會(huì)顯示在Linux上不會(huì)輕易檢測(cè)到的錯(cuò)誤,因?yàn)橘Y源不一樣。
當(dāng)然可移植性不單單指讓NFS Ganesha可以運(yùn)行在不同的OS上,能夠適配不同的文件系統(tǒng)也是考量之一。在NFSv2和NFSv3中,由于語義設(shè)計(jì)上偏向Unix類的文件系統(tǒng),因此基本不可能適配非Unix類的文件系統(tǒng)。這一情況在NFSv4中大有改觀,NFSv4的語義設(shè)計(jì)出發(fā)點(diǎn)是讓NFS能盡可能多地適配不同的文件系統(tǒng),因此加強(qiáng)了文件/目錄屬性參數(shù)的抽象。Ganesha設(shè)計(jì)初衷是成為一個(gè)NFSv4通用服務(wù)器,可以實(shí)現(xiàn)NFSv4的所有功能,因此也需要適配各種文件系統(tǒng)。在內(nèi)核中實(shí)現(xiàn)這一功能是不容易的(內(nèi)和編程會(huì)有很多限制),然而在user-space中實(shí)現(xiàn)這一點(diǎn)會(huì)便捷一些。
更便捷的訪問機(jī)制
內(nèi)核中的NFSv4訪問用戶空間中的服務(wù)不是那么方便,因此其引入了rpc_pipefs機(jī)制, 用于解決用戶空間服務(wù)的橋梁,并且 使用kerberos5管理安全性或idmapd守護(hù)程序來進(jìn)行用戶名轉(zhuǎn)換。然而Ganesha不需要這些,它使用常規(guī)API來對(duì)外提供服務(wù)。
對(duì)接FUSE
由于NFS Ganesha是一個(gè)運(yùn)行在用戶空間的程序,因此它還提供了對(duì)一些用戶空間文件系統(tǒng)(FUSE)的支持,可以讓我們直接把FUSE掛載在NFS上而不需要內(nèi)核的幫助。
3. NFS-Ganesha框架淺析
NFS Ganehsha是完全使用開源自由軟件開發(fā)的,由于Linux上的系統(tǒng)編程開發(fā)資源巨大,因此開發(fā)起來比在其他Unix系統(tǒng)上更為便捷。
Figure 1 – NFS Ganesha分層架構(gòu)圖
由上圖我們可以看到,Ganesha是一個(gè)基于模塊的程序,每個(gè)模塊都負(fù)責(zé)各自的任務(wù)和目標(biāo)。開發(fā)團(tuán)隊(duì)在寫代碼之前就對(duì)每個(gè)模塊進(jìn)行了精心的設(shè)計(jì),保證了后期擴(kuò)展的便捷性。比如緩存管理模塊只負(fù)責(zé)管理緩存,任何在緩存管理模塊上做出的更改不能影響其他模塊。這么做大大減少了每個(gè)模塊間的耦合,雖然開發(fā)初期顯得困難重重,但在中后期就方便了很多,每個(gè)模塊可以獨(dú)立交給不同開發(fā)人員來進(jìn)行開發(fā)、驗(yàn)證和測(cè)試。
Ganesha的核心模塊
- – Memory Manager: 負(fù)責(zé)Ganesha的內(nèi)存管理。
- – RPCSEC_GSS:負(fù)責(zé)使用RPCSEC_GSS的數(shù)據(jù)傳輸,通常使用krb5, SPKM3或LIPKEY來管理安全。
- – NFS協(xié)議模塊:負(fù)責(zé)NFS消息結(jié)構(gòu)的管理
- – Metadata(Inode) Cache: 負(fù)責(zé)元數(shù)據(jù)緩存管理
- – File Content Cache:負(fù)責(zé)數(shù)據(jù)緩存管理
- – File System Abstraction Layer(FSAL): 非常重要的模塊,通過一個(gè)接口來完成對(duì)命名空間的訪問。所訪問的對(duì)象隨后會(huì)放置在inode cache和file content cache中。
- – Hash Tables:提供了基于紅黑樹的哈希表,這個(gè)模塊在Ganesha里用到很多。
內(nèi)存管理
內(nèi)存管理是開發(fā)Ganesha時(shí)比較大的問題,因?yàn)榇蠖鄶?shù)Ganesha架構(gòu)中的所有模塊都必須執(zhí)行動(dòng)態(tài)內(nèi)存分配。 例如,管理NFS請(qǐng)求的線程可能需要分配用于存儲(chǔ)所請(qǐng)求結(jié)果的緩沖器。 如果使用常規(guī)的LibC malloc / free調(diào)用,則存在內(nèi)存碎片的風(fēng)險(xiǎn),因?yàn)槟承┠K將分配大的緩沖區(qū),而其他模塊將使用較小的緩沖區(qū)。 這可能導(dǎo)致程序使用的部分內(nèi)存被交換到磁盤,性能會(huì)迅速下降的情況。
因此Ganesha有一個(gè)自己的內(nèi)存管理器,來給各個(gè)線程分配需要的內(nèi)存。內(nèi)存管理器使用了Buddy Malloc algorithm,和內(nèi)核使用的內(nèi)存分配是一樣的。內(nèi)存分配器中調(diào)用了madvise來管束Linux內(nèi)存管理器不要移動(dòng)相關(guān)頁。其會(huì)向Linux申請(qǐng)一大塊內(nèi)存來保持高性能表現(xiàn)。
線程管理
管理CPU相比較內(nèi)存會(huì)簡(jiǎn)單一些。Ganesha使用了大量的線程,可能在同一時(shí)間會(huì)有幾十個(gè)線程在并行工作。開發(fā)團(tuán)隊(duì)在這里用到了很多POSIX調(diào)用來管理線程,讓Linux調(diào)度進(jìn)程單獨(dú)處理每一個(gè)線程,使得負(fù)載可以覆蓋到所有的CPU。
開發(fā)團(tuán)隊(duì)也考慮了死鎖情況,雖然引入互斥鎖可以用來防止資源訪問沖突,但是如果大量線程因此陷入死鎖狀態(tài),會(huì)大大降低性能。因此開發(fā)團(tuán)隊(duì)采用了讀寫鎖,但是由于讀寫鎖可能因系統(tǒng)而異,因此又開發(fā)了一個(gè)庫(kù)來完成讀寫鎖的轉(zhuǎn)換。
當(dāng)一個(gè)線程池中同時(shí)存在太多線程時(shí),這個(gè)線程池會(huì)成為性能瓶頸。為了解決這個(gè)問題,Ganesha給每一個(gè)線程分配了單獨(dú)的資源,這樣也要求每個(gè)線程自己處理垃圾回收,并且定期重新組合它的資源。同時(shí)”dispatcher thread”提供了一些機(jī)制來防止太多線程在同一時(shí)間執(zhí)行垃圾回收;在緩存層中垃圾回收被分成好幾個(gè)步驟,每個(gè)步驟由單獨(dú)代理處理。經(jīng)過生產(chǎn)環(huán)境實(shí)測(cè),這種設(shè)計(jì)時(shí)得當(dāng)?shù)摹?/p>
哈希表
關(guān)聯(lián)尋找功能在Ganesha被大量使用,比如我們想通過對(duì)象的父節(jié)點(diǎn)和名稱來尋找對(duì)象元數(shù)據(jù)等類似行為是很經(jīng)常的,因此為了保證Ganesha整體的高性能,關(guān)聯(lián)尋找功能必須非常高效。
為了達(dá)到這個(gè)目的,開發(fā)團(tuán)隊(duì)采用了紅黑樹,它會(huì)在add/update操作后自動(dòng)沖平衡。由于單棵紅黑樹會(huì)引發(fā)進(jìn)程調(diào)用沖突(多個(gè)進(jìn)程同時(shí)add/update,引發(fā)同時(shí)重平衡),如果加讀寫鎖在紅黑樹上,又會(huì)引發(fā)性能瓶頸。因此開發(fā)團(tuán)隊(duì)設(shè)計(jì)了紅黑樹數(shù)組來解決了這個(gè)問題,降低了兩個(gè)線程同時(shí)訪問一個(gè)紅黑樹的概率,從而避免了訪問沖突。
大型多線程守護(hù)程序
運(yùn)行Ganesha需要很多線程同時(shí)工作,因此設(shè)計(jì)一個(gè)大型的線程守護(hù)程序在設(shè)計(jì)之初尤為重要,線程分為以下不同類型:
- – dispatcher thread: 用于監(jiān)聽和分發(fā)傳入的NFS、MOUNT請(qǐng)求。它會(huì)選擇處于最空閑的worker線程然后將請(qǐng)求添加到這個(gè)worker線程的待處理列表中。這個(gè)線程會(huì)保留最近10分鐘內(nèi)的請(qǐng)求答復(fù),如果在10分鐘內(nèi)收到相同指令(存在哈希表并用RPC Xid4值尋址),則會(huì)返回以前的請(qǐng)求回復(fù)。
- – worker thread: Ganesha中的核心線程,也是使用最多的線程。worker線程等待dispatcher的調(diào)度,收到請(qǐng)求后先對(duì)其進(jìn)行解碼,然后通過調(diào)用inode cache API和file content API來完成請(qǐng)求操作。
- – statistics thread: 收集每個(gè)module中的線程統(tǒng)計(jì)信息,并定期用CSV格式記錄數(shù)據(jù),以便于進(jìn)一步處理。
- – admin gateway: 用于遠(yuǎn)程管理操作,包括清楚緩存,同步數(shù)據(jù)到FSAL存儲(chǔ)端,關(guān)閉進(jìn)程等。ganeshaadmin這個(gè)程序?qū)iT用于與admin gateway線程交互。
緩存處理
在上文中提到,Ganesha使用了大片內(nèi)存用于建立元數(shù)據(jù)和數(shù)據(jù)緩存。我們先從元數(shù)據(jù)緩存開始講起。metadata cache存放在Cache Inode Layer(MDCache Layer)層 。每個(gè)實(shí)例對(duì)應(yīng)一個(gè)命名空間中的實(shí)例(文件,符號(hào)鏈接,目錄)。這些Cache Inode Layer中的實(shí)例對(duì)應(yīng)一個(gè)FSAL中的對(duì)象,把從FSAL中讀取到的對(duì)象結(jié)構(gòu)映射在內(nèi)存中。
Cache Inode Layer將元數(shù)據(jù)與對(duì)應(yīng)FSAL對(duì)象handle放入哈希表中,用來關(guān)聯(lián)條目。初版的Ganesha采用’write through’緩存策略來做元數(shù)據(jù)緩存。實(shí)例的屬性會(huì)在一定的時(shí)間(可定義)后過期,過期后該實(shí)例將會(huì)在內(nèi)存中刪除。每個(gè)線程有一個(gè)LRU(Least Recently Used) 列表,每個(gè)緩存實(shí)例只能存在于1個(gè)線程的LRU中,如果某個(gè)線程獲得了某個(gè)實(shí)例,將會(huì)要求原線程在LRU列表中釋放對(duì)應(yīng)條目。
每個(gè)線程需要自己負(fù)責(zé)垃圾回收,當(dāng)垃圾回收開始時(shí),線程將從LRU列表上最舊的條目開始執(zhí)行。 然后使用特定的垃圾策略來決定是否保存或清除條目。由于元數(shù)據(jù)緩存應(yīng)該非常大(高達(dá)數(shù)百萬條目),因此占滿分配內(nèi)存的90%(高位)之前不會(huì)發(fā)生垃圾回收。Ganesha盡可能多得將FSAL對(duì)象放入緩存的‘樹型拓?fù)?rsquo;中,其中節(jié)點(diǎn)代表目錄,葉子可代表文件和符號(hào)鏈接;葉子的垃圾回收要早于節(jié)點(diǎn),當(dāng)節(jié)點(diǎn)中沒有葉子時(shí)才會(huì)做垃圾回收。
File Content Cache數(shù)據(jù)緩存并不是獨(dú)立于與Inode Cache,一個(gè)對(duì)象的元數(shù)據(jù)緩存和數(shù)據(jù)緩存會(huì)一一對(duì)應(yīng)(數(shù)據(jù)緩存是元數(shù)據(jù)緩存的‘子緩存’),從而避免了緩存不統(tǒng)一的情況。文件內(nèi)容會(huì)被緩存至本地文件系統(tǒng)的專用目錄中,一個(gè)數(shù)據(jù)緩存實(shí)例會(huì)對(duì)應(yīng)2個(gè)文件:索引文件和數(shù)據(jù)文件。數(shù)據(jù)文件等同于被緩存的文件。索引文件中包含了元數(shù)據(jù)信息,其中包含了對(duì)重要的FSAL handle。索引文件主要用于重建數(shù)據(jù)緩存,當(dāng)服務(wù)器端崩潰后沒有干凈地清掉緩存時(shí),F(xiàn)SAL handle會(huì)讀取索引文件中的信息來重建元數(shù)據(jù)緩存,并將其指向數(shù)據(jù)文件,用以重建數(shù)據(jù)緩存實(shí)例。
當(dāng)緩存不足時(shí),worker thread會(huì)查看LRU列表中很久未被打開的實(shí)例,然后開始做元數(shù)據(jù)緩存回收。當(dāng)元數(shù)據(jù)緩存回收開始時(shí),數(shù)據(jù)緩存的垃圾回收也會(huì)同時(shí)進(jìn)行:在回收文件緩存實(shí)例時(shí),元數(shù)據(jù)緩存會(huì)問詢數(shù)據(jù)緩存是否認(rèn)識(shí)該文件實(shí)例,如果不認(rèn)識(shí)則代表該數(shù)據(jù)緩存已經(jīng)無效,則元數(shù)據(jù)回收正常進(jìn)行,并完成實(shí)例緩存回收;如果認(rèn)識(shí),對(duì)應(yīng)的文件緩存以及數(shù)據(jù)緩存均會(huì)被回收,隨后對(duì)應(yīng)的元數(shù)據(jù)緩存也會(huì)被回收。這樣保證了一個(gè)數(shù)據(jù)緩存有效的實(shí)例不會(huì)被回收。
這種方式很符合Ganesha的架構(gòu)設(shè)計(jì):worker線程可以同時(shí)管理元數(shù)據(jù)緩存和數(shù)據(jù)緩存,兩者一直保持一致。Ganesha在小文件的數(shù)據(jù)緩存上采用’write back’策略,如果文件很大的話則會(huì)直接讀取,而不經(jīng)過緩存;可以改進(jìn)的地方是可以把大文件分割成部分放入緩存中,提高讀寫效率。
FSAL(File System Abstraction Layer)
FSAL是相當(dāng)重要的模塊。FSAL本身給Inode Cache和File Content Cache提供了通用接口,收到請(qǐng)求后會(huì)調(diào)用具體的FSAL(FSAL_SNMP, FSAL_RGW等)。FSAL中的對(duì)象對(duì)應(yīng)一個(gè)FSAL handle。由于FSAL的語義設(shè)計(jì)與NFSv4很相似,因此開發(fā)和可以自己編寫新的FSAL API來適配Ganesha。Ganehsa軟件包還提供了FSAL源代碼模板。
4. 一個(gè)栗子
介紹了許多NFS Ganesha的內(nèi)部構(gòu)造,這邊通過一個(gè)NFS Ganesha對(duì)接Ceph RGW的例子來闡述一下代碼IO:
Figure 2 – NFS Ganesha workflow
以open()為例來,如上圖所示。首先用戶或者應(yīng)用程序開始調(diào)用文件操作,經(jīng)過系統(tǒng)調(diào)用 sys_open(),到達(dá)虛擬文件系統(tǒng)層 vfs_open(),然后交給 NFS 文件系統(tǒng)nfs_open()來處理。NFS 文件系統(tǒng)無法操作存儲(chǔ)介質(zhì),它調(diào)用 NFS 客戶端函數(shù)nfs3_proc_open() 進(jìn)行通信,把文件操作轉(zhuǎn)發(fā)到NFS Ganesha服務(wù)器。
Ganesha中監(jiān)聽客戶端請(qǐng)求的是Dispatcher這個(gè)進(jìn)程:其中的nfs_rpc_get_funcdesc()函數(shù)通過調(diào)用svc_getargs()來讀取xprt(rpc通信句柄)中的數(shù)據(jù),從而得到用戶的具體請(qǐng)求,然后將這些信息注入到reqdata這個(gè)變量中。隨后Dispatcher這個(gè)線程會(huì)把用戶請(qǐng)求-reqdata插入到請(qǐng)求隊(duì)列中,等待處理。
Ganesha會(huì)選擇一個(gè)最空閑的worker thread來處理請(qǐng)求:通過調(diào)用nfs_rpc_dequeue_req()將一個(gè)請(qǐng)求從等待隊(duì)列中取出,隨后調(diào)用nfs_rpc_execute()函數(shù)處理請(qǐng)求。Ganesha內(nèi)部自建了一個(gè)請(qǐng)求/回復(fù)緩存,nfs_dupreq_start()函數(shù)會(huì)在哈希表中尋找是否有一樣的請(qǐng)求,如果找到,則尋找到對(duì)應(yīng)回復(fù),然后調(diào)用svc_sendreply()將回復(fù)發(fā)送給客戶端,從而完成一個(gè)請(qǐng)求的處理。
如果Ganesha沒有在哈希表中找到一樣的請(qǐng)求,nfs_dupreq_start()這個(gè)函數(shù)會(huì)在緩存中新建一個(gè)請(qǐng)求,隨后調(diào)用service_function(),也就是nfs_open()。FSAL(filesystem abstract layer)收到nfs_open()調(diào)用請(qǐng)求后,會(huì)調(diào)用fsal_open2()函數(shù)。由于我們已經(jīng)在初始化階段,在ganesha.conf指定了FSAL為RGW,并且在FSAL/FSAL_RGW/handle.c文件下我們已經(jīng)重定向了FSAL的操作函數(shù),因此fsal_open2()實(shí)際會(huì)調(diào)用rgw_fsal_open2(),通過使用librgw來進(jìn)行具體操作。請(qǐng)求完成后,回復(fù)會(huì)插入到對(duì)應(yīng)哈希表中,與請(qǐng)求建立映射,隨后回復(fù)通過svc_sendreply()發(fā)送給客戶端。由此完成了sys_open()這個(gè)函數(shù)的調(diào)用。