大数据_Hadoop

简述

1、大数据:就是对海量数据进行分析处理,得到一些有价值的信息,然后帮助企业做出判断和决策。
获取数据—处理数据—展现结果。

2、Hadoop:一个分布式系基础框架,它允许使用简单的编程模型跨大型计算机的大型数据集进行分布式处理。
大数据存储问题:HDFS
大数据计算问题:MapReduce

3、Hadoop 的组成

  • Hadoop分布式文件系统(HDFS) 提供对应用程序数据的高吞吐量访问的分布式文件系统
  • Hadoop Common 其他Hadoop模块所需的Java库和实用程序。这些库提供文件系统和操作系统级抽象,并包含启动Hadoop所需的必要Java文件和脚本
  • Hadoop MapReduce 基于YARN的大型数据集并行处理系统
  • Hadoop YARN 作业调度和集群资源管理的框架

4、Hadoop的历史版本介绍
1.x版本系列:hadoop版本当中的第二代开源版本,主要修复0.x版本的一些bug等
2.x版本系列:架构产生重大变化,引入了yarn平台等许多新特性
3.x版本系列: 加入多namenoode新特性

5、Hadoop三大公司发型版本介绍
免费开源版本apache(目前使用)
免费开源版本hortonWorks
软件收费版本ClouderaManager

架构模型

1.x版本

在这里插入图片描述
文件系统核心模块:
NameNode:集群当中的主节点,管理元数据(文件的大小,文件的位置,文件的权限),主要用于管理集群当中的各种数据。
secondaryNameNode:主要能用于hadoop当中元数据信息的辅助管理。
DataNode:集群当中的从节点,主要用于存储集群当中的各种数据。

数据计算核心模块:
JobTracker:接收用户的计算请求任务,并分配任务给从节点。
TaskTracker:负责执行主节点JobTracker分配的任务。

2.x版本

1、NameNode与ResourceManager单节点架构模型
在这里插入图片描述
文件系统核心模块:
NameNode:集群当中的主节点,主要用于管理集群当中的各种数据。
secondaryNameNode:主要能用于hadoop当中元数据信息的辅助管理。
DataNode:集群当中的从节点,主要用于存储集群当中的各种数据。

数据计算核心模块:
ResourceManager:接收用户的计算请求任务,并负责集群的资源分配。
NodeManager:负责执行主节点APPmaster分配的任务。

2、NameNode单节点与ResourceManager高可用架构模型
在这里插入图片描述
文件系统核心模块:
NameNode:集群当中的主节点,主要用于管理集群当中的各种数据。
secondaryNameNode:主要能用于hadoop当中元数据信息的辅助管理。
DataNode:集群当中的从节点,主要用于存储集群当中的各种数据。

数据计算核心模块:
ResourceManager:接收用户的计算请求任务,并负责集群的资源分配,以及计算任务的划分,通过zookeeper实现ResourceManager的高可用。
NodeManager:负责执行主节点ResourceManager分配的任务。

3、NameNode高可用与ResourceManager单节点架构模型
在这里插入图片描述
文件系统核心模块:
NameNode:集群当中的主节点,主要用于管理集群当中的各种数据,其中nameNode可以有两个,形成高可用状态。
DataNode:集群当中的从节点,主要用于存储集群当中的各种数据。
JournalNode:文件系统元数据信息管理。

数据计算核心模块:
ResourceManager:接收用户的计算请求任务,并负责集群的资源分配,以及计算任务的划分。
NodeManager:负责执行主节点ResourceManager分配的任务。

4、NameNode与ResourceManager高可用架构模型
在这里插入图片描述
文件系统核心模块:
NameNode:集群当中的主节点,主要用于管理集群当中的各种数据,一般都是使用两个,实现HA高可用。
JournalNode:元数据信息管理进程,一般都是奇数个。
DataNode:从节点,用于数据的存储。

数据计算核心模块:
ResourceManager:Yarn平台的主节点,主要用于接收各种任务,通过两个,构建成高可用。
NodeManager:Yarn平台的从节点,主要用于处理ResourceManager分配的任务。

HDFS

简述

HDFS(Hadoop Distributed File System)是一个 Apache Software Foundation 项目, 是Apache Hadoop 项目的一个子项目. Hadoop 非常适于存储大型数据 (比如 TB 和 PB), 其就是使用 HDFS 作为存储系统. HDFS 使用多台计算机存储文件, 并且提供统一的访问接口, 像是访问一个普通文件系统一样使用分布式文件系统. HDFS 对数据文件的访问通过流的方式进行处理, 这意味着通过命令和 MapReduce 程序的方式可以直接使用 HDFS. HDFS 是容错的, 且提供对大数据集的高吞吐量访问.

HDFS 的一个非常重要的特点就是一次写入、多次读取, 该模型降低了对并发控制的要求, 简化了数据聚合性, 支持高吞吐量访问. 而吞吐量是大数据系统的一个非常重要的指标, 吞吐量高意味着能处理的数据量就大.

通过跨多个廉价计算机集群分布数据和处理来节约成本。
通过自动维护多个数据副本和在故障发生时来实现可靠性。
它们为存储和处理超大规模数据提供所需的扩展能力。

发展历史:
1、Doug Cutting 在做 Lucene 的时候, 需要编写一个爬虫服务, 这个爬虫写的并不顺利, 遇到了一些问题, 诸如: 如何存储大规模的数据, 如何保证集群的可伸缩性, 如何动态容错等。
2、2013年的时候, Google 发布了三篇论文, 被称作为三驾马车, 其中有一篇叫做 GFS, 是描述了 Google 内部的一个叫做 GFS 的分布式大规模文件系统, 具有强大的可伸缩性和容错性。
3、Doug Cutting 后来根据 GFS 的论文, 创造了一个新的文件系统, 叫做 HDFS。

架构

1、NameNode是一个中心服务器, 单一节点(简化系统的设计和实现), 负责管理文件系统的名字空间(NameSpace)以及客户端对文件的访问
2、文件操作, NameNode 是负责文件元数据的操作, DataNode 负责处理文件内容的读写请求, 跟文件内容相关的数据流不经过 NameNode, 只询问它跟哪个 DataNode联系, 否则 NameNode 会成为系统的瓶颈
3、副本存放在哪些 DataNode 上由 NameNode 来控制, 根据全局情况作出块放置决定, 读取文件时 NameNode 尽量让用户先读取最近的副本, 降低读取网络开销和读取延时
4、NameNode 全权管理数据库的复制, 它周期性的从集群中的每个 DataNode 接收心跳信合和状态报告, 接收到心跳信号意味着 DataNode 节点工作正常, 块状态报告包含了一个该 DataNode 上所有的数据列表

NameNode:
存储元数据、元数据保存在内存中、保存文件, block, DataNode 之间的关系
DataNode:
存储文件内容、文件内容保存在磁盘、维护了 block id 到 DataNode 文件之间的关系

HDFS 文件副本和 Block 块存储

所有的文件都是以 block 块的方式存放在 HDFS 文件系统当中, 在 Hadoop1 当中, 文件的 block 块默认大小是 64M, hadoop2 当中, 文件的 block 块大小默认是 128M, block 块的大小可以通过 hdfs-site.xml 当中的配置文件进行指定

<property>
    <name>dfs.block.size</name>
    <value>块大小 以字节为单位</value>
</property>

1、引入块机制的好处
一个文件有可能大于集群中任意一个磁盘。
使用块抽象而不是文件可以简化存储子系统。
块非常适合用于数据备份进而提供数据容错能力和可用性。

