分布式存储HDFS必知必会

1 HDFS基本概念的思维导图

在这里插入图片描述

2 使用Java API操作HDFS

2.1 maven依赖

<?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>com.dhhy</groupId>
    <artifactId>hadoopapp</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <encoding>UTF-8</encoding>
    </properties>

    <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.7.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>2.7.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-hdfs</artifactId>
            <version>2.7.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-assembly-plugin </artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <mainClass>com.dhhy.mr.wordcount.WordcountDriver</mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>


    <repositories>
        <repository>
            <id>aliyunmaven</id>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
        </repository>

    </repositories>


</project>

2.2 源码

package com.dhhy.hdfs;

/**
 * Created by JayLai on 2019-06-14 21:41:09
 */

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
import org.apache.hadoop.io.IOUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.net.URI;

/**
 * 使用Java API 操作HDFS
 * Created by JayLai on 2019/04/2019
 */
public class HDFSApp {

    public  static  final String HDFS_PATH = "hdfs://localhost:8020";

    FileSystem fileSystem = null;
    Configuration configuration = null;

    /**
     * 新增文件夹
     */
    @Test
    public void mkdir() throws Exception{
        fileSystem.mkdirs(new Path("/javaapi"));
    }

    /**
     * 上传文件到HDFS
     *
     * @throws Exception
     */
    @Test
    public void copyFromLocalFile() throws Exception {
        Path localPath = new Path("/opt/bigdata/data/hello.txt");
        Path hdfsPath = new Path("/javaapi");
        fileSystem.copyFromLocalFile(localPath, hdfsPath);
    }


    /**
     * 创建文件
     */
    @Test
    public void create() throws Exception {
        FSDataOutputStream output = fileSystem.create(new Path("/javaapi/test.txt"));
        output.write("hello hadoop".getBytes());
        output.flush();
        output.close();
    }


    /**
     * 查看某个目录下的所有文件
     */
    @Test
    public void listFiles() throws Exception {
        FileStatus[] fileStatuses = fileSystem.listStatus(new Path("/"));

        for(FileStatus fileStatus : fileStatuses) {
            String isDir = fileStatus.isDirectory() ? "文件夹" : "文件";
            short replication = fileStatus.getReplication();
            long len = fileStatus.getLen();
            String path = fileStatus.getPath().toString();

            System.out.println(isDir + "\t" + replication + "\t" + len + "\t" + path);
        }

    }



    /**
     * 查看文件内容
     */
    @Test
    public void cat() throws Exception{
        FSDataInputStream in = fileSystem.open(new Path("/javaapi/hello.txt"));
        IOUtils.copyBytes(in, System.out, 1024);
        in.close();
    }



    /**
     * 下载HDFS文件
     */
    @Test
    public void copyToLocalFile() throws Exception {
        Path localPath = new Path("/opt/bigdata/data/hello2.txt");
        Path hdfsPath = new Path("/javaapi/hello.txt");
        fileSystem.copyToLocalFile(hdfsPath, localPath);
    }
    

    /**
     * 删除
     */
    @Test
    public void delete() throws Exception{
        fileSystem.delete(new Path("/javaapi"), true);
    }


    /**
     * 建立链接
     * @throws Exception
     */
    @Before
    public void setUp() throws Exception {
        System.out.println("HDFSApp.setUp");
        configuration = new Configuration();
        #DistributedFileSystem类的实例
        fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration, "hadoop");
    }

    /**
     * 关闭链接
     * @throws Exception
     */
    @After
    public void tearDown() throws Exception {
        configuration = null;
        fileSystem = null;
        System.out.println("HDFSApp.tearDown");
    }

}

3 HDFS的读数据流程

1)客户端获取DistributedFileSystem类的一个实例
2)DistributedFileSystem通过RPC向NameNode请求下载文件,NameNode通过查询元数据,找到文件块所在的DataNode地址。
3)DistributedFileSystem类返回一个FSDataOutputStream对象给客户端以便读取数据。
4)客户端调用read()方法,与网络拓扑中距离最近的文件中第一个block所在DataNode建立连接。DataNode开始传输数据给客户端(从磁盘里面读取数据输入流,以Packet为单位来做校验),客户端以Packet为单位接收,先在本地缓存,然后写入目标文件。
5)读取结束后关闭越datanode的连接,寻找下一块最佳datanode
6)全部读取完毕,FSDataOutputStream调用close()方法。
在这里插入图片描述

4 HDFS的写数据流程

