目录

第五部分 HDFS分布式⽂件系统

第 1 节 HDFS 简介

第 2 节 HDFS的重要概念

典型的 Master/Slave 架构

分块存储(block机制)

命名空间(NameSpace)

NameNode元数据管理

DataNode数据存储

副本机制

⼀次写⼊,多次读出

第 3 节 HDFS 架构

NameNode(nn):

DataNode(dn):

Client:

第 4 节 HDFS 客户端操作

4.1 Shell 命令⾏操作HDFS

4.2 JAVA客户端

4.2.1 客户端环境准备

4.2.2 HDFS的API操作

第 5 节 HDFS读写解析

5.1 HDFS读数据流程

5.2 HDFS写数据流程

第 6 节 NN与2NN

6.1 HDFS元数据管理机制

第⼀阶段:NameNode启动

第⼆阶段:Secondary NameNode⼯作

6.2 Fsimage与Edits⽂件解析

6.2.1 Fsimage⽂件内容

6.2.2 Edits⽂件内容

6.3 checkpoint周期

第 7 节 NN故障处理

第 8 节 Hadoop的限额与归档以及集群安全模式

HDFS⽂件限额配置

数量限额

 空间⼤⼩限额

HDFS的安全模式

Hadoop归档技术

1. 归档命令查询

2. 归档⽂件

3. 查看归档

4. 解归档⽂件

第九节 日志采集综合案例

9.1 需求分析

9.2 代码实现

9.3 代码优化


 

第五部分 HDFS分布式⽂件系统

第 1 节 HDFS 简介

HDFS (全称:Hadoop Distribute File System,Hadoop 分布式⽂件系统)是 Hadoop 核⼼组成,是分布式存储服务。

HDFS是分布式⽂件系统中的⼀种。

 

第 2 节 HDFS的重要概念

HDFS 通过统⼀的命名空间⽬录树来定位⽂件; 另外,它是分布式的,由很多服务器联合起来实现其功能,集群中的服务器有各⾃的⻆⾊(分布式本质是拆分,各司其职);

典型的 Master/Slave 架构

HDFS 的架构是典型的 Master/Slave 结构。
HDFS集群往往是⼀个NameNode(HA架构会有两个NameNode,联邦机制)+多个DataNode组成;
NameNode是集群的主节点,DataNode是集群的从节点。

 

分块存储(block机制)

HDFS 中的⽂件在物理上是分块存储(block)的,块的⼤⼩可以通过配置参数来规定;
Hadoop2.x版本中默认的block⼤⼩是128M;

 

命名空间(NameSpace)

HDFS ⽀持传统的层次型⽂件组织结构。⽤户或者应⽤程序可以创建⽬录,然后将⽂件保存在这些⽬录⾥。⽂件系统名字空间的层次结构和⼤多数现有的⽂件系统类似:⽤户可以创建、删除、移动或重命名文件

Namenode 负责维护⽂件系统的名字空间,任何对⽂件系统名字空间或属性的修改都将被Namenode 记录下来。
HDFS提供给客户单⼀个抽象⽬录树,访问形式:hdfs://namenode的hostname:port/test/input

hdfs://linux121:9000/test/input

 

NameNode元数据管理

我们把⽬录结构及⽂件分块位置信息叫做元数据。
NameNode的元数据记录每⼀个⽂件所对应的block信息(block的id,以及所在的DataNode节点的信息)

 

DataNode数据存储

⽂件的各个 block 的具体存储管理由 DataNode 节点承担。⼀个block会有多个DataNode来存储,DataNode会定时向NameNode来汇报⾃⼰持有的block信息。

 

副本机制

为了容错,⽂件的所有 block 都会有副本。每个⽂件的 block ⼤⼩和副本系数都是可配置的。应⽤程序可以指定某个⽂件的副本数⽬。副本系数可以在⽂件创建的时候指定,也可以在之后改变。
副本数量默认是3个。

 

⼀次写⼊,多次读出

HDFS 是设计成适应⼀次写⼊,多次读出的场景,且不⽀持⽂件的随机修改。 (⽀持追加写⼊,不只⽀持随机更新)
正因为如此,HDFS 适合⽤来做⼤数据分析的底层存储服务,并不适合⽤来做⽹盘等应⽤(修改不⽅便延迟⼤⽹络开销⼤成本太⾼)

 

第 3 节 HDFS 架构

hdfs命令调整日志级别 hdfs操作日志_HDFS

NameNode(nn):

Hdfs集群的管理者,Master

维护管理Hdfs的名称空间(NameSpace)
维护副本策略
记录⽂件块(Block)的映射信息
负责处理客户端读写请求

DataNode(dn):

NameNode下达命令,DataNode执⾏实际操作,Slave节点。
保存实际的数据块
负责数据块的读写

Client:

客户端上传⽂件到HDFS的时候,Client负责将⽂件切分成Block,然后进⾏上传
请求NameNode交互,获取⽂件的位置信息
读取或写⼊⽂件,与DataNode交互
Client可以使⽤⼀些命令来管理HDFS或者访问HDFS

hdfs命令调整日志级别 hdfs操作日志_hdfs_02

 

第 4 节 HDFS 客户端操作

4.1 Shell 命令⾏操作HDFS

1. 基本语法

bin/hadoop fs 具体命令 OR bin/hdfs dfs 具体命令

2. 命令⼤全