2、块缓存
通常 DataNode 从磁盘中读取块, 但对于访问频繁的文件, 其对应的块可能被显式的缓存在 DataNode 的内存中, 以堆外块缓存的形式存在. 默认情况下,一个块仅缓存在一个 DataNode 的内存中,当然可以针对每个文件配置 DataNode 的数量. 作业调度器通过在缓存块的 DataNode 上运行任务, 可以利用块缓存的优势提高读操作的性能。
例如:
连接(join) 操作中使用的一个小的查询表就是块缓存的一个很好的候选。
用户或应用通过在缓存池中增加一个 Cache Directive 来告诉 NameNode 需要缓存哪些文件及存多久. 缓存池(Cache Pool) 是一个拥有管理缓存权限和资源使用的管理性分组。
例如一个文件 130M, 会被切分成 2 个 block 块, 保存在两个 block 块里面, 实际占用磁盘 130M 空间, 而不是占用256M的磁盘空间。

3、HDFS 文件权限验证
HDFS 的文件权限机制与 Linux 系统的文件权限机制类似(r:read w:write x:execute)。
权限 x 对于文件表示忽略, 对于文件夹表示是否有权限访问其内容。
如果 Linux 系统用户 zhangsan 使用 Hadoop 命令创建一个文件, 那么这个文件在 HDFS 当中的 Owner 就是 zhangsan。
HDFS 文件权限的目的, 防止好人做错事, 而不是阻止坏人做坏事. HDFS相信你告诉我你是谁, 你就是谁。

HDFS 的元信息和 SecondaryNameNode

当 Hadoop 的集群当中, 只有一个 NameNode 的时候, 所有的元数据信息都保存在了 FsImage 与 Eidts 文件当中, 这两个文件就记录了所有的数据的元数据信息, 元数据信息的保存目录配置在了 hdfs-site.xml 当中

<property>
  <name>dfs.namenode.name.dir</name>
  <value>file:///export/servers/hadoop-3.1.1/datas/namenode/namenodedatas</value>
</property>
<property>
  <name>dfs.namenode.edits.dir</name>
  <value>file:///export/servers/hadoop-3.1.1/datas/dfs/nn/edits</value>
</property>

1、FsImage 和 Edits 详解
edits:
edits 存放了客户端最近一段时间的操作日志。
客户端对 HDFS 进行写文件时会首先被记录在 edits 文件中。
edits 修改时元数据也会更新。
每次 HDFS 更新时edits 先更新后客户端才会看到最新信息。

使用命令 hdfs  oiv  文件信息查看
cd /export/servers/hadoop-3.1.1/datas/namenode/namenodedatas
hdfs oiv -i fsimage_0000000000000000864 -p XML -o hello.xml

fsimage:
NameNode 中关于元数据的镜像, 一般称为检查点, fsimage 存放了一份比较完整的元数据信息。
因为 fsimage 是 NameNode 的完整的镜像, 如果每次都加载到内存生成树状拓扑结构,这是非常耗内存和CPU, 所以一般开始时对 NameNode 的操作都放在 edits 中。
fsimage 内容包含了 NameNode 管理下的所有 DataNode 文件及文件 block 及 block 所在的 DataNode 的元数据信息。
随着 edits 内容增大, 就需要在一定时间点和 fsimage 合并。

使用命令 hdfs  oev  文件信息查看
cd /export/servers/hadoop-3.1.1/datas/dfs/nn/edits
hdfs oev -i  edits_0000000000000000865-0000000000000000866 -o myedit.xml -p XML

2、SecondaryNameNode 如何辅助管理 fsimage 与 edits 文件
SecondaryNameNode 定期合并 fsimage 和 edits, 把 edits 控制在一个范围内。

配置 SecondaryNameNode:

1、SecondaryNameNode 在 conf/masters 中指定
2、在 masters 指定的机器上, 修改 hdfs-site.xml
<property>
  <name>dfs.http.address</name>
  <value>host:50070</value>
</property>
3、修改 core-site.xml, 这一步不做配置保持默认也可以
<!-- 多久记录一次 HDFS 镜像, 默认 1小时 -->
<property>
  <name>fs.checkpoint.period</name>
  <value>3600</value>
</property>
<!-- 一次记录多大, 默认 64M -->
<property>
  <name>fs.checkpoint.size</name>
  <value>67108864</value>
</property>

SecondaryNameNode 通知 NameNode 切换 editlog。
SecondaryNameNode 从 NameNode 中获得 fsimage 和 editlog(通过http方式)。
SecondaryNameNode 将 fsimage 载入内存, 然后开始合并 editlog, 合并之后成为新的 fsimage。
SecondaryNameNode 将新的 fsimage 发回给 NameNode。
NameNode 用新的 fsimage 替换旧的 fsimage。

3、特点
1)完成合并的是 SecondaryNameNode, 会请求 NameNode 停止使用 edits, 暂时将新写操作放入一个新的文件中 edits.new。
2)SecondaryNameNode 从 NameNode 中通过 Http GET 获得 edits, 因为要和 fsimage 合并, 所以也是通过 Http Get 的方式把 fsimage 加载到内存, 然后逐一执行具体对文件系统的操作, 与 fsimage 合并, 生成新的 fsimage, 然后通过 Http POST 的方式把 fsimage 发送给 NameNode. NameNode 从 SecondaryNameNode 获得了 fsimage 后会把原有的 fsimage 替换为新的 fsimage, 把 edits.new 变成 edits. 同时会更新 fstime。
3)Hadoop 进入安全模式时需要管理员使用 dfsadmin 的 save namespace 来创建新的检查点。
4)SecondaryNameNode 在合并 edits 和 fsimage 时需要消耗的内存和 NameNode 差不多, 所以一般把 NameNode 和 SecondaryNameNode 放在不同的机器上。

HDFS 文件写入过程

Client 发起文件上传请求, 通过 RPC 与 NameNode 建立通讯, NameNode 检查目标文件是否已存在, 父目录是否存在, 返回是否可以上传

Client 请求第一个 block 该传输到哪些 DataNode 服务器上

NameNode 根据配置文件中指定的备份数量及机架感知原理进行文件分配, 返回可用的 DataNode 的地址如: A, B, C
Hadoop 在设计时考虑到数据的安全与高效, 数据文件默认在 HDFS 上存放三份, 存储策略为本地一份, 同机架内其它某一节点上一份, 不同机架的某一节点上一份。

Client 请求 3 台 DataNode 中的一台 A 上传数据(本质上是一个 RPC 调用,建立 pipeline ), A 收到请求会继续调用 B, 然后 B 调用 C, 将整个 pipeline 建立完成, 后逐级返回 client

Client 开始往 A 上传第一个 block(先从磁盘读取数据放到一个本地内存缓存), 以 packet 为单位(默认64K), A 收到一个 packet 就会传给 B, B 传给 C. A 每传一个 packet 会放入一个应答队列等待应答

数据被分割成一个个 packet 数据包在 pipeline 上依次传输, 在 pipeline 反方向上, 逐个发送 ack(命令正确应答), 最终由 pipeline 中第一个 DataNode 节点 A 将 pipelineack 发送给 Client

当一个 block 传输完成之后, Client 再次请求 NameNode 上传第二个 block 到服务 1

HDFS 文件读取过程

