HDFS(Hadoop Distributed File System),即Hadoop分布式文件系统
HDFS由多台主机搭建服务器集群来实现,集群中的服务器分别负责不同的功能
HDFS通过目录树来定位文件
优点
缺点
HDFS适合一次写入,多次读取,并且不支持文件的修改,适合做数据分析
HDFS由NameNode,DataNode,Client和SecondaryNameNode组成,在
HDFS中,文件以切块的形式存在,即一个大文件分成多个部分来存储,每个块大小为128MB(1.x版本中是64MB)
一个块(block)如果太小,会增加寻址时间,如果一个block太大,会导致MapReduce处理这块数据时会非常慢
注意
HDFS的块大小主要取决与磁盘的传输速率
由于加使用的操作系统,开发软件可能都不一样,就比如我使用的是基于Linux的Deepin15.11桌面版,开发软件使用的是idea,相比大多数人使用的是Windows系统下Eclipse开发,所以操作不太一样
虽然操作不同,但是配置的思路给大家列举一下,如果大家不会配置,就根据下面的步骤百度一下:
注意
1. 在执行命令时,使用bin/hadoop fs
和bin/hdfs dfs
的效果是一样的,这是因为dfs是fs的实现类,它们底层调用的jar包都是一样的
2. HDFS的命令和Linux的命令很像,如果熟悉Linux指令,HDFS的Shell操作可以很快上手
3. 下面所有的命令演示,涉及到HDFS文件(夹)的变更,都可以通过浏览器访问NameNode50070端口查看HDFS的文件系统详情来查看命令执行的结果
hadoop fs
也可以通过下面的指令查看
hadoop fs --help
注意
1. 同样的hadoop dfs
和hadoop dfs --help
也一样,下面我们就不把fs
和dfs
一一做列举了,我们统一使用fs
2. 通过以上指令可以查看HDFS的所有指令,下面我们只列举最常用的几个指令,如果大家感兴趣可以把HDFS所有的命令自己测试一遍
hadoop fs -ls <目录路径>
hadoop fs -cat <文件路径>
hadoop fs -cp <文件(夹)路径> <拷贝后的文件夹路径>
hadoop fs -rm -r <要删除的文件夹路径>
Hadoop fs -df [选项]
选项:
选项 | 说明 |
---|---|
-h | 存储单位以比较友好的方式展现,说白了就是把单位转换成GB,MB等 |
hadoop fs -du [选项] <路径>
选项:
选项 | 说明 |
---|---|
-h | 同df,存储单位以比较友好的方式展现,说白了就是把单位转换成GB,MB等 |
hadoop fs -setrep <副本数量n>
注意
设置的数量一定要小于DataNode的数量,否则只能创建DataNode个数的副本
hadoop fs -copyFromLocal <本地文件路径>
或者
hadoop fs -put <本地文件路径>
注意
-copyFromLocal
和-put
的作用是一样的,在底层copyFromLocal
其实是put
的一个子类,但是这个子类什么其他的功能都没有实现,只是换了一个名字而已
其实moveFromLocal
与copyFromLocal
唯一的区别就是一个是剪切,一个是复制
hadoop fs -moveFromLocal <本地文件路径>
hadoop -appendToFile <本地文件路径> <要追加到的文件路径>
下载同上传一样,有两个方法
hadoop fs -copyToLocal <下载到本地的文件路径>
或者
hadoop fs -get <下载到本地的文件路径>
hadoop fs -getmerge ... <本地合并后的文件路径>
首先,新建一个Maven项目(不需要官方模板,直接新建简单项目即可)
然后引入依赖
<dependencies>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>RELEASEversion>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
<version>2.8.2version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-commonartifactId>
<version>2.7.7version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-clientartifactId>
<version>2.7.7version>
dependency>
<dependency>
<groupId>org.apache.hadoopgroupId>
<artifactId>hadoop-hdfsartifactId>
<version>2.7.7version>
dependency>
<dependency>
<groupId>jdk.toolsgroupId>
<artifactId>jdk.toolsartifactId>
<version>1.8version>
<scope>systemscope>
<systemPath>${JAVA_HOME}/lib/tools.jarsystemPath>
dependency>
dependencies>
resources
里新建一个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
//注意:导入的包都是org.apache.hadoop中的
@Test
public void put() throws IOException, InterruptedException {
//获取文件系统,参数: ①URI,②配置(其实就是获取了Hadoop中的core-site.xml文件中的信息),③云主机中的用户名
FileSystem fileSystem = FileSystem.get(URI.create("hdfs://:9000" ),new Configuration(),"" );
//上传文件, 参数: ①本地的文件路径,②Hadoop中存储的路径
fileSystem.copyFromLocalFile(new Path("<本机上的文件路径>"), new Path("/"));
//关流
fileSystem.close();
}
如果执行结果返回的是0
,说明上传成功
@Test
public void put() throws IOException, InterruptedException {
//获取文件系统,参数: ①URI,②配置(其实就是获取了Hadoop中的core-site.xml文件中的信息),③云主机中的用户名
FileSystem fileSystem = FileSystem.get(URI.create("hdfs://:9000" ),new Configuration(),"" );
//上传文件, 参数: ①本地的文件路径,②Hadoop中存储的路径
fileSystem.copyFromLocalFile(new Path("/1.txt"),new Path("<本机上的文件路径>"));
//关流
fileSystem.close();
}
@Test
public void rename() throws IOException, InterruptedException {
FileSystem fileSystem = FileSystem.get(URI.create("hdfs://:9000" ), new Configuration(), "" );
fileSystem.rename(new Path("<要重命名的文件路径>"), new Path("<重命名之后文件路径>"));
fileSystem.close();
}
@Test
public void del() throws IOException, InterruptedException {
FileSystem fileSystem = FileSystem.get(URI.create("hdfs://:9000" ), new Configuration(), "" );
//delete方法返回一个boolean值表示是否删除成功, 参数: ①HDFS中的文件路径,②是否递归删除
boolean delete = fileSystem.delete(new Path("" ), true);
if (delete) {
System.out.println("删除成功");
} else {
System.out.println("删除失败");
}
fileSystem.close();
}
@Test
public void read() throws IOException, InterruptedException {
FileSystem fileSystem = FileSystem.get(URI.create("hdfs://:9000" ), new Configuration(), "" );
FileStatus[] fileStatuses = fileSystem.listStatus(new Path("/"));
for (FileStatus fileStatus:fileStatuses) {
//判断是否是一个文件
if (fileStatus.isFile()) {
//获取文件路径
System.out.print("文件路径"+fileStatus.getPath()+"\t");
//获取文件长度
System.out.println("文件长度"+fileStatus.getLen());
} else {
System.out.println("这是一个文件夹,路径:"+fileStatus.getPath());
}
}
}
总之可以获取各种各样的文件信息,输入fileStatus.
,后面会自动提示很多方法,大家可以自己去试一下,这里就不一一举例了
除此之外,如果只是想获取指定HDFS目录下的文件内容(可以选择是否递归),可以使用listFiles
方法,该方法返回一个迭代器,可以通过hadNext
方法判断是否有下一个文件,然后处理每个文件,使用listFiles
还可以查看每个文件所在块的信息
@Test
public void getFiles() throws IOException, InterruptedException {
FileSystem fileSystem = FileSystem.get(URI.create("hdfs://192.168.8.101:9000"), new Configuration(), "alasky");
//创建一个迭代器,接收HDFS指定目录下的所有文件,参数:①HDFS文件路径,②是否递归
RemoteIterator<LocatedFileStatus> locatedFileStatusRemoteIterator = fileSystem.listFiles(new Path("/"), true);
//查看迭代器中是否还有下一个内容
while (locatedFileStatusRemoteIterator.hasNext()) {
System.out.print("文件信息: ");
//接收迭代器中的当前文件
LocatedFileStatus locatedFileStatus = locatedFileStatusRemoteIterator.next();
System.out.println(locatedFileStatus.getPath());
System.out.println("块信息:");
//创建一个BlockLocation数组,接收这给文件存储的块信息
BlockLocation[] blockLocations=locatedFileStatus.getBlockLocations();
for (BlockLocation blockLocation : blockLocations) {
//获取每个块所在的主机
String[] hosts = blockLocation.getHosts();
for (String host : hosts) {
System.out.println(host);
}
}
}
fileSystem.close();
}
说明 为什么使用listStatus()
方法获取的FileStatus
没有块信息,而使用listFiles()
方法获取的BlockLocation
有块信息?
因为使用listStatus获取的内容不止有文件,还有文件夹(目录),但是目录并没有块(block)
的概念,只有文件才有,如果获取文件夹的块信息会出错,所以FileStatus
并没有获取块信息的方法
这里说明一下,其实空文件也没有占用block的,但是Hadoop的API里面默认所有的文件都有block
说明
在HDFS中的每个块的信息,都会被NameNode记录到元数据中,而且上面说过的读写操作,Client都会先从NameNode获取块的信息,这些信息就存储在元数据当中.
元数据会分别存储在内存和磁盘中,存在内存中的元数据是为了快速读取信息,而存储在磁盘中是为了备份,方便恢复
在磁盘中的元数据是以fsimage的文件存在,但是如果每次操作都在fsimage中更新,就会极大的降低效率,但是如果隔一段时间才更新一次,如果更新之前NameNode出现宕机,那么最后一次更新之后的元数据有没法保存,所以NameNode除了fsimage之外,还会将每次增删改操作以日志的形式记录在edits文件中,edits文件只做追加操作(append),不做修改,所以效率很高,然后每隔一段时间,就将edits文件中的新内容合并到fsimage中
如果出现宕机,NameNode会结合fsimage文件和edits文件来获取到全部的元数据,然后重新加载到内存中,以保证元数据的完整性
总结一下,元数据会存储在内存中和磁盘中,磁盘中的元数据存在fsimage文件里,每次写操作都会记录日志,存在edits文件里
注意
由于fsimage文件每隔一段时间才会更新一次,所以fsimage文件中的元数据和内存中的元数据不是实时同步的
说明
可以看到fsimage中没有存储数据块的实际物理存储路径,这是因为在HDFS启动之后,DataNode每隔一段时间(默认1h)之后会向NameNode上报该DataNode存储的块信息,然后由NameNode加载到内存中
fs.checkpoint.size
属性来指定,默认单位是字节fs.checkpoint.period
来指定,默认单位是秒hadoop dfsadmin -rollEdits
命令来手动更新更新fsimage时,如果集群中存在.SecondaryNameNode节点,就会将edits和fsimage的合并工作在.SecondaryNameNode中进行,如果没有,就会在NameNode本地进行
在更新的时候,会将edits_inprogress重命名为edits_XXX-XXX(XXX-XXX是这个edits文件所记录的全局事务id的范围),同时产生一个新的edits_inprogress,然后将重命名之后的edits文件和fsimage文件发送到.SecondaryNameNode中,由SecondaryNameNode进行合并
SecondaryNameNode合并完成之后,将新的fsimage文件发回到NameNode
注意
为了数据的可靠性和完整性,在NameNode中会保存2个fsimage文件,一个是最新的fsimage文件,一个是上一个fsimage文件,当SecondaryNameNode将新的fsimage文件发送过来时,NameNode会自动删除最旧的那个fsimage文件
hdfs oiv -p <文件格式> -i -o <转换文件格式之后输出到的路径>
然后查看输出的文件即可
一般将fsimage转换为xml格式文件
举例
hdfs oiv -p XML -i fsimage_0000000000000000025 -o /opt/hadoop-2.7.7/fsimage.xml
hdfs oev -p <文件格式> -i -o <转换文件格式之后输出到的路径>
如果有一天我们在某个DataNode节点上新添加了一块硬盘,想要也将数据存储到这块硬盘中,那么可以再从hdfs-site.xml配置文件中添加多个DataNode的存储路径,配置如下
dfs.datanode.data.dir
file:///${hadoop.tmp.dir}<路径1>,file:///${hadoop.tmp.dir}<路径2>
介绍
顾名思义,黑名单就是不允许指定的DataNode接入集群
配置
dfs.hosts.exclude
<黑名单文件路径>
hdfs dfsadmin -refreshNodes
yarn rmadmin -refreshNodes
注意
配置了黑名单的DataNode节点,仍旧可以连接集群,只不过是NameNode不在为其分配数据块的存储
介绍
白名单就是只允许指定的DataNode连接集群,可以有效防止其他节点混入,保证集群的数据安全
配置
dfs.hosts
<白名单文件路径>
hdfs dfsadmin -refreshNodes
yarn rmadmin -refreshNodes
扩展新的DataNode节点很容易,只需要配置好分布式的那些配置(尤其是指定好NameNode),DataNode就会自动向NameNode发送注册请求,然后加入集群
注意
不需要在配置中配置slave就可以添加新的节点,但是为了下次可以直接从NameNode启动新的DataNode节点,还是建议在slave里添加该新DataNode节点
如果想要是某个DataNode节点退出集群,有多种方式可以选择
直接将要退出的DataNode节点关机或者断开与NameNode的连接,一关时间之后NameNode会认为该节点宕机,然后就会自动剔除该节点
但是这种方法需要额外的等NameNode检测(10分钟),而且不能重新启动该DataNode节点,一旦重新启动或者恢复连接,就会重新加入集群
配置一个黑名单,把该节点的地址写进去,然后刷新NameNode和ResourceManager,该DataNode就会自动将自身的文件块备份到其他节点,然后退出集群,但是就像刚刚黑名单介绍的,此时DataNode仍会与NameNode保持连接,只不过是不再分配资源了
在生产环境中,推荐使用黑名单的方式退役节点
配置一个白名单,然后白名单中不加上该节点的名字,刷新NameNode和ResourceManager,该节点就会被强制退出集群,并且关闭DataNode进程
小文件在HDFS中可以说是噩梦般的存在,因为每个小文件在HDFS中也占用一个块和一个元数据,而且多个小文件的元数据也会占用大量的NameNode内存,寻址也很慢,所以HDFS中我们不建议存储小文件
如果没有办法避免小文件,Hadoop也提供了相应的处理措施,就是把多个小文件打包成一个har包,这个包在HDFS中只占用一个文件的元数据,而且寻址和读取也比较方便
start-yarn.sh
/bin/hadoop archive -archiveName <自定义包名>.har –p <打包之前的路径> <打包后的har包路径>
然后将har包上传到HDFS就可以了,例如:
bin/hadoop archive -archiveName input.har –p ./input ./output
hadoop fs -lsr
hadoop fs -cp har:// <解压后的路径>
taNode仍会与NameNode保持连接,只不过是不再分配资源了
在生产环境中,推荐使用黑名单的方式退役节点
配置一个白名单,然后白名单中不加上该节点的名字,刷新NameNode和ResourceManager,该节点就会被强制退出集群,并且关闭DataNode进程
小文件在HDFS中可以说是噩梦般的存在,因为每个小文件在HDFS中也占用一个块和一个元数据,而且多个小文件的元数据也会占用大量的NameNode内存,寻址也很慢,所以HDFS中我们不建议存储小文件
如果没有办法避免小文件,Hadoop也提供了相应的处理措施,就是把多个小文件打包成一个har包,这个包在HDFS中只占用一个文件的元数据,而且寻址和读取也比较方便
start-yarn.sh
/bin/hadoop archive -archiveName <自定义包名>.har –p <打包之前的路径> <打包后的har包路径>
然后将har包上传到HDFS就可以了,例如:
bin/hadoop archive -archiveName input.har –p ./input /
hadoop fs -lsr har:///
hadoop fs -cp har:/// <解压后的路径>