Hadoop

文章目录

Hadoop

一、引言

什么是大数据 (大数据的概述)

大数据是指需要新处理模式才能具有更强的决策力、洞察发现力和流程优化能力的海量高增长率多样化的信息资产

大数据面临的问题

  1. 数据的存储 : 单节点服务器存储能力有限,无法解决海量(TB + 级别)数据的存储
  2. 数据的分析 : 单节点服务器的计算能力有限,无法在合理的时间内对数据进行成本运算

大数据的特点

# 大数据的4V特性
1. Volume : 体量大 起步存储当量为TB级,或者日均增长GB级
2. Variety : 样式多 数据种类多
	a. 结构化数据  例如:数据库中的数据
	b. 半结构化数据 例如:JSON文件、XML文件
	c. 非结构化数据 例如:图像、视频、音频文件
3. Velocity : 速度快 保证数据的时效性
4. Value : 低价值密度的数据挖掘出高价值

大数据的应用场景

  1. 个人推荐

    例如:多数用户会在购买了A商品以后会去购买B商品,那当某一个用户购买了A商品后,系统就会推荐B商品给用户

  2. 风险控制

    例如: 根据用户的行为模型判断用户的操作是否存在异常,支付系统中根据用户输入密码的动作模型判断用户是否是同一人

  3. 气候预测

    例如: 根据往年某一天的气候信息预测未来某一天的气候信息

  4. 人工智能

    例如:无人汽车,语音助手

分布式系统

为了解决数据的存储和分析问题,采用多台机器组成分布式集群,共同对数据进行存储和计算分析

大数据解决方案

存储问题

  • Hadoop Distributed File System (HDFS)

计算问题

  • 批处理
    • MapReduce : 代表基于磁盘的离线静态大数据批处理
    • Spark : 代表基于内存的离线静态大数据批处理
  • 流处理
    • Strom、Spark Streaming、Flink、Kafka Stream : 实时流处理 达到对记录级别数据的毫秒级处理

二、Hadoop

概述

  1. Hadoop : 适合大数据的分布式存储和计算平台

  2. Hadoop并不是一个具体的框架或者组件,它是 Apache软件基金会 下用 Java语言 开发的一个 开源分布式计算平台 ,实现在大量计算机组成的集群中对海量数据进行分布式计算。

  3. Hadoop1.x中包括两个核心组件:MapReduceHadoop Distributed File System (HDFS)

    HDFS : 对海量数据进行分布式存储 【解决大数据的存储问题】

    MapReduce : 对海量数据进行并行分析计算 【解决大数据的计算分析问题】

Hadoop生态圈

  1. HDFS : Hadoop分布式文件存储系统 实现对海量数据的存储

  2. MapReduce : Hadoop分布式计算框架 实现对海量数据的计算分析

  3. HBase : 一款基于列式存储的NOSQL

  4. Hive : 一款SQL解释引擎,将SQL解释为MapReduce任务,在集群中运行

  5. Flume : 分布式日志收集系统 用于数据采集

  6. Kafka : 分布式的消息系统

  7. Zookeeper : 分布式协调服务,用于集群选举(选举主机)、状态监控(监控节点状态)、注册中心(注册节点)、配置中心(记录配置信息)、分布式锁

HDFS

一、安装

1) 准备虚拟机

配置IP地址

[root@HadoopNode00 ~]# vim /etc/sysconfig/network-scripts/ifcfg-eth0
ONBOOT=yes				# 改为开启启动
BOOTPROTO=static		# 改为静态IP地址

# 增加下面两行
IPADDR=192.168.xx.xx 	# 第一个xx 局域网的网段   第二个xx 随便给个IP就行 例如 100
NETMAKE=255.255.255.0	# 添加子网掩码
root@HadoopNode00 ~]# service network resatrt 		# 重启网络服务
[root@HadoopNode00 ~]# vim /etc/sysconfig/network	# 修改主机名
NETWORKING=yes
HOSTNAME=HadoopNode00	# 改为自己定义的主机名
[root@HadoopNode00 ~]# service iptables stop  		# 关闭防火墙
[root@HadoopNode00 ~]# chkconfig iptables off 		# 关闭防火墙自启
[root@HadoopNode00 ~]# vi /etc/hosts           	 	# 配置IP和HOSTNAME的映射关系
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
192.168.xx.xx HadoopNode00
[root@HadoopNode00 ~]# reboot 						# 重启虚拟机

2)安装JDK8

[root@HadoopNode00 ~]# mkdir /home/java 									# 创建存放JDK的文件夹
[root@HadoopNode00 ~]# tar -zxvf jdk-8u231-linux-x64.tar.gz -C /home/java	# 解压缩到目标文件夹
[root@HadoopNode00 ~]# vi .bashrc											# 配置环境变量
# 在文件中添加如下两行
export JAVA_HOME=/home/java/jdk1.8.0_181
export PATH=$PATH:$JAVA_HOME/bin

3)配置SSH免密登录

[root@HadoopNode00 .ssh]# ssh-keygen -t rsa    # 生成公私玥 
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
39:e3:a3:d6:5a:19:4f:c5:02:01:e0:00:a9:f0:5c:4a root@HadoopNode00
The key's randomart image is:
+--[ RSA 2048]----+
|.o. ....o.       |
|o Eo.    . .     |
|o+ o.     . o    |
|. +      . o     |
|        S .      |
|       . B       |
|       .= .      |
|      .o..       |
|     .o.         |
+-----------------+
[root@HadoopNode00 .ssh]# ssh-copy-id HadoopNode00  # 复制Hadoopnode00的公钥
The authenticity of host 'hadoopnode00 (192.168.126.10)' can't be established.
RSA key fingerprint is 4d:18:40:3d:24:1a:85:ce:ea:3c:a2:76:85:47:e8:12.
Are you sure you want to continue connecting (yes/no)? yes	# 这里输入yes
Warning: Permanently added 'hadoopnode00,192.168.126.10' (RSA) to the list of known hosts.
root@hadoopnode00's password:			# 这里需要输入登录的主机名的密码
Now try logging into the machine, with "ssh 'HadoopNode00'", and check in:

  .ssh/authorized_keys

to make sure we haven't added extra keys that you weren't expecting.

[root@HadoopNode00 .ssh]# ssh hadoopnode00   # 测试 免密登陆 hadoopnode00
Last login: Thu Oct 24 19:30:44 2019 from 192.168.126.1
[root@HadoopNode00 ~]# exit;	# 退出登录
logout
Connection to hadoopnode00 closed.

4)正式安装Hadoop

准备 hadoop-2.9.2.tar.gz

上传至虚拟机

[root@HadoopNode00 ~]# mkdir /home/hadoop # 创建存放Hadoop的目录
[root@HadoopNode00 ~]# tar -zxvf hadoop-2.9.2.tar.gz -C /home/hadoop # 解压缩到刚才创建的目录

5)配置环境变量

[root@HadoopNode00 ~]# vi .bashrc 		# 编辑配置文件
export HADOOP_HOME=/home/hadoop/hadoop-2.9.2
export PATH=$PATH:$HADOOP_HOME/sbin:$HADOOP_HOME/bin	# 不要丢了JDK的配置
[root@HadoopNode00 ~]# source .bashrc	# 使环境变量生效

HADOOP_HOME环境变量被第三方所依赖, 如HBase、Hive、Flume、Spark 在集成Hadoop的时候,是通过读取HADOOP_HOME环境确定Hadoop的位置,所以这里的变量名必须是 HADOOP_HOME

6)修改配置文件

移动到Hadoop安装目录下

[root@HadoopNode00 ~]# cd /home/hadoop/hadoop-2.9.2/etc/hadoop
  1. 配置 core-site.xml

    [root@HadoopNode00 ~]# vi core-site.xml
    
    <configuration>
    
    <!-- 指定HDFS的外部访问路径 -->
    <property>
      <name>fs.defaultFS</name>
      <value>hdfs://HadoopNode00:9000</value>
    </property>
    
    <!-- 指定元数据的存储路径 特别重要! -->
    <property>
      <name>hadoop.tmp.dir</name>
      <value>/home/hadoop/hadoop-2.9.2/hadoop-${user.name}</value>
    </property>
    
    </configuration>
    
  2. 配置 hdfs-site.xml

    [root@HadoopNode00 ~]# vi hdfs-site.xml
    
    <configuration>
    
    <!-- 指定数据冗余备份的个数 单节点模式下设置为1即可 -->
    <property>
      <name>dfs.replication</name>
      <value>1</value>
    </property>
    
    </configuration>
    

7)启动HDFS

[root@HadoopNode00 ~]# cd 							# 回到root目录
[root@HadoopNode00 ~]# hdfs namenode -format 		# 第一次启动HDFS时需要格式化NameNode
[root@HadoopNode00 ~]# start-dfs.sh   				# 启动hdfs
[root@HadoopNode00 ~]# jps  						# 查看相关进程
13779 DataNode
13669 NameNode
14201 SecondaryNameNode
14413 Jps

HDFS启动成功后,可以使用Web界面查看当前节点的一些信息

访问地址: IP|HostName : 50070

注意:在Windows下使用Host Name访问Web界面的时候,需要配置域名与IP的映射关系

找到 C:\Windows\System32\drivers\etc\HOSTS 文件,在文件最后添加一行 192.168.xx.xx HadoopNode00

二、HDFS Shell

1) 查看所有命令

