HDFS
优点
高容错性
- 数据自动保存多个副本,通过增加多个副本的形式提高容错性
- 某个副本丢失以后,可以自动恢复
适合处理大数据
数据规模:能够处理数据规模达到GB、TB甚至PB级别的数据
文件规模:能够处理百万规模以上的文件数量,数量相当之大
可构建在廉价机器上
通过多副本机制,提高可靠性
缺点
不适合低延时数据访问
无法毫秒级存储
无法高效地对大量小文件进行存储
存储大量小文件会占用NameNode大量的内存来存储文件目录和块信息。这样是不可取的,因为NameNode的内存总是有限的。
小文件存储的寻址时间会超过读取时间,违反了HDFS的设计目标
不支持并发写入、文件随机修改
一个文件只能有一个写,不允许多个线程同时写。
仅支持数据append(追加),不支持文件的随机修改。
组成架构
NameNode(nn) Master
-
管理HDFS的名称空间。
元数据
-
配置副本策略;
每个文件有多少副本
-
管理数据块(Block)映射信息。
副本存储位置
-
处理客户端读写请求。
DataNode Slave
NameNode下达命令,DataNode执行实际的操作。
- 存储实际的数据块
- 执行数据块的读/写操作
Client
客户端
- 文件切分:文件上传HDFS的时候,Client将文件切分成一个一个的Block,如何进行上传。(按NameNode的文件块来分)
- 与NameNode交互,获取文件的位置信息
- 与DataNode交互,读取或者写入数据
- Client提供一些命令来管理HDFS,比如NameNode格式化
- Client可以通过一些命令来访问HDFS,比如对HDFS增删改查
Secondary NameNode
并非NameNode的热备,当NameNode挂掉的时候,不能马上替换NameNode提供服务
- 辅助NameNode,分担其工作量,比如定期合并Fsimage和Edits,并推送给NameNode
- 在紧急情况下,可辅助恢复NameNode
Block
HDFS中的文件在物理上是分块存储(Block),块的大小可以通过配置参数(dfs.blocksize)来规定,默认大小在2.x、3.x是128M,在1.x是64M
<property>
<name>dfs.blocksize</name>
<value>134217728</value>
<description>
The default block size for new files, in bytes.
You can use the following suffix (case insensitive):
k(kilo), m(mega), g(giga), t(tera), p(peta), e(exa) to specify the size (such as 128k, 512m, 1g, etc.),
Or provide complete size in bytes (such as 134217728 for 128 MB).
</description>
</property>
如果寻址时间约为10ms,即查找到目标block的时间为10ms
寻址时间为传输时间的1%时,则为最佳状态。
而目前磁盘的传输速率普遍为100MB/s
如果为200~300MB/s,则数据块大小取256M
HDFS块大小设置主要取决于磁盘传输速率
太小,增加寻址时间吗,程序一直在找块的开始位置
太大,从磁盘传输数据的时间会明显大于定位这个块开始位置所需的时间,导致程序在处理这块数据时会非常慢(计算的单元是块)。
Shell命令
datanode命令
开启本地的datanode
hdfs --daemon start datanode
文件相关命令
以下两个等效
-
hadoop fs
-
hdfs dfs
基本
-help rm
查看rm帮助
-mkdir /xxx
创建xxx目录
-ls
类似于linux的ls
-cat
类似于linux的cat
-chgrp
-chmod
-chown
同linux
-cp
同linux
-mv
同linux
-tail
同linux
-rm
删除文件或文件夹
-rm -r
递归删除目录及目录里的内容
-du
统计文件大小
-du -h 人性化
-du -s 显示总计
-setrep num targetfile:设置HDFS中文件的副本数量
上传
-moveFromLocal local targetdir
将本地文件移动到hdfs指定目录
-copyFromLocal local targetdir
将本地文件复制到hdfs指定目录
-put local targetdir
等同于-copyFromLocal
-appendToFile localfile targetfile
将本地文件追加到hdfs指定文件末尾
下载
-copyToLocal target localdir
将目标文件拷贝到本地
-get
等同于-copyToLocal
安装hadoop的windows依赖
https://gitee.com/eeasy/hadoop_windows_dependency
https://github.com/steveloughran/winutils
创建Maven项目
dependency
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.32</version>
</dependency>
log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
HdfsClient.java (API)
package com.hayaku.hdfs;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
public class HdfsClient {
private FileSystem fs;
@BeforeEach
public void init() throws IOException, InterruptedException, URISyntaxException {
// 内部通讯端口8020
URI uri = new URI("hdfs://hadoop102:8020");
Configuration entries = new Configuration();
entries.set("dfs.replication", "7");
String user = "root";
fs = FileSystem.get(uri, entries, user);
}
@AfterEach
public void close() throws IOException {
// 关闭资源
fs.close();
}
@Test
// 新建目录
public void testMkdir() throws URISyntaxException, IOException, InterruptedException {
// 创建文件夹
Path path = new Path("/hello/hdfs1");
fs.mkdirs(path);
}
@Test
// 上传
public void testPut() throws IOException {
Path src = new Path("C:\\Users\\86191\\OneDrive\\桌面\\doctest测试框架\\doctest.h");
Path dest = new Path("hdfs://hadoop102/doctest");
// 是否删除原数据、是否允许覆盖、原数据路径、目的地路径
fs.copyFromLocalFile(false, true, src, dest);
}
@Test
// 下载
public void testDownload() throws IOException {
Path src = new Path("/doctest/doctest.h");
Path dest = new Path("C:\\Users\\86191\\OneDrive\\桌面");
// 是否删除原文件,源路径,目的地路径,使用原试本地文件系统(false则会下载crc文件)
fs.copyToLocalFile(false, src, dest, false);
}
@Test
// 删除
public void testDelete() throws IOException {
Path path1 = new Path("/doctest/doctest.h");
Path path2 = new Path("/doctest");
Path path3 = new Path("/a/b");
// 如果要删除一个非空目录,则要指定递归删除
fs.delete(path1,false);
fs.delete(path2, false);
fs.mkdirs(path3);
fs.delete(new Path("/a"), true);
}
@Test
// 移动
public void testMove() throws IOException {
Path dst = new Path("/sunquan.txt");
Path src = new Path("/sanguo/wuguo.txt");
fs.rename(src, dst);
}
@Test
// 获取文件信息
public void testDetail() throws IOException {
RemoteIterator<LocatedFileStatus> listFiles = fs.listFiles(new Path("/"), true);
while (listFiles.hasNext()) {
LocatedFileStatus fileStatus = listFiles.next();
System.out.println("======" + fileStatus.getPath() + "=====");
System.out.println(fileStatus.getPermission());
System.out.println(fileStatus.getOwner());
System.out.println(fileStatus.getGroup());
System.out.println(fileStatus.getLen());
System.out.println(fileStatus.getModificationTime());
System.out.println(fileStatus.getReplication());
System.out.println(fileStatus.getBlockSize());
BlockLocation[] blockLocations = fileStatus.getBlockLocations();
System.out.println(Arrays.toString(blockLocations));
}
}
@Test
// 判断是文件还是目录
public void testIsfile() throws IOException {
FileStatus[] fileStatuses = fs.listStatus(new Path("/"));
for (FileStatus status : fileStatuses) {
if (status.isFile()) {
System.out.println("File: " + status.getPath().getName());
} else {
System.out.println("Directory: " + status.getPath().getName());
}
}
}
}
配置的优先级
代码 > resource中的hdfs-site.xml > hdfs-site.xml > hdfs-default.xml
HDFS读写数据流程
写详细步骤
- 客户端向NameNode发出写文件请求。
- 检查是否已存在文件、检查权限。若通过检查,直接先将操作写入EditLog,并返回输出流对象。
(注:WAL,write ahead log,先写Log,再写内存,因为EditLog记录的是最新的HDFS客户端执行所有的写操作。如果后续真实写操作失败了,由于在真实写操作之前,操作就被写入EditLog中了,故EditLog中仍会有记录,我们不用担心后续client读不到相应的数据块,因为在第5步中DataNode收到块后会有一返回确认信息,若没写成功,发送端没收到确认信息,会一直重试,直到成功) - client端按block(128MB)的块切分文件。
- client将NameNode返回的分配的可写的DataNode列表和Data数据一同发送给最近的第一个DataNode节点,此后client端和NameNode分配的多个DataNode构成pipeline管道,client端向输出流对象中写数据。client每向第一个DataNode写入一个packet,这个packet便会直接在pipeline里传给第二个、第三个…DataNode。
(注:并不是写好一个块或一整个文件后才向后分发) - 每个DataNode写完一个块后,会返回确认信息。
(注:并不是每写完一个packet后就返回确认信息,个人觉得因为packet中的每个chunk都携带校验信息,没必要每写一个就汇报一下,这样效率太慢。正确的做法是写完一个block块后,对校验信息进行汇总分析,就能得出是否有块写错的情况发生) - 写完数据,关闭输出流。
- 发送完成信号给NameNode。
(注:发送完成信号的时机取决于集群是强一致性还是最终一致性,强一致性则需要所有DataNode写完后才向NameNode汇报。最终一致性则其中任意一个DataNode写完后就能单独向NameNode汇报,HDFS一般情况下都是强调强一致性)
副本节点选择
距离指的是到最近公共祖先的距离之和
读详细步骤
- client访问NameNode,查询元数据信息,获得这个文件的数据块位置列表,返回输入流对象。
- 就近挑选一台datanode服务器(同时考虑负载均衡),请求建立输入流 。
- DataNode向输入流中中写数据,以packet为单位来校验。
- 关闭输入流
读block是串行读,读完block1再读block2,追加
NameNode和2nn工作流程
企业中基本不用2nn,而是用2个nn搭建高可用
hadoop-3.3.1/data/dfs/name/current
fsimage
HDFS文件系统元数据的一个永久性的检查点,其中包含HDFS文件系统的所有目录和文件inode的序列化信息。
使用oiv查看fsimage文件:
hdfs oiv -p 文件类型 -i 镜像文件 -o 转换后文件输出路径
hdfs oiv -p XML -i fsimage_0000000000000000392 -o /opt/software/image.xml
Edits
存放HDFS文件系统的所有更新操作的路径,文件系统客户端执行的所有写操作首先会被记录到Edits文件中
使用oev查看edits文件:
hdfs oev -p 文件类型 -i 编辑日志 -o 转换后文件输出路径
hdfs oev -p XML -i edits_0000000000000000396-0000000000000000402 -o /opt/software/edits2.xml
只有在NameNode重启时,edits才会合并到fsimage文件中,从而得到一个文件系统的最新快照。但是在生产环境集群中的NameNode是很少重启的,这意味者当NameNode运行来很长时间后,edits文件会变的很大。在这种情况下就会出现下面这些问题:
- edits文件会变的很大,如何去管理这个文件?
- NameNode的重启会花费很长的时间,因为有很多改动要合并到fsimage文件上。
- 如果NameNode宕掉了,那我们就丢失了很多改动,因为此时的fsimage文件时间戳比较旧。
因此为了克服这个问题,我们需要一个易于管理的机制来帮助我们减小edits文件的大小和得到一个最新的fsimage文件,这样也会减小在NameNode上的压力。而Secondary NameNode就是为了帮助解决上述问题提出的,它的职责是合并NameNode的edits到fsimage文件中。如图所示:
seen_txid
seen_txid文件保存的是一个数字,就是最后一个edits_的数字
2nn合并edits到fsimage流程
2nn默认每小时检查一次
如果edits数超过默认100 0000也会进行合并,默认每60秒查询一次uncheckpointed transactions
DataNode
提供真实文件数据的存储服务。
DataNode与NameNode
超出TimeOut仍没有收到心跳则认为不可用
TimeOut = 2 * dfs.namenode.heartbeat.recheck-interval + 10 * dfs.heartbeat.interval
上报周期
默认每6小时上报
自查周期
默认每6小时扫描自己的块
crc校验
循环冗余校验(Cyclic Redundancy Check)