万字长文带你了解大数据基石之Hadoop

509 阅读36分钟

一、分布式文件存储系统—HDFS

(1)、介绍

HDFSHadoop Distributed File System)是Hadoop下的分布式文件系统,具有高容错(fault-tolerant)、高吞吐量(high throughput)等特性,可以部署在低成本(low-cost)的硬件上。

(2)、HDFS 设计原理

2.1 HDFS 架构

HDFS 遵循主/从架构,由单个 NameNode(NN) 和多个 DataNode(DN) 组成:

  • NameNode : 负责执行有关 文件系统命名空间 的操作,例如打开,关闭、重命名文件和目录等。它同时还负责集群元数据的存储,记录着文件中各个数据块的位置信息。
  • DataNode:负责提供来自文件系统客户端的读写请求,执行块的创建、删除等操作。

2.2 文件系统命名空间

HDFS 的 文件系统命名空间 的层次结构与大多数文件系统类似 (如 Linux), 支持目录和文件的创建、移动、删除和重命名等操作,支持配置用户和访问权限,但不支持硬链接和软连接。NameNode 负责维护文件系统名称空间,记录对名称空间或其属性的任何更改。

2.3 数据复制

由于 Hadoop 被设计运行在廉价的机器上,这意味着硬件是不可靠的,为了保证容错性,HDFS 提供了数据复制机制。HDFS 将每一个文件存储为一系列,每个块由多个副本来保证容错,块的大小和复制因子可以自行配置(默认情况下,块大小是 128M,默认复制因子是 3)。

2.4 数据复制的实现原理

大型的 HDFS 实例在通常分布在多个机架的多台服务器上,不同机架上的两台服务器之间通过交换机进行通讯。在大多数情况下,同一机架中的服务器间的网络带宽大于不同机架中的服务器之间的带宽。因此 HDFS 采用机架感知副本放置策略,对于常见情况,当复制因子为 3 时,HDFS 的放置策略是:

在写入程序位于 datanode 上时,就优先将写入文件的一个副本放置在该 datanode 上,否则放在相对空闲datanode 上。之后在该机架上的另一个节点上的任意一个节点上放置另一个副本,并在另一个远程机架上放置最后一个副本。此策略可以减少机架间的写入流量,从而提高写入性能。

如果复制因子大于 3,则随机确定第 4 个和之后副本的放置位置,同时保持每个机架的副本数量低于上限,上限值通常为 (复制系数 - 1)/机架数量 + 2,需要注意的是不允许同一个 dataNode 上具有同一个块的多个副本。

2.5 副本的选择

为了最大限度地减少带宽消耗和读取延迟,HDFS 在执行读取请求时,优先读取距离读取器最近的副本。如果在与读取器节点相同的机架上存在副本,则优先选择该副本。如果 HDFS 群集跨越多个数据中心,则优先选择本地数据中心上的副本。

2.6 架构的稳定性

1. 心跳机制和重新复制

每个 DataNode 定期(默认3秒)向 NameNode 发送心跳消息,如果超过指定时间没有收到心跳消息,则将 DataNode 标记为死亡。NameNode 不会将任何新的 IO 请求转发给标记为死亡的 DataNode,也不会再使用这些 DataNode 上的数据。 由于数据不再可用,可能会导致某些块的复制因子小于其指定值,NameNode 会跟踪这些块,并在必要的时候进行重新复制。

2. 数据的完整性

由于存储设备故障等原因,存储在 DataNode 上的数据块也会发生损坏。为了避免读取到已经损坏的数据而导致错误,HDFS 提供了数据完整性校验机制来保证数据的完整性,具体操作如下:

当客户端创建 HDFS 文件时,它会计算文件的每个块的 校验和,并将 校验和 存储在同一 HDFS 命名空间下的单独的隐藏文件中。当客户端检索文件内容时,它会验证从每个 DataNode 接收的数据是否与存储在关联校验和文件中的 校验和 匹配。如果匹配失败,则证明数据已经损坏,此时客户端会选择从其他 DataNode 获取该块的其他可用副本。

3.元数据的磁盘故障

FsImageEditLog 是 HDFS 的核心数据,这些数据的意外丢失可能会导致整个 HDFS 服务不可用。为了避免这个问题,可以配置 NameNode 使其支持 FsImageEditLog 多副本同步,这样 FsImageEditLog 的任何改变都会引起每个副本 FsImageEditLog 的同步更新。

4.支持快照

快照支持在特定时刻存储数据副本,在数据意外损坏时,可以通过回滚操作恢复到健康的数据状态。

(3)、HDFS 的特点

3.1 高容错

由于 HDFS 采用数据的多副本方案,所以部分硬件的损坏不会导致全部数据的丢失。

3.2 高吞吐量

HDFS 设计的重点是支持高吞吐量的数据访问,而不是低延迟的数据访问。

3.3 大文件支持

HDFS 适合于大文件的存储,文档的大小应该是是 GB 到 TB 级别的。

3.4 简单一致性模型

HDFS 更适合于一次写入多次读取 (write-once-read-many) 的访问模型。支持将内容追加到文件末尾,但不支持数据的随机访问,不能从文件任意位置新增数据。

3.5 跨平台移植性

HDFS 具有良好的跨平台移植性,这使得其他大数据计算框架都将其作为数据持久化存储的首选方案。

附:图解HDFS存储原理

说明:以下图片引用自博客:翻译经典 HDFS 原理讲解漫画

1. HDFS写数据原理

2. HDFS读数据原理

3. HDFS故障类型和其检测方法

第二部分:读写故障的处理

第三部分:DataNode 故障处理

副本布局策略

二、分布式计算框架—MapReduce

练习代码下载地址:MR-WordCount

(1)、MapReduce概述

Hadoop MapReduce 是一个分布式计算框架,用于编写批处理应用程序。编写好的程序可以提交到 Hadoop 集群上用于并行处理大规模的数据集。

MapReduce 作业通过将输入的数据集拆分为独立的块,这些块由 map 以并行的方式处理,框架对 map 的输出进行排序,然后输入到 reduce 中。MapReduce 框架专门用于 <key,value> 键值对处理,它将作业的输入视为一组 <key,value> 对,并生成一组 <key,value> 对作为输出。输入和输出的 keyvalue 都必须实现Writable 接口。

(input) <k1, v1> -> map -> <k2, v2> -> combine -> <k2, v2> -> reduce -> <k3, v3> (output)

1. MapReduce优缺点

优点

1)MapReduce易于编程。它简单的实现一个接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的PC机器上运行。

2)良好的扩展性。当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。

3)高容错性。MapReduce设计的初衷就是使程序能够部署在廉价的PC机器上,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由Hadoop内部完成的。

4)适合PB级别以上海量数据的离线处理

缺点

MapReduce不擅长做实时计算、流式计算、DAG(有向图)计算。

1)实时计算。MapReduce无法像MySQL一样在毫秒或秒级内返回结果。

2)流式计算。流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化。

3)DAG(有向图)计算。多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce并不是不能做,而是使用后,每个MapReduce作业的输出结果都会写入磁盘,会造成大量磁盘IO,导致性能非常的低下。