[root@HadoopNode00 ~]# hdfs
Usage: hdfs [--config confdir] COMMAND
       where COMMAND is one of:
  dfs                  run a filesystem command on the file systems supported in Hadoop.
  namenode -format     format the DFS filesystem
  secondarynamenode    run the DFS secondary namenode
  namenode             run the DFS namenode
  journalnode          run the DFS journalnode
  zkfc                 run the ZK Failover Controller daemon
  datanode             run a DFS datanode
  dfsadmin             run a DFS admin client
  haadmin              run a DFS HA admin client
  fsck                 run a DFS filesystem checking utility
  balancer             run a cluster balancing utility
  jmxget               get JMX exported values from NameNode or DataNode.
  mover                run a utility to move block replicas across
                       storage types
  oiv                  apply the offline fsimage viewer to an fsimage
  oiv_legacy           apply the offline fsimage viewer to an legacy fsimage
  oev                  apply the offline edits viewer to an edits file
  fetchdt              fetch a delegation token from the NameNode
  getconf              get config values from configuration
  groups               get the groups which users belong to
  snapshotDiff         diff two snapshots of a directory or diff the
                       current directory contents with a snapshot
  lsSnapshottableDir   list all snapshottable dirs owned by the current user
                                                Use -help to see options
  portmap              run a portmap service
  nfs3                 run an NFS version 3 gateway
  cacheadmin           configure the HDFS cache
  crypto               configure HDFS encryption zones
  storagepolicies      get all the existing block storage policies
  version              print the version

Most commands print help when invoked w/o parameters.

2) 查看所有HDFS操作相关命令

HDFS中的大部分命令与Linux操作系统中的命令相同,功能相同

[root@HadoopNode00 ~]# 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] <localsrc> ... <dst>]
        [-copyToLocal [-p] [-ignoreCrc] [-crc] <src> ... <localdst>]
        [-count [-q] [-h] <path> ...]
        [-cp [-f] [-p | -p[topax]] <src> ... <dst>]
        [-createSnapshot <snapshotDir> [<snapshotName>]]
        [-deleteSnapshot <snapshotDir> <snapshotName>]
        [-df [-h] [<path> ...]]
        [-du [-s] [-h] <path> ...]
        [-expunge]
        [-get [-p] [-ignoreCrc] [-crc] <src> ... <localdst>]
        [-getfacl [-R] <path>]
        [-getfattr [-R] {-n name | -d} [-e en] <path>]
        [-getmerge [-nl] <src> <localdst>]
        [-help [cmd ...]]
        [-ls [-d] [-h] [-R] [<path> ...]]
        [-mkdir [-p] <path> ...]
        [-moveFromLocal <localsrc> ... <dst>]
        [-moveToLocal <src> <localdst>]
        [-mv <src> ... <dst>]
        [-put [-f] [-p] [-l] <localsrc> ... <dst>]
        [-renameSnapshot <snapshotDir> <oldName> <newName>]
        [-rm [-f] [-r|-R] [-skipTrash] <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> ...]
        [-usage [cmd ...]]

Generic options supported are
-conf <configuration file>     specify an application configuration file
-D <property=value>            use value for given property
-fs <local|namenode:port>      specify a namenode
-jt <local|resourcemanager:port>    specify a ResourceManager
-files <comma separated list of files>    specify comma separated files to be copied to the map reduce cluster
-libjars <comma separated list of jars>    specify comma separated jar files to include in the classpath.
-archives <comma separated list of archives>    specify comma separated archives to be unarchived on the compute machines.

The general command line syntax is
bin/hadoop command [genericOptions] [commandOptions]

3) 上传文件

[root@HadoopNode00 ~]# hdfs fs -put 1.txt /	# 将当前目录下的1.txt文件上传至DFS的根目录(/)下

4)文件下载

# 将文件系统中 /1.txt 这个文件下载至 /root/2.txt 文件中
# 相当于下载并且给文件起了一个别名
# 如果不指定目录文件名称,默认使用原文件名
[root@HadoopNode00 ~]# hdfs fs -get /1.txt /root/2.txt	

5)显示文件列表

[root@HadoopNode00 ~]# hdfs fs -ls /	# 显示根目录下的文件和文件夹
Found 1 items
-rw-r--r--   1 root supergroup       8917 2019-10-24 23:44 /1.txt

6) 复制文件

发生在HDFS内部的,并不是将HDFS上的文件拷贝到本地磁盘

[root@HadoopNode00 ~]# hdfs fs -cp /1.txt /2.txt	# 将 /1.txt 文件拷贝到 /2.txt中

7)创建文件夹

[root@HadoopNode00 ~]# hdfs fs -mkdir /FuXin 	# 在根目录下创建一个名叫FuXin的文件夹

8)从本地移动文件到HDFS

此操作会将本地文件系统的文件删除 类似于剪切-粘贴

[root@HadoopNode00 ~]# hdfs fs -moveFromLocal 1.txt.tar.gz /	# 将当前目录的1.txt.tar.gz文件移动到HDFS的根目录

9)从HDFS复制到本地

与下载功能相同

[root@HadoopNode00 ~]# hdfs fs -copyToLocal  /1.txt /root/3.txt	# 将 HDFS 上 /1.txt文件 复制到 本地 /root/3.txt中

10)删除文件

[root@HadoopNode00 ~]# hdfs fs -rm -r -f /1.txt # 删除HDFS上 /1.txt	-r -f 表示强制删除不提示

11)删除文件夹

[root@HadoopNode00 ~]# hdfs fs -rmdir /baizhi	# 删除HDFS上的文件夹

12)显示文件内容

[root@HadoopNode00 ~]# hdfs fs -cat /2.txt	# 查看/2.txt的文件内容

13)追加文件内容

[root@HadoopNode00 ~]# hdfs fs -appendToFile  1.txt /2.txt # 将文件1.txt中的内容追加到HDFS/2.txt中

三、回收站机制

为了防止用户的误删除操作,HDFS引入了回收站的机制,当文件删除后可以在一段时间内重新恢复

1)配置 core-site.xml

<!-- 设置文件的过期时间 (单位:分钟) -->
<property>
    <name>fs.trash.interval</name>
    <value>1</value>
</property>

2)重启 HDFS

[root@HadoopNode00 ~]# stop-dfs.sh
[root@HadoopNode00 ~]# start-dfs.sh

3)测试

# 删除文件 一分钟后删除
[root@HadoopNode00 ~]# hdfs fs -rm -r -f /1.txt 
19/10/25 00:16:59 INFO fs.TrashPolicyDefault: Namenode trash configuration: Deletion interval = 1 minutes, Emptier interval = 0 minutes.
Moved: 'hdfs://HadoopNode00:9000/1.txt' to trash at: hdfs://HadoopNode00:9000/user/root/.Trash/Current

# 能够在对应文件夹中看到文件
[root@HadoopNode00 ~]# hdfs fs -ls /user/root/.Trash/191025001700   
Found 1 items
-rw-r--r--   1 root supergroup       8917 2019-10-25 00:16 /user/root/.Trash/191025001700/1.txt