Client向NameNode发起RPC请求,来确定请求文件block所在的位置;
NameNode会视情况返回文件的部分或者全部block列表,对于每个block,NameNode 都会返回含有该 block 副本的 DataNode 地址; 这些返回的 DN 地址,会按照集群拓扑结构得出 DataNode 与客户端的距离,然后进行排序,排序两个规则:网络拓扑结构中距离 Client 近的排靠前;心跳机制中超时汇报的 DN 状态为 STALE,这样的排靠后;
Client 选取排序靠前的 DataNode 来读取 block,如果客户端本身就是DataNode,那么将从本地直接获取数据(短路读取特性);
底层上本质是建立 Socket Stream(FSDataInputStream),重复的调用父类 DataInputStream 的 read 方法,直到这个块上的数据读取完毕;
当读完列表的 block 后,若文件读取还没有结束,客户端会继续向NameNode 获取下一批的 block 列表;
读取完一个 block 都会进行 checksum 验证,如果读取 DataNode 时出现错误,客户端会通知 NameNode,然后再从下一个拥有该 block 副本的DataNode 继续读。
read 方法是并行的读取 block 信息,不是一块一块的读取;NameNode 只是返回Client请求包含块的DataNode地址,并不是返回请求块的数据;
最终读取来所有的 block 会合并成一个完整的最终文件。

HDFS 的 API 操作

POM依赖

<repositories>
    <repository>
        <id>cloudera</id>
        <url>https://repository.cloudera.com/artifactory/cloudera-repos/</url>
    </repository>
</repositories>
<dependencies>
  <dependency>
      <groupId>jdk.tools</groupId>
      <artifactId>jdk.tools</artifactId>
      <version>1.8</version>
      <scope>system</scope>
      <systemPath>${JAVA_HOME}/lib/tools.jar</systemPath>
  </dependency>
  <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-common</artifactId>
      <version>3.0.0</version>
      <scope>provided</scope>
  </dependency>
  <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-hdfs</artifactId>
      <version>3.0.0</version>
  </dependency>
  <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-hdfs-client</artifactId>
      <version>3.0.0</version>
      <scope>provided</scope>
  </dependency>
  <dependency>
      <groupId>org.apache.hadoop</groupId>
      <artifactId>hadoop-client</artifactId>
      <version>3.0.0</version>
  </dependency>
  <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
  </dependency>
</dependencies>

获取 FileSystem 的几种方式

第一种方式

@Test
public void getFileSystem() throws URISyntaxException, IOException {
    Configuration configuration = new Configuration();
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), configuration);
    System.out.println(fileSystem.toString());
}

第二种方式

@Test
public void getFileSystem2() throws URISyntaxException, IOException {
    Configuration configuration = new Configuration();
    configuration.set("fs.defaultFS","hdfs://192.168.52.250:8020");
    FileSystem fileSystem = FileSystem.get(new URI("/"), configuration);
    System.out.println(fileSystem.toString());
}

第三种方式

@Test
public void getFileSystem3() throws URISyntaxException, IOException {
    Configuration configuration = new Configuration();
    FileSystem fileSystem = FileSystem.newInstance(new URI("hdfs://192.168.52.250:8020"), configuration);
    System.out.println(fileSystem.toString());
}

第四种方式

@Test
public void getFileSystem4() throws  Exception{
    Configuration configuration = new Configuration();
    configuration.set("fs.defaultFS","hdfs://192.168.52.250:8020");
    FileSystem fileSystem = FileSystem.newInstance(configuration);
    System.out.println(fileSystem.toString());
}

遍历 HDFS 中所有文件

方式一 递归遍历

@Test
public void listFile() throws Exception{
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.100:8020"), new Configuration());
    FileStatus[] fileStatuses = fileSystem.listStatus(new Path("/"));
    for (FileStatus fileStatus : fileStatuses) {
        if(fileStatus.isDirectory()){
            Path path = fileStatus.getPath();
            listAllFiles(fileSystem,path);
        }else{
            System.out.println("文件路径为"+fileStatus.getPath().toString());
​
        }
    }
}
public void listAllFiles(FileSystem fileSystem,Path path) throws  Exception{
    FileStatus[] fileStatuses = fileSystem.listStatus(path);
    for (FileStatus fileStatus : fileStatuses) {
        if(fileStatus.isDirectory()){
            listAllFiles(fileSystem,fileStatus.getPath());
        }else{
            Path path1 = fileStatus.getPath();
            System.out.println("文件路径为"+path1);
        }
    }
}

方式二使用 API 遍历

@Test
public void listMyFiles()throws Exception{
    //获取fileSystem类
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration());
    //获取RemoteIterator 得到所有的文件或者文件夹,第一个参数指定遍历的路径,第二个参数表示是否要递归遍历
    RemoteIterator<LocatedFileStatus> locatedFileStatusRemoteIterator = fileSystem.listFiles(new Path("/"), true);
    while (locatedFileStatusRemoteIterator.hasNext()){
        LocatedFileStatus next = locatedFileStatusRemoteIterator.next();
        System.out.println(next.getPath().toString());
    }
    fileSystem.close();
}

下载文件到本地

@Test
public void getFileToLocal()throws  Exception{
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration());
    FSDataInputStream open = fileSystem.open(new Path("/test/input/install.log"));
    FileOutputStream fileOutputStream = new FileOutputStream(new File("c:\\install.log"));
    IOUtils.copy(open,fileOutputStream );
    IOUtils.closeQuietly(open);
    IOUtils.closeQuietly(fileOutputStream);
    fileSystem.close();
}

HDFS 上创建文件夹

@Test
public void mkdirs() throws  Exception{
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration());
    boolean mkdirs = fileSystem.mkdirs(new Path("/hello/mydir/test"));
    fileSystem.close();
}

HDFS 文件上传

@Test
public void putData() throws  Exception{
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration());
    fileSystem.copyFromLocalFile(new Path("file:///c:\\install.log"),new Path("/hello/mydir/test"));
    fileSystem.close();
}

伪造用户

停止hdfs集群,在node01机器上执行以下命令
 
cd /export/servers/hadoop-3.1.1
sbin/stop-dfs.sh
修改node01机器上的hdfs-site.xml当中的配置文件
 
cd /export/servers/hadoop-3.1.1/etc/hadoop
vim hdfs-site.xml
 
<property>
    <name>dfs.permissions.enabled</name>
    <value>true</value>
</property>
修改完成之后配置文件发送到其他机器上面去
 
scp hdfs-site.xml node02:$PWD
scp hdfs-site.xml node03:$PWD
重启hdfs集群
 
cd /export/servers/hadoop-3.1.1
sbin/start-dfs.sh
随意上传一些文件到我们hadoop集群当中准备测试使用
 
cd /export/servers/hadoop-3.1.1/etc/hadoop
hdfs dfs -mkdir /config
hdfs dfs -put *.xml /config
hdfs dfs -chmod 600 /config/core-site.xml
使用代码准备下载文件
 
@Test
public void getConfig()throws  Exception{
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration(),"hadoop");
    fileSystem.copyToLocalFile(new Path("/config/core-site.xml"),new Path("file:///c:/core-site.xml"));
    fileSystem.close();
}

小文件合并

由于 Hadoop 擅长存储大文件,因为大文件的元数据信息比较少,如果 Hadoop 集群当中有大量的小文件,那么每个小文件都需要维护一份元数据信息,会大大的增加集群管理元数据的内存压力,所以在实际工作当中,如果有必要一定要将小文件合并成大文件进行一起处理。

在我们的 HDFS 的 Shell 命令模式下,可以通过命令行将很多的 hdfs 文件合并成一个大文件下载到本地。