2. MapReduce进程

一个完整的MapReduce程序在分布式运行时有三类实例进程:

1. MrAppMaster:负责整个程序的过程调度以及状态协调。

2. MapTask:负责map阶段的整个数据处理流程。

3. ReduceTask:负责reduce阶段的整个数据处理流程。

(2)、Hadoop序列化

2.1 序列化概述

序列化就是把内存中的对象转换成字节序列(或其它数据传输协议)以便于存储(持久化)和网络传输。

反序列化就是将收到字节序列(或其它数据传输协议)或者是磁盘的持久化数据转换成内存中的对象。

为什么需要序列化?

一般来说,“活的”对象只生存在内存里,关机断电就消失了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。然而序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算机。

Hadoop中为什么不使用Java的序列化

Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息、header、继承体系等),不便于在网络中高效传输。

Hadoop序列化机制(Writable)特点

  • 紧凑:紧凑的格式能让我们充分利用网络带宽,而带宽是数据中心最稀缺的资源
  • 快速:进程通信形成了分布式系统的骨架,所以需要尽量减少序列化和反序列化的性能开销
  • 可扩展:协议为了满足新的需求变化,所以控制客户端和服务器过程中,需要直接引进相应的协议,这些是新协议,原序列化方式能支持新的协议报文
  • 互操作:能支持不同语言写的客户端和服务端进行交互

2.2 常用数据序列化类型

Java中常用的数据类型对应的Hadoop数据序列化类型

Java类型 Hadoop Writable类型
byte ByteWritable
int IntWritable,VIntWritable(可变长度)
long LongWritable,VLongWritable(可变长度)
float FloatWritable
double DoubleWritable
boolean BooleanWritable
String Text
Map MapWritable
Array ArrayWritable
null NullWritable

2.3 实现数据排序

输入数据

4
5
6
7
11
1
2
8
99
8
4
200
1
3
4
5

输出数据

1
2
3
4
5
6
7
8
11
99
200

自定义Mapper

//自定义Mapper
    static class SortMapper extends Mapper<LongWritable, Text, IntWritable, NullWritable> {
        IntWritable intWritable = new IntWritable();

        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
            logger.info("map接收》》》每行数据偏移量(行号):" + key + "--" + value.toString());
            intWritable.set(Integer.parseInt(value.toString()));
            logger.info("map一行:" + Integer.parseInt(value.toString()) + "-xiaokang");
            //将结果输出到环形缓冲区context(默认大小100m)
            context.write(intWritable, NullWritable.get());
        }
    }

自定义Reducer

//自定义Reducer
    static class SortReducer extends Reducer<IntWritable, NullWritable, IntWritable, NullWritable> {
        IntWritable intWritable = new IntWritable();
        @Override
        protected void reduce(IntWritable key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
            intWritable.set(key.get());
            context.write(key, NullWritable.get());
        }
    }

Driver

public static void main(String[] args) {
        //  文件输入路径和输出路径由外部传参指定
        if (args == null || args.length < 2) {
            System.out.println("Input and Output paths are necessary!");
            return;
        }
        //编写Driver类
        try {
            //配置类,指定HDFS的路径
            Configuration conf = new Configuration();
            conf.set("fs.defaultFS", HDFS_URL);
            //创建一个Job作业
            Job job = Job.getInstance(conf, "数据排序-微信公众号:小康新鲜事儿");
            job.setJarByClass(SortNum.class);
            //设置Mapper
            job.setMapperClass(SortMapper.class);
            job.setMapOutputKeyClass(IntWritable.class);
            job.setMapOutputValueClass(NullWritable.class);
            //设置Reducer
            job.setReducerClass(SortReducer.class);
            job.setOutputKeyClass(IntWritable.class);
            job.setOutputValueClass(NullWritable.class);
            //如果输出目录已存在,则先删除,否则程序运行时会抛出异常
            FileSystem fs = FileSystem.get(new URI(HDFS_URL), conf, HDFS_USER);
            Path outputPath = new Path(args[1]);
            if (fs.exists(outputPath)) {
                fs.delete(outputPath, true);
            }
            //指定作业的输入文件和输出结果文件的路径
            FileInputFormat.setInputPaths(job, new Path(args[0]));
            FileOutputFormat.setOutputPath(job, outputPath);
            //将作业提交到集群并等待完成,参数设置true代表打印显示对应的进度
            boolean result = job.waitForCompletion(true);
            if (result) {
                System.out.println(job.getJobName() + ">>>execute success!");
            } else {
                System.out.println(job.getJobName() + ">>>execute failed!");
            }
        } catch (Exception e) {
            System.out.println("程序执行异常" + e.getMessage());
        }
    }

2.4 自定义Bean对象实现序列化接口(Writable)

自定义的Bean对象要想序列化传输,必须实现序列化接口,需要注意以下7项

1、必须实现Writable接口

2、反序列化时,需要反射调用空参构造函数,所以必须有空参构造

3、重写序列化方法

4、重写反序列化方法

5、反序列化的顺序必须和序列化的顺序完全一致

6、后期为了结果的展示,需要重写toString方法

7、如果需要将自定义的Bean放在key中传输,则还需要实现WritableComparable接口(shuffle过程一定会对key进行排序)

package cool.xiaokang.pojo;

import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

/**
 * 序列化案例
 *
 * @author xiaokang
 */
//1。实现Writable接口,自定义比较规则的话也要实现WritableComparable接口
public class Transaction implements Writable, WritableComparable<Transaction> {
    //交易金额
    private double tradeMoney;
    //退款金额
    private double refundMoney;
    //评分
    private int mark;
    //平均交易金额
    private double sumTradeMoney;
    //平均退款金额
    private double sumRefundMoney;
    //平均评分
    private double avgMark;
    //手机号
    private String phoneNum;

    //2.反序列化时需要反射调用空构造方法
    public Transaction() {
    }

    //3.重写序列化方法
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeDouble(tradeMoney);
        out.writeDouble(refundMoney);
        out.writeInt(mark);
        out.writeDouble(sumTradeMoney);
        out.writeDouble(sumRefundMoney);
        out.writeDouble(avgMark);
        out.writeUTF(phoneNum);
    }

    //4.重写反序列化方法
    //5.反序列方法读顺序和写序列方法的写顺序必须一致
    @Override
    public void readFields(DataInput in) throws IOException {
        tradeMoney = in.readDouble();
        refundMoney = in.readDouble();
        mark = in.readInt();
        sumTradeMoney = in.readDouble();
        sumRefundMoney = in.readDouble();
        avgMark = in.readDouble();
        phoneNum = in.readUTF();
    }


    //6.重写toString,方便后续打印到文本
    @Override
    public String toString() {
        return sumTradeMoney + "|" + sumRefundMoney + "|" + avgMark;
    }

    @Override
    public int compareTo(Transaction o) {
        //按总退款金额降序
        return this.sumRefundMoney > o.sumRefundMoney ? -1 : 1;
    }
}

(3)、MapReduce编程模型以及Shuffle完整过程

