董昭通,李小勇
(上海交通大學 網絡空間安全學院,上海 200240)
大數據處理平臺主要由上層的分布式計算組件和底層的分布式存儲系統兩層構成。存儲層的熱門產品主要有HDFS[1]、Ceph[2]及OpenStack Swift[3]等,計算層的熱門產品主要有MapReduce[4]和Spark[5]等。以大數據熱門項目Hadoop為例,存儲組件為HDFS,計算組件是MapReduce或Spark。它的大致工作流程為:HDFS存儲海量的數據信息,計算組件啟動job作業從HDFS中下載數據后進行數據計算與分析;如果job2需要job1運算后的數據,需要job1將中間結果寫入HDFS的block中,此時會產生硬盤甚至跨網絡的讀寫,同時HDFS默認的三副本策略需要將數據鏈式推送到三個存儲節點,從而進一步造成性能的損失。若將計算節點一側的DRAM/SSD設備作為底層存儲系統的讀寫緩存,一方面DRAM/SSD設備的讀寫性能要遠遠高于機械硬盤,另一方面緩存層與計算節點的網絡距離更加接近,所以可以減輕底層存儲系統對于上層計算應用的性能制約,從而大幅提高大數據處理的生產力。基于以上原因,研究分布式緩存系統具有極高價值。
目前成熟的開源分布式緩存系統主要有Memcached[6]、Redis[7]及 Alluxio[8]等。
Memcached通過一致性哈希算法完成數據定位,但Memcached并不是嚴格意義上的分布式。一方面沒有完善的容錯機制;另一方面Memcached沒有持久化機制,故存在斷電丟失數據的風險。相比之下,Redis有著比較完善的分布式機制,支持數據備份,即master-slave主從模式的數據備份,當服務器斷電重啟后可以通過RDB機制或AOF機制進行數據重放而恢復數據。Alluxio是基于內存的分布式文件系統。相較于Memcached和Redis,Alluxio提供文件接口,存儲并維護文件元數據。元數據主要記錄文件的block信息和block所處的緩存服務器信息等。Alluxio異構管理后端大數據文件存儲,并統一向大數據計算框架和平臺提供數據存儲服務。
對于以上解決方案,Memcached并未提供完善的容錯機制和高可用機制,且和Redis一樣,均未向上層應用提供文件接口。Alluxio的缺陷在于:一方面所有元數據存放于主備master節點的JVM中,那么在海量小文件的存儲場景下,同一namespace下元數據規模量存在瓶頸,無法達到十億或百億級;另一方面,Alluxio不支持隨機讀寫,故作為分布式存儲系統的緩存子系統而言,無法支持更多的應用場景。此外,以上三種解決方案均未很好地針對DRAM+SSD的混合緩存存儲進行設計與優化。
基于以上分析,本系統建立在BlueOcean Storage System(BOSS)底層存儲的基礎上,設計并實現了面向大數據應用的分布式緩存子系統。基于DRAM+SSD混合存儲場景,支持全面的讀寫類型,通過優良的設計大幅提升了底層存儲系統的讀寫性能。
本文設計并實現的分布式緩存系統的各模塊組件與BoSS系統各組件之間的集成框架,如圖1所示。

圖1 緩存系統各組件集成框架
整個系統共存在3種角色,分別為緩存客戶端、緩存服務器以及元數據服務器。
2.1.1 緩存客戶端(CacheClient)
在生產環境下的大數據平臺中,緩存客戶端作為BoSS Client庫的一部分部署在計算節點上。上層計算應用通過調用BoSS Client庫訪問BoSS系統,BoSS Client庫通過調用CacheClient模塊來訪問緩存,CacheClient根據文件/對象名請求元數據服務器查找緩存是否命中以及該文件/對象的緩存所在位置,并請求對應的緩存服務器。
2.1.2 緩存服務器(CacheServer)
緩存服務器與BoSS Client并置部署在計算節點上,以daemon的方式持續監聽特定的端口,接收Cache Client的數據訪問請求。此外,管理本節點上的節點級元數據。每個CacheServer管理本地的DRAM易失性存儲設備和SSD持久化存儲設備。CacheServer接收到CacheClient的數據訪問請求后,根據文件/對象名查找本機內存中維護的元數據hashmap得出該文件/對象在該臺服務器的RAMDisk/SSD掛載路徑,從而返回DRAM/SSD中該文件/對象的數據流。
2.1.3 元數據服務器(MetaServer)
緩存系統的元數據由MongoDB管理,與BoSS Monitor組件并置部署在3個及以上的監控節點上。每臺監控節點上分別部署1個BoSS Monitor組件和1個MongoDB服 務。3個 BoSS Monitor和3個MongoDB主機之間均基于raft協議實現primary節點選舉操作。緩存元數據存放在名為object_cache的collection中,每條元數據document的信息為文件/對象名到對應的緩存服務器節點的映射信息。元數據的添加和刪除操作由CacheClient模塊控制。
當CacheClient發起寫請求時,整體流程如下。
(1)CacheClient請求元數據服務器上的cache_server集合,得出當前所有的CacheServer信息,并計算出最佳的CacheServer:

