Namenode中主要存储fsimage和editlog文件,Datanode中主要存储数据块blk文件。下面分别介绍Namenode和Datanode中的文件存储结构。
文件所在位置由hdfs-site.xml中的配置项dfs.namenode.name.dir配置。这些文件都存于${dfs.namenode.name.dir}/current文件夹下,在dfs.namenode.name.dir配置项中可以配置多个目录,各个目录存储的文件结构和内容都完全一样,配置多个目录只是起到冗余备份的作用。
<property>
<name>dfs.namenode.name.dirname>
<value>file://${hadoop.tmp.dir}/dfs/namevalue>
property>
dfs.namenode.name.dir在hdfs-site.xml中定义的默认值为file://$
{hadoop.tmp.dir}/dfs/name,其中hadoop.tmp.dir是core-site.xml中的配置项,hadoop.tmp.dir的默认值为/tmp/hadoop-$
{user.name}。因此文件默认都是存放在/tmp/hadoop-$
{user.name}/dfs/name/current路径下的。
<property>
<name>hadoop.tmp.dirname>
<value>/tmp/hadoop-${user.name}value>
<description>A base for other temporary directories.description>
property>
具体到我的服务器,dfs.namenode.name.dir用了配置的/cloud/data/hadoop/dfs/nn,因此我的服务器上的文件的路径就直接为:/cloud/data/hadoop/dfs/nn/current。
<property>
<name>dfs.namenode.name.dirname>
<value>/cloud/data/hadoop/dfs/nn/value>
property>
nn文件夹下的内容有(之所以从nn文件夹而不是current文件夹下开始是因为要讲解和current同级的in_user.lock文件):
nn
├── current
│ ├── edits_0000000001362702459-0000000001363401818
│ ├── edits_0000000001363401819-0000000001363931603
│ ├── edits_0000000001363931604-0000000001364566627
│ ├── edits_0000000001364566628-0000000001365069009
│ ├── edits_0000000001365069010-0000000001365209404
│ ├── edits_0000000001365209405-0000000001365211445
│ ├── edits_0000000001365211446-0000000001365211447
│ ├── edits_0000000001365211448-0000000001365211451
│ ├── edits_0000000001365211452-0000000001365211453
│ ├── edits_0000000001365211454-0000000001365211625
│ ├── edits_0000000001365211626-0000000001365213635
│ ├── edits_inprogress_0000000001365213636
│ ├── fsimage_0000000001365431029
│ ├── fsimage_0000000001365431029.md5
│ ├── fsimage_0000000001365431090
│ ├── fsimage_0000000001365431090.md5
│ ├── seen_txid
│ └── VERSION
└── in_use.lock
从上面的目录树中可以发现,主要有6类文件:
VERSION是java属性文件,内容大致如下:
namespaceID=644097999
clusterID=CID-da027b7b-4e9f-4287-be7a-03735d895bc2
cTime=1603347772521
storageType=NAME_NODE
blockpoolID=BP-1345407316-192.168.100.148-1603347772521
layoutVersion=-63
namespaceID是文件系统唯一标识符,在文件系统首次格式化之后生成,在引入Federation特性后,可能会有多个namespace。
clusterID是系统生成或手动指定的集群ID,在-clusterID选项中可以使用它。
cTime表示Namenode存储的创建时间。
storageType表示这个文件存储的是什么进程的数据结构信息(如果是Datanode,则为DATA_NODE)。
blockpoolID表示每一个Namespace对应的块池id,这个id包括了其对应的Namenode节点的ip地址。
layoutVersion表示HDFS永久性数据结构的版本信息,只要数据结构变更,版本号也要递减,此时的HDFS也需要升级,否则磁盘仍旧使用旧版本的数据结构,这会导致新版本的Namenode无法使用。
edits文件中存放的是客户端执行的所有更新命名空间的操作。
这里我们首先了解一下transactionId的概念。transactionId与客户端每次发起的RPC操作相关,当客户端发起一次RPC请求对Namenode的命名空间修改后,Namenode就会在editlog中发起一个新的transaction用于记录这次操作,每个transaction会用一个唯一的transactionId标识。
edits_*分为两类文件,一类是edits_startTransactionId-endTransactionId,另一类是edits_inprogress_startTransactionId。对于第一类文件,每个edits文件都包含了文件名中从startTransactionId开始到endTransactionId之间的所有事务。
第二类文件表示正在进行处理的editlog,所有从startTransactionId开始的新的修改操作都会记录在这个文件中,直到HDFS重置这个日志文件,重置操作会将inprogress文件关闭,并将inprogress文件改名为正常的editlog文件,即第一类文件,同时还会打开一个新的inprogress文件,记录正在进行的事务。
可以通过oev命令查看edits文件,基本命令格式为:
hdfs oev -p 文件类型(通常为xml) -i edits文件 -o 转换后文件输出路径
<EDITS>
<EDITS_VERSION>-63EDITS_VERSION>
<RECORD>
<OPCODE>OP_START_LOG_SEGMENTOPCODE>
<DATA>
<TXID>63949TXID>
DATA>
RECORD>
<RECORD>
<OPCODE>OP_END_LOG_SEGMENTOPCODE>
<DATA>
<TXID>63950TXID>
DATA>
RECORD>
EDITS>
fsimage文件其实是hadoop文件系统元数据的一个永久性的检查点,其中包含hadoop文件系统中的所有目录和文件inode的序列化信息。
fsimage_*的具体文件命名规则是这样的:fsimage_endTransactionId,它包含hadoop文件系统中endTransactionId前的完整的HDFS命名空间元数据镜像。
可以通过oiv命令查看fsimage文件,基本命令格式为:
hdfs oiv -p 文件类型(通常为xml) -i fsimage文件 -o 转换后文件输出路径
md5校验文件,用于确保fsimage文件的正确性,可以作用于磁盘异常导致文件损坏的情况。
这个文件中保存了一个事务id,这个事务id值只会在两种情况下更新:
可以发现,这个事务id并不是Namenode内存中最新的事务id。这个文件的作用在于Namenode启动时,利用这个文件判断是否有edits文件丢失,Namenode启动时会检查seen_txid并确保内存中加载的事务id至少超过seen_txid,否则Namenode将终止启动操作。
这是一个被Namenode线程持有的锁文件,用于防止多个Namenode线程启动并且并发修改这个存储目录。
文件所在位置由hdfs-site.xml中的配置项dfs.datanode.data.dir配置。这些文件都存于${dfs.datanode.data.dir}/current文件夹下,在dfs.namenode.name.dir配置项中可以配置多个目录,这里要注意,Datanode的多个存储目录存储的数据块并不相同,并且不同的存储目录可以是异构的,这样的设计可以提高数据块IO的吞吐率,这是与Namenode中很大不同的一个地方。
<property>
<name>dfs.datanode.data.dirname>
<value>file://${hadoop.tmp.dir}/dfs/datavalue>
property>
dfs.datanode.data.dir在hdfs-site.xml中定义的默认值为file://$
{hadoop.tmp.dir}/dfs/data,其中hadoop.tmp.dir是core-site.xml中的配置项,hadoop.tmp.dir的默认值为/tmp/hadoop-$
{user.name}。因此文件默认都是存放在/tmp/hadoop-$
{user.name}/dfs/data/current路径下的。
<property>
<name>hadoop.tmp.dirname>
<value>/tmp/hadoop-${user.name}value>
<description>A base for other temporary directories.description>
property>
具体到我的服务器,dfs.datanode.data.dir配置了如下值,因此我的服务器上的文件的路径就包括以下8个路径:/cloud/data*/hadoop/dfs/dn/current。
<property>
<name>dfs.datanode.data.dirname> <value>/cloud/data1/hadoop/dfs/dn,/cloud/data2/hadoop/dfs/dn,/cloud/data3/hadoop/dfs/dn,/cloud/data4/hadoop/dfs/dn,/cloud/data5/hadoop/dfs/dn,/cloud/data6/hadoop/dfs/dn,/cloud/data7/hadoop/dfs/dn,/cloud/data8/hadoop/dfs/dn
value>
property>
以dfs.datanode.data.dir配置项配置的其中一个目录/cloud/data1/hadoop/dfs/dn为例,它的目录结构为:
以dfs.datanode.data.dir配置项配置的其中一个目录/cloud/data1/hadoop/dfs/dn为例,它的目录结构为:
dn
├── current
│ ├── BP-615261695-192.168.100.74-1602499969654
│ │ ├── current
│ │ │ ├── finalized
│ │ │ │ ├── subdir0
│ │ │ │ ├── subdir1
│ │ │ │ ├── subdir10
│ │ │ │ ├── subdir11
│ │ │ │ ├── subdir12
│ │ │ │ ├── subdir13
│ │ │ │ ├── subdir14
│ │ │ │ ├── subdir15
│ │ │ │ ├── subdir16
│ │ │ │ ├── subdir17
│ │ │ │ ├── subdir18
│ │ │ │ ├── subdir19
│ │ │ │ ├── subdir2
│ │ │ │ ├── subdir20
│ │ │ │ ├── subdir21
│ │ │ │ ├── subdir22
│ │ │ │ ├── subdir23
│ │ │ │ ├── subdir24
│ │ │ │ ├── subdir25
│ │ │ │ ├── subdir26
│ │ │ │ ├── subdir27
│ │ │ │ ├── subdir28
│ │ │ │ ├── subdir29
│ │ │ │ ├── subdir3
│ │ │ │ ├── subdir30
│ │ │ │ ├── subdir31
│ │ │ │ ├── subdir4
│ │ │ │ ├── subdir5
│ │ │ │ ├── subdir6
│ │ │ │ ├── subdir7
│ │ │ │ ├── subdir8
│ │ │ │ └── subdir9
│ │ │ ├── rbw
│ │ │ └── VERSION
│ │ ├── scanner.cursor
│ │ └── tmp
│ │ ├── blk_1080824082
│ │ ├── blk_1080824082_7111789.meta
│ │ ├── blk_1080834049
│ │ └── blk_1080834049_7120152.meta
│ └── VERSION
└── in_use.lock
由于篇幅所限,这里只展开了5层,在实际情况中,subdir0subdir31这32个文件夹每个文件夹下面还是subdir0subdir31这32个文件夹,这些文件夹下面才是类似于tmp文件夹下的blk_*文件。
*
-*
-*
文件夹这个目录是一个块池目录,块池目录保存了一个块池在当前存储目录下存储的所有数据块,在Federation部署方式中,Datanode的一个存储目录会包含多个以BP开头的块池目录。BP后面会紧跟一个唯一的随机块池ID。接下来的第2个*
代表当前块池对应的Namenode的IP地址。最后一个*
代表这个块池的创建时间。
storageID=DS-fd6c86e2-c1ab-4541-9693-5844c2359a0f
clusterID=CID-a56f3601-ac4b-4c37-95fd-3b04c24a54c9
cTime=0
datanodeUuid=b0f4eafa-54b1-4966-acf9-3986435d5cd6
storageType=DATA_NODE
layoutVersion=-57
storageID:存储 id 号
clusterID :集群 id,全局唯一
cTime :标记了 datanode 存储系统的创建时间,对于刚刚格式化的存储系统,这个属性为 0;但是在文件系统升级之后,该值会更新到新的时间戳。
datanodeUuid:datanode 的唯一识别码
storageType:存储类型
layoutVersion :是一个负整数。通常只有 HDFS 增加新特性时才会更新这个版本号。
namespaceID=800713668
cTime=1602499969654
blockpoolID=BP-615261695-192.168.100.74-1602499969654
layoutVersion=-57
namespaceID:是 datanode 首次访问 namenode 的时候从 namenode 处获取的storageID, 对每个 datanode 来说是唯一的(但对于单个 datanode 中所有存储目录来说则是相同的),namenode 可用这个属性来区分不同 datanode。(但我查看了不同datanode上的都相同)
cTime :标记了 datanode 存储系统的创建时间,对于刚刚格式化的存储系统,这个属性为 0;但是在文件系统升级之后,该值会更新到新的时间戳。
blockpoolID:一个 block pool id 标识一个 block pool,并且是跨集群的全局唯一。当一个新的 Namespace 被创建的时候(format 过程的一部分)会创建并持久化一个唯一 ID。在创建过程构建全局唯一的 BlockPoolID 比人为的配置更可靠一些。NN 将 BlockPoolID 持久化到磁盘中,在后续的启动过程中,会再次 load 并使用。(查看了发现不同datanode上的都相同,BP-1008895165-10.252.87.50-1593312108051的命名就遵循BP-random integer-NameNode-IP address-creation time)
layoutVersion: 是一个负整数。通常只有 HDFS 增加新特性时才会更新这个版本号。
finalized、rbw和tmp目录都是用于存储数据块的文件夹,包括数据块文件及其对应的校验和文件。他们的区别是存放不同状态下的数据块副本文件。我们知道Datanode上保存的数据块副本有5种状态:FINALIZED,RBW(replica being written),RUR(replica under recovery),RWR(replica waiting to be recovered),TEMPORATY(复制数据块或者进行集群数据块平衡操作时的状态)
finalized目录保存所有FINALIZED状态的副本,rbw目录保存RBW、RWR、RUR状态的副本,tmp目录保存TEMPORARY状态的副本。
如果对应到具体的过程,那么当客户端发起写请求创建一个新的副本时,这个副本会被放到rbw目录中;当在数据块备份和集群平衡存储过程中创建一个新的副本时,这个副本就会放到tmp目录中;一旦一个副本完成写操作并被提交,它就会被移到finalized目录中。当Datanode重启时,tmp目录中的所有副本将会被删除,rbw目录中的副本将会被加载为RWR状态,finalized目录中的副本将会被加载为FINALIZED状态。
finalized目录存储了已经完成写入操作的数据块,由于这样的数据块可能非常多,所以finalized目录会以特定的目录结构存储这些数据块。
在我的机器上,finalized文件夹有32个子文件夹(分别为subdir0-31),每个子文件夹下又有32个子文件夹(同样为subdir0-31),接着才是存的真正的数据块blk文件及其校验文件。你可能会发现自己的目录下不是32个子文件夹,而是64个或者256个,不要奇怪,这是hadoop不同版本之间的差异,下面简要介绍一下不同版本下的目录结构:
早期版本:当存储目录中的数据块超过64个时,将创建64个子目录来存储数据块。一个父目录最多可以创建64个子目录,一个子目录最多可以存放64个数据块以及64个子目录。
2.6版本:finalized目录下拥有256个一级子目录,每个一级子目录下可以拥有256个二级子目录。
2.8.5版本:finalized目录下拥有32个一级子目录,每个一级子目录下可以拥有32个二级子目录。
以2.8.5版本为例,数据块是根据什么判断自己应放在哪个目录下的呢?答案是根据数据块的id,具体的判断规则如下代码所示(DatanodeUtil类的idToBlockDir方法):
public static File idToBlockDir(File root, long blockId) {
int d1 = (int) ((blockId >> 16) & 0x1F);
int d2 = (int) ((blockId >> 8) & 0x1F);
String path = DataStorage.BLOCK_SUBDIR_PREFIX + d1 + SEP +
DataStorage.BLOCK_SUBDIR_PREFIX + d2;
return new File(root, path);
}
一级目录由d1指定,二级目录由d2指定,d1是数据块id右移16位,再取最后5位得到的数值,可以发现,它的范围是0-31。d2是数据块id右移8位,再取最后5位得到的数值,可以发现,它的范围也是0-31。一级目录和二级目录都对应32个文件夹,与预期相符。
2.6版本的判断规则与2.8.5完全一样,不同点是都是取最后8位,而不是5位,因此,2.6版本的一级目录和二级目录对应的文件夹个数都是256个。
这是一个被Datanode线程持有的锁文件,用于防止多个Datanode线程启动并发修改这个存储目录。
这个文件记录了访问文件的游标,文件具体内容如下所示:
{
"lastSavedMs" : 1604459692263,
"iterStartMs" : 1604314428656,
"curFinalizedDir" : null,
"curFinalizedSubDir" : null,
"curEntry" : null,
"atEnd" : true
}
blk_*
是数据块文件,其中*
代表的数据是数据块id。
blk_*_*
.meta是数据块校验文件,其中第一个*
是数据块id,第二个*
代表数据块的版本号。
blk的meta校验文件(保存blk的checksum信息)大小大概是blk文件大小的1/128,因为每512字节做一次校验生成4字节校验,在机器上验证了一下大小,接近1/128。
VERSION文件属性解释修改(像namespaceId有问题)