这里以词频统计为例进行说明,MapReduce 处理的流程如下:

  1. Input : 读取文本文件;
  2. Splitting : 将文件按照行进行拆分,此时得到的 K1 行数,V1 表示对应行的文本内容;
  3. Mapping : 并行将每一行按照空格进行拆分,拆分得到的 List(K2,V2),其中 K2 代表每一个单词,由于是做词频统计,所以 V2 的值为 1,代表出现 1 次;
  4. Shuffling:由于 Mapping 操作可能是在不同的机器上并行处理的,所以需要通过 shuffling 将相同 key 值的数据分发到同一个节点上去合并,这样才能统计出最终的结果,此时得到 K2 为每一个单词,List(V2) 为可迭代集合,V2 就是 Mapping 中的 V2;
  5. Reducing : 这里的案例是统计单词出现的总次数,所以 ReducingList(V2) 进行归约求和操作,最终输出。

MapReduce 编程模型中 splittingshuffling 操作都是由框架实现的,需要我们自己编程实现的只有 mappingreducing,这也就是 MapReduce 这个称呼的来源。

MapReduce完整的Shuffle过程

1、Map端的Shuffle

①首先,MapTask的结果输出到默认大小为100MB的环形内存缓冲区中,保存的是key/value,同时进行二次排序,首先根据数据所属的Partition进行分区,然后每个Partition中的数据再按key来排序(默认字典顺序)。

②接着就会运行Combiner(假如有设置的话),Combiner的本质是一个Reducer,其目的是对将要写入磁盘的文件先进行一次本地处理,这样写入磁盘的数据量就会减少很多。

③当写入的数据达到设定的阈值时(默认唤醒内存缓冲区内存的80%),系统会启动一个线程将缓冲区的数据写到磁盘(spill-溢写)产生spill文件(spill文件保存在mapred.local.dir指定的目录,MapTask结束后就会被删除),如果有压缩设置,则还会对数据进行压缩操作。

④等MapTask任务的数据处理完成之后(多个spill文件),会对所有Map产生的数据结果进行一次合并操作(多路归并算法),以确保一个MapTask最终只产生一个中间数据文件。

2、Reduce端的Shuffle

ReduceTask在执行之前会不断地拉取当前Job中每个MapTask的最终结果,然后对这些数据不断地进行merge,最终形成多个分区文件作为ReduceTask的输入文件

①Map端进行Partition的时候,就相当于指定了每个Reducer要处理的数据,所以Reducer只需拷贝与自己对应的Partition中的数据即可。ReduceTask从各个MapTask上远程拷贝数据,这些数据默认会保存在内存的缓冲区中,当内存的缓冲区达到一定的阀值的时候,就会将数据写到磁盘之上。

②在ReduceTask远程复制数据的同时,会在后台开启两个线程对内存和本地磁盘的数据文件进行合并操作。

③在对数据进行合并的同时会进行排序操作,由于MapTask阶段已经对数据进行了局部排序,ReduceTask只需做一次归并排序就可以保证Copy的数据的整体有效性。

(4)、Combiner & Partitioner

4.1 InputFormat & RecordReaders

InputFormat 将输出文件拆分为多个 InputSplit,并由 RecordReadersInputSplit 转换为标准的<key,value>键值对,作为 map 的输出。这一步的意义在于只有先进行逻辑拆分并转为标准的键值对格式后,才能为多个 map 提供输入,以便进行并行处理。

4.2 Combiner

combinermap 运算后的可选操作,它实际上是一个本地化的 reduce 操作,它主要是在 map 计算出中间文件后做一个简单的合并重复 key 值的操作。这里以词频统计为例:

map 在遇到一个 hadoop 的单词时就会记录为 1,但是这篇文章里 hadoop 可能会出现 n 多次,那么 map 输出文件冗余就会很多,因此在 reduce 计算前对相同的 key 做一个合并操作,那么需要传输的数据量就会减少,传输效率就可以得到提升。

但并非所有场景都适合使用 combiner,使用它的原则是 combiner 的输出不会影响到 reduce 计算的最终输入,例如:求总数,最大值,最小值时都可以使用 combiner,但是做平均值计算则不能使用 combiner

不使用 combiner 的情况:

使用 combiner 的情况:

可以看到使用 combiner 的时候,需要传输到 reducer 中的数据由 12keys,降低到 10keys。降低的幅度取决于你 keys 的重复率,下文词频统计案例会演示用 combiner 降低数百倍的传输量。

4.3 Partitioner

partitioner 可以理解成分类器,将 map 的输出按照 key 值的不同分别分给对应的 reducer,支持自定义实现,下文案例会给出演示。

(5)、MapReduce词频统计案例

5.1 项目简介

这里给出一个经典的词频统计的案例:统计如下样本数据中每个单词出现的次数。

xiaokang	Hive	Hadoop	Flink	微信公众号:小康新鲜事儿	MapReduce	Spark
HBase	Spark	微信公众号:小康新鲜事儿
HBase	Spark	微信公众号:小康新鲜事儿	Hadoop	MapReduce	Hive
xiaokang	MapReduce	HBase	Flink	Spark	Hadoop
Flink	Hadoop	Spark	xiaokang
xiaokang	MapReduce	Hive	HBase	Spark
微信公众号:小康新鲜事儿	HBase	MapReduce	Hive	Spark	xiaokang
MapReduce	HBase	Flink	Hadoop	微信公众号:小康新鲜事儿	Spark	xiaokang	Hive
Hadoop	Flink	Spark	xiaokang	Hive	MapReduce	HBase	微信公众号:小康新鲜事儿
Spark	微信公众号:小康新鲜事儿	Hive	xiaokang	HBase	Flink
MapReduce	Spark	Hadoop	微信公众号:小康新鲜事儿

为方便开发,在项目源码中放置了一个工具类 WordCountDataUtils,用于模拟产生词频统计的样本,生成的文件支持输出到本地或者直接写到 HDFS 上。

项目完整源码下载地址:hadoop-wordcount

5.2 项目依赖

想要进行 MapReduce 编程,需要导入 hadoop-client 依赖:

<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-client</artifactId>
    <version>2.7.7</version>
</dependency>

5.3 WordCountMapper

将每行数据按照指定分隔符进行拆分。这里需要注意在 MapReduce 中必须使用 Hadoop 定义的类型,因为 Hadoop 预定义的类型都是可序列化、可比较的,所有类型均实现了 WritableComparable 接口。

package cool.xiaokang.component;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

/**
 * 将每行数据按照指定分隔符进行拆分
 *
 * @author:xiaokang
 */
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String[] words = value.toString().split("\t");
        for (String word : words) {
            context.write(new Text(word), new IntWritable(1));
        }
    }
}

WordCountMapper 对应下图的 Mapping 操作:

WordCountMapper 继承自 Mappe 类,这是一个泛型类,定义如下:

WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable>