cd /export/servers
hdfs dfs -getmerge /config/*.xml ./hello.xml

既然可以在下载的时候将这些小文件合并成一个大文件一起下载,那么肯定就可以在上传的时候将小文件合并到一个大文件里面去

@Test
public void mergeFile() throws  Exception{
    //获取分布式文件系统
    FileSystem fileSystem = FileSystem.get(new URI("hdfs://192.168.52.250:8020"), new Configuration(),"hadoop");
    FSDataOutputStream outputStream = fileSystem.create(new Path("/bigfile.xml"));
    //获取本地文件系统
    LocalFileSystem local = FileSystem.getLocal(new Configuration());
    //通过本地文件系统获取文件列表,为一个集合
    FileStatus[] fileStatuses = local.listStatus(new Path("file:///F:\\传智播客大数据离线阶段课程资料\\3、大数据离线第三天\\上传小文件合并"));
    for (FileStatus fileStatus : fileStatuses) {
        FSDataInputStream inputStream = local.open(fileStatus.getPath());
       IOUtils.copy(inputStream,outputStream);
        IOUtils.closeQuietly(inputStream);
    }
    IOUtils.closeQuietly(outputStream);
    local.close();
    fileSystem.close();
}

MapReduce

简介

MapReduce思想在生活中处处可见。或多或少都曾接触过这种思想。MapReduce的思想核心是“分而治之”,适用于大量复杂的任务处理场景(大规模数据处理场景)。

Map负责“分”,即把复杂的任务分解为若干个“简单的任务”来并行处理。可以进行拆分的前提是这些小任务可以并行计算,彼此间几乎没有依赖关系。
Reduce负责“合”,即对map阶段的结果进行全局汇总。
MapReduce运行在yarn集群(ResourceManager、NodeManager)

这两个阶段合起来正是MapReduce思想的体现。

还有一个比较形象的语言解释MapReduce:
我们要数图书馆中的所有书。你数1号书架,我数2号书架。这就是“Map”。我们人越多, 数书就更快。
现在我们到一起,把所有人的统计数加在一起。这就是“Reduce”。

设计构思和框架结构:
MapReduce是一个分布式运算程序的编程框架,核心功能是将用户编写的业务逻辑代码 和自带默认组件整合成一个完整的分布式运算程序,并发运行在Hadoop集群上。
既然是做计算的框架,那么表现形式就是有个输入(input),MapReduce操作这个输 入(input),通过本身定义好的计算模型,得到一个输出(output)。

编程规范

MapReduce 的开发一共有八个步骤, 其中 Map 阶段分为 2 个步骤,Shuffle 阶段 4 个步骤,Reduce 阶段分为 2 个步骤。

Map 阶段 2 个步骤
设置 InputFormat 类, 将数据切分为 Key-Value(K1和V1) 对, 输入到第二步 。
自定义 Map 逻辑, 将第一步的结果转换成另外的 Key-Value(K2和V2)对, 输出结果。

Shuffle 阶段 4 个步骤
对输出的 Key-Value 对进行分区 。
对不同分区的数据按照相同的 Key 排序。
(可选) 对分组过的数据初步规约, 降低数据的网络拷贝。
对数据进行分组, 相同 Key 的 Value 放入一个集合中。

Reduce 阶段 2 个步骤
对多个 Map 任务的结果进行排序以及合并, 编写 Reduce 函数实现自己的逻辑, 对输 入的 Key-Value 进行处理, 转为新的 Key-Value(K3和V3)输出。
设置 OutputFormat 处理并保存 Reduce 输出的 Key-Value 数据

WordCount

需求: 在一堆给定的文本文件中统计输出每一个单词出现的总次数。
1、数据格式准备
创建一个新的文件

  cd /export/servers   
  vim wordcount.txt

向其中放入以下内容并保存

  hello,world,hadoop
  hive,sqoop,flume,hello
  kitty,tom,jerry,world
  hadoop

上传到 HDFS

hdfs dfs ‐mkdir /wordcount/ 
hdfs dfs ‐put wordcount.txt /wordcount/

2、Mapper

 public class WordCountMapper extends  Mapper<LongWritable,Text,Text,LongWritable> {     
     @Override     
     public void map(LongWritable key, Text value, Context context) throws 
     IOException, InterruptedException {         
	     String line = value.toString();         
	     String[] split = line.split(",");         
	     for (String word : split) {             
	    	 context.write(new Text(word),new LongWritable(1));         
	     }
     } 
}

3、Reducer

public class WordCountReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
	/**
	 * * 自定义我们的reduce逻辑 * 所有的key都是我们的单词,所有的values都是我们单词出现的次数 
	 * * @param key * @param
	 * values * @param context * @throws IOException * @throws InterruptedException
	 */
	@Override
	protected void reduce(Text key, Iterable<LongWritable> values, Context context)
			throws IOException, InterruptedException {
		long count = 0;
		for (LongWritable value : values) {
			count += value.get();
		}
		context.write(key, new LongWritable(count));
	}
}

4、定义主类, 描述 Job 并提交 Job

public class JobMain extends Configured implements Tool {
	 @Override 
	 public int run(String[] args) throws Exception {         
		Job job = Job.getInstance(super.getConf(),  JobMain.class.getSimpleName());         
		//打包到集群上面运行时候,必须要添加以下配置,指定程序的main函数         
		job.setJarByClass(JobMain.class);         
		//第一步:读取输入文件解析成key,value对         
		job.setInputFormatClass(TextInputFormat.class);         
		TextInputFormat.addInputPath(job,new  Path("hdfs://192.168.52.250:8020/wordcount"));
		        
		//第二步:设置我们的mapper类         
		job.setMapperClass(WordCountMapper.class);         
		//设置我们map阶段完成之后的输出类型         
		job.setMapOutputKeyClass(Text.class);         
		job.setMapOutputValueClass(LongWritable.class);         
		//第三步,第四步,第五步,第六步,省略         
		//第七步:设置我们的reduce类         
		job.setReducerClass(WordCountReducer.class);         
		//设置我们reduce阶段完成之后的输出类型         
		job.setOutputKeyClass(Text.class);         
		job.setOutputValueClass(LongWritable.class);         
		//第八步:设置输出类以及输出路径         
		job.setOutputFormatClass(TextOutputFormat.class);         
		TextOutputFormat.setOutputPath(job,new  Path("hdfs://192.168.52.250:8020/wordcount_out"));         
		boolean b = job.waitForCompletion(true);         
		return b?0:1;     
	}
	 /**
	 *        程序main函数的入口类        
	 * @param args        
	 * @throws Exception      
	 */
	public static void main(String[] args) throws Exception {         
		Configuration configuration = new Configuration();         
		Tool tool  =  new JobMain();
		int run = ToolRunner.run(configuration, tool, args); 
		System.exit(run);     
	} 
}

运行模式

集群运行模式

  1. 将 MapReduce 程序提交给 Yarn 集群, 分发到很多的节点上并发执行
  2. 处理的数据和输出结果应该位于 HDFS 文件系统
  3. 提交集群的实现步骤: 将程序打成JAR包,然后在集群的任意一个节点上用hadoop命令启动

yarn jar hadoop_hdfs_operate‐1.0‐SNAPSHOT.jar
org.plxc.hdfs.demo.JobMain

分区

在 MapReduce 中, 通过我们指定分区, 会将同一个分区的数据发送到同一个 Reduce 当中进行处理。
例如: 为了数据的统计, 可以把一批类似的数据发送到同一个 Reduce 当中, 在同一个 Reduce 当中统计相同类型的数据, 就可以实现类似的数据分区和统计等。
其实就是相同类型的数据, 有共性的数据, 送到一起去处理。

Reduce 当中默认的分区只有一个:
自定义 Partitioner
主要的逻辑就在这里, 这也是这个案例的意义, 通过 Partitioner 将数据分发给不同的 Reducer

//这里的输入类型与我们map阶段的输出类型相同 
public class MyPartitioner extends Partitioner<Text,NullWritable>{ 

	/**      
	* 返回值表示我们的数据要去到哪个分区      
	* 返回值只是一个分区的标记,标记所有相同的数据去到指定的分区      
	*/ 
	 @Override     
	public int getPartition(Text text, NullWritable nullWritable, int i)  {

		String result = text.toString().split("\t")[5];
		System.out.println(result);
		if(Integer.parseInt(result) > 15){
		    return 1; 
		}else{       
		     return 0;
		}
	} 
}


