1. HDFS介绍
HDFS(The Hadoop Distributed File System),是被设计成适合运行在通用硬件上的 Hadoop 的分布式文件系统。它与其他的分布式系统有非常显著的不同,首先 HDFS 具有高容错性,并且它可以被部署到廉价的硬件上。此外,HDFS 提供对应用程序数据的高吞吐量访问,适用于具有大型数据集的应用程序。
1.1 HDFS的设计目标
- 运行在大量廉价商用机器上:硬件错误是常态,提供容错机制
- 简单一致性模型:一次写入多次读取,支持追加,不允许修改,保证数据一致性
- 流式数据访问:批量读而非随机读,关注吞吐量而非时间
- 存储大规模数据集:HDFS 适合于大文件的存储,文档的大小应该是是 GB 到 TB 级别的。
1.2 HDFS的优点
- 高容错、高可用、高扩展
- 数据冗余,多Block多副本,副本丢失后自动恢复
- NameNode HA、安全模式
- 10K节点规模
- 海量数据存储
- 典型文件大小GB~TB,百万以上文件数量, PB以上数据规模
- 构建成本低、安全可靠
- 构建在廉价的商用服务器上
- 提供了容错和恢复机制
- 适合大规模离线批处理
- 流式数据访问
- 数据位置暴露给计算框架
1.3 HDFS的缺点
-
不适合低延迟数据访问
-
不适合大量小文件存储
-
元数据占用NameNode大量内存空间
- 每个文件、目录和Block的元数据都要占用150Byte
- 存储1亿个元素,大约需要20GB内存
- 如果一个文件为10KB,1亿个文件大小仅有1TB,却要消耗掉20GB内存
- 磁盘寻道时间超过读取时间
-
不支持并发写入
- 一个文件同时只能有一个写入者
-
不支持文件随机修改,仅支持追加写入
2. HDFS 架构
Active NameNode
- 管理命名空间
- 管理数据块
Block映射信息 - 管理元数据:文件的位置、所有者、权限、数据块等
- 管理Block副本策略:默认3个副本
- 处理客户端读写请求,为DataNode分配任务 Standby NameNode
- Hadoop 3.0允许配置多个Standby NameNode
- Active NameNode宕机后,快速升级为新的Active
- 同步元数据,即周期性下载edits编辑日志,生成fsimage镜像检查点文件 Secondary NameNode
- 辅助
NameNode,分担其工作量。 - 定期合并 NameNode 的 edit logs(对文件系统的改动序列) 到 fsimage(对整个文件系统的快照),并拷贝修改后的 fsimage 到 NameNode。
- 在紧急情况下,可辅助恢复
NameNode。 (并非NameNode的热备,当NameNode挂掉的时候,它并不能马上替换NameNode并提供服务)
DataNode
- Slave工作节点
- 存储Block和数据校验和
- 执行客户端发送的读写操作
- 通过心跳机制定期(默认3秒)向NameNode汇报运行状态和Block列表信息
- 集群启动时,DataNode向NameNode提供Block列表信息 Block数据块
- HDFS最小存储单元
- 文件写入HDFS会被切分成若干个Block
- Block大小固定,默认为128MB,可自定义
- 若一个Block的大小小于设定值,不会占用整个块空间
- 默认情况下每个Block有3个副本
文件分块的好处
- Block的拆分使得单个文件大小可以大于整个磁盘的容量,构成文件的Block可以分布在整个集群, 理论上,单个文件可以占据集群中所有机器的磁盘。
- Block的抽象也简化了存储系统,对于Block,无需关注其权限,所有者等内容(这些内容都在文件级别上进行控制)。
- Block作为容错和高可用机制中的副本单元,即以Block为单位进行复制。
Client
- 文件切分。文件上传
HDFS的时候,Client将文件切分成一个一个的Block,然后进行存储。 - 与
NameNode交互,获取文件的位置信息。 - 与
DataNode交互,读取或者写入数据。 - Client提供一些命令来管理
HDFS,比如启动或者关闭HDFS。 - Client可以通过一些命令来访问
HDFS。
2.1 数据复制
由于 Hadoop 被设计运行在廉价的机器上,这意味着硬件是不可靠的,为了保证容错性,HDFS 提供了数据复制机制。HDFS 将每一个文件存储为一系列块,每个块由多个副本来保证容错,块的大小和复制因子可以自行配置(默认复制因子是 3)。
2.2 数据复制的实现原理
Block存储
- Block是HDFS的最小存储单元
- 如何设置Block大小
- 目标:最小化寻址开销,降到1%以下
- 默认大小:128M
- 块太小:寻址时间占比过高
- 块太大:Map任务数太少,作业执行速度变慢
如果寻址时间约为10ms,而传输速率为100MB/s,为了使寻址时间仅占传输时间的1%, 我们要将块大小设置约为100MB。
默认的块大小128MB。块的大小:10ms * 100 * 100M/s = 100M
- Block多副本
- 以DataNode节点为备份对象
- 机架感知:将副本存储到不同的机架上,实现数据的高容错
- 副本均匀分布:提高访问带宽和读取性能,实现负载均衡
Block副本放置策略
- 副本1:放在Client所在节点 -对于远程Client,系统会随机选择节点
- 副本2:放在不同的机架节点上
- 副本3:放在与第二个副本同一机架的不同节点上
- 副本N:随机选择
大型的 HDFS 实例在通常分布在多个机架的多台服务器上,不同机架上的两台服务器之间通过交换机进行通讯。 在大多数情况下,同一机架中的服务器间的网络带宽大于不同机架中的服务器之间的带宽。
对于常见情况,当复制因子为 3 时,HDFS 的放置策略是:在写入程序位于 datanode 上时,就优先将写入文件的一个副本放置在该 datanode 上(采用机架感知副本放置策略),否则放在随机 datanode 上。 之后在另一个远程机架上的任意一个节点上放置另一个副本,并在该机架上的另一个节点上放置最后一个副本。此策略可以减少机架间的写入流量,从而提高写入性能。
2.3 副本的选择
为了最大限度地减少带宽消耗和读取延迟,HDFS 在执行读取请求时,优先读取距离读取器最近的副本。
如果在与读取器节点相同的机架上存在副本,则优先选择该副本。如果 HDFS 群集跨越多个数据中心,则优先选择本地数据中心上的副本。
2.4 架构的稳定性
心跳机制和重新复制
一个数据块在datanode上以文件形式存储在磁盘上,包括两个文件,一个是数据本身,一个是元数据包括数据块的长度,块数据的校验和,以及时间戳。
每个 DataNode 定期向 NameNode 发送心跳消息,如果超过指定时间没有收到心跳消息,则将 DataNode 标记为死亡。
NameNode 不会将任何新的 IO 请求转发给标记为死亡的 DataNode,也不会再使用这些 DataNode 上的数据。
由于数据不再可用,可能会导致某些块的复制因子小于其指定值,NameNode 会跟踪这些块,并在必要的时候进行重新复制。
数据的完整性
由于存储设备故障等原因,存储在 DataNode 上的数据块也会发生损坏。为了避免读取到已经损坏的数据而导致错误,HDFS 提供了数据完整性校验机制来保证数据的完整性,具体操作如下:
当客户端创建 HDFS 文件时,它会计算文件的每个块的校验和,并将校验和存储在同一HDFS 命名空间下的单独的隐藏文件中。当客户端检索文件内容时,它会验证从每个 DataNode 接收的数据是否与存储在关联校验和文件中的校验和匹配。如果匹配失败,则证明数据已经损坏,此时客户端会选择从其他 DataNode 获取该块的其他可用副本。
2.5 NameNode & Secondary NameNode 工作机制
NameNode是怎么存放元数据的呢?
- 如果NameNode只是把元数据放到内存中,那如果NameNode这台机器重启了,那元数据就没了。
- 如果NameNode将每次写入的数据都存储到硬盘中,那如果只针对磁盘查找和修改又会很慢
说到这里,想起了Kafka。Kafka也是将partition写到磁盘里边的,但人家是怎么写的?答案是顺序IO。
NameNode同样也是,修改内存中的元数据,然后把修改的信息追加到一个名为editlog的文件上。由于append是顺序IO,所以效率也不会低。现在我们增删改查都是走内存,只不过增删改的时候往磁盘文件editlog里边追加一条。这样我们即便重启了NameNode,还是可以通过editlog文件将元数据恢复。
现在有个问题,如果NameNode一直长期运行的话,那editlog文件会越来越大,因为所有的修改元数据信息都需要在这追加一条,重启的时候需要依赖editlog文件来恢复数据。当文件特别大时,启动时就会特别慢。
那HDFS是怎么做的呢?为了防止editlog过大,导致在重启的时候需要较长的时间恢复数据,所以NameNode会有一个内存快照,叫做fsimage。这样一来,重启的时候只需要加载内存快照fsimage+部分的editlog就可以了。
想法很美好,现实还需要解决一些事:我什么时候生成一个内存快照fsimage?我怎么知道加载哪一部分的editlog?于是HDFS用了一个新的角色SecondNameNode来处理这个问题:
第一阶段:namenode启动
- 第一次启动
namenode格式化后,创建fsimage和edits文件。如果不是第一次启动,直接加载编辑日志和镜像文件到内存。 - 客户端对元数据进行增删改的请求。
namenode记录操作日志,更新滚动日志。namenode在内存中对数据进行增删改查。
第二阶段:Secondary NameNode工作
Secondary NameNode询问namenode是否需要checkpoint。直接带回namenode是否检查结果。Secondary NameNode请求执行checkpoint。namenode滚动正在写的edits日志。- 将滚动前的编辑日志和镜像文件拷贝到
Secondary NameNode。 Secondary NameNode加载编辑日志edits和镜像文件fsimage到内存,并合并。- 生成新的镜像文件
fsimage.chkpoint。 - 拷贝
fsimage.chkpoint到namenode。 namenode将fsimage.chkpoint重新命名成fsimage。
2.6 Hadoop HA 高可用
现在问题还是来了,此时的架构NameNode是单机的。SecondNameNode的作用只是给NameNode合并editlog和fsimage文件,如果NameNode挂了,那client就请求不到了,而所有的请求都需要走NameNode,这导致整个HDFS集群都不可用了。
于是我们需要保证NameNode是高可用的。一般现在我们会通过Zookeeper来实现。架构图如下:
主NameNode和从NameNode需要保持元数据的信息一致(因为如果主NameNode挂了,那从NameNode需要顶上,这时从NameNode需要有主NameNode的信息)。
所以,引入了Shared Edits来实现主从NameNode之间的同步,Shared Edits也叫做 JournalNode。
实际上就是主NameNode如果有更新元数据的信息,它的editlog会写到JournalNode,然后从NameNode会在JournalNode读取到变化信息,然后同步。从NameNode也实现了上面所说的SecondNameNode功能(合并editlog和fsimage)。
总结
- NameNode需要处理client请求,它是存储元数据的地方
- NameNode的元数据操作都在内存中,会把增删改以
editlog持续化到硬盘中(因为是顺序io,所以不会太慢) - 由于
editlog可能存在过大的问题,导致重新启动NameNode过慢(因为要依赖editlog来恢复数据),引出了fsimage内存快照。需要跑一个定时任务来合并fsimage和editlog,引出了SecondNameNode - 又因为NameNode是单机的,可能存在单机故障的问题。所以我们可以通过Zookeeper来维护主从NameNode,通过JournalNode(Share Edits)来实现主从NameNode元数据的一致性。最终实现NameNode的高可用。
3. HDFS存储原理
3.1 HDFS写数据原理
- 客户端通过Distributed FileSystem模块向namenode请求上传文件,namenode检查目标文件是否已存在,父目录是否存在。namenode返回是否可以上传。
- 客户端请求第一个block上传到哪几个datanode服务器上。
- namenode返回3个
datanode节点,分别为dn1、dn2、dn3。 - 客户端通过
FSDataOutputStream模块请求dn1上传数据,dn1收到请求会继续调用dn2,然后dn2调用dn3,将这个通信管道建立完成。 dn1、dn2、dn3逐级应答客户端。- 客户端开始往
dn1上传第一个block(先从磁盘读取数据放到一个本地内存缓存),以packet为单位,dn1收到一个packet就会传给dn2,dn2传给dn3;dn1每传一个packet会放入一个应答队列等待应答。 - 当一个
block传输完成之后,客户端再次请求namenode上传第二个block的服务器。(重复执行3-7步)
读写过程中保证数据完整性
HDFS 通过校验和保证数据完整性。因为每个 chunk 中都有一个校验位,一个个 chunk 构成 packet ,一个个 packet 最终形成 block ,故可在 block 上求校验和。
HDFS 的 client 端实现了对 HDFS 文件内容的校验和检查。当客户端创建一个新的 HDFS 文件时候,分块后会计算这个文件每个数据块的校验和,此校验和会以一个隐藏文件形式保存在同一个 HDFS 命名空间下。
当 client 端从HDFS 中读取文件内容后,它会检查分块时候计算出的校验和(隐藏文件里)读取到的文件块中校验和是否匹配,如果不匹配,客户端可以选择从其他 DataNode 获取该数据块的副本。
3.2 HDFS读数据原理
- 客户端通过
Distributed FileSystem向namenode请求下载文件,namenode通过查询元数据,找到文件块所在的datanode地址。 - 挑选一台
datanode(就近原则,然后随机)服务器,请求读取数据。 datanode开始传输数据给客户端(从磁盘里面读取数据输入流,以packet为单位来做校验)。- 客户端以
packet为单位接收,先在本地缓存,然后写入目标文件。
4. HDFS故障类型
5.常用命令
文件操作
-
列出文件
hdfs dfs -ls <path> -
创建目录
hdfs dfs -mkdir <path> -
上传文件
hdfs dfs -put <localsrc> <dst> -
输出文件内容
hdfs dfs -cat <src> -
文件复制到本地
hdfs dfs -get <src> <localdst> -
删除文件和目录
hdfs dfs -rm <src> hdfs dfs -rmdir <dir>
管理
-
查看统计信息
hdfs dfsadmin -report -
进入和退出安全模式(该模式不允许文件系统有任何修改)
hdfs dfsadmin -safemode enter hdfs dfsadmin -safemode leave
6. 编程实例
IDEA 新建 Maven 项目
pom.xml 中添加依赖
<dependencies>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>2.9.2</version> //根据 Hadoop 版本进行选择
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
创建 Sample 类编写相应的读写函数
-
Sample 类
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import java.io.*; /** * @author ikroal */ public class Sample { //默认的 HDFS 地址 private static final String DEFAULT_FS = "hdfs://localhost:9000"; private static final String PATH = DEFAULT_FS + "/tmp/demo.txt"; private static final String DEFAULT_FILE = "demo.txt"; public static void main(String[] args) { Configuration conf = new Configuration(); FileSystem fs = null; conf.set("fs.defaultFS", DEFAULT_FS); //配置 HDFS 地址 try { fs = FileSystem.get(conf); write(fs, DEFAULT_FILE, PATH); read(fs, PATH); } catch (IOException e) { e.printStackTrace(); } finally { try { if (fs != null) { fs.close(); } } catch (IOException e) { e.printStackTrace(); } } } } -
write 函数
/** * 进行文件写入 * @param inputPath 待写入文件路径 * @param outPath HDFS 的写入路径 */ public static void write(FileSystem fileSystem, String inputPath, String outPath) { FSDataOutputStream outputStream = null; FileInputStream inputStream = null; try { outputStream = fileSystem.create(new Path(outPath)); //获得 HDFS 的写入流 inputStream = new FileInputStream(inputPath); //读取本地文件 int data; while ((data = inputStream.read()) != -1) { //写入操作 outputStream.write(data); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (outputStream != null) { outputStream.close(); } if (inputStream != null) { inputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } } -
read 函数
/** * 进行文件读取 * @param path HDFS 上待读取文件路径 */ public static void read(FileSystem fileSystem, String path) { FSDataInputStream inputStream = null; BufferedReader reader = null; try { inputStream = fileSystem.open(new Path(path)); //获取 HDFS 读取流 reader = new BufferedReader(new InputStreamReader(inputStream)); String content; while ((content = reader.readLine()) != null) { //读取并输出到控制台 System.out.println(content); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (inputStream != null) { inputStream.close(); } if (reader != null) { reader.close(); } } catch (IOException e) { e.printStackTrace(); } } }
在工程文件夹的根目录下创建计划上传的文件(这里是 demo.txt),填入 Hello World!
启动 Hadoop 然后运行程序查看结果,通过 http://localhost:50070/explorer.html#/ 可以查看写入结果
控制台则会输出上传文件的内容