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