//Main入口
public class PartitionMain  extends Configured implements Tool {
	public static void main(String[] args) throws  Exception{
		int run = ToolRunner.run(new Configuration(), new  PartitionMain(), args);
		System.exit(run);
	}
	@Override
	public int run(String[] args) throws Exception {
		Job job = Job.getInstance(super.getConf(),PartitionMain.class.getSimpleName());
		job.setJarByClass(PartitionMain.class); 
		job.setInputFormatClass(TextInputFormat.class);
		job.setOutputFormatClass(TextOutputFormat.class);
		TextInputFormat.addInputPath(job,new  Path("hdfs://192.168.52.250:8020/partitioner"));
		TextOutputFormat.setOutputPath(job,new  Path("hdfs://192.168.52.250:8020/outpartition"));
		job.setMapperClass(MyMapper.class);
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(NullWritable.class);
		job.setOutputKeyClass(Text.class);
		job.setMapOutputValueClass(NullWritable.class); 
		job.setReducerClass(MyReducer.class);
		/**          
		* 设置我们的分区类,以及我们的reducetask的个数,注意reduceTask的个数 一定要与我们的          
		* 分区数保持一致          
		*/ 
		job.setPartitionerClass(MyPartitioner.class);
		job.setNumReduceTasks(2);
		boolean b = job.waitForCompletion(true);
		return b?0:1;
	}
}

序列化和排序

Java 的序列化 (Serializable) 是一个重量级序列化框架, 一个对象被序列化后, 会附带 很多额外的信息 (各种校验信息, header, 继承体系等), 不便于在网络中高效传输. 所 以, Hadoop 自己开发了一套序列化机制(Writable), 精简高效. 不用像 Java 对象类一 样传输多层的父子关系, 需要哪个属性就传输哪个属性值, 大大的减少网络传输的开销

Writable 是 Hadoop 的序列化格式, Hadoop 定义了这样一个 Writable 接口. 一个类 要支持可序列化只需实现这个接口即可

另外 Writable 有一个子接口是 WritableComparable, WritableComparable 是既可 实现序列化, 也可以对key进行比较, 我们这里可以通过自定义 Key 实现 WritableComparable 来实现我们的排序功能

1、数据格式:
a 1
a 9
b 3
a 7
b 8
b 10
a 5

2、要求:
第一列按照字典顺序进行排列
第一列相同的时候, 第二列按照升序进行排列

3、解决思路:
将Map 端输出的 <key,value> 中的 key 和 value 组合成一个新的 key (newKey), value值不变。
这里就变成 <(key,value),value> , 在针对 newKey 排序的时候, 如果 key 相同, 就再 对value进行排序。

4、实现

①自定义类型和比较器

public class PairWritable implements WritableComparable<PairWritable> {     
    // 组合key,第一部分是我们第一列,第二部分是我们第二列
    private String first;
    private int second;
    public PairWritable() {
    } 
    
    public PairWritable(String first, int second) {
  	 	 this.set(first, second);
    } 
    /**      
    * 方便设置字段      
    */
    public void set(String first, int second) {
	    this.first = first;
	    this.second = second;
    } 
    /**      
    * 反序列化      
    */     
    @Override     
    public void readFields(DataInput input) throws IOException {
	    this.first = input.readUTF();         
	    this.second = input.readInt();     
    }     
    /**      
    * 序列化      
    */     
    @Override     
    public void write(DataOutput output) throws IOException {
	    output.writeUTF(first);
	    output.writeInt(second);
    }
    /*      
    * 重写比较器      
    */     
    public int compareTo(PairWritable o) {
	    //每次比较都是调用该方法的对象与传递的参数进行比较,说白了就是第一行与第 二行比较完了之后的结果与第三行比较,         
	    //得出来的结果再去与第四行比较,依次类推
	    
	    System.out.println(o.toString());
	    System.out.println(this.toString());
	    int comp = this.first.compareTo(o.first);         
	    if (comp != 0) {             
	   		 return comp;         
	    } else { 
		    // 若第一个字段相等,则比较第二个字段             
		    return Integer.valueOf(this.second).compareTo(                     
		    Integer.valueOf(o.getSecond()));         
	    }     
    }
        
    public int getSecond() {         
    	return second;     
    }
        
    public void setSecond(int second) {         
    	this.second = second;     
    }     
    public String getFirst() {         
    	return first;     
    }     
    public void setFirst(String first) {         
	   	 this.first = first;     
    }     
    @Override     
    public String toString() {         
	    return "PairWritable{" +                 
	    "first='" + first + '\'' +                
	    ", second=" + second +                 
    	'}';     
    } 
}

②Mapper

public class SortMapper extends  Mapper<LongWritable,Text,PairWritable,IntWritable> {
    private PairWritable mapOutKey = new PairWritable(); 
    private IntWritable mapOutValue = new IntWritable();
    @Override     
    public  void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {  
	    String lineValue = value.toString();   
	    String[] strs = lineValue.split("\t"); 
	    //设置组合key和value ==> <(key,value),value> 
	    mapOutKey.set(strs[0], Integer.valueOf(strs[1])); 
	    mapOutValue.set(Integer.valueOf(strs[1])); 
	    context.write(mapOutKey, mapOutValue); 
    } 
}

③Reducer

public class SortReducer extends  Reducer<PairWritable,IntWritable,Text,IntWritable> {
    private Text outPutKey = new Text();
    @Override
    public void reduce(PairWritable key, Iterable<IntWritable> values,  Context context) throws IOException, InterruptedException { 
	    //迭代输出        
	    for(IntWritable value : values) {
		    outPutKey.set(key.getFirst());
		    context.write(outPutKey, value); 
	    }
    } 
}

④Main入口

public class SecondarySort  extends Configured implements Tool { 
     @Override     
    public int run(String[] args) throws Exception {         
	    Configuration conf = super.getConf();         
	    conf.set("mapreduce.framework.name","local");         
	    Job job = Job.getInstance(conf,  SecondarySort.class.getSimpleName());         
	    job.setJarByClass(SecondarySort.class);
	    job.setInputFormatClass(TextInputFormat.class);         
	    TextInputFormat.addInputPath(job,new Path("file:///L:\\大数据离线 阶段备课教案以及资料文档——by老王\\4、大数据离线第四天\\排序\\input"));         
	    TextOutputFormat.setOutputPath(job,new Path("file:///L:\\大数据离 线阶段备课教案以及资料文档——by老王\\4、大数据离线第四天\\排序\\output"));         
	    job.setMapperClass(SortMapper.class);         
	    job.setMapOutputKeyClass(PairWritable.class);         
	    job.setMapOutputValueClass(IntWritable.class);         
	    job.setReducerClass(SortReducer.class);         
	    job.setOutputKeyClass(Text.class);         
	    job.setOutputValueClass(IntWritable.class);         
	    boolean b = job.waitForCompletion(true);         
	    return b?0:1;     
    }
    public static void main(String[] args) throws Exception {         
	    Configuration entries = new Configuration();         
	    ToolRunner.run(entries,new SecondarySort(),args);     
    } 
}

计数器

计数器是收集作业统计信息的有效手段之一,用于质量控制或应用级统计。计数器还可辅 助诊断系统故障。
1、Hadoop内置计数器列表
MapReduce任务计数器 org.apache.hadoop.mapreduce.TaskCounter
文件系统计数器 org.apache.hadoop.mapreduce.FileSystemCounter
FileInputFormat计数器 org.apache.hadoop.mapreduce.lib.input.FileInputFormatCounter
FileOutputFormat计数器 org.apache.hadoop.mapreduce.lib.output.FileOutputFormatCounter
作业计数器 org.apache.hadoop.mapreduce.JobCounter

2、自定义计数器
需求:以上面排序以及序列化为案例,统计map接收到的数据记录条数

方式一:

//通过context上下文对象可以获取我们的计数器,进行记录 通过context上下文对象,在map端使用计数器进行统计
public class SortMapper extends Mapper<LongWritable,Text,PairWritable,IntWritable> {
    private PairWritable mapOutKey = new PairWritable();     
    private IntWritable mapOutValue = new IntWritable();
    @Override     
    public  void map(LongWritable key, Text value, Context context) throws  IOException, InterruptedException { 
	    //自定义我们的计数器,这里实现了统计map数据数据的条数         
	    Counter counter = context.getCounter("MR_COUNT", "MapRecordCounter");         
	    counter.increment(1L);
	    String lineValue = value.toString();         
	    String[] strs = lineValue.split("\t");
	    //设置组合key和value ==> <(key,value),value>         
	    mapOutKey.set(strs[0], Integer.valueOf(strs[1]));         
	    mapOutValue.set(Integer.valueOf(strs[1]));         
	    context.write(mapOutKey, mapOutValue);     
    }
}

方式二:

//通过enum枚举类型来定义计数器 统计reduce端数据的输入的key有多少个,对应的value有多少个
public class SortReducer extends Reducer<PairWritable,IntWritable,Text,IntWritable> {
    private Text outPutKey = new Text();    
    public static enum Counter{         
    	REDUCE_INPUT_RECORDS, REDUCE_INPUT_VAL_NUMS,     
    }     
    @Override     
    public void reduce(PairWritable key, Iterable<IntWritable> values,  Context context) throws IOException, InterruptedException {         
	    context.getCounter(Counter.REDUCE_INPUT_RECORDS).increment(1L);         
	    //迭代输出        
	    for(IntWritable value : values) {              
	    context.getCounter(Counter.REDUCE_INPUT_VAL_NUMS).increment(1L);             
	    outPutKey.set(key.getFirst());             
	    context.write(outPutKey, value);         
	    }     
    } 
}

规约Combiner

每一个 map 都可能会产生大量的本地输出,Combiner 的作用就是对 map 端的输出先做 一次合并,以减少在 map 和 reduce 节点之间的数据传输量,以提高网络IO 性能,是 MapReduce 的一种优化手段之一。

步骤:

  1. 自定义一个 combiner 继承 Reducer,重写 reduce 方法
  2. 在 job 中设置 job.setCombinerClass(CustomCombiner.class)
    combiner 能够应用的前提是不能影响最终的业务逻辑,而且,combiner 的输出 kv 应该 跟 reducer 的输入 kv 类型要对应起来 。

流量统计

统计求和

统计每个手机号的上行流量总和,下行流量总和,上行总流量之和,下行总流量之和 分析:以手机号码作为key值,上行流量,下行流量,上行总流量,下行总流量四个字段作 为value值,然后以这个key,和value作为map阶段的输出,reduce阶段的输入。

1、自定义map的输出value对象FlowBean

public class FlowBean implements Writable { 
    private Integer upFlow; 
    private Integer  downFlow; 
    private Integer upCountFlow; 
    private Integer downCountFlow; 
    @Override 
    public void write(DataOutput out) throws IOException { 
	    out.writeInt(upFlow); 
	    out.writeInt(downFlow);    
	    out.writeInt(upCountFlow); 
	    out.writeInt(downCountFlow); 
    } 
    @Override 
    public void readFields(DataInput in) throws IOException { 
	    this.upFlow = in.readInt(); 
	    this.downFlow = in.readInt(); 
	    this.upCountFlow = in.readInt(); 
	    this.downCountFlow = in.readInt(); 
    } 
    public FlowBean() {  
    } 
    public FlowBean(Integer upFlow, Integer downFlow, Integer upCountFlow,  Integer downCountFlow) { 
	    this.upFlow = upFlow; 
	    this.downFlow = downFlow; 
	    this.upCountFlow = upCountFlow; 
	    this.downCountFlow = downCountFlow; 
    } 
    public Integer getUpFlow() { 
   	 return upFlow; 
    } 
    public void setUpFlow(Integer upFlow) { 
	    this.upFlow = upFlow; 
    } 
    public Integer getDownFlow() { 
   		 return downFlow; 
    } 
    public void setDownFlow(Integer downFlow) { 
    	this.downFlow = downFlow; 
    }
    public Integer getUpCountFlow() { 
    	return upCountFlow; 
    } 
    public void setUpCountFlow(Integer upCountFlow) { 
    	this.upCountFlow = upCountFlow; 
    } 
    public Integer getDownCountFlow() { 
    	return downCountFlow; 
    } 
    public void setDownCountFlow(Integer downCountFlow) { 
    	this.downCountFlow = downCountFlow; 
    } 
    @Override 
    public String toString() { 
	    return "FlowBean{" + "upFlow=" + upFlow + ", downFlow=" + downFlow + ", upCountFlow=" + upCountFlow + ", downCountFlow=" + downCountFlow + '}'; 
    } 
}

2、定义FlowMapper类

public class FlowCountMapper extends Mapper<LongWritable,Text,Text,FlowBean> {     
    @Override     
    protected void map(LongWritable key, Text value, Context context)  throws IOException, InterruptedException {        
	    //1:拆分手机号 
	    String[] split = value.toString().split("\t"); 
	    String phoneNum = split[1]; 
	    //2:获取四个流量字段 
	    FlowBean flowBean = new FlowBean(); 
	    flowBean.setUpFlow(Integer.parseInt(split[6])); 
	    flowBean.setDownFlow(Integer.parseInt(split[7])); 
	    flowBean.setUpCountFlow(Integer.parseInt(split[8])); 
	    flowBean.setDownCountFlow(Integer.parseInt(split[9]));
	    //3:将k2和v2写入上下文中 
	    context.write(new Text(phoneNum), flowBean);     
    } 
}

3、定义FlowReducer类

public class FlowCountReducer extends Reducer<Text,FlowBean,Text,FlowBean>  {     
    @Override     
    protected void reduce(Text key, Iterable<FlowBean> values, Context  context) throws IOException, InterruptedException {        
	    //封装新的FlowBean         
	    FlowBean flowBean = new FlowBean();         
	    Integer upFlow = 0;         
	    Integer  downFlow = 0;         
	    Integer upCountFlow = 0;         
	    Integer downCountFlow = 0;         
	    for (FlowBean value : values) {             
		    upFlow  += value.getUpFlow();             
		    downFlow += value.getDownFlow();             
		    upCountFlow += value.getUpCountFlow();             
		    downCountFlow += value.getDownCountFlow();         
	    }         
	    flowBean.setUpFlow(upFlow);         
	    flowBean.setDownFlow(downFlow);         
	    flowBean.setUpCountFlow(upCountFlow);         
	    flowBean.setDownCountFlow(downCountFlow);         
	    //将K3和V3写入上下文中         
	    context.write(key, flowBean);     
    }
} 

4、程序main函数入口FlowMain

