我們在分布式存儲原理總結中了解了分布式存儲的三大特點:
- 數據分塊,分布式的存儲在多台機器上
- 數據塊冗余存儲在多台機器以提高數據塊的高可用性
- 遵從主/從(master/slave)結構的分布式存儲集群
HDFS作為分布式存儲的實現,肯定也具有上面3個特點。
HDFS分布式存儲:
在HDFS中,數據塊默認的大小是128M
,當我們往HDFS上上傳一個300多M的文件的時候,那么這個文件會被分成3個數據塊:
所有的數據塊是分布式的存儲在所有的DataNode上:
為了提高每一個數據塊的高可用性,在HDFS中每一個數據塊默認備份存儲3份,在這里我們看到的只有1份,是因為我們在hdfs-site.xml
中配置了如下的配置:
<property> <name>dfs.replication</name> <value>1</value> <description>表示數據塊的備份數量,不能大於DataNode的數量,默認值是3</description> </property>
我們也可以通過如下的命令,將文件/user/hadoop-twq/cmd/big_file.txt
的所有的數據塊都備份存儲3份:
hadoop fs -setrep 3 /user/hadoop-twq/cmd/big_file.txt
我們可以從如下可以看出:每一個數據塊都冗余存儲了3個備份
在這里,可能會問這里為什么看到的是2個備份呢?這個是因為我們的集群只有2個DataNode,所以最多只有2個備份,即使你設置成3個備份也沒用,所以我們設置的備份數一般都是比集群的DataNode的個數相等或者要少
一定要注意:當我們上傳362.4MB的數據到HDFS上后,如果數據塊的備份數是3個話,那么在HDFS上真正存儲的數據量大小是:362.4MB * 3 = 1087.2MB
注意:我們上面是通過HDFS的WEB UI來查看HDFS文件的數據塊的信息,除了這種方式查看數據塊的信息,我們還可以通過命令fsck來查看
數據塊的實現
在HDFS的實現中,數據塊被抽象成類org.apache.hadoop.hdfs.protocol.Block(我們以下簡稱Block)
。在Block類中有如下幾個屬性字段:
public class Block implements Writable, Comparable<Block> { private long blockId; // 標識一個Block的唯一Id private long numBytes; // Block的大小(單位是字節) private long generationStamp; // Block的生成時間戳 }
我們從WEB UI上的數據塊信息也可以看到:
一個Block除了存儲上面的3個字段信息,還需要知道這個Block含有多少個備份,每一個備份分別存儲在哪一個DataNode上,為了存儲這些信息,HDFS中有一個名為org.apache.hadoop.hdfs.server.blockmanagement.BlockInfoContiguous(下面我們簡稱為BlockInfo)
的類來存儲這些信息,這個BlockInfo類繼承Block類,如下:
BlockInfo類中只有一個非常核心的屬性,就是名為triplets的數組,這個數組的長度是3*replication
,replication
表示數據塊的備份數。這個數組中存儲了該數據塊所有的備份數據塊對應的DataNode信息,我們現在假設備份數是3
,那么這個數組的長度是3*3=9
,這個數組存儲的數據如下:
也就是說,triplets包含的信息:
- triplets[i]:Block所在的DataNode;
- triplets[i+1]:該DataNode上前一個Block;
- triplets[i+2]:該DataNode上后一個Block;
其中i表示的是Block的第i個副本,i取值[0,replication)。
我們在HDFS的NameNode中的Namespace管理中講到了,一個HDFS文件包含一個BlockInfo數組,表示這個文件分成的若干個數據塊,這個BlockInfo數組實際上就是我們這里說的BlockInfoContiguous
數組。以下是INodeFile的屬性:
public class INodeFile { private long header = 0L; // 用於標識存儲策略ID、副本數和數據塊大小的信息 private BlockInfoContiguous[] blocks; // 該文件包含的數據塊數組 }
那么,到現在為止,我們了解到了這些信息:文件包含了哪些Block,這些Block分別被實際存儲在哪些DataNode上,DataNode上所有Block前后鏈表關系。
如果從信息完整度來看,以上信息數據足夠支持所有關於HDFS文件系統的正常操作,但還存在一個使用場景較多的問題:怎樣通過blockId快速定位BlockInfo?
我們其實可以在NameNode上用一個HashMap來維護blockId到Block的映射,也就是說我們可以使用HashMap<Block, BlockInfo>
來維護,這樣的話我們就可以快速的根據blockId定位BlockInfo,但是由於在內存使用、碰撞沖突解決和性能等方面存在問題,Hadoop團隊之后使用重新實現的LightWeightGSet代替HashMap,該數據結構本質上也是利用鏈表解決碰撞沖突的HashTable,但是在易用性、內存占用和性能等方面表現更好。
HDFS為了解決通過blockId快速定位BlockInfo的問題,所以引入了BlocksMap,BlocksMap底層通過LightWeightGSet實現。
在HDFS集群啟動過程,DataNode會進行BR(BlockReport,其實就是將DataNode自身存儲的數據塊上報給NameNode),根據BR的每一個Block計算其HashCode,之后將對應的BlockInfo插入到相應位置逐漸構建起來巨大的BlocksMap。前面在INodeFile里也提到的BlockInfo集合,如果我們將BlocksMap里的BlockInfo與所有INodeFile里的BlockInfo分別收集起來,可以發現兩個集合完全相同,事實上BlocksMap里所有的BlockInfo就是INodeFile中對應BlockInfo的引用;通過Block查找對應BlockInfo時,也是先對Block計算HashCode,根據結果快速定位到對應的BlockInfo信息。至此涉及到HDFS文件系統本身元數據的問題基本上已經解決了。
BlocksMap內存估算
HDFS將文件按照一定的大小切成多個Block,為了保證數據可靠性,每個Block對應多個副本,存儲在不同DataNode上。NameNode除需要維護Block本身的信息外,還需要維護從Block到DataNode列表的對應關系,用於描述每一個Block副本實際存儲的物理位置,BlocksMap結構即用於Block到DataNode列表的映射關系,BlocksMap是常駐在內存中,而且占用內存非常大,所以對BlocksMap進行內存的估算是非常有必要的。我們先看下BlocksMap的內部結構:
以下的內存估算是在64位操作系統上且沒有開啟指針壓縮功能場景下
以下的內存估算是在64位操作系統上且沒有開啟指針壓縮功能場景下 class BlocksMap { private final int capacity; // 占 4 字節 // 我們使用GSet的實現者:LightWeightGSet private GSet<Block, BlockInfoContiguous> blocks; // 引用類型占8字節 }
可以得出BlocksMap的直接內存大小是對象頭16字節 + 4字節 + 8字節 = 28字節
Block的結構如下:
public class Block implements Writable, Comparable<Block> { private long blockId; // 標識一個Block的唯一Id 占 8字節 private long numBytes; // Block的大小(單位是字節) 占 8字節 private long generationStamp; // Block的生成時間戳 占 8字節 }
可以得出Block的直接內存大小是對象頭16字節 + 8字節 + 8字節 + 8字節 = 40字節
BlockInfoContiguous的結構如下:
public class BlockInfoContiguous extends Block { private BlockCollection bc; // 引用類型占8字節 private LightWeightGSet.LinkedElement nextLinkedElement; // 引用類型占8字節 private Object[] triplets; // 引用類型 8字節 + 數組對象頭24字節 + 3*3(備份數假設為3)*8 = 104字節 }
可以得出BlockInfoContiguous的直接內存大小是對象頭16字節 + 8字節 + 8字節 + 104字節 = 136字節
LightWeightGSet的結構如下:
public class LightWeightGSet<K, E extends K> implements GSet<K, E> { private final LinkedElement[] entries; // 引用類型 8字節 + 數組對象頭24字節 = 32字節 private final int hash_mask; // 4字節 private int size = 0; // 4字節 private int modification = 0; // 4字節 }
LightWeightGSet本質是一個鏈式解決沖突的哈希表,為了避免rehash過程帶來的性能開銷,初始化時,LightWeightGSet的索引空間直接給到了整個JVM可用內存的2%,並且不再變化。 所以LightWeightGSet的直接內存大小為:對象頭16字節 + 32字節 + 4字節 + 4字節 + 4字節 + (2%*JVM可用內存) = 60字節 + (2%*JVM可用內存)
假設集群中共1億Block,NameNode可用內存空間固定大小128GB,則BlocksMap占用內存情況:
BlocksMap直接內存大小 + (Block直接內存大小 + BlockInfoContiguous直接內存大小) * 100M + LightWeightGSet直接內存大小 即: 28字節 + (40字節 + 136字節) * 100M + 60字節 + (2%*128G) = 19.7475GB
上面為什么是乘以100M呢? 因為100M = 100 * 1024 * 1024 bytes = 104857600 bytes,約等於1億字節,而上面的內存的單位都是字節的,我們乘以100M,就相當於1億Block
BlocksMap數據在NameNode整個生命周期內常駐內存,隨着數據規模的增加,對應Block數會隨之增多,BlocksMap所占用的JVM堆內存空間也會基本保持線性同步增加。