Hadoop - 03 - HDFS分布式文件系统

版权声明:本文为博主原创文章,可以转载,但请注明出处。 https://blog.csdn.net/Simba_cheng/article/details/82765471

目录

HDFS的概念

数据块

NameNode和DataNode

NameNode(Master)

Client客户端

DataNode(Slave)

HA:HDFS的可靠性

HDFS HA原因分析

现有HDFS HA 解决方案

文件数据和元数据

元数据

文件数据

HDFS Java API

数据流

文件读取

文件写入


 HDFS是Hadoop生态中的一个产品、组件,亦可作为单独使用。

这里暂不会对HDFS深入研究,而是从应用的角度去学习。

脚踏实地慢慢啃。

相关代码已经上传GitHub:Hadoop相关完整示例代码,如果觉得还不错,请点Star。

在官方文档的引言中简略的介绍了HDFS:

Hadoop分布式文件系统(HDFS)被设计成适合运行在通用硬件(commodity hardware)上的分布式文件系统。

HDFS是一个高度容错性的系统,适合部署在廉价的机器上。

HDFS能提供高吞吐量的数据访问,非常适合大规模数据集上的应用。

HDFS的概念

数据块

每个磁盘都有默认的数据块大小,这是磁盘进行数据读/写的最小单位。

构建于单个磁盘之上的文件系统(HDFS)通过磁盘块来管理该文件系统中的块,该文件系统块的大小可以是磁盘块的整数倍,磁盘块一般为512字节。

HDFS中同样有块(Block)的概念,但是大的多,默认为64MB。

注:从2.7.3版本开始,官方关于Data Blocks 的说明中,block size由64 MB变成了128 MB。

为何HDFS中的块(block)如此之大?

HDFS的块比磁盘的块大,其目的是为了最小化寻址开销。

NameNode和DataNode

HDFS架构是主/从架构(Master/Slave),即一个NameNode和多个DataNode。

HDFS暴露了文件系统的命名空间,用户能够以文件的形式在上面存储数据。

文件系统命名空间的层次结构和大多数现有的文件系统(Linux)类似:用户可以创建、删除、移动、重命名文件。

[图片来自:《Hadoop实战》]

NameNode(Master)

NameNode存储文件的元数据

NameNode是整个文件系统的管理节点,它负责文件系统'命名空间(NameSpace)'的管理与维护;

比如:打开、关闭、重命名文件或目录,任何对文件系统命名空间或属性的修改都将被NameNode记录下来;

它也负责确定数据块(Block)到具体DataNode节点的映射,同时负责客户端文件操作的控制以及具体存储任务的管理与分配。
 
NameNode维护着文件系统树及整棵树内所有的文件和目录。

这些信息以两个文件形式永久保存在本地磁盘上:命名空间镜像文件 和 编辑日志文件。

NameNode也记录着每个文件中各个块所在的数据节点信息,但它并不永久保存块的位置信息,因为这些信息会在系统启动时由数据节点重建。

Client客户端

它代表用户通过与NameNode与DataNode交互来访问整个文件系统。

Client提供了一个文件系统接口,因此用户在编程时无需知道NameNode和DataNode,也可实现其功能。

DataNode(Slave)

DataNode存储了实际的数据(文件数据)

DataNode提供真实文件数据的存储服务。

负责处理文件系统客户端的读写请求。在NameNode的统一调度下进行数据块的创建、删除和复制。

DataNode是文件系统的工作节点。

它们根据需要存储并检索数据块(受客户端或NameNode调度),并定期向NameNode发送它们所存储的块列表。

从 图9-1 可以看出,客户端Client联系NameNode,以获取文件的元数据或修饰属性,而真正的文件I/O操作是直接和DataNode进行交互的。

HA:HDFS的可靠性

从上面对NameNode 和 DataNode的了解之后,可以想到:没有NameNode或NameNode故障后,文件系统将无法使用。

事实上,如果运行NameNode服务的机器损坏,文件系统上所有的文件将会丢失。

因此对NameNode实现容错非常重要,Hadoop为此提供了两种机制:

    • 备份数据持久状态元文件

    • 运行一个辅助的NameNode。

(此小节内容直接摘选复制,详细请查阅原书《HDFS—Hadoop分布式文件系统深度实践》)

HA的英文全称是 High Availability ,中文翻译为高可用性。

HA的定义为系统对外正常提供服务时间的百分比。

HA更多地是从系统对外的角度来说的,除了包含系统正常工作的能力,它还强调系统中止服务后迅速恢复的能力:一个可靠性很高的系统,如果其中止服务后,修复时间很长,那么它的可用性也不会很高,而一个可靠性不是特别高的系统,如果发生中止服务后,可迅速恢复,那么其可用性也可能会很高。

因此只有HA才能准确度量系统对外正常服务的能力。

HDFS HA原因分析

可靠性

我们知道,HDFS由NameNode和DataNode两类节点组成,由于NameNode只有1个,且负责整个HDFS文件系统的管理和控制,因此当NameNode不能提供正常服务时,会直接导致HDFS不能对外正常服务,因此NameNode的可靠性是影响HDFS可靠性的重要因素。

由于NameNode只有一个,它的正常运行与否直接决定了HDFS能否正常服务,因此它也就成为了HDFS 系统的一个单一故障点(single point of failure——SPOF)

DataNode负责存储真实文件数据,每个文件可以指定副本个数,因此同一个文件可在多个DataNode上进行存储,当DataNode发生故障时,客户端可以访问其他DataNode上的副本,因此DataNode发生故障并不会影响HDFS对外正常服务。

可维护性

当NameNode不能正常服务时,通常需要重新启动NameNode来恢复服务,NameNode启动时需要加载磁盘上的元数据文件:

如果此时元数据没有损坏,那么直接启动NameNode就可以恢复HDFS对外正常服务;

如果元数据损坏,将导致NameNode无法启动,无法再对外正常服务,也就是说平均维护时间是无限大。

因此元数据的可靠性决定了HDFS的可维护时间。

当DataNode无法正常工作时,HDFS会自动启动该DataNode上所有数据的复制任务,将丢失的数据重新分布到其他DataNode上,因此DataNode并不影响HDFS的可维护性。

综上所述,HDFS的HA主要由NameNode的HA决定,NameNode的可靠性主要取决于自身计算机硬件系统的可靠性、系统软件以及HDFS 软件的可靠性;

NameNode的可维护性则取决于元数据的可靠性以及NameNode服务恢复时间。

现有HDFS HA 解决方案

    1. Hadoop元数据备份

    2. Secondary NameNode方法

    3. Hadoop的Backup Node方案

    4. DRDB机制进行元数据备份

详情可以查阅原书。

文件数据和元数据

HDFS中有两种数据:文件数据和元数据

元数据

元数据是指'数据'的数据,简单的说就是 'DataNode文件数据'的相关信息。

HDFS的元数据就是指维护HDFS文件系统中的文件和目录所需要的信息。

注意:

DataNode中具体的文件内容并不是元数据,元数据的可用性直接决定了HDFS的可用性。

从形式上讲,元数据可分为 内存元数据 和 元数据文件 两种。

内存元数据:NameNode在内存中维护整个文件系统的元数据镜像,用于HDFS的管理;

元数据文件:用于持久化存储。
 
从类型上讲,元数据有三类重要的信息:

一、第1类是 文件和目录自身的属性信息,例如:文件名、目录名、父目录信息、文件大小、创建时间等。

二、第2类是记录文件内容(Block)存储的相关信息,例如:文件分块情况、副本个数、每个副本所在的DataNode信息等。

三、第三类用来记录HDFS中所有DataNode的信息,用于DataNode管理。

元数据的主要来源于NameNode磁盘上的元数据文件,以及各个DataNode的上报信息。

文件数据

文件数据指用户保存在HDFS上文件的具体内容。

HDFS将用户保存的文件按照大小进行分块(一块简称为一个Block),并保存在各个DataNode上,每一个块(Block)可能会有多个副本,具体数量可以设置;

相同块对应的副本通常保存在不同的DataNode上,这样可以有效保证文件数据块的可靠性。

注意:从2.7.3版本开始,官方关于Data Blocks 的说明中,block size由64 MB变成了128 MB。

HDFS Java API

Hadoop文件系统中通过Hadoop Path对象来代表文件。

可以将路径视为一个Hadoop文件系统URL,例如:/HDFS-JAVA-API-Demo-01/input/hdfs-test02.txt

(图片来自网络)

相关API 挺简单的,下面直接贴代码:

package com.server;

import com.server.conf.LoadConfig;
import org.apache.commons.io.IOUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.BlockLocation;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.PathFilter;
import org.apache.hadoop.hdfs.DistributedFileSystem;
import org.apache.hadoop.hdfs.protocol.DatanodeInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Java API 操作HDFS
 *
 * @author CYX
 */
public class HDFSApp {

    private static final Logger LOGGER = LoggerFactory.getLogger(HDFSApp.class);

    private static final String HDFS_PATH = "hdfs://192.168.137.160:9000";

    public static void main(String[] args) {

        try {
            LoadConfig.getInstance();
        } catch (Exception e) {
            System.exit(1);
        }

        try {

            //createFile();

            //copyFromLocalFile();

            //moveFromLocalFileAndDelete();

            //deleteDirectory();

            //isDirectory();

            //exist();

            //findAllDirectory();

            //createDirectory();

            //createMultiDirectory();

            //readerFile();

            //createMultiDirectory();

            //appendDataToHDFSFile();

            //globStatus();

            //renameHDFSFile();
            
            //getModificationTime();
            
            //getFileBlockLocation();
            
            //getHostName();
            
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }

    }

    /**
     * 创建一个文件,并写入数据
     */
    private static void createFile() {
        FSDataOutputStream fsDataOutputStream = null;
        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);
            fsDataOutputStream = fileSystem.create(new Path("/HDFS-JAVA-API-Demo-01/input/test01.txt"));
            fsDataOutputStream.writeUTF("HelloWorld");
            fsDataOutputStream.flush();
            LOGGER.info("创建文件并写入数据成功");
        } catch (Exception e) {
            LOGGER.error("创建文件失败");
            LOGGER.error(e.getMessage(), e);
        } finally {
            try {
                fsDataOutputStream.close();
            } catch (IOException e) {
                LOGGER.error(e.getMessage(), e);
            }
        }
    }

    /**
     * 从本地拷贝文件到HDFS
     */
    private static void copyFromLocalFile() {

        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);
            Path src = new Path("./file/test02.txt");
            Path dst = new Path("/HDFS-JAVA-API-Demo-01/input/");
            fileSystem.copyFromLocalFile(src, dst);
            LOGGER.info("本地文件拷贝至HDFS成功");
        } catch (Exception e) {
            LOGGER.error("拷贝失败");
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * 移动本地文件到HDFS,同时删除本地文件
     */
    private static void moveFromLocalFileAndDelete() {

        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);

            Path src = new Path("./file/test02.txt");
            Path dst = new Path("/HDFS-JAVA-API-Demo-01/input/hdfs-test02.txt");

            fileSystem.moveFromLocalFile(src, dst);
            LOGGER.info("移动文件成功");

        } catch (Exception e) {
            LOGGER.error("移动文件失败");
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * 递归删除某个文件或某个文件夹
     */
    private static void deleteDirectory() {

        boolean result;
        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);
            result = fileSystem.delete(new Path("/HDFS-JAVA-API-Demo-01"), true);
            LOGGER.info("result : {}", new Object[]{result});
        } catch (Exception e) {
            LOGGER.error("删除失败");
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * 查看某个路径是目录还是文件
     */
    private static void isDirectory() {

        boolean result = false;
        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);

            Path path = new Path("/HDFS-JAVA-API-Demo-01/");
            result = fileSystem.isDirectory(path);

        } catch (Exception e) {
            LOGGER.error("检查异常");
            LOGGER.error(e.getMessage(), e);
        }
        LOGGER.info("该路径是否是目录:{}", new Object[]{result});
    }

    /**
     * 查看某个路径是否存在
     */
    private static void exist() {

        boolean result = false;
        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);
            Path path = new Path("/HDFS-JAVA-API-Demo-01/output");
            result = fileSystem.exists(path);
        } catch (Exception e) {
            LOGGER.error("检查异常");
            LOGGER.error(e.getMessage(), e);
        }
        LOGGER.info("路径是否存在:{}", new Object[]{result});
    }


    /**
     * 查找指定目录下的子目录
     *
     * @throws Exception
     */
    private static void findAllDirectory() throws Exception {
        Configuration configuration = new Configuration();
        FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);
        FileStatus[] listFiles = fileSystem.listStatus(new Path("/"));

        FileStatus fileStatus;
        if (null != listFiles && listFiles.length > 0) {
            for (int i = 0; i < listFiles.length; i++) {
                fileStatus = listFiles[i];
                LOGGER.info("path : {} ", new Object[]{fileStatus.toString()});
            }
        }
    }

    /**
     * 创建单级目录
     *
     * @throws Exception
     */
    private static void createDirectory() {
        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);
            fileSystem.mkdirs(new Path("/HDFS-JAVA-API-Demo-01"));
            LOGGER.info("创建目录成功");
        } catch (Exception e) {
            LOGGER.error("创建目录失败");
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * 创建多级目录
     *
     * @throws Exception
     */
    private static void createMultiDirectory() {
        boolean result;
        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);
            result = fileSystem.mkdirs(new Path("/HDFS-JAVA-API-Demo-01/input"));
            LOGGER.info("result : {}", new Object[]{result});
        } catch (Exception e) {
            LOGGER.error("创建目录失败");
            LOGGER.error(e.getMessage(), e);
        }
    }


    /**
     * 读取指定文件的数据
     *
     * @throws Exception
     */
    private static void readerFile() throws Exception {
        FSDataInputStream fsDataInputStream = null;
        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);
            fsDataInputStream = fileSystem.open(new Path("/HDFS-JAVA-API-Demo-01/input/test02.txt"));
            String fileResult = IOUtils.toString(fsDataInputStream, "UTF-8");
            LOGGER.info("fileResult : {}", new Object[]{fileResult});
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        } finally {
            fsDataInputStream.close();
        }
    }

    /**
     * 向HDFS文件上重写内容
     * <p>
     * 向HDFS文件上追加内容,需要修改配置文件内容,这里不测试了
     * https://www.iteblog.com/archives/881.html
     */
    private static void appendDataToHDFSFile() {

        FSDataOutputStream fsDataOutputStream = null;

        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);
            Path path = new Path("/HDFS-JAVA-API-Demo-01/input/hdfs-test02.txt");
            fsDataOutputStream = fileSystem.append(path);
            fsDataOutputStream.writeUTF("9998885557744");
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        } finally {
            try {
                fsDataOutputStream.close();
            } catch (IOException e) {
                LOGGER.error(e.getMessage(), e);
            }
        }
    }

    /**
     * 根据某些规则,列出指定路径下的文件
     */
    private static void globStatus() {

        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);
            Path allPath = new Path("/*/*/");

            FileStatus[] fileStatuses = fileSystem.globStatus(allPath, new PathFilter() {
                @Override
                public boolean accept(Path path) {
                    String contidion = "01";
                    return path.toString().contains(contidion);
                }
            });

            for (int i = 0; i < fileStatuses.length; i++) {
                FileStatus statuses = fileStatuses[i];
                LOGGER.info("statuses : {}", new Object[]{statuses});
            }
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }

    }

    /**
     * 重命名HDFS上的文件、目录
     */
    private static void renameHDFSFile() {
        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);

            Path src = new Path("/HDFS-JAVA-API-Demo-01/input");
            Path dst = new Path("/HDFS-JAVA-API-Demo-01/inputxx");
            boolean result = fileSystem.rename(src, dst);
            LOGGER.info("result : " + result);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }
    
   /**
     * 查看HDFS文件的最后修改时间
     */
    private static void getModificationTime() {

        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);

            Path src = new Path("/HDFS-JAVA-API-Demo-01/inputxx");

            FileStatus fileStatus = fileSystem.getFileStatus(src);
            long time = fileStatus.getModificationTime();
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String timeStr = dateFormat.format(new Date(time));
            LOGGER.info("modificationTime : {}", new Object[]{timeStr});
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }
    
    /**
     * 查找某个文件在HDFS集群的位置
     */
    private static void getFileBlockLocation() {

        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);

            Path path = new Path("/HDFS-JAVA-API-Demo-01/inputxx/testxx.txt");
            FileStatus fileStatus = fileSystem.getFileStatus(path);
            
            BlockLocation[] blockLocations = fileSystem.getFileBlockLocations(fileStatus, 0L, fileStatus.getLen());

            String[] hosts;
            for (int i = 0; i < blockLocations.length; i++) {
                hosts = blockLocations[i].getHosts();
                LOGGER.info("block_" + i + "_location:" + hosts[0]);
            }
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }
    
    /**
     * 获取HDFS集群上所有节点名称信息
     */
    private static void getHostName() {

        try {
            Configuration configuration = new Configuration();
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration);

            DistributedFileSystem hdfs = (DistributedFileSystem) fileSystem;
            DatanodeInfo[] datanodeInfos = hdfs.getDataNodeStats();

            for (int i = 0; i < datanodeInfos.length; i++) {
                LOGGER.info("DataNode_" + i + "_Name:" + datanodeInfos[i].getHostName());
            }

        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }
    
}

数据流

文件读取

了解客户端及与之交互的HDFS、NameNode和DataNode之间的数据流是什么样的。

客户端通过调用FileSystem对象的open()方法打开希望读取的文件。

(步骤1、2)

DistributedFileSystem通过RPC调用NameNode,获得起始块的位置。

DistributedFileSystem会返回一个FSDataInputStream对象。

FSDataInputStream对象封装着DFSInputStream对象,该对象管理着DataNode和NameNode的I/O。

(步骤3/4)

接着客户端调用DFSIputStream对象的read()方法。

存储着文件起始几个块的DataNode地址的DFSInputStream随机连接距离最近的DataNode。

通过对数据流反复调用read()方法,可以将数据从DataNode传输到客户端。

(步骤5)

到达块的末端时,DFSIputStream关闭与该DataNode的连接,然后寻找下一个块的最佳DataNode。

客户端只需要读取连续的流,对于客户端都是透明的。

(步骤6)

客户端从流中读取数据时,块是按照打开DSFInputStream与DataNode新建连接的顺序读取的。

它也会根据需要询问NameNode来检索下一批数据块的DataNode的位置。

一旦客户端完成读取,就对FSDataInputStream调用Close()方法。

简单的来说:

    Client向NameNode发送读取请求。

    NameNode返回数据块(Block)在DataNode上的位置。

    block位置有顺序的,按照顺序,依次读取(优先读取本机架的数据)。

文件写入

(网上看到一篇不错的博文,http://www.daniubiji.cn/archives/596 精简一下)

有一个文件FileA,100MB,Client将FileA写到HDFS上。

HDFS按照默认进行配置。同时 HDFS分布在三个机架上。

    1. Client 将FileA按64M分成两块:block1(64MB) 和 block2(36MB)    

    2. Client 向NameNode发送请求,然后NameNode返回可用的DataNode信息。

                    block1 : host2 -----> host1 -----> host3

                    block2 : host7 -----> host8 -----> host4

    3. Client向DataNode发送block;发送过程是流式写入。

    具体过程:

        a. Client将block1(64MB)按64K块划分,然后将第一个64K块发送给host2。

        b. host2接收Client完成之后,host2的DataNode将第一个64K块同步发送给host1,同时,Client继续向host2发送第二个64K块

        c. host1接收完第一个64K块后,接着同步给host3,同时同步接收来自host2的第二个64K块

        d. 依次类推,直到将block1发送完毕。

        e. host2、host1、host3向NameNode发送通知host2向Client发送通知,说"发送完毕"。(疑问?为啥是host2向Client报告?)

        f. Client接收到host2的消息后,向NameNode发送消息,说写完了。这样,block1就彻底发送结束了

        g. 接着继续发送block2,host7、host8、host4向NameNode发送通知,host7向Client发送通知,然后Client向NameNode发送消息,block2也彻底结束了。

这样就完毕了。

(最近工作太忙,没有时间往下扣,暂时将这个问题记录下来)

问题:

    1. 为什么block1全部写完之后,host2要向Client发送通知,而不是host1 或 host3?

    2. block1写完之后,host2、host1、host3向NameNode发送了什么?

参考资料:

http://hadoop.apache.org/docs/

http://www.daniubiji.cn/archives/596

《HDFS—Hadoop分布式文件系统深度实践》

猜你喜欢

转载自blog.csdn.net/Simba_cheng/article/details/82765471
今日推荐