[root@linux121 hadoop-2.9.2]# bin/hdfs dfs
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] <localsrc> ... <dst>]
  [-copyToLocal [-f] [-p] [-ignoreCrc] [-crc] <src> ... <localdst>]
  [-count [-q] [-h] [-v] [-t [<storage type>]] [-u] [-x] <path> ...]
  [-cp [-f] [-p | -p[topax]] [-d] <src> ... <dst>]
  [-createSnapshot <snapshotDir> [<snapshotName>]]
  [-deleteSnapshot <snapshotDir> <snapshotName>]
  [-df [-h] [<path> ...]]
  [-du [-s] [-h] [-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>]
  [-help [cmd ...]]
  [-ls [-C] [-d] [-h] [-q] [-R] [-t] [-S] [-r] [-u] [<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] <file>]
  [-test -[defsz] <path>]
  [-text [-ignoreCrc] <src> ...]
  [-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 machin

3.HDFS命令演示

1. start-dfs.sh   start-yarn.sh 启动Hadoop集群(⽅便后续的测试)

2. -help:输出这个命令的帮助

hadoop fs -help rm

3. -ls: 显示⽬录信息

4. -mkdir:在HDFS上创建⽬录

5. -moveFromLocal:从本地剪切粘贴到HDFS

hadoop fs -moveFromLocal ./hadoop.txt /lagou/bigdata

6. -appendToFile:追加⼀个⽂件到已经存在的⽂件末尾

hadoop fs -appendToFile hdfs.txt /lagou/bigdata/hadoop.txt

7. -cat:显示⽂件内容

8. -chgrp 、-chmod、-chown:Linux⽂件系统中的⽤法⼀样,修改⽂件所属权限

hadoop fs -chmod 666 /lagou/bigdata/hadoop.txt

hadoop fs -chown root:root /lagou/bigdata/hadoop.txt

9. -copyFromLocal:从本地⽂件系统中拷⻉⽂件到HDFS路径去

[root@linux121 hadoop-2.9.2]$ hadoop fs -copyFromLocal README.txt /

10. -copyToLocal:从HDFS拷⻉到本地

hadoop fs -copyToLocal /lagou/bigdata/hadoop.txt ./

11. -cp :从HDFS的⼀个路径拷⻉到HDFS的另⼀个路径

[root@linux121 hadoop-2.9.2]$ hadoop fs -cp /lagou/bigdata/hadoop.txt /hdfs.txt

12. -mv:在HDFS⽬录中移动⽂件

13. -get:等同于copyToLocal,就是从HDFS下载⽂件到本地

hadoop fs -get /lagou/bigdata/hadoop.txt ./

14. -put:等同于copyFromLocal

 hadoop fs -put ./yarn.txt /user/root/test/

16. -tail:显示⼀个⽂件的末尾

17. -rm:删除⽂件或⽂件夹

18. -rmdir:删除空⽬录

19. -du统计⽂件夹的⼤⼩信息 (-s表示文件夹 -h表示文件)

[root@linux121 hadoop-2.9.2]$ hadoop fs -du -s -h /user/root/test

[root@linux121 hadoop-2.9.2]$ hadoop fs -du -h /user/root/test

20. -setrep:设置HDFS中⽂件的副本数量

[root@linux121 hadoop-2.9.2]$ hadoop fs -setrep 10 /lagou/bigdata/hadoop.txt

副本数量

这⾥设置的副本数只是记录在NameNode的元数据中,是否真的会有这么多副本,还得看DataNode的数量。因为⽬前只有3台设备,最多也就3个副本,只有节点数的增加到10台时,副本数才能达到10

hdfs命令调整日志级别 hdfs操作日志_hdfs_03

 

4.2 JAVA客户端

4.2.1 客户端环境准备

1. 将Hadoop-2.9.2安装包解压到⾮中⽂路径(例如:E:\hadoop-2.9.2)。

hdfs命令调整日志级别 hdfs操作日志_HDFS_04

2. 配置HADOOP_HOME环境变量

hdfs命令调整日志级别 hdfs操作日志_hdfs_05

3. 配置Path环境变量。

hdfs命令调整日志级别 hdfs操作日志_HDFS_06

4. 创建⼀个Maven⼯程ClientDemo
5. 导⼊相应的依赖坐标+⽇志配置⽂件

<dependencies>
     <dependency>
         <groupId>junit</groupId>
        <artifactId>junit</artifactId>
         <version>RELEASE</version>
     </dependency>
     <dependency>
         <groupId>org.apache.logging.log4j</groupId>
         <artifactId>log4j-core</artifactId>
         <version>2.8.2</version>
         </dependency>
     <dependency>
         <groupId>org.apache.hadoop</groupId>
         <artifactId>hadoop-common</artifactId>
         <version>2.9.2</version>
     </dependency>

 <!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoopclient -->
     <dependency>
         <groupId>org.apache.hadoop</groupId>
         <artifactId>hadoop-client</artifactId>
         <version>2.9.2</version>
     </dependency>

 <!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-hdfs-->
     <dependency>
         <groupId>org.apache.hadoop</groupId>
         <artifactId>hadoop-hdfs</artifactId>
         <version>2.9.2</version>
     </dependency>
</dependencies>

为了便于控制程序运⾏打印的⽇志数量,需要在项⽬的src/main/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

6. 创建包名:com.lagou.hdfs

7. 创建HdfsClient类

package com.lagou.htfs;

/**
 * @author CH
 * @date 2020/12/24 14:07
 */
public class HtfsClient {

    @Test
    public void testMkdirs() throws URISyntaxException, IOException, InterruptedException {

        //1. 获取Hadoop集群的configuration对象
        Configuration configuration = new Configuration();
        configuration.set("fs.defaultFS", "hdfs://linux121:9000");

        //2. 根据configuration获取Filesystem对象
        // 注掉下面, 采用上面的方式
//        FileSystem fs = FileSystem.get(new URI("hdfs://linux121:9000"), configuration, "root");

        // 建议采用上面的方式, 可以避免用户名问题
        FileSystem fs = FileSystem.get(configuration);

        //3. 使用FIlesystem对象创建一个测试目录
        fs.mkdirs(new Path("/api_test1"));

        //4. 释放FileSystem对象(类似数据库连接)
        fs.close();
    }
}

遇到问题:
如果不指定操作HDFS集群的⽤户信息,默认是获取当前操作系统的⽤户信息,出现权限被拒绝的问题,报错如下:

hdfs命令调整日志级别 hdfs操作日志_hdfs命令调整日志级别_07

 

 

4.2.2 HDFS的API操作

1 上传⽂件

1. 编写源代码

package com.lagou.htfs;

/**
 * @author CH
 * @date 2020/12/24 14:07
 */
public class HtfsClient {

    FileSystem fs = null;

    @Before
    public void init() throws URISyntaxException, IOException, InterruptedException {
        //1. 获取Hadoop集群的configuration对象
        Configuration configuration = new Configuration();
        //configuration.set("dfs.replication", "2");

        //2. 根据configuration获取Filesystem对象

        fs = FileSystem.get(new URI("hdfs://linux121:9000"), configuration, "root");
    }


    @After
    public void destroy() throws IOException {

        //4. 释放FileSystem对象(类似数据库连接)
        fs.close();
    }


    // 上传文件
    @Test
    public void copyFromLocalToHdfs() throws URISyntaxException, IOException, InterruptedException {

        // src: 源文件目录(本地路径)
        // dst: 目标目录(hdfs路径)
        fs.copyFromLocalFile(new Path("D:/b.txt"), new Path("/b.txt"));

        // 上传文件到hdfs默认3个副本

        // 改变副本数量,
        //      1, configuration对象中指定新的副本数量, 见init方法中的 dfs.replication
        //      2, 配置文件hdfs-site中设置

    }
}

2. 将hdfs-site.xml拷⻉到项⽬的根⽬录下

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>

<configuration>
     <property>
         <name>dfs.replication</name>
         <value>1</value>
     </property>
</configuration>

3. 参数优先级
参数优先级排序:(1)代码中设置的值 >(2)⽤户⾃定义配置⽂件 >(3)服务器的默认配置

2 下载⽂件

// 下载文件
    @Test
    public void copyFromHdfsToLocal() throws URISyntaxException, IOException, InterruptedException {

        // src: 源文件目录(hdfs路径)
        // dst: 目标目录(本地路径)
        fs.copyToLocalFile(true, new Path("/b.txt"), new Path("D:/b.txt"));

    }

3 删除⽂件/⽂件夹

// 删除文件或者文件夹
    @Test
    public void edelete() throws URISyntaxException, IOException, InterruptedException {

        // path: 目标目录(hdfs路径)
        // boolean: 是否递归删除
        fs.delete(new Path("/api_test/"), true);

    }

4 查看⽂件名称、权限、⻓度、块信息

// 遍历指定目录获取信息, 名称, 长度, 权限等
    @Test
    public void list() throws URISyntaxException, IOException, InterruptedException {

        // path: 目标目录(hdfs路径)
        // boolean: 是否递归
        // 得到一个迭代器, 装有指定目录下所有文件信息
        RemoteIterator<LocatedFileStatus> remoteIterator = fs.listFiles(new Path("/"), true);
        //遍历迭代器
        while(remoteIterator.hasNext()){
            LocatedFileStatus fileStatus = remoteIterator.next();
            // 文件名
            String fileName = fileStatus.getPath().getName();
            // 长度
            long len = fileStatus.getLen();
            // 权限
            FsPermission permission = fileStatus.getPermission();
            // 分组
            String group = fileStatus.getGroup();
            // 所属用户
            String owner = fileStatus.getOwner();
            
            //  \t 是制表符
            System.out.println(fileName + "\t" + len + "\t" + permission + "\t" + group + "\t" + owner);

            // 块信息
            BlockLocation[] blockLocations = fileStatus.getBlockLocations();
            for (BlockLocation blockLocation : blockLocations) {
                String[] hosts = blockLocation.getHosts();
                for (String host : hosts) {
                    System.out.println("主机名称" + host);
                }
            }

            System.out.println("-----------------------------------");
        }
    }

hdfs命令调整日志级别 hdfs操作日志_HDFS_08

5 ⽂件夹判断

// 文件以及文件夹判断
    @Test
    public void isFile() throws URISyntaxException, IOException, InterruptedException {
        
        FileStatus[] fileStatuses = fs.listStatus(new Path("/"));
        for (FileStatus fileStatus : fileStatuses) {
            boolean flag = fileStatus.isFile();

            if (flag) {
                System.out.println("文件: " + fileStatus.getPath().getName());
            } else {
                System.out.println("文件夹: " + fileStatus.getPath().getName());
            }
        }
    }

hdfs命令调整日志级别 hdfs操作日志_hdfs命令调整日志级别_09

 

6 I/O流操作HDFS

以上我们使⽤的API操作都是HDFS系统框架封装好的。我们⾃⼰也可以采⽤IO流的⽅式实现⽂件的上传和下载。

6.1 ⽂件上传

1. 需求:把本地e盘上的lagou.txt⽂件上传到HDFS根⽬录
2. 编写代码

// 使用IO流操作HDFS
    // 上传文件: 准备输入流读取本地文件, 使用hdfs的输出流写数据到hdfs
    @Test
    public void uploadFileIO() throws IOException {

        //1. 读取本地文件的输入流
        FileInputStream inputStream = new FileInputStream(new File("D:/lagou.txt"));

        //2. 准备写数据到hdfs
        FSDataOutputStream outputStream = fs.create(new Path("/lagou.txt"));

        //3. 输入流数据拷贝到输出流
        // 下面的方法设置缓冲区大小, 以及自动关闭流
        //IOUtils.copyBytes(inputStream,outputStream,4096, true);
        // 不自动关闭, 用下面的, 因为底层有默认值
        IOUtils.copyBytes(inputStream,outputStream,configuration);
        // 可以再次关闭流
        IOUtils.closeStream(outputStream);
        IOUtils.closeStream(inputStream);
    }

hdfs命令调整日志级别 hdfs操作日志_hdfs命令调整日志级别_10

 

6.2 ⽂件下载

1. 需求:从HDFS上下载lagou.txt⽂件到本地e盘上

2. 编写代码

// 使用IO流操作HDFS
    // 下载文件
    @Test
    public void downloadFileIO() throws IOException {

         //1. 读取hdfs文件的输入流
        FSDataInputStream in = fs.open(new Path("/lagou.txt"));

        //2. 本地文件的输出流
        FileOutputStream out = new FileOutputStream(new File("D:/lagou_io_copy.txt"));

        //3. 流的拷贝
        IOUtils.copyBytes(in,out,configuration);

        //4. 可以再次关闭流
        IOUtils.closeStream(out);
        IOUtils.closeStream(in);
    }

hdfs命令调整日志级别 hdfs操作日志_hdfs_11

 

6.3 seek 定位读取

1. 需求:将HDFS上的lagou.txt的内容在控制台输出两次
2. 编写代码

// seek 定位读取 hdfs 指定文件 : 使用IO流读取/lagou.txt文件,并把内容输出两次, 本质就是读取文件内容并两次输出
    @Test
    public void seekReadFile() throws IOException {

        //1. 创建一个读取hdfs文件的输入流
        FSDataInputStream in = fs.open(new Path("/lagou.txt"));

        //2. 控制台输出 system.out

        //3. 实现流拷贝,  输入流 → 控制台
        // 下面的读取方式, 会自动结束并关闭流
        // IOUtils.copyBytes(in, System.out, configuration);

        //4. 采用下面的方式, 可以再次读取文件
        IOUtils.copyBytes(in, System.out, 4096, false);
        in.seek(0);  // 定位从0偏移量(文件头部) 再次读取
        IOUtils.copyBytes(in,System.out, 4096, false);

        //5. 关闭输入流
        IOUtils.closeStream(in);
    }

hdfs命令调整日志级别 hdfs操作日志_HDFS_12

注意
windows解压安装Hadoop后,在调⽤相关API操作HDFS集群时可能会报错,这是由于Hadoop安装缺少windows操作系统相关⽂件所致,如下图:

hdfs命令调整日志级别 hdfs操作日志_hadoop_13

解决⽅案:
从资料⽂件夹中找到winutils.exe拷⻉放到windows系统Hadoop安装⽬录的bin⽬录下即可!!

HDFS⽂件系统权限问题
hdfs的⽂件权限机制与linux系统的⽂件权限机制类似!!
r:read w:write x:execute 权限x对于⽂件表示忽略,对于⽂件夹表示是否有权限访问其内容
如果linux系统⽤户zhangsan使⽤hadoop命令创建⼀个⽂件,那么这个⽂件在HDFS当中的owner就是zhangsan

HDFS⽂件权限的⽬的,防⽌好⼈做错事,⽽不是阻⽌坏⼈做坏事。HDFS相信你告诉我你是谁,你就是谁!!

解决⽅案
指定⽤户信息获取FileSystem对象
关闭HDFS集群权限校验 

vim hdfs-site.xml

#添加如下属性 <property> <name>dfs.permissions.enabled</name> <value>false</value> </property>

修改完成之后要分发到其它节点,同时要重启HDFS集群

基于HDFS权限本身⽐较鸡肋的特点,我们可以彻底放弃HDFS的权限校验,如果⽣产环境中,  我们可以考虑借助kerberos以及sentry等安全框架来管理⼤数据集群安全。

所以我们直接修改HDFS的根⽬录权限为777

hadoop fs -chmod -R 777 /

 

 

第 5 节 HDFS读写解析

5.1 HDFS读数据流程

hdfs命令调整日志级别 hdfs操作日志_hdfs_14

1. 客户端通过Distributed FileSystem向NameNode请求下载⽂件,NameNode通过查询元数据,找到⽂件块所在的DataNode地址。
2. 挑选⼀台DataNode(就近原则,然后随机)服务器,请求读取数据。
3. DataNode开始传输数据给客户端(从磁盘⾥⾯读取数据输⼊流,以Packet为单位来做校验)。
4. 客户端以Packet为单位接收,先在本地缓存,然后写⼊⽬标⽂件。

 

5.2 HDFS写数据流程

hdfs命令调整日志级别 hdfs操作日志_hdfs命令调整日志级别_15

1. 客户端通过Distributed FileSystem模块向NameNode请求上传⽂件,NameNode检查⽬标⽂件是否已存在,⽗⽬录是否存在。
2. NameNode返回是否可以上传。
3. 客户端请求第⼀个 Block上传到哪⼏个DataNode服务器上。
4. NameNode返回3个DataNode节点,分别为dn1、dn2、dn3。
5. 客户端通过FSDataOutputStream模块请求dn1上传数据,dn1收到请求会继续调⽤dn2,然后dn2调⽤dn3,将这个通信管道建⽴完成。
6. dn1、dn2、dn3逐级应答客户端。
7. 客户端开始往dn1上传第⼀个Block(先从磁盘读取数据放到⼀个本地内存缓存),以Packet为单位,dn1收到⼀个Packet就会传给dn2,dn2传给dn3;dn1每传⼀个packet会放⼊⼀个确认队列等待确认。
8. 当⼀个Block传输完成之后,客户端再次请求NameNode上传第⼆个Block的服务器。(重复执⾏3-7步)。

验证Packet代码

// 验证Packet
    @Test
    public void testUploadPacket() throws IOException {

        //1 准备读取本地文件的输入流
        FileInputStream in = new FileInputStream(new File("d:/lagou.txt"));
        //2 准备好写出数据到hdfs的输出流
        FSDataOutputStream out = fs.create(new Path("/lagou.txt"), new Progressable() {

            //这个progress方法就是每传输64KB(packet)就会执行⼀次,
            public void progress() {

                System.out.println("&");    // 打印了两个 & & , 原因只要有数据, 第一次建立通道时, 就会打印一次
            }                               // 无数据, 则不会建立通道
        });

        //3 实现流拷⻉
        IOUtils.copyBytes(in, out, configuration); //默认关闭流选项是true,所以会自动关闭

        //4 关流 可以再次关闭也可以不关了
    }

240kb 打印了5次

hdfs命令调整日志级别 hdfs操作日志_hdfs命令调整日志级别_16

 

第 6 节 NN与2NN

6.1 HDFS元数据管理机制

问题1:NameNode如何管理和存储元数据?

计算机中存储数据两种:内存或者是磁盘

元数据存储磁盘:存储磁盘⽆法⾯对客户端对元数据信息的任意的快速低延迟的响应,但是安全性⾼

元数据存储内存:元数据存放内存,可以⾼效的查询以及快速响应客户端的查询请求,数据保存在内存,如果断点,内存中的数据全部丢失。

解决⽅案:内存+磁盘;NameNode内存+FsImage的⽂件(磁盘)

新问题:磁盘和内存中元数据如何划分?

两个数据⼀模⼀样,还是两个数据合并到⼀起才是⼀份完整的数据呢?

⼀模⼀样:client如果对元数据进⾏增删改操作,需要保证两个数据的⼀致性。FsImage⽂件操作起来效率也不⾼。

两个合并=完整数据:NameNode引⼊了⼀个edits⽂件(⽇志⽂件:只能追加写⼊)edits⽂件记录的是client的增删改操作,
不再选择让NameNode把数据dump出来形成FsImage⽂件(这种操作是⽐较消耗资源)。

 

元数据管理流程图

hdfs命令调整日志级别 hdfs操作日志_hadoop_17

第⼀阶段:NameNode启动

    第⼀次启动NameNode格式化后,创建Fsimage和Edits⽂件。如果不是第⼀次启动,直接加载编辑⽇志和镜像⽂件到内存。
    客户端对元数据进⾏增删改的请求。
    NameNode记录操作⽇志,更新滚动⽇志。
    NameNode在内存中对数据进⾏增删改。

第⼆阶段:Secondary NameNode⼯作

    Secondary NameNode询问NameNode是否需要CheckPoint(检查点)。直接带回NameNode是否执⾏检查点操作结果。
    Secondary NameNode请求执⾏CheckPoint。
    NameNode滚动正在写的Edits⽇志。
    将滚动前的编辑⽇志和镜像⽂件拷⻉到Secondary NameNode。
    Secondary NameNode加载编辑⽇志和镜像⽂件到内存,并合并。
    ⽣成新的镜像⽂件fsimage.chkpoint。
    拷⻉fsimage.chkpoint到NameNode。
    NameNode将fsimage.chkpoint重新命名成fsimage。

    再次进行相同操作时, 因为2NN本地已经有和NN中fsimage相同的文件, 所以只需要单独拉取edits文件即可

 

6.2 Fsimage与Edits⽂件解析

NameNode在执⾏格式化之后,会在/opt/lagou/servers/hadoop-2.9.2/data/tmp/dfs/name/current⽬录下产⽣如下⽂件

hdfs命令调整日志级别 hdfs操作日志_hdfs命令调整日志级别_18

Fsimage⽂件:是namenode中关于元数据的镜像,⼀般称为检查点,这⾥包含了HDFS⽂件系统所有⽬录以及⽂件相关信息(Block数量,副本数量,权限等信息)

Edits⽂件 :存储了客户端对HDFS⽂件系统所有的更新操作记录,Client对HDFS⽂件系统所有的更新操作都会被记录到Edits⽂件中(不包括查询操作)

seen_txid:该⽂件是保存了⼀个数字,数字对应着最后⼀个Edits⽂件名的数字

VERSION:该⽂件记录namenode的⼀些版本号信息,⽐如:CusterId,namespaceID等

hdfs命令调整日志级别 hdfs操作日志_HDFS_19

hdfs命令调整日志级别 hdfs操作日志_hdfs_20

NameNode启动时会将Fsimage⽂件加载到内存中,同时也把之前未合并元数据的Edits⽂件加载,集合两个⽂件中的元数据这样保证了NameNode中的元数据是最新最全的。通俗点说就是NameNode启动时把Fsimage和Edits⽂件进⾏了合并

 

6.2.1 Fsimage⽂件内容

官⽅地址

https://hadoop.apache.org/docs/r2.9.2/hadoop-project-dist/hadoophdfs/HdfsImageViewer.html

1. 查看oiv和oev命令

[root@linux121 current]$ hdfs

oiv Offline Image Viewer View a Hadoop fsimage INPUTFILE using the specified
PROCESSOR,saving the results in OUTPUTFILE.

oev Offline edits viewer Parse a Hadoop edits log file INPUT_FILE and save results in
OUTPUT_FILE

2. 基本语法
hdfs oiv -p ⽂件类型 -i 镜像⽂件 -o 转换后⽂件输出路径

3. 案例实操

[root@linux121 current]$ cd /opt/lagou/servers/hadoop-2.9.2/data/tmp/dfs/name/current

[root@linux121 current]$ hdfs oiv -p XML -i fsimage_0000000000000000265 -o/opt/lagou/servers/fsimage.xml

[root@linux121 current]$ cat /opt/lagou/servers/fsimage.xml

 

问题:Fsimage中为什么没有记录块所对应DataNode?

在内存元数据中,有记录块所对应的dn信息, 但是fsimage中剔除了这个信息, 在HDFS集群启动后,会加载image和edits文件 ,  block对应的dn信息都没有记录 , 

集群启动时会有一个安全模式,  NameNode要求DataNode上报数据块信息补全元数据,并间隔⼀段时间后再次上报。

hdfs命令调整日志级别 hdfs操作日志_hdfs命令调整日志级别_21

 

6.2.2 Edits⽂件内容

1. 基本语法
hdfs oev -p ⽂件类型 -i编辑⽇志 -o 转换后⽂件输出路径

2. 案例实操

[root@linux121 current]$ hdfs oev -p XML -i edits_0000000000000000266-0000000000000000267 -o /opt/lagou/servers/hadoop-2.9.2/edits.xml

[root@linux121 current]$ cat /opt/lagou/servers/hadoop-2.9.2/edits.xml

备注:Edits中只记录了更新相关的操作,查询或者下载⽂件并不会记录在内!!

问题:NameNode启动时如何确定加载哪些Edits⽂件呢?

需要借助fsimage⽂件最后数字编码,来确定哪些edits之前是没有合并到fsimage中,启动时只需要加载那些未合并的edits⽂件即可。 

hdfs命令调整日志级别 hdfs操作日志_hdfs_22

 

6.3 checkpoint周期

hdfs-default.xml

<!-- 定时⼀⼩时 --> <property> <name>dfs.namenode.checkpoint.period</name> <value>3600</value> </property> <!-- ⼀分钟检查⼀次操作次数,3当操作次数达到1百万时,SecondaryNameNode执⾏⼀次 --> <property> <name>dfs.namenode.checkpoint.txns</name> <value>1000000</value> <description>操作动作次数</description> </property> <property> <name>dfs.namenode.checkpoint.check.period</name> <value>60</value> <description> 1分钟检查⼀次操作次数</description> </property >

 

第 7 节 NN故障处理

NameNode故障后,HDFS集群就⽆法正常⼯作,因为HDFS⽂件系统的元数据需要由NameNode来管理维护并与Client交互,如果元数据出现损坏和丢失同样会导致NameNode⽆法正常⼯作进⽽HDFS⽂件系统⽆法正常对外提供服务。

如果元数据出现丢失损坏如何恢复呢?

1. 将2NN的元数据拷⻉到NN的节点下
    此种⽅式会存在元数据的丢失。checkpoint之间的数据会丢失

2. 搭建HDFS的HA(high availability ⾼可⽤)集群,解决NN的单点故障问题!!(借助Zookeeper实现HA,⼀个ActiveNameNode,⼀个是StandbyNameNode

 

第 8 节 Hadoop的限额与归档以及集群安全模式

HDFS⽂件限额配置

         HDFS⽂件的限额配置允许我们以⽂件⼤⼩或者⽂件个数来限制我们在某个⽬录下上传的⽂件数量或者⽂件内容总量,以便达到我们类似百度⽹盘⽹盘等限制每个⽤户允许上传的最⼤的⽂件的量

数量限额

#查看hdfs管理命令
 hdfs dfsadmin -help#创建hdfs⽂件夹
 hdfs dfs -mkdir -p /user/root/lagou     # 给该⽂件夹下⾯设置最多上传
 hdfs dfsadmin -setQuota 2 /user/root/lagou两个⽂件,上传⽂件,发现只能上传⼀个⽂件
# 清除⽂件数量限制
 hdfs dfsadmin -clrQuota /user/root/lagou 空间⼤⼩限额
 # 限制空间⼤⼩4KB
 hdfs dfsadmin -setSpaceQuota 4k /user/root/lagou#上传超过4Kb的⽂件⼤⼩, 提示⽂件超过限额
 hdfs dfs -put /export/softwares/xxx.tar.gz /user/root/lagou#清除空间限额
 hdfs dfsadmin -clrSpaceQuota /user/root/lagou#查看hdfs⽂件限额数量
 hdfs dfs -count -q -h /user/root/lagou

HDFS的安全模式

安全模式是HDFS所处的⼀种特殊状态,在这种状态下,⽂件系统只接受读数据请求,⽽不接受删除、修改等变更请求。在NameNode主节点启动时,HDFS⾸先进⼊安全模式,DataNode在启动的时候会向NameNode汇报可⽤的block等状态,当整个系统达到安全标准时,HDFS⾃动离开安全模式。如果HDFS出于安全模式下,则⽂件block不能进⾏任何的副本复制操作,因此达到最⼩的副本数量要求是基于DataNode启动时的状态来判定的,启动时不会再做任何复制(从⽽达到最⼩副本数量要求),HDFS集群刚启动的时候,默认30S钟的时间是出于安全期的,只有过了30S之后,集群脱离了安全期,然后才可以对集群进⾏操作。

hdfs dfsadmin -safemode

hdfs命令调整日志级别 hdfs操作日志_hadoop_23

Hadoop归档技术

主要解决HDFS集群存在⼤量⼩⽂件的问题!!

由于⼤量⼩⽂件会占⽤NameNode的内存,因此对于HDFS来说存储⼤量⼩⽂件造成NameNode内存资源的浪费!
Hadoop存档⽂件HAR⽂件,是⼀个更⾼效的⽂件存档⼯具,HAR⽂件是由⼀组⽂件通过archive⼯具创建⽽来,在减少了NameNode的内存使⽤的同时,可以对⽂件进⾏透明的访问,通俗来说就是HAR⽂件对NameNode来说是⼀个⽂件减少了内存的浪费,对于实际操作处理⽂件依然是⼀个⼀个独⽴的⽂件。

hdfs命令调整日志级别 hdfs操作日志_HDFS_24

案例

1. 归档命令查询

[root@linux121 hadoop-2.9.2]$ hadoop archive -help

hdfs命令调整日志级别 hdfs操作日志_HDFS_25

2. 归档⽂件

把/user/lagou/input⽬录⾥⾯的所有⽂件归档成⼀个叫input.har的归档⽂件,并把归档后⽂件存储到/user/lagou/output路径下。

[root@linux121 hadoop-2.9.2]$ bin/hadoop archive -archiveName input.har –p /user/root/input /user/root/output

hdfs命令调整日志级别 hdfs操作日志_hdfs命令调整日志级别_26

 

3. 查看归档

[root@linux121 hadoop-2.9.2]$ hadoop fs -lsr /user/root/output/input.har

hdfs命令调整日志级别 hdfs操作日志_hdfs_27

[root@linux121 hadoop-2.9.2]$ hadoop fs -lsrhar:///user/root/output/input.har

hdfs命令调整日志级别 hdfs操作日志_hdfs_28

 

4. 解归档⽂件

[root@linux121 hadoop-2.9.2]$ hadoop fs -cp har:/// user/root/output/input.har/* /user/root

 

 

第九节 日志采集综合案例

9.1 需求分析

hdfs命令调整日志级别 hdfs操作日志_hdfs命令调整日志级别_29

  • 定时采集已滚动完毕日志文件
  • 将待采集文件上传到临时目录
  • 备份日志文件

 

 

9.2 代码实现

pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>collect_log</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.8.2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-common</artifactId>
            <version>2.9.2</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-client -->
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>2.9.2</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-hdfs-->
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-hdfs</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>compile</scope>
        </dependency>

    </dependencies>
</project>

 

代码实现

LogCollector

package com.ch.collect;

import java.util.Timer;

/**
 * @author CH
 * @date 2021/2/20 15:40
 */
public class LogCollector {

    /*
    - 定时采集已滚动完毕日志文件
    - 将待采集文件上传到临时目录
    - 备份日志文件
    */
    public static void main(String[] args) {

        Timer timer = new Timer();

        //定时采集任务的调度
        // para1: task:采集的业务逻辑, 是个线程
        // para2: 延迟时间
        // para3: 周期时间
        timer.scheduleAtFixedRate(new LogCollectorTask(), 0, 3600 * 1000);
    }

}

 

LogCollectorTask

package com.ch.collect;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimerTask;

/**
 * @author CH
 * @date 2021/2/20 15:41
 */
public class LogCollectorTask extends TimerTask {

    public void run() {

        //采集的业务逻辑 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓

        // 获取今天日期, 以供下面按日期存放使用
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String todayStr = sdf.format(new Date());

        // 1 扫描指定目录,找到待上传文件,原始日志目录
        File logsDir = new File("e:/logs/");
        //使用文件名过滤器拿到文件
        File[] uploadFiles = logsDir.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.startsWith("access.log.");
            }
        });

        //2 把待上传文件转移到临时目录
        //先判断临时目录是否存在,不存在就创建
        File tmpFile = new File("e:/log_tmp/");
        if (!tmpFile.exists()) {
            tmpFile.mkdirs();
        }
        for (File file : uploadFiles) {
            file.renameTo(new File(tmpFile.getPath() + "/" + file.getName()));
        }

        //3 使用hdfs api上传日志文件到指定目录
        Configuration conf = new Configuration();
        conf.set("fs.defaultFS", "hdfs://linux121:9000");
        FileSystem fs = null;
        try {
            fs = FileSystem.get(conf);

            //判断hdfs目标路径是否存在,备份目录是否存在
            Path path = new Path("/collect_log/" + todayStr);
            if (!fs.exists(path)) {
                fs.mkdirs(path);
            }

            File bakFolder = new File("e:/log_bak/" + todayStr);
            if (!bakFolder.exists()) {
                bakFolder.mkdirs();
            }

            File[] files = tmpFile.listFiles();
            for (File file : files) {

                //按照日期分别存放
                fs.copyFromLocalFile(new Path(file.getPath()),    // log_tmp路径下文件
                        new Path(path.getName()         // hdfs上路径
                                + todayStr + "/" + file.getName()));

                //4 上传后的文件转移到备份目录
                file.renameTo(new File(bakFolder.getPath() + "/" + file.getName()));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

 

 

9.3 代码优化

  • 配置文件
  • 单例模式
  • 线程安全

 

LogCollectorTask2

package com.ch.collect;


import com.ch.common.Constant;
import com.ch.singleton.PropTool;
import com.ch.singleton.PropTool2;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
import java.util.TimerTask;

/**
 * @author CH
 * @date 2021/2/20 15:41
 */
public class LogCollectorTask2 extends TimerTask {

    public void run() {

// 优化: 使用类加载器读取配置文件:  prop_old
        Properties prop_old = new Properties();
        try {
            prop_old.load(LogCollectorTask2.class.getClassLoader().getResourceAsStream("collector.properties"));

        } catch (IOException e) {
            e.printStackTrace();
        }

// 以上为基础, 再优化: 由于本类是线程类, 每个线程都要读取一次properties,
// 但实际只需要读取一次就可, 因此可以改造成为单例工具类

        // 方式一:  prop_new1
        Properties prop_new1 = PropTool.getProp();

        // 方式二:
        Properties prop_new2 = null;
        try {
            prop_new2 = PropTool2.getProp();
        } catch (IOException e) {
            e.printStackTrace();
        }


        //采集的业务逻辑 ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓

        Properties prop = prop_new2;

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String todayStr = sdf.format(new Date());

        // 1 扫描指定目录,找到待上传文件,原始日志目录
// 优化: 使用常量来防止写错参数
        File logsDir = new File(prop.getProperty(Constant.LOGS_DIR));
        final String log_prefix = prop.getProperty(Constant.LOG_PREFIX);
        File[] uploadFiles = logsDir.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.startsWith(log_prefix);
            }
        });

        //2 把待上传文件转移到临时目录
        //判断临时目录是否存在,
        File tmpFile = new File(prop.getProperty(Constant.LOG_TMP_FOLDER));
        if (!tmpFile.exists()) {
            tmpFile.mkdirs();
        }
        for (File file : uploadFiles) {
            file.renameTo(new File(tmpFile.getPath() + "/" + file.getName()));
        }

        //3 使用hdfs api上传日志文件到指定目录
        Configuration conf = new Configuration();
        conf.set("fs.defaultFS", "hdfs://linux121:9000");
        FileSystem fs = null;
        try {
            fs = FileSystem.get(conf);
            //判断hdfs目标路径是否存在,备份目录是否存在
            Path path = new Path(prop.getProperty(Constant.HDFS_TARGET_FOLDER) + todayStr);
            if (!fs.exists(path)) {
                fs.mkdirs(path);
            }

            File bakFolder = new File(prop.getProperty(Constant.BAK_FOLDER) + todayStr);
            if (!bakFolder.exists()) {
                bakFolder.mkdirs();
            }
            File[] files = tmpFile.listFiles();
            for (File file : files) {
                //按照日期分门别列存放
                fs.copyFromLocalFile(new Path(file.getPath()), new
                        Path(prop.getProperty(Constant.HDFS_TARGET_FOLDER) + todayStr + "/" + file.getName()));
                //4 上传后的文件转移到备份目录
                file.renameTo(new File(bakFolder.getPath() + "/" + file.getName()));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

常量类Constant

package com.ch.common;

/**
 * @author CH
 * @date 2021/2/20 16:36
 */
public class Constant {

    public static final String LOGS_DIR = "LOGS.DIR";
    public static final String LOG_PREFIX = "LOG.PREFIX";
    public static final String LOG_TMP_FOLDER = "LOG.TMP.FOLDER";
    public static final String HDFS_TARGET_FOLDER = "HDFS.TARGET.FOLDER";
    public static final String BAK_FOLDER = "BAK.FOLDER";
}

 

配置文件 collector.properties

# 优化一: 防止硬编码

# log目录
LOGS.DIR=e:/logs/
# 文件过滤器前缀
LOG.PREFIX=access.log.
# 临时文件夹
LOG.TMP.FOLDER=e:/log_tmp/
# hdfs目标文件夹
HDFS.TARGET.FOLDER=/collect_log/
# 备份文件夹
BAK.FOLDER=e:/log_bak/

 

单例模式1  PropTool1

package com.ch.singleton;

import com.ch.collect.LogCollectorTask2;

import java.io.IOException;
import java.util.Properties;

/**
 * @author CH
 * @date 2021/2/20 16:52
 */
public class PropTool {

    // 单例模式 方式一:

    // 类加载时初始化执行一次即可
    // 使用静态代码块实现, 饿汉式加载
    private static Properties prop = null;


    static {
        prop = new Properties();
        try {
            prop.load(LogCollectorTask2.class.getClassLoader().getResourceAsStream("collector.properties"));

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static Properties getProp() {
        return prop;
    }
}

 

单例模式2 PropTool 2 (线程安全)

package com.ch.singleton;

import com.ch.collect.LogCollectorTask;

import java.io.IOException;
import java.util.Properties;

/**
 * @author CH
 * @date 2021/2/20 15:45
 */
public class PropTool2 {

    // 如果要进一步保证线程安全, 还应添加下面的关键字
    // volatile关键字是java中禁止指令重排序的关键字,保证有序性和可见性
    // java → C → 汇编 的编译过程中, 有可能会进行小范围的指令重排序, 多线程情况下指令重排可能会出错
    private static volatile Properties prop=null;

    // 多个方法进来, 直接执行 new Properties初始化 会出现线程安全问题, 因此加锁
    // 锁加在方法上太重了, 因此改造为将锁加在方法中的代码块上
    public static Properties getProp() throws IOException {

        // 第一个方法执行结束后, prop已经实例化,
        // 后续方法进来时, 不应该在获得锁之后 判断是否已经实例化, 因此先判断
        if(prop == null){
            synchronized ("lock"){
                if(prop == null){
                    prop=new Properties();
                    prop.load(LogCollectorTask.class.getClassLoader()
                            .getResourceAsStream("collector.properties"));
                }
            }
        }
        return prop;
    }
}