NFS-Ganesha 核心架構解讀
NFSv4 簡要概述
NFS 這個協議( NFSv2 )最初由 Sun Microsystems 在 1984 年設計提出,由於存在一些不足,因此在隨後由幾家公司聯合推出了 NFSv3。到了 NFSv4 時,開發完全由 IETF 主導,設計目標是:
提高互聯下的 NFS 訪問和性能
提供安全性
更強的跨平台操作
方便後期擴展
我們可以看到 NFSv4 在緩存能力、擴展性、高可用性方面取得了很大的突破,放棄了之前版本的無狀態性,採用了強狀態機制,客戶端和服務端採用了複雜的方式交互,由此保證了伺服器端的負載均衡,減少了客戶端或服務端的 RTO。
在安全性方面,NFSv4 採用了面向連接的協議,強制使用 RPCSEC_GSS 並且提供基於 RPC 的安全機制。放棄了之前版本中採用的 UDP,採用了 TCP。NFSv4 支持通過次要版本進行擴展,我們可以看到在 NFSv4.1 支持了 RDMA、pNFS 範式以及目錄委派等功能。
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 的方法會帶來更多有意思的功能。
1. 靈活的內存分配
首先,user-space 的程序可以分配大量的內存讓程序使用,這些內存可以用來建立軟體內部緩存,經過測試,我們只需要 4GB 就可以實現百萬級別的數據緩存。在一些 x86_64 平台的機器上,我們甚至可以分配更大的內存(16 32GB),來實現千萬級別的數據緩存。
2. 更強的可移植性
如果 NFS Ganesha 是 kernel-space 的話,那樣 NFS Ganesha 的內部結構只能適應一款特定的 OS,而很難移植到別的 OS 上。另外考慮的是代碼本身:在不同的平台上編譯和運行的產品比在一個單一平台上開發的產品更安全。 我們開發人員的經驗表明,只在單一平台上開發會讓開發後期困難重重;它通常會顯示在 Linux 上不會輕易檢測到的錯誤,因為資源不一樣。
當然可移植性不單單指讓 NFS Ganesha 可以運行在不同的 OS 上,能夠適配不同的文件系統也是考量之一。在 NFSv2 和 NFSv3 中,由於語義設計上偏向 Unix 類的文件系統,因此基本不可能適配非 Unix 類的文件系統。這一情況在 NFSv4 中大有改觀,NFSv4 的語義設計出發點是讓 NFS 能儘可能多地適配不同的文件系統,因此加強了文件/目錄屬性參數的抽象。
Ganesha 設計初衷是成為一個 NFSv4 通用伺服器,可以實現 NFSv4 的所有功能,因此也需要適配各種文件系統。在內核中實現這一功能是不容易的(內核編程會有很多限制),然而在 user-space 中實現這一點會便捷一些。
3. 更便捷的訪問機制
內核中的 NFSv4 訪問用戶空間中的服務不是那麼方便,因此其引入了 rpc_pipefs 機制, 用於解決用戶空間服務的橋樑,並且使用 kerberos5 管理安全性或 idmapd 守護程序來進行用戶名轉換。然而 Ganesha 不需要這些,它使用常規 API 來對外提供服務。
4. 對接FUSE
由於 NFS Ganesha 是一個運行在用戶空間的程序,因此它還提供了對一些用戶空間文件系統( FUSE )的支持,可以讓我們直接把 FUSE 掛載在 NFS 上而不需要內核的幫助。
NFS-Ganesha 框架淺析
Figure 1 – NFS Ganesha 分層架構圖
由上圖我們可以看到,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 源代碼模板。
NFS Ganesha 對接 Ceph RGW 實例
介紹了許多 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( ) 這個函數的調用。
出處:https://zhuanlan.zhihu.com/p/34833897
版權申明:內容來源網路,版權歸原創者所有。除非無法確認,我們都會標明作者及出處,如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝。
架構文摘
互聯網應用架構丨架構技術丨大型網站丨大數據丨機器學習


TAG:架構文摘 |