HDFS概述
HDFS产出背景及定义
随着数据量越来越大,在一个操作系统存不下所有的数据,那么就分配到更多的操作系统管理的磁盘中,但是不方便管理和维护,迫切需要一种系统来管理多台机器上的文件,这就是分布式文件管理系统。HDFS只是分布式文件管理系统中的一种。
HDFS(Hadoop Distributed File System),它是一个文件系统,用于存储文件,通过目录树来定位文件;它是分布式的,由很多服务器联合起来实现其功能,集群中的服务器有各自的角色。
HDFS的使用场景:适合一次写入,多次读出的场景。一个文件经过创建、写入和关闭之后就不需要改变。
HDFS优缺点
优点:
- 高容错性:数据会保存多个副本,当某个副本损坏或丢失,可以自动恢复
- 适合处理大数据:数据规模大,文件规模大
- 可以构建在廉价机器上,通过多副本机制,提高可靠性
缺点:
- 不适合低延迟数据访问:毫秒级的访问是做不到的
- 无法高效对大量小文件进行存储:存储大量小文件会占用NameNode存储空间,大量小文件会增加寻址时间,违反了HDFS设计目标
- 不支持并发写入、文件随机修改:一个文件不允许多线程同时写,仅支持数据追加,不支持文件随机修改
HDFS组成架构
NameNode(nn):作为HDFS的管理者,管理HDFS名称空间、配置副本策略、管理数据库映射信息、处理客户端的读写请求。
DataNode:当NameNode下达指令,DataNode就去执行实际操作,存储实际的数据块,执行数据块的读写操作。
Client:在大文件上传的时候,Client会对文件按照Block切分,再进行上传、与NameNode交互,获取文件位置信息、与DataNode交互,读写数据、Client提供一些命令来管理HDFS,比如NameNode的格式化、Client可以通过命令访问HDFS,实现对HDFS的增删查改。
SecondaryNameNode(2nn):只是起到一个辅助作用,不能作为NameNode的热备,可以帮助NameNode分担工作,比如定期合并Fsimage和Edits,并推送给NameNode、紧急情况下,辅助恢复NameNode。
HDFS文件块大小
在HDFS中,文件在物理上是分块存储的(Block),块的大小可以通过配置参数(dfs.blocksize)来确定,在Hadoop 2.x和Hadoop 3.x中,是128M。通常来说,寻址时间为传输时间的1%是最佳状态。块大小约为磁盘传输时间乘以磁盘传输速率,取接近的二次幂为结果,机械硬盘可以是128M,固态硬盘可以是256M。
如果块大小设置的过小,会增加寻址时间,如果块大小设置的过大,会增加传输时间和数据处理时间。
HDFS的块大小主要取决于磁盘传输速率。
HDFS的Shell操作
基本语法
hadoop fs 具体命令
hdfs dfs 具体命令
这两个命令是完全等效的。
命令大全
[root@hadoop102 hadoop-3.1.3]# hadoop fs
Usage: hadoop fs [generic options]
[-appendToFile <localsrc> ... <dst>]
[-cat [-ignoreCrc] <src> ...]
[-checksum <src> ...]
[-chgrp [-R] GROUP PATH...]
[-chmod [-R] <MODE[,MODE]... | OCTALMODE> PATH...]
[-chown [-R] [OWNER][:[GROUP]] PATH...]
[-copyFromLocal [-f] [-p] [-l] [-d] [-t <thread count>] <localsrc> ... <dst>]
[-copyToLocal [-f] [-p] [-ignoreCrc] [-crc] <src> ... <localdst>]
[-count [-q] [-h] [-v] [-t [<storage type>]] [-u] [-x] [-e] <path> ...]
[-cp [-f] [-p | -p[topax]] [-d] <src> ... <dst>]
[-createSnapshot <snapshotDir> [<snapshotName>]]
[-deleteSnapshot <snapshotDir> <snapshotName>]
[-df [-h] [<path> ...]]
[-du [-s] [-h] [-v] [-x] <path> ...]
[-expunge]
[-find <path> ... <expression> ...]
[-get [-f] [-p] [-ignoreCrc] [-crc] <src> ... <localdst>]
[-getfacl [-R] <path>]
[-getfattr [-R] {-n name | -d} [-e en] <path>]
[-getmerge [-nl] [-skip-empty-file] <src> <localdst>]
[-head <file>]
[-help [cmd ...]]
[-ls [-C] [-d] [-h] [-q] [-R] [-t] [-S] [-r] [-u] [-e] [<path> ...]]
[-mkdir [-p] <path> ...]
[-moveFromLocal <localsrc> ... <dst>]
[-moveToLocal <src> <localdst>]
[-mv <src> ... <dst>]
[-put [-f] [-p] [-l] [-d] <localsrc> ... <dst>]
[-renameSnapshot <snapshotDir> <oldName> <newName>]
[-rm [-f] [-r|-R] [-skipTrash] [-safely] <src> ...]
[-rmdir [--ignore-fail-on-non-empty] <dir> ...]
[-setfacl [-R] [{-b|-k} {-m|-x <acl_spec>} <path>]|[--set <acl_spec> <path>]]
[-setfattr {-n name [-v value] | -x name} <path>]
[-setrep [-R] [-w] <rep> <path> ...]
[-stat [format] <path> ...]
[-tail [-f] [-s <sleep interval>] <file>]
[-test -[defsz] <path>]
[-text [-ignoreCrc] <src> ...]
[-touch [-a] [-m] [-t TIMESTAMP ] [-c] <path> ...]
[-touchz <path> ...]
[-truncate [-w] <length> <path> ...]
[-usage [cmd ...]]
Generic options supported are:
-conf <configuration file> specify an application configuration file
-D <property=value> define a value for a given property
-fs <file:///|hdfs://namenode:port> specify default filesystem URL to use, overrides 'fs.defaultFS' property from configurations.
-jt <local|resourcemanager:port> specify a ResourceManager
-files <file1,...> specify a comma-separated list of files to be copied to the map reduce cluster
-libjars <jar1,...> specify a comma-separated list of jar files to be included in the classpath
-archives <archive1,...> specify a comma-separated list of archives to be unarchived on the compute machines
The general command line syntax is:
command [genericOptions] [commandOptions]
这么来看,其实,好多命令和Linux下是差不多的,熟悉Linux下的常用命令,这里也很好上手,就是在前面加上一个“hadoop fs”。
常用命令实操
准备工作
启动Hadoop集群。
[root@hadoop102 hadoop-3.1.3]# sbin/start-dfs.sh
[root@hadoop102 hadoop-3.1.3]# sbin/start-yarn.sh
查看命令帮助。
[root@hadoop102 hadoop-3.1.3]# hadoop fs -help rm
-rm [-f] [-r|-R] [-skipTrash] [-safely] <src> ... :
Delete all files that match the specified file pattern. Equivalent to the Unix
command "rm <src>"
-f If the file does not exist, do not display a diagnostic message or
modify the exit status to reflect an error.
-[rR] Recursively deletes directories.
-skipTrash option bypasses trash, if enabled, and immediately deletes <src>.
-safely option requires safety confirmation, if enabled, requires
confirmation before deleting large directory with more than
<hadoop.shell.delete.limit.num.files> files. Delay is expected when
walking over large directory recursively to count the number of
files to be deleted before the confirmation.
创建一个测试目录,用于后面的测试。
[root@hadoop102 hadoop-3.1.3]# hadoop fs -mkdir /test
在http://hadoop102:9870/的Utilities的Browse the file system里可以看到。
上传
# 将本地的demo.txt剪切到HDFS的/test目录下
[root@hadoop102 hadoop-3.1.3]# hadoop fs -moveFromLocal demo.txt /test
# 将本地的demo.sh复制粘贴到HDFS的/test目录下,put和copyFromLocal一样,生产环境,put用的更多
[root@hadoop102 hadoop-3.1.3]# hadoop fs -copyFromLocal demo.sh /test
# 把demo.sh的内容追加到HDFS目录下/test/demo.txt文件后
[root@hadoop102 hadoop-3.1.3]# hadoop fs -appendToFile demo.sh /test/demo.txt
下载
# 将HDFS上的/test/demo.txt下载到当前路径,get也是同样的效果,生产环境get用的多一些
[root@hadoop102 hadoop-3.1.3]# hadoop fs -copyToLocal /test/demo.txt
HDFS直接操作
# 显示目录信息
[root@hadoop102 hadoop-3.1.3]# hadoop fs -ls /test
# 显示文件内容
[root@hadoop102 hadoop-3.1.3]# hadoop fs -cat /test/demo.sh
# -chrgp、-chmod、chown:和Linux下效果一样
[root@hadoop102 hadoop-3.1.3]# hadoop fs -chmod 777 /test/demo.sh
# 创建目录
[root@hadoop102 hadoop-3.1.3]# hadoop fs -mkdir /hello
# 从HDFS的一个路径拷贝到HDFS的另一个路径
[root@hadoop102 hadoop-3.1.3]# hadoop fs -cp /hello /test
# 在HDFS目录中移动文件
[root@hadoop102 hadoop-3.1.3]# hadoop fs -mv /hello /test
# 显示一个文件末尾的1kb数据
[root@hadoop102 hadoop-3.1.3]# hadoop fs -tail /test/demo.txt
# 统计文件夹大小信息
[root@hadoop102 hadoop-3.1.3]# hadoop fs -du /test
# 设置HDFS中副本数量,当然也可以在http://hadoop102:9870/里直接修改
# 当设置的副本数超过集群里的机器数时,实际不会有这么多副本,副本最多等于机器数
# 如果之后集群增加结点了,那么副本会自动复制到新结点上
[root@hadoop102 hadoop-3.1.3]# hadoop fs -setrep 5 /test/demo.sh
# 删除文件或文件夹
[root@hadoop102 hadoop-3.1.3]# hadoop fs -rm /test/demo.sh
# 递归删除
[root@hadoop102 hadoop-3.1.3]# hadoop fs -rm -r /test
HDFS的API操作
首先下载对应版本的winutils,不同版本的去github找下就可以,我这里用的Hadoop-3.1.3,从这里下载的。
解压后放到非中文目录下,设置环境变量,并加入PATH中。
双击运行winutils.exe,如果一闪而过,说明正常,如果出现错误信息,根据错误信息解决问题。
新建一个Maven项目HDFSClientDemo。
pom.xml需要用到的jar包坐标。
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</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类。
package com.demo.hdfs;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
public class HDFSClient {
private FileSystem fileSystem;
@Before
public void before() throws URISyntaxException, IOException, InterruptedException {
URI uri = new URI("hdfs://hadoop102:8020");
Configuration configuration = new Configuration();
String user = "root";
fileSystem = FileSystem.get(uri, configuration, user);
}
@After
public void after() throws IOException {
fileSystem.close();
}
@Test
public void testMkdirs() throws IOException {
fileSystem.mkdirs(new Path("/test/demo"));
}
@Test
public void testCopyFromLocalFile() throws IOException {
boolean delSrc = false;// 是否将本地文件删除
boolean overwrite = false;// 同名文件上传后是否允许覆盖
Path src = new Path("H:\\IDEAProject\\demo\\src\\main\\resources\\log4j.properties");// 本地路径
Path dest = new Path("/test");// HDFS的路径
fileSystem.copyFromLocalFile(delSrc, overwrite, src, dest);
}
@Test
public void testCopyToLocalFile() throws IOException {
boolean delSrc = false;// 是否删除HDFS的文件
Path src = new Path("/test/log4j.properties");// HDFS的路径
Path dest = new Path("H:\\");// 本地路径
boolean userRawLocalFileSystem = true;// 是否进行CRC校验
fileSystem.copyToLocalFile(delSrc, src, dest, false);
}
@Test
public void testRename() throws IOException {
Path path = new Path("/test/log4j.properties");
Path path1 = new Path("/test/demo.properties");
Path path2 = new Path("/test/demo/demo.properties");
fileSystem.rename(path, path1);// 重命名
fileSystem.rename(path1, path2);// 移动位置
}
@Test
public void testDelete() throws IOException {
Path path = new Path("/test/demo");// 要删除的路径
boolean recursive = true;// 是否递归
fileSystem.delete(path, recursive);
}
@Test
public void testListFiles() throws IOException {
Path path = new Path("/");// 要查看的HDFS目录
boolean recursive = true;// 是否递归
RemoteIterator<LocatedFileStatus> locatedFileStatusRemoteIterator = fileSystem.listFiles(path, recursive);
while (locatedFileStatusRemoteIterator.hasNext()) {
LocatedFileStatus locatedFileStatus = locatedFileStatusRemoteIterator.next();
System.out.println("========" + locatedFileStatus.getPath() + "=========");
System.out.println(locatedFileStatus.getPermission());
System.out.println(locatedFileStatus.getOwner());
System.out.println(locatedFileStatus.getGroup());
System.out.println(locatedFileStatus.getLen());
System.out.println(locatedFileStatus.getModificationTime());
System.out.println(locatedFileStatus.getReplication());
System.out.println(locatedFileStatus.getBlockSize());
System.out.println(locatedFileStatus.getPath().getName());
// 获取块信息
BlockLocation[] blockLocations = locatedFileStatus.getBlockLocations();
System.out.println(Arrays.toString(blockLocations));
}
}
@Test
public void testListStatus() throws IOException {
Path path = new Path("/");
FileStatus[] fileStatuses = fileSystem.listStatus(path);
for (FileStatus fileStatus : fileStatuses) {
if (fileStatus.isFile()) {
System.out.println("f:" + fileStatus.getPath().getName());
} else {
System.out.println("d:" + fileStatus.getPath().getName());
}
}
}
}
代码里的Configuration对象,我们并没有给它set任何属性,如果有需要,可以通过configuration.set(String name, String value);
方法设置。
这里就涉及到一个优先级问题,对于同一个属性,如果有多个地方设置了不同的值,优先级按照如下顺序:
代码里configuration的设置>classpath下用户自定义配置文件(xxx-site.xml)>服务器自定义配置文件(xxx-default.xml)。
HDFS的读写流程
HDFS写数据流程
剖析文件写入
- 客户端通过Distributed FileSystem向NameNode发送上传文件请求,NameNode检测目标文件,父目录等信息
- NameNode返回是否可以上传
- 客户端请求上传第一个Block
- NameNode告知客户端具体存储数据的3个DataNode结点,分别为dn1,dn2,dn3
- 客户端通过FSDataOutputStream,向dn1发送数据,dn1收到请求后,继续访问dn2,dn2继续访问dn3,建立通信管道
- dn1,dn2,dn3逐级应答客户端
- 客户端开始向dn1发送第一个Block,一个Block又分为多个Packet,dn1收到一个Packet就会传给dn2,dn2传给dn3,同时dn1每传递一个Packet就会放入一个应答队列等待应答
- 当一个Block传输完毕后,客户端再次请求NameNode上传第二个Block
网络拓扑-结点距离计算
在网络拓扑中,结点距离的定义是:两个结点到达最近公共祖先结点的距离之和。
机架感知
官方说明
在选取副本的时候,第一个副本位于Client所在结点,如果客户端不在集群里,那么随机选择一个,第二个副本在另一个机架上随机选择一个结点,第三个副本和第二个副本在同一台机架上,随机选择一个结点。
这样的设计可以兼顾可靠性和传输速度。
HDFS读数据流程
- 客户端通过DistributedFileSystem向NameNode发送一个下载请求,NameNode查询元数据,找到文件对应的DataNode
- 按照就近原则,选择一台DataNode,读取数据
- DataNode传输数据给客户端,以Packet为单位校验
- 客户端以Packet为单位接收,再写入目标文件
NameNode和SecondaryNameNode
NN和2NN工作机制
NameNode的元数据是分两部分的,一部分是内存,一部分是硬盘,内存和硬盘的优劣是互补的。所以,内存和磁盘中都是要存储的,磁盘中的这部分,我们称为FsImage。对于访问数据,通过内存获取,速度非常快,对于修改数据,修改内存中的数据,并把操作记录下来,保存到一个叫Edits的文件中,避免内存断电数据丢失,这个Edits文件只进行append操作,所以效率非常高。
不过Edits也是有限的,长期存在里面会导致文件变大,效率降低,因此需要定期对FsImage和Edits进行合并,这个操作由2NN来完成,它专门处理FsImage和Edits的合并工作。
- 如果第一次启动NameNode格式化后,会创建FsImage和Edits文件。否则直接加载FsImags和Edits到内存。
- 客户端对数据进行增删查改操作。
- NameNode记录操作日志,更新滚动日志。
- NameNode在内存中对数据进行增删查改。
- 经过一段时间后,Secondary NameNode会询问NameNode是否需要CheckPoint。
- 如果需要CheckPoint,Secondary NameNode执行CheckPoint操作。
- NameNode滚动正在写的Edits日志,就是产生一个新的日志文件。
- 将滚动前的Edits和FsImage交给Secondary NameNode。
- Secondary NameNode加载FsImage和Edits到内存,做合并操作。
- 将会生成的新的镜像文件FsImage.chkpoint。
- 拷贝FsImage.chkpoint到NameNode。
- NameNode将FsImage.chkpoint重命名为FsImage。
FsImage和Edits解析
NameNode格式化后,会在/opt/module/hadoop-3.1.3/data/tmp/dfs/name/current目录产生如下文件:
fsimage_0000000000000000000
fsimage_0000000000000000000.md5
seen_txid
VERSION
FsImage:HDFS元数据的一个永久检查点,包含HDFS所有目录和文件inode序列化信息。
Edits:存放HDFS所有更新操作的路径,客户端执行的所有写操作首先被记录到Edits中。
seen_txid:保存当前的进度,和Edits_后的数字保持一致。
每次NameNode启动的时候,都会将FsImage读取到内存,并根据Edits的操作日志,更新内存里的信息。可以认为,在NameNode启动的时候,会做一个FsImage和Edits的合并操作。
# 查看FsImage
# hdfs oiv -p 文件类型 -i 镜像文件 -o 转换后文件输出路径
[root@hadoop102 hadoop-3.1.3]# hdfs oiv -p XML -i fsimage_0000000000000000025 -o /opt/module/hadoop-3.1.3/fsimage.xml
# 查看Edits
# hdfs oev -p 文件类型 -i 编辑日志 -o 转换后文件输出路径
[root@hadoop102 hadoop-3.1.3]# hdfs oev -p XML -i edits_0000000000000000012-0000000000000000013 -o /opt/module/hadoop3.1.3/edits.xml
CheckPoint时间设置
通常情况下,Secondary NameNode每隔一小时执行一次,在hdfs-default.xml里由dfs.namenode.checkpoint.period参数控制。
每分钟查看一次操作次数,如果操作次数达到100W次时,Secondary NameNode也会执行一次,在hdfs-default.xml里由dfs.namenode.checkpoint.txns和dfs.namenode.checkpoint.check.period参数控制。
DataNode工作机制
工作机制
- 一个数据块在DataNode上以文件形式存储在磁盘,包括两部分,一个是数据本身,一个是数据属性和校验信息。
- DataNode启动后,向NameNode注册,周期性(间隔6小时)的向NameNode上报块信息,DataNode向NameNode汇报信息间隔是6小时,由dfs.blockreport.intervalMsec参数控制,DataNode扫描自己结点信息列表时间也是6小时,由dfs.datanode.directoryscan.interval参数控制。
- DataNode和NameNode之间用每隔3秒的心跳信号保活,心跳返回结果带有NameNode给该DataNode的命令如复制块数据到另一台机器,或删除某个数据块。如果超过10分钟没有收到某个DataNode的心跳,则认为该节点不可用。
- 集群运行中可以安全加入和退出一些机器。
数据完整性
- DataNode结点读取Block信息的时候,会计算CheckSum。
- 如果计算后的CheckSum与Block创建时不一样,说明Block已经损坏。
- Client读取其他DataNode上的Block。
- 校验算法:crc(32),md5(128),sha1(160)。
- DataNode在文件创建后,周期性验证CheckSum。
掉线时限参数设置
当DataNode死亡或网络故障无法通信的时候,NameNode不会立即把该结点判定为死亡,经过一段时间(超时时长)后,宣布DataNode死亡。HDFS默认超时时长是10分钟+30秒。
TimeOut = 2 * dfs.namenode.heartbeat.recheck-interval + 10 * dfs.heartbeat.interval。而默认的dfs.namenode.heartbeat.recheck-interval 大小为5分钟,dfs.heartbeat.interval默认为3秒。
在hdfs-site.xml里可以找到heartbeat.recheck.interval和dfs.heartbeat.interval,heartbeat.recheck.interval的单位是毫秒,dfs.heartbeat.interval的单位是秒。
当DataNode死亡后,NameNode就不会继续和它进行数据交互了,如果后面DataNode复活,会通知到NameNode,此时会做数据同步,之后保持正常运行。