public class JobMain extends Configured  implements Tool {  
    @Override     
    public int run(String[] strings) throws Exception { 
	    //创建一个任务对象 
	    Job job = Job.getInstance(super.getConf(), "mapreduce_flowcount");
	    //打包放在集群运行时,需要做一个配置 
	    job.setJarByClass(JobMain.class); 
	    //第一步:设置读取文件的类: K1 和V1 
	    job.setInputFormatClass(TextInputFormat.class); 
	    TextInputFormat.addInputPath(job, new Path("hdfs://node01:8020/input/flowcount"));
	    //第二步:设置Mapper类 
	    job.setMapperClass(FlowCountMapper.class); 
	    //设置Map阶段的输出类型: k2 和V2的类型 
	    job.setMapOutputKeyClass(Text.class); 
	    job.setMapOutputValueClass(FlowBean.class);
	    //第三,四,五,六步采用默认方式(分区,排序,规约,分组)
	    //第七步 :设置文的Reducer类 
	    job.setReducerClass(FlowCountReducer.class); 
	    //设置Reduce阶段的输出类型 
	    job.setOutputKeyClass(Text.class); 
	    job.setOutputValueClass(FlowBean.class);
	    //设置Reduce的个数
	    //第八步:设置输出类 
	    job.setOutputFormatClass(TextOutputFormat.class); 
	    //设置输出的路径 
	    TextOutputFormat.setOutputPath(job, new  Path("hdfs://node01:8020/out/flowcount_out"));
	    boolean b = job.waitForCompletion(true); 
	    return b?0:1;
	} 
	public static void main(String[] args) throws Exception { 
	    Configuration configuration = new Configuration();
	    //启动一个任务 
	    int run = ToolRunner.run(configuration, new JobMain(), args); 
	    System.exit(run);     
    }
}

上行流量倒序排序(递减排序)

分析,以需求一的输出数据作为排序的输入数据,自定义FlowBean,以FlowBean为map输 出的key,以手机号作为Map输出的value,因为MapReduce程序会对Map阶段输出的key 进行排序。

1、定义FlowBean实现WritableComparable实现比较排序

public class FlowBean implements WritableComparable {
    private Integer upFlow;  
    private Integer  downFlow;  
    private Integer upCountFlow;  
    private Integer downCountFlow;  
    public FlowBean() {  }  
    public FlowBean(Integer upFlow, Integer downFlow, Integer upCountFlow,  Integer downCountFlow) {      
	    this.upFlow = upFlow;      
	    this.downFlow = downFlow;      
	    this.upCountFlow = upCountFlow;      
	    this.downCountFlow = downCountFlow;  
    }  
    @Override  
    public void write(DataOutput out) throws IOException {      
	    out.writeInt(upFlow);      
	    out.writeInt(downFlow);      
	    out.writeInt(upCountFlow);      
	    out.writeInt(downCountFlow);  
    }  
    @Override  
    public void readFields(DataInput in) throws IOException {      
	    upFlow = in.readInt();      
	    downFlow = in.readInt();      
	    upCountFlow = in.readInt();      
	    downCountFlow = in.readInt();  
    }  
    public Integer getUpFlow() {      
  	 	 return upFlow;  
    }  
    public void setUpFlow(Integer upFlow) {      
  	  	this.upFlow = upFlow;  
    }  
    public Integer getDownFlow() {      
	    return downFlow;  
    }  
    public void setDownFlow(Integer downFlow) {      
    	this.downFlow = downFlow;  
    }  
    public Integer getUpCountFlow() {      
    	return upCountFlow;
    }  
    public void setUpCountFlow(Integer upCountFlow) {      
    	this.upCountFlow = upCountFlow;  
    }  
    public Integer getDownCountFlow() {      
    	return downCountFlow;  
    }  
    public void setDownCountFlow(Integer downCountFlow) {     
    	this.downCountFlow = downCountFlow;  
    }  
    @Override  
    public String toString() {      
    	return upFlow+"\t"+downFlow+"\t"+upCountFlow+"\t"+downCountFlow;  
    }  
    @Override  
    public int compareTo(FlowBean o) {      
    	return this.upCountFlow > o.upCountFlow ?‐1:1;  
    }
}

2、定义FlowMapper

public class FlowCountSortMapper extends  Mapper<LongWritable,Text,FlowBean,Text> {     
    @Override     
    protected void map(LongWritable key, Text value, Context context)  throws IOException, InterruptedException { 
	    FlowBean flowBean = new FlowBean(); 
	    String[] split = value.toString().split("\t");
	    //获取手机号,作为V2 
	    String phoneNum = split[0]; 
	    //获取其他流量字段,封装flowBean,作为K2 
	    flowBean.setUpFlow(Integer.parseInt(split[1])); 
	    flowBean.setDownFlow(Integer.parseInt(split[2])); 
	    flowBean.setUpCountFlow(Integer.parseInt(split[3])); 
	    flowBean.setDownCountFlow(Integer.parseInt(split[4]));
	    //将K2和V2写入上下文中 
	    context.write(flowBean, new Text(phoneNum));
    } 
}

3、定义FlowReducer

public class FlowCountSortReducer extends  Reducer<FlowBean,Text,Text,FlowBean> {     
    @Override     
    protected void reduce(FlowBean key, Iterable<Text> values, Context  context) throws IOException, InterruptedException {         
	    for (Text value : values) { 
	   		 context.write(value, key);         
	    }     
    } 
}

4、Main

 public class JobMain extends Configured  implements Tool { 
    @Override     
    public int run(String[] strings) throws Exception { 
	    //创建一个任务对象 
	    Job job = Job.getInstance(super.getConf(),  "mapreduce_flowcountsort");
	    //打包放在集群运行时,需要做一个配置 
	    job.setJarByClass(JobMain.class); 
	    //第一步:设置读取文件的类: K1 和V1 
	    job.setInputFormatClass(TextInputFormat.class); 
	    TextInputFormat.addInputPath(job, new Path("hdfs://node01:8020/out/flowcount_out"));
	    //第二步:设置Mapper类 
	    job.setMapperClass(FlowCountSortMapper.class); 
	    //设置Map阶段的输出类型: k2 和V2的类型 
	    job.setMapOutputKeyClass(FlowBean.class); 
	    job.setMapOutputValueClass(Text.class);
	    //第三,四,五,六步采用默认方式(分区,排序,规约,分组)
	    
	    //第七步 :设置文的Reducer类 
	    job.setReducerClass(FlowCountSortReducer.class); 
	    //设置Reduce阶段的输出类型 
	    job.setOutputKeyClass(Text.class); 
	    job.setOutputValueClass(FlowBean.class);
	    //设置Reduce的个数
	    //第八步:设置输出类 
	    job.setOutputFormatClass(TextOutputFormat.class); 
	    //设置输出的路径 
	    TextOutputFormat.setOutputPath(job, new  Path("hdfs://node01:8020/out/flowcountsort_out"));
	    boolean b = job.waitForCompletion(true); 
	    return b?0:1;
    }     
    public static void main(String[] args) throws Exception { 
	    Configuration configuration = new Configuration();
	    //启动一个任务 
	    int run = ToolRunner.run(configuration, new JobMain(), args); 
	    System.exit(run);     
    }
}

手机号码分区

在需求一的基础上,继续完善,将不同的手机号分到不同的数据文件的当中去,需要自定 义分区来实现,这里我们自定义来模拟分区,将以下数字开头的手机号进行分开。

135 开头数据到一个分区文件
136 开头数据到一个分区文件
137 开头数据到一个分区文件
其他分区

1、自定义分区

public class FlowPartition extends Partitioner<Text,FlowBean> {     
    @Override     
    public int getPartition(Text text, FlowBean flowBean, int i) {         
	    String line = text.toString();         
	    if (line.startsWith("135")){ 
	   	 	return 0;         
	    }else if(line.startsWith("136")){ 
	    	return 1;         
	    }else if(line.startsWith("137")){ 
	    	return 2;         
	    }else{ 
	    	return 3;         
	    }     
    } 
}

2、作业运行添加分区设置
job.setPartitionerClass(FlowPartition.class);

3、修改输入输出路径, 并放入集群运行

