HDFS是GFS的简化版,它同一时刻只允许一个用户对同一文件进行追加写操作(GFS允许并发写)。它适合存储大文件,并提供高吞吐量的顺序读/写访问。
它的早期版本两大问题,例如:单点失效和水平扩展不佳。针对这两个问题,在hadoop2.0提出统一的解决方案,即HA和NameNode联盟。
HDFS的设计目标
- 存储大文件
- 文件一次写(支持追加写)/顺序读
HDFS优点:
- 适合大数据处理(支持GB,TB,PB级别的数据存储,支持百万规模以上的文件数量)
- 适合批处理(支持离线的批量数据处理,支持高吞吐率)
- 高容错性(以数据块存储,可以保存多个副本,容易实现负载均衡)
HDFS缺点:
- 小文件存取(占用namenode大量内存,浪费磁盘空间)
- 不支持并发写入(同一时刻只能有一个进程写入,不支持随机修改)
HDFS整体架构
HDFS采用master/slave架构。一个HDFS集群包含一个单独的NameNode,一个Secondary NameNode和多个DataNode。
NameNode
负责存储整个分布式文件系统的元数据,包括:
- 负责管理文件系统的名字空间操作,比如打开、关闭、重命名文件或目录。
- 负责确定数据块到具体Datanode节点的映射(文件到块的映射,块到DataNode的映射)。
- 监督Data nodes的健康
- 协调数据的存取。
这些数据保存在内存里,同时磁盘里面有两个元数据文件,fsimage和editlog,这两个结合可以构建完整的内存元数据。
Name元数据大小估计
对象类别 | 估算大小(bytes) | 计算方法 | 估计总大小(bytes) |
---|---|---|---|
文件 | 224 | 224+2*文件名长度 | 250 |
目录 | 264 | 264+2*文件名长度 | 290 |
块 | 152 | 152+72*副本数 | 368 |
Secondary NameNode
secondary NameNode并不是NameNode的热被,它负责拉取fsimage和editlog并且把它们合并形成新的fsimage并传回。所以本质上,Secondary NameNode是NameNode的一个检查点。
DataNode
DataNode保存文件数据,并负责数据块实际的读/写操作。HDFS会自动将用户上传的大文件切分为block,每个block默认大小是64M,每个block在HDFS中会备份三份
Client
Client和NameNode交互获得元数据,与DataNode交互进行实际的读写操作。
HDFS写文件操作
HDFS只允许同一时刻只有一个客户端对文件进行操作,同时HDFS支持追加写,不支持随机写
- Client调用DistributedFileSystem对象的create方法,创建一个文件输出流(FSDataOutputStream)对象
- 通过DistributedFileSystem对象与Hadoop集群的NameNode进行一次RPC远程调用,在HDFS的Namespace中创建一个文件条目(Entry),该条目没有任何的Block
- 写文件前,向NameNode申请Block,NameNode返回可写文件的DataNode列表
- 通过FSDataOutputStream对象,向DataNode写入数据,数据首先被写入FSDataOutputStream对象内部的Buffer中,然后数据被分割成一个个Packet(64k)数据包
- 以Packet最小单位,基于Socket连接发送到按特定算法选择的HDFS集群中一组DataNode(正常是3个,可能大于等于1)中的一个节点上,在这组DataNode组成的Pipeline上依次传输Packet
- 这组DataNode组成的Pipeline反方向上,发送ack,最终由Pipeline中第一个DataNode节点将Pipeline ack发送给Client。
- 一个Block已经写入到DataNode节点磁盘,Client调用fsync让NameNode持久化Block的位置信息数据,DataNode也会通知NameNode成功持久化Block(这里存在一个不一致时间)。因为每个Block默认大小是64M,文件大于64M,跳到3。
- 完成向文件写入数据,Client在文件输出流(FSDataOutputStream)对象上调用close方法,关闭流
- 调用DistributedFileSystem对象的complete方法,通知NameNode文件写入成功
static String[] contents = new String[] {
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"dddddddddddddddddddddddddddddddd",
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
};
public static void main(String[] args) {
String file = "hdfs://h1:8020/data/test/test.log";
Path path = new Path(file);
Configuration conf = new Configuration();
FileSystem fs = null;
FSDataOutputStream output = null;
try {
fs = path.getFileSystem(conf);
output = fs.create(path); // 创建文件
for(String line : contents) { // 写入数据
output.write(line.getBytes("UTF-8"));
output.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端写数据内部实现原理
打开一个DFSOutputStream流,Client会写数据到流内部的一个缓冲区中,然后数据被分解成多个Packet,每个Packet大小为64k字节,每个Packet又由一组chunk和这组chunk对应的checksum数据组成,默认chunk大小为512字节,每个checksum是对512字节数据计算的校验和数据。
创建Packet
当长度满足一个Chunk大小(512B)时,便会创建一个Packet对象,然后向该Packet对象中写Chunk Checksum校验和数据,以及实际数据块Chunk Data,每次满足一个Chunk大小时,都会向Packet中写上述数据内容,直到达到一个Packet对象大小(64K),就会将该Packet对象放入到dataQueue队列中,等待DataStreamer线程取出并发送到DataNode节点。
发送Packet
DataStreamer线程从dataQueue队列中取出Packet对象,放到ackQueue队列中,然后向DataNode节点发送这个Packet对象所对应的数据。
接收ack
发送一个Packet数据包以后,会有一个用来接收ack的ResponseProcessor线程,如果收到成功的ack,则表示一个Packet发送成功。如果成功,则ResponseProcessor线程会将ackQueue队列中对应的Packet删除。如果发生错误,则重新加入dataQueue。
DataNode写数据内部原理
DataNode接受到数据Packet,首先将数据写道下一个DataNode的PipeLine中,然后再写入本地磁盘的Block中,写完后回复上一个DataNode ack。整个Block写成功后,通知NameNode已经收到Block。实际流程如图所示:
持久化包括block的持久化和block checksum的持久化,
HDFS读文件
HDFS读文件分为两步:从NameNode读取文件Block列表,从DataNode读取Block。下面是一段读文件的代码
package org.shirdrn.hadoop.hdfs;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
public class HdfsFileReader {
public static void main(String[] args) {
String file = "hdfs://hadoop-cluster-m:8020/data/logs/basis_user_behavior/201405071237_10_10_1_73.log";
Path path = new Path(file);
Configuration conf = new Configuration();
FileSystem fs;
FSDataInputStream in;
BufferedReader reader = null;
try {
fs = FileSystem.get(conf);
in = fs.open(path); // 打开文件path,返回一个FSDataInputStream流对象
reader = new BufferedReader(new InputStreamReader(in));
String line = null;
while((line = reader.readLine()) != null) { // 读取文件行内容
System.out.println("Record: " + line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(reader != null) reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
读取文件流程:
- 客户端调用open打开文件,然后从NameNode获取Block信息(默认预先获取10个),文件长度等信息。
- 获取Block列表后,会对Block列表中的DataNode进行排序,排序规则为以下两点
- Client到Block所在的Datanode的距离
- 如果某个DataNode很久没有心跳,那么就放在列表的最后
- 开始对Block进行读取,通过偏移量获取Block,然后根据Block选择合适的DataNode,从DataNode读取数据。重复3,直到读完数据。
- 如果客户端没有缓存需要的Block,又向NameNode拉取Block。跳2
注:有个博客说一旦写入的数据超过一个块的数据,新的读取者就能看见第一个块。对于之后的块也是这样。总之,它始终是当前正在被写入的块,其他读取者是看不见它的。
一致性模型
根据上面的写入操作,我们知道在写完了一个Block后,客户端会调用fsync向NameNode提交Block完成信息。所以:
- 对于一个完整的Block而言,是强一致性的。
- 对于不足一个Block的数据而言,数据可能因为HDFS宕机而丢失。这种情况用户主动调用fsyc向NameNode持久化
容错
HDFS通过复制副本实现容错,NameNode来控制所有的block的复制决策。一般而言,3副本配置是同一机架不同机器放置2个,不同机架放置1个。
DataNode容错
通过心跳机制检测DataNode,如果一段时间没有收到DataNode的心跳信息,判定机器宕机,设置这台机器不可用。因此Namenode会检测是否有文件block的副本数目小于设置值,如果小于就自动开始复制新的副本并分发到其他Datanode节点。
如果在写入的时候,发现某台DataNode一直不回应,向NameNode重新申请DataNode。
检查文件的块的完整性
客户端读取文件后,会检查和文件关联checksum是否一致,如果不一致,从其它DataNode读取数据
NameNode容错
NameNode磁盘中会有元数据信息的持久化信息,可以根据磁盘数据恢复元数据信息。万一NameNode磁盘也损坏了,Secondary NameNode还有历史信息。
集群均衡
- 数据均衡服务(Rebalancing Server)首先要求 NameNode 生成 DataNode 数据分布分析报告,获取每个DataNode磁盘使用情况
- Rebalancing Server汇总需要移动的数据分布情况,计算具体数据块迁移路线图。数据块迁移路线图,确保网络内最短路径
- 开始数据块迁移任务,Proxy Source Data Node复制一块需要移动数据块
- 将复制的数据块复制到目标DataNode上
- 删除原始数据块
- 目标DataNode向Proxy Source Data Node确认该数据块迁移完成
- Proxy Source Data Node向Rebalancing Server确认本次数据块迁移完成。然后继续执行这个过程,直至集群达到数据均衡标准
第2步中,HDFS会把当前的DataNode节点,根据阈值的设定情况划分到Over、Above、Below、Under四个组中。在移动数据块的时候,Over组、Above组中的块向Below组、Under组移动。
发展与改进
NameNode的HA方案
需要有如下保证
- 共享第三方存储,NameNode强一致性
- 需要隔离措施保证不会出现脑裂
NameNode联盟
单点NameNode联盟成为HDFS的瓶颈,主要表现在:限制了文件的个数;都与NameNode交互性能瓶颈。提出NameNode联盟解决这个问题