1)客户端获取DistributedFileSystem类的一个实例
2)DistributedFileSystem对NameNode创建一个RPC调用请求上传文件,NameNode检查目标文件是否已存在,父目录是否存在。检查通过后namenode就会为创建新文件记录一条记录。
3)DistributedFileSystem类返回一个FSDataOutputStream对象给客户端以便写数据。
4)客户端请求第一个 Block上传到哪几个DataNode服务器上。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的服务器。(重复执行4-7步)。
在这里插入图片描述

5 HDFS主节点的工作机制

5.1 NameNode

namenode以内存形式存储集群的元数据信息。

5.2 Fsimage和Edits

为了保证元数据信息不丢失,NameNode将元数据备份到磁盘的fsimage(映像文件)。后续客户端发起事务操作时,把这个动作追加到Edits(编辑日志),然后才更新内存中的信息。这样做的好处是速度快和安全。如果只写在内存中修改元素据信息,发生断电则会造成数据丢失。如果所有元数据信息都写入到Fsimage,则速度下降。

5.3 辅助NameNode

如果发生断电,NameNode重启时先加载fsimage到内存中,再执行Edits中的操作来恢复元数据信息。系统运行一段时间后,Edits文件越来越庞大,重启NameNode后执行Edits中的操作时间越会越来越久。因此需要辅助NameNode 定时定量的把集群中的Edits转为 Fsimage 文件。

5.4 检查点触发条件

触发辅助NameNode合并Edits到Fsimages主要有2个条件,可以通过hdfs-default.xml进行修改。

5.4.1 时间

辅助namenode默认每隔一小时执行一次合并操作。

<property>
  <name>dfs.namenode.checkpoint.period</name>
  <value>3600</value>
</property>

5.4.2 edits的大小

edits中记载的事务达到100万个时,也会触发合并,检查edits中事务大小的频率是1分钟。

<property>
    <name>dfs.namenode.checkpoint.txns</name>
    <value>1000000</value>
    <description>操作动作次数</description>
</property>

<property>
  <name>dfs.namenode.checkpoint.check.period</nam>
    <value>60</value>
    <description> 1分钟检查一次操作次数</description>
</property >

5.5 安全模式

nanenode启动时会进入安全模式,此时客户端只能进行非事务性操作,如读取数据。只有整个文件系统中99.9%的快满足最小复本级别(默认为1)时,namenode才会在30后退出安全模式。

<property>
    <name>dfs.namenode.replication.min</name>
    <value>1</value>
</property>

5.6 创建检查点的流程

在这里插入图片描述

备注:红色的操作为辅助NameNode工作流程
黑色的操作为NameNode的工作流程

6 HDFS数据节点的工作机制

6.1 DataNode的工作流程

在这里插入图片描述

6.2 新增数据节点

HDFS集群运行运行时添加新的数据节点。直接启动DataNode,即可关联到集群

hadoop@ubuntu18:/opt/bigdata/hadoop-2.6.0-cdh5.9.3$ sbin/hadoop-daemon.sh start 
datanode
hadoop@ubuntu18:/opt/bigdata/hadoop-2.6.0-cdh5.9.3$ sbin/yarn-daemon.sh start 

6.3 掉线时间

DataNode超过10分钟+30秒(默认值)没有向NameNode汇报心跳信息,则会被NameNode判定为掉线。

掉线时间 = 2 * recheck-interval + 10 * interval,配置中dfs.namenode.heartbeat.recheck-interval单位是毫秒,dfs.heartbeat.interval单位是秒

<property>
    <name>dfs.namenode.heartbeat.recheck-interval</name>
    <value>300000</value>
</property>
<property>
    <name>dfs.heartbeat.interval</name>
    <value>3</value>
</property>

7 HDFS的高可用

7.1 单点故障(SPOF)

单一NameNode存在单点故障问题,因此需要通过两个NameNode消除单点故障。其中一个namenode处于活跃状态,负责集群中所有客户端操作;另外一个处于备用状态。当Aacive namenode发生故障,备份NameNode迅速切换为活跃状态,为客户端提供服务,保证HDFS高可用。

7.2 JournalNode共享存储

主备NameNode直接如何确保数据一致性?答案是通过JournalNode(简称JN)。
Active NameNode将编辑日志写入JN,而Standby NameNode只能读取JN中的数据,从而确保数据同步。

在这里插入图片描述

7.3 主备NameNode的切换

楼上提到主备节点的数据共享,但是还是没有解决主备NameNode的切换问题。
主流解决方案式通过Zookeeper实现主备切换,流程如下所示:
在这里插入图片描述

7.4 组件功能

1)ZKFailoverController
基于Zookeeper的故障转移控制器,负责控制主备NameNode的切换。ZKFailoverController会监测namenode的监控状态,当发现Active NameNode出现异常时会通过Zookeeper进行一次新的选举,完成Active和Standby的切换。