TextInputFormat.addInputPath(job,new  Path("hdfs://node01:8020/partition_flow/"));    
TextOutputFormat.setOutputPath(job,new  Path("hdfs://node01:8020/partition_out"));

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. Mapper 逻辑结束之后, 将 Mapper 的每条结果通过 context.write 进行collect数据 收集. 在 collect 中, 会先对其进行分区处理,默认使用 HashPartitioner。
    MapReduce 提供 Partitioner 接口, 它的作用就是根据 Key 或 Value 及 Reducer 的数量来决定当前的这对输出数据最终应该交由哪个 Reduce task 处理, 默认对 Key Hash 后再以 Reducer 数量取模. 默认的取模方式只是为了 平均 Reducer 的处理能力, 如果用户自己对 Partitioner 有需求, 可以订制并设 置到 Job 上。
  5. 接下来, 会将数据写入内存, 内存中这片区域叫做环形缓冲区, 缓冲区的作用是批量收集 Mapper 结果, 减少磁盘 IO 的影响. 我们的 Key/Value 对以及 Partition 的结果都会 被写入缓冲区. 当然, 写入之前,Key 与 Value 值都会被序列化成字节数组 。
  6. 当溢写线程启动后, 需要对这 80MB 空间内的 Key 做排序 (Sort). 排序是 MapReduce 模型默认的行为, 这里的排序也是对序列化的字节做的排序。
  7. 合并溢写文件, 每次溢写会在磁盘上生成一个临时文件 (写之前判断是否有 Combiner), 如果 Mapper 的输出结果真的很大, 有多次这样的溢写发生, 磁盘上相应的就会有多个 临时文件存在. 当整个数据处理结束之后开始对磁盘中的临时文件进行 Merge 合并, 因 为最终的文件只有一个, 写入磁盘, 并且为这个文件提供了一个索引文件, 以记录每个 reduce对应数据的偏移量。

配置:
mapreduce.task.io.sort.mb 100 设置 环型 缓冲 区的 内存 值大 小
mapreduce.map.sort.spill.percent 0.8 设置 溢写 的比 例
mapreduce.cluster.local.dir ${hadoop.tmp.dir}/mapred/local 溢写 数据 目录
mapreduce.task.io.sort.factor 10 设置 一次 合并 多少 个溢 写文 件

ReduceTask 工作机制和 ReduceTask 并行度

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

详细步骤:

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

Shuffle 过程

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

shuffle 是 Mapreduce 的核心,它分布在 Mapreduce 的 map 阶段和 reduce 阶段。一般 把从 Map 产生输出开始到 Reduce 取得数据作为输入之前的过程称作 shuffle。

  1. Collect阶段 :将 MapTask 的结果输出到默认大小为 100M 的环形缓冲区,保存的是 key/value,Partition 分区信息等。
  2. Spill阶段 :当内存中的数据量达到一定的阀值的时候,就会将数据写入本地磁盘,在 将数据写入磁盘之前需要对数据进行一次排序的操作,如果配置了 combiner,还会将 有相同分区号和 key 的数据进行排序。
  3. Merge阶段 :把所有溢出的临时文件进行一次合并操作,以确保一个 MapTask 最终只 产生一个中间数据文件。
  4. Copy阶段 :ReduceTask 启动 Fetcher 线程到已经完成 MapTask 的节点上复制一份 属于自己的数据,这些数据默认会保存在内存的缓冲区中,当内存的缓冲区达到一定 的阀值的时候,就会将数据写到磁盘之上。
  5. Merge阶段 :在 ReduceTask 远程复制数据的同时,会在后台开启两个线程对内存到 本地的数据文件进行合并操作。
  6. Sort阶段 :在对数据进行合并的同时,会进行排序操作,由于 MapTask 阶段已经对 数据进行了局部的排序,ReduceTask 只需保证 Copy 的数据的最终整体有效性即可。 Shuffle 中的缓冲区大小会影响到 mapreduce 程序的执行效率,原则上说,缓冲区越 大,磁盘io的次数越少,执行速度就越快缓冲区的大小可以通过参数调整, 参数:mapreduce.task.io.sort.mb 默认100M

Reduce 端实现 JOIN

1、 需求
假如数据量巨大,两表的数据是以文件的形式存储在 HDFS 中, 需要用 MapReduce 程 序来实现以下 SQL 查询运算。
select a.id,a.date,b.name,b.category_id,b.price from t_order a left join t_product b on a.pid = b.id

在这里插入图片描述
实现机制
通过将关联的条件作为map输出的key,将两表满足join条件的数据并携带数据所来源的文 件信息,发往同一个reduce task,在reduce中进行数据的串联 。

2、定义 Mapper

public class ReduceJoinMapper extends Mapper<LongWritable,Text,Text,Text> {
    @Override     
    protected void map(LongWritable key, Text value, Context context)  throws IOException, InterruptedException {        
	    //首先判断数据来自哪个文件 
	    FileSplit fileSplit = (FileSplit) context.getInputSplit(); 
	    String fileName = fileSplit.getPath().getName();
	    if(fileName.equals("orders.txt")){ 
		    //获取pid     
		    String[] split = value.toString().split(",");     
		    context.write(new Text(split[2]), value); 
	    }else{     
		    //获取pid     
		    String[] split = value.toString().split(",");     
		    context.write(new Text(split[0]), value); 
	    }     
    }
}

3、定义 Reducer

public class ReduceJoinReducer extends Reducer<Text,Text,Text,Text> {
    @Override     
    protected void reduce(Text key, Iterable<Text> values, Context context)  throws IOException, InterruptedException {
	    String first = ""; 
	    String second = ""; 
	    for (Text value : values) {     
		    if(value.toString().startsWith("p")){ 
		    	first = value.toString();     
		    }else{ 
		    	second  = value.toString();     
		    } 
	    }
	    if(first.equals("")){     
	    	context.write(key, new Text("NULL"+"\t"+second)); 
	    }else{     
	    	context.write(key, new Text(first+"\t"+second)); 
	    }
    }
}

4、Main

public class JobMain extends Configured implements Tool {
    @Override     
    public int run(String[] strings) throws Exception { 
	    //创建一个任务对象 
	    Job job = Job.getInstance(super.getConf(),  "mapreduce_reduce_join");
	    //打包放在集群运行时,需要做一个配置 
	    job.setJarByClass(JobMain.class); 
	    //第一步:设置读取文件的类: K1 和V1 
	    job.setInputFormatClass(TextInputFormat.class); 
	    TextInputFormat.addInputPath(job, new Path("hdfs://node01:8020/input/reduce_join"));
	    //第二步:设置Mapper类 
	    job.setMapperClass(ReduceJoinMapper.class); 
	    //设置Map阶段的输出类型: k2 和V2的类型 
	    job.setMapOutputKeyClass(Text.class); 
	    job.setMapOutputValueClass(Text.class);
	    //第三,四,五,六步采用默认方式(分区,排序,规约,分组)
	    
	    //第七步 :设置文的Reducer类 
	    job.setReducerClass(ReduceJoinReducer.class); 
	    //设置Reduce阶段的输出类型 
	    job.setOutputKeyClass(Text.class); 
	    job.setOutputValueClass(Text.class);
	    //第八步:设置输出类 
	    job.setOutputFormatClass(TextOutputFormat.class); 
	    //设置输出的路径 
	    TextOutputFormat.setOutputPath(job, new Path("hdfs://node01:8020/out/reduce_join_out"));
	    boolean b = job.waitForCompletion(true); 
	    return b?0:1;
    }
    public static void main(String[] args) throws Exception {
	    Configuration configuration = new Configuration(); 
	    //启动一个任务 
	    int run = ToolRunner.run(configuration, new JobMain(), args); 
	    System.exit(run);     
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值