# 一分钟后就无法看到文件了
[root@HadoopNode00 ~]# hdfs fs -ls /user/root/.Trash/191025001700   
ls: `/user/root/.Trash/191025001700': No such file or directory

四、HDFS Java API

1)导入Maven依赖

注意:依赖的版本必须与Hadoop的版本一致

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

2)Windows Hadoop环境安装配置

  1. 解压 hadoop-2.9.2-src.tar.gz 到本地磁盘

  2. winutils.exe、hadoop.dll 放入Hadoop的bin目录下,覆盖原文件

  3. 配置Windows下的Hadoop环境变量

3)IDEA运行提示权限不足的解决方案

org.apache.hadoop.security.AccessControlException: Permission denied: user=Administrator, access=WRITE, inode="/":root:supergroup:drwxr-xr-x
	at org.apache.hadoop.hdfs.server.namenode.FSPermissionChecker.checkFsPermission(FSPermissionChecker.java:271)
	at org.apache.hadoop.hdfs.server.namenode.FSPermissionChecker.check(FSPermissionChecker.java:257)
	at org.apache.hadoop.hdfs.server.namenode.FSPermissionChecker.check(FSPermissionChecker.java:238)
	at org.apache.hadoop.hdfs.server.namenode.FSPermissionChecker.checkPermission(FSPermissionChecker.java:179)

方案一 :

// 在代码中加入系统参数设置
System.setProperty("HADOOP_USER_NAME","root");

方案二 :

# 运行时添加JVM参数【常用,可以适应不同的编程语言环境】
-DHADOOP_USER_NAME=root

方案三 :

配置 hdfs-site.xml

<!-- 关闭HDFS的权限检查 -->
<property>
  <name>dfs.permissions.enabled</name>
  <value>false</value>
</property>

4)获取HDFS客户端

// 解决访问权限不足的问题
System.setProperty("HADOOP_USER_NAME","root");

// 定义配置文件对象
Configuration conf = new Configuration();

// 读取配置文件
conf.addResource("core-site.xml");
conf.addResource("hdfs-site.xml");

// 通过配置文件构建客户端对象
FileSystem fileSystem = FileSystem.newInstance(conf);

// 使用完毕后关闭客户端
fileSystem.close();

5)上传操作

方式一 :

// 定义本地文件位置
Path localFile = new Path("D:\\大数据训练营课程\\Note\\Day02-Hadoop\\Hadoop.md");
// 定义上传的文件路径
Path hdfsFile = new Path("/3.md");

// 执行文件上传操作
fileSystem.copyFromLocalFile(localFile, hdfsFile);

方式二 :

// 创建文件输入流
FileInputStream inputStream = new FileInputStream(new File("D:\\大数据训练营课程\\Note\\Day02-Hadoop\\Hadoop.md"));
// 通过客户端创建输出流 - 输出至HDFS
FSDataOutputStream outputStream = fileSystem.create(new Path("/4.md"));

// 使用工具类进行流的传输
// 参数一 : 输入流
// 参数二 : 输出流
// 参数三 : 缓冲区大小
// 参数四 : 是否关闭流	工具类可以帮助我们关闭流
IOUtils.copyBytes(inputStream,outputStream,1024,true);

6)下载操作

方式一 :

// 参数一 : 是否删除源目录
// 参数二 : 需要下载的文件地址
// 参数三 : 目标路径
// 参数四 : 是否使用本地文件系统 使用Java的IO流传输
fileSystem.copyToLocalFile(false,new Path("/4.md"),new Path("D:\\大数据训练营课程\\Note\\Day02-Hadoop\\5.md"),true);

方式二 :

// 定义文件输出流  从HDFS输出到本地文件系统
FileOutputStream outputStream = new FileOutputStream(new File("D:\\大数据训练营课程\\Note\\Day02-Hadoop\\6.md"));
// 定义文件输入流
FSDataInputStream inputStream = fileSystem.open(new Path("/4.md"));

// 使用工具类进行流的传输 
IOUtils.copyBytes(inputStream,outputStream,1024,true);

7)删除操作

// 参数一 : 指定删除的路径
// 参数二 : 是否递归删除
// 返回值 : Boolean: true -> 删除成功  false -> 删除失败
boolean delete = fileSystem.delete(new Path("/1.md"), true);
if (delete){
    System.out.println("删除成功");
}else {
    System.out.println("删除失败");
}

8)判断文件是否存在

// 参数一 : 指定删除的文件路径
// 返回值 : Boolean  true -> 存在   false -> 不存在
boolean exists = fileSystem.exists(new Path("/123122.md"));
if (exists){
	System.out.println("存在");
}else {
	System.out.println("不存在");
}

9)显示文件列表

// 参数一 : 服务器文件夹地址
// 参数二 : 是否递归查找所有子目录
// 返回值 : 文件状态迭代器 可通过此迭代器获得所有文件或文件夹的信息
RemoteIterator<LocatedFileStatus> remoteIterator = fileSystem.listFiles(new Path("/"), true);
while (remoteIterator.hasNext()){
    LocatedFileStatus fileStatus = remoteIterator.next();
    System.out.println(fileStatus.getPath());
}

10)创建文件夹

// 参数一 : 指定要创建的文件夹路径
// 返回值 : Booealn   true -> 成功  false -> 失败
boolean is = fileSystem.mkdirs(new Path("/baizhi"));
if (is) {
    System.out.println("创建成功");
} else {
    System.out.println("创建失败");
}

五、HDFS的体系结构

在这里插入图片描述

HDFS是一个主/从架构的分布式文件系统,HDFS集群由1个Name Node节点和多个Data Node节点组成,Name Node是一个主服务器,它管理着文件系统的命名空间并且控制客户端对文件的访问。Data Node是集群中存储数据的节点,通常每台机器都是一个Data Node,HDFS公开一个文件系统命名空间,并允许用户数据存储在文件中。在HDFS内部,文件被分成一个或多个 ,这些块存储在一组数据节点中。Name Node负责执行文件系统的命名空间操作,例如打开、关闭和重命名文件或目录。它还确定了 到数据节点的映射【指示数据块在那个Data Node上】,数据节点(Data Node)负责为客户端的读写请求提供服务。Data Node 根据 Name Node的指令执行块创建、删除、复制操作。

1)名词解释

  1. NameNode(NN) : 存储系统运行所需的元数据、例如:文件命名空间、块到数据节点的映射、Data Node的状态信息,同时负责管理Data Node

  2. DataNode(DN) : 用于存储文件数据的节点,负责响应客户端的读写请求,向Name Node汇报自身状态信息

  3. block : 数据块,是对文件拆分的最小单元,HDFS将用户上传的文件拆分为多份,分布在数据块中,每个数据块默认大小为 128MB ,同时,每一个数据块默认都有 3个 副本,用户可以通过 dfs.replication 设置副本的数量,也可以通过 dfs.blocksize 设置数据块的大小

  4. rack : 机架,使用机架对存储节点做物理的编排,用于优化存储和计算,HDFS能够根据节点的物理位置将存储与计算的网络IO和磁盘IO降到最低

# 使用以下命令可以查看机架配置
[root@HadoopNode00 ~]# hdfs dfsadmin -printTopology
Rack: /default-rack
   192.168.126.10:50010 (HadoopNode00)

2)什么是Block块

Block块是HDFS中存储文件数据的最小单位,所有上传到HDFS的文件都会根据Block Size被划分为多块,每一块分散的分布在集群的各个节点上,而且默认每一块都会有3个副本,副本通过机架感知技术有序的分布在各个节点[机器]上

可以通过配置 hdfs-site.xml 文件改变Block块的大小

<property>
  <name>dfs.blocksize</name>
  <value>134217728</value>
  <description>
      The default block size for new files, in bytes.
      You can use the following suffix (case insensitive):
      k(kilo), m(mega), g(giga), t(tera), p(peta), e(exa) to specify the size (such as 128k, 512m, 1g, etc.),
      Or provide complete size in bytes (such as 134217728 for 128 MB).
  </description>
</property>
1. 为什么Block块是128MB?
	硬件限制:廉价PC使用的是机械硬盘,读写速度慢
	软件优化:通常数据传输的最佳状态为:寻址时间时传输时间的100分之1
2. Block块的大小能够随意设置?
	不行!如果过小会造成集群中小文件堆积过多,寻址时间增加,读写效率降低;如果过大会造成读写时间较长,效率降低。

3)机架感知技术

为了保证HDFS集群的 可靠性 、 **高效性 ** ,Name Node会根据节点在机架上的分配情况,尽可能的保证集群中节点之间的通讯是在同一个机架上的,而且为了提高容错的能力,Name Node会将数据块的备份尽可能的分布在不同的机架上,通常本地机架机器【当前节点】会存放一个副本,本地机架其他机器会存放一个副本,其他机架其他机器会存放一个副本,剩下的副本随机分布在各个机架的机器上,这就是机架感知技术。机架感知技术一般用于集群较大的环境下。

总结:

  1. 尽可能的保证节点之间的通讯是发生在同一个机架上的 【高效】
  2. 尽可能的保证数据块的备份是分布在不同的机器上的 【可靠】

4)Secondary Name Node 和 Name Node 的关系

Meta Data : 元数据,存放在Name Node中,例如Block在集群中的位置信息、文件的命名空间信息、Data Node的状态信息等HDFS运行所必须的信息

FSImage : 元数据的备份,是文件系统某一时刻的快照,保存着文件系统某一时刻的全部元数据【Meta Data】

EditsLog : 记录着某一时刻之后,发生在集群中的文件更新和增加等操作

1. NameNode在启动时会加载FSImage和EditsLog文件【这就是为什么第一次启动时需要格式化NameNode,为了生成这些文件】
2. HDFS在运行时将新发生的操作写入到EditsLog文件中,也就是说,在任意时刻FSImage + EditsLog文件 = HDFS的元数据
3. 当HDFS的一直在运行时,EditsLog会不断的变大,不利于系统的维护,系统的启动时间会变长
4. 此时,SecondaryNameNode会承担起合并FSImage和EditsLog文件的任务,SecondaryNameNode会从NameNode节点将两个文件拉取到自己的节点上,对文件进行合并
5. 在合并文件的同时,如果有写请求,就会将新的写请求写入到新建的edits-inprogress文件中。
6. 当合并过程完成后,SecondaryNameNode会将合并好的FSImage文件重新上传至NameNode节点,替换原来的FSImage,同时,edits-inprogress文件会更名为EditsLog文件,继续负责记录新的写请求。至此SecondaryNameNode和NameNode共同合作完成了合并元数据的任务。

5)检查点机制

当HDFS运行到达检查点时(满足检查点所需要的要求),Secondary Name Node和Name Node就会进行元数据的合并操作。

<!-- 设置两个检查点之间的时间间隔 单位:秒 -->
<property>
  <name>dfs.namenode.checkpoint.period</name>
  <value>3600</value>
</property>
<!-- 定义NameNode上的非检查点事务数,即当操作达到100万次时,即使没有到达检查点时间,也会执行合并操作 -->
<property>
  <name>dfs.namenode.checkpoint.txns</name>
  <value>1000000</value>
</property>

6)安全模式

安全模式就是HDFS集群的只读模式。

HDFS在启动时会默认开启安全模式,此时,各个Data Node开始向Name Node汇报自身的状态信息,当Name Node 发现绝大多数的Block块可用时,会自动退出安全模式。

用户可以手动启动将HFDS置于安全模式,使用 hdfs dfsadmin -safemode 命令即可。

[root@HadoopNode00 ~]# hadoop fs -put 1.txt /			# 首先正常模式尝试添加文件
[root@HadoopNode00 ~]# hadoop fs -ls /					# 验证是否添加成功
Found 6 items
-rw-r--r--   1 root          supergroup       8917 2019-10-25 23:27 /1.txt
-rw-r--r--   1 root          supergroup      16946 2019-10-25 18:01 /2.md
-rw-r--r--   1 Administrator supergroup      18279 2019-10-25 18:07 /3.md
-rw-r--r--   1 Administrator supergroup      18279 2019-10-25 18:16 /4.md
drwxr-xr-x   - Administrator supergroup          0 2019-10-25 18:37 /baizhi
drwx------   - root          supergroup          0 2019-10-25 00:15 /user
[root@HadoopNode00 ~]# hdfs  dfsadmin -safemode enter   # 开启安全模式
Safe mode is ON
[root@HadoopNode00 ~]# hadoop fs -put 1.txt /2.txt		# 添加文件时报错  添加失败  安全模式即只读模式 只能读取 不能写入
put: Cannot create file/2.txt._COPYING_. Name node is in safe mode.
[root@HadoopNode00 ~]# hdfs  dfsadmin -safemode leave   # 关闭安全模式
Safe mode is OFF
[root@HadoopNode00 ~]# hadoop fs -put 1.txt /2.txt		# 再次添加文件 添加成功

7)为什么HDFS不擅长存储小文件?

任何大小的文件在进入HDFS时都会有元数据写入到Name Node中,且元数据占用的存储空间几乎相同,也就是说,无论存入的文件时1KB或者是1GB,它们在Name Node中的占用的空间都是相同的,而在一个HDFS集群中,Name Node有且只有一个,无法扩容,如果存放了过多的小文件,会导致Name Node的内存紧张

文件大小/个数Name Node内存占用Data Node磁盘占用
128MB单个文件1KB 元数据 | 1kB128MB*3
128KB*1000个文件1000 1KB 元数据 | 1MB128MB*3
  • 如果解决HDFS中小文件存储问题?

    可通过自定义Input Format组件来解决小文件的存储问题,将多个文件合并成一个序列文件。

8)HDFS写数据流程

在这里插入图片描述

1. 客户端向NameNode请求上传文件
2. NameNode收到请求后校验请求的文件是否可以上传【内存是否足够,当前模式是否允许写入、是否存在同名文件等】,返回校验的信息
3. NameNode允许上传后客户端计算【对文件切分】并上传块信息
4. NameNode根据上传的块信息计算出文件存放的位置信息【存放块的DataNode】,并将位置信息返回给客户端
5. 客户端验证请求的DataNode是否可用
6. 逐个验证完成后返回验证的信息
7. 客户端向不同的DataNode上传文件块,DataNode内部对文件块进行备份
8. 文件块存储完成后返回上传文件成功的响应

9)HDFS读数据流程

在这里插入图片描述

1. 客户端向NameNode发送文件的Path请求下载文件
2. NameNode对请求的Path进行校验【文件是否存在,是否完整等】,校验完成后返回文件存放的位置【DataNode的位置信息】
3. 客户端获取到DataNode的位置信息后向DataNode请求下载对应的文件块
4. DataNode将文件块返回给客户端,客户端接收到文件块后,将文件块暂存在本地
5. 客户端向其他的DataNode请求下载文件块,下载完成后将文件块暂存在本地
6. 当所有的文件块都下载完成后,客户端将文件块合并成一个完成的文件,完成下载

10)Data Node的工作机制

在这里插入图片描述

1. DataNode启动时会向NameNode发送自身状态信息进行注册,目的是为了让NameNode能够发现自己
2. 注册成功后,DataNode会进行周期性的上报和心跳检查
3. DataNode会周期性的上报块信息【数据长度、校验和、时间戳等】
4. DataNode每3秒钟想NameNode发送一次心跳,NameNode收到心跳后,返回DataNode需要执行的指令
5. 当NameNode超过10分钟没有收到DataNode的心跳时,则认为该节点已经失效,需要重新启动

MapReduce

一、概述

MapReduce是一种编程模型,用于对大规模的数据集进行并行计算。

  • Map(映射)、Reduce(规约)是MapReduce的核心思想。这种思想是从函数式编程和矢量编程中借鉴过来的。
  • MapReduce极大的方便了编程人员在不会分布式并行编程的情况下,将自己的程序运行在分布式的系统上。
  • Map:将一组键值对映射成一组新的键值对。
  • Reduce:用来保证所有键值对共享相同的键组。

MapReduce也是一个并行计算框架,即将一个任务(Job)拆分成2个阶段,一个Map阶段,一个Reduce阶段

  • MapReduce充分的利用存储节点的计算资源【CPU、内存、网络、少许的硬盘】进行并行计算。
  • 执行MapReduce计算必须启动Yarn,集群中每一个节点都会启动一个Node Manager进程,用来对当前节点的计算机资源进行管理和使用。
  • Node Manager默认情况下会将当前节点的物理主机资源抽象成8个计算单元,这些单元被称为Container
  • 集群中的所有Node Manager都听从Resource Manager的调度。

MapReduce擅长进行大数据计算,其核心思想是 分而治之

  • Map负责 ,即把复杂的任务划分为若干个小的任务
    • 数据的规模或者计算的规模比原任务大大缩小
    • 该计算属于就近计算,即任务会被分配到所需的节点上进行计算
    • 可以进行并行计算,彼此之间没有干扰和依赖。
  • Reduce负责对Map阶段的结果进行 汇总

1)为什么使用MapReduce?

​ 例如某台计算机在执行某一个计算任务时,在数据量小的时候不会出现问题,一旦数据量过大时,势必就会出现计算时间过长,无法满足在合理时间段内分析出来数据,而结果这个问题有2种方案:

1.  对当前计算机的硬件进行纵向扩展,而这样做的成本较高、扩展有上限,并且当计算机出现故障的时候无法容错。
2.  使用多台计算机组成集群,横向扩展,这样既提高了计算能力,而且提高了容错能力,理论上来讲,计算能够能够无限扩展。

2)MapReduce的优缺点

  • 优点
    • 易于编写分布式应用程序
    • 有良好的的拓展能力
    • 适合进行离线批处理
    • 容错性高
  • 缺点
    • 不适合进行流处理
    • 不适合进行图计算
    • 不适合进行迭代计算

二、运行环境的搭建(Yarn)

1)修改配置文件

$HADOOP_HOME/etc/hadoop/yarn-site.xml

<!-- 配置NodeManager上运行的附属服务 计算过程中需要使用 -->
<property>
    <name>yarn.nodemanager.aux-services</name>
    <value>mapreduce_shuffle</value>
</property>

<!-- 指定ResourceManager节点的IP地址 -->
<property>
    <name>yarn.resourcemanager.hostname</name>
    <value>HadoopNode00</value>
</property>

$HADOOP_HOME/etc/hadoop/mapred-site.xml

<!-- 使用集群模式运行MapReduce -->
<property>
    <name>mapreduce.framework.name</name>
    <value>yarn</value>
</property>

2)启动Yarn

[root@HadoopNode00 ~]# start-yarn.sh	# 启动命令
starting yarn daemons
starting resourcemanager, logging to /home/hadoop/hadoop-2.6.0/logs/yarn-root-resourcemanager-HadoopNode00.out
localhost: starting nodemanager, logging to /home/hadoop/hadoop-2.6.0/logs/yarn-root-nodemanager-HadoopNode00.out

[root@HadoopNode00 ~]# jps				# 查看运行的Java进程
43856 Jps
43362 ResourceManager
2403 NameNode
43636 NodeManager
2776 SecondaryNameNode
2556 DataNode

[root@HadoopNode00 ~]# stop-yarn.sh		# 停止命令
stopping yarn daemons
stopping resourcemanager
localhost: stopping nodemanager
no proxyserver to stop

Yarn模式启动后可以通过 http://HadoopNode00:8088 网址提供Web界面查看任务的运行状态,集群的计算资源使用情况等。

3)进程及名词解释

NodeManager : 每个计算节点上都有一个Node Manager,管理主机上的计算资源,是每台机器在框架中的代理,向Resource Manager汇报自身的状态信息。

ResrouceManager : 一个集群中只有一个Resource Manager负责集群资源的统筹规划,拥有着对集群资源的最终决策权。

ApplicationMaster : 每运行一个计算任务就会启动一个Application Master进程,主要负责申请计算资源、协调计算任务。

YarnChild : 负责最实际的计算任务(Map Task|Reduce Task)。

Container : 是计算资源的抽象,代表着一组内存、CPU、网络的占用,无论是Application Master进程还是Yarn Child进程都需要消耗一个Container。

三、编写MapReduce程序

使用MapReduce程序实现单词计数(Word Count)

# 假设有一数据文件 内容如下
apple xiaojiao bunana taozi
shouji xinyongka huoji pingban
apple xiaojiao taozi
shouji xinyongka huoji pingban
apple xiaojiao bunana taozi
shouji huoji pingban
apple xiaojiao bunana taozi
shouji xinyongka huoji pingban

1)配置Maven依赖

<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-common</artifactId>
    <version>2.6.0</version>
</dependency>	

<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-hdfs</artifactId>
    <version>2.6.0</version>
</dependency>

<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-mapreduce-client-core</artifactId>
    <version>2.6.0</version>
</dependency>

<dependency>
    <groupId>org.apache.hadoop</groupId>
    <artifactId>hadoop-mapreduce-client-jobclient</artifactId>
    <version>2.6.0</version>
</dependency>

2)编写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;

// 继承Mapper接口 规定4个泛型 keyIn valueIn keyOut valueOut:
// 这里使用TextInputFormat组件作为数据输入组件
// keyIn必须为LongWritable类型 valueIn必须为Text类型 这两个值由输入组件提供所以不能改变
// keyOut 指定为Text类型 将来输出单词
// valueOut 指定为IntWritable类型 将来输出单词出现的次数
public class WCMapper extends Mapper<LongWritable, Text,Text, IntWritable> {
    
    // Map端使用TextInputFormat组件解析文件,每解析一行文本就会调用一个map()方法
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
       	// key 是一个Long类型的值,代表文本的偏移量\文本在文件中的位置
        // value 是一个Text类型的值,代表的是读取到的一行文本
        // 这里对一行文本数据进行拆分得到每一个单词
        String[] words = value.toString().split(" ");
        for (String word : words) {
            // 为每一个单词标识一个出现次数,每个单词都出现1次
            context.write(new Text(word),new IntWritable(1));
        }
    }
}

这里输入的数据是 apple xiaojiao bunana taozi ,输出的数据是 (apple,1)、(xiaojiao,1)、(bunana,1)、(taozi,1)

3)编写Reduce逻辑

将Map端映射的结果进行汇总

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

import java.io.IOException;

// 继承Reduce接口 同样规定4个泛型 keyIn valueIn keyOut valueOut:
// Reduce端的keyIn与valueIn必须和Map端的keyOut与valueOut一一对应
// 所以这里的keyIn使用Text类型,valueIn使用IntWritable类型
public class WCReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    
    // Reduce端会根据分区的数量去调用reduce方法 默认的分区数为1
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        
        // key 代表一个单词
        // values 代表单词被标识的出现次数
        // 定义一个变量存放单词出现的总次数
        int sum = 0;
        // 遍历数组
        for (IntWritable value : values) {
            sum+=value.get();	// 累加
        }
        // 将统计的结果交给OutputFormat组件输出
        context.write(key,new IntWritable(sum));
    }
}

这里输入的数据是(apple,[1,1,1,1]) ,输出的数据是(apple,4)

在Map端与Reduce端之间存在一个Shuffle(洗牌)过程,即将Key相同的Value分配到一个组内,因为 (apple,1)(apple,1) 的key相同,所以会被合并成 (apple,[1,1]) 的形式,这就是Shuffle。

4)编写Job类

用于配置任务信息,提交计算任务

import jdk.nashorn.internal.scripts.JO;
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.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;

public class WCJob {
    public static void main(String[] args) throws Exception {
        
        // 封装Job对象 可以指定任务的名称
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf, "WC-JOB01");
		
        // 设置任务类
        job.setJarByClass(WCJob.class);

        // 设置数据的写入和写出格式
        job.setInputFormatClass(TextInputFormat.class);
        job.setOutputFormatClass(TextOutputFormat.class);

        // 设置数据的读取路径
        TextInputFormat.setInputPaths(job, new Path("/wcdata.txt"));
        
        // 设置结果的输出路径 注意输出不能存在
        TextOutputFormat.setOutputPath(job, new Path("/test01"));

		// 设置任务的计算逻辑
        job.setMapperClass(WCMapper.class);
        job.setReducerClass(WCReducer.class);
		
        // 指定Map端K,V的输出类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

        // 指定Reduce端K,V的输出类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        // 提交任务
        //job.submit();
        job.waitForCompletion(true);
    }
}

5)提交计算任务

a.Jar包提交

直接在Yarn集群中运行任务

  1. 在Job类中设置jar包中提交任务的类

    job.setJarByClass(WCJob.class);
    
  2. 通过Maven打包
    pom.xml中配置

    <packaging>jar</packaging>
    

    clean -> package

  3. 将打包好的Jar包上传到Yarn集群中

    # 运行命令:hadoop jar jar包名字  job类的全限定名
    hadoop jar Hadoop_Test-1.0-SNAPSHOT.jar com.doudou.mr.test01.WCJob
    
b.本地提交

在IDEA上运行任务

  1. 添加log4j的Maven依赖

    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    
  2. 配置 log4j.properties

    log4j.rootLogger = info,stdout
    
    log4j.appender.stdout = org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.Target = System.out
    log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n
    
  3. 直接在本地运行Job类的main函数即可

    # 在指定需要计算的文件的路径时
    1. 如果文件在HDFS上 需要在文件路径前面加上 hdfs:///
    2. 如果文件在本地磁盘上 需要在文件路径前面加上 file:///
    
c.跨平台提交

在IDEA上运行程序,在Yarn集群上执行计算

  1. 从集群中拷贝配置文件到项目resource目录下 (core-site.xml,hdfs-site.xml,mapred-site.xml,yarn-site.xml

  2. 添加配置到Linux集群的 mapred-site.xml

    <!-- 启用跨平台提交,允许用于从Windows向Linux集群提交任务 -->
    <property>
        <name>mapreduce.app-submission.cross-platform</name>
        <value>true</value>
    </property>
    
  3. 添加代码到Job类中

    // 启动跨平台提交
    conf.set("mapreduce.app-submission.cross-platform","true");
    
    // 读取resource目录中的配置文件
    conf.addResource("conf2/core-site.xml");
    conf.addResource("conf2/hdfs-site.xml");
    conf.addResource("conf2/mapred-site.xml");
    conf.addResource("conf2/yarn-site.xml");
    
    // 设置 jar 包的路径
    conf.set(MRJobConfig.JAR,"D:\\大数据训练营课程\\Code\\BigData\\Hadoop_Test\\target\\Hadoop_Test-1.0-SNAPSHOT.jar");
    
  4. 重新打包后运行Job类

四、自定义Bean对象

1)为什么需要自定义Bean对象

Hadoop只提供了几种基本类型的序列化对象,在实际开发中,这些对象往往是不够用的,或者说不能更好的应对复杂的开发,所以说使用自定义Bean对象可以简化开发流程,但是,自定义Bean对象就需要对涉及到的对象进行序列化操作。

由于Java提供的序列化方案是重量级的,不符合MapReduce计算过程中需要的高效快速的传输要求,因此,Hadoop提供了一套序列化方案 Writeable

2)应用

需求:假设有一份流量日志,包含用户的上行流量和下行流量,要求统计用户的总流量

输入格式: 电话 上行流量 下行流量 用户名

15713770999 12113 123 hn
15713770929 12133 123 zz
15713770909 123 1123 bj
13949158086 13 1213 kf
15713770929 11 12113 xy
15713770999 11113 123 hn
15713770929 123233 123 zz
15713770909 12113 1123 bj
13949158086 13 1213 kf
15713770929 121 12113 xy

输出格式:对象的to String()格式

  1. 自定义Bean对象

    import org.apache.hadoop.io.Writable;
    
    import java.io.DataInput;
    import java.io.DataOutput;
    import java.io.IOException;
    
    public class FlowBean implements Writable {
        private String phone;
        private Long upFlow;
        private Long downFlow;
        private Long sumFlow;
    
        public FlowBean() {
    
        }
    
        public FlowBean(String phone, Long upFlow, Long downFlow, Long sumFlow) {
            this.phone = phone;
            this.upFlow = upFlow;
            this.downFlow = downFlow;
            this.sumFlow = sumFlow;
        }
    
        public String getPhone() {
            return phone;
        }
    
        public void setPhone(String phone) {
            this.phone = phone;
        }
    
        public Long getUpFlow() {
            return upFlow;
        }
    
        public void setUpFlow(Long upFlow) {
            this.upFlow = upFlow;
        }
    
        public Long getDownFlow() {
            return downFlow;
        }
    
        public void setDownFlow(Long downFlow) {
            this.downFlow = downFlow;
        }
    
        public Long getSumFlow() {
            return sumFlow;
        }
    
        public void setSumFlow(Long sumFlow) {
            this.sumFlow = sumFlow;
        }
    
        @Override
        public String toString() {
            return "FlowBean{" +
                    "phone='" + phone + '\'' +
                    ", baizhi upFlow=" + upFlow +
                    ", downFlow=" + downFlow +
                    ", sumFlow=" + sumFlow +
                    '}'  ;
        }
    
        // 对对象进行序列化 编码
        public void write(DataOutput dataOutput) throws IOException {
            if (this.phone!=null) {
                dataOutput.writeUTF(this.phone);
            }
            dataOutput.writeLong(this.upFlow);
            dataOutput.writeLong(this.downFlow);
            dataOutput.writeLong(this.sumFlow);
        }
    
        // 对对象进行反序列化  解码
        public void readFields(DataInput dataInput) throws IOException {
            if (this.phone!=null) {
                this.phone = dataInput.readUTF();
            }
            this.upFlow = dataInput.readLong();
            this.downFlow = dataInput.readLong();
            this.sumFlow = dataInput.readLong();
        }
    }
    
  2. 编写Map逻辑

    import org.apache.hadoop.io.LongWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Mapper;
    
    import java.io.IOException;
    
    public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
    
        @Override
        protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
           	
            // 将每一行日志通过空格进行分割
            String[] infos = value.toString().split(" ");
    
           	// 获取电话号码
            String phone = infos[0];
    
            // 获取上行流量
            Long up = Long.valueOf(infos[1]);
    
            // 获取下行流量
            Long down = Long.valueOf(infos[2]);
    
            // key -> 电话号码 value -> flowbean对象
            context.write(new Text(phone), new FlowBean(null, up, down, up + down));
        }
    }
    
  3. 编写Reduce逻辑

    import org.apache.hadoop.io.NullWritable;
    import org.apache.hadoop.io.Text;
    import org.apache.hadoop.mapreduce.Reducer;
    
    import java.io.IOException;
    
    public class FlowReducer extends Reducer<Text, FlowBean, NullWritable, FlowBean> {
    
        @Override
        protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
            // 定义初始值
            Long up = 0L;
            Long down = 0L;
            Long sum = 0L;
    
            // 遍历values 获取同一个手机号的所有上行 和 下行流量数据
            for (FlowBean value : values) {
                // 对流量数据进行累加
                up += value.getUpFlow();
                down += value.getDownFlow();
                sum += value.getSumFlow();
            }
            // 输出流量数据
            context.write(NullWritable.get(),new FlowBean(key.toString(),up,down,sum));
        }
    }
    
  4. 编写Job类

    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.TextInputFormat;
    import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
    
    public class FlowJob {
        public static void main(String[] args) throws Exception {
            Configuration conf = new Configuration();
            Job job = Job.getInstance(conf);
    
    
            job.setJarByClass(FlowJob.class);
    
            job.setInputFormatClass(TextInputFormat.class);
            job.setOutputFormatClass(TextOutputFormat.class);
    
            TextInputFormat.setInputPaths(job, new Path("D:\\test\\flow.dat"));
            TextOutputFormat.setOutputPath(job, new Path("D:\\out6"));
    
    
            job.setMapperClass(FlowMapper.class);
            job.setReducerClass(FlowReducer.class);
    
    
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(FlowBean.class);
    
    
            job.setOutputKeyClass(NullWritable.class);
            job.setOutputValueClass(FlowBean.class);
    
    
            job.waitForCompletion(true);
        }
    }
    
  5. 运行结果

在这里插入图片描述

五、MapReduce的计算流程

在这里插入图片描述

1. 运行MapReduce任务后,启动JVM进程【RunJar】,实例化MapReduce Job任务实例
2. Job任务实例请求集群的ResourceManager注册应用,ResourceManager返回一个应用标识 【JobID】
3. Job任务实例将任务运行所需的资源【Jar包、切片信息、配置文件】上传到共享文件系统【HDFS】 -> 移动计算而不是移动数据
4. 提交Job,交给ResourceManager进行调度处理
5. Yarn集群的ResourceManager会在一个空间的NodeManager上启动一个容器MRAppMaster【负责对当前的Job进行监控管理,故障恢复】
6. MRAppMaster初始化Job
7. MRAppMaster从共享的文件系统中获取数据切片的信息,用于计算MapTask的数量
8. MRAppMaster向ResourceManager请求计算资源(MapTask的数量 + ReduceTask的数量 = 请求的计算资源数量)
9. MRAPPMaster会在ResourceManager分配的NodeManager上启动对应数量的计算进程【YARNChild】
10. YARNChild从共享文件系统拷贝Jar包,然后执行计算
11. 在YARNChild的JVM进程中运行MapTask或者ReduceTask
12. 将计算结果存放至存储系统,释放占用的资源

六、Job的提交流程

1. 检查是否设置了输出目录,并且设定的输出目录是否存在,存在则报错
2. 创建资源路径【staging】
3. 在staging路径下创建以JobID为名字的文件夹
4. 拷贝Job资源【包括运行时依赖的Jar包、运行的jar包等】到集群
5. 计算切片,生成切片规划文件
6. 向stating路径写入配置文件

七、MapReduce的组件

1)概述

MapReduce的执行顺序:input -> map -> partition -> reduce -> output

1. 输入组件将数据切分成若干个输入切片【InputSplit】,并将每一个InputSplit交给一个MapTask处理。
2. MapTask不断的从对应的InputSplit中解析出一个个的Key、Value,并调用map()函数处理。
3. 分区组件将map()函数处理后的key-value对根据某种分区规则划分在不同的分区【partition】内,然后将分区数据写入到磁盘。
4. 根据分区个数启动对应个数的ReduceTask【一个ReduceTask处理一个Partition内的数据】,ReduceTask从每个MapTask节点拉取属于自己处理的那份数据,然后使用基于排序的方法将key相同的value聚集在一起组成 key-values。
5. 调用reduce()函数处理key-values,然后输出组件将结果输出到文件中。
  • Hadoop为我们提供了5个可编程组件:InputFormatMapperPartitionReducerOutputFormat
  • 另外还有一个 Conbiner 组件,通常同于优化MapReduce程序的性能,但是需要根据具体的业务场景决定是否需要使用。

2)InputFormat组件

InputFormat主要用于描述数据输入的格式,它提供了两个功能:

  1. 数据切分:按照某种规则将输入数据切分成若干个InputSplit,以确定MapTask的个数
  2. 数据输入:为Mapper组件提供数据
  • 什么是输入切片【InputFormat】?

    按照一定的切分规则将物理文件划分为逻辑上的多份文件,即输入切片在物理形式上依然是一个完整的文件,但是在逻辑形式【代码中】上则是一块一块的文件

  • 如何切片?

    public List<InputSplit> getSplits(JobContext job) throws IOException {
        	
        Stopwatch sw = (new Stopwatch()).start();
        // 最小大小 为计算splitSize 做准备
        long minSize = Math.max(this.getFormatMinSplitSize(), getMinSplitSize(job));
        // 最大大小 为计算splitSize 做准备
        long maxSize = getMaxSplitSize(job);
        // 准备存储切片的集合
        List<InputSplit> splits = new ArrayList();
        // 获取所有文件的集合
        List<FileStatus> files = this.listStatus(job);
        // 迭代
        Iterator i$ = files.iterator();
    
        while(true) {
            while(true) {
                while(i$.hasNext()) {
                    // 获取文件状态  (到底是hdfs 上的文件还是本地文件)
                    FileStatus file = (FileStatus)i$.next();
                    //  获取文件路径
                    Path path = file.getPath();
                    // 获取文件长度
                    long length = file.getLen();
                    // 如果文件不为空 则 继续执行
                    if (length != 0L) {
                        //准备存储BlockLocation的数组
    
                        BlockLocation[] blkLocations;
                        // 判断文件是否为本地文件
                        if (file instanceof LocatedFileStatus) {
                            blkLocations = ((LocatedFileStatus)file).getBlockLocations();
                        } else {
                            //获取到fs 对象
                            FileSystem fs = path.getFileSystem(job.getConfiguration());
                            //通过fs 对象获取到文件所有的block的地址(位置)
                            blkLocations = fs.getFileBlockLocations(file, 0L, length);
                        }
    
                        // isSplitable 方法默认可以切分
                        if (this.isSplitable(job, path)) {
                            // i获取到当前的blcoksize大小 一般为128MB
                            long blockSize = file.getBlockSize();
                            /*
                                protected long computeSplitSize(long blockSize, long minSize, long maxSize) {
                                    return Math.max(minSize, Math.min(maxSize, blockSize));
                                }
                                上述代码是计算切片策略的大小
                                */
                            long splitSize = this.computeSplitSize(blockSize, minSize, maxSize);
                            //  准备剩余 的字节数的对象
                            long bytesRemaining;
                            //  blockIndex
                            int blkIndex;
    
                            //  for 循环 剩余大小等于当前的长度  使用剩余大小除切片策略的大小 如果大于1.1 则向下执行 否则退出循环
                            for(bytesRemaining = length; (double)bytesRemaining / (double)splitSize > 1.1D; bytesRemaining -= splitSize) {
                                // 剩余大小除切片策略的大小 大于1.1 
                                blkIndex = this.getBlockIndex(blkLocations, length - bytesRemaining);
                                // 给定文件路径 ,文件开始的位置  文件长度    如果剩余长度除以splitSize 大于1.1 则进入到次循环
                                splits.add(this.makeSplit(path, length - bytesRemaining, splitSize, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts()));
                            }
                            //  如果剩余长度除以splitSize 小于1.1 则进入到判断语句
                            if (bytesRemaining != 0L) {
                                blkIndex = this.getBlockIndex(blkLocations, length - bytesRemaining);
                                // 给定文件路径 ,文件开始的位置  文件长度    如果剩余长度除以splitSize 大于1.1 则进入到次循环
                                splits.add(this.makeSplit(path, length - bytesRemaining, bytesRemaining, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts()));
                            }
                        } else {
                            splits.add(this.makeSplit(path, 0L, length, blkLocations[0].getHosts(), blkLocations[0].getCachedHosts()));
                        }
                    } else {
                        splits.add(this.makeSplit(path, 0L, length, new String[0]));
                    }
                }
    
                job.getConfiguration().setLong("mapreduce.input.fileinputformat.numinputfiles", (long)files.size());
                sw.stop();
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Total # of splits generated by getSplits: " + splits.size() + ", TimeTaken: " + sw.elapsedMillis());
                }
                return splits;
            }
        }
    }
    
  • 如何为Mapper提供数据?

    TextInputFormat内部使用RecordReader中 org.apache.hadoop.mapreduce.lib.input.LineRecordReader 类的方法。首先调用初始化方法获取切片的初始化位置和结束位置,然后打开文件输入流,使用 LineRecordReader.nextKeyValue() 方法获取一个Key-Value对,Key被设置成当前文本的偏移量,Value被设置为 LineRecordReader.readDefaultLine() 方法读到的每一行文本。

  • 切片数量和MapTask的关系

    MapTask的数量由切片的数量决定,有多少个切片就会启动多少个MapTask。

    ReduceTask的数量默认为1,可以通过Job对象的 setNumReduceTasks() 方法手动设置。

  • 常用的InputFormat

    1. FileInputFormat 【读HDFS】

      • TextInputFormat

        key : 字节偏移量

        value : 当前行文本

        切片 : 以文件为单位,按照splitSize划分

      • NLineInputFormat

        key : 字节偏移量

        value : 当前行文本

        切片 : 以文件为单位,n行一个切片,默认一行一个切片 可以设置

        // 指定输入路径
        NLineInputFormat.setInputPaths(job, new Path("D:\\大数据训练营课程\\Note\\Day02-Hadoop\\数据文件\\flow.dat"));
        // 指定Job对象以及按几行切分
        NLineInputFormat.setNumLinesPerSplit(job,3);
        
      • CombineTextInputFormat

        key : 字节偏移量

        value : 当前行文本

        切片 : 按照splitSize进行切分,一个切片可能对应多个文件

        // 指定输入路径
        CombineTextInputFormat.setInputPaths(job, new Path("D:\\大数据训练营课程\\Note\\Day02-Hadoop\\数据文件\\littlewenjian"));
        // 指定文件的切分大小
        CombineTextInputFormat.setMinInputSplitSize(job,1024000);
        
    2. DBInputFormat 【读RDBMS】

      编写实体类

      import org.apache.hadoop.io.Writable;
      import org.apache.hadoop.mapreduce.lib.db.DBWritable;
      
      import java.io.DataInput;
      import java.io.DataOutput;
      import java.io.IOException;
      import java.sql.PreparedStatement;
      import java.sql.ResultSet;
      import java.sql.SQLException;
      
      public class User implements Writable, DBWritable {
      
          private Integer id;
          private String name;
      
          public User() {
          }
      
          public User(Integer id, String name) {
              this.id = id;
              this.name = name;
          }
      
          public Integer getId() {
              return id;
          }
      
          public void setId(Integer id) {
              this.id = id;
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          // 定义序列化方式 编码 【用于计算】
          public void write(DataOutput dataOutput) throws IOException {
              dataOutput.writeInt(this.id);
              dataOutput.writeUTF(this.name);
          }
      
          // 定义反序列化方式  解码	【用于计算】
          public void readFields(DataInput dataInput) throws IOException {
              this.id = dataInput.readInt();
              this.name = dataInput.readUTF();
          }
      
          // 定义序列化方式 编码 【用于向数据库写入】
          public void write(PreparedStatement pstm) throws SQLException {
              pstm.setInt(1, this.id);
              pstm.setString(2, this.name);
          }
      
          // 定义反序列化方式  解码	【用于读取数据库】
          public void readFields(ResultSet rs) throws SQLException {
              this.id = rs.getInt(1);
              this.name = rs.getString(2);
          }
      
          @Override
          public String toString() {
              return "User{" +
                      "id=" + id +
                      ", name='" + name + '\'' +
                      '}';
          }
      }
      

      编写Map逻辑

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

      编写Reduce逻辑、

      import org.apache.hadoop.io.LongWritable;
      import org.apache.hadoop.io.Text;
      import org.apache.hadoop.mapreduce.Reducer;
      
      import java.io.IOException;
      
      public class DBReducer extends Reducer<LongWritable, Text, LongWritable, Text> {
      
          @Override
          protected void reduce(LongWritable key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
              for (Text value : values) {
                  context.write(key, value);
              }
          }
      }
      

      编写Job类

      package com.baizhi.mr.test05;
      
      import org.apache.hadoop.conf.Configuration;
      import org.apache.hadoop.fs.Path;
      import org.apache.hadoop.io.LongWritable;
      import org.apache.hadoop.io.Text;
      import org.apache.hadoop.mapred.lib.db.DBConfiguration;
      import org.apache.hadoop.mapred.lib.db.DBInputFormat;
      import org.apache.hadoop.mapreduce.Job;
      import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
      import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
      
      public class DBJob {
          public static void main(String[] args) throws Exception {
      
              Configuration conf = new Configuration();
              // 配置连接的要素
              DBConfiguration.configureDB(conf, "com.mysql.jdbc.Driver", "jdbc:mysql://localhost:3306/hadoop", "root", "1234");
      
              Job job = Job.getInstance(conf);
      		// 设置为DBInputFormat类型的输入组件
              job.setInputFormatClass(DBInputFormat.class);
              job.setOutputFormatClass(TextOutputFormat.class);
      		
              // 参数一:Job对象
              // 参数二:自定义的实体类类对象
              // 参数三:查询数据的SQL语句
              // 参数四:查询数据的总条数
              DBInputFormat.setInput(job, User.class, "select id,name from user", "select count(1) from user");
              
              TextOutputFormat.setOutputPath(job, new Path("D:\\大数据训练营课程\\Code\\BigData\\Hadoop_Test\\src\\main\\java\\com\\baizhi\\mr\\test05\\out1"));
      
              job.setMapperClass(DBMapper.class);
              job.setReducerClass(DBReducer.class);
      
              job.setMapOutputKeyClass(LongWritable.class);
              job.setMapOutputValueClass(Text.class);
      
              job.setOutputKeyClass(LongWritable.class);
              job.setOutputValueClass(Text.class);
      
              job.waitForCompletion(true);
          }
      }
      

      注意:

      • 本地提交模式下需要在POM文件中加入SQL驱动依赖
      • Jar包提交模式下需要将SQL的驱动Jar包上传到 $HADOOP_HOME/share/hadoop/yarn/ 目录下
    3. TableInputFormat 【读HBase】

    4. 自定义InputFormat

      可用于解决HDFS中小文件的存储问题,将多个小文件合并成一个SequenceFile

      SequenceFile : 是Hadoop用来存储二进制式的key-value对的文件格式,SequenceFile的 Key是文件的路径+文件名 Value是文件的内容

      自定义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;
      
      // 继承RecordReader 加入keyIn,valueIn泛型
      public class OwnRecordReader extends RecordReader<Text, BytesWritable> {
          private FileSplit fileSplit;
          private Configuration conf;
      
          private Text key = new Text();
          private BytesWritable value = new BytesWritable();
      
          boolean isProgress = true;
      	
          // 初始化
          public void initialize(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
              // 获得到文件切片对象
              this.fileSplit = (FileSplit) inputSplit;
              // 获取到 Configuration 对象
              conf = taskAttemptContext.getConfiguration();
          }
      
          // 自定义文件读取形式
          public boolean nextKeyValue() throws IOException, InterruptedException {
               
              if (isProgress) {
                  // 设置 存储 文件 内容的二进制数组
                  byte[] bytes = new byte[(int) fileSplit.getLength()];
      
                  // 获取文件路径
                  Path path = fileSplit.getPath();
                  // 获取FS对象
                  FileSystem fileSystem = path.getFileSystem(conf);
                  // 获取文件流对象
                  FSDataInputStream fsDataInputStream = fileSystem.open(path);
                  // 拷贝文件
                  IOUtils.readFully(fsDataInputStream, bytes, 0, bytes.length);
                  // 设置 key 的值为文件路径
                  key.set(path.toString());
                  // 设置 value 的值为文件内容
                  value.set(bytes, 0, bytes.length);
      			// 拷贝完成后将值设置为false 防止读取不到数据而报错
                  isProgress = false;
                  return true;
              }
              return false;
          }
      
          public Text getCurrentKey() throws IOException, InterruptedException {
              return this.key;
          }
      
          public BytesWritable getCurrentValue() throws IOException, InterruptedException {
              return this.value;
          }
      
          public float getProgress() throws IOException, InterruptedException {
              return 0;
          }
      
          public void close() throws IOException {
      
          }
      }
      

      自定义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 OwnInputFormat extends FileInputFormat<Text, BytesWritable> {
      
          @Override
          protected boolean isSplitable(JobContext context, Path filename) {
             	// 此方法判断是否对文件进行分割 直接返回false 不允许分割
              return false;
          }
      	
          public RecordReader<Text, BytesWritable> createRecordReader(InputSplit inputSplit, TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
              // 创建自定义的RecordReader对象
              OwnRecordReader recordReader = new OwnRecordReader();
              // 调用初始化方法
              recordReader.initialize(inputSplit, taskAttemptContext);
              // 返回RecordReader对象
              return recordReader;
          }
      }
      

      编写Map逻辑

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

      编写Reduce逻辑

      import org.apache.hadoop.io.ByteWritable;
      import org.apache.hadoop.io.BytesWritable;
      import org.apache.hadoop.io.Text;
      import org.apache.hadoop.mapreduce.Reducer;
      
      import java.io.IOException;
      
      public class OwnReducer extends Reducer<Text, BytesWritable, Text, BytesWritable> {
          @Override
          protected void reduce(Text key, Iterable<BytesWritable> values, Context context) throws IOException, InterruptedException {
              for (BytesWritable value : values) {
                  context.write(key, value);
              }
          }
      }
      

      编写Job类

      import com.baizhi.mr.test01.WCJob;
      import com.baizhi.mr.test01.WCMapper;
      import com.baizhi.mr.test01.WCReducer;
      import org.apache.hadoop.conf.Configuration;
      import org.apache.hadoop.fs.Path;
      import org.apache.hadoop.io.BytesWritable;
      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.output.SequenceFileOutputFormat;
      import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
      
      public class OwnJob {
          public static void main(String[] args) throws Exception {
      
              // 1 封装Job 对象
              Configuration conf = new Configuration();
              Job job = Job.getInstance(conf, "WC-JOB01");
              job.setJarByClass(OwnJob.class);
      
              // 2 设置数据的写入和写出格式
              job.setInputFormatClass(OwnInputFormat.class);
              job.setOutputFormatClass(SequenceFileOutputFormat.class);
      
              // 3 设置数据读取和写出路径
              OwnInputFormat.addInputPath(job,new Path("D:\\大数据训练营课程\\Note\\Day02-Hadoop\\数据文件\\littlewenjian"));
              SequenceFileOutputFormat.setOutputPath(job,new Path("D:\\大数据训练营课程\\Code\\BigData\\Hadoop_Test\\src\\main\\java\\com\\baizhi\\mr\\test06\\out4"));
      
              // 4 设置数据的计算逻辑
              job.setMapperClass(OwnMapper.class);
              job.setReducerClass(OwnReducer.class);
      
              // 5 设置Mapper 和Reducer 的输出泛型
              job.setMapOutputKeyClass(Text.class);
              job.setMapOutputValueClass(BytesWritable.class);
      
              job.setOutputKeyClass(Text.class);
              job.setOutputValueClass(BytesWritable.class);
      
              // 6 任务提交
              //job.submit();
              job.waitForCompletion(true);
          }
      }
      

3)Partition组件

MapReduce默认使用HashPartitioner提供的分区方法进行分区,对应的分区策略是 根据Key的哈希值去计算当前键值对属于哪个分区

package org.apache.hadoop.mapreduce.lib.partition;

import org.apache.hadoop.classification.InterfaceAudience.Public;
import org.apache.hadoop.classification.InterfaceStability.Stable;
import org.apache.hadoop.mapreduce.Partitioner;

public class HashPartitioner<K, V> extends Partitioner<K, V> {
    public HashPartitioner() {
    }
	
    // 给定分区方式
    public int getPartition(K key, V value, int numReduceTasks) {
        return (key.hashCode() & 2147483647) % numReduceTasks;
    }
}

自定义分区方式及分区数量

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

import java.util.HashMap;

public class OwnPartatiner extends Partitioner<Text, FlowBean> {

    private static HashMap<String, Integer> areaMap = new HashMap<String, Integer>();

    static {
        areaMap.put("hunan", 0);
        areaMap.put("xy", 1);
        areaMap.put("kf", 2);
        areaMap.put("bj", 3);
        areaMap.put("zz", 4);
    }
	
   	// 自定义分区方式 实现将不同地区的数据写入不同的文件
    public int getPartition(Text key, FlowBean value, int i) {
        return areaMap.get(key.toString())==null ? 0 :areaMap.get(key.toString());
    }
}
// Job类中需要指定Partitioner的类对象
job.setPartitionerClass(OwnPartatiner.class);

// Job类中需要设置合理的ReduceTask的数量 数量必须大于等于分区数 例如上面分区会返回5个值 那数量必须大于等于5
// 多余的ReduceTask依然会产生结果文件 但是结果文件中没有数据
job.setNumReduceTasks(100);

4)OutputFormat组件

自定义OutputFormat

编写自定义RecordWriter类

import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;

import java.io.IOException;

public class OenRecordWriter extends RecordWriter<LongWritable, Text> {
    private FSDataOutputStream outputStream;

    public OenRecordWriter(TaskAttemptContext context) throws Exception {
        // 获取FS对象
        FileSystem fileSystem = FileSystem.get(context.getConfiguration());
		// 定义数据文件输出目录
        outputStream = fileSystem.create(new Path("D:\\大数据训练营课程\\Code\\BigData\\Hadoop_Test\\src\\main\\java\\com\\baizhi\\mr\\test08\\out1\\1.txt"));
    }

    public void write(LongWritable longWritable, Text text) throws IOException, InterruptedException {
		// 自定义写出方式
        outputStream.write((text.toString()+"\n").getBytes());
    }

    public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
		// 关闭流
        IOUtils.closeStream(outputStream);
    }
    
}

编写自定义OutputFormat类

import org.apache.hadoop.io.LongWritable;
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;
// 继承FileOutputFormat类
public class OwnOutputFormat extends FileOutputFormat<LongWritable, Text> {
    // 提供获取RecordWriter对象
    public RecordWriter<LongWritable, Text> getRecordWriter(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
        try {
            return new OenRecordWriter(taskAttemptContext);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

编写Map逻辑

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

import java.io.IOException;

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

编写Reduce逻辑

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

import java.io.IOException;

public class MyReducer extends Reducer<LongWritable, Text, LongWritable, Text> {

    @Override
    protected void reduce(LongWritable key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
        for (Text value : values) {
            context.write(key, value);
        }
    }
}

编写Job类

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;

public class MyJob {
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf);

        job.setJarByClass(MyJob.class);

        job.setInputFormatClass(TextInputFormat.class);
        // 自定义OutputFormat组件
        job.setOutputFormatClass(OwnOutputFormat.class);

        TextInputFormat.setInputPaths(job, new Path("D:\\大数据训练营课程\\Note\\Day02-Hadoop\\数据文件\\flow.dat"));
        
        // 指定校验文件的输出路径 数据文件的输出目录在组件中已经指定
        OwnOutputFormat.setOutputPath(job, new Path("D:\\大数据训练营课程\\Note\\Day02-Hadoop\\数据文件\\asdjk1ha1"));

        job.setMapperClass(MyMapper.class);
        job.setReducerClass(MyReducer.class);

        job.setMapOutputKeyClass(LongWritable.class);
        job.setMapOutputValueClass(Text.class);

        job.setOutputKeyClass(LongWritable.class);
        job.setOutputValueClass(Text.class);


        job.waitForCompletion(true);
    }
}

5)Combiner组件

  • Combiner是MapReduce程序中Mapper和Reducer之外的一种组件

  • Combiner组件的父类就是Reducer

  • Combiner和Reducer的区别在于运行的位置不同

    Combiner是在每一个MapTask所在的节点上运行【运行在MapTask做局部汇总】
    Reducer是在接收全局所有的Mapper的输出结果后运行【运行在分区后的每个ReduceTask上做聚合操作】
    
  • Combiner的意义在于对每一个MapTask的输出结果做局部汇总,用以减少数据传输过程中的网络流量消耗

  • Combiner的使用可能会影响到最终的业务结果 谨慎使用

  • Combiner输出的K,V类型要与Reduce接受的KV对应

  • 使用方式

    方式一 : 自定义类继承Reducer

    方式二 : 直接使用Reducer逻辑

    // 在Job类中指定Combiner的类即可
    job.setCombinerClass(WCReducer.class);
    

八、Shuffle

在这里插入图片描述

为了让Reduce能够并行处理Map的结果,需要对Map的输出结果进行一定的分区(Partition)、排序(sort)、合并(Combine)、归并(Merge)等操作,进而得到一个<key,value-list>类型的中间结果,再交给Reduce处理,这个过程就叫做Shuffle,即从无序的<key,value>变为有序的<key,value-list>。

Shuffle过程是MapReduce的核心,描述着数据从MapTask输出【key,value】到ReduceTask输入【key,value-list】的这段过程。

由于在大数据的处理过程中,采用的是集群计算模式,不同节点之间进行并行计算,在计算过程中一定会出现节点与节点之间传输数据的情况,此时,数据传输的速度直接影响着整个应用程序的运行效率,而MapReduce框架采用以磁盘溢写文件作为数据中间结果的存储介质,也会影响计算的效率。

因此,Shuffle的过程应该尽可能的满足以下几点要求:

  • 完整的从MapTask拉取数据到ReduceTask
  • 在拉取数据的过程中,尽可能的减少网络资源的消耗
  • 尽可能的减少磁盘IO对于计算效率的影响

Shuffle分为2部分:Map端的Shuffle、Reduce端的Shuffle,二者共同完成对Map端结果进行分区、排序、合并等处理并交由Reduce端的过程

1)Map端的Shuffle

在这里插入图片描述

# Map端的Shuffle过程
1. 将map()方法的输出结果缓存到内存中(环状缓冲区),默认情况下当大小达到缓冲区大小【100MB】的80%时,则会将缓冲区中的数据溢写到磁盘中
2. 在溢写到磁盘之前,要对数据进行分区和排序,之后再写入磁盘,每次溢写操作都会产生新的磁盘文件
3. 随着MapTask的运行,会不断的产生很多的小文件,最终当Map端计算结束之后,这些小文件会被合并成大文件 【key,value-list】
4. 最后通知响应的ReduceTask领取属于自己分区的数据

通过修改 mapred-default.xml 文件可以控制缓冲区的大小及溢写的阈值

<!-- 设置环状缓冲区的大小 单位:MB -->
</property> 
	<name>mapreduce.task.io.sort.mb</name>
	<value>100</value>
</property>

<!-- 设置溢写的阈值 不超过1.0 -->
<property>
    <name>mapreduce.map.sort.spill.percent</name>
    <value>0.80</value>
</property>

2)Reduce端的Shuffle

在这里插入图片描述

Reduce端Shuffle的主要作用 :

  • 领取Map节点的数据
  • 对领取的数据进行归并处理
  • 将数据输入给Reduce任务

九、MapReduce优化

1. 小文件计算可以使用CombineTextInputFormat组件干预切片的计算逻辑
2. 自定义分区策略【Partition】,防止数据倾斜
3. 适当的调整溢写缓冲区的大小阈值
4. 适当的调用合并文件并行度 【mapreduce.task.io.sort.factor】
5. 对Map端输出溢写文件使用GZIP策略压缩,节省网络带宽

使用GZIP的方法

<!-- mapred-default.xml -->
<!-- 规定在map输出前对文件进行压缩 -->
<property>
    <name>mapreduce.map.output.compress</name>
    <value>false</value>
</property>

<!-- 设定压缩策略 -->
<property>
    <name>mapreduce.map.output.compress.codec</name>
    <value>org.apache.hadoop.io.compress.DefaultCodec</value>
</property>
// Job类中添加配置
conf.setClass("mapreduce.map.output.compress.codec", GzipCodec.class, CompressionCodec.class);
conf.set("mapreduce.map.output.compress","true");

十、总结

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值