2)HealthMonitor
周期性调用NameNode的HAService RPC接口(monitorHealth和getServiceStatus),监控NameNode的健康状态并向ZKFailoverController反馈。

3)ActiveStandbyElector
接受ZKFC的选举请求,通过ZooKeeper自动完成主备选举,选举完成后回调ZKFailoverController的主备切换方法对NameNode进行Active和Standby状态的切换

4)JournalNode
存储元数据,实现主备NameNode数据同步,Active NameNode写入和Standby NameNode读取通过JN实现元数据同步。当主备切换过程中,从节点如果没有完全同步元数据信息,是不能对外提供服务的。

7.5 脑裂问题(split-brain)

正常情况下,一个Hadoop集群只能有一个Active NameNode,假如系统发生故障(比如网络问题等),导致Standby NameNode的状态也发生改变,成为Active NameNode。此时,两个主节点开始争抢共享资源,导致系统混乱,数据质量无法保证。

7.6 围栏机制(fencing)

Hadoop集群通过JN的 fencing机制,确保只有一个NameNode能写成功。具体流程如下:
1)每个NameNode改变状态的时候,向DataName发送自身的状态和一个序列号。
2) DataNode在运行过程中维护此序列号,当主备切换时,新的NameNode在返回DataNode心跳时会返回自己的Active状态和一个更大的序列号。DataName接收到这个返回是认为该NameNode为新的Active节点。
3) 如果旧的Aactive NameNode(比如发生Full GC导致长时间卡顿)恢复,返回给DataNode的心跳信息包含Active状态和原来的序列号,这时DataNode就会拒绝这个NameNode的命令。

8 HDFS小文件合并

8.1 小文件合并成一个大文件下载到本地

HDFS上的javaapi目录下面有n个小文件,合并后下载到本地,命名为local.txt

hadoop@ubuntu18:/opt/bigdata$ hadoop fs -ls /javaapi
Found 2 items
-rwxrwxrwx   3 hadoop supergroup         13 2020-06-15 00:55 /javaapi/hello.txt
-rw-r--r--   3 hadoop supergroup         12 2020-07-15 21:30 /javaapi/test.txt

hadoop@ubuntu18:/opt/bigdata$ hadoop fs -getmerge /javaapi/*.txt  ./local.txt

8.2 小文件合并成一个大文件后上传到HDFS

/**
 * 合并小文件
 * @throws Exception
 */
@Test
public void mergeFIle() throws Exception{
    FSDataOutputStream outputStream = fileSystem.create(new Path("/javaapi/bigfile.txt"));

    LocalFileSystem localFileSystem = FileSystem.getLocal(new Configuration());

    FileStatus[] fileStatuses = localFileSystem.listStatus(new Path("/opt/bigdata/data/hdfs"));

    //便历每个小文件
    for (FileStatus fileStatus : fileStatuses) {
        //获取每个小文件的输入流
        FSDataInputStream inputStream = localFileSystem.open(fileStatus.getPath());

        //将小文件的数据复制到大文件的输出流
        IOUtils.copyBytes(inputStream, outputStream, 1024);
        inputStream.close();
    }


    localFileSystem.close();
    outputStream.close();
}

9 HDFS的联邦

HDFS的HA解决节点的高可用问题,但是没解决单个NameNode内存容量问题。
每个block会占用NameName约150b内存,随着文件数量的提高,NameNode迟早面临内存耗尽问题。

目前解决方案式通过联邦机制,横向拓展NameNode的个数。
优点是优点:
1)解决单个NameNode的内存不足问题
2)共享存储集群(共享DataNode),提高DataNode的利用率,
缺点:每个NameNode的元数据是隔离的,客户端不知道数据存放在哪个NameNode上,需要在上层做一层封装。

10 HDFS的故障处理

10.1 NameNode发生故障

SecondaryNameNode上也备份着元数据信息,可以将SecondaryNameNode中数据拷贝到NameNode存储数据的目录。具体流程如下:
1)拷贝SecondaryNameNode中数据到原NameNode存储数据目录
2)重新启动NameNode

hadoop@ubuntu18:/opt/bigdata/hadoop-2.6.0-cdh5.9.3$ pwd
/opt/bigdata/hadoop-2.6.0-cdh5.9.3
hadoop@ubuntu18:/opt/bigdata/hadoop-2.6.0-cdh5.9.3$ sbin/hadoop-daemon.sh start 

11 参考文献

1)TomWhite,Hadoop权威指南 第4版. 2017, 清华大学出版
2)社尚硅谷频 http://www.atguigu.com/bigdata_video.shtml

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值