式(1)涉及到的變量的釋義如下。
①latency為CacheClient到當前CacheServer的網絡延遲,若兩者在同一主機,則該latency值為0。該值的獲取:每臺CacheServer主機啟動時,會向當前所有CacheServer發起Ping命令,分別得出所有CacheServer的平均網絡延遲從而建立map,latency的值則是通過查詢該map所得。
②weight為每臺CacheServer加入集群時寫入MongoDB的cache_server集合中的權重值,由用戶配置文件中定義,或調用CacheClient的SetServer Weight方法設定。
③freeCap為當前CacheServer上所管理的所有存儲設備的剩余空間,通過讀取MongoDB的cache_server集合得出。
(2)根據式(1)所選的最佳CacheServer,更新MongoDB中該文件/對象的全局元數據。
(3)CacheClient向該 CacheServer發起寫請求,給出寫類型和文件/對象數據流。若寫類型為隨機寫,由于SSD設備存在擦寫次數限制,故將文件/對象寫入DRAM,并通過預先定義的同步寫/異步寫策略,將文件/對象數據持久化到BoSS DataServer中。如果使用異步寫策略,則無法保證在持久化過程中該服務器突然宕機而丟失數據的問題。若寫類型為順序寫,則將文件/對象數據寫入SSD中,向CacheClient返回寫入成功的響應后,異步將文件/對象數據更新到底層BoSS DataServer中。
當CacheClient發起讀請求時,CacheClient訪問MongoDB得出該文件/對象所在的CacheServer地址后,向該CacheServer發起數據讀寫請求。
根據緩存數據命中情況將有如下流程:
(1)緩存命中本地CacheServer:此時將進行短路讀取,繞開socket接口直接讀取本地文件系統;
(2)緩存命中遠程CacheServer:CacheClient調用socket接口,CacheServer將文件/對象數據流通過網絡傳輸給CacheClient節點,數據拷貝過程通過sendfile()系統調用+DMA的方式進行零拷貝網絡傳輸;
(3)緩存未命中:CacheClient向相應的BoSS DataServer發起socket連接,從底層進行讀取。緩存未命中通常發生在第一次從底層BoSS DataServer中加載或者CacheServer將該文件/對象進行緩存替換的情況。
在計算任務較重的情況下,系統會產生大量的隨機讀寫,則緩存系統需要能夠承受短時間內的高并發量請求,這是提高性能的關鍵。程序并發模型的解決方案通常有多進程、多線程、線程池及I/O事件驅動模型等。
本文設計的分布式緩存系統采用的是事件驅動并發模型,基于Go語言編寫實現。該處理模塊遵循Reactor的設計模式,將I/O協程與事件處理協程相分離。I/O協程采用epoll統一處理所有socket事件和設備I/O事件,事件處理協程采用協程池處理I/O協程解析好的事件消息。這樣的設計框架將使得CPU每個核心、存儲設備及網卡設備等達到最大程度的并行狀態,最大程度地減少系統中的阻塞。
事件處理協程池的結構體定義如下:

協程池初始化時創建workerNum個協程,每個協程監聽taskQueue channel獲取任務,從而處理已準備好的文件描述符。協程池通過done channel結束協程池中所有協程。添加或刪除任務到taskQueue前都需要使用mutex來鎖定隊列,以避免資源競爭造成錯誤。
對于設備I/O,采用libaio庫來完成異步I/O操作,而異步I/O操作會阻塞于libaio的io_getevent(),故socket的高并發性能將受制于設備I/O的阻塞。因此,本文設計的分布式緩存子系統的I/O處理協程采用epoll統一處理所有socket事件和設備I/O事件。對于異步I/O操作,通過Linux 2.6.22及以上版本中的eventfd,可將libaio和epoll事件處理很好地結合,具體處理流程為:
(1)創建一個iocb結構體,用于處理本次異步I/O請求;
(2)創建一個eventfd,并將此eventfd設置到iocb結構中;
(3)調用io_submit()系統調用提交aio請求;
(4)調用epoll_ctl()系統調用將eventfd添加到epoll中;
(5)當eventfd可讀/可寫時,從eventfd讀出完成I/O請求的事件數量,并調用io_getevents()獲取到這些I/O事件。
對于異步I/O和網絡socket事件的統一處理,事件類型也將通過eventfd的信息來判斷。I/O協程將在空閑時間段阻塞在epoll_wait()上,等待事件的完成。當某個事件完成后,通過文件描述符判斷是否與aio事件的文件描述符相同。如果不同則為socket事件,正常處理網絡數據讀寫流程;如果相同則為aio事件,調用io_getevent()獲取完成的aio請求,并將其回調函數添加到協程池中進行事件的處理。
在實際的應用場景中,大數據平臺將會產生海量的小文件,故元數據模塊的設計應滿足至少十億級海量小文件的場景。若采用類似于HDFS的master-worker結構,所有元數據信息存儲在主備master上,當元數據量較大時,master節點的內存將會成為容量瓶頸。若采用一致性哈希算法,當增加或移除節點時,會有相當一部分緩存數據需進行遷移,那么數據遷移產生的I/O將會對正常業務I/O造成影響。因此,本文設計的分布式緩存系統的元數據模塊基于MongoDB實現。MongoDB可以在數據規模和性能之間取得很好的平衡,從而滿足業務需要。
本文設計的緩存子系統主要為兩級元數據模型,由全局元數據和節點級元數據組成。全局元數據存放于MongoDB中,記錄每個文件/對象映射到每個緩存數據副本所在CacheServer的信息。在選擇CacheServer時,同樣根據公式選擇最佳的CacheServer。CacheServer上存放節點級元數據,在/dev/shm中用一個hashmap存放所有文件/對象到DRAM/SSD設備掛載目錄的映射。
在寫數據時,流程為:
(1)CacheClient根據式(1)計算最佳的CacheServerIp;
(2)CacheClient將文件/對象與相應的Cache Server的映射信息作為document插入到MongoDB中;
(3)CacheClient更新該CacheServer上的節點級元數據的hashmap。
上述3個過程在CacheClient通過事務保證。若失敗則進行回滾;若在事務過程中出錯,則在之后的讀取過程中或者CacheServer的維護協程中定期清除此元數據。
執行此事務過程中的錯誤主要分為兩種情況。
(1)CacheClient寫入MongoDB后發生錯誤。此時,MongoDB存在全局元數據而相應CacheServer不存在節點級元數據,之后讀取該文件/對象時,CacheServer查詢自身節點級元數據時未找到該緩存數據,說明之前將該文件/對象全局元數據寫入MongoDB后CacheClient發生了宕機或者網絡錯誤,CacheServer則將MongoDB中該文件/對象的全局元數據刪除。
(2)CacheClient節點級元數據更新完成后文件/對象流推送過程中失敗。每臺CacheServer在啟動時都會根據實際的緩存數據信息建立自身的節點元數據hashmap,因此可以保證實際的數據讀寫不會出現差錯。此外,在CacheServer運行過程中會啟動定期維護的子協程,對節點元數據和實際緩存數據進行check,以消除長時間運行導致的錯誤累積現象。
讀數據時,流程為:
(1)根據文件/對象名和文件/對象哈希值為組合條件查詢MongoDB,得出該文件/對象所在CacheServer的IP地址;
(2)向該節點的CacheServer守護進程發送讀取請求;
(3)CacheServer根據自身節點級元數據,查詢hashmap得出該文件/對象所在的RAMDisk路徑或者SSD設備掛載路徑;
(4)CacheServer返回文件/對象數據流。
本文設計并實現的分布式緩存系統相關的部署集群有計算集群、存儲集群和MongoDB元數據集群。
3.1.1 硬件方面
每個集群內由3臺服務器主機組成,CPU為Intel i9-9900X,主頻為 3.5 GHz,10 Cores。內存為DDR3,1 600 MHz,容量為16 GB。網卡為Intel萬兆以太網卡,各主機之間通過萬兆光纖寬帶相連。計算集群內均配備SAMSUNG 500GB SSD設備,采用NVMe協議。
3.1.2 軟件方面
計算集群內的主機上部署CacheServer進程,DRAM設備建立RAMDisk進行數據讀寫。存儲集群內的服務器主機作為BoSS DataServer管理本機上的HDD。元數據集群內的服務器主機上分別部署BoSS Monitor監控進程和MongoDB的守護進程。
本文在計算集群內的主機上使用fio工具調用BoSS CLient庫進行讀寫測試,模擬I/O負載。限于篇幅,本文的測試方案主要側重于系統的性能,故選用讀寫類型為順序讀、順序寫、隨機讀和隨機寫共4種場景,使用fio工具模擬不同的線程數,分別對使用緩存和不使用緩存時的吞吐率變化情況或者IOPS變化情況進行測試與分析。
圖2為順序寫場景下帶緩存和無緩存的吞吐率變化情況,帶緩存的讀寫性能略低于無緩存。這是由于無緩存時,I/O負載直接寫入BoSS數據服務器節點的HDD上。加上緩存后的寫入流程為寫入CacheServer的DRAM并持久化到BoSS數據服務器上。因此,并發數較低時,吞吐率略低于無緩存情況。但是,當并發數較大時,由于緩存子系統的epoll+workerpoll的事件驅動框架,使得CPU、硬盤、網卡達到了最大程度的并行狀態。因此,在并發線程數較大時,有緩存的情況可以降低性能抖動。