public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
   ......
}
  • KEYIN : mapping 输入 key 的类型,即每行的偏移量 (每行第一个字符在整个文本中的位置),Long 类型,对应 Hadoop 中的 LongWritable 类型;
  • VALUEIN : mapping 输入 value 的类型,即每行数据;String 类型,对应 Hadoop 中 Text 类型;
  • KEYOUTmapping 输出的 key 的类型,即每个单词;String 类型,对应 Hadoop 中 Text 类型;
  • VALUEOUTmapping 输出 value 的类型,即每个单词出现的次数;这里用 int 类型,对应 IntWritable 类型。

5.4 WordCountReducer

在 Reduce 中进行单词出现次数的统计:

package cool.xiaokang.component;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

/**
 * 进行词频统计
 *
 * @author:xiaokang
 */
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int count = 0;
        for (IntWritable value : values) {
            count += value.get();
        }
        context.write(key, new IntWritable(count));
    }
}

如下图,shuffling 的输出是 reduce 的输入。这里的 key 是每个单词,values 是一个可迭代的数据类型,类似 (1,1,1,...)

5.5 WordCountApp

组装 MapReduce 作业,并提交到服务器运行,代码如下:

package cool.xiaokang;

import cool.xiaokang.component.WordCountMapper;
import cool.xiaokang.component.WordCountReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;
import java.net.URI;

/**
 * 组装作业,并提交到集群运行
 *
 * @author:xiaokang
 */
public class WordCountApp {
    // 这里为了直观显示参数 使用了硬编码,实际开发中可以通过外部传参
    private static final String HDFS_URL = "hdfs://192.168.239.161:9000";
    private static final String HADOOP_USER_NAME = "root";

    public static void main(String[] args) throws Exception {
        //  文件输入路径和输出路径由外部传参指定
        if (args.length < 2) {
            System.out.println("Input and Output paths are necessary!");
            return;
        }

        // 需要指明hadoop用户名,否则在HDFS上创建目录时可能会抛出权限不足的异常
        System.setProperty("HADOOP_USER_NAME", HADOOP_USER_NAME);

        Configuration configuration = new Configuration();
        // 指明HDFS的地址
        configuration.set("fs.defaultFS", HDFS_URL);

        // 创建一个Job
        Job job = Job.getInstance(configuration);

        // 设置运行的主类
        job.setJarByClass(WordCountApp.class);

        // 设置Mapper和Reducer
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);

        // 设置Mapper输出key和value的类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

        // 设置Reducer输出key和value的类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        // 如果输出目录已经存在,则必须先删除,否则重复运行程序时会抛出异常
        FileSystem fs = FileSystem.get(new URI(HDFS_URL), configuration, HADOOP_USER_NAME);
        Path outputPath = new Path(args[1]);
        if (fs.exists(outputPath)) {
            fs.delete(outputPath, true);
        }

        // 设置作业输入文件和输出文件的路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, outputPath);

        // 将作业提交到群集并等待它完成,参数设置为true代表打印显示对应的进度
        boolean result = job.waitForCompletion(true);

        // 关闭之前创建的fileSystem
        fileSystem.close();

        // 根据作业结果,终止当前运行的Java虚拟机,退出程序
        System.exit(result ? 0 : -1);
    }
}

需要注意的是:如果不设置 Mapper 操作的输出类型,则程序默认它和 Reducer 操作输出的类型相同。

5.6 提交到服务器运行

将项目打成jar包:

D:\IDEA_Projects\hadoop-wordcount>mvn clean package

使用以下命令提交作业:

[root@hadoop01 hadoop-2.7.7]# bin/hadoop jar /root/wordcountjar/hadoop-wordcount-1.0.jar cool.xiaokang.WordCountApp /wordcount/wordcount_input.txt /wordcount/output/WordCountApp

作业完成后查看 HDFS 上生成目录:

#查看目录
[root@hadoop01 hadoop-2.7.7]# hadoop fs -ls /wordcount/output/WordCountApp
#查看统计结果
[root@hadoop01 hadoop-2.7.7]# hadoop fs -cat /wordcount/output/WordCountApp/part-r-00000

(6)、MapReduce词频统计案例进阶之Combiner

6.1 代码实现

若要使用 combiner 功能,只要在组装作业时添加下面一行代码即可:

 // 设置Combiner
 job.setCombinerClass(WordCountReducer.class);

6.2 执行结果

加入 combiner 后统计结果是不会改变的,但是可以从打印的日志看出 combiner 的效果:

没有加入 combiner 的打印日志:

加入 combiner 后的打印日志如下:

这里我们只有一个输入文件并且小于 128M(样本文件实际大小只有 53.03 KB ),所以只有一个 Map 进行处理。可以看到经过 combiner 后,records 由 5052 降低为 8(样本中单词种类就只有 8 种),在这个用例中 combiner 就能极大地降低需要传输的数据量。

(7)、MapReduce词频统计案例进阶之Partitioner

7.1 默认的Partitioner

这里假设有个需求:将不同单词的统计结果输出到不同文件。这种需求实际上比较常见,比如统计商品的销量时,需要将结果按照商品种类进行拆分。要实现这个功能,就需要用到自定义 Partitioner

这里先介绍下 MapReduce 默认的分类规则:在构建 job 时候,如果不指定,默认的使用的是 HashPartitioner:对 key 值进行哈希散列并对 numReduceTasks 取余。其实现如下:

public class HashPartitioner<K, V> extends Partitioner<K, V> {
  public int getPartition(K key, V value,int numReduceTasks) {
    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
  }
}

7.2 自定义Partitioner

这里我们继承 Partitioner 自定义分类规则,这里按照单词进行分类:

package cool.xiaokang.component;

import cool.xiaokang.utils.WordCountDataUtils;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

/**
 * 自定义Partitioner,按照单词分区
 *
 * @author:xiaokang
 */
public class CustomPartitioner extends Partitioner<Text, IntWritable> {
    @Override
    public int getPartition(Text text, IntWritable intWritable, int numPartitions) {
        return WordCountDataUtils.WORD_LIST.indexOf(text.toString());
    }
}

在构建 job 时候指定使用我们自己的分类规则,并设置 reduce 的个数:

// 设置自定义分区规则
job.setPartitionerClass(CustomPartitioner.class);
// 设置reduce个数
job.setNumReduceTasks(WordCountDataUtils.WORD_LIST.size());

7.3 执行结果如下

执行结果如下,分别生成 8 个文件,每个文件中为对应单词的统计结果:

三、集群资源管理器—YARN

(1)、YARN 简介

Apache YARN (Yet Another Resource Negotiator) 是 Hadoop 2.0 引入的集群资源管理系统。用户可以将各种服务框架部署在 YARN 上,由 YARN 进行统一地管理和资源分配。

(2)、YARN架构

1. ResourceManager

ResourceManager 通常在独立的机器上以后台进程的形式运行,整个集群只有一个(未实现HA), 它是整个集群资源的主要协调者和管理者。ResourceManager 负责给用户提交的所有应用程序分配资源,它根据应用程序优先级、队列容量、ACLs、数据位置等信息做出决策,然后以共享的、安全的、多租户的方式制定分配策略、调度集群资源。

2. NodeManager

NodeManager 是 YARN 集群中的每个具体节点的管理者。主要负责该节点内所有容器的生命周期的管理,监视资源和跟踪节点健康。具体如下:

  • 启动时向 ResourceManager 注册并定时发送心跳消息,等待 ResourceManager 的指令;
  • 维护 Container 的生命周期,监控 Container 的资源使用情况;
  • 管理任务运行时的相关依赖,根据 ApplicationMaster 的需要,在启动 Container 之前将需要的程序及其依赖拷贝到本地。

3. ApplicationMaster

在用户提交一个应用程序时,YARN 会启动一个轻量级的进程 ApplicationMasterApplicationMaster 负责协调来自 ResourceManager 的资源,并通过 NodeManager 监视容器内资源的使用情况,同时还负责任务的监控与容错。具体如下:

  • 根据应用的运行状态来决定动态计算资源需求;
  • ResourceManager 申请资源,监控申请的资源的使用情况,并进一步分配给内部任务;
  • 跟踪任务状态和进度,报告资源的使用情况和应用的进度信息;
  • 负责任务的容错。

4. Container

Container 是 YARN 中的资源抽象,它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等。当 AM 向 RM 申请资源时,RM 为 AM 返回的资源是用 Container 表示的。YARN 会为每个任务分配一个 Container,该任务只能使用该 Container 中描述的资源。ApplicationMaster 可在 Container 内运行任何类型的任务。例如,MapReduce ApplicationMaster 请求一个容器来启动 map 或 reduce 任务,而 Giraph ApplicationMaster 请求一个容器来运行 Giraph 任务。

(3)、YARN工作原理简述

  1. Client 提交作业到 YARN 上;

  2. Resource Manager 选择一个 Node Manager,启动一个 Container 并运行 Application Master 实例;

  3. Application Master 根据实际需要向 Resource Manager 请求更多的 Container 资源(如果作业很小, 应用管理器会选择在其自己的 JVM 中运行任务);

  4. Application Master 通过获取到的 Container 资源执行分布式计算。

(4)、YARN工作原理详述

1. 作业提交

client 向ResourceManager提交作业。首先,用户根据自己的需求配置好所有应该配置的参数。作业提交后,就会进入自动化执行。在这个过程中,用户只能监控程序的执行情况和强制中断作业,不能对作业的执行过程进行任何干预。

(第 1 步) client 调用 job.waitForCompletion 方法,向整个集群提交 MapReduce 作业。

(第 2 步) 新的作业 ID(应用 ID) 由资源管理器分配( getNewJobId() )。

(第 3 步) 作业的 client 核实作业的输出, 计算输入的 split, 将作业的资源 (包括 Jar 包,配置文件, split 信息) 复制到一个以作业ID命名的目录中拷贝给 HDFS。

(第 4 步) 最后,通过调用资源管理器的 submitApplication() 来提交作业。

2. 作业初始化

在ResourceManager端开始初始化工作,包括在其内存里建立一系列数据结构,来记录这个 Job 的运行情况。当资源管理器收到对 submitApplciation() 的调用后,就会把这个调用放入一个内部队列中,交由作业调度器(scheduler)进行调度。初始化主要是创建一个表示正在运行作业的对象,以便跟踪任务的状态和进程。

(第 5 步) 调度器(scheduler)分配 container(5a), 然后资源管理器(ResourceManager)在节点管理器(NodeManager)的管理下在该 container 内启动应用管理器进程, 由节点管理器监控。

(第 6 步) MapReduce 作业的应用管理器是一个主类为 MRAppMaster 的 Java 应用,其通过创造一些 bookkeeping 对象来监控作业的进度, 得到任务的进度和完成报告。

(第 7 步) MRAppMaster 通过分布式文件系统得到由客户端计算好的输入 split,然后为每个输入 split 创建一个 map 任务, 根据 mapreduce.job.reduces 创建 reduce 任务对象。

MRAppMaster决定如何运行构成 MapReduce 作业的各个任务。如果作业很小并且配置了 yarn 的 Uber 模式(true)的话,就选择在与它同一个JVM上运行任务(默认情况下 Uber 模式不可用为false)。

1. 小任务概念

默认情况下:小任务就是小于10个mapper并且只有1个reducer并且输入大小小于一个HDFS块的任务

修改参数
mapreduce.job.ubertask.maxmaps(默认9)
mapreduce.job.ubertask.maxreduces(默认1)
mapreduce.job.ubertask.maxbytes(默认128MB)
mapreduce.job.ubertask.enable设置为false的话表示禁用uber
2. uber模式

uber mode 可以理解为 JVM 重用,是对小作业进行优化,不会给每个任务分别分配 Container 资源,这些小任务将统一在一个Container中按照先执行map任务后执行reduce任务的顺序串行执行。

该模式是Hadoop2.x开始引入的,以uber模式运行MR作业,所有的 Map 任务和 Reduce 任务将会在 Application Master 所在的 Container 中运行,也就是说整个MR作业运行的过程中只会启动 AM Container ,不需要启动 Mapper Container 和 Reducer Container。

如果作业够小,则所有任务在一个 JVM 中完成要比为每个任务启动一个 JVM 更划算。

3. 任务分配

如果作业很小, 应用管理器会选择在其自己的 JVM 中运行任务。

(第 8 步) 如果不是小作业, 那么应用管理器向资源管理器请求 Container 来运行所有的 map 和 reduce 任务。这些请求是通过心跳来传输的, 包括每个 map 任务的数据位置,比如存放输入 split 的主机名和机架 (rack),调度器利用这些信息来调度任务,尽量将任务分配给存储数据的节点或者分配给和存放输入 split 的节点相同机架的节点。默认情况下,map 任务和 reduce 任务都分配到1024MB内存。但这也可以通过mapreduce.map.memory.mbmapreduce.reduce.memory.mb来设置。

4. 任务运行

(第 9 步) 当一个任务由资源管理器的调度器分配给一个 container 后,应用管理器通过联系节点管理器来启动 Container。

(第 10 步) 任务由一个主类为 YarnChild 的 Java 应用执行, 在运行任务之前首先本地化任务需要的资源,比如作业配置、JAR 文件以及分布式缓存的所有文件。

(第 11 步) 最后,运行 map 或 reduce 任务。

YarnChild 运行在一个专用的 JVM 中, 但是 YARN 不支持 JVM 重用。

5. 进度和状态更新

YARN 中的**任务(task)**每 3 秒将其进度和状态 (包括 counter) 返回给应用管理器, 客户端每 1 秒 (通过 mapreduce.client.progressmonitor.pollinterval 设置) 向应用管理器请求进度更新, 展示给用户(8088)。

6. 作业完成

除了向应用管理器请求作业进度外, 客户端每 5 秒钟都会通过调用 waitForCompletion() 来检查作业是否完成,时间间隔可以通过 mapreduce.client.completion.pollinterval 来设置。作业完成之后, 应用管理器和 Container 会清理工作状态, OutputCommiter 的作业清理方法也会被调用。作业的信息会被作业历史服务器存储以备之后用户核查。

(4)、提交作业到YARN上运行

这里以提交 Hadoop Examples 中计算 Pi 的 MapReduce 程序为例,相关 Jar 包在 Hadoop 安装目录的 share/hadoop/mapreduce 目录下:

# 提交格式: hadoop jar jar包路径 主类名称 主类参数
# hadoop jar hadoop-mapreduce-examples-2.7.7.jar pi 11 24

四、HDFS常用Shell命令

基本语法:hdfs dfs 具体命令hadoop fs 具体命令(过时)

1. 查看目录

# 显示目录结构
hdfs dfs -ls <path>
# 以人性化的方式递归显示目录结构
hdfs dfs -ls -R -h <path>
# 显示根目录下内容
hdfs dfs -ls /

2. 创建目录

# 创建目录
hdfs dfs -mkdir <path> 
# 递归创建目录
hdfs dfs -mkdir -p <path> 
#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -mkdir /xiaokang
[xiaokang@hadoop01 ~]$ hdfs dfs -mkdir -p /wordcount/input

3. 创建文件

#创建一个文件大小为0的文件
hdfs dfs -touchz <文件名>
#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -touchz /xiaokang/aaa.txt

4. 删除操作

# 删除文件
hdfs dfs -rm <path>
# 删除空文件夹
hdfs dfs -rmdir <path>
# 递归删除目录和文件
hdfs dfs -rm -R <path> 

5. 拷贝操作

#该命令允许多个来源,但此时目标必须是一个目录
hdfs dfs -cp [-f] [-p] <src> <dst>
-f :当文件存在时,进行覆盖
-p :将权限、所属组、时间戳、ACL以及XATTR等也进行拷贝

#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -cp -f -p /xiaokang/bbb.txt /wordcount/input/bbb.txt

6. 移动操作(重命名)

#命令允许多个来源,但此时目的地需要是一个目录。跨文件系统移动文件是不允许的
hdfs dfs -mv <src> <dst>
#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -mv /xiaokang /xiaokang1 /wordcount
[xiaokang@hadoop01 ~]$ hdfs dfs -mv /wordcount/xiaokang /wordcount/xiaokang_mv

7. 从本地加载文件到HDFS

# 二选一执行即可
hdfs dfs -put <localsrc> <dst>
hdfs dfs -copyFromLocal <localsrc> <dst>
-f :当文件存在时,进行覆盖
-p :将权限、所属组、时间戳、ACL以及XATTR等也进行拷贝

#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -put -f -p ~/bbb.txt /xiaokang/bbb.txt

8. 从本地剪切文件到HDFS

hdfs dfs -moveFromLocal <localsrc> <dst>
#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -moveFromLocal ~/xiaokang.txt /wordcount/input

9. 从HDFS导出文件到本地

# 二选一执行即可
hadoop fs -get <src> <localdst>
hadoop fs -copyToLocal <src> <localdst>
#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -get /wordcount/input/xiaokang.txt ./

10. 从HDFS剪切文件到本地(还没实现呢)

[xiaokang@hadoop01 ~]$ hdfs dfs -moveToLocal /wordcount/input/xiaokang.txt ./
moveToLocal: Option '-moveToLocal' is not implemented yet.

11. 追加一个或多个文件到已经存在的文件的末尾

hdfs dfs -appendToFile <localsrc> ... <dst>
#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -appendToFile ./test_1.txt /wordcount/input/aaa.txt
[xiaokang@hadoop01 ~]$ hdfs dfs -appendToFile ./test_1.txt ./test_2.txt /wordcount/input/aaa.txt

12. 查看文件内容

# 二选一执行即可
hdfs dfs -cat <path> 
#将HDFS中文件以文本形式输出(包括zip包,jar包等形式)
hdfs dfs -text <path> 
#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -cat /wordcount/input/aaa.txt
[xiaokang@hadoop01 ~]$ hdfs dfs -text /wordcount/input/aaa.txt

13. 显示文件的最后一千字节 (1kb)

hdfs dfs -tail <path> 
#和Unix中tail -f命令类似,当文件内容更新时,输出将会改变,具有实时性
hdfs dfs -tail -f <path> 
#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -tail /wordcount/input/wordcount_input.txt
[xiaokang@hadoop01 root]$ hdfs dfs -tail -f /wordcount/input/wordcount_input.txt
[xiaokang@hadoop01 ~]$ hdfs dfs -appendToFile ~/test_1.txt /wordcount/input/wordcount_input.txt

14. 统计目录下各文件大小

hdfs dfs -du [-s] [-h] URI [URI ...]
-s : 显示所有文件大小总和
-h : 将以更友好的方式显示文件大小(例如 64.0m 而不是 67108864)
#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -du /wordcount
55524  /wordcount/input
0      /wordcount/xiaokang1
37     /wordcount/xiaokang_mv
[xiaokang@hadoop01 ~]$ hdfs dfs -du -h /wordcount
54.2 K  /wordcount/input
0       /wordcount/xiaokang1
37      /wordcount/xiaokang_mv
[xiaokang@hadoop01 ~]$ hdfs dfs -du -h -s /wordcount
54.3 K  /wordcount

15. 合并下载多个文件

hdfs dfs -getmerge [-nl] <src> <localdst>
# 案例 将HDFS上的wordcount_input.txt和aaa.txt合并后下载到本地的当前用户家目录的merge.txt
[xiaokang@hadoop01 ~]$ hdfs dfs -getmerge /wordcount/input/wordcount_input.txt /wordcount/input/aaa.txt ~/merge.txt
-nl 在每个文件的末尾添加换行符(LineFeed)
-skip-empty-file 跳过空文件

16. 统计文件系统的可用空间

hdfs dfs -df -h /
-h : 将以更友好的方式显示文件大小(例如 64.0m 而不是 67108864)

17. 改变文件复制因子

#更改文件的复制因子。如果 path 是目录,则更改其下所有文件的复制因子
hdfs dfs -setrep [-w] <numReplicas> <path>
-w : 标志的请求,命令等待复制完成。这有可能需要很长的时间。
#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -setrep 3 /wordcount/input/bbb.txt
[xiaokang@hadoop01 ~]$ hdfs dfs -setrep -w 5 /wordcount/input/bbb.txt

18. 权限控制

# 权限控制和Linux上使用方式一致
# 变更文件或目录的所属群组。 用户必须是文件的所有者或超级用户。
hdfs dfs -chgrp [-R] GROUP URI [URI ...]
# 修改文件或目录的访问权限  用户必须是文件的所有者或超级用户。
hdfs dfs -chmod [-R] <MODE[,MODE]... | OCTALMODE> URI [URI ...]
# 修改文件的拥有者  用户必须是超级用户。
hdfs dfs -chown [-R] [OWNER][:[GROUP]] URI [URI ]
#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -chmod -R 774 /tmp
[xiaokang@hadoop01 ~]$ hdfs dfs -chown -R xiaokang:hadoopenv /tmp
[xiaokang@hadoop01 ~]$ hdfs dfs -chgrp -R test /tmp

19. 文件检测

hdfs dfs -test -[defsz] URI
-d:如果路径是目录,返回 0
-e:如果路径存在,则返回 0
-f:如果路径是文件,则返回 0
-s:如果路径不为空,则返回 0
-r:如果路径存在且授予读权限,则返回 0
-w:如果路径存在且授予写入权限,则返回 0
-z:如果文件长度为零,则返回 0
#案例
[xiaokang@hadoop01 ~]$ hdfs dfs -test -d /wordcount/xiaokang_mv && echo "true"

20. 管理HDFS集群(dfsadmin)

#返回集群状态信息
hdfs dfsadmin -report
#查看、进入和离开安全模式
hdfs dfsadmin -safemode get/enter/leave
#提前进入安全模式,将内存中的fsimage保存为一个fsimage文件,重置edits文件(前提要打开安全模式)
hdfs dfsadmin -saveNamespace
#保存datanode和块信息,存储到logs下
hdfs dfsadmin -metasave 名称.tt
#重新读取hosts和exclude文件,刷新集群节点
hdfs dfsadmin -refreshNodes
#设置文件目录配额
hdfs dfsadmin -setQuota 10 /wordcount/input
#强制滚动edits
hdfs dfsadmin -rollEdits

21. 其它命令(以下操作用hdfs用户)

#检查HDFS上文件和目录的健康状态、获取文件的block信息和位置信息
hdfs fsck hdfs路径
#检查并列出所有文件状态
hdfs fsck /wordcount/input/ -files
#打印文件的block列表报告
hdfs fsck /wordcount/input/aaa.txt -files -blocks
#打印文件块的位置信息
hdfs fsck /wordcount/input/aaa.txt -files -blocks -locations
#打印文件块所在的机架信息
hdfs fsck /wordcount/input/aaa.txt -files -blocks -locations -racks
#案例
[xiaokang@hadoop01 logs]$ hdfs fsck /wordcount/input/aaa.txt -files -blocks -locations -racks
Connecting to namenode via http://hadoop01:50070/fsck?ugi=xiaokang&files=1&blocks=1&locations=1&racks=1&path=%2Fwordcount%2Finput%2Faaa.txt
FSCK started by xiaokang (auth:SIMPLE) from /192.168.239.124 for path /wordcount/input/aaa.txt at Wed Feb 12 15:28:42 CST 2020
/wordcount/input/aaa.txt 348 bytes, 1 block(s):  OK
0. BP-1004851153-192.168.239.124-1581483619988:blk_1073741828_1005 len=348 repl=1 [/default-rack/192.168.239.124:50010]

Status: HEALTHY
 Total size:	348 B
 Total dirs:	0
 Total files:	1
 Total symlinks:		0
 Total blocks (validated):	1 (avg. block size 348 B)
 Minimally replicated blocks:	1 (100.0 %)
 Over-replicated blocks:	0 (0.0 %)
 Under-replicated blocks:	0 (0.0 %)
 Mis-replicated blocks:		0 (0.0 %)
 Default replication factor:	1
 Average block replication:	1.0
 Corrupt blocks:		0
 Missing replicas:		0 (0.0 %)
 Number of data-nodes:		1
 Number of racks:		1
FSCK ended at Wed Feb 12 15:28:42 CST 2020 in 3 milliseconds


The filesystem under path '/wordcount/input/aaa.txt' is HEALTHY

五、HDFS—JavaAPI的使用

1. Windows下配置开发环境

2. 基于IDEA创建 maven jar工程

想要使用 HDFS API,需要导入依赖hadoop-commonhadoop-clienthadoop-hdfshadoop-client

