-
概述
HDFS作爲Hadoop底層存儲架構實現,提供了高可容錯性,以及較高的吞吐量等特性。在Hadoop 2.3版本里,HDFS提供了一個新特性——Centralized Cache Management。該特性能夠讓用戶顯式地把某些HDFS文件強制映射到內存中,防止被操作系統換出內存頁,提高內存利用效率,有效加快文件訪問速度。對於Hive來說,如果對某些SQL查詢裏需要經常讀取的表格文件進行緩存,通常能夠提升運行效率。
-
架構介紹
NameNode主要負責協調所有DataNode的堆外內存。DataNode會按照一定的時間間隔通過heartbeat向NameNode彙報所緩存的文件block塊記錄。NameNode會接收用戶緩存或清空某個路徑的緩存命令,然後通過heartbeat通知含有對應文件block的DataNode進行緩存。
NameNode提供一個緩存池(cache pools)來方便管理緩存命令(cache directives),緩存命令負責決定具體進行緩存的路徑。
目前緩存只能夠在文件夾或者文件的粒度進行控制。文件塊和子塊緩存仍然在計劃開發中。
-
緩存命令接口
具體用法和參數意義參見官網介紹,這裏只作簡單介紹。
Cache directive命令
-
addDirective
添加一個新的cache directive。如: hdfs cacheadmin -addDirective -path <path> -pool <pool-name>
- removeDirective
移除一個cache directive。如:hdfs cacheadmin -removeDirective <id>
-
removeDirectives
移除指定路徑的每個cache directive。如:hdfs cacheadmin -removeDirectives <path>
-
listDirectives
列出cache directive。如: hdfs cacheadmin -listDirectives
Cache pool命令
-
addPool
添加新的內存池。如:hdfs cacheadmin -addPool <name>
-
modifyPool
修改緩存池的metadata。如:hdfs cacheadmin -modifyPool <name> [-owner <owner>] [-group <group>] [-mode <mode>] [-limit <limit>] [-maxTtl <maxTtl>]
-
removePool
移除緩存池,同時也會移除在緩存池裏的路徑。如: hdfs cacheadmin -removePool <name>
-
listPools
顯示緩存池信息。如:hdfs cacheadmin -listPools [-stats] [<name>]
-
實踐與思考
上面的內容基本從官網簡單摘取過來,這節纔是本文章的重點,也是真正的乾貨所在。
首先要注意的是,官網沒有說明的一點,要讀取到cache到內存的文件block,必須要首先開啓Short-Circuit Local Reads特性。該特性簡單來說,就是對於在讀取文件位於相同節點的時候,能夠避免建立TCP socket,直接讀取位於磁盤上的文件block。需要注意的是,該特性有兩種不同實現方案(HDFS-2246和HDFS-347),其中,HDFS-347是Hadoop 2.x的重新實現方案,並且只有這個方案纔可以讀取Cache到內存的文件block。
在配置Cache的過程中,由於DataNode需要強制鎖定文件映射到內存中,需要設置hdfs-site.xml參數dfs.datanode.max.locked.memory參數(單位bytes)。另外要注意的是通常還需要更改操作系統參數ulimit -l(單位KB),否則DataNode在啓動的時候會拋出異常。
爲了能夠更加深入理解這個特性帶來的性能影響,在測試環境下進行了簡單的測試。
測試環境是一個有3個節點的小集羣。2個NameNode(啓用NameNode HA特性),3個DataNode。其中Active NameNode(93)總共有12G內存,8個Intel(R) Xeon(R) CPU 2.50GHz;另外backup NameNode(24)和DataNode(25)均只有4GB內存,4個Intel(R) Xeon(R) CPU 2.00GHz。
爲了能夠減少干擾,更加清晰地瞭解該特性帶來的性能變化,並且注意到要使用該特性,必須使用一個全新的API來進行讀取操作,因此寫了一個簡單的讀HDFS文件的測試程序來測量耗時。
對於一個約400MB大小的HDFS文件,進行讀取,得出數據如下:
|
Non Short-Circuit(ms)
|
Short-Circuit(ms)
|
Short-Circuit + Centralized Cache Management(ms)
|
---|---|---|---|
93 | 2993 | 1059 | 1014 |
24 | 2155 |
1137 |
1170 |
25 | 2751 | 1264 | 1199 |
從上面數據可以看到,開啓了Short-Circuit特性的時候,讀取文件至少快了2-3倍;當啓用Centralized Cache Management特性的時候,讀取文件並沒有太大的速度提升。
關於這個Cache Management不甚明顯的原因,查找相關資料(interactive-hadoop-via-flash-and-memory.pptx),可能說明了部分問題的原因:開啓了Short-Circuit本地讀取後,操作系統可能會在讀取的過程中同時緩存文件到內存裏,但是,如果此時節點內存使用比較緊張的時候(可能由於多個MR子任務的執行),操作系統可能會隨時把這些緩存換出,導致緩存無效。使用Cache Management顯式指定文件緩存後,可以保證該部分內存不被換出,同時由於使用的是off-heap memory,免於JVM的管理,避免產生GC。
因此考慮到目前測試環境上的僅僅執行簡單測試,沒有模擬出激烈的內存使用情況,可能導致效果不甚明顯。
另外,還嘗試過做Hive、HBase的讀取測試,無論是開啓Short-Circuit還是Short-Circuit + Centralized Cache Management都暫時沒有發現太大的性能提升,暫時懷疑是由於測試環境的節點實在太少,而且其中兩個節點的4G內存實在少得可憐,開啓任務後,空閒情況下只有5-600MB可用,而這兩個特性極度依賴於內存的使用情況,因此沒有辦法體現出真正的性能提升優勢。
當然,這只是一個推論,最好的做法還是,待仔細查看源碼理解具體實現,明白更加深層次的原因。
-
源代碼實現分析(Hadoop 2.6.0)
(1)DataNode把文件block進行緩存並鎖定內存
NameNode向DataNode請求把對應的HDFS的文件block進行cache的時候,DataNode將接收到的RPC命令是DatanodeProtocol.DNA_CACHE。然後會調用FsDataSetImpl.cache()把所有請求block塊進行緩存。接着調用cacheBlock()把每個Block進行逐個緩存。
FsDataSetImpl.cacheBlock()函數主要是把每個block的緩存任務分配到每個Volume的線程中異步執行。通過找到對應的Volume後,其會調用FsDatasetCache.cacheBlock()進行具體的異步緩存任務執行。
FsDatasetCache.cacheBlock()主要負責進行緩存異步任務提交。爲了追蹤緩存異步任務執行結果,首先把block id封裝成ExtendedBlockId對象,然後放入到mappableBlockMap裏,同時更新狀態爲CACHING。接着向FsVolumeImpl.cacheExecutor線程池提交一個CachingTask異步任務。
CachingTask主要負責把具體的文件Block進行緩存和鎖定。具體緩存和鎖定的操作會調用MappableBlock.load()執行。然後把返回的MappableBlock保存到 FsDatasetCache的mappableBlockMap中,同時更新狀態爲Cached(如果之前狀態爲CACHING_CANCELLED時,則不會保留),並且更新引用計數。
MappableBlock.load主要是調用FileChannelImpl.map和POSIX.mlock(最終會調用到libc的mmap和mlock),同時進行文件塊校驗。然後返回新建MappableBlock對象。
(2)客戶端HDFS讀邏輯
HDFS的讀主要有三種: 網絡I/O讀 -> short circuit read -> zero-copy read。網絡I/O讀就是傳統的HDFS讀,通過DFSClient和Block所在的DataNode建立網絡連接傳輸數據。short circuit read就是直接讀取本地文件。zero-copy read就是在short circuit read基礎上,直接讀取之前緩存了的文件block內存。
這裏給出一個利用新API進行zero-copy read的例子。
爲了能夠進行short circuit read和zero-copy read,需要採用指定的API——ByteBuffer read(ByteBufferPool bufferPool,int maxLength, EnumSet<ReadOption> opts)。在調用函數FileSystem.get獲取具體的FileSystem子類(如果是HDFS,則是DistributedFileSystem),會創建DFSClient對象,DFSClient對象內部封裝了ClientProtocol對象,負責和NameNode進行RPC通信,獲取需要讀取的block信息。
FSDataInputStream.read()函數負責HDFS文件block讀邏輯處理。真正的實現在DFSInputStream.tryReadZeroCopy()函數裏。具體主要調用BlockReader.getClientMmap()函數,如果是開啓了short circuit read特性,則具體調用的是BlockReaderLocal.getClientMmap()。getClientMmap()會調用到ShortCircuitCache.getOrCreateClientMmap(),函數實現會查看緩存是否已經進行過MMap過的文件block塊信息。如果不存在,則會調用ShortCircuitReplica.loadMmapInternal(),然後最終會調用到FileChannelImpl.map(),如果之前進行過文件緩存,並mmlock鎖定,則這裏就會把具體的物理內存映射到本進程的虛擬內存地址空間中,如果沒有,則會重新讀取文件進行mmap操作。
可見,HDFS的這個zero copy read特性有一定限制,需要在本地上有需要讀取的block塊數據,也就是同樣需要考慮data-lock限制,因此綜合來看,主要是針對short circuit read的性能提升。目前的情況是在測試環境下,short circuit read時開啓後有較大的性能提升,zero copy read對比起short circuit read暫時沒有太大的明顯變化。