圖2 緩存子系統順序寫性能
圖3為隨機寫場景下帶緩存和無緩存的IOPS變化情況。在隨機寫場景下,I/O負載直接寫入DRAM,之后在固定的時間點將RAMDisk中數據持久化到硬盤上。這個過程相當于對I/O序列進行合并排序后寫入HDD,故節省了HDD的往返尋道時間。而HDD的性能瓶頸在于磁頭尋道,故將I/O合并排序后相當于順序讀寫。磁頭只需要按照同一個方向轉動,所以IOPS將會大幅增加。同時,在并發度較大時,帶有緩存的隨機寫場景下,性能抖動要比無緩存時好得多。

圖3 緩存子系統隨機寫性能
圖4為順序讀場景下帶緩存和無緩存的吞吐率變化情況,圖5為隨機讀場景下帶緩存和無緩存時的IOPS變化情況。當無緩存時,BoSS系統的順序讀和隨機讀均是讀取HDD。而開啟緩存時,讀I/O將在BoSS Client側訪問RAMDisk中的緩存;若緩存不存在或者被替換,則對BoSS DataServer側的HDD進行讀取。若緩存命中且緩存位置在同一計算集群內其他BoSS Client節點,萬兆以太網和RAMDisk的讀取環境也要比讀取BoSS DataServer側的HDD設備讀取性能高。上述測試場景排除了第一次加載冷數據的情況和緩存替換的情況,故帶緩存的I/O讀寫要遠比無緩存的I/O讀寫的性能高。

圖4 緩存子系統順序讀性能

圖5 緩存子系統隨機讀性能
本文設計的面向大數據應用的分布式緩存子系統的創新之處在于:(1)NoSQL全局元數據和CacheServer節點級元數據組成的兩級元數據模型很好地達到了元數據規模和性能之間的平衡,更加適用于海量小文件的應用場景;(2)I/O事件驅動模型采用epoll協程+workerpool的架構,epoll協程統一管理socket事件和異步I/O事件,workerpool處理請求完成后的回調函數,這種架構使得CPU、網卡、硬盤達到最大程度的并行狀態,因此存儲系統在高并發情況下依舊能夠維持較高的吞吐量,性能平穩,波動較小。
本系統的不足在于未完善緩存數據持久化過程的異步機制和持久化過程中主機斷電可能造成的數據丟失問題。在后續工作中,將對異步持久化機制進行完善,使本緩存系統成為更加穩定、可靠、高性能的系統。