HDFS中的數據塊(Block)


我們在分布式存儲原理總結中了解了分布式存儲的三大特點:

  1. 數據分塊,分布式的存儲在多台機器上
  2. 數據塊冗余存儲在多台機器以提高數據塊的高可用性
  3. 遵從主/從(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*replicationreplication表示數據塊的備份數。這個數組中存儲了該數據塊所有的備份數據塊對應的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堆內存空間也會基本保持線性同步增加。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM