Hadoop基础部分-2

 

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

第一节 HDFS 简介

HDFS (全称:Hadoop Distribute File System,Hadoop 分布式文件系统)是 Hadoop 核心组成,是分布式存储服务。
分布式文件系统横跨多台计算机,在大数据时代有着广泛的应用前景,它们为存储和处理超大规模数据提供所需的扩展能力。
HDFS是分布式文件系统中的一种。


第二节 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 适合用来做大数据分析的底层存储服务,并不适合用来做网盘等应用(修改不方便,延迟大,网络开销大,成本太高)

第三节 HDFS 架构

 

NameNode(nn):Hdfs集群的管理者,Master

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

DataNode:NameNode下达命令,DataNode执行实际操作,Slave节点。

  • 保存实际的数据块
  • 负责数据块的读写

Client:客户端

  • 上传文件到HDFS的时候,Client负责将文件切分成Block,然后进行上传
  • 请求NameNode交互,获取文件的位置信息
  • 读取或写入文件,与DataNode交互
  • Client可以使用一些命令来管理HDFS或者访问HDFS

第四节 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 machines

3.HDFS命令演示
1. 启动Hadoop集群(方便后续的测试)

[root@linux121 hadoop-2.9.2]$ sbin/start-dfs.sh
[root@linux122 hadoop-2.9.2]$ sbin/start-yarn.sh


2. -help:输出这个命令参数

[root@linux121 hadoop-2.9.2]$ hadoop fs -help rm


3. -ls: 显示目录信息

[root@linux121 hadoop-2.9.2]$ hadoop fs -ls /


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

[root@linux121 hadoop-2.9.2]$ hadoop fs -mkdir -p /lagou/bigdata


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

[root@linux121 hadoop-2.9.2]$ touch hadoop.txt
[root@linux121 hadoop-2.9.2]$ hadoop fs -moveFromLocal ./hadoop.txt /lagou/bigdata


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

[root@linux121 hadoop-2.9.2]$ touch hdfs.txt
[root@linux121 hadoop-2.9.2]$ vi hdfs.txt

输入

namenode datanode block replication
[root@linux121 hadoop-2.9.2]$ hadoop fs -appendToFile hdfs.txt /lagou/bigdata/hadoop.txt

7. -cat:显示文件内容

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

8. -chgrp 、-chmod、-chown:Linux文件系统中的用法一样,修改文件所属权限

[root@linux121 hadoop-2.9.2]$ hadoop fs -chmod 666 /lagou/bigdata/hadoop.txt
[root@linux121 hadoop-2.9.2]$ 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拷贝到本地

[root@linux121 hadoop-2.9.2]$ 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目录中移动文件

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


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

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


14. -put:等同于copyFromLocal

[root@linux121 hadoop-2.9.2]$ hadoop fs -mkdir -p /user/root/test/
#本地文件系统创建yarn.txt
[root@linux121 hadoop-2.9.2]$ vim yarn.txt
resourcemanager nodemanager
[root@linux121 hadoop-2.9.2]$ hadoop fs -put ./yarn.txt /user/root/test/

16. -tail:显示一个文件的末尾

[root@linux121 hadoop-2.9.2]$ hadoop fs -tail /user/root/test/yarn.txt

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

[root@linux121 hadoop-2.9.2]$ hadoop fs -rm /user/root/test/yarn.txt

18. -rmdir:删除空目录

[root@linux121 hadoop-2.9.2]$ hadoop fs -mkdir /test
[root@linux121 hadoop-2.9.2]$ hadoop fs -rmdir /test

19. -du统计文件夹的大小信息

[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


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


4.2 JAVA客户端

4.2.1 客户端环境准备
1. 将Hadoop-2.9.2安装包解压到非中文路径(例如:E:\hadoop-2.9.2)。


2. 配置HADOOP_HOME环境变量

3. 配置Path环境变量。

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/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>
</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类



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

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

public class Hdfs {
    @Test
    public void testMkdir() throws URISyntaxException, IOException, InterruptedException {
        // 1 获取文件系统
        // 配置在集群上运行
        // configuration.set("fs.defaultFS", "hdfs://linux121:9000");
        // FileSystem fs = FileSystem.get(configuration);
        Configuration configuration = new Configuration();
        FileSystem fs = FileSystem.get(new URI("hdfs://hadoop-1:9000"), configuration, "root");
        // 2 创建目录
        fs.mkdirs(new Path("/api_test11"));
        // 3 关闭资源
        fs.close();
    }
}

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

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


解决方案:
从资料文件夹中找到winutils.exe拷贝放到windows系统Hadoop安装目录的bin目录下即可!!

4.2.2 HDFS的API操作
1 上传文件
1. 编写源代码

@Test
public void testCopyFromLocalFile() throws IOException,
InterruptedException, URISyntaxException {
// 1 获取文件系统
Configuration configuration = new Configuration();
configuration.set("dfs.replication", "2");
FileSystem fs = FileSystem.get(new URI("hdfs://linux121:9000"),
configuration, "root");
// 2 上传文件
fs.copyFromLocalFile(new Path("e:/lagou.txt"), new
Path("/lagou.txt"));
// 3 关闭资源
fs.close();
System.out.println("end");
}

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 testCopyToLocalFile() throws IOException, InterruptedException,
URISyntaxException{
// 1 获取文件系统
Configuration configuration = new Configuration();
FileSystem fs = FileSystem.get(new URI("hdfs://linux121:9000"),
configuration, "root");
// 2 执行下载操作
// boolean delSrc 指是否将原文件删除
// Path src 指要下载的文件路径
// Path dst 指将文件下载到的路径
// boolean useRawLocalFileSystem 是否开启文件校验
fs.copyToLocalFile(false, new Path("/lagou.txt"), new
Path("e:/lagou_copy.txt"), true);
// 3 关闭资源
fs.close();
}


3 删除文件/文件夹

@Test
public void testDelete() throws IOException, InterruptedException,
URISyntaxException{
// 1 获取文件系统
Configuration configuration = new Configuration();
FileSystem fs = FileSystem.get(new URI("hdfs://linux121:9000"),
configuration, "root");
// 2 执行删除
fs.delete(new Path("/api_test/"), true);
// 3 关闭资源
fs.close();
}


4 查看文件名称、权限、长度、块信息

@Test
public void testListFiles() throws IOException, InterruptedException,
URISyntaxException{
// 1获取文件系统
Configuration configuration = new Configuration();
FileSystem fs = FileSystem.get(new URI("hdfs://linux121:9000"),
configuration, "root");
// 2 获取文件详情
RemoteIterator<LocatedFileStatus> listFiles = fs.listFiles(new Path("/"),
true);
while(listFiles.hasNext()){
LocatedFileStatus status = listFiles.next();
// 输出详情
// 文件名称
System.out.println(status.getPath().getName());
// 长度
System.out.println(status.getLen());
// 权限
System.out.println(status.getPermission());
// 分组
System.out.println(status.getGroup());
// 获取存储的块信息
BlockLocation[] blockLocations = status.getBlockLocations();
for (BlockLocation blockLocation : blockLocations) {
// 获取块存储的主机节点
String[] hosts = blockLocation.getHosts();
for (String host : hosts) {
System.out.println(host);
}
}
System.out.println("-----------华丽的分割线----------");
}
// 3 关闭资源
fs.close();
}

5 文件夹判断

@Test
public void testListStatus() throws IOException, InterruptedException,
URISyntaxException{
// 1 获取文件配置信息
Configuration configuration = new Configuration();
FileSystem fs = FileSystem.get(new URI("hdfs://linux121:9000"),
configuration, "root");
// 2 判断是文件还是文件夹
FileStatus[] listStatus = fs.listStatus(new Path("/"));
for (FileStatus fileStatus : listStatus) {
// 如果是文件
if (fileStatus.isFile()) {
System.out.println("f:"+fileStatus.getPath().getName());
}else {
System.out.println("d:"+fileStatus.getPath().getName());
}
}
// 3 关闭资源
fs.close();
}

6 I/O流操作HDFS
以上我们使用的API操作都是HDFS系统框架封装好的。我们自己也可以采用IO流的方式实现文件的上传和下载。
6.1 文件上传
1. 需求:把本地e盘上的lagou.txt文件上传到HDFS根目录
2. 编写代码

@Test
public void putFileToHDFS() throws IOException, InterruptedException,
URISyntaxException {
// 1 获取文件系统
Configuration configuration = new Configuration();
FileSystem fs = FileSystem.get(new URI("hdfs://linux121:9000"),
configuration, "root");
// 2 创建输入流
FileInputStream fis = new FileInputStream(new File("e:/lagou.txt"));
// 3 获取输出流
FSDataOutputStream fos = fs.create(new Path("/lagou_io.txt"));
// 4 流对拷
IOUtils.copyBytes(fis, fos, configuration);
// 5 关闭资源
IOUtils.closeStream(fos);
IOUtils.closeStream(fis);
fs.close();
}

6.2 文件下载
1. 需求:从HDFS上下载lagou.txt文件到本地e盘上
2. 编写代码

// 文件下载
@Test
public void getFileFromHDFS() throws IOException, InterruptedException,
URISyntaxException{
// 1 获取文件系统
Configuration configuration = new Configuration();
FileSystem fs = FileSystem.get(new URI("hdfs://linux121:9000"),
configuration, "root");
// 2 获取输入流
FSDataInputStream fis = fs.open(new Path("/lagou_io.txt"));
// 3 获取输出流
FileOutputStream fos = new FileOutputStream(new
File("e:/lagou_io_copy.txt"));
// 4 流的对拷
IOUtils.copyBytes(fis, fos, configuration);
// 5 关闭资源
IOUtils.closeStream(fos);
IOUtils.closeStream(fis);
fs.close();
}

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

    @Test
    public void readFileSeek2() throws IOException, InterruptedException,
            URISyntaxException{
// 1 获取文件系统
        Configuration configuration = new Configuration();
        FileSystem fs = FileSystem.get(new URI("hdfs://linux121:9000"),
                configuration, "root");
// 2 打开输入流,读取数据输出到控制台
        FSDataInputStream in = null;
        try{
            in= fs.open(new Path("/lagou.txt"));
            IOUtils.copyBytes(in, System.out, 4096, false);
            in.seek(0); //从头再次读取
            IOUtils.copyBytes(in, System.out, 4096, false);
        }finally {
            IOUtils.closeStream(in);
        }
    }

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</name>
<value>true</value>
</property>


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

基于HDFS权限本身比较鸡肋的特点,我们可以彻底放弃HDFS的权限校验,如果生产环境中我们可以考虑借助kerberos以及sentry等安全框架来管理大数据集群安全。所以我们直接修改HDFS的根目录权限为777

hadoop fs -chmod -R 777 /


参考代码


import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.util.Progressable;
import org.apache.hadoop.yarn.webapp.hamlet.Hamlet;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
public class HdfsClientDemo {
    FileSystem fs = null;
    Configuration configuration = null;
    @Before
    public void init() throws URISyntaxException, IOException,
            InterruptedException {
//1 获取Hadoop 集群的configuration对象
        configuration = new Configuration();
// configuration.set("fs.defaultFS", "hdfs://linux121:9000");
// configuration.set("dfs.replication", "2");
//2 根据configuration获取Filesystem对象
        fs = FileSystem.get(new URI("hdfs://linux121:9000"), configuration,
                "root");
    }
    @After
    public void destory() throws IOException {
//4 释放FileSystem对象(类似数据库连接)
        fs.close();
    }
    @Test
    public void testMkdirs() throws URISyntaxException, IOException,
            InterruptedException {
// FileSystem fs = FileSystem.get(configuration);
//3 使用FileSystem对象创建一个测试目录
        fs.mkdirs(new Path("/api_test2"));
    }
    //上传文件
    @Test
    public void copyFromLocalToHdfs() throws URISyntaxException, IOException,
            InterruptedException {
//上传文件
//src:源文件目录:本地路径
//dst:目标文件目录,hdfs路径
        fs.copyFromLocalFile(new Path("e:/lagou.txt"), new Path("/lagou.txt"));
//上传文件到hdfs默认是3个副本,
//如何改变上传文件的副本数量?
//1 configuration对象中指定新的副本数量
    }
    //下载文件
    @Test
    public void copyFromHdfsToLocal() throws URISyntaxException, IOException,
            InterruptedException {
        // boolean:是否删除源文件
//src:hdfs路径
//dst:目标路径,本地路径
        fs.copyToLocalFile(true, new Path("/lagou.txt"), new
                Path("e:/lagou_copy.txt"));
    }
    //删除文件或者文件夹
    @Test
    public void deleteFile() throws URISyntaxException, IOException,
            InterruptedException {
        fs.delete(new Path("/api_test2"), true);
    }
    //遍历hdfs的根目录得到文件以及文件夹的信息:名称,权限,长度等
    @Test
    public void listFiles() throws URISyntaxException, IOException,
            InterruptedException {
//得到一个迭代器:装有指定目录下所有文件信息
        RemoteIterator<LocatedFileStatus> remoteIterator = fs.listFiles(new
                Path("/"), true);
//遍历迭代器
        while (remoteIterator.hasNext()) {
            LocatedFileStatus fileStatus = remoteIterator.next();
//文件名称
            final String fileName = fileStatus.getPath().getName();
//长度
            final long len = fileStatus.getLen();
//权限
            final FsPermission permission = fileStatus.getPermission();
//分组
            final String group = fileStatus.getGroup();
//用户
            final String owner = fileStatus.getOwner();
            System.out.println(fileName + "\t" + len + "\t" + permission + "\t"
                    + group + "\t" + owner);
//块信息
            final BlockLocation[] blockLocations =
                    fileStatus.getBlockLocations();
            for (BlockLocation blockLocation : blockLocations) {
                final String[] hosts = blockLocation.getHosts();
                for (String host : hosts) {
                    System.out.println("主机名称" + host);
                }
            }
            System.out.println("---------------------------------");
        }
    }
    //文件以及文件夹判断
    @Test
    public void isFile() throws URISyntaxException, IOException,
            InterruptedException {
        final FileStatus[] fileStatuses = fs.listStatus(new Path("/"));
        for (FileStatus fileStatus : fileStatuses) {
            final boolean flag = fileStatus.isFile();
            if (flag) {
                System.out.println("文件:" + fileStatus.getPath().getName());
            } else {
                System.out.println("文件夹:" + fileStatus.getPath().getName());
            }
        }
    }
    //使用IO流操作HDFS
//上传文件:准备输入流读取本地文件,使用hdfs的输出流写数据到hdfs
    @Test
    public void uploadFileIO() throws IOException {
//1. 读取本地文件的输入流
        final FileInputStream inputStream = new FileInputStream(new
                File("e:/lagou.txt"));
//2. 准备写数据到hdfs的输出流
        final FSDataOutputStream outputStream = fs.create(new
                Path("/lagou.txt"));
// 3.输入流数据拷贝到输出流 :数组的大小,以及是否关闭流底层有默认值
        IOUtils.copyBytes(inputStream, outputStream, configuration);
//4.可以再次关闭流
        IOUtils.closeStream(outputStream);
        IOUtils.closeStream(inputStream);
    }
    //下载文件
    @Test
    public void downLoadFileIO() throws IOException {
//1. 读取hdfs文件的输入流
        final FSDataInputStream in = fs.open(new Path("/lagou.txt"));
//2. 本地文件的输出流
        final FileOutputStream out = new FileOutputStream(new
                File("e:/lagou_io_copy.txt"));
//3. 流的拷贝
        IOUtils.copyBytes(in, out, configuration);
//4.可以再次关闭流
        IOUtils.closeStream(out);
        IOUtils.closeStream(in);
    }
//seek定位读取hdfs指定文件 :使用io流读取/lagou.txt文件并把内容输出两次,本质就是读取文件内容两次并输出
    @Test
    public void seekReadFile() throws IOException {
//1 创建一个读取hdfs文件的输入流
        final FSDataInputStream in = fs.open(new Path("/lagou.txt"));
//2.控制台数据:System.out
//3 实现流拷贝,输入流--》控制台输出
// IOUtils.copyBytes(in, System.out, configuration);
        IOUtils.copyBytes(in, System.out, 4096, false);
//4. 再次读取文件
        in.seek(0); //定位从0偏移量(文件头部)再次读取
        IOUtils.copyBytes(in, System.out, 4096, false);
//5.关闭输入流
        IOUtils.closeStream(in);
    }
}

第五节 HDFS读写解析


5.1 HDFS读数据流程

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

5.2 HDFS写数据流程

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代码

    @Test
    public void testUploadPacket() throws IOException {
//1 准备读取本地文件的输入流
        final FileInputStream in = new FileInputStream(new
                File("e:/lagou.txt"));
//2 准备好写出数据到hdfs的输出流
        final FSDataOutputStream out = fs.create(new Path("/lagou.txt"), new
                Progressable() {
                    public void progress() { //这个progress方法就是每传输64KB(packet)就会执
                        行一次,
                        System.out.println("&");
                    }
                });
//3 实现流拷贝
        IOUtils.copyBytes(in, out, configuration); //默认关闭流选项是true,所以会自动
        关闭
//4 关流 可以再次关闭也可以不关了
    }

第六节 NN与2NN


6.1 HDFS元数据管理机制


问题1:NameNode如何管理和存储元数据?
计算机中存储数据两种:内存或者是磁盘

  • 元数据存储磁盘:存储磁盘无法面对客户端对元数据信息的任意的快速低延迟的响应,但是安全性高
  • 元数据存储内存:元数据存放内存,可以高效的查询以及快速响应客户端的查询请求,数据保存在内存,如果断点,内存中的数据全部丢失。

解决方案:内存+磁盘;NameNode内存+FsImage的文件(磁盘)
新问题:磁盘和内存中元数据如何划分?
两个数据一模一样,还是两个数据合并到一起才是一份完整的数据呢?

  • 一模一样:client如果对元数据进行增删改操作,需要保证两个数据的一致性。FsImage文件操作起来效率也不高。
  • 两个合并=完整数据:NameNode引入了一个edits文件(日志文件:只能追加写入)edits文件记录的是client的增删改操作,

不再选择让NameNode把数据dump出来形成FsImage文件(这种操作是比较消耗资源)。
元数据管理流程图

第一阶段: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。

6.2 Fsimage与Edits文件解析
NameNode在执行格式化之后,会在/opt/lagou/servers/hadoop-2.9.2/data/tmp/dfs/name/current目录下产生如下文件

 

  • Fsimage文件:是namenode中关于元数据的镜像,一般称为检查点,这里包含了HDFS文件系统所有目录以及文件相关信息(Block数量,副本数量,权限等信息)
  • Edits文件 :存储了客户端对HDFS文件系统所有的更新操作记录,Client对HDFS文件系统所有的更新操作都会被记录到Edits文件中(不包括查询操作)
  • seen_txid:该文件是保存了一个数字,数字对应着最后一个Edits文件名的数字
  • VERSION:该文件记录namenode的一些版本号信息,比如:CusterId,namespaceID等

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 文件类型(xml) -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
        <?xml version="1.0"?>
<fsimage>
<version>
    <layoutVersion>-63</layoutVersion>
    <onDiskVersion>1</onDiskVersion>
    <oivRevision>826afbeae31ca687bc2f8471dc841b66ed2c6704</oivRevision>
</version>
<NameSection>
    <namespaceId>1393381414</namespaceId>
    <genstampV1>1000</genstampV1>
    <genstampV2>1024</genstampV2>
    <genstampV1Limit>0</genstampV1Limit>
    <lastAllocatedBlockId>1073741848</lastAllocatedBlockId>
    <txid>265</txid>
</NameSection>
<INodeSection>
    <inode>
        <id>16398</id>
        <type>DIRECTORY</type>
        <name>history</name>
        <mtime>1592376391028</mtime>
        <permission>root:supergroup:0777</permission>
        <nsquota>-1</nsquota>
        <dsquota>-1</dsquota>
    </inode>
    <inode>
        <id>16399</id>
        <type>DIRECTORY</type>
        <name>done_intermediate</name>
        <mtime>1592375256896</mtime>
        <permission>root:supergroup:1777</permission>
        <nsquota>-1</nsquota>
        <dsquota>-1</dsquota>
    </inode>
    <inode>
        <id>16400</id>
        <type>DIRECTORY</type>
        <name>root</name>
        <mtime>1592378079208</mtime>
        <permission>root:supergroup:0777</permission>
        <nsquota>-1</nsquota>
        <dsquota>-1</dsquota>
    </inode>
    <inode>
        <id>16413</id>
        <type>FILE</type>
        <name>job_1592375222804_0001-1592375231176-root-word+count-
            1592375281926-1-1-SUCCEEDED-default-1592375261492.jhist</name>
        <replication>3</replication>
        <mtime>1592375282039</mtime>
        <atime>1592375281980</atime>
        <preferredBlockSize>134217728</preferredBlockSize>
        <permission>root:supergroup:0777</permission>
        <blocks>
            <block>
                <id>1073741834</id>
                <genstamp>1010</genstamp>
                <numBytes>33584</numBytes>
            </block>
        </blocks>
        <storagePolicyId>0</storagePolicyId>
    </inode>
    <inode>
        <id>16414</id>
        <type>FILE</type>
        <name>job_1592375222804_0001_conf.xml</name>
        <replication>3</replication>
        <mtime>1592375282121</mtime>
        <atime>1592375282053</atime>
        <preferredBlockSize>134217728</preferredBlockSize>
        <permission>root:supergroup:0777</permission>
        <blocks>
            <block>
                <id>1073741835</id>
                <genstamp>1011</genstamp>
                <numBytes>196027</numBytes>
            </block>
        </blocks>
        <storagePolicyId>0</storagePolicyId>
    </inode>
    <inode>
        <id>16415</id>
        <type>DIRECTORY</type>
        <name>done</name>
        <mtime>1592376776670</mtime>
        <permission>root:supergroup:0777</permission>
        <nsquota>-1</nsquota>
        <dsquota>-1</dsquota>
    </inode>
    <inode>
        <id>16427</id>
        <type>DIRECTORY</type>
        <name>logs</name>
        <mtime>1592378009623</mtime>
        <permission>root:root:0770</permission>
        <nsquota>-1</nsquota>
        <dsquota>-1</dsquota>
    </inode>
    <inode>
        <id>16428</id>
        <type>DIRECTORY</type>
        <name>application_1592376944601_0001</name>
        <mtime>1592378045481</mtime>
        <permission>root:root:0770</permission>
        <nsquota>-1</nsquota>
        <dsquota>-1</dsquota>
    </inode>
    <inode>
        <id>16430</id>
        <type>DIRECTORY</type>
        <name>wcoutput</name>
        <mtime>1592378037463</mtime>
        <permission>root:supergroup:0755</permission>
        <nsquota>-1</nsquota>
        <dsquota>-1</dsquota>
    </inode>
    <inode>
        <id>16436</id>
        <type>FILE</type>
        <name>part-r-00000</name>
        <replication>3</replication>
        <mtime>1592378037264</mtime>
        <atime>1592378037074</atime>
        <preferredBlockSize>134217728</preferredBlockSize>
        <permission>root:supergroup:0644</permission>
        <blocks>
            <block>
                <id>1073741842</id>
                <genstamp>1018</genstamp>
                <numBytes>43</numBytes>
            </block>
        </blocks>
        <storagePolicyId>0</storagePolicyId>
    </inode>
    <inode>
        <id>16445</id>
        <type>FILE</type>
        <name>linux123_39919</name>
        <replication>3</replication>
        <mtime>1592378045469</mtime>
        <atime>1592378045331</atime>
        <preferredBlockSize>134217728</preferredBlockSize>
        <permission>root:root:0640</permission>
        <blocks>
            <block>
                <id>1073741848</id>
                <genstamp>1024</genstamp>
                <numBytes>56910</numBytes>
            </block>
        </blocks>
        <storagePolicyId>0</storagePolicyId>
    </inode>
    <inode>
        <id>16446</id>
        <type>DIRECTORY</type>
        <name>0617</name>
        <mtime>1592387393490</mtime>
        <permission>root:supergroup:0755</permission>
        <nsquota>-1</nsquota>
        <dsquota>-1</dsquota>
    </inode>
    <inode>
        <id>16449</id>
        <type>FILE</type>
        <name>banzhang.txt</name>
        <replication>1</replication>
        <mtime>1592388309046</mtime>
        <atime>1592388309026</atime>
        <preferredBlockSize>134217728</preferredBlockSize>
        <permission>root:supergroup:0644</permission>
        <storagePolicyId>0</storagePolicyId>
    </inode>
</INodeSection>
</fsimage>

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


在内存元数据中是有记录块所对应的dn信息,但是fsimage中就剔除了这个信息;HDFS集群在启动的时候会加载image以及edits文件,block对应的dn信息都没有记录,集群启动时会有一个安全模式(safemode),安全模式就是为了让dn汇报自己当前所持有的block信息给nn来补全元数据。后续每隔一段时间dn都要汇报自己持有的block信息。

6.2.2 Edits文件内容
1. 基本语法
hdfs oev -p 文件类型 -i编辑日志 -o 转换后文件输出路径

[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

2. 案例实操

        <?xml version="1.0" encoding="UTF-8"?>
<EDITS>
<EDITS_VERSION>-63</EDITS_VERSION>
<RECORD>
    <OPCODE>OP_START_LOG_SEGMENT</OPCODE>
    <DATA>
        <TXID>113</TXID>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_SET_PERMISSIONS</OPCODE>
    <DATA>
        <TXID>114</TXID>
        <SRC>/wcoutput/_SUCCESS</SRC>
        <MODE>493</MODE>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_SET_PERMISSIONS</OPCODE>
    <DATA>
        <TXID>115</TXID>
        <SRC>/wcoutput/part-r-00000</SRC>
        <MODE>493</MODE>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_SET_PERMISSIONS</OPCODE>
    <DATA>
        <TXID>116</TXID>
        <SRC>/wcoutput</SRC>
        <MODE>511</MODE>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_SET_PERMISSIONS</OPCODE>
    <DATA>
        <TXID>117</TXID>
        <SRC>/wcoutput/_SUCCESS</SRC>
        <MODE>511</MODE>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_SET_PERMISSIONS</OPCODE>
    <DATA>
        <TXID>118</TXID>
        <SRC>/wcoutput/part-r-00000</SRC>
        <MODE>511</MODE>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_DELETE</OPCODE>
    <DATA>
        <TXID>119</TXID>
        <LENGTH>0</LENGTH>
        <PATH>/wcoutput/part-r-00000</PATH>
        <TIMESTAMP>1592377324171</TIMESTAMP>
        <RPC_CLIENTID></RPC_CLIENTID>
        <RPC_CALLID>-2</RPC_CALLID>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_SET_PERMISSIONS</OPCODE>
    <DATA>
        <TXID>120</TXID>
        <SRC>/</SRC>
        <MODE>511</MODE>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_SET_PERMISSIONS</OPCODE>
    <DATA>
        <TXID>121</TXID>
        <SRC>/tmp</SRC>
        <MODE>511</MODE>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_SET_PERMISSIONS</OPCODE>
    <DATA>
        <TXID>122</TXID>
        <SRC>/tmp/hadoop-yarn</SRC>
        <MODE>511</MODE>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_SET_PERMISSIONS</OPCODE>
    <DATA>
        <TXID>123</TXID>
        <SRC>/tmp/hadoop-yarn/staging</SRC>
        <MODE>511</MODE>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_SET_PERMISSIONS</OPCODE>
    <DATA>
        <TXID>124</TXID>
        <SRC>/tmp/hadoop-yarn/staging/history</SRC>
        <MODE>511</MODE>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_SET_PERMISSIONS</OPCODE>
    <DATA>
        <TXID>125</TXID>
        <SRC>/tmp/hadoop-yarn/staging/history/done</SRC>
        <MODE>511</MODE>
    </DATA>
</RECORD>
<RECORD>
    <OPCODE>OP_SET_PERMISSIONS</OPCODE>
    <DATA>
        <TXID>126</TXID>
        <SRC>/tmp/hadoop-yarn/staging/history/done/2020</SRC>
        <MODE>511</MODE>
    </DATA>
</RECORD>
<RECORD>

备注:Edits中只记录了更新相关的操作,查询或者下载文件并不会记录在内!!
问题:NameNode启动时如何确定加载哪些Edits文件呢?
nn启动时需要加载fsimage文件以及那些没有被2nn进行合并的edits文件,nn如何判断哪些edits已经被合并了呢?
可以通过fsimage文件自身的编号来确定哪些已经被合并。

6.3 checkpoint周期
[hdfs-default.xml]

<!-- 定时一小时 -->
<property>
<name>dfs.namenode.checkpoint.period</name>
<value>3600</value>
</property>
<!-- 一分钟检查一次操作次数,当操作次数达到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 >


第七节 NN故障处理


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

  • 1. 将2NN的元数据拷贝到NN的节点下,此种方式会存在元数据的丢失。
  • 2. 搭建HDFS的HA(高可用)集群,解决NN的单点故障问题!!(借助Zookeeper实现HA,一个Active的NameNode,一个是Standby的NameNode)

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

 

高级命令
HDFS文件限额配置

  • HDFS文件的限额配置允许我们以文件大小或者文件个数来限制我们在某个目录下上传的文件数量或者文件内容总量,以便达到我们类似百度网盘网盘等限制每个用户允许上传的最大的文件的量

1. 数量限额

hdfs dfs -mkdir -p /user/root/lagou #创建hdfs文件夹
hdfs dfsadmin -setQuota 2 /user/root/lagou # 给该文件夹下面设置最多上传两个文件,上传文件,发现只能上传一个文件
hdfs dfsadmin -clrQuota /user/root/lagou # 清除文件数量限制

2. 空间大小限额

hdfs dfsadmin -setSpaceQuota 4k /user/root/lagou # 限制空间大小4KB
#上传超过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

     

Hadoop归档技术

  • 主要解决HDFS集群存在大量小文件的问题!!由于大量小文件会占用NameNode的内存,因此对于HDFS来说存储大量小文件造成NameNode内存资源的浪费!Hadoop存档文件HAR文件,是一个更高效的文件存档工具,HAR文件是由一组文件通过archive工具创建而来,在减少了NameNode的内存使用的同时,可以对文件进行透明的访问,通俗来说就是HAR文件对NameNode来说是一个文件减少了内存的浪费,对于实际操作处理文件依然是一个一个独立的文件。

案例
1. 启动YARN集群

[root@linux121 hadoop-2.9.2]$ start-yarn.sh

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

3. 查看归档

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

4. 解归档文件

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


第九节 日志采集综合案例

9.1 需求分析

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

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>com.lagou</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>
        <!-- https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-common -->
        <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>


    </dependencies>
</project>

代码实现
LogCollector

import java.util.Timer;

public class LogCollector {
    /*
    - 定时采集已滚动完毕日志文件
    - 将待采集文件上传到临时目录
    - 备份日志文件
     */
    public static void main(String[] args) {
        Timer timer = new Timer();
        //定时采集任务的调度
        // task:采集的业务逻辑
        //延迟时间
        //周期时间
        timer.schedule(new LogCollectorTask(), 0, 3600*1000);
    }
}

Constant 

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";

}

LogCollectorTask

import com.lagou.common.Constant;
import com.lagou.singlton.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;

public class LogCollectorTask extends TimerTask {

    public void run() {

        Properties prop = null;
        try {
            prop = PropTool2.getProp();
        } catch (IOException e) {
            e.printStackTrace();
        }


        //采集的业务逻辑
        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();
        }


    }
}

9.3 代码优化

单例模式
配置文件
PropTool

import com.lagou.collect.LogCollectorTask;

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

public class PropTool2 {

    //volatile关键字是java中禁止指令重排序的关键字,保证有序性和可见性
    private static volatile Properties prop=null;



    //出现线程安全问题
    public static  Properties getProp() throws IOException {
        if(prop ==null){
            synchronized ("lock"){
                if(prop ==null){
                    prop=new Properties();
                    prop.load(LogCollectorTask.class.getClassLoader()
                            .getResourceAsStream("collector.properties"));
                }
            }
        }

        return prop;
    }

}

第六部分 MapReduce编程框架


第一节 MapReduce思想


MapReduce思想在生活中处处可见。我们或多或少都曾接触过这种思想。MapReduce的思想核心是分而治之,充分利用了并行处理的优势。即使是发布过论文实现分布式计算的谷歌也只是实现了这种思想,而不是自己原创。
MapReduce任务过程是分为两个处理阶段:

  • Map阶段:Map阶段的主要作用是“分”,即把复杂的任务分解为若干个“简单的任务”来并行处理。
  • Map阶段的这些任务可以并行计算,彼此间没有依赖关系。
  • Reduce阶段:Reduce阶段的主要作用是“合”,即对map阶段的结果进行全局汇总。

再次理解MapReduce的思想


第二节 官方WordCount案例源码解析

经过查看分析官方WordCount案例源码我们发现一个统计单词数量的MapReduce程序的代码由三个部分组成,

  • Mapper类
  • Reducer类
  • 运行作业的代码(Driver)

Mapper类继承了org.apache.hadoop.mapreduce.Mapper类重写了其中的map方法,Reducer类继承了org.apache.hadoop.mapreduce.Reducer类重写了其中的reduce方法。
重写的Map方法作用:map方法其中的逻辑就是用户希望mr程序map阶段如何处理的逻辑;
重写的Reduce方法作用:reduce方法其中的逻辑是用户希望mr程序reduce阶段如何处理的逻辑;


1. Hadoop序列化


为什么进行序列化?
序列化主要是我们通过网络通信传输数据时或者把对象持久化到文件,需要把对象序列化成二进制的结构。

观察源码时发现自定义Mapper类与自定义Reducer类都有泛型类型约束,比如自定义Mapper有四个形参类型,但是形参类型并不是常见的java基本类型。


为什么Hadoop要选择建立自己的序列化格式而不使用java自带serializable?
序列化在分布式程序中非常重要,在Hadoop中,集群中多个节点的进程间的通信是通过RPC(远程过程调用:Remote Procedure Call)实现;RPC将消息序列化成二进制流发送到远程节点,远程节点再将接收到的二进制数据反序列化为原始的消息,因此RPC往往追求如下特点:

  • 紧凑:数据更紧凑,能充分利用网络带宽资源
  • 快速:序列化和反序列化的性能开销更低

Hadoop使用的是自己的序列化格式Writable,它比java的序列化serialization更紧凑速度更快。一个对象使用Serializable序列化后,会携带很多额外信息比如校验信息,Header,继承体系等。
Java基本类型与Hadoop常用序列化类型

第三节 MapReduce编程规范及示例编写

3.1 Mapper类

 

  • 用户自定义一个Mapper类继承Hadoop的Mapper类
  • Mapper的输入数据是KV对的形式(类型可以自定义)
  • Map阶段的业务逻辑定义在map()方法中
  • Mapper的输出数据是KV对的形式(类型可以自定义)
  • 注意:map()方法是对输入的一个KV对调用一次!!

3.2 Reducer类

  • 用户自定义Reducer类要继承Hadoop的Reducer类
  • Reducer的输入数据类型对应Mapper的输出数据类型(KV对)
  • Reducer的业务逻辑写在reduce()方法中
  • Reduce()方法是对相同K的一组KV对调用执行一次

3.3 Driver阶段


创建提交YARN集群运行的Job对象,其中封装了MapReduce程序运行所需要的相关参数入输入数据路径,输出数据路径等,也相当于是一个YARN集群的客户端,主要作用就是提交我们MapReduce程序运行。

3.4 WordCount代码实现

3.4.1 需求
在给定的文本文件中统计输出每一个单词出现的总次数
输入数据:wc.txt;
输出:

apache 2
clickhouse 2
hadoop 1
mapreduce 1
spark 2
xiaoming 1

3.4.2 具体步骤
按照MapReduce编程规范,分别编写Mapper,Reducer,Driver。
1. 新建maven工程
1. 导入hadoop依赖 

<?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.lagou.hadoop</groupId>
    <artifactId>wordcount</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>

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

    <!--maven打包插件 -->
    <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>

                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

注意:以上依赖第一次需要联网下载!!
2. 添加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

2. 整体思路梳理(仿照源码)
Map阶段:

  • 1. map()方法中把传入的数据转为String类型
  • 2. 根据空格切分出单词
  • 3. 输出<单词,1>

Reduce阶段:

  • 1. 汇总各个key(单词)的个数,遍历value数据进行累加
  • 2. 输出key的总数

Driver

  • 1. 获取配置文件对象,获取job对象实例
  • 2. 指定程序jar的本地路径
  • 3. 指定Mapper/Reducer类
  • 4. 指定Mapper输出的kv数据类型
  • 5. 指定最终输出的kv数据类型
  • 6. 指定job处理的原始数据路径
  • 7. 指定job输出结果路径
  • 8. 提交作业

3. 编写Mapper类

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;

//需求:单词计数
//1 继承Mapper类
//2 Mapper类的泛型参数:共4个,2对kv
//2.1 第一对kv:map输入参数类型
//2.2 第二队kv:map输出参数类型
// LongWritable, Text-->文本偏移量(后面不会用到),一行文本内容
//Text, IntWritable-->单词,1
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    //3 重写Mapper类的map方法
    /*
    1 接收到文本内容,转为String类型
    2 按照空格进行切分
    3 输出<单词,1>
     */

    //提升为全局变量,避免每次执行map方法都执行此操作
    final Text word = new Text();
    final IntWritable one = new IntWritable(1);

    // LongWritable, Text-->文本偏移量,一行文本内容,map方法的输入参数,一行文本就调用一次map方法
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//        1 接收到文本内容,转为String类型
        final String str = value.toString();
//        2 按照空格进行切分
        final String[] words = str.split(" ");
//        3 输出<单词,1>

        //遍历数据
        for (String s : words) {
            word.set(s);
            context.write(word, one);
        }

    }
}

4. 编写Reducer类

import com.sun.org.apache.bcel.internal.generic.NEW;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

//继承的Reducer类有四个泛型参数,2对kv
//第一对kv:类型要与Mapper输出类型一致:Text, IntWritable
//第二队kv:自己设计决定输出的结果数据是什么类型:Text, IntWritable
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    //1 重写reduce方法

    //Text key:map方法输出的key,本案例中就是单词,
    // Iterable<IntWritable> values:一组key相同的kv的value组成的集合
    /*
    假设map方法:hello 1;hello 1;hello 1
    reduce的key和value是什么?
    key:hello,
    values:<1,1,1>

    假设map方法输出:hello 1;hello 1;hello 1,hadoop 1, mapreduce 1,hadoop 1
    reduce的key和value是什么?
    reduce方法何时调用:一组key相同的kv中的value组成集合然后调用一次reduce方法
    第一次:key:hello ,values:<1,1,1>
    第二次:key:hadoop ,values<1,1>
    第三次:key:mapreduce ,values<1>
     */
    IntWritable total = new IntWritable();

    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        //2 遍历key对应的values,然后累加结果
        int sum = 0;
        for (IntWritable value : values) {
            int i = value.get();
            sum += 1;
        }
        // 3 直接输出当前key对应的sum值,结果就是单词出现的总次数
        total.set(sum);
        context.write(key, total);
    }
}

 

5. 编写Driver驱动类

import org.apache.hadoop.conf.Configuration;
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.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

//封装任务并提交运行
public class WordCountDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        /*
        1. 获取配置文件对象,获取job对象实例
        2. 指定程序jar的本地路径
        3. 指定Mapper/Reducer类
        4. 指定Mapper输出的kv数据类型
        5. 指定最终输出的kv数据类型
        6. 指定job处理的原始数据路径
        7. 指定job输出结果路径
        8. 提交作业
         */
//        1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();
        //针对reduce端输出使用snappy压缩
        conf.set("mapreduce.output.fileoutputformat.compress", "true");
        conf.set("mapreduce.output.fileoutputformat.compress.type", "RECORD");
        conf.set("mapreduce.output.fileoutputformat.compress.codec", "org.apache.hadoop.io.compress.SnappyCodec");
        final Job job = Job.getInstance(conf, "WordCountDriver");
//        2. 指定程序jar的本地路径
        job.setJarByClass(WordCountDriver.class);
//        3. 指定Mapper/Reducer类
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);
//        4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);
//        5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        //5.1 设置使用combiner组件
//        job.setCombinerClass(WordCountCombiner.class);
        job.setCombinerClass(WordCountReducer.class);  //直接使用Reducer作为Combiner组件来使用是可以的!!
        //设置使用CombineTextInputFormat读取数据
        job.setInputFormatClass(CombineTextInputFormat.class);
        //设置虚拟存储切片的最大值4M ,单位是byte
        CombineTextInputFormat.setMaxInputSplitSize(job, 41943040);
        FileInputFormat.setInputPaths(job, new Path(args[0])); //指定读取数据的原始路径
//        7. 指定job输出结果路径
        FileOutputFormat.setOutputPath(job, new Path(args[1])); //指定结果数据输出路径
//        8. 提交作业
        final boolean flag = job.waitForCompletion(true);
        //jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);

    }
}

 6. 运行任务
1. 本地模式
直接Idea中运行驱动类即可
idea运行需要传入参数:


选择editconfiguration

2. Yarn集群模式

  • 把程序打成jar包,改名为wc.jar;上传到Hadoop集群

  • 选择合适的Jar包准备原始数据文件,上传到HDFS的路径,不能是本地路径,因为跨节点运行无法获取数据!!
  • 启动Hadoop集群(Hdfs,Yarn)
  • 使用Hadoop 命令提交任务运行
hadoop jar wc.jar com.lagou.wordcount.WordcountDriver /user/lagou/input /user/lagou/output

Yarn集群任务运行成功展示图

第四节 序列化Writable接口

基本序列化类型往往不能满足所有需求,比如在Hadoop框架内部传递一个自定义bean对象,那么该对象就需要实现Writable序列化接口。


4.1 实现Writable序列化步骤如下

 

  • 1. 必须实现Writable接口
  • 2. 反序列化时,需要反射调用空参构造函数,所以必须有空参构造
public CustomBean() {
super();
}
  • 3. 重写序列化方法
@Override
public void write(DataOutput out) throws IOException {
....
}
  • 4. 重写反序列化方法
@Override
public void readFields(DataInput in) throws IOException {
....
}
  • 5. 反序列化的字段顺序和序列化字段的顺序必须完全一致
  • 6. 方便展示结果数据,需要重写bean对象的toString()方法,可以自定义分隔符
  • 7. 如果自定义Bean对象需要放在Mapper输出KV中的K,则该对象还需实现Comparable接口,因为因为MapReduce框中的Shuffle过程要求对key必须能排序!!
@Override
public int compareTo(CustomBean o) {
// 自定义排序规则
return this.num > o.getNum() ? -1 : 1;
}

4.2 Writable接口案例

1. 需求
统计每台智能音箱设备内容播放时长
原始日志格式

001 001577c3 kar_890809 120.196.100.99 1116 954 200
日志id 设备id appkey(合作硬件厂商) 网络ip 自有内容时长(秒) 第三方内容时长(秒) 网络状态码

输出结果

001577c3 11160 9540 20700
设备id 自有内容时长(秒) 第三方内容时长(秒) 总时长

2. 编写MapReduce程序
1. 创建SpeakBean对象

import org.apache.hadoop.io.Writable;

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

//这个类型是map输出kv中value的类型,需要实现writable序列化接口
public class SpeakBean implements Writable {

    //定义属性
    private Long selfDuration;//自有内容时长
    private Long thirdPartDuration;//第三方内容时长
    private String deviceId;//设备id
    private Long sumDuration;//总时长

    //准备一个空参构造

    public SpeakBean() {
    }


    //序列化方法:就是把内容输出到网络或者文本中
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeLong(selfDuration);
        out.writeLong(thirdPartDuration);
        out.writeUTF(deviceId);
        out.writeLong(sumDuration);
    }

    //有参构造

    public SpeakBean(Long selfDuration, Long thirdPartDuration, String deviceId) {
        this.selfDuration = selfDuration;
        this.thirdPartDuration = thirdPartDuration;
        this.deviceId = deviceId;
        this.sumDuration = this.selfDuration + this.thirdPartDuration;
    }

    //反序列化方法
    @Override
    public void readFields(DataInput in) throws IOException {
        this.selfDuration = in.readLong();//自由时长
        this.thirdPartDuration = in.readLong();//第三方时长
        this.deviceId = in.readUTF();//设备id
        this.sumDuration = in.readLong();//总时长
    }


    public Long getSelfDuration() {
        return selfDuration;
    }

    public void setSelfDuration(Long selfDuration) {
        this.selfDuration = selfDuration;
    }

    public Long getThirdPartDuration() {
        return thirdPartDuration;
    }

    public void setThirdPartDuration(Long thirdPartDuration) {
        this.thirdPartDuration = thirdPartDuration;
    }

    public String getDeviceId() {
        return deviceId;
    }

    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }

    public Long getSumDuration() {
        return sumDuration;
    }

    public void setSumDuration(Long sumDuration) {
        this.sumDuration = sumDuration;
    }


    //为了方便观察数据,重写toString()方法

    @Override
    public String toString() {
        return
                selfDuration +
                        "\t" + thirdPartDuration +
                        "\t" + deviceId + "\t" + sumDuration;
    }
}

2. 编写Mapper类

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

import java.io.IOException;

//四个参数:分为两对kv
//第一对kv:map输入参数的kv类型;k-->一行文本偏移量,v-->一行文本内容
//第二对kv:map输出参数kv类型;k-->map输出的key类型,v:map输出的value类型
public class SpeakMapper extends Mapper<LongWritable, Text, Text, SpeakBean> {
    /*
    1 转换接收到的text数据为String
    2 按照制表符进行切分;得到自有内容时长,第三方内容时长,设备id,封装为SpeakBean
    3 直接输出:k-->设备id,value-->speakbean
     */
    Text device_id = new Text();
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//        1 转换接收到的text数据为String
        final String line = value.toString();
//        2 按照制表符进行切分;得到自有内容时长,第三方内容时长,设备id,封装为SpeakBean
        final String[] fields = line.split("\t");
        //自有内容时长
        String selfDuration = fields[fields.length - 3];
        //第三方内容时长
        String thirdPartDuration = fields[fields.length - 2];
        //设备id
        String deviceId = fields[1];
        final SpeakBean bean = new SpeakBean(Long.parseLong(selfDuration), Long.parseLong(thirdPartDuration), deviceId);
//        3 直接输出:k-->设备id,value-->speakbean
        device_id.set(deviceId);
        context.write(device_id, bean);
    }
}

3. 编写Reducer

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

import java.io.IOException;

public class SpeakReducer extends Reducer<Text, SpeakBean, Text, SpeakBean> {
    @Override
    protected void reduce(Text key, Iterable<SpeakBean> values, Context context) throws IOException, InterruptedException {
        //定义时长累加的初始值
        Long self_duration = 0L;
        Long third_part_duration = 0L;

        //reduce方法的key:map输出的某一个key
        //reduce方法的value:map输出的kv对中相同key的value组成的一个集合
        //reduce 逻辑:遍历迭代器累加时长即可
        for (SpeakBean bean : values) {
            final Long selfDuration = bean.getSelfDuration();
            final Long thirdPartDuration = bean.getThirdPartDuration();
            self_duration += selfDuration;
            third_part_duration += thirdPartDuration;
        }
        //输出,封装成一个bean对象输出
        final SpeakBean bean = new SpeakBean(self_duration, third_part_duration, key.toString());
        context.write(key, bean);
    }
}

4. 编写驱动

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
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;

public class SpeakDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        final Configuration conf = new Configuration();
        final Job job = Job.getInstance(conf, "speakDriver");
        //设置jar包本地路径
        job.setJarByClass(SpeakDriver.class);
        //使用的mapper和reducer
        job.setMapperClass(SpeakMapper.class);
        job.setReducerClass(SpeakReducer.class);
        //map的输出kv类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(SpeakBean.class);
        //设置reduce输出
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(SpeakBean.class);
        //读取的数据路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
        //提交任务
        final boolean flag = job.waitForCompletion(true);
        System.exit(flag ? 0 : 1);
    }
}

mr编程技巧总结

  • 结合业务设计Map输出的key和v,利用key相同则去往同一个reduce的特点!!
  • map()方法中获取到只是一行文本数据尽量不做聚合运算
  • reduce()方法的参数要清楚含义

第五节 MapReduce原理分析

5.1 MapTask运行机制详解

MapTask流程
详细步骤:

  • 1. 首先,读取数据组件InputFormat(默认TextInputFormat)会通过getSplits方法对输入目录中文件进行逻辑切片规划得到splits,有多少个split就对应启动多少个MapTask。split与block的对应关系默认是一对一。
  • 2. 将输入文件切分为splits之后,由RecordReader对象(默认LineRecordReader)进行读取,以\n作为分隔符,读取一行数据,返回<key,value>。Key表示每行首字符偏移值,value表示这一行文本内容。
  • 3. 读取split返回<key,value>,进入用户自己继承的Mapper类中,执行用户重写的map函数。RecordReader读取一行这里调用一次。
  • 4. map逻辑完之后,将map的每条结果通过context.write进行collect数据收集。在collect中,会先对其进行分区处理,默认使用HashPartitioner。

MapReduce提供Partitioner接口,它的作用就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数量取模。默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以订制并设置到job上。

  • 5. 接下来,会将数据写入内存,内存中这片区域叫做环形缓冲区,缓冲区的作用是批量收集map结果,减少磁盘IO的影响。我们的key/value对以及Partition的结果都会被写入缓冲区。当然写入之前,key与value值都会被序列化成字节数组。
  • 环形缓冲区其实是一个数组,数组中存放着key、value的序列化数据和key、value的元数据信息,包括partition、key的起始位置、value的起始位置以及value的长度。环形结构是一个抽象概念。
  • 缓冲区是有大小限制,默认是100MB。当map task的输出结果很多时,就可能会撑爆内存,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写。这个溢写是由单独线程来完成,不影响往缓冲区写map结果的线程。溢写线程启动时不应该阻止map的结果输出,所以整个缓冲区有个溢写的比例spill.percent。这个比例默认是0.8,也就是当缓冲区的数据已经达到阈值(buffer size * spill percent = 100MB * 0.8 = 80MB),溢写线程启动,锁定这80MB的内存,执行溢写过程。Maptask的输出结果还可以往剩下的20MB内存中写,互不影响。
  • 6、当溢写线程启动后,需要对这80MB空间内的key做排序(Sort)。排序是MapReduce模型默认的行为!如果job设置过Combiner,那么现在就是使用Combiner的时候了。将有相同key的key/value对的value加起来,减少溢写到磁盘的数据量。Combiner会优化MapReduce的中间结果,所以它在整个模型中会多次使用。
  • 那哪些场景才能使用Combiner呢?从这里分析,Combiner的输出是Reducer的输入,Combiner绝不能改变最终的计算结果。Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景。比如累加,最大值等。Combiner的使用一定得慎重,如果用好,它对job执行效率有帮助,反之会影响reduce的最终结果。
  • 7. 合并溢写文件:每次溢写会在磁盘上生成一个临时文件(写之前判断是否有combiner),如果map的输出结果真的很大,有多次这样的溢写发生,磁盘上相应的就会有多个临时文件存在。当整个数据处理结束之后开始对磁盘中的临时文件进行merge合并,因为最终的文件只有一个,写入磁盘,并且为这个文件提供了一个索引文件,以记录每个reduce对应数据的偏移量。

至此map整个阶段结束!!
MapTask的一些配置


官方参考地址

https://hadoop.apache.org/docs/r2.9.2/hadoop-mapreduce-client/hadoop-mapreduceclient- core/mapred-default.xml

5.2 MapTask的并行度


1. MapTask并行度思考

  • MapTask的并行度决定Map阶段的任务处理并发度,从而影响到整个Job的处理速度。
  • 思考:MapTask并行任务是否越多越好呢?哪些因素影响了MapTask并行度?

2. MapTask并行度决定机制

  • 数据块:Block是HDFS物理上把数据分成一块一块。
  • 切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。

5.2.1 切片机制源码阅读


默认就是128M;
MapTask并行度是不是越多越好呢?
答案不是,如果一个文件仅仅比128M大一点点也被当成一个split来对待,而不是多个split.

MR框架在并行运算的同时也会消耗更多资源,并行度越高资源消耗也越高,假设129M文件分为两个分片,一个是128M,一个是1M;
对于1M的切片的Maptask来说,太浪费资源。
129M的文件在Hdfs存储的时候会不会切成两块?


5.3 ReduceTask 工作机制

Reduce大致分为copy、sort、reduce三个阶段,重点在前两个阶段。copy阶段包含一个eventFetcher来获取已完成的map列表,由Fetcher线程去copy数据,在此过程中会启动两个merge线程,分别为inMemoryMerger和onDiskMerger,分别将内存中的数据merge到磁盘和将磁盘中的数据进行merge。待数据copy完成之后,copy阶段就完成了,开始进行sort阶段,sort阶段主要是执行finalMerge操作,纯粹的sort阶段,完成之后就是reduce阶段,调用用户定义的reduce函数进行处理。


详细步骤

 

  • Copy阶段,简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求maptask获取属于自己的文件。
  • Merge阶段。这里的merge如map端的merge动作,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端的更为灵活。merge有三种形式:内存到内存;内存到磁盘;磁盘到磁盘。默认情况下第一种形式不启用。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的文件。
  • 合并排序。把分散的数据合并成一个大的数据后,还会再对合并后的数据排序。
  • 对排序后的键值对调用reduce方法,键相等的键值对调用一次reduce方法,每次调用会产生零个或者多个键值对,最后把这些输出的键值对写入到HDFS文件中。

5.4 ReduceTask并行度


ReduceTask的并行度同样影响整个Job的执行并发度和执行效率,但与MapTask的并发数由切片数决定不同,ReduceTask数量的决定是可以直接手动设置:

// 默认值是1,手动设置为4
job.setNumReduceTasks(4);

注意事项

  • 1. ReduceTask=0,表示没有Reduce阶段,输出文件数和MapTask数量保持一致;
  • 2. ReduceTask数量不设置默认就是一个,输出文件数量为1个;
  • 3. 如果数据分布不均匀,可能在Reduce阶段产生倾斜;

5.5 Shuffle机制


map阶段处理的数据如何传递给reduce阶段,是MapReduce框架中最关键的一个流程,这个流程就叫shuffle。
shuffle: 洗牌、发牌——(核心机制:数据分区,排序,分组,combine,合并等过程)

5.5.1 MapReduce的分区与reduceTask的数量

  • 在MapReduce中,通过我们指定分区,会将同一个分区的数据发送到同一个reduce当中进行处理(默认是key相同去往同个分区),例如我们为了数据的统计,我们可以把一批类似的数据发送到同一个reduce当中去,在同一个reduce当中统计相同类型的数据,
  • 如何才能保证相同key的数据去往同个reduce呢?只需要保证相同key的数据分发到同个分区即可。结合以上原理分析我们知道MR程序shuffle机制默认就是这种规则!!

1. 分区源码
翻阅源码验证以上规则,MR程序默认使用的HashPartitioner,保证了相同的key去往同个分区!!

2. 自定义分区
实际生产中需求变化多端,默认分区规则往往不能满足需求,需要结合业务逻辑来灵活控制分区规则以及分区数量!!
如何制定自己需要的分区规则?
具体步骤

  • 1. 自定义类继承Partitioner,重写getPartition()方法
  • 2. 在Driver驱动中,指定使用自定义Partitioner
  • 3. 在Driver驱动中,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask数量。

需求 按照不同的appkey把记录输出到不同的分区中
原始日志格式

001 001577c3 kar_890809 120.196.100.99 1116 954 200
日志id 设备id appkey(合作硬件厂商) 网络ip 自有内容时长(秒) 第三方内容时长(秒) 网络状态码

输出结果

根据appkey把不同厂商的日志数据分别输出到不同的文件中

需求分析

  • 面对业务需求,结合mr的特点,来设计map输出的kv,以及reduce输出的kv数据。
  • 一个ReduceTask对应一个输出文件,因为在shuffle机制中每个reduceTask拉取的都是某一个分区的数据,一个分区对应一个输出文件。
  • 结合appkey的前缀相同的特点,同时不能使用默认分区规则,而是使用自定义分区器,只要appkey前缀相同则数据进入同个分区。

整体思路
Mapper

  • 1. 读取一行文本,按照制表符切分
  • 2. 解析出appkey字段,其余数据封装为PartitionBean对象(实现序列化Writable接口)

3. 设计map()输出的kv,key-->appkey(依靠该字段完成分区),PartitionBean对象作为Value输出

Partition

  • 自定义分区器,实现按照appkey字段的前缀来区分所属分区

Reduce

  • 1. reduce()正常输出即可,无需进行聚合操作

Driver

  • 1. 在原先设置job属性的同时增加设置使用自定义分区器
  • 2. 注意设置ReduceTask的数量(与分区数量保持一致)

Mapper

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

import java.io.IOException;

/*
1. 读取一行文本,按照制表符切分

2. 解析出appkey字段,其余数据封装为PartitionBean对象(实现序列化Writable接口)

3. 设计map()输出的kv,key-->appkey(依靠该字段完成分区),PartitionBean对象作为Value输出
 */
public class PartitionMapper extends Mapper<LongWritable, Text, Text, PartitionBean> {
    final PartitionBean bean = new PartitionBean();
    final Text k = new Text();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        final String[] fields = value.toString().split("\t");
        String appkey = fields[2];

        bean.setId(fields[0]);
        bean.setDeviceId(fields[1]);
        bean.setAppkey(fields[2]);
        bean.setIp(fields[3]);
        bean.setSelfDuration(Long.parseLong(fields[4]));
        bean.setThirdPartDuration(Long.parseLong(fields[5]));
        bean.setStatus(fields[6]);

        k.set(appkey);
        context.write(k, bean); //shuffle开始时会根据k的hashcode值进行分区,但是结合我们自己的业务,默认hash分区方式不能满足需求
    }
}

PartitionBean

import org.apache.hadoop.io.Writable;

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

public class PartitionBean implements Writable {

    //准备一个空参构造


    public PartitionBean() {
    }

    public PartitionBean(String id, String deviceId, String appkey, String ip, Long selfDuration, Long thirdPartDuration, String status) {
        this.id = id;
        this.deviceId = deviceId;
        this.appkey = appkey;
        this.ip = ip;
        this.selfDuration = selfDuration;
        this.thirdPartDuration = thirdPartDuration;
        this.status = status;
    }

    //序列化方法
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(id);
        out.writeUTF(deviceId);
        out.writeUTF(appkey);
        out.writeUTF(ip);
        out.writeLong(selfDuration);
        out.writeLong(thirdPartDuration);
        out.writeUTF(status);

    }

    //反序列化方法  要求序列化与反序列化字段顺序要保持一致
    @Override
    public void readFields(DataInput in) throws IOException {
        this.id = in.readUTF();
        this.deviceId = in.readUTF();
        this.appkey = in.readUTF();
        this.ip = in.readUTF();
        this.selfDuration = in.readLong();
        this.thirdPartDuration = in.readLong();
        this.status = in.readUTF();

    }

    //定义属性
    private String id;//日志id
    private String deviceId;//设备id
    private String appkey;//appkey厂商id
    private String ip;//ip地址
    private Long selfDuration;//自有内容播放时长
    private Long thirdPartDuration;//第三方内容时长
    private String status;//状态码

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getDeviceId() {
        return deviceId;
    }

    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }

    public String getAppkey() {
        return appkey;
    }

    public void setAppkey(String appkey) {
        this.appkey = appkey;
    }

    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public Long getSelfDuration() {
        return selfDuration;
    }

    public void setSelfDuration(Long selfDuration) {
        this.selfDuration = selfDuration;
    }

    public Long getThirdPartDuration() {
        return thirdPartDuration;
    }

    public void setThirdPartDuration(Long thirdPartDuration) {
        this.thirdPartDuration = thirdPartDuration;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

//方便文本中的数据易于观察,重写toString()方法

    @Override
    public String toString() {
        return id + '\t' +
                "\t" + deviceId + '\t' + appkey + '\t' +
                ip + '\t' +
                selfDuration +
                "\t" + thirdPartDuration +
                "\t" + status;
    }
}

CustomPartitioner

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

//Partitioner分区器的泛型是map输出的kv类型
public class CustomPartitioner extends Partitioner<Text, PartitionBean> {
    @Override
    public int getPartition(Text text, PartitionBean partitionBean, int numPartitions) {
        int partition = 0;

        if (text.toString().equals("kar")) {
            //只需要保证满足此if条件的数据获得同个分区编号集合
            partition = 0;
        } else if (text.toString().equals("pandora")) {
            partition = 1;
        } else {
            partition = 2;
        }
        return partition;
    }
}

PartitionReducer

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

import java.io.IOException;

//reduce输入类型:Text,PartitionBean,输出:Text,PartitionBean
public class PartitionReducer extends Reducer<Text, PartitionBean, Text, PartitionBean> {
    @Override
    protected void reduce(Text key, Iterable<PartitionBean> values, Context context) throws IOException, InterruptedException {
        //无需聚合运算,只需要进行输出即可
        for (PartitionBean bean : values) {
            context.write(key, bean);
        }
    }
}

PartitionDriver

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
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;

public class PartitionDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {

        //1 获取配置文件
        final Configuration conf = new Configuration();
        //2 获取job实例
        final Job job = Job.getInstance(conf);
        //3 设置任务相关参数
        job.setJarByClass(PartitionDriver.class);
        job.setMapperClass(PartitionMapper.class);
        job.setReducerClass(PartitionReducer.class);

        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(PartitionBean.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(PartitionBean.class);

        // 4 设置使用自定义分区器
        job.setPartitionerClass(CustomPartitioner.class);
        //5 指定reducetask的数量与分区数量保持一致,分区数量是3
        job.setNumReduceTasks(3); //reducetask不设置默认是1个
//        job.setNumReduceTasks(5);
//        job.setNumReduceTasks(2);
        // 6 指定输入和输出数据路径
        FileInputFormat.setInputPaths(job, new Path("e:/speak.data"));
        FileOutputFormat.setOutputPath(job, new Path("e:/parition/out"));
        // 7 提交任务
        final boolean flag = job.waitForCompletion(true);

        System.exit(flag ? 0 : 1);

    }
}

总结

  • 1. 自定义分区器时最好保证分区数量与reduceTask数量保持一致;
  • 2. 如果分区数量不止1个,但是reduceTask数量1个,此时只会输出一个文件。
  • 3. 如果reduceTask数量大于分区数量,但是输出多个空文件
  • 4. 如果reduceTask数量小于分区数量,有可能会报错。

5.5.2 MapReduce中的Combiner
combiner运行机制:

  • 1. Combiner是MR程序中Mapper和Reducer之外的一种组件
  • 2. Combiner组件的父类就是Reducer
  • 3. Combiner和reducer的区别在于运行的位置
  • 4. Combiner是在每一个maptask所在的节点运行;
  • 5. Combiner的意义就是对每一个maptask的输出进行局部汇总,以减小网络传输量。
  • 6. Combiner能够应用的前提是不能影响最终的业务逻辑,此外,Combiner的输出kv应该跟reducer的输入kv类型要对应起来。

举例说明
假设一个计算平均值的MR任务
Map阶段
2个MapTask
MapTask1输出数据:10,5,15 如果使用Combiner:(10+5+15)/3=10
MapTask2输出数据:2,6 如果使用Combiner:(2+6)/2=4
Reduce阶段汇总
(10+4)/2=7
而正确结果应该是

(10+5+15+2+6)/5=7.6


自定义Combiner实现步骤

  • 自定义一个Combiner继承Reducer,重写Reduce方法
  • 在驱动(Driver)设置使用Combiner(默认是不适用Combiner组件)

1. 改造WordCount程序

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

import java.io.IOException;

//combiner组件的输入和输出类型与map()方法保持一致
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
    final IntWritable total = new IntWritable();

    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int num = 0;
        //进行局部汇总,逻辑是与reduce方法保持一致
        for (IntWritable value : values) {
            final int i = value.get();
            num += i;
        }
        total.set(num);
        //输出单词,累加结果
        context.write(key, total);
    }
}

在驱动(Driver)设置使用Combiner

job.setCombinerClass(WordcountCombiner.class);

验证结果
观察程序运行日志

如果直接使用WordCountReducer作为Combiner使用是否可以?

直接使用Reducer作为Combiner组件来使用是可以的!!

5.6 MapReduce中的排序


排序是MapReduce框架中最重要的操作之一。
MapTask和ReduceTask均会对数据按照key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑.上是否需要。默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
MapTask

  • 它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,
  • 溢写完毕后,它会对磁盘上所有文件进行归并排序。

ReduceTask 当所有数据拷贝完毕后,ReduceTask统-对内存和磁盘上的所有数据进行一次归并排序。
1. 部分排序.

  • MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。

2. 全排序

  • 最终输出结果只有一个文件,且文件内部有序。实现方式是只设置- -个ReduceTask。但该方法在处理大型文件时效率极低,因为- -台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。

3. 辅助排序: ( GroupingComparator分组)

  • 在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。

4. 二次排序.

  • 在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。

5.6.1 WritableComparable


Bean对象如果作为Map输出的key时,需要实现WritableComparable接口并重写compareTo方法指定排序规则
1 全排序
基于统计的播放时长案例的输出结果对总时长进行排序
实现全局排序只能设置一个ReduceTask!!

播放时长案例输出结果

00fdaf3 33180 33420 00fdaf3 66600
00wersa4 30689 35191 00wersa4 65880
0a0fe2 43085 44254 0a0fe2 87339
0ad0s7 31702 29183 0ad0s7 60885
0sfs01 31883 29101 0sfs01 60984
a00df6s 33239 36882 a00df6s 70121
adfd00fd5 30727 31491 adfd00fd5 62218

需求分析
如何设计map()方法输出的key,value
MR框架中shuffle阶段的排序是默认行为,不管你是否需要都会进行排序。
key:把所有字段封装成为一个bean对象,并且指定bean对象作为key输出,如果作为key输出,需要实现排序接口,指定自己的排序规则;
具体步骤
Mapper

  • 1. 读取结果文件,按照制表符进行切分
  • 2. 解析出相应字段封装为SpeakBean
  • 3. SpeakBean实现WritableComparable接口重写compareTo方法
  • 4. map()方法输出kv;key-->SpeakBean,value-->NullWritable.get()

Reducer

  • 1. 循环遍历输出

Mapper代码

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

import java.awt.image.BandCombineOp;
import java.io.IOException;

public class SortMapper extends Mapper<LongWritable, Text, SpeakBean, NullWritable> {
    final SpeakBean bean = new SpeakBean();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        //1 读取一行文本,转为字符串,切分
        final String[] fields = value.toString().split("\t");
        //2 解析出各个字段封装成SpeakBean对象
        bean.setDeviceId(fields[0]);
        bean.setSelfDrutation(Long.parseLong(fields[1]));
        bean.setThirdPartDuration(Long.parseLong(fields[2]));
        bean.setSumDuration(Long.parseLong(fields[4]));
        //3 SpeakBean作为key输出
        context.write(bean, NullWritable.get());
    }
}

Reducer


import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class SortReducer extends Reducer<SpeakBean, NullWritable, SpeakBean, NullWritable> {
    //reduce方法的调用是相同key的value组成一个集合调用一次
    /*
    java中如何判断两个对象是否相等?
    根据equals方法,比较还是地址值
     */
    @Override
    protected void reduce(SpeakBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
        //讨论按照总流量排序这件事情,还需要在reduce端处理吗?因为之前已经利用mr的shuffle对数据进行了排序
        //为了避免前面compareTo方法导致总流量相等被当成对象相等,而合并了key,所以遍历values获取每个key(bean对象)
        for (NullWritable value : values) { //遍历value同时,key也会随着遍历。
            context.write(key, value);
        }
    }
}

Bean对象实现WritableComparable接口

package com.lagou.mr.sort;

import org.apache.hadoop.io.WritableComparable;

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

//因为这个类的实例对象要作为map输出的key,所以要实现writablecomparalbe接口
public class SpeakBean implements WritableComparable<SpeakBean> {
    //定义属性
    private Long selfDrutation;//自有内容播放时长
    private Long thirdPartDuration;//第三方内容播放时长
    private String deviceId;//设备id
    private Long sumDuration;//总时长

    //准备构造方法


    public SpeakBean() {
    }

    public SpeakBean(Long selfDrutation, Long thirdPartDuration, String deviceId, Long sumDuration) {
        this.selfDrutation = selfDrutation;
        this.thirdPartDuration = thirdPartDuration;
        this.deviceId = deviceId;
        this.sumDuration = sumDuration;
    }

    public Long getSelfDrutation() {
        return selfDrutation;
    }

    public void setSelfDrutation(Long selfDrutation) {
        this.selfDrutation = selfDrutation;
    }

    public Long getThirdPartDuration() {
        return thirdPartDuration;
    }

    public void setThirdPartDuration(Long thirdPartDuration) {
        this.thirdPartDuration = thirdPartDuration;
    }

    public String getDeviceId() {
        return deviceId;
    }

    public void setDeviceId(String deviceId) {
        this.deviceId = deviceId;
    }

    public Long getSumDuration() {
        return sumDuration;
    }

    public void setSumDuration(Long sumDuration) {
        this.sumDuration = sumDuration;
    }


    //序列化方法
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeLong(selfDrutation);
        out.writeLong(thirdPartDuration);
        out.writeUTF(deviceId);
        out.writeLong(sumDuration);
    }

    //反序列化方法
    @Override
    public void readFields(DataInput in) throws IOException {
        this.selfDrutation = in.readLong();
        this.thirdPartDuration = in.readLong();
        this.deviceId = in.readUTF();
        this.sumDuration = in.readLong();
    }

    //指定排序规则,我们希望按照总时长进行排序
    @Override
    public int compareTo(SpeakBean o) {  //返回值三种:0:相等 1:小于 -1:大于
        System.out.println("compareTo 方法执行了。。。");
        //指定按照bean对象的总时长字段的值进行比较
        if (this.sumDuration > o.sumDuration) {
            return -1;
        } else if (this.sumDuration < o.sumDuration) {
            return 1;
        } else {
            return 0; //加入第二个判断条件,二次排序
        }

    }

    @Override
    public boolean equals(Object o) {
        System.out.println("equals方法执行了。。。");
       return super.equals(o);
    }

    @Override
    public int hashCode() {
        return Objects.hash(getSelfDrutation(), getThirdPartDuration(), getDeviceId(), getSumDuration());
    }

    @Override
    public String toString() {
        return selfDrutation +
                "\t" + thirdPartDuration +
                "\t" + deviceId + '\t' +
                sumDuration
                ;
    }
}

Driver

package com.lagou.mr.sort;

import com.lagou.mr.wc.WordCountDriver;
import com.lagou.mr.wc.WordCountMapper;
import com.lagou.mr.wc.WordCountReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
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;

public class SortDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
         /*
        1. 获取配置文件对象,获取job对象实例
        2. 指定程序jar的本地路径
        3. 指定Mapper/Reducer类
        4. 指定Mapper输出的kv数据类型
        5. 指定最终输出的kv数据类型
        6. 指定job处理的原始数据路径
        7. 指定job输出结果路径
        8. 提交作业
         */
//        1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();

        final Job job = Job.getInstance(conf, "SortDriver");
//        2. 指定程序jar的本地路径
        job.setJarByClass(SortDriver.class);
//        3. 指定Mapper/Reducer类
        job.setMapperClass(SortMapper.class);
        job.setReducerClass(SortReducer.class);
//        4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(SpeakBean.class);
        job.setMapOutputValueClass(NullWritable.class);
//        5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(SpeakBean.class);
        job.setOutputValueClass(NullWritable.class);

        //指定reduceTask的数量,默认是1个
        job.setNumReduceTasks(1);
//        6. 指定job处理的原始数据路径
        //import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
        //import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
        FileInputFormat.setInputPaths(job, new Path("E:\\speak\\out")); //指定读取数据的原始路径
//        7. 指定job输出结果路径
        FileOutputFormat.setOutputPath(job, new Path("e:\\speak\\sortout")); //指定结果数据输出路径
//        8. 提交作业
        final boolean flag = job.waitForCompletion(true);
        //jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);

    }
}

