本文介绍了Hadoop体系中最重要的HDFS原理。
Hadoop由HDFS、MapReduce、HBase、Hive和ZooKeeper等成员组成,其中最基础最重要元素为底层用于存储集群中所有存储节点文件的文件系统HDFS(Hadoop Distributed File System)来执行MapReduce程序的MapReduce引擎。
HDFS是一个高度容错性的分布式文件系统,可以被广泛的部署于廉价的PC上。它以流式访问模式访问应用程序的数据,这大大提高了整个系统的数据吞吐量,因而非常适合用于具有超大数据集的应用程序中。
HDFS的架构如图所示。HDFS架构采用主从架构(master/slave)。
一个典型的HDFS集群包含一个NameNode节点和多个DataNode节点。NameNode节点负责整个HDFS文件系统中的文件的元数据的保管和管理,集群中通常只有一台机器上运行NameNode实例,DataNode节点保存文件中的数据,集群中的机器分别运行一个DataNode实例。
在HDFS中,NameNode节点被称为名称节点,DataNode节点被称为数据节点。DataNode节点通过心跳机制与NameNode节点进行定时的通信。
HDFS拥有如下特点:
NameNode是分布式文件系统中的管理者,存储文件系统的meta-data,主要负责管理文件系统的namespace(命名空间),集群配置信息,block(存储块)的复制。
这里的NameSpace
就是指HDFS类似经典的层级文件组织架构,可创建、删除文件,从一个目录移动文件到另一个文件,重命名文件或目录等。
NameSpace由NameNode管理,会记录所有对HDFS文件系统NameSpace的操作到EditsLog。
文件副本数被称为该文件的复制因子,该信息由NameNode保存。
保存最近一次checkpoint的时间
注:以上这些文件(Fsimage EditLog Fstime)是保存在linux的文件系统中,而不是HDFS
truncate
旧的EditLog(因为所记录的操作事务已经合并到新的Fsimage)dfs.namenode.checkpoint.period
指定两次checkpoint的最大时间间隔,默认3600秒。dfs.namenode.checkpoint.txns
文件系统操作事务累积阈值fs.checkpoint.size
规定edits文件的最大值,一旦超过这个值则强制checkpoint,不管是否到达最大时间间隔。默认大小是64M。fsimage
的文件中,每次保存fsimage之后到下次保存之间的所有HDFS相关操作,将会记录在editlog
文件中DataNode
的HeartBeat
,记录DataNode上报的block信息NameNode会维护一个fsimage文件,也就是namenode中metedata
的镜像,但是fsimage不会随时与namenode内存中的metedata保持一致,而是每隔一段时间通过合并edits文件来更新内容。Standby Namenode
主要工作就是合并fsimage和edits文件,以此更新NameNode的metedata。
NameNode上维护一个故障转移控制器进程(FailoverController,比如zkfc),监控着Active NameNode(心跳),并在失效时进行故障切换。
NameNode迁移执行过程:
从NameNode上下载元数据信息(fsimage,edits),然后把二者合并,生成新的fsimage,在本地保存,并将其推送到NameNode,替换旧的fsimage。
Fsimage
在该方案中主备 NameNode 之间通过一组JournalNode
同步元数据信息,一条数据只要成功写入多数的 JournalNode 即认为写入成功。 通常配置奇数个(2N+1)个 JournalNode,这样,只要 N+1 个写入成功就认为数据写入成功,此时最多容忍 N-1 个 JournalNode 挂掉,比如 3 个 JournalNode 时,最多允许 1 个 JournalNode 挂掉,5 个 JournalNode 时,最多允许 2 个 JournalNode 挂掉。
StandBy NameNode观察到EditLog变化,就会读取并更新其内部的Fsimage。 一旦Active NameNode挂掉,StandBy NameNode会保证读取所有EditLog。其实原理和上面通用的方案步骤相同。
Blockreport
,包含该DataNode的所有block列表hdfs文件块的副本,默认是三个。所以一般存hdfs文件的硬盘不用做RAID。
HDFS 运行在跨越大量机架的集群之上。两个不同机架上的节点是通过交换机实现通信的,在大多数情况下,相同机架上机器间的网络带宽优于在不同机架上的机器。
在开始的时候,每一个数据节点自检它所属的机架 id,然后在向名字节点注册的时候告知它的机架 id。HDFS 提供接口以便很容易地挂载检测机架标示的模块。一个简单但不是最优的方式就是将副本放置在不同的机架上,这就防止了机架故障时数据的丢失,并且在读数据的时候可以充分利用不同机架的带宽。这个方式均匀地将复制分散在集群中,这就简单地实现了组建故障时的负载均衡。然而这种方式增加了写的成本,因为写的时候需要跨越多个机架传输文件块。
下面展示了NameNode拥有的两个文件的副本元数据,及在多个DataNode上的分布情况:
因为Namenode保存所有文件有内存扩展限制,所以在超大集群扩展时会成为瓶颈。2.x引入Federal HDFS概念,允许启动多个NameNode来进行扩展,每个NameNode管理文件系统命名空间的一部分。例如,NameNode1管理/user目录下的所有文件,NameNode2管理/share目录下的所有文件。
每个NameNode会维护一个NameSpace Volume(命名空间卷),包括了命名空间的源数据,以及有一个容纳该Namespace下的文件的所有Block的Block池。也就是说,每个NameNode之间相互独立不影响。所以,DataNode需要注册到所有NameNode,并且存放来自多个Block池的Block。
在进行数据读写的流程分析前,我们先讲三个基本概念:
例如,在client端向DataNode传数据的时候,HDFSOutputStream会有一个chunk buffer,写满一个chunk后,会计算校验和并写入当前的chunk,之后再把带有校验和的chunk写入packet。
当一个packet写满后,packet会进入DataQueue
队列,其他的DataNode就是从这个DataQueue获取client端上传的数据并存储的。同时一个DataNode成功存储一个packet之后会返回一个ack packet
,放入ack Queue
中。
packet
(数据包),并写入内部DataQueue
(数据队列)。pipeline
管道。当DataStreamer把数据流式传输到管道中的第一个DataNode后,该DataNode会先存放数据包(packet),然后转发给流水线中的第二个DataNode。第二个DataNode收到后同样转发给第三个DataNode。注意这里是流的形式在管道传输,而不是一个block或一个文件,所以效率很高。ackQueue
用来存放那些在DataNode写数据成功后发回的 ack
。只有当所有管道中的DataNode都发回了 ack 后,才会将该packet
从管道中移除。flush
所有剩余的数据包到DataNode 管道中,然后联系NameNode来标记文件已经写入完成,并等待发回的ack
。关闭pipeline,将ackQueue
中的所有packet 放到 DataQueue
的前段,这样管道中失败DataNode节点下游的DataNode不会丢失任何packet。当前写入的Block(位于正确的DataNode的上的副本)会被给与新的标识然后传递给NameNode。这样做的目的是在失败的DataNode恢复后删掉这个不完整的Block(因为写入失败了)。
在出错DataNode从管道中删除后,会在两个工况良好的DataNode间建立新的pipeline。随后将该block的剩余的数据写入管道中正常的DataNode。
最后,NameNode将在新的其他DataNode上创建该block的副本。接下来,其他block就按正常流程处理即可。
这样情况很少发生。只要写入的副本数达到了dfs.namenode.replication.min
(默认为1)就会成功,然后会按dfs.replication
(默认为3)异步复制该block到其他节点上。
如果有更多副本,那就随机选择,原则是尽量在一个机架上少放分本
HDFS不能自己定义网络拓扑结构,必须手动配置。顺序关系如下:
同节点->同机架上的不同节点->同数据中心不同机架->不同数据中心
HDFS为了均衡性能设计了一套数据一致性模型。
HDFS写入的数据不能保证立即可见(如果是写入的新建的文件,文件可见数据不可见)。也就是说即使已经做了flush
操作该文件长度也有可能为0。
具体来说,数据流必须当数据超过一个Block后,这第一个和之后的Block才对新的reader可见。总之,正在写入的Block对其他reader不可见,除非已经写完该Block。
HDFS提供了FSDataOutputStream.hflush方法来使得数据对reader可见。注意,hflush不能保证数据被DataNode写入磁盘而只能保证到内存,也就是说如果机器断电等故障可能导致数据丢失。
还有一个hsync方法,和hflush很像,不同之处是hsync除了保证数据对reader可见还会强制持久化数据。hsync有一定性能开销,要适当选择hsync时机。
close方法关闭文件流,并且在此之前会执行hflush。
HDFS中如果存在大量小文件会占用大量NameNode内存,十分低效。但必须注意,比如1MB的小文件用128MB的Block存储,也只会实际占用1MB磁盘。
Hadoop存档文件或HAR文件,高效的文件存档工具,将文件存入HDFS Block,减少NameNode内存使用,对文件访问透明,甚至可以直接作为MapReduce的输入。HAR是通过Archive工具来创建,需要运行MR程序来并行处理输入文件。HAR主要由索引文件和数据文件组成。
建立HAR开销、不可修改、作为MR输入时低效
读写过程,数据完整性如何保持?
HDFS会对写入的所有数据计算校验和,并在读取时验证校验和(io.bytes.per.checksum)。一般使用CRC-32校验,占用4个字节。
具体来说,HDFS做法主要是通过校验和验证数据完整性。因为每个chunk中都有一个校验位,一个个chunk构成packet,一个个packet最终形成block,故可在block上求校验和。
HDFS 的client端即实现了对 HDFS 文件内容的校验和 (checksum) 检查:
HDFS中文件块目录结构具体格式如下:
${dfs.datanode.data.dir}/
├── current
│ ├── BP-526805057-127.0.0.1-1411980876842
│ │ └── current
│ │ ├── VERSION
│ │ ├── finalized
│ │ │ ├── blk_1073741825
│ │ │ ├── blk_1073741825_1001.meta
│ │ │ ├── blk_1073741826
│ │ │ └── blk_1073741826_1002.meta
│ │ └── rbw
│ └── VERSION
└── in_use.lock
in_use.lock表示DataNode正在对文件夹进行操作
rbw是“replica being written”的意思,该目录用于存储用户当前正在写入的数据。
Block元数据文件(*.meta)由一个包含版本、类型信息的头文件和一系列校验值组成。校验和也正是存在其中。
Client写入数据时,会带上数据的校验和。在写入数据的管道上的最后一个DataNode负责校验数据,如果出错会抛异常,由Client决定下一步操作。
Clietn读取数据时会利用从DataNode读取到的校验和(隐藏文件内)和读到的数据进行校验比对,如果校验成功会通知DataNode,DataNode更新日志,可帮助检测磁盘损坏。
DataNode上运行着DataBlockScanner,定期自检其上的数据。
数据文件压缩可以减少磁盘空间开销,加速数据在网络和磁盘上传输速度。
压缩算法需要权衡空间和时间,压缩速度快的代价往往是压缩比低。
压缩格式 | 工具 | 算法 | 扩展名 | 是否可切分 |
---|---|---|---|---|
Gzip | gzip | DEFLATE | .gz | 否 |
bzip2 | bzip2 | bzip2 | .bz2 | 是 |
LZ4 | 无 | LZ4 | .lz4 | 否 |
LZO | lzop | LZO | .lzo | 是(取决于使用的库) |
注:是否可切分指的是搜索数据流的任意位置并进一步往下读取数据,可切分更适合MapReduce。
总的来说,LZO,Snappy压缩速度更快,比gzip快一个数量级,但压缩比较低。Snappy解压速度比LZO高出很多。
Hadoop应用处理的数据集非常大,因此需要借助于压缩,使用哪种压缩格式与待处理的文件的大小格式和所使用的工具相关。
对于gzip,MR不会直接尝试切分.gzip格式文件,而是要读取这个文件的所有块,然后用一个map任务处理所有这些块。但这会牺牲数据本地性,因为大多数文件快没有存储在该map任务的节点。
LZO可以在预处理的时候使用Hadoop LZO库中的索引工具,来构建切分点索引。
下面按照效率从高到低排列:
因为HDFS中各个节点通信用的RPC协议,会将消息序列化成二进制流发送,到达目的地后再将二进制流反序列化为原始消息。
RPC序列化格式的四大理想属性是:格式紧凑、序列化反序列化速度快、格式可扩展、支持不同语言读写。
Hadoop使用的是自己的序列化格式Writable,它符合紧凑、速度快,但扩展性不好。
解决了Writable多语言支持不足的问题。
考虑到某些分布式场景中大对象存在单个文件中不能实现扩展,所以在HDFS有一些高层次容器。
SequenceFile
面向行SequenceFile
是一个二进制key/value键值对持久数据结构,可以自己选择key(比如LongWritable类型所表示的时间戳),值可以使Writable类型(表示日志记录的数量)。
SequenceFile也可以作为小文件的容器,以获得更高效的存储和处理。
SequenceFile.writer写入时会在顺序写文件过程中插入特殊字符来分隔若干记录,称为同步标识。注意该同步标识体积小,且始终处于每条记录的边界处即不会出现在记录内部。所以可以将SequenceFile作为MR输入,因为该文件可使用SequenceFileInputFormat读取并切分map处理。
以星号代替同步标识的一个SequenceFile部分内容如下:
SequenceFile包括文件头(代码、版本号、key/value名称,数据压缩、用户定义元数据、随机生成的该文件的同步标识(Sync))和若干Record。
Record的Value默认未开启压缩,可按单条(Key无压缩)或Block压缩(多条Record)。未开启压缩和单Record压缩如图:
开启Block压缩如图:
可以使用如下命令查看HDFS中的SequenceFile:
hadoop fs -text number.seq | head
MapFile
面向行MapFile
是已经排序过的SequenceFile**,有索引(默认隔128个key就放到索引,也是个SequenceFile),可以按key查找。Avro
面向行row split
,每个Split面向列存储。ClientProtocol
与NameNode机器配置的TCP端口的建立连接。所有DataNode都会周期性地向NameNode发送心跳,当网络异常即发送网络分区时,会导致部分DataNode无法连接到NameNode。此时NameNode会侦测到最近无心跳的DataNode(阈值默认为10分钟),并标记为死亡节点,不再转发新的客户端IO请求给他们,其上的数据也不再可用。
DataNode死亡还会使得某些Block的副本数少于指定数量,NameNode会一直追踪那些需要被复制的block,而且一旦需要(比如DataNode失联或硬盘损坏、副本崩溃、文件副本因子调大等)就会立刻构建、初始化Block副本。
当某个DataNode上的空闲磁盘空间下降到阈值时,某些主题的数据移动到其他DataNode。
可能会发生获取一个Block数据中途发生崩溃,原因如存储设备故障、网络故障、软件Bug等。
HDFS客户端实现了校验和来检查HDFS文件内容。创建HDFS文件时,同时会为该文件的每个Block计算校验和并将这些校验和存在单独的一个隐藏文件中(同一个namespace)。
当客户端读取文件内容时,会先通过关联的校验和文件来对接收到的文件进行校验匹配检查。当校验不通过时,客户端可以重选其他DataNode来获取该文件Block副本。
FsImage和EditLog如果挂了,可导致HDFS服务失效。可对NameNode设置,持有多个这两个文件的副本,同步更新。
也可以采用NFS共享存储或是JounalNode(推荐)对NameNode做HA处理。
或是用zkfc(ZKFailoverController)来检测NameNode在ZK中的状态,异常时切换为StandBy Namenode。
Snapshots支持在特定时刻存储数据副本。 快照功能的一种用途可以是将损坏的HDFS实例回滚到先前已知的工作良好的某个时间点。
Hadoop2.6.0提供了堆外内存持久化的能力,如果在数据刷入磁盘前重启服务,可能丢失数据。
可参见Memory Storage Support in HDFS
Archival Storage可将不断增长的存储容量与计算容量分离。可用那些更高密度和低成本的存储能力及较低的计算能力的节点可用于存储冷数据。
可参见Archival Storage, SSD & Memory
hdfs dfs -text /xxx/20190813/abc |more
hdfs dfs -ls -h /xxx/20190813
hdfs fsck /xxx/20190813/abc.lzo -locations -blocks -files
hdfs fsck /xxx/20190813 -openforwrite | grep OPENFORWRITE |awk -F ' ' '{print $1}'
hdfs debug recoverLease -path /xxx/20190813/abc.lzo
HDFS 副本放置策略的研究和优化
Hadoop学习笔记—1.基本介绍与环境配置
《Hadoop权威指南第四版》