<?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>cool.xiaokang</groupId>
    <artifactId>hdfs-java-api</artifactId>
    <version>1.0</version>
    <properties>
        <!-- jdk版本 -->
        <jdk.version>8</jdk.version>
        <!-- 设置字符编码 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <hadoop.version>2.7.7</hadoop.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.11.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-common</artifactId>
            <version>${hadoop.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
            <version>${hadoop.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-hdfs</artifactId>
            <version>${hadoop.version}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <!-- 设置编译字符编码 -->
                    <encoding>UTF-8</encoding>
                    <!-- 设置编译jdk版本 -->
                    <source>${jdk.version}</source>
                    <target>${jdk.version}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

log4j.properties

log4j.rootLogger=INFO,console,file
#配置输出到控制台
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.Encoding=UTF-8
log4j.appender.console.layout.ConversionPattern=%d{yyyy-MM-dd HH\:mm\:ss} -%-4r [%t] %-5p  %x - %m%n
#配置输入日志到日志文件
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=target/mylog.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d %p [%c] - %m%n

3. FileSystem

FileSystem 是所有 HDFS 操作的主入口。由于之后的每个小功能的测试都需要用到它,这里使用写了一个工具类HDFSUtils

package cool.xiaokang.utils;

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

import java.io.IOException;
import java.net.URI;


/**
 * HDFS工具类
 *
 * @author xiaokang
 */
public class HDFSUtils {
    private static final String HDFS_PATH = "hdfs://192.168.239.161:9000";
    private static final String HDFS_USER = "xiaokang";

    /**
     * 获取FileSystem对象
     *
     * @return
     */
    public static FileSystem getFileSystem() {
        try {
            Configuration conf = new Configuration();
            //conf.set("dfs.replication", "3");
       		//conf.addResource("hdfs-site.xml");
            FileSystem fileSystem = FileSystem.get(new URI(HDFS_PATH), conf, HDFS_USER);
            return fileSystem;
        } catch (Exception e) {
            System.out.println("获取FileSystem失败!!!---" + e.getMessage());
            return null;
        }
    }

    /**
     * 关闭FileSystem对象
     * @param fileSystem
     */
    public static void closeFileSystem(FileSystem fileSystem) {
        if (fileSystem != null) {
            try {
                fileSystem.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

4. 实现-ls列表查看

//HDFS Shell操作:hdfs dfs -ls /input
//查看指定目录下所有文件的信息
static void testFileStatus(String path){
    FileSystem fs = null;
    try {
        fs=HDFSUtils.getFileSystem();
        if (fs!=null){
            FileStatus[] fileStatuses = fs.listStatus(new Path(path));
            for(FileStatus file:fileStatuses){
                //fileStatus的toString方法被重写过,直接打印可以看到所有信息
                //System.out.println(file.toString());
                //System.out.println("路径:"+file.getPath());
                //System.out.println("拥有者:"+file.getOwner());
                //System.out.println("权限:"+file.getPermission().toString());
                //System.out.println();
                if (file.isFile()){
                    System.out.println(file.getPath().getName()+"是文件");
                }else if (file.isDirectory()){
                    System.out.println(file.getPath().getName()+"是目录");
                }
            }
        }else{
            System.out.println("获取FileSystem失败!!");
        }
    }catch (Exception e){
        System.out.println("error"+e.getMessage());
    }finally {
        HDFSUtils.closeFileSystem(fs);
    }
}

FileStatus 中包含了文件的基本信息,比如文件路径,是否是文件夹,修改时间,访问时间,所有者,所属组,文件权限,是否是符号链接等,输出内容示例如下:

FileStatus{
path=hdfs://192.168.239.161:9000/xiaokang/test; 
isDirectory=true; 
modification_time=1556680796191; 
access_time=0; 
owner=xiaokang; 
group=supergroup; 
permission=rwxr-xr-x; 
isSymlink=false
}

5. 递归查看目录下所有文件的详细信息

//递归查看目录下所有文件的详细信息
    static void testFileDetails(String path){
        FileSystem fs = null;
        try {
            fs=HDFSUtils.getFileSystem();
            if (fs!=null){
                RemoteIterator<LocatedFileStatus> fileList = fs.listFiles(new Path(path),true);
                while (fileList.hasNext()){
                    LocatedFileStatus file = fileList.next();
                    //每个文件的详细信息
                    System.out.println(file.getOwner()+"--"+file.getPermission());
                    //文件的块信息
                    BlockLocation[] blocks = file.getBlockLocations();
                    int i=0;
                    for(BlockLocation block:blocks){
                        String[] hosts = block.getHosts();
                        System.out.println("块数:"+block.getLength());
                        System.out.println("第【"+i+"】块:"+ Arrays.toString(hosts));
                        i++;
                    }
                    System.out.println("===================");
                }
            }else{
                System.out.println("获取FileSystem失败!!");
            }
        }catch (Exception e){
            System.out.println("创建失败"+e.getMessage());
        }finally {
            HDFSUtils.closeFileSystem(fs);
        }
    }

6. 创建目录

//创建目录,支持递归创建
static void testMkdirs(String path) throws Exception{
    FileSystem fs = null;
    try {
        fs=HDFSUtils.getFileSystem();
        if (fs!=null){
            fs.mkdirs(new Path(path));
            System.out.println("创建成功!!!");
        }else{
            System.out.println("获取FileSystem失败!!");
        }
    }catch (Exception e){
        System.out.println("创建失败"+e.getMessage());
    }finally {
        HDFSUtils.closeFileSystem(fs);
    }
}

7. 文件复制

//集群内文件复制
static void testCopy(String src,String dest){
    FileSystem fileSystem=null;
    FSDataInputStream in=null;
    FSDataOutputStream out=null;
    try{
        fileSystem = HDFSUtils.getFileSystem();
        if (fileSystem!=null){
            in=fileSystem.open(new Path(src));
            out=fileSystem.create(new Path(dest));
            IOUtils.copyBytes(in,out,4096);
            System.out.println("复制完成");
        }else{
            System.out.println("获取FileSystem失败!");
        }
    }catch (Exception e){
        System.out.println("error"+e.getMessage());
    }finally {
        try {
            out.close();
            in.close();
            HDFSUtils.closeFileSystem(fileSystem);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

8. 文件重命名

//集群文件重命名
static void testRename(String oldName,String newName){
    FileSystem fs = null;
    try {
        fs=HDFSUtils.getFileSystem();
        if (fs!=null){
            fs.rename(new Path(oldName),new Path(newName));
            System.out.println("重命名成功!!!");
        }else{
            System.out.println("获取FileSystem失败!!");
        }
    }catch (Exception e){
        System.out.println("重命名失败"+e.getMessage());
    }finally {
        HDFSUtils.closeFileSystem(fs);
    }
}

9. 删除目录或文件

	/*
     *  delete方法中第二个参数代表是否递归删除
     *    +  如果 path 是一个目录且递归删除为 true, 则删除该目录及其中所有文件;
     *    +  如果 path 是一个目录但递归删除为 false,则会则抛出异常。
     */
//删除集群中目录或文件
static void testDelete(String path){
    FileSystem fs = null;
    try {
        fs=HDFSUtils.getFileSystem();
        if (fs!=null){
            fs.delete(new Path(path),true);
            System.out.println("删除成功!!!");
        }else{
            System.out.println("获取FileSystem失败!!");
        }
    }catch (Exception e){
        System.out.println("删除失败"+e.getMessage());
    }finally {
        HDFSUtils.closeFileSystem(fs);
    }
}

10. 上传文件到HDFS

//本地文件上传至HDFS
static void testPut(String localSrc,String hdfsDest){
    FileSystem fs = null;
    try {
        fs=HDFSUtils.getFileSystem();
        if (fs!=null){
            fs.copyFromLocalFile(new Path(localSrc),new Path(hdfsDest));
            System.out.println("上传成功!!!");
        }else{
            System.out.println("获取FileSystem失败!!");
        }
    }catch (Exception e){
        System.out.println("上传失败"+e.getMessage());
    }finally {
        HDFSUtils.closeFileSystem(fs);
    }
}

11. 上传文件到HDFS-IO流方式

//本地文件上传至HDFS-IO流方式
static void testPutWithIO(String localSrc,String hdfsDest){
    FileSystem fs = null;
    FileInputStream in=null;
    FSDataOutputStream out=null;
    try {
        fs=HDFSUtils.getFileSystem();
        if (fs!=null){
            in=new FileInputStream(new File(localSrc));
            out=fs.create(new Path(hdfsDest));
            IOUtils.copyBytes(in,out,4096);
            System.out.println("IO流-上传成功!!!");
        }else{
            System.out.println("获取FileSystem失败!!");
        }
    }catch (Exception e){
        System.out.println("IO流-上传失败"+e.getMessage());
    }finally {
        try {
            out.close();
            in.close();
            HDFSUtils.closeFileSystem(fs);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

12. 从HDFS上下载文件

//从HDFS下载文件到本地
static void testGet(String hdfsSrc,String localDest){
    FileSystem fs = null;
    try {
        fs=HDFSUtils.getFileSystem();
        if (fs!=null){
            fs.copyToLocalFile(new Path(hdfsSrc),new Path(localDest));
            System.out.println("下载成功!!!");
        }else{
            System.out.println("获取FileSystem失败!!");
        }
    }catch (Exception e){
        System.out.println("下载失败"+e.getMessage());
    }finally {
        HDFSUtils.closeFileSystem(fs);
    }
}

	/*
     * 第一个参数控制下载完成后是否删除源文件,默认是 true,即删除;
     * 最后一个参数表示是否将 RawLocalFileSystem 用作本地文件系统;
     * RawLocalFileSystem 默认为 false,通常情况下可以不设置,
     * 但如果你在执行时候抛出 NullPointerException 异常,则代表你的文件系统与程序可能存在不兼容的情况 (window 下常见),
     * 此时可以将 RawLocalFileSystem 设置为 true
     */
    fs.copyToLocalFile(false, src, dst, true);

13. 从HDFS上下载文件-IO流方式

//从HDFS下载文件到本地-IO流方式
static void testGetWithIO(String hdfsSrc,String localDest){
    FileSystem fs = null;
    FSDataInputStream in=null;
    FileOutputStream out=null;
    try {
        fs=HDFSUtils.getFileSystem();
        if (fs!=null){
            in=fs.open(new Path(hdfsSrc));
            out=new FileOutputStream(new File(localDest));
            IOUtils.copyBytes(in,out,4096);
            System.out.println("IO流-下载成功!!!");
        }else{
            System.out.println("获取FileSystem失败!!");
        }
    }catch (Exception e){
        System.out.println("IO流-下载失败"+e.getMessage());
    }finally {
        HDFSUtils.closeFileSystem(fs);
    }
}

以上所有测试用例下载地址HDFS-JavaAPI