总结

  • 1. 自定义对象作为Map的key输出时,需要实现WritableComparable接口,排序:重写compareTo()方法,序列以及反序列化方法
  • 2. 再次理解reduce()方法的参数;reduce()方法是map输出的kv中key相同的kv中的v组成一个集合调用一次reduce()方法,选择遍历values得到所有的key.
  • 3. 默认reduceTask数量是1个;
  • 4. 对于全局排序需要保证只有一个reduceTask!!

2 分区排序(默认的分区规则,区内有序)

5.6.2 GroupingComparator


GroupingComparator是mapreduce当中reduce端的一个功能组件,主要的作用是决定哪些数据作为一组,调用一次reduce的逻辑,默认是每个不同的key,作为多个不同的组,每个组调用一次reduce逻辑,我们可以自定义GroupingComparator实现不同的key作为同一个组,调用一次reduce逻辑。

1. 需求
原始数据


需要求出每一个订单中成交金额最大的一笔交易。
2. 实现思路
Mapper

  • 读取一行文本数据,切分出每个字段;
  • 订单id和金额封装为一个Bean对象,Bean对象的排序规则指定为先按照订单Id排序,订单Id相等再按照金额降序排;
  • map()方法输出kv;key-->bean对象,value-->NullWritable.get();

Shuffle

  • 指定分区器,保证相同订单id的数据去往同个分区(自定义分区器)
  • 指定GroupingComparator,分组规则指定只要订单Id相等则认为属于同一组;

Reduce

  • 每个reduce()方法写出一组key的第一个

参考代码
OrderBean
OrderBean定义两个字段,一个字段是orderId,第二个字段是金额(注意金额一定要使用Double或者DoubleWritable类型,否则没法按照金额顺序排序)
排序规则指定为先按照订单Id排序,订单Id相等再按照金额降序排!!

package com.lagou.mr.group;

import org.apache.hadoop.io.WritableComparable;

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

public class OrderBean implements WritableComparable<OrderBean> {

    private String orderId;//订单id
    private Double price;//金额


    public OrderBean(String orderId, Double price) {
        this.orderId = orderId;
        this.price = price;
    }

    public OrderBean() {
    }

    public String getOrderId() {
        return orderId;
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public Double getPrice() {
        return price;
    }

    public void setPrice(Double price) {
        this.price = price;
    }

    //指定排序规则,先按照订单id比较再按照金额比较,按照金额降序排
    @Override
    public int compareTo(OrderBean o) {
        int res = this.orderId.compareTo(o.getOrderId()); //0 1 -1
        if (res == 0) {
            //订单id相同,比较金额
            res = - this.price.compareTo(o.getPrice());

        }
        return res;
    }

    //序列化
    @Override
    public void write(DataOutput out) throws IOException {

        out.writeUTF(orderId);
        out.writeDouble(price);
    }

    //反序列化
    @Override
    public void readFields(DataInput in) throws IOException {
        this.orderId = in.readUTF();
        this.price = in.readDouble();
    }

    //重写toString()

    @Override
    public String toString() {
        return orderId + '\t' +
                price
                ;
    }
}

自定义分区器
保证ID相同的订单去往同个分区最终去往同一个Reduce中

import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Partitioner;

public class CustomPartitioner extends Partitioner<OrderBean, NullWritable> {
    @Override
    public int getPartition(OrderBean orderBean, NullWritable nullWritable, int numPartitions) {
        //希望订单id相同的数据进入同个分区

        return (orderBean.getOrderId().hashCode() & Integer.MAX_VALUE) % numPartitions;
    }
}

自定义GroupingComparator
保证id相同的订单进入一个分组中,进入分组的数据已经是按照金额降序排序。reduce()方法取出第一个即是金额最高的交易

import com.sun.corba.se.impl.orb.ParserTable;
import org.apache.hadoop.io.WritableComparable;
import org.apache.hadoop.io.WritableComparator;

public class CustomGroupingComparator extends WritableComparator {

    public CustomGroupingComparator() {
        super(OrderBean.class, true); //注册自定义的GroupingComparator接受OrderBean对象
    }

    //重写其中的compare方法,通过这个方法来让mr接受orderid相等则两个对象相等的规则,key相等

    @Override
    public int compare(WritableComparable a, WritableComparable b) { //a 和b是orderbean的对象
        //比较两个对象的orderid
        final OrderBean o1 = (OrderBean) a;
        final OrderBean o2 = (OrderBean) b;
        return o1.getOrderId().compareTo(o2.getOrderId()); // 0 1 -1
    }
}

Mapper

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

import java.io.IOException;

public class GroupMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable> {
    OrderBean bean = new OrderBean();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        final String[] fields = value.toString().split("\t");
        //订单id与jine封装为一个orderBean
        bean.setOrderId(fields[0]);
        bean.setPrice(Double.parseDouble(fields[2]));
        context.write(bean, NullWritable.get());
    }
}

Reducer

import org.apache.avro.Schema;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class GroupReducer extends Reducer<OrderBean, NullWritable, OrderBean, NullWritable> {

    //key:reduce方法的key注意是一组相同key的kv的第一个key作为传入reduce方法的key,因为我们已经指定了排序的规则
    //按照金额降序排列,则第一个key就是金额最大的交易数据
    //value:一组相同key的kv对中v的集合
    //对于如何判断key是否相同,自定义对象是需要我们指定一个规则,这个规则通过Groupingcomaprator来指定
    @Override
    protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
        //直接输出key就是金额最大的交易
        context.write(key, NullWritable.get());
    }
}

Driver

package com.lagou.mr.group;

import com.lagou.mr.wc.WordCountDriver;
import com.lagou.mr.wc.WordCountMapper;
import com.lagou.mr.wc.WordCountReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
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;

public class GroupDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
         /*
        1. 获取配置文件对象,获取job对象实例
        2. 指定程序jar的本地路径
        3. 指定Mapper/Reducer类
        4. 指定Mapper输出的kv数据类型
        5. 指定最终输出的kv数据类型
        6. 指定job处理的原始数据路径
        7. 指定job输出结果路径
        8. 提交作业
         */
//        1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();

        final Job job = Job.getInstance(conf, "GroupDriver");
//        2. 指定程序jar的本地路径
        job.setJarByClass(GroupDriver.class);
//        3. 指定Mapper/Reducer类
        job.setMapperClass(GroupMapper.class);
        job.setReducerClass(GroupReducer.class);
//        4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(OrderBean.class);
        job.setMapOutputValueClass(NullWritable.class);
//        5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(OrderBean.class);
        job.setOutputValueClass(NullWritable.class);

        //指定分区器
        job.setPartitionerClass(CustomPartitioner.class);
        //指定使用groupingcomparator
        job.setGroupingComparatorClass(CustomGroupingComparator.class);
        FileInputFormat.setInputPaths(job, new Path("E:\\teach\\hadoop框架\\资料\\data\\GroupingComparator")); //指定读取数据的原始路径
//        7. 指定job输出结果路径
        FileOutputFormat.setOutputPath(job, new Path("E:\\group\\out")); //指定结果数据输出路径

        //指定reducetask的数量,不要使用默认的一个,分区效果不明显
        job.setNumReduceTasks(2);
//        8. 提交作业
        final boolean flag = job.waitForCompletion(true);
        //jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);

    }
}

5.7 MapReduce Join实战


5.7.1 MR reduce端join
1.1 需求分析
需求:
投递行为数据表deliver_info:

职位表position

假如数据量巨大,两表的数据是以文件的形式存储在HDFS中,需要用mapreduce程序来实现一下SQL查询运算
1.2 代码实现
通过将关联的条件作为map输出的key,将两表满足join条件的数据并携带数据所来源的文件信息,发往同一个reduce task,在reduce中进行数据的串联
Driver

import com.lagou.mr.wc.WordCountCombiner;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
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;
public class ReduceJoinDriver {
    public static void main(String[] args) throws IOException,
            ClassNotFoundException, InterruptedException {
// 1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();
        final Job job = Job.getInstance(conf, "ReduceJoinDriver");
// 2. 指定程序jar的本地路径
        job.setJarByClass(ReduceJoinDriver.class);
// 3. 指定Mapper/Reducer类
        job.setMapperClass(ReduceJoinMapper.class);
        job.setReducerClass(ReduceJoinReducer.class);
// 4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(DeliverBean.class);
// 5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(DeliverBean.class);
        job.setOutputValueClass(NullWritable.class);
// 6. 指定job输出结果路径
        FileInputFormat.setInputPaths(job, new Path(args[0])); //指定读取数据的原始路径
// 7. 指定job输出结果路径
        FileOutputFormat.setOutputPath(job, new Path(args[1])); //指定结果数据输出路径
// 8. 提交作业
        final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);
    }
}

Mapper

package com.lagou;

import com.sun.org.apache.bcel.internal.generic.NEW;
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 org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
public class ReduceJoinMapper extends Mapper<LongWritable, Text, Text,
        DeliverBean> {
    String name;
    DeliverBean bean = new DeliverBean();
    Text k = new Text();
    @Override
    protected void setup(Context context) throws IOException,
            InterruptedException {
// 1 获取输入文件切片
        FileSplit split = (FileSplit) context.getInputSplit();
// 2 获取输入文件名称
        name = split.getPath().getName();
    }
    @Override
    protected void map(LongWritable key, Text value, Context context) throws
            IOException, InterruptedException {
// 1 获取输入数据
        String line = value.toString();
// 2 不同文件分别处理
        if (name.startsWith("deliver_info")) {
// 2.1 切割
            String[] fields = line.split("\t");
// 2.2 封装bean对象
            bean.setUserId(fields[0]);
            bean.setPositionId(fields[1]);
            bean.setDate(fields[2]);
            bean.setPositionName("");
            bean.setFlag("deliver");
            k.set(fields[1]);
        } else {
// 2.3 切割
            String[] fields = line.split("\t");
// 2.4 封装bean对象
            bean.setPositionId(fields[0]);
            bean.setPositionName(fields[1]);
            bean.setUserId("");
            bean.setDate("");
            bean.setFlag("position");
            k.set(fields[0]);
        }
// 3 写出
        context.write(k, bean);
    }
}

Reducer

package com.lagou;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.ArrayList;
public class ReduceJoinReducer extends Reducer<Text, DeliverBean, DeliverBean,
        NullWritable> {
    @Override
    protected void reduce(Text key, Iterable<DeliverBean> values, Context
            context) throws IOException, InterruptedException {
// 1准备投递行为数据的集合
        ArrayList<DeliverBean> deBeans = new ArrayList<>();
// 2 准备bean对象
        DeliverBean pBean = new DeliverBean();
        for (DeliverBean bean : values) {
            if ("deliver".equals(bean.getFlag())) {//
                DeliverBean dBean = new DeliverBean();
                try {
                    BeanUtils.copyProperties(dBean, bean);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                deBeans.add(dBean);
            } else {
                try {
                    BeanUtils.copyProperties(pBean, bean);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
// 3 表的拼接
        for (DeliverBean bean : deBeans) {
            bean.setPositionName(pBean.getPositionName());
// 4 数据写出去
            context.write(bean, NullWritable.get());
        }
    }
}

Bean

package com.lagou;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class DeliverBean implements Writable {
    private String userId;
    private String positionId;
    private String date;
    private String positionName;
    private String flag;
    public DeliverBean() {
    }
    public DeliverBean(String userId, String positionId, String date, String
            positionName,
                       String flag) {
        this.userId = userId;
        this.positionId = positionId;
        this.date = date;
        this.positionName = positionName;
        this.flag = flag;
    }
    public String getUserId() {
        return userId;
    }
    public void setUserId(String userId) {
        this.userId = userId;
    }
    public String getPositionId() {
        return positionId;
    }
    public void setPositionId(String positionId) {
        this.positionId = positionId;
    }
    public String getDate() {
        return date;
    }
    public void setDate(String date) {
        this.date = date;
    }
    public String getPositionName() {
        return positionName;
    }
    public void setPositionName(String positionName) {
        this.positionName = positionName;
    }
    public String getFlag() {
        return flag;
    }
    public void setFlag(String flag) {
        this.flag = flag;
    }
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(userId);
        out.writeUTF(positionId);
        out.writeUTF(date);
        out.writeUTF(positionName);
        out.writeUTF(flag);
    }
    @Override
    public void readFields(DataInput in) throws IOException {
        this.userId = in.readUTF();
        this.positionId = in.readUTF();
        this.date = in.readUTF();
        this.positionName = in.readUTF();
        this.flag=in.readUTF();
    }
    @Override
    public String toString() {
        return "DeliverBean{" +
                "userId='" + userId + '\'' +
                ", positionId='" + positionId + '\'' +
                ", date='" + date + '\'' +
                ", positionName='" + positionName ;
    }
}

缺点:这种方式中,join的操作是在reduce阶段完成,reduce端的处理压力太大,map节点的运算负载则很低,资源利用率不高,且在reduce阶段极易产生数据倾斜
解决方案: map端join实现方式
5.7.2 MR map端join
2.1 需求分析
适用于关联表中有小表的情形;
可以将小表分发到所有的map节点,这样,map节点就可以在本地对自己所读到的大表数据进行join并输出最终结果,可以大大提高join操作的并发度,加快处理速度
2.2 代码实现

  • 在Mapper的setup阶段,将文件读取到缓存集合中
  • 在驱动函数中加载缓存。

// 缓存普通文件到Task运行节点。
job.addCacheFile(new URI("file:///e:/cache/position.txt"));

Driver

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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;
import java.net.URISyntaxException;
public class MapJoinDriver {
    public static void main(String[] args) throws IOException,
            ClassNotFoundException, InterruptedException, URISyntaxException {
// 1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();
        final Job job = Job.getInstance(conf, "ReduceJoinDriver");
// 2. 指定程序jar的本地路径
        job.setJarByClass(MapJoinDriver.class);
// 3. 指定Mapper类
        job.setMapperClass(MapJoinMapper.class);
// 4. 指定最终输出的kv数据类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(NullWritable.class);
//5.指定job读取数据路径
        FileInputFormat.setInputPaths(job, new Path(args[0])); //指定读取数据的原始
        路径
// 6. 指定job输出结果路径
        FileOutputFormat.setOutputPath(job, new Path(args[1])); //指定结果数据输出
        路径
// 7.加载缓存文件
        job.addCacheFile(new URI("file:///E:/map_join/cache/position.txt"));
        job.setNumReduceTasks(0);
// 8. 提交作业
        final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);
    }
}

Mapper

import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
public class MapJoinMapper extends Mapper<LongWritable, Text, Text,
        NullWritable> {
    String name;
    DeliverBean bean = new DeliverBean();
    Text k = new Text();
    Map<String, String> pMap = new HashMap<>();
    //读取文件
    @Override
    protected void setup(Context context) throws IOException,
            InterruptedException {
// 1 获取缓存的文件
        BufferedReader reader = new BufferedReader(new InputStreamReader(new
                FileInputStream("position.txt"),"UTF-8"));
        String line;
        while(StringUtils.isNotEmpty(line = reader.readLine())){
// 2 切割
            String[] fields = line.split("\t");
// 3 缓存数据到集合
            pMap.put(fields[0], fields[1]);
        }
// 4 关流
        reader.close();
    }
    @Override
    protected void map(LongWritable key, Text value, Context context) throws
            IOException, InterruptedException {
// 1 获取一行
        String line = value.toString();
// 2 截取
        String[] fields = line.split("\t");
// 3 获取职位id
        String pId = fields[1];
// 4 获取职位名称
        String pName = pMap.get(pId);
// 5 拼接
        k.set(line + "\t"+ pName);
// 写出
        context.write(k, NullWritable.get());
    }
}

DeliverBean

import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class DeliverBean implements Writable {
    private String userId;
    private String positionId;
    private String date;
    private String positionName;
    private String flag;
    public DeliverBean() {
    }
    public DeliverBean(String userId, String positionId, String date, String
            positionName, String flag) {
        this.userId = userId;
        this.positionId = positionId;
        this.date = date;
        this.positionName = positionName;
        this.flag = flag;
    }
    public String getUserId() {
        return userId;
    }
    public void setUserId(String userId) {
        this.userId = userId;
    }
    public String getPositionId() {
        return positionId;
    }
    public void setPositionId(String positionId) {
        this.positionId = positionId;
    }
    public String getDate() {
        return date;
    }
    public void setDate(String date) {
        this.date = date;
    }
    public String getPositionName() {
        return positionName;
    }
    public void setPositionName(String positionName) {
        this.positionName = positionName;
    }
    public String getFlag() {
        return flag;
    }
    public void setFlag(String flag) {
        this.flag = flag;
    }
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(userId);
        out.writeUTF(positionId);
        out.writeUTF(date);
        out.writeUTF(positionName);
        out.writeUTF(flag);
    }
    @Override
    public void readFields(DataInput in) throws IOException {
        this.userId = in.readUTF();
        this.positionId = in.readUTF();
        this.date = in.readUTF();
        this.positionName = in.readUTF();
        this.flag=in.readUTF();
    }
    @Override
    public String toString() {
        return "DeliverBean{" +
                "userId='" + userId + '\'' +
                ", positionId='" + positionId + '\'' +
                ", date='" + date + '\'' +
                ", positionName='" + positionName + '\'' +
                '}';
    }
}

5.7.3 数据倾斜解决方案
什么是数据倾斜?

  • 数据倾斜无非就是大量的相同key被partition分配到一个分区里,

现象

  • 绝大多数task执行得都非常快,但个别task执行的极慢。甚至失败!

通用解决方案:

  • 对key增加随机数。

5.8 MapReduce读取和输出数据


5.8.1 InputFormat
运行MapReduce程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。那么,针对不同的数据类型,MapReduce是如何读取这些数据的呢?
InputFormat是MapReduce框架用来读取数据的类。
InputFormat常见子类包括:

  • TextInputFormat (普通文本文件,MR框架默认的读取实现类型)
  • KeyValueTextInputFormat(读取一行文本数据按照指定分隔符,把数据封装为kv类型)
  • NLineInputF ormat(读取数据按照行数进行划分分片)
  • CombineTextInputFormat(合并小文件,避免启动过多MapTask任务)
  • 自定义InputFormat

1. CombineTextInputFormat案例
MR框架默认的TextInputFormat切片机制按文件划分切片,文件无论多小,都是单独一个切片,然后由一个MapTask处理,如果有大量小文件,就对应的会生成并启动大量的 MapTask,而每个MapTask处理的数据量很小大量时间浪费在初始化资源启动收回等阶段,这种方式导致资源利用率不高。
CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上划分成一个切片,这样多个小文件就可以交给一个MapTask处理,提高资源利用率。
需求
将输入数据中的多个小文件合并为一个切片处理
运行WordCount案例,准备多个小文件
具体使用方式

// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
//虚拟存储切片最大值设置4m
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);

验证切片数量的变化!!
CombineTextInputFormat切片原理
切片生成过程分为两部分:虚拟存储过程和切片过程
假设设置setMaxInputSplitSize值为4M
四个小文件:1.txt -->2M ;2.txt-->7M;3.txt-->0.3M;4.txt--->8.2M

  • 虚拟存储过程:把输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值进行比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)。
  • 比如如setMaxInputSplitSize值为4M,输入文件大小为8.02M,则先逻辑上分出一个4M的块。剩余的大小为4.02M,如果按照4M逻辑划分,就会出现0.02M的非常小的虚拟存储文件,所以将剩余的4.02M文件切分成(2.01M和2.01M)两个文件。
  • 1.txt-->2M;2M<4M;一个块;
  • 2.txt-->7M;7M>4M,但是不大于两倍,均匀分成两块;两块:每块3.5M;
  • 3.txt-->0.3M;0.3<4M ,0.3M<4M ,一个块
  • 4.txt-->8.2M;大于最大值且大于两倍;一个4M的块,剩余4.2M分成两块,每块2.1M
  • 所有块信息:2M,3.5M,3.5M,0.3M,4M,2.1M,2.1M 共7个虚拟存储块。

切片过程

  • 判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片。
  • 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
  • 按照之前输入文件:有4个小文件大小分别为2M、7M、0.3M以及8.2M这四个小文件,则虚拟存储之后形成7个文件块,大小分别为:2M,3.5M,3.5M,0.3M,4M,2.1M,2.1M

最终会形成3个切片,大小分别为:
(2+3.5)M,(3.5+0.3+4)M,(2.1+2.1)M
注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。


2. 自定义InputFormat


HDFS还是MapReduce,在处理小文件时效率都非常低,但又难免面临处理大量小文件的场景,此时,就需要有相应解决方案。可以自定义InputFormat实现小文件的合并。
需求
将多个小文件合并成一个SequenceFile文件(SequenceFile文件是Hadoop用来存储二进制形式的key-value对的文件格式),SequenceFile里面存储着多个文件,存储的形式为文件路径+名称为key,文件内容为value。
结果
得到一个合并了多个小文件的SequenceFile文件
整体思路

  • 1. 定义一个类继承FileInputFormat
  • 2. 重写isSplitable()指定为不可切分;重写createRecordReader()方法,创建自己的RecorderReader对象
  • 3. 改变默认读取数据方式,实现一次读取一个完整文件作为kv输出;
  • 4. Driver指定使用的InputFormat类型

代码参考
自定义InputFormat

import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import java.io.IOException;
public class CustomFileInputformat extends FileInputFormat<Text, BytesWritable>
{
    //文件不可切分
    @Override
    protected boolean isSplitable(JobContext context, Path filename) {
        return false;
    }
    //获取自定义RecordReader对象用来读取数据
    @Override
    public RecordReader<Text, BytesWritable> createRecordReader(InputSplit
                                                                        split, TaskAttemptContext context)
            throws IOException, InterruptedException {
        CustomRecordReader recordReader = new CustomRecordReader();
        recordReader.initialize(split, context);
        return recordReader;
    }
}

自定义RecordReader

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
public class CustomRecordReader extends RecordReader<Text, BytesWritable> {
    private Configuration configuration;
//切片
private FileSplit split;
    //是否读取到内容的标识符
    private boolean isProgress = true;
    //输出的kv
    private BytesWritable value = new BytesWritable();
    private Text k = new Text();
    @Override
    public void initialize(InputSplit split, TaskAttemptContext context) throws
            IOException, InterruptedException {
//获取到文件切片以及配置文件对象
        this.split = (FileSplit) split;
        configuration = context.getConfiguration();
    }
    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {
        if (isProgress) {
// 1 定义缓存区
            byte[] contents = new byte[(int) split.getLength()];
            FileSystem fs = null;
            FSDataInputStream fis = null;
            try {
// 2 获取文件系统
                Path path = split.getPath();
                fs = path.getFileSystem(configuration);
// 3 读取数据
                fis = fs.open(path);
// 4 读取文件内容
                IOUtils.readFully(fis, contents, 0, contents.length);
// 5 输出文件内容
                value.set(contents, 0, contents.length);
// 6 获取文件路径及名称
                String name = split.getPath().toString();
// 7 设置输出的key值
                k.set(name);
            } catch (Exception e) {
            } finally {
                IOUtils.closeStream(fis);
            }
            isProgress = false;
            return true;
        }
        return false;
    }
    @Override
    public Text getCurrentKey() throws IOException, InterruptedException {
//返回key
        return k;
    }
    @Override
    public BytesWritable getCurrentValue() throws IOException,
            InterruptedException {
//返回value
        return value;
    }
    @Override
    public float getProgress() throws IOException, InterruptedException {
        return 0;
    }
    @Override
    public void close() throws IOException {
    }
}

Mapper

import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class SequenceFileMapper extends Mapper<Text,
        BytesWritable,Text,BytesWritable> {
    @Override
    protected void map(Text key, BytesWritable value, Context context) throws
            IOException, InterruptedException {
//读取内容直接输出
        context.write(key, value);
    }
}

Reducer

import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class SequenceFileReducer extends Reducer<Text,
        BytesWritable,Text,BytesWritable> {
    @Override
    protected void reduce(Text key, Iterable<BytesWritable> values, Context
            context) throws IOException, InterruptedException {
//输出value值,其中只有一个BytesWritable 所以直接next取出即可
        context.write(key, values.iterator().next());
    }
}

Driver

import com.lagou.mr.partition.*;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
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 org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;
import java.io.IOException;
public class SequenceFileDriver {
    public static void main(String[] args) throws IOException,
            ClassNotFoundException, InterruptedException {
        final Configuration conf = new Configuration();
        final Job job = Job.getInstance(conf);
        job.setJarByClass(SequenceFileDriver.class);
        job.setMapperClass(SequenceFileMapper.class);
        job.setReducerClass(SequenceFileReducer.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(BytesWritable.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(BytesWritable.class);
        job.setInputFormatClass(CustomFileInputformat.class);
        job.setOutputFormatClass(SequenceFileOutputFormat.class);
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));
        final boolean flag = job.waitForCompletion(true);
        System.exit(flag ? 0 : 1);
    }
}

5.8.2 OutputFormat
OutputFormat:是MapReduce输出数据的基类,所有MapReduce的数据输出都实现了OutputFormat抽象类。下面我们介绍几种常见的OutputFormat子类
TextOutputFormat

  • 默认的输出格式是TextOutputFormat,它把每条记录写为文本行。它的键和值可以是任意类型,因为TextOutputFormat调用toString()方 法把它们转换为字符串。

SequenceFileOutputFormat

  • 将SequenceFileOutputFormat输出作为后续MapReduce任务的输入,这是一种好的输出格式,因为它的格式紧凑,很容易被压缩。

自定义OutputFormat
需求分析

  • 要在一个MapReduce程序中根据数据的不同输出两类结果到不同目录,这类输出需求可以通过自定义OutputFormat来实现。

实现步骤

  • 1. 自定义一个类继承FileOutputFormat。
  • 2. 改写RecordWriter,改写输出数据的方法write()。

需求
网络请求日志数据

http://www.baidu.com
http://www.google.com
http://cn.bing.com
http://www.lagou.com
http://www.sohu.com
http://www.sina.com
http://www.sin2a.com
http://www.sin2desa.com
http://www.sindsafa.com

输出结果
lagou.log

http://www.lagou.com

other.log

http://cn.bing.com
http://www.baidu.com
http://www.google.com
http://www.sin2a.com
http://www.sin2desa.com
http://www.sina.com
http://www.sindsafa.com
http://www.sohu.com

参考代码
Mapper

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

import java.io.IOException;

public class OutputMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        context.write(value, NullWritable.get());
    }
}

Reducer

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


import java.io.IOException;

public class OutputReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
    @Override
    protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
        context.write(key, NullWritable.get());
    }
}

OutputFormat

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;


public class CustomOutputFormat extends FileOutputFormat<Text, NullWritable> {
    //写出数据的对象
    @Override
    public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext context) throws IOException, InterruptedException {
        //定义写出数据的路径信息,并获取到输出流传入writer对象中
        final Configuration conf = context.getConfiguration();
        final FileSystem fs = FileSystem.get(conf);
        //定义输出的路径
        final FSDataOutputStream lagouOut = fs.create(new Path("e:/lagou.log"));
        final FSDataOutputStream otherOut = fs.create(new Path("e:/other.log"));
        CustomWriter customWriter = new CustomWriter(lagouOut, otherOut);
        return customWriter;
    }
}

RecordWriter

import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;

import java.io.IOException;


public class CustomWriter extends RecordWriter<Text, NullWritable> {
    //定义成员变量
    private FSDataOutputStream lagouOut;
    private FSDataOutputStream otherOut;

    //定义构造方法接收两个输出流


    public CustomWriter(FSDataOutputStream lagouOut, FSDataOutputStream otherOut) {
        this.lagouOut = lagouOut;
        this.otherOut = otherOut;
    }

    //写出数据的逻辑,控制写出的路径
    @Override
    public void write(Text key, NullWritable value) throws IOException, InterruptedException {
        //写出数据需要输出流
        final String line = key.toString();
        if (line.contains("lagou")) {
            lagouOut.write(line.getBytes());
            lagouOut.write("\r\n".getBytes());
        } else {
            otherOut.write(line.getBytes());
            otherOut.write("\r\n".getBytes());
        }
    }

    //关闭,释放资源
    @Override
    public void close(TaskAttemptContext context) throws IOException, InterruptedException {

        IOUtils.closeStream(lagouOut);
        IOUtils.closeStream(otherOut);
    }
}

Driver

import com.lagou.mr.wc.WordCountDriver;
import com.lagou.mr.wc.WordCountMapper;
import com.lagou.mr.wc.WordCountReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

public class OutputDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
              /*
        1. 获取配置文件对象,获取job对象实例
        2. 指定程序jar的本地路径
        3. 指定Mapper/Reducer类
        4. 指定Mapper输出的kv数据类型
        5. 指定最终输出的kv数据类型
        6. 指定job处理的原始数据路径
        7. 指定job输出结果路径
        8. 提交作业
         */
//        1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();

        final Job job = Job.getInstance(conf, "OutputDriver");
//        2. 指定程序jar的本地路径
        job.setJarByClass(OutputDriver.class);
//        3. 指定Mapper/Reducer类
        job.setMapperClass(OutputMapper.class);
        job.setReducerClass(OutputReducer.class);
//        4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(NullWritable.class);
//        5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(NullWritable.class);

        //指定使用自定义outputformat
        job.setOutputFormatClass(CustomOutputFormat.class);
        FileInputFormat.setInputPaths(job, new Path("E:\\teach\\hadoop框架\\资料\\data\\click_log")); //指定读取数据的原始路径
//        7. 指定job输出结果路径,因为mr默认要输出一个success等标识文件
        FileOutputFormat.setOutputPath(job, new Path("E:\\clicklog\\out")); //指定结果数据输出路径
//        8. 提交作业
        final boolean flag = job.waitForCompletion(true);
        //jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);
    }
}

5.9 shuffle阶段数据的压缩机制


5.9.1 hadoop当中支持的压缩算法
数据压缩有两大好处,节约磁盘空间,加速数据在网络和磁盘上的传输!!
我们可以使用bin/hadoop checknative 来查看我们编译之后的hadoop支持的各种压缩,如果出现openssl为false,那么就在线安装一下依赖包!!

安装openssl

yum install -y openssl-devel

为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器


常见压缩方式对比分析


5.9.2 压缩位置
Map输入端压缩

  • 此处使用压缩文件作为Map的输入数据,无需显示指定编解码方式,Hadoop会自动检查文件扩展名,如果压缩方式能够匹配,Hadoop就会选择合适的编解码方式对文件进行压缩和解压。

Map输出端压缩

  • Shuffle是Hadoop MR过程中资源消耗最多的阶段,如果有数据量过大造成网络传输速度缓慢,可以考虑使用压缩

Reduce端输出压缩

  • 输出的结果数据使用压缩能够减少存储的数据量,降低所需磁盘的空间,并且作为第二个MR的输入时可以复用压缩。

5.9.3 压缩配置方式
1. 在驱动代码中通过Configuration直接设置使用的压缩方式,可以开启Map输出和Reduce输出压缩

设置map阶段压缩
Configuration configuration = new Configuration();
configuration.set("mapreduce.map.output.compress","true");
configuration.set("mapreduce.map.output.compress.codec","org.apache.hadoop.io.compress.SnappyCodec");
设置reduce阶段的压缩
configuration.set("mapreduce.output.fileoutputformat.compress","true");
configuration.set("mapreduce.output.fileoutputformat.compress.type","RECORD");
configuration.set("mapreduce.output.fileoutputformat.compress.codec","org.apache.hadoop.io.compress.SnappyCodec");

2. 配置mapred-site.xml(修改后分发到集群其它节点,重启Hadoop集群),此种方式对运行在集群的所有MR任务都会执行压缩。

<property>
    <name>mapreduce.output.fileoutputformat.compress</name>
    <value>true</value>
</property>
<property>
    <name>mapreduce.output.fileoutputformat.compress.type</name>
    <value>RECORD</value>
</property>
<property>
    <name>mapreduce.output.fileoutputformat.compress.codec</name>
    <value>org.apache.hadoop.io.compress.SnappyCodec</value>
</property>

5.9.4 压缩案例
需求
使用snappy压缩方式压缩WordCount案例的输出结果数据
具体实现
在驱动代码中添加压缩配置

configuration.set("mapreduce.output.fileoutputformat.compress","true");
configuration.set("mapreduce.output.fileoutputformat.compress.type","RECORD");
configuration.set("mapreduce.output.fileoutputformat.compress.codec","org.apache.hadoop.io.compress.SnappyCodec");

重新打成jar包,提交集群运行,验证输出结果是否已进行了snappy压缩!!


第 六 节 MR综合案例

1.1 需求


现在有一些订单的评论数据,需求,将订单按照好评与差评区分开来,将数据输出到不同的文件目录下,数据内容如下,其中数据第九个字段表示好评,中评,差评。0:好评,1:中评,2:差评。
现需要根据好评,中评,差评把数据分类并输出到不同的目录中,并且要求按照时间顺序降序排列。

300 东西很不错,物流也很快 \N 1 106 131******33 0 2019-02-06 19:10:13
301 还行,洗完有点干,不知道怎么回事 \N 1 106 136******44 0 2019-03-2214:16:41
302 还可以吧,保质期短,感觉貌似更天然些 \N 1 106 134******34 0 2019-04-1013:40:06
303 还可以吧,保质期短,感觉貌似更天然些 \N 1 105 134******33 0 2019-01-1514:40:21
304 还没用,,不知道效果怎么样 \N 1 105 137******66 0 2019-02-28 18:55:43
305 刚收到,还没用,用后再追评!不过,听朋友说好用,才买的! \N 1 105 138******600 2019-03-13 19:10:09
306 一般,感觉用着不是很好,可能我头发太干了 \N 1 105 132******44 0 2019-04-09 10:35:49
307 非常好用,之前买了10支,这次又买了10支,不错,会继续支持! \N 1 103 131******330 2019-01-15 13:10:46
308 喜欢茶树油的 \N 1 103 135******33 0 2019-02-08 14:35:09
309 好像比其他的强一些,继续使用中 \N 1 103 133******99 0 2019-03-1419:55:36
310 感觉洗后头发很干净,头皮有一定改善。 \N 1 103 138******44 0 2019-04-0922:55:59
311 从出生到现在一直都是惠氏 现在宝宝两周半了 \N 1 157 那***情 0 2017-12-01 06:05:30
312 口感不错,孩子很喜欢。推荐。 \N 1 157 w***4 0 2017-12-12 08:35:06
313 价格优惠,日期新鲜,包装完好!发货速度快,非常喜欢!还有赠品! \N 1 157 j***00 2019-01-09 22:55:41

现在有大量类似上面的小文件!


1.2 分析

  • 自定义InputFormat合并小文件
  • 自定义分区根据评论等级把数据分区
  • 自定义OutputFormat把数据输出到多个目录

1.3 开发步骤


1、合并小文件
1、Mapper

import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
//text:代表的是一个文件的path+名称,BytesWritable:一个文件的内容
public class MergeMapper extends Mapper<Text, BytesWritable, Text,
        BytesWritable> {
    @Override
    protected void map(Text key, BytesWritable value, Context context) throws
            IOException, InterruptedException {
        context.write(key, value);
    }
}

2、自定义InputFormat
MergeInputFormat

//自定义inputformat读取多个小文件合并为一个SequenceFile文件
//SequenceFile文件中以kv形式存储文件,key--》文件路径+文件名称,value-->文件的整个内容
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.JobContext;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import java.io.IOException;
//TextInputFormat中泛型是LongWritable:文本的偏移量, Text:一行文本内容;指明当前
inputformat的输出数据类型
//自定义inputformat:key-->文件路径+名称,value-->整个文件内容
public class MergeInputFormat extends FileInputFormat<Text, BytesWritable> {
    //重写是否可切分
    @Override
    protected boolean isSplitable(JobContext context, Path filename) {
//对于当前需求,不需要把文件切分,保证一个切片就是一个文件
        return false;
    }
    //recordReader就是用来读取数据的对象
    @Override
    public RecordReader<Text, BytesWritable> createRecordReader(InputSplit
                                                                        split, TaskAttemptContext context) throws IOException, InterruptedException {
        MergeRecordReader recordReader = new MergeRecordReader();
//调用recordReader的初始化方法
        recordReader.initialize(split, context);
        return recordReader;
    }
}

MergeRecordReader

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit;
import org.apache.hadoop.mapreduce.RecordReader;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
//负责读取数据,一次读取整个文件内容,封装成kv输出
public class MergeRecordReader extends RecordReader<Text, BytesWritable> {
    private FileSplit split;
    //hadoop配置文件对象
    private Configuration conf;
    //定义key,value的成员变量
    private Text key = new Text();
    private BytesWritable value = new BytesWritable();
    //初始化方法,把切片以及上下文提升为全局
    @Override
    public void initialize(InputSplit split, TaskAttemptContext context) throws
            IOException, InterruptedException {
        this.split = (FileSplit) split;
        conf = context.getConfiguration();
    }
    private Boolean flag = true;
    //用来读取数据的方法
    @Override
    public boolean nextKeyValue() throws IOException, InterruptedException {
//对于当前split来说只需要读取一次即可,因为一次就把整个文件全部读取了。
        if (flag) {
//准备一个数组存放读取到的数据,数据大小是多少?
            byte[] content = new byte[(int) split.getLength()];
            final Path path = split.getPath();//获取切片的path信息
            final FileSystem fs = path.getFileSystem(conf);//获取到文件系统对象
            final FSDataInputStream fis = fs.open(path); //获取到输入流
            IOUtils.readFully(fis, content, 0, content.length); //读取数据并把数据
            放入byte[]
//封装key和value
            key.set(path.toString());
            value.set(content, 0, content.length);
            IOUtils.closeStream(fis);
//把再次读取的开关置为false
            flag = false;
            return true;
        }
        return false;
    }
    //获取到key
    @Override
    public Text getCurrentKey() throws IOException, InterruptedException {
        return key;
    }
    //获取到value
    @Override
    public BytesWritable getCurrentValue() throws IOException,
            InterruptedException {
        return value;
    }
    //获取进度
    @Override
    public float getProgress() throws IOException, InterruptedException {
        return 0;
    }
    //关闭资源
    @Override
    public void close() throws IOException {
    }
}

3、Reducer

import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class MergeReducer extends Reducer<Text, BytesWritable, Text,
BytesWritable> {
@Override
protected void reduce(Text key, Iterable<BytesWritable> values, Context
context) throws IOException, InterruptedException {
//输出value值(文件内容),只获取其中第一个即可(只有一个)
context.write(key, values.iterator().next());
}
}

4、Driver

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
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 org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;
import java.io.IOException;
public class MergeDriver {
    public static void main(String[] args) throws IOException,
            ClassNotFoundException, InterruptedException {
// 1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();
        final Job job = Job.getInstance(conf, "MergeDriver");
// 2. 指定程序jar的本地路径
        job.setJarByClass(MergeDriver.class);
// 3. 指定Mapper/Reducer类
        job.setMapperClass(MergeMapper.class);
        job.setReducerClass(MergeReducer.class);
// 4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(BytesWritable.class);
// 5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(BytesWritable.class);
//设置使用自定义InputFormat读取数据
        job.setInputFormatClass(MergeInputFormat.class);
        FileInputFormat.setInputPaths(job, new Path("E:\\teach\\hadoop框架\\资料
                \\data\\mr综合案例\\input2")); //指定读取数据的原始路径
                job.setOutputFormatClass(SequenceFileOutputFormat.class);
// 7. 指定job输出结果路径
        FileOutputFormat.setOutputPath(job, new Path("E:\\teach\\hadoop框架\\资料
                \\data\\mr综合案例\\out")); //指定结果数据输出路径
// 8. 提交作业
        final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);
    }
}

2、分区排序多目录输出
1、Mapper

import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
//第一对kv:使用SequenceFileinputformat读取,所以key:Text,Value:BytesWritable(原因是生
成sequencefile文件指定就是这种类型)
public class CommentMapper extends Mapper<Text, BytesWritable, CommentBean,
        NullWritable> {
    //key就是文件名
//value:一个文件的完整内容
    @Override
    protected void map(Text key, BytesWritable value, Context context) throws
            IOException, InterruptedException {
//且分区每一行
        String str = new String(value.getBytes());
        String[] lines = str.split("\n");
        for (String line : lines) {
            CommentBean commentBean = parseStrToCommentBean(line);
            if (null != commentBean) {
                context.write(commentBean, NullWritable.get());
            }
        }
    }
    //切分字符串封装成commentbean对象
    public CommentBean parseStrToCommentBean(String line) {
        if (StringUtils.isNotBlank(line)) {
//每一行进行切分
            String[] fields = line.split("\t");
            if (fields.length >= 9) {
                return new CommentBean(fields[0], fields[1], fields[2],
                        Integer.parseInt(fields[3]), fields[4], fields[5], fields[6],
                        Integer.parseInt(fields[7]),
                        fields[8]);
            }
            {
                return null;
            }
        }
        return null;
    }
}

2、CommentBean

import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class CommentBean implements WritableComparable<CommentBean> {
    private String orderId;
    private String comment;
    private String commentExt;
    private int goodsNum;
    private String phoneNum;
    private String userName;
    private String address;
    private int commentStatus;
    private String commentTime;
    @Override
    public String toString() {
        return
                orderId+"\t"+comment+"\t"+commentExt+"\t"+goodsNum+"\t"+phoneNum+"\t"+userName+"
\t"+address+"\t"+commentStatus+"\t"+commentTime;
    }
    //无参构造
    public CommentBean() {
    }
    public CommentBean(String orderId, String comment, String commentExt, int
            goodsNum, String phoneNum, String userName, String address, int commentStatus,
                       String commentTime) {
        this.orderId = orderId;
        this.comment = comment;
        this.commentExt = commentExt;
        this.goodsNum = goodsNum;
        this.phoneNum = phoneNum;
        this.userName = userName;
        this.address = address;
        this.commentStatus = commentStatus;
        this.commentTime = commentTime;
    }
    public String getOrderId() {
        return orderId;
    }
    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }
    public String getComment() {
        return comment;
    }
    public void setComment(String comment) {
        this.comment = comment;
    }
    public String getCommentExt() {
        return commentExt;
    }
    public void setCommentExt(String commentExt) {
        this.commentExt = commentExt;
    }
    public int getGoodsNum() {
        return goodsNum;
    }
    public void setGoodsNum(int goodsNum) {
        this.goodsNum = goodsNum;
    }
    public String getPhoneNum() {
        return phoneNum;
    }
    public void setPhoneNum(String phoneNum) {
        this.phoneNum = phoneNum;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public int getCommentStatus() {
        return commentStatus;
    }
    public void setCommentStatus(int commentStatus) {
        this.commentStatus = commentStatus;
    }
    public String getCommentTime() {
        return commentTime;
    }
    public void setCommentTime(String commentTime) {
        this.commentTime = commentTime;
    }
    //定义排序规则,按照时间降序;0,1,-1
    @Override
    public int compareTo(CommentBean o) {
        return o.getCommentTime().compareTo(this.commentTime);
    }
    //序列化
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(orderId);
        out.writeUTF(comment);
        out.writeUTF(commentExt);
        out.writeInt(goodsNum);
        out.writeUTF(phoneNum);
        out.writeUTF(userName);
        out.writeUTF(address);
        out.writeInt(commentStatus);
        out.writeUTF(commentTime);
    }
    //反序列化
    @Override
    public void readFields(DataInput in) throws IOException {
        this.orderId = in.readUTF();
        this.comment = in.readUTF();
        this.commentExt = in.readUTF();
        this.goodsNum = in.readInt();
        this.phoneNum = in.readUTF();
        this.userName = in.readUTF();
        this.address = in.readUTF();
        this.commentStatus = in.readInt();
        this.commentTime = in.readUTF();
    }
}

3、自定义分区器

import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Partitioner;
public class CommentPartitioner extends Partitioner<CommentBean, NullWritable> {
    @Override
    public int getPartition(CommentBean commentBean, NullWritable nullWritable,
                            int numPartitions) {
// return (commentBean.getCommentStatus() & Integer.MAX_VALUE) %
        numPartitions;
        return commentBean.getCommentStatus();//0,1,2 -->对应分区编号的
    }
}

4、自定义OutputFormat
CommentOutputFormat

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
//最终输出的kv类型
public class CommentOutputFormat extends FileOutputFormat<CommentBean,
        NullWritable> {
    //负责写出数据的对象
    @Override
    public RecordWriter<CommentBean, NullWritable>
    getRecordWriter(TaskAttemptContext job) throws IOException,
            InterruptedException {
        Configuration conf = job.getConfiguration();
        FileSystem fs = FileSystem.get(conf);
//当前reducetask处理的分区编号来创建文件获取输出流
//获取到在Driver指定的输出路径;0是好评,1是中评,2是差评
        String outputDir =
                conf.get("mapreduce.output.fileoutputformat.outputdir");
        FSDataOutputStream goodOut=null;
        FSDataOutputStream commonOut=null;
        FSDataOutputStream badOut=null;
        int id = job.getTaskAttemptID().getTaskID().getId();//当前reducetask
        处理的分区编号
        if(id==0){
//好评数据
            goodOut =fs.create(new Path(outputDir + "\\good\\good.log"));
        }else if(id ==1){
//中评数据
            commonOut = fs.create(new Path(outputDir +
                    "\\common\\common.log"));
        }else{
            badOut = fs.create(new Path(outputDir + "\\bad\\bad.log"));
        }
        return new CommentRecorderWrtier(goodOut,commonOut,badOut);
    }
}

RecordWriter

import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
public class CommentRecorderWrtier extends RecordWriter<CommentBean,
        NullWritable> {
    //定义写出数据的流
    private FSDataOutputStream goodOut;
    private FSDataOutputStream commonOut;
    private FSDataOutputStream badOut;
    public CommentRecorderWrtier(FSDataOutputStream goodOut,
                                 FSDataOutputStream commonOut, FSDataOutputStream badOut) {
        this.goodOut = goodOut;
        this.commonOut = commonOut;
        this.badOut = badOut;
    }
    //实现把数据根据不同的评论类型输出到不同的目录下
//写出数据的逻辑
    @Override
    public void write(CommentBean key, NullWritable value) throws
            IOException, InterruptedException {
        int commentStatus = key.getCommentStatus();
        String beanStr = key.toString();
        if (commentStatus == 0) {
            goodOut.write(beanStr.getBytes());
            goodOut.write("\n".getBytes());
            goodOut.flush();
        } else if (commentStatus == 1) {
            commonOut.write(beanStr.getBytes());
            commonOut.write("\n".getBytes());
            commonOut.flush();
        } else {
            badOut.write(beanStr.getBytes());
            badOut.write("\n".getBytes());
            badOut.flush();
        }
    }
    //释放资源
    @Override
    public void close(TaskAttemptContext context) throws IOException,
            InterruptedException {
        IOUtils.closeStream(goodOut);
        IOUtils.closeStream(commonOut);
        IOUtils.closeStream(badOut);
    }
}

5、Reducer

import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class CommentReducer extends Reducer<CommentBean, NullWritable,
        CommentBean, NullWritable> {
    @Override
    protected void reduce(CommentBean key, Iterable<NullWritable> values,
                          Context context) throws IOException, InterruptedException {
//遍历values,输出的是key;key:是一个引用地址,底层获取value同时,key的值也发生了
        变化
        for (NullWritable value : values) {
            context.write(key, value);
        }
    }
}

6、Driver

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.SequenceFileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class CommentDriver {
    public static void main(String[] args) throws IOException,
            ClassNotFoundException, InterruptedException {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf, "CommentDriver");
        job.setJarByClass(CommentDriver.class);
        job.setMapperClass(CommentMapper.class);
        job.setReducerClass(CommentReducer.class);
        job.setMapOutputKeyClass(CommentBean.class);
        job.setMapOutputValueClass(NullWritable.class);
        job.setOutputKeyClass(CommentBean.class);
        job.setOutputValueClass(NullWritable.class);
        job.setPartitionerClass(CommentPartitioner.class);
//指定inputformat类型
        job.setInputFormatClass(SequenceFileInputFormat.class);
//指定输出outputformat类型
        job.setOutputFormatClass(CommentOutputFormat.class);
//指定输入,输出路径
        FileInputFormat.setInputPaths(job,
                new Path("E:\\teach\\hadoop框架\\资料\\data\\mr综合案例\\out"));
        FileOutputFormat.setOutputPath(job,
                new Path("E:\\teach\\hadoop框架\\资料\\data\\mr综合案例\\multiout"));
//指定reducetask的数量
        job.setNumReduceTasks(3);
        boolean b = job.waitForCompletion(true);
        if (b) {
            System.exit(0);
        }
    }
}

1.4 程序调优


预合并
CombineMapper

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class CombineMapper extends Mapper<LongWritable,Text,
        NullWritable,Text>{
    @Override
    protected void map(LongWritable key, Text value, Context context) throws
            IOException, InterruptedException {
        context.write(NullWritable.get(), value);
    }
}

CombineDriver

import com.lagou.mr.comment.step2.*;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.SequenceFileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class CombineDriver {
    public static void main(String[] args) throws IOException,
            ClassNotFoundException, InterruptedException {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf, "CombineDriver");
        job.setJarByClass(CombineDriver.class);
        job.setMapperClass(CombineMapper.class);
        job.setMapOutputKeyClass(NullWritable.class);
        job.setMapOutputValueClass(Text.class);
        job.setOutputKeyClass(NullWritable.class);
        job.setOutputValueClass(Text.class);
//指定inputformat
        job.setInputFormatClass(CombineTextInputFormat.class);
        CombineTextInputFormat.setMaxInputSplitSize(job, 1024 * 1024 * 4);
//指定输入,输出路径
        FileInputFormat.setInputPaths(job,
                new Path("E:\\teach\\hadoop框架\\资料\\data\\mr综合案例
                        \\input"));
                        FileOutputFormat.setOutputPath(job,
                                new Path("E:\\teach\\hadoop框架\\资料\\data\\mr综合案例
                                        \\merge-out"));
//指定reducetask的数量
                                        job.setNumReduceTasks(3);
        boolean b = job.waitForCompletion(true);
        if (b) {
            System.exit(0);
        }
    }
}

输出压缩
MergeDriver

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.BytesWritable;
import org.apache.hadoop.io.SequenceFile;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.DefaultCodec;
import org.apache.hadoop.io.compress.SnappyCodec;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;
import java.io.IOException;
public class MergeDriver {
    public static void main(String[] args) throws IOException,
            ClassNotFoundException, InterruptedException {
// 1. 获取配置文件对象,获取job对象实例
        final Configuration conf = new Configuration();
        final Job job = Job.getInstance(conf, "MergeDriver");
// 2. 指定程序jar的本地路径
        job.setJarByClass(MergeDriver.class);
// 3. 指定Mapper/Reducer类
        job.setMapperClass(MergeMapper.class);
// job.setReducerClass(MergeReducer.class);
// 4. 指定Mapper输出的kv数据类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(BytesWritable.class);
// 5. 指定最终输出的kv数据类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(BytesWritable.class);
//设置使用自定义InputFormat读取数据
        job.setInputFormatClass(MergeInputFormat.class);
        FileInputFormat.setInputPaths(job, new Path("E:\\teach\\hadoop框架\\资料
                \\data\\mr综合案例\\merge-out")); //指定读取数据的原始路径
//指定输出使用的outputformat
                job.setOutputFormatClass(SequenceFileOutputFormat.class);
        FileOutputFormat.setOutputCompressorClass(job, SnappyCodec.class);
//record压缩
        SequenceFileOutputFormat.setOutputCompressionType(job,
                SequenceFile.CompressionType.RECORD );
        SequenceFileOutputFormat.setOutputCompressorClass(job,
                DefaultCodec.class);
// block压缩
        SequenceFileOutputFormat.setOutputCompressionType(job,
                SequenceFile.CompressionType.BLOCK );
        SequenceFileOutputFormat.setOutputCompressorClass(job,
                DefaultCodec.class);
// 7. 指定job输出结果路径
        FileOutputFormat.setOutputPath(job, new Path("E:\\teach\\hadoop框架\\资料
                \\data\\mr综合案例\\out")); //指定结果数据输出路径
// 8. 提交作业
        final boolean flag = job.waitForCompletion(true);
//jvm退出:正常退出0,非0值则是错误退出
        System.exit(flag ? 0 : 1);
    }
}

第七节 MR算法扩展

7.1 MergeSort 归并排序


合并


合并细节

1. 不断地将当前序列平均分割成 2个子序列

  • 直到不能再分割(序列中只剩 1个元素)

2. 不断地将 2个子序列合并成一个有序序列

  • 直到最终只剩下 1个子序列

时间复杂度:O(nlogn)
空间复杂度:O(n)


7.2 QuickSort-快排

第一步

  • 从数组中选择一个轴点元素(Pivot element),一般选择0位置元素为轴点元素

第二步

  • 利用Pivot将数组分割成2个子序列
  • 将小于 Pivot的元素放在Pivot前面(左侧)
  • 将大于 Pivot的元素放在Pivot后面(右侧)
  • 等于Pivot的元素放哪边都可以(暂定放在左边)

第三步

  • 对子数组进行第一步,第二步操作,直到不能再分割(子数组中只有一个元素)

空间复杂度

  • 由于递归调用,每次类似折半效果所以空间复杂度是O(logn)

第七部分 YARN资源调度


第一节 Yarn架构

  • ResourceManager(rm):处理客户端请求、启动/监控ApplicationMaster、监控NodeManager、资源分配与调度;
  • NodeManager(nm):单个节点上的资源管理、处理来自ResourceManager的命令、处理来自ApplicationMaster的命令;
  • ApplicationMaster(am):数据切分、为应用程序申请资源,并分配给内部任务、任务监控与容错。
  • Container:对任务运行环境的抽象,封装了CPU、内存等多维资源以及环境变量、启动命令等任务运行相关的信息。

第二节 Yarn任务提交(工作机制)

作业提交过程之YARN
作业提交

  • 第1步:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业。
  • 第2步:Client向RM申请一个作业id。
  • 第3步:RM给Client返回该job资源的提交路径和作业id。
  • 第4步:Client提交jar包、切片信息和配置文件到指定的资源提交路径。
  • 第5步:Client提交完资源后,向RM申请运行MrAppMaster。

作业初始化

  • 第6步:当RM收到Client的请求后,将该job添加到容量调度器中。
  • 第7步:某一个空闲的NM领取到该Job。
  • 第8步:该NM创建Container,并产生MRAppmaster。
  • 第9步:下载Client提交的资源到本地。

任务分配

  • 第10步:MrAppMaster向RM申请运行多个MapTask任务资源。
  • 第11步:RM将运行MapTask任务分配给另外两个NodeManager,另两个NodeManager分别领取任务并创建容器。

任务运行

  • 第12步:MR向两个接收到任务的NodeManager发送程序启动脚本,这两个NodeManager分别启动MapTask,MapTask对数据分区排序。
  • 第13步:MrAppMaster等待所有MapTask运行完毕后,向RM申请容器,运行ReduceTask。
  • 第14步:ReduceTask向MapTask获取相应分区的数据。
  • 第15步:程序运行完毕后,MR会向RM申请注销自己。

进度和状态更新

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

作业完成

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

第三节 Yarn调度策略


Hadoop作业调度器主要有三种:FIFO、Capacity Scheduler和Fair Scheduler。Hadoop2.9.2默认的资源调度器是Capacity Scheduler。
可以查看yarn-default.xml

1. FIFO(先进先出调度器)

2. 容量调度器(Capacity Scheduler 默认的调度器)
Apache Hadoop默认使用的调度策略。Capacity 调度器允许多个组织共享整个集群,每个组织可以获得集群的一部分计算能力。通过为每个组织分配专门的队列,然后再为每个队列分配一定的集群资源,这样整个集群就可以通过设置多个队列的方式给多个组织提供服务了。除此之外,队列内部又可以垂直划分,这样一个组织内部的多个成员就可以共享这个队列资源了,在一个队列内部,资源的调度是采用的是先进先出(FIFO)策略。

 3. Fair Scheduler(公平调度器,CDH版本的hadoop默认使用的调度器)
Fair调度器的设计目标是为所有的应用分配公平的资源(对公平的定义可以通过参数来设置)。公平调度在也可以在多个队列间工作。举个例子,假设有两个用户A和B,他们分别拥有一个队列。当A启动一个job而B没有任务时,A会获得全部集群资源;当B启动一个job后,A的job会继续运行,不过一会儿之后两个任务会各自获得一半的集群资源。如果此时B再启动第二个job并且其它job还在运行,则它将会和B的第一个job共享B这个队列的资源,也就是B的两个job会用于四分之一的集群资源,而A的job仍然用于集群一半的资源,结果就是资源最终在两个用户之间平等的共享

第四节 Yarn多租户资源隔离配置


Yarn集群资源设置为A,B两个队列,

  • A队列设置占用资源70%主要用来运行常规的定时任务,
  • B队列设置占用资源30%主要运行临时任务,
  • 两个队列间可相互资源共享,假如A队列资源占满,B队列资源比较充裕,A队列可以使用B队列的资源,使总体做到资源利用最大化.

选择使用Fair Scheduler调度策略!!
具体配置
1. yarn-site.xml

<!-- 指定我们的任务调度使用fairScheduler的调度方式 -->
<property>
    <name>yarn.resourcemanager.scheduler.class</name>
    <value>org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler</value>
    <description>In case you do not want to use the defaultscheduler</description>
</property>


2. 创建fair-scheduler.xml文件
在Hadoop安装目录/etc/hadoop创建该文件

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<allocations>
<defaultQueueSchedulingPolicy>fair</defaultQueueSchedulingPolicy>
<queue name="root" >
    <queue name="default">
        <aclAdministerApps>*</aclAdministerApps>
        <aclSubmitApps>*</aclSubmitApps>
        <maxResources>9216 mb,4 vcores</maxResources>
        <maxRunningApps>100</maxRunningApps>
        <minResources>1024 mb,1vcores</minResources>
        <minSharePreemptionTimeout>1000</minSharePreemptionTimeout>
        <schedulingPolicy>fair</schedulingPolicy>
        <weight>7</weight>
    </queue>
    <queue name="queue1">
        <aclAdministerApps>*</aclAdministerApps>
        <aclSubmitApps>*</aclSubmitApps>
        <maxResources>4096 mb,4vcores</maxResources>
        <maxRunningApps>5</maxRunningApps>
        <minResources>1024 mb, 1vcores</minResources>
        <minSharePreemptionTimeout>1000</minSharePreemptionTimeout>
        <schedulingPolicy>fair</schedulingPolicy>
        <weight>3</weight>
    </queue>
</queue>
<queuePlacementPolicy>
    <rule create="false" name="specified"/>
    <rule create="true" name="default"/>
</queuePlacementPolicy>
</allocations>

界面验证

第八部分 Apache Hadoop 核心源码剖析

第一节 源码阅读准备


1. 下载Apache Hadoop-2.9.2官方源码
2. 将源码导入idea中
启动idea在提示界面选择导入

 

第二节 NameNode 启动流程


命令启动Hdfs集群

start-dfs.sh

该命令会启动Hdfs的NameNode以及DataNode,启动NameNode主要是通过org.apache.hadoop.hdfs.server.namenode.NameNode类。
我们重点关注NameNode在启动过程中做了哪些工作(偏离主线的技术细节不深究)
对于分析启动流程主要关注两部分代码:

public class NameNode extends ReconfigurableBase implements
        NameNodeStatusMXBean {
    //该静态代码块主要是初始化一些HDFS的配置信息
    static{
        HdfsConfiguration.init();//进入之后发现方法是空的,没有任何操作?其实不是观察
        HdfsConfiguration的静态代码块
    }
    //HdfsConfiguration的类以及静态代码块
    public class HdfsConfiguration extends Configuration {
        static {
            addDeprecatedKeys();
// adds the default resources
            Configuration.addDefaultResource("hdfs-default.xml");
            Configuration.addDefaultResource("hdfs-site.xml");
        }
。。。。
        //main方法
        public static void main(String argv[]) throws Exception{
//分析传入的参数是否为帮助参数,如果是帮助的话打印帮助信息,并退出。
            if(DFSUtil.parseHelpArgument(argv,NameNode.USAGE,System.out,true)){
                System.exit(0);
            }
            try{
//格式化输出启动信息,并且创建hook(打印节点关闭信息)
                StringUtils.startupShutdownMessage(NameNode.class,argv,LOG);
//创建namenode
                NameNode namenode=createNameNode(argv,null);
                if(namenode!=null){
//加入集群
                    namenode.join()
                }
            }catch(Throwable e){
//异常处理
                LOG.error("Failed to start namenode.",e)
                terminate(1,e);
            }
        }
        ----------------------------------------------------------------------
        //关注createNameNode
        public static NameNode createNameNode(String argv[], Configuration conf)
                throws IOException {
            LOG.info("createNameNode " + Arrays.asList(argv));
            if (conf == null)
                conf = new HdfsConfiguration();
// Parse out some generic args into Configuration.
            GenericOptionsParser hParser = new GenericOptionsParser(conf, argv);
            argv = hParser.getRemainingArgs();
// Parse the rest, NN specific args.
//解析启动的参数
            StartupOption startOpt = parseArguments(argv);
            if (startOpt == null) {
                printUsage(System.err);
                return null;
            }
            setStartupOption(conf, startOpt);
            switch (startOpt) {
....
                default: { //正常启动进入该分支
//初始化metric系统
                    DefaultMetricsSystem.initialize("NameNode");
//返回新的NameNode
                    return new NameNode(conf);
                }
            }
        }
----------------------------------------------------------------------

        //NameNode的构造
        public NameNode(Configuration conf) throws IOException {
            this(conf, NamenodeRole.NAMENODE);
        }...
        protected NameNode(Configuration conf, NamenodeRole role)
                throws IOException {
            this.conf = conf;
            this.role = role;
// 设置NameNode#clientNamenodeAddress为"hdfs://localhost:9000"
            setClientNamenodeAddress(conf);
            String nsId = getNameServiceId(conf);
            String namenodeId = HAUtil.getNameNodeId(conf, nsId);
// HA相关
            this.haEnabled = HAUtil.isHAEnabled(conf, nsId);
            state = createHAState(getStartupOption(conf));
            this.allowStaleStandbyReads = HAUtil.shouldAllowStandbyReads(conf);
            this.haContext = createHAContext();
            try {
                initializeGenericKeys(conf, nsId, namenodeId);
// 完成实际的初始化工作
                initialize(conf);
// HA相关
                try {
                    haContext.writeLock();
                    state.prepareToEnterState(haContext);
                    state.enterState(haContext);
                } finally {
                    haContext.writeUnlock();
                }
            } catch (IOException e) {
                this.stop();
                throw e;
            } catch (HadoopIllegalArgumentException e) {
                this.stop();
                throw e;
            }
        }
//尽管本地没有开启HA(haEnabled=false**),**namenode依然拥有一个HAState,namenode的HAState状态为active.
----------------------------------------------------------------------
        // 完成实际的初始化工作
// initialize(conf);
        protected void initialize(Configuration conf) throws IOException {
            if (conf.get(HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS) == null) {
                String intervals = conf.get(DFS_METRICS_PERCENTILES_INTERVALS_KEY);
                if (intervals != null) {
                    conf.set(HADOOP_USER_GROUP_METRICS_PERCENTILES_INTERVALS,
                            intervals);
                }
            }
            UserGroupInformation.setConfiguration(conf);
            loginAsNameNodeUser(conf);
// 初始化metric
            NameNode.initMetrics(conf, this.getRole());
            StartupProgressMetrics.register(startupProgress);
// 启动httpServer
            if (NamenodeRole.NAMENODE == role) {startHttpServer(conf);
            }
            this.spanReceiverHost = SpanReceiverHost.getInstance(conf);
// 从namenode目录加载fsimage与editlog,初始化FsNamesystem、FsDirectory、
            LeaseManager等
            loadNamesystem(conf);
// 创建RpcServer,封装了NameNodeRpcServer clientRpcServer,支持
            ClientNamenodeProtocol、DatanodeProtocolPB等协议
                    rpcServer = createRpcServer(conf);
            if (clientNamenodeAddress == null) {
// This is expected for MiniDFSCluster. Set it now using
// the RPC server's bind address.
                clientNamenodeAddress =
                        NetUtils.getHostPortString(rpcServer.getRpcAddress());
                LOG.info("Clients are to use " + clientNamenodeAddress + " to access"
                        + " this namenode/service.");
            }
            if (NamenodeRole.NAMENODE == role) {
                httpServer.setNameNodeAddress(getNameNodeAddress());
                httpServer.setFSImage(getFSImage());
            }
// 启动JvmPauseMonitor等,反向监控JVM
            pauseMonitor = new JvmPauseMonitor(conf);
            pauseMonitor.start();
            metrics.getJvmMetrics().setPauseMonitor(pauseMonitor);
// 启动执行多个非常重要工作的多个线程
            startCommonServices(conf);
        }
----------------------------------------------------------------------
        private void startCommonServices(Configuration conf) throws IOException {
// 创建NameNodeResourceChecker、激活BlockManager等
            namesystem.startCommonServices(conf, haContext);
            registerNNSMXBean();
// 角色非`NamenodeRole.NAMENODE`的在此处启动HttpServer
            if (NamenodeRole.NAMENODE != role) {
                startHttpServer(conf);
                httpServer.setNameNodeAddress(getNameNodeAddress());
                httpServer.setFSImage(getFSImage());
            }
// 启动RPCServer
            rpcServer.start();
...// 启动各插件
            LOG.info(getRole() + " RPC up at: " + rpcServer.getRpcAddress());
            if (rpcServer.getServiceRpcAddress() != null) {
                LOG.info(getRole() + " service RPC up at: "
                        + rpcServer.getServiceRpcAddress());
            }
        }
--------------------------------------------------------------------------------------

        void startCommonServices(Configuration conf, HAContext haContext) throws
                IOException {
            this.registerMBean(); // register the MBean for the FSNamesystemState
            writeLock();
            this.haContext = haContext;
            try {
// 创建NameNodeResourceChecker,并立即检查一次
                nnResourceChecker = new NameNodeResourceChecker(conf);
                checkAvailableResources();
                assert safeMode != null && !isPopulatingReplQueues();
// 设置一些启动过程中的信息
                StartupProgress prog = NameNode.getStartupProgress();
                prog.beginPhase(Phase.SAFEMODE);
                prog.setTotal(Phase.SAFEMODE, STEP_AWAITING_REPORTED_BLOCKS,
                        getCompleteBlocksTotal());
// 设置已完成的数据块总量
                setBlockTotal();
// 激活BlockManager
                blockManager.activate(conf);
            } finally {
                writeUnlock();
            }
            registerMXBean();
            DefaultMetricsSystem.instance().register(this);
            snapshotManager.registerMXBean();
        }
//blockManager.activate(conf)激活BlockManager主要完成PendingReplicationMonitor、
        DecommissionManager#Monitor、HeartbeatManager#Monitor、ReplicationMonitor
        public void activate(Configuration conf) {
// 启动PendingReplicationMonitor
            pendingReplications.start();
// 激活DatanodeManager:启动DecommissionManager--Monitor、HeartbeatManager--
            Monitor
            datanodeManager.activate(conf);
// 启动BlockManager--ReplicationMonitor
            this.replicationThread.start();
        }

namenode的主要责任是文件元信息与数据块映射的管理。相应的,namenode的启动流程需要关注与客户端、datanode通信的工作线程,文件元信息的管理机制,数据块的管理机制等。其中,RpcServer主要负责与客户端、datanode通信,FSDirectory主要负责管理文件元信息。


第三节 DataNode 启动流程

datanode的Main Class是DataNode,先找到DataNode.main()

public class DataNode extends ReconfigurableBase
        implements InterDatanodeProtocol, ClientDatanodeProtocol,
        TraceAdminProtocol, DataNodeMXBean, ReconfigurationProtocol {
    public static final Logger LOG = LoggerFactory.getLogger(DataNode.class);
    static{
        HdfsConfiguration.init();
    }
    public static void main(String args[]) {
        if (DFSUtil.parseHelpArgument(args, DataNode.USAGE, System.out, true)) {
            System.exit(0);
        }
        secureMain(args, null);
    }
...
    public static void secureMain(String args[], SecureResources resources) {
        int errorCode = 0;
        try {
// 打印启动信息
            StringUtils.startupShutdownMessage(DataNode.class, args, LOG);
// 完成创建datanode的主要工作
            DataNode datanode = createDataNode(args, null, resources);
            if (datanode != null) {
                datanode.join();
            } else {
                errorCode = 1;
            }
        } catch (Throwable e) {
            LOG.fatal("Exception in secureMain", e);
            terminate(1, e);
        } finally {
            LOG.warn("Exiting Datanode");
            terminate(errorCode);
        }
    }
---------------------------------------------------
    public static DataNode createDataNode(String args[], Configuration conf,
                                          SecureResources resources) throws IOException {
// 完成大部分初始化的工作,并启动部分工作线程
        DataNode dn = instantiateDataNode(args, conf, resources);
        if (dn != null) {
// 启动剩余工作线程
            dn.runDatanodeDaemon();
        }
        return dn;
    }
--------------------------------------------------
    /** Start a single datanode daemon and wait for it to finish.
     * If this thread is specifically interrupted, it will stop waiting.
     */
    public void runDatanodeDaemon() throws IOException {
// 在DataNode.instantiateDataNode()执行过程中会调用该方法(见后)
        blockPoolManager.startAll();
        dataXceiverServer.start();
        if (localDataXceiverServer != null) {
            localDataXceiverServer.start();
        }
        ipcServer.start();
        startPlugins(conf);
    }
--------------------------------------------------------
    public static DataNode instantiateDataNode(String args [], Configuration
            conf,
                                               SecureResources resources) throws IOException {
        if (conf == null)
            conf = new HdfsConfiguration();
... // 参数检查等
        Collection<StorageLocation> dataLocations = getStorageLocations(conf);
        UserGroupInformation.setConfiguration(conf);
        SecurityUtil.login(conf, DFS_DATANODE_KEYTAB_FILE_KEY,
                DFS_DATANODE_KERBEROS_PRINCIPAL_KEY);
        return makeInstance(dataLocations, conf, resources);
    }
--------------------------------------------------------------------
    //DataNode.makeInstance()开始创建DataNode
    static DataNode makeInstance(Collection<StorageLocation> dataDirs,
                                 Configuration conf, SecureResources resources) throws IOException {
...// 检查数据目录的权限
        assert locations.size() > 0 : "number of data directories should be > 0";
        return new DataNode(conf, locations, resources);
    }
...
    DataNode(final Configuration conf,
             final List<StorageLocation> dataDirs,
             final SecureResources resources) throws IOException {
        super(conf);
...// 参数设置
        try {
            hostName = getHostName(conf);
            LOG.info("Configured hostname is " + hostName);
            startDataNode(conf, dataDirs, resources);
        } catch (IOException ie) {
            shutdown();
            throw ie;
        }
    }
...
    void startDataNode(Configuration conf,
                       List<StorageLocation> dataDirs,
                       SecureResources resources
    ) throws IOException {
...// 参数设置
// 初始化DataStorage
        storage = new DataStorage();
// global DN settings
// 注册JMX
        registerMXBean();
// 初始化DataXceiver(流式通信),DataNode runDatanodeDaemon()中启动
        initDataXceiver(conf);
// 启动InfoServer(Web UI)
        startInfoServer(conf);
// 启动JVMPauseMonitor(反向监控JVM情况,可通过JMX查询)
        pauseMonitor = new JvmPauseMonitor(conf);
        pauseMonitor.start();
...// 略
// 初始化IpcServer(RPC通信),DataNode-runDatanodeDaemon()中启动
        initIpcServer(conf);
        metrics = DataNodeMetrics.create(conf, getDisplayName());
        metrics.getJvmMetrics().setPauseMonitor(pauseMonitor);
// 按照namespace(nameservice)、namenode的结构进行初始化
        blockPoolManager = new BlockPoolManager(this);
        blockPoolManager.refreshNamenodes(conf);
...// 略
    }
//BlockPoolManager抽象了datanode提供的数据块存储服务。BlockPoolManager按照
    namespace(nameservice)、namenode结构组织。
    //BlockPoolManager-refreshNamenodes()
//除了初始化过程主动调用,还可以由namespace通过datanode心跳过程下达刷新命令
    void refreshNamenodes(Configuration conf)
            throws IOException {
        LOG.info("Refresh request received for nameservices: " + conf.get
                (DFSConfigKeys.DFS_NAMESERVICES));
        Map<String, Map<String, InetSocketAddress>> newAddressMap = DFSUtil
                .getNNServiceRpcAddressesForCluster(conf);
        synchronized (refreshNamenodesLock) {
            doRefreshNamenodes(newAddressMap);
        }
    }
-------------------------------------------------------
    private void doRefreshNamenodes(
            Map<String, Map<String, InetSocketAddress>> addrMap) throws IOException {
        assert Thread.holdsLock(refreshNamenodesLock);
        Set<String> toRefresh = Sets.newLinkedHashSet();
        Set<String> toAdd = Sets.newLinkedHashSet();
        Set<String> toRemove;
        synchronized (this) {
// Step 1. For each of the new nameservices, figure out whether
// it's an update of the set of NNs for an existing NS,
// or an entirely new nameservice.
            for (String nameserviceId : addrMap.keySet()) {
                if (bpByNameserviceId.containsKey(nameserviceId)) {
                    toRefresh.add(nameserviceId);
                } else {
                    toAdd.add(nameserviceId);
                }
            }
...// 略
// Step 2. Start new nameservices
            if (!toAdd.isEmpty()) {
                LOG.info("Starting BPOfferServices for nameservices: " +
                        Joiner.on(",").useForNull("<default>").join(toAdd));
                for (String nsToAdd : toAdd) {
                    ArrayList<InetSocketAddress> addrs =
                            Lists.newArrayList(addrMap.get(nsToAdd).values());
// 为每个namespace创建对应的BPOfferService
                    BPOfferService bpos = createBPOS(addrs);
                    bpByNameserviceId.put(nsToAdd, bpos);
                    offerServices.add(bpos);
                }
            }
// 然后通过startAll启动所有BPOfferService
            startAll();
        }
...// 略
    }
------------------------------------------------
    protected BPOfferService createBPOS(List<InetSocketAddress> nnAddrs) {
        return new BPOfferService(nnAddrs, dn);
    }
    BPOfferService(List<InetSocketAddress> nnAddrs, DataNode dn) {
        Preconditions.checkArgument(!nnAddrs.isEmpty(),
                "Must pass at least one NN.");
        this.dn = dn;
        for (InetSocketAddress addr : nnAddrs) {
            this.bpServices.add(new BPServiceActor(addr, this));
        }
    }
--------------------------------------------
//BlockPoolManager#startAll()启动所有BPOfferService(实际是启动所有
    BPServiceActor)。
    synchronized void startAll() throws IOException {
        try {
            UserGroupInformation.getLoginUser().doAs(
                    new PrivilegedExceptionAction<Object>() {
                        @Override
                        public Object run() throws Exception {
                            for (BPOfferService bpos : offerServices) {
                                bpos.start();
                            }
                            return null;
                        }
                    });
        } catch (InterruptedException ex) {
            IOException ioe = new IOException();
            ioe.initCause(ex.getCause());
            throw ioe;
        }
    }
-------------------------------------------------------
//在datanode启动的主流程中,启动了多种工作线程,包括InfoServer、JVMPauseMonitor、
    BPServiceActor等。其中,最重要的是BPServiceActor线程,真正代表datanode与namenode通信的
    正是BPServiceActor线程。
//DataNode--initBlockPool():
/**
 * One of the Block Pools has successfully connected to its NN.
 * This initializes the local storage for that block pool,
 * checks consistency of the NN's cluster ID, etc.
 *
 * If this is the first block pool to register, this also initializes
 * the datanode-scoped storage.
 *
 * @param bpos Block pool offer service
 * @throws IOException if the NN is inconsistent with the local storage.
 */
void initBlockPool(BPOfferService bpos) throws IOException {
...// 略
// 将blockpool注册到BlockPoolManager
    blockPoolManager.addBlockPool(bpos);
// 初步初始化存储结构
    initStorage(nsInfo);
...// 检查磁盘损坏
// 启动扫描器
    initPeriodicScanners(conf);
// 将blockpool添加到FsDatasetIpml,并继续初始化存储结构
    data.addBlockPool(nsInfo.getBlockPoolID(), conf);
}

第四节 NameNode如何支撑高并发访问(双缓冲机制)

高并发访问NameNode会遇到什么样的问题:
经过学习HDFS的元数据管理机制,Client每次请求NameNode修改一条元数据(比如说申请上传一个文件,都要写一条edits log,包括两个步骤:

  • 写入本地磁盘--edits文件
  • 通过网络传输给JournalNodes集群(Hadoop HA集群--结合zookeeper来学习)。

高并发的难点主要在于数据的多线程安全以及每个操作效率!!
对于多线程安全:
NameNode在写edits log时几个原则:

  • 写入数据到edits_log必须保证每条edits都有一个全局顺序递增的transactionId(简称为txid),这样才可以标识出来一条一条的edits的先后顺序。
  • 如果要保证每条edits的txid都是递增的,就必须得加同步锁。也就是每个线程修改了元数据,要写一条edits 的时候,都必须按顺序排队获取锁后,才能生成一个递增的txid,代表这次要写的edits的序号。

产生的问题:
如果每次都是在一个加锁的代码块里,生成txid,然后写磁盘文件edits log,这种既有同步锁又有写磁盘操作非常耗时!!


HDFS优化解决方案
问题产生的原因主要是在于,写edits时串行化排队生成自增txid + 写磁盘操作费时,
HDFS的解决方案

  • 1. 串行化:使用分段锁
  • 2. 写磁盘:使用双缓冲

分段加锁机制
首先各个线程依次第一次获取锁,生成顺序递增的txid,然后将edits写入内存双缓冲的区域1,接着就立马第一次释放锁了。趁着这个空隙,后面的线程就可以再次立马第一次获取锁,然后立即写自己的edits到内存缓冲。
双缓冲机制

程序中将会开辟两份一模一样的内存空间,一个为bufCurrent,产生的数据会直接写入到这个bufCurrent,而另一个叫bufReady,在bufCurrent数据写入(达到一定标准)后,两片内存就会exchange(交换)。直接交换双缓冲的区域1和区域2。保证接收客户端写入数据请求的都是操作内存而不是同步写磁盘。


双缓冲源码分析 找到FsEditLog.java

....
        void logEdit(final FSEditLogOp op) {
        boolean needsSync = false;//是否同步的标识
        synchronized (this) {//
        assert isOpenForWrite() :
        "bad state: " + state;
// wait if an automatic sync is scheduled 如果当前操作被其它线程调度,则等待1s钟
        waitIfAutoSyncScheduled();
// check if it is time to schedule an automatic sync
        needsSync = doEditTransaction(op);
        if (needsSync) {
        isAutoSyncScheduled = true;//标识bufCurrent满了,进行双缓冲刷写
        }
        }
// Sync the log if an automatic sync is required.
        if (needsSync) {
        logSync();//将缓冲区数据刷写到磁盘
        }
        }
        ...


扩展 Hadoop 3.x 新特性概述

Hadoop3.x中增强了很多特性,在Hadoop3.x中,不再允许使用jdk1.7,要求jdk1.8以上版本。这是因为Hadoop 2.0是基于JDK 1.7开发的,而JDK 1.7在2015年4月已停止更新,这直接迫使Hadoop社区基于JDK 1.8重新发布一个新的Hadoop版本,而这正是Hadoop3.x。Hadoop3.x以后将会调整方案架构,将Mapreduce 基于内存+io+磁盘,共同处理数据。Hadoop 3.x中引入了一些重要的功能和优化,包括HDFS 可擦除编码、多Namenode支持、MR NativeTask优化、YARN基于cgroup的内存和磁盘IO隔离、YARN container resizing等。
Hadoop3.x官方文档地址如下:

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

第一节 Hadoop3.x新特性之Common改进

Hadoop Common改进:

  • 1. 精简Hadoop内核,包括剔除过期的API和实现,将默认组件实现替换成最高效的实现(比如将FileOutputCommitter缺省实现换为v2版本,废除hftp转由webhdfs替代,移除Hadoop子实现序列化库org.apache.hadoop.Records
  • 2. lasspath isolation以防止不同版本jar包冲突,比如google Guava在混合使用Hadoop、HBase和Spark时,很容易产生冲突。(https://issues.apache.org/jira/browse/HADOOP-11656)
  • 3. Shell脚本重构。 Hadoop 3.0对Hadoop的管理脚本进行了重构,修复了大量bug,增加了新特性,支持动态命令等。使用方式上则和之前版本的一致。(https://issues.apache.org/jira/browse/HADOOP-9902)Hadoop3.x新特性之HDFS改进

Hadoop3.x中最大改变的是HDFS,HDFS通过最近black块计算,根据最近计算原则,本地black块,加入到内存,先计算,通过IO,共享内存计算区域,最后快速形成计算结果。

  • 1. HDFS支持数据的擦除编码,这使得HDFS在不降低可靠性的前提下,节省一半存储空间。(https://issues.apache.org/jira/browse/HDFS-7285)
  • 2. 多NameNode支持,即支持一个集群中,一个active、多个standby namenode部署方式。注:多ResourceManager特性在hadoop 2.0中已经支持。(https://issues.apache.org/jira/browse/HDFS-6440)

关于这两个特性的官方文档地址:

http://hadoop.apache.org/docs/r3.0.1/hadoop-project-dist/hadoophdfs/HDFSErasureCoding.html
http://hadoop.apache.org/docs/r3.0.1/hadoop-project-dist/hadoophdfs/HDFSHighAvailabilityWithQJM.html

第二节 Hadoop3.x新特性之YARN改进

 

  • 1. 基于cgroup的内存隔离和IO Disk隔离(https://issues.apache.org/jira/browse/YARN-2619)
  • 2. 用curator实现RM leader选举(https://issues.apache.org/jira/browse/YARN-4438)
  • 3. containerresizing(https://issues.apache.org/jira/browse/YARN-1197)
  • 4. Timelineserver next generation (https://issues.apache.org/jira/browse/YARN-2928)

官方文档地址:

http://hadoop.apache.org/docs/r3.0.1/hadoop-yarn/hadoop-yarnsite/TimelineServiceV2.html

第三节 Hadoop3.x新特性之MapReduce改进

  • 1. Tasknative优化。为MapReduce增加了C/C++的map output collector实现(包括Spill,Sort和IFile等),通过作业级别参数调整就可切换到该实现上。对于shuffle密集型应用,其性能可提高约30%。(https://issues.apache.org/jira/browse/MAPREDUCE-2841)
  • 2. MapReduce内存参数自动推断。在Hadoop 2.0中,为MapReduce作业设置内存参数非常繁琐,涉及到两个参数:mapreduce.{map,reduce}.memory.mb和mapreduce.{map,reduce}.java.opts,一旦设置不合理,则会使得内存资源浪费严重,比如将前者设置为4096MB,但后者却是“-Xmx2g”,则剩余2g实际上无法让java heap使用到。(https://issues.apache.org/jira/browse/MAPREDUCE-5785)Hadoop3.x新特性之其他
  • 3. 添加新的 hadoop-client-api 和 hadoop-client-runtime 组件到一个单独的jar包里,以此解决依赖不兼容的问题。 (https://issues.apache.org/jira/browse/HADOOP-11804)
  • 4. 支持微软的Azure分布式文件系统和阿里的aliyun分布式文件系统

第九部分 调优及二次开发示例


第一节 Job执行三原则

  • 充分利用集群资源
  • reduce阶段尽量放在一轮
  • 每个task的执行时间要合理

1.1 原则一 充分利用集群资源

Job运行时,尽量让所有的节点都有任务处理,这样能尽量保证集群资源被充分利用,任务的并发度达到最大。可以通过调整处理的数据量大小,以及调整map和reduce个数来实现。

  • Reduce个数的控制使用“mapreduce.job.reduces”
  • Map个数取决于使用了哪种InputFormat,默认的TextFileInputFormat将根据block的个数来分配map数(一个block一个map)。

1.2 原则二 ReduceTask并发调整


努力避免出现以下场景

  • 观察Job如果大多数ReduceTask在第一轮运行完后,剩下很少甚至一个ReduceTask刚开始运行。这种情况下,这个ReduceTask的执行时间将决定了该job的运行时间。可以考虑将reduce个数减少。
  • 观察Job的执行情况如果是MapTask运行完成后,只有个别节点有ReduceTask在运行。这时候集群资源没有得到充分利用,需要增加Reduce的并行度以便每个节点都有任务处理。

1.3 原则三 Task执行时间要合理

一个job中,每个MapTask或ReduceTask的执行时间只有几秒钟,这就意味着这个job的大部分时间都消耗在task的调度和进程启停上了,因此可以考虑增加每个task处理的数据大小。建议一个task处理时间为1分钟。


第二节 Shuffle调优

Shuffle阶段是MapReduce性能的关键部分,包括了从MapTaskask将中间数据写到磁盘一直到ReduceTask拷贝数据并最终放到Reduce函数的全部过程。这一块Hadoop提供了大量的调优参数。

2.1 Map阶段

1、判断Map内存使用
判断Map分配的内存是否够用,可以查看运行完成的job的Counters中(历史服务器),对应的task是否发生过多次GC,以及GC时间占总task运行时间之比。通常,GC时间不应超过task运行时间的10%,即GC time elapsed (ms)/CPU time spent (ms)<10%。


Map需要的内存还需要随着环形缓冲区的调大而对应调整。可以通过如下参数进行调整。

mapreduce.map.memory.mb

Ma需要的CPU核数可以通过如下参数调整

mapreduce.map.cpu.vcores

可以看到内存默认是1G,CPU默认是1核。
如果集群资源充足建议调整:
mapreduce.map.memory.mb=3G(默认1G)mapreduce.map.cpu.vcores=1(默认也是1)
环形缓冲区

Map方法执行后首先把数据写入环形缓冲区,为什么MR框架选择先写内存而不是直接写磁盘?这样的目的主要是为了减少磁盘i/o

  • 环形缓冲默认100M(mapreduce.task.io.sort.mb),当到达80%(mapreduce.map.sort.spill.percent)时就会溢写磁盘。
  • 每达到80%都会重写溢写到一个新的文件。

当集群内存资源充足,考虑增大mapreduce.task.io.sort.mb提高溢写的效率,而且会减少中间结果的文件数量。
建议:

  • 调整mapreduce.task.io.sort.mb=512M。
  • 当文件溢写完后,会对这些文件进行合并,默认每次合并10(mapreduce.task.io.sort.factor)个溢写的文件,建议调整mapreduce.task.io.sort.factor=64。这样可以提高合并的并行度,减少合并的次数,降低对磁盘操作的次数。

2、Combiner
在Map阶段,有一个可选过程,将同一个key值的中间结果合并,叫做Combiner。(一般将reduce类设置为combiner即可)
通过Combine,一般情况下可以显著减少Map输出的中间结果,从而减少shuffle过程的网络带宽占用。
建议:
不影响最终结果的情况下,加上Combiner!!


2.2 Copy阶段

  • 对Map的中间结果进行压缩,当数据量大时,会显著减少网络传输的数据量,
  • 但是也因为多了压缩和解压,带来了更多的CPU消耗。因此需要做好权衡。当任务属于网络瓶颈类型时,压缩Map中间结果效果明显。
  • 在实际经验中Hadoop的运行的瓶颈一般都是IO而不是CPU,压缩一般可以10倍的减少IO操作

2.3 Reduce阶段

1、Reduce资源
每个Reduce资源

mapreduce.reduce.memory.mb=5G(默认1G)
mapreduce.reduce.cpu.vcores=1(默认为1)。

2、Copy
ReduceTask在copy的过程中默认使用5(mapreduce.reduce.shuffle.parallelcopies参数控制)个并行度进行复制数据。
该值在实际服务器上比较小,建议调整为50-100.
3、溢写归并
Copy过来的数据会先放入内存缓冲区中,然后当使用内存达到一定量的时候spill磁盘。这里的缓冲区大小要比map端的更为灵活,它基于JVM的heap size设置。这个内存大小的控制是通过mapreduce.reduce.shuffle.input.buffer.percent(default 0.7)控制的。

shuffile在reduce内存中的数据最多使用内存量为:0.7 × maxHeap of reduce task,内存到磁盘merge的启动可以通过mapreduce.reduce.shuffle.merge.percent(default0.66)配置。
copy完成后,reduce进入归并排序阶段,合并因子默认为10(mapreduce.task.io.sort.factor参数控制),如果map输出很多,则需要合并很多趟,所以可以提高此参数来减少合并次数。


第三节 Job调优

1、推测执行

集群规模很大时(几百上千台节点的集群),个别机器出现软硬件故障的概率就变大了,并且会因此延长整个任务的执行时间推测执行通过将一个task分给多台机器跑,取先运行完的那个,会很好的解决这个问题。对于小集群,可以将这个功能关闭。


建议:

  • 大型集群建议开启,小集群建议关闭!
  • 集群的推测执行都是关闭的。在需要推测执行的作业执行的时候开启

2、Slow Start

MapReduce的AM在申请资源的时候,会一次性申请所有的Map资源,延后申请reduce的资源,这样就能达到先执行完大部分Map再执行Reduce的目的。
mapreduce.job.reduce.slowstart.completedmaps
当多少占比的Map执行完后开始执行Reduce。默认5%的Map跑完后开始起Reduce。
如果想要Map完全结束后执行Reduce调整该值为1
4 、小文件优化

  • HDFS:hadoop的存储每个文件都会在NameNode上记录元数据,如果同样大小的文件,文件很小的话,就会产生很多文件,造成NameNode的压力。
  • MR:Mapreduce中一个map默认处理一个分片或者一个小文件,如果map的启动时间都比数据处理的时间还要长,那么就会造成性能低,而且在map端溢写磁盘的时候每一个map最终会产生reduce数量个数的中间结果,如果map数量特别多,就会造成临时文件很多,而且在reduce拉取数据的时候增加磁盘的IO。

如何处理小文件?

  • 从源头解决,尽量在HDFS上不存储小文件,也就是数据上传HDFS的时候就合并小文件
  • 通过运行MR程序合并HDFS上已经存在的小文件
  • MR计算的时候可以使用CombineTextInputFormat来降低MapTask并行度

5 、数据倾斜


MR是一个并行处理的任务,整个Job花费的时间是作业中所有Task最慢的那个了。

  • 为什么会这样呢?为什么会有的Task快有的Task慢?
  • 数据倾斜,每个Reduce处理的数据量不是同一个级别的,所有数据量少的Task已经跑完了,数据量大的Task则需要更多时间。
  • 有可能就是某些作业所在的NodeManager有问题或者container有问题,导致作业执行缓慢。

数据倾斜那么为什么会产生数据倾斜呢?
数据本身就不平衡,所以在默认的hashpartition时造成分区数据不一致问题
那如何解决数据倾斜的问题呢?

  • 默认的是hash算法进行分区,我们可以尝试自定义分区,修改分区实现逻辑,结合业务特点,使得每个分区数据基本平衡
  • 可以尝试修改分区的键,让其符合hash分区,并且使得最后的分区平衡,比如在key前加随机数nkey。
  • 抽取导致倾斜的key对应的数据单独处理。

如果不是数据倾斜带来的问题,而是节点服务有问题造成某些map和reduce执行缓慢呢?
使用推测执行找个其他的节点重启一样的任务竞争,谁快谁为准。推测执行时以空间换时间的优化。会带来集群资源的浪费,会给集群增加压力。


第四节 YARN调优


1、NM配置

可用内存

  • 刨除分配给操作系统、其他服务的内存外,剩余的资源应尽量分配给YARN。
  • 默认情况下,Map或Reduce container会使用1个虚拟CPU内核和1024MB内存,ApplicationMaster使用1536MB内存。
yarn.nodemanager.resource.memory-mb 默认是8192

CPU虚拟核数

  • 建议将此配置设定在逻辑核数的1.5~2倍之间。如果CPU的计算能力要求不高,可以配置为2倍的逻辑CPU。
yarn.nodemanager.resource.cpu-vcores
该节点上YARN可使用的虚拟CPU个数,默认是8。
目前推荐将该值设值为逻辑CPU核数的1.5~2倍之间

2、Container启动模式

YARN的NodeManager提供2种Container的启动模式。
默认,YARN为每一个Container启动一个JVM,JVM进程间不能实现资源共享,导致资源本地化的时间开销较大。针对启动时间较长的问题,新增了基于线程资源本地化启动模式,能够有效提升container启动效率。

yarn.nodemanager.container-executor.class
  • 设置为“org.apache.hadoop.yarn.server.nodemanager.DefaultContainerExecutor”,则每次启动container将会启动一个线程来实现资源本地化。
  • 该模式下,启动时间较短,但无法做到资源(CPU、内存)隔离。
  • 设置为“org.apache.hadoop.yarn.server.nodemanager.LinuxContainerExecutor” ,则每次启动container都会启动一个JVM进程来实现资源本地化。
  • 该模式下,启动时间较长,但可以提供较好的资源(CPU、内存)隔离能力。

3、AM调优

运行的一个大任务,map总数达到了上万的规模,任务失败,发现是ApplicationMaster(以下简称AM)反应缓慢,最终超时失败。
失败原因是Task数量变多时,AM管理的对象也线性增长,因此就需要更多的内存来管理。AM默认分配的内存大小是1.5GB。
建议:
任务数量多时增大AM内存

  • yarn.app.mapreduce.am.resource.mb

第五节 Namenode Full GC


JVM堆内存

  • JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(OldGeneration),非堆内存就一个永久代(Permanent Generation)。
  • 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。
  • 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
  • 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。

补充:
JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。


1、对象分代

  • 新生成的对象首先放到年轻代Eden区,
  • 当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,
  • Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。
  • 经过多次Minor GC仍然存活的对象移动到老年代。
  • 老年代存储长期存活的对象,占满时会触发Major GC(Full GC),GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。

Minor GC : 清理年轻代
Major GC(Full GC) : 清理老年代,清理整个堆空间,会停止应用所有线程。

2、Jstat

查看当前jvm内存使用以及垃圾回收情况

jstat -gc -t 58563 1s #显示pid是58563的垃圾回收堆的行为统计
Timestamp S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
9751.8 12288.0 12288.0 0.0 0.0 158208.0 8783.6 54272.023264.6 35496.0 34743.9 4144.0 3931.8 9 0.231 2 0.123 0.354
9752.8 12288.0 12288.0 0.0 0.0 158208.0 8783.6 54272.023264.6 35496.0 34743.9 4144.0 3931.8 9 0.231 2 0.123 0.354
9753.8 12288.0 12288.0 0.0 0.0 158208.0 8783.6 54272.023264.6 35496.0 34743.9 4144.0 3931.8 9 0.231 2 0.123 0.354
9754.8 12288.0 12288.0 0.0 0.0 158208.0 8783.6 54272.023264.6 35496.0 34743.9 4144.0 3931.8 9 0.231 2 0.123 0.354
9755.8 12288.0 12288.0 0.0 0.0 158208.0 8783.6 54272.023264.6 35496.0 34743.9 4144.0 3931.8 9 0.231 2 0.123 0.354
9756.9 12288.0 12288.0 0.0 0.0 158208.0 8783.6 54272.023264.6 35496.0 34743.9 4144.0 3931.8 9 0.231 2 0.123 0.354


结果解释:

#C即Capacity 总容量,U即Used 已使用的容量
S0C: 当前survivor0区容量(kB)。
S1C: 当前survivor1区容量(kB)。
S0U: survivor0区已使用的容量(KB)
S1U: survivor1区已使用的容量(KB)
EC: Eden区的总容量(KB)
EU: 当前Eden区已使用的容量(KB)
OC: Old空间容量(kB)。
OU: Old区已使用的容量(KB)
MC: Metaspace空间容量(KB)
MU: Metacspace使用量(KB)
CCSC: 压缩类空间容量(kB)。
CCSU: 压缩类空间使用(kB)。
YGC: 新生代垃圾回收次数
YGCT: 新生代垃圾回收时间
FGC: 老年代 full GC垃圾回收次数
FGCT: 老年代垃圾回收时间
GCT: 垃圾回收总消耗时间

开启HDFS GC详细日志输出
编辑hadoop-env.sh

export HADOOP_LOG_DIR=/hadoop/logs/

增加JMX配置打印详细GC信息
指定一个日志输出目录;注释掉之前的ops
增加新的打印配置

#JMX配置
export HADOOP_JMX_OPTS="-Dcom.sun.management.jmxremote.authenticate=false -
Dcom.sun.management.jmxremote.ssl=false"
export HADOOP_NAMENODE_OPTS="-Dhadoop.security.logger=${HADOOP_SECURITY_LOGGER:-
INFO,RFAS} -Dhdfs.audit.logger=${HDFS_AUDIT_LOGGER:-INFO,NullAppender}
$HADOOP_NAMENODE_OPTS"
export HADOOP_DATANODE_OPTS="-Dhadoop.security.logger=ERROR,RFAS
$HADOOP_DATANODE_OPTS"
export NAMENODE_OPTS="-verbose:gc -XX:+PrintGCDetails -
Xloggc:${HADOOP_LOG_DIR}/logs/hadoop-gc.log \
-XX:+PrintGCDateStamps -XX:+PrintGCApplicationConcurrentTime -
XX:+PrintGCApplicationStoppedTime \
-server -Xms150g -Xmx150g -Xmn20g -XX:SurvivorRatio=8 -
XX:MaxTenuringThreshold=15 \
-XX:ParallelGCThreads=18 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -
XX:+UseCMSCompactAtFullCollection -XX:+DisableExplicitGC -
XX:+CMSParallelRemarkEnabled \
-XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=70 -
XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -
XX:CMSMaxAbortablePrecleanTime=5000 \
-XX:+UseGCLogFileRotation -XX:GCLogFileSize=20m -
XX:ErrorFile=${HADOOP_LOG_DIR}/logs/hs_err.log.%p -
XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${HADOOP_LOG_DIR}/logs/%p.hprof
\
"
export DATENODE_OPTS="-verbose:gc -XX:+PrintGCDetails -
Xloggc:${HADOOP_LOG_DIR}/hadoop-gc.log \
-XX:+PrintGCDateStamps -XX:+PrintGCApplicationConcurrentTime -
XX:+PrintGCApplicationStoppedTime \
-server -Xms15g -Xmx15g -Xmn4g -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
\
-XX:ParallelGCThreads=18 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -
XX:+UseCMSCompactAtFullCollection -XX:+DisableExplicitGC -
XX:+CMSParallelRemarkEnabled \
-XX:+CMSClassUnloadingEnabled -XX:CMSInitiatingOccupancyFraction=70 -
XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -
XX:CMSMaxAbortablePrecleanTime=5000 \
-XX:+UseGCLogFileRotation -XX:GCLogFileSize=20m -
XX:ErrorFile=${HADOOP_LOG_DIR}/logs/hs_err.log.%p -
XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${HADOOP_LOG_DIR}/logs/%p.hprof
\
"
export HADOOP_NAMENODE_OPTS="$NAMENODE_OPTS $HADOOP_NAMENODE_OPTS"
export HADOOP_DATANODE_OPTS="$DATENODE_OPTS $HADOOP_DATANODE_OPTS"
  • -Xms150g -Xmx150g :堆内存大小最大和最小都是150g
  • -Xmn20g :新生代大小为20g,等于eden+2*survivor,意味着老年代为150-20=130g。
  • -XX:SurvivorRatio=8 :Eden和Survivor的大小比值为8,意味着两个Survivor区和一个Eden区的比值为2:8,一个Survivor占整个年轻代的1/10
  • -XX:ParallelGCThreads=10 :设置ParNew GC的线程并行数,默认为8 +(Runtime.availableProcessors - 8) * 5/8 ,24核机器为18。
  • -XX:MaxTenuringThreshold=15 :设置对象在年轻代的最大年龄,超过这个年龄则会晋升到老年代
  • -XX:+UseParNewGC :设置新生代使用Parallel New GC
  • -XX:+UseConcMarkSweepGC :设置老年代使用CMS GC,当此项设置时候自动设置新生代为ParNew GC
  • -XX:CMSInitiatingOccupancyFraction=70 :老年代第一次占用达到该百分比时候,就会引发CMS的第一次垃圾回收周期。后继CMS GC由HotSpot自动优化计算得到。

3、GC 日志解析

jstat命令输出

ime S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
5.7 34048.0 34048.0 0.0 34048.0 272640.0 194699.7 1756416.0 181419.9 18304.0 17865.1 2688.0 2497.6 3 0.275 0 0.000 0.275
6.7 34048.0 34048.0 34048.0 0.0 272640.0 247555.4 1756416.0 263447.9 18816.0 18123.3 2688.0 2523.1 4 0.359 0 0.000 0.359 
7.7 34048.0 34048.0 0.0 34048.0 272640.0 257729.3 1756416.0 345109.819072.0 18396.6 2688.0 2550.3 5 0.451 0 0.000 0.451
8.7 34048.0 34048.0 34048.0 34048.0 272640.0 272640.0 1756416.0 4 44982.519456.0 18681.3 2816.0 2575.8 7 0.550 0 0.000 0.550
9.7 34048.0 34048.0 34046.7 0.0 272640.0 16777.0 1756416.0 587906.3 20096.0 19235.1 2944.0 2631.8 8 0.720 0 0.000 0.720
10.7 34048.0 34048.0 0.0 34046.2 272640.0 80171.6 1756416.0 664913.4 20352.0 19495.9 2944.0 2657.4 9 0.810 0 0.000 0.810
11.7 34048.0 34048.0 34048.0 0.0 272640.0 129480.8 1756416.0 745100.2 20608.0 19704.5 2944.0 2678.4 10 0.896 0 0.000 0.896
12.7 34048.0 34048.0 0.0 34046.6 272640.0 164070.7 1756416.0 822073.7 20992.0 19937.1 3072.0 2702.8 11 0.978 0 0.000 0.978
13.7 34048.0 34048.0 34048.0 0.0 272640.0 211949.9 1756416.0 897364.4 21248.0 20179.6 3072.0 2728.1 12 1.087 1 0.004 1.091
14.7 34048.0 34048.0 0.0 34047.1 272640.0 245801.5 1756416.0 597362.6 21504.0 20390.6 3072.0 2750.3 13 1.183 2 0.050 1.233
15.7 34048.0 34048.0 0.0 34048.0 272640.0 21474.1 1756416.0 757347.0 22012.0 20792.0 3200.0 2791.0 15 1.336 2 0.050 1.386
16.7 34048.0 34048.0 34047.0 0.0 272640.0 48378.0 1756416.0 838594.4 22268.0 21003.5 3200.0 2813.2 16 1.433 2 0.050 1.484

查看GC日志输出

3.157: [GC (Allocation Failure) 3.157: [ParNew: 272640K->34048K(306688K),0.0844702 secs] 272640K->69574K(2063104K), 0.0845560 secs] [Times: user=0.23 sys=0.03, real=0.09 secs]
4.092: [GC (Allocation Failure) 4.092: [ParNew: 306688K->34048K(306688K),0.1013723 secs] 342214K->136584K(2063104K), 0.1014307 secs] [Times: user=0.25sys=0.05, real=0.10 secs]
... cut for brevity ...
11.292: [GC (Allocation Failure) 11.292: [ParNew: 306686K->34048K(306688K),0.0857219 secs] 971599K->779148K(2063104K), 0.0857875 secs] [Times: user=0.26sys=0.04, real=0.09 secs]
12.140: [GC (Allocation Failure) 12.140: [ParNew: 306688K->34046K(306688K),0.0821774 secs] 1051788K->856120K(2063104K), 0.0822400 secs] [Times: user=0.25 sys=0.03, real=0.08 secs]
12.989: [GC (Allocation Failure) 12.989: [ParNew: 306686K->34048K(306688K),0.1086667 secs] 1128760K->931412K(2063104K), 0.1087416 secs] [Times: user=0.24 sys=0.04, real=0.11 secs]
13.098: [GC (CMS Initial Mark) [1 CMS-initial-mark: 897364K(1756416K)]936667K(2063104K), 0.0041705 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
13.102: [CMS-concurrent-mark-start]
13.341: [CMS-concurrent-mark: 0.238/0.238 secs] [Times: user=0.36 sys=0.01,real=0.24 secs]
13.341: [CMS-concurrent-preclean-start]
13.350: [CMS-concurrent-preclean: 0.009/0.009 secs] [Times: user=0.03 sys=0.00,real=0.01 secs]
13.350: [CMS-concurrent-abortable-preclean-start]
13.878: [GC (Allocation Failure) [ParNew: 16844397K->85085K(18874368K),0.0960456 secs]116885867K->100127390K(155189248K), 0.0961542 secs] [Times: user=0.14 sys=0.00,real=0.05 secs]
14.366: [CMS-concurrent-abortable-preclean: 0.917/1.016 secs] [Times: user=2.22 sys=0.07, real=1.01 secs]
14.366: [GC (CMS Final Remark) [YG occupancy: 182593 K (306688 K)]14.366:[Rescan (parallel) , 0.0291598 secs]14.395: [weak refs processing, 0.0000232secs]14.395: [class unloading, 0.0117661 secs]14.407: [scrub symbol table,0.0015323 secs]14.409: [scrubstring table, 0.0003221 secs][1 CMS-remark:976591K(1756416K)] 1159184K(2063104K), 0.0462010secs] [Times: user=0.14sys=0.00, real=0.05 secs]
14.412: [CMS-concurrent-sweep-start]
14.633: [CMS-concurrent-sweep: 0.221/0.221 secs] [Times: user=0.37 sys=0.00,real=0.22 secs]
14.633: [CMS-concurrent-reset-start]
14.636: [CMS-concurrent-reset: 0.002/0.002 secs] [Times: user=0.00 sys=0.00,real=0.00 secs]
  • 1. ParNew: 16844397K->85085K(18874368K), 0.0960456 secs其中, 16844397K 表示GC前的新生代占用量, 85085K 表示GC后的新生代占用量,GC后Eden和一个Survivor为空,所以85085K 也是另一个Survivor的占用量。括号中的18874368K 是Eden+一个被占用Survivor的总和(18g)。
  • 2. 116885867K->100127390K(155189248K), 0.0961542 secs其中,分别是Java堆在垃圾回收前后的大小,和Java堆大小。说明堆使用为116885867K=111.47g,回收大小为100127390K=95.49g,堆大小为155189248K=148g(去掉其中一个Survivor),回收了16g空间.

总结:

  • 在HDFS Namenode内存中的对象大都是文件,目录和blocks,这些数据只要不被程序或者数据的拥有者人为的删除,就会在Namenode的运 行生命期内一直存在,所以这些对象通常是存在在old区中,所以,如果整个hdfs文件和目录数多,blocks数也多,内存数据也会很大,如何降低Full GC的影响?

计算NN所需的内存大小,合理配置JVM


使用低卡顿G1收集器
为什么会有G1呢?
因为并发、并行和CMS垃圾收集器都有2个共同的问题:

  • 老年代收集器大部分操作都必须扫描整个老年代空间(标记,清除和压缩)。这就导致了GC随着Java堆空间而线性增加或减少
  • 年轻代和老年代是独立的连续内存块,所以要先决定年轻代和年老代放在虚拟地址空间的位置

G1垃圾收集器利用分而治之的思想将堆进行分区,划分为一个个的区域。
G1垃圾收集器将堆拆成一系列的分区,这样的话,大部分的垃圾收集操作就只在一个分区内执行,从而避免很多GC操作在整个Java堆或者整个年轻代进行。
编辑hadoop-env.sh

export HADOOP_NAMENODE_OPTS="-server -Xmx220G -Xms200G -XX:+UseG1GC -
XX:MaxGCPauseMillis=200 -XX:+UnlockExperimentalVMOptions -
XX:+ParallelRefProcEnabled -XX:-ResizePLAB -XX:+PerfDisableSharedMem -XX:-OmitStackTraceInFastThrow -XX:G1NewSizePercent=2 -XX:ParallelGCThreads=23 -
XX:InitiatingHeapOccupancyPercent=40 -XX:G1HeapRegionSize=32M -
XX:G1HeapWastePercent=10 -XX:G1MixedGCCountTarget=16 -verbose:gc -
XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -
XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=100M -Xloggc:/var/log/hbase/gc.log -Dhadoop.security.logger=${HADOOP_SECURITY_LOGGER:-INFO,RFAS} -Dhdfs.audit.logger=${HDFS_AUDIT_LOGGER:-INFO,NullAppender}$HADOOP_NAMENODE_OPTS"


注意:如果现在采用的垃圾收集器没有问题,就不要选择G1,如果追求低停顿,可以尝试使用G1


第六节 Hadoop二次开发环境搭建

系统环境

系统: CentOS-7_x86_64
protobuf: protoc-2.5.0
maven: maven-3.6.0
hadoop: hadoop-2.9.2
java: jdk1.8.0_131
cmake: cmake-2.8.12.2
OpenSSL: OpenSSL 1.0.2k-fips
findbugs: findbugs-1.3.9

准备工作

# 安装编译需要的依赖库
yum install -y lzo-devel zlib-devel autoconf automake libtool cmake openssldevel cmake gcc gcc-c++


安装Maven

#上传maven安装包
# 解压缩
$ tar -zxvf apache-maven-3.6.3-bin.tar.gz -C /usr/local/
# 配置到系统环境变量
$ vim /etc/profile
export MAVEN_HOME=/usr/local/apache-maven-3.6.3
export PATH=$PATH:$MAVEN_HOME/bin
# 刷新配置文件
$ source /etc/profile
# 验证maven安装是是否成功
$ mvn -version
[root@localhost ~]# mvn -version
Apache Maven 3.6.0 (ff8f5e7444045639af65f6095c62210b5713f426; 2018-10-
25T03:39:06+08:00)
Maven home: /usr/local/apache-maven-3.6.3
Java version: 1.8.0_131, vendor: Oracle Corporation
Java home: /usr/local/jdk1.8.0_131/jre
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "4.20.13-1.el7.elrepo.x86_64", arch: "amd64", family:"unix"

安装protobuf

# 安装依赖环境
$ yum groupinstall Development tools -y
# 下载
$ https://github.com/protocolbuffers/protobuf/releases/download/v2.5.0/protobuf-2.5.0.tar.gz
#上传protobuf安装包
# 解压缩
$ tar -zxvf protobuf-2.5.0.tar.gz
cd protobuf-2.5.0
# 进入解压目录 配置安装路径(--prefix=/usr/local/protobuf-2.5.0)
$ ./configure --prefix=/usr/local/protobuf-2.5.0
# 编译
$ make
# 验证编译文件
$ make check
# 安装
$ make install
# 配置protobuf环境变量
$ vim /etc/profile
export PROTOCBUF_HOME=/usr/local/protobuf-2.5.0
export PATH=$PATH:$PROTOCBUF_HOME/bin
# 刷新配置文件
$ source /etc/profile
# 验证是否安装成功
$ protoc --version
[root@localhost ~]# protoc --version
libprotoc 2.5.0

安装Findbugs

#下载
$ https://jaist.dl.sourceforge.net/project/findbugs/findbugs/1.3.9/findbugs-1.3.9.tar.gz
#上传安装包
# 解压缩
$ tar -zxvf findbugs-1.3.9.tar.gz -C /usr/local/
# 配置系统环境变量
$ vim /etc/profile
export FINDBUGS_HOME=/usr/local/findbugs-1.3.9
export PATH=$PATH:$FINDBUGS_HOME/bin
# 刷新配置文件
$ source /etc/profile
# 验证是否安装成功
$ findbugs -version
[root@localhost ~]# findbugs -version
1.3.9


添加aliyun镜像
找到maven环境下的settings.xml文件,添加镜像代理

<mirror>
    <id>nexus</id>
    <mirrorOf>*</mirrorOf>
    <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>
<mirror>
    <id>nexus-public-snapshots</id>
    <mirrorOf>public-snapshots</mirrorOf>
    <url>http://maven.aliyun.com/nexus/content/repositories/snapshots/</url>
</mirror>

上传源码文件
进入代码文件目标路径

/root/hadoop-2.9.2-src/hadoop-mapreduce-project/hadoop-mapreduce-client/hadoopmapreduce-client-core/src/main/java/org/apache/hadoop/mapreduce/lib/input

编译
进入Hadoop源码目录

cd /root/hadoop-2.9.2-src


执行编译命令

mvn package -Pdist,native -DskipTests -Dtar


问题解决
hadoop-aws:jar时缺少依赖包DynamoDBLocal:jar
选择手动下载该Jar包,上传到本地maven仓库

cd /root/.m2/repository/com/amazonaws/DynamoDBLocal/1.11.86


编译成功 

[INFO] Reactor Summary for Apache Hadoop Main 2.9.2:
[INFO]
[INFO] Apache Hadoop Main ................................. SUCCESS [ 1.165 s]
[INFO] Apache Hadoop Build Tools .......................... SUCCESS [ 0.747 s]
[INFO] Apache Hadoop Project POM .......................... SUCCESS [ 0.808 s]
[INFO] Apache Hadoop Annotations .......................... SUCCESS [ 1.773 s]
[INFO] Apache Hadoop Assemblies ........................... SUCCESS [ 0.202 s]
[INFO] Apache Hadoop Project Dist POM ..................... SUCCESS [ 1.468 s]
[INFO] Apache Hadoop Maven Plugins ........................ SUCCESS [ 2.847 s]
[INFO] Apache Hadoop MiniKDC .............................. SUCCESS [ 3.001 s]
[INFO] Apache Hadoop Auth ................................. SUCCESS [ 4.041 s]
[INFO] Apache Hadoop Auth Examples ........................ SUCCESS [ 2.509 s]
[INFO] Apache Hadoop Common ............................... SUCCESS [ 49.423 s]
[INFO] Apache Hadoop NFS .................................. SUCCESS [ 3.809 s]
[INFO] Apache Hadoop KMS .................................. SUCCESS [ 10.492 s]
[INFO] Apache Hadoop Common Project ....................... SUCCESS [ 0.063 s]
[INFO] Apache Hadoop HDFS Client .......................... SUCCESS [ 12.434 s]
[INFO] Apache Hadoop HDFS ................................. SUCCESS [ 37.065 s]
[INFO] Apache Hadoop HDFS Native Client ................... SUCCESS [ 1.951 s]
[INFO] Apache Hadoop HttpFS ............................... SUCCESS [ 15.952 s]
[INFO] Apache Hadoop HDFS BookKeeper Journal .............. SUCCESS [ 3.922 s]
[INFO] Apache Hadoop HDFS-NFS ............................. SUCCESS [ 3.316 s]
[INFO] Apache Hadoop HDFS-RBF ............................. SUCCESS [ 15.651 s]
[INFO] Apache Hadoop HDFS Project ......................... SUCCESS [ 0.041 s]
[INFO] Apache Hadoop YARN ................................. SUCCESS [ 0.050 s]
[INFO] Apache Hadoop YARN API ............................. SUCCESS [ 8.927 s]
[INFO] Apache Hadoop YARN Common .......................... SUCCESS [01:10 min]
[INFO] Apache Hadoop YARN Registry ........................ SUCCESS [ 4.114 s]
[INFO] Apache Hadoop YARN Server .......................... SUCCESS [ 0.040 s]
[INFO] Apache Hadoop YARN Server Common ................... SUCCESS [ 9.403 s]
[INFO] Apache Hadoop YARN NodeManager ..................... SUCCESS [ 18.255 s]
[INFO] Apache Hadoop YARN Web Proxy ....................... SUCCESS [ 2.929 s]
[INFO] Apache Hadoop YARN ApplicationHistoryService ....... SUCCESS [ 5.629 s]
[INFO] Apache Hadoop YARN Timeline Service ................ SUCCESS [ 4.198 s]
[INFO] Apache Hadoop YARN ResourceManager ................. SUCCESS [ 37.668 s]
[INFO] Apache Hadoop YARN Server Tests .................... SUCCESS [ 0.762 s]
[INFO] Apache Hadoop YARN Client .......................... SUCCESS [ 4.561 s]
[INFO] Apache Hadoop YARN SharedCacheManager .............. SUCCESS [ 3.043 s]
[INFO] Apache Hadoop YARN Timeline Plugin Storage ......... SUCCESS [ 2.427 s]
[INFO] Apache Hadoop YARN Router .......................... SUCCESS [ 3.277 s]
[INFO] Apache Hadoop YARN TimelineService HBase Backend ... SUCCESS [ 5.804 s]
[INFO] Apache Hadoop YARN Timeline Service HBase tests .... SUCCESS [ 1.815 s]
[INFO] Apache Hadoop YARN Applications .................... SUCCESS [ 0.040 s]
[INFO] Apache Hadoop YARN DistributedShell ................ SUCCESS [ 2.236 s]
[INFO] Apache Hadoop YARN Unmanaged Am Launcher ........... SUCCESS [ 1.769 s]
[INFO] Apache Hadoop YARN Site ............................ SUCCESS [ 0.045 s]
[INFO] Apache Hadoop YARN UI .............................. SUCCESS [ 0.046 s]
[INFO] Apache Hadoop YARN Project ......................... SUCCESS [ 6.043 s]
[INFO] Apache Hadoop MapReduce Client ..................... SUCCESS [ 0.180 s]
[INFO] Apache Hadoop MapReduce Core ....................... SUCCESS [ 59.165 s]
[INFO] Apache Hadoop MapReduce Common ..................... SUCCESS [ 28.713 s]
[INFO] Apache Hadoop MapReduce Shuffle .................... SUCCESS [ 3.509 s]
[INFO] Apache Hadoop MapReduce App ........................ SUCCESS [ 9.007 s]
[INFO] Apache Hadoop MapReduce HistoryServer .............. SUCCESS [ 4.384 s]
[INFO] Apache Hadoop MapReduce JobClient .................. SUCCESS [ 3.720 s]
[INFO] Apache Hadoop MapReduce HistoryServer Plugins ...... SUCCESS [ 1.733 s]
[INFO] Apache Hadoop MapReduce Examples ................... SUCCESS [ 3.492 s]
[INFO] Apache Hadoop MapReduce ............................ SUCCESS [ 3.083 s]
[INFO] Apache Hadoop MapReduce Streaming .................. SUCCESS [ 3.086 s]
[INFO] Apache Hadoop Distributed Copy ..................... SUCCESS [ 6.657 s]
[INFO] Apache Hadoop Archives ............................. SUCCESS [ 1.497 s]
[INFO] Apache Hadoop Archive Logs ......................... SUCCESS [ 1.641 s]
[INFO] Apache Hadoop Rumen ................................ SUCCESS [ 3.512 s]
[INFO] Apache Hadoop Gridmix .............................. SUCCESS [ 3.253 s]
[INFO] Apache Hadoop Data Join ............................ SUCCESS [ 1.906 s]
[INFO] Apache Hadoop Ant Tasks ............................ SUCCESS [ 1.669 s]
[INFO] Apache Hadoop Extras ............................... SUCCESS [ 2.252 s]
[INFO] Apache Hadoop Pipes ................................ SUCCESS [ 0.319 s]
[INFO] Apache Hadoop OpenStack support .................... SUCCESS [ 2.962 s]
[INFO] Apache Hadoop Amazon Web Services support .......... SUCCESS [ 7.011 s]
[INFO] Apache Hadoop Azure support ........................ SUCCESS [ 5.998 s]
[INFO] Apache Hadoop Aliyun OSS support ................... SUCCESS [ 4.510 s]
[INFO] Apache Hadoop Client ............................... SUCCESS [ 5.978 s]
[INFO] Apache Hadoop Mini-Cluster ......................... SUCCESS [ 0.536 s]
[INFO] Apache Hadoop Scheduler Load Simulator ............. SUCCESS [ 3.995 s]
[INFO] Apache Hadoop Resource Estimator Service ........... SUCCESS [ 4.019 s]
[INFO] Apache Hadoop Azure Data Lake support .............. SUCCESS [ 3.439 s]
[INFO] Apache Hadoop Tools Dist ........................... SUCCESS [ 18.642 s]
[INFO] Apache Hadoop Tools ................................ SUCCESS [ 0.038 s]
[INFO] Apache Hadoop Distribution ......................... SUCCESS [ 47.384 s]
[INFO] Apache Hadoop Cloud Storage ........................ SUCCESS [ 2.062 s]
[INFO] Apache Hadoop Cloud Storage Project ................ SUCCESS [ 0.034 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值