【Hadoop】Hadoop3 MapReduce 基础

文章整理自尚硅谷B站视频:https://www.bilibili.com/video/BV1Qp4y1n7EN?t=7&p=39

文章目录

一、MapReduce概述

MapReduce是分布式运算程序的编程框架

  • 核心功能:将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,运行在一个Hadoop集群上
  • 优点:
    • 易于编程:用户只关心业务逻辑,实现框架接口即可
    • 良好的扩展性:可以动态增加服务器,解决计算资源不够的问题
    • 高容错性:任何服务器挂掉,可以将任务转移到其他节点
    • 适合海量数据(TB/PB)的计算,可以几千台服务器共同计算
  • 缺点:
    • 不擅长实时计算:不能像MySQL达到毫秒级计算
    • 不擅长流式计算:Spark、Flink擅长
    • 不擅长DAG有向无环图计算(多job串联):Spark擅长

1.1 MR核心思想

  • MR运算程序一般需要分成2个阶段:Map阶段和Reduce阶段
  • Map阶段的MapTask,完全并行运行,互不相干
  • Reduce阶段的并发ReduceTask,完全互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出
  • MR编程模型只能包含一个Map阶段和一个Reduce阶段
    • 如果用户的业务逻辑诶长复杂,那就只能多个MR程序,串行运行

1.2 MR进程

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

  • MrAppMaster:负责整个程序的过程调度及状态协调
  • MapTask:负责Map阶段的整个数据处理流程
  • ReduceTask:负责Reduce阶段的整个数据处理流程

1.3 常用数据序列化类型

  • String变为Text外,其余均在Java类型后面加Writable

1.4 MR 编程规范

  • Mapper阶段
    • 用户自定义的Mapper要继承自己的父类
    • Mapper的输入数据是K-V对的形式(K-V的类型可自定义,K是偏移量,V是对应行的内容)
    • Mapper中的业务逻辑写在map()方法中
    • Mapper的输出数据是K-V对的形式(K-V的类型可自定义)
    • map()方法(MapTask进程)对每一个<K, V>调用一次(对每一行调用一次)
  • Reducer阶段
    • 用户自定义的Reducer要继承自己的父类
    • Reducer的输入数据类型对应Mapper的输出数据类型,也是K-V
    • Reducer的业务逻辑写在reduce()方法中
    • ReduceTask进行对每一组相同K的<K, V>组调用一次reduce()方法
  • Driver阶段
    • 相当于YARN集群的客户端,用户提交整个程序到YARN集群,提交的是封装了MR程序相关运行参数的job对象

1.5 WordCount 案例

1.5.1 分析

  • Mapper
    • 将MapTask传来的文本内容先转换成String
    • 根据空格将一行切分为单词
    • 将单词输出为:<单词, 1>
  • Reducer
    • 汇总各个key的个数
    • 输出该key的总次数
  • Driver
    • 获取配置信息,获取job对象实例
    • 指定本程序jar包所在的本地路径
    • 关联Mapper/Reducer业务类
    • 指定Mapper输出数据的KV类型
    • 指定最终输出数据的KV类型(不是Reducer的输出类型)
    • 指定job的输入原始文件所在目录
    • 指定job的输出结果所在目录(不能提前存在)
    • 提交作业

1.5.2 代码

1.5.2.1 Mapper
package YARN.WordCount;

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;

/**
 * 2.x和3.x导入 org.apache.hadoop.mapreduce.Mapper
 *
 * KEYIN: map阶段输入key的类型:LongWritable
 * VALUEIN: map阶段输入value的类型:Text
 * KEYOUT: map阶段输出key的类型:Text
 * VALUEOUT: map阶段输出value的类型:IntWritable
 */
public class WCMapper extends Mapper<LongWritable, Text, Text, IntWritable> {

	// 减少new的次数
	private Text outKey = new Text();
	private IntWritable outValue = new IntWritable(1);

	@Override
	protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
		// 1 获取一行
		String line = value.toString();
		// 2 切分
		String[] words = line.split(" ");
		// 3 迭代写出
		for (String word : words) {
			// 封装
			outKey.set(word);
			// 写出
			context.write(outKey, outValue);
		}
	}
}

1.5.2.2 Reducer
package YARN.WordCount;

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

import java.io.IOException;

/**
 * 2.x和3.x导入 org.apache.hadoop.mapreduce.Reducer
 *
 * KEYIN: reduce阶段输入key的类型:Text
 * VALUEIN: reduce阶段输入value的类型:IntWritable
 * KEYOUT: reduce阶段输出key的类型:Text
 * VALUEOUT: reduce阶段输出value的类型:IntWritable
 */
public class WCReducer extends Reducer<Text, IntWritable, Text, IntWritable> {

	private IntWritable outValue = new IntWritable();

	@Override
	protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {

		int sum = 0;

		// 累加
		for (IntWritable value : values) {
			sum += value.get();
		}

		// 封装
		outValue.set(sum);
		// 写出
		context.write(key, outValue);
	}
}

1.5.2.3 Driver
package YARN.WordCount;

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

import java.io.IOException;

public class WCDriver {

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

		// 1 获取job
		Configuration conf = new Configuration();
		Job job = Job.getInstance(conf);

		// 2 设置jar包路径
		job.setJarByClass(WCDriver.class);

		// 3 关联Mapper和Reducer
		job.setMapperClass(WCMapper.class);
		job.setReducerClass(WCReducer.class);

		// 4 设置map输出的K-V类型
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(IntWritable.class);

		// 5 设置最终输出的K-V类型
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(IntWritable.class);

		// 6 设置输入路径和输出路径(导包导长的)
		FileInputFormat.setInputPaths(job, new Path("/wcinput/word.txt"));
		FileOutputFormat.setOutputPath(job, new Path("/output1"));

		// 7 提交job
		boolean result = job.waitForCompletion(true);

		System.exit(result ? 0 : 1);
	}
}

1.5.2.4 执行
  • 编辑好 Artifact 后 Build 打包,上传后运行:

    hadoop jar ./HadoopStu.jar YARN.WordCount.WCDriver

  • 运行后查看统计结果:

    hdfs dfs -cat /output1/*

二、Hadoop 序列化

2.1 序列化概述

  • 什么是序列化
    • 序列化就是把内存中的对象,装换成字节序列(或其他数据传输协议),以便于存储到磁盘(持久化)和网络传输
    • 反序列化就是将收到的字节序列或者磁盘的持久化数据,转换成内存中的对象
  • Hadoop 序列化的优点
    • Java Serializable 包含各种校验信息和继承体系,很重量级
    • Hadoop 序列化,只有简单的校验
      • 紧凑:存储空间小
      • 快速:传输速度快
      • 互操作性:支持多语言的使用

2.2 自定义序列化步骤

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

实现bean对象序列化的步骤:

  • 实现Writable接口
  • 定义空参构造
  • 重写序列化方法write()
    • 将所需数据依次写入
  • 重写反序列化方法readFields()
    • 按照序列化的顺序依次读取
    • 反序列化时,需要反射调用空参构造函数,所以必须有空参构造
  • 注意反序列化的顺序和序列化的顺序完全一致
  • 要想把结果显示在文件中,需要重写toString()方法
  • 如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,因为MapReduce框中的Shuffle过程要求对key必须能排序

2.3 序列化案例

2.3.1 需求

  • 输入数据:id,手机号码,IP,上行流量,下行流量,总流量,网络状态码
1,13726230503,120.196.100.82,2481,24681,200
2,13826544101,120.197.40.4,264,0,200
3,13926435656,120.196.100.99,132,1512,200
4,13926251106,120.197.40.4,240,0,200
5,18211575961,120.196.100.99,1527,2106,200
6,18584138413,120.197.40.4,4116,1432,200
7,13560439658,120.196.100.99,1116,954,200
8,15920133257,120.197.40.4,3156,2936,200
9,13719199419,120.196.100.82,240,0,200
10,13660577991,120.197.40.4,6960,690,200
11,15013685858,120.197.40.4,3659,3538,200
12,15989002119,120.196.100.99,1938,180,200
13,13560439658,120.196.100.99,918,4938,200
14,13480253104,120.197.40.4,180,180,200
15,13602846565,120.197.40.4,1938,2910,200
16,13922314466,120.196.100.82,3008,3720,200
17,13502468823,120.196.100.99,7335,110349,200
18,18320173382,120.196.100.99,9531,2412,200
19,13925057413,120.196.100.55,11058,48243,200
20,13760778710,120.196.100.82,120,120,200
21,13560436666,120.196.100.82,2481,24681,200
22,13560436666,120.196.100.99,1116,954,200
  • 期望输出:手机号,总上行流量,总下行流量,总流量

2.3.2 编码

2.3.2.1 Bean
package YARN.PhoneData;

import org.apache.hadoop.io.Writable;

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

public class Phone implements Writable {

	private long upFlow;
	private long downFlow;
	private long sumFlow;

	// 空参构造
	public 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;
	}

	public void setSumFlow() {
		this.sumFlow = this.upFlow + this.downFlow;
	}

	// 序列化
	public void write(DataOutput dataOutput) throws IOException {
		dataOutput.writeLong(upFlow);
		dataOutput.writeLong(downFlow);
		dataOutput.writeLong(sumFlow);
	}

	// 反序列化
	public void readFields(DataInput dataInput) throws IOException {
		this.upFlow = dataInput.readLong();
		this.downFlow = dataInput.readLong();
		this.sumFlow = dataInput.readLong();
	}

	@Override
	public String toString() {
		return upFlow+"\t"+downFlow+"\t"+sumFlow;
	}
}

2.3.2.2 Mapper
package YARN.PhoneData;

import org.apache.hadoop.io.Writable;

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

public class Phone implements Writable {

	private long upFlow;
	private long downFlow;
	private long sumFlow;

	// 空参构造
	public 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;
	}

	public void setSumFlow() {
		this.sumFlow = this.upFlow + this.downFlow;
	}

	// 序列化
	public void write(DataOutput dataOutput) throws IOException {
		dataOutput.writeLong(upFlow);
		dataOutput.writeLong(downFlow);
		dataOutput.writeLong(sumFlow);
	}

	// 反序列化
	public void readFields(DataInput dataInput) throws IOException {
		this.upFlow = dataInput.readLong();
		this.downFlow = dataInput.readLong();
		this.sumFlow = dataInput.readLong();
	}

	@Override
	public String toString() {
		return upFlow+"\t"+downFlow+"\t"+sumFlow;
	}
}

2.3.2.3 Reducer
package YARN.PhoneData;

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

import java.io.IOException;

public class PhoneReducer extends Reducer<Text, Phone, Text, Phone> {

	private Phone outV = new Phone();

	@Override
	protected void reduce(Text key, Iterable<Phone> values, Context context) throws IOException, InterruptedException {

		long totalUp = 0;
		long totalDown = 0;

		for (Phone phone : values) {
			totalUp += phone.getUpFlow();
			totalDown += phone.getDownFlow();
		}

		outV.setUpFlow(totalUp);
		outV.setDownFlow(totalDown);
		outV.setSumFlow();

		context.write(key, outV);
	}

}

2.3.2.4 Driver
package YARN.PhoneData;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

public class PDriver {

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

		Job job = new Job(new Configuration());

		job.setJarByClass(PDriver.class);

		job.setMapperClass(PhoneMapper.class);
		job.setReducerClass(PhoneReducer.class);

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

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

		FileInputFormat.setInputPaths(job, new Path("/input/phone_data.txt"));
		FileOutputFormat.setOutputPath(job, new Path("/output2"));

		boolean b = job.waitForCompletion(true);

		System.exit(b ? 0 : 1);
	}
}

2.3.2.5 测试运行

编辑好 Artifact 后,打包,提交运行:hadoop jar ./HadoopStu-P.jar YARN.PhoneData.PDriver

运行后cat查看统计结果:

三、MapReduce 核心框架原理

3.1 InputFormat 输入数据

3.1.1 切片与MapTask 并行度决定机制

  • 数据块:block是HDFS物理上把数据分成一块一块的,数据块是HDFS的存储数据单位
  • 数据切片:数据切片知识在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储,数据切片是MR程序阶段输入数据的单位,一个切片会对应启动一个MapTask
  • 默认情况下:切片大小=BlockSize

3.1.2 Job 提交流程和切片流程

3.1.2.1 Job 提交
  • waitForCompletion()
  • submit()
  • 建立连接
    • 创建提交Job的代理
      • 判断是本地运行环境还是YARN集群运行环境
  • 提交job
    • 创建给集群提交数据的Stag路径
    • 获取jobid,并创建Job路径
    • 拷贝jar包到集群
    • 计算切片,生成切片规划文件
    • 向Stag路径写XML配置文件
    • 提交Job,返回提交状态
3.1.2.2 切片

FileInputFormat切片源码:

  • 程序先找到数据存储的目录
  • 开始遍历处理(规划切片)目录下的每一个文件
  • 遍历第一个文件
    • 获取文件大小
    • 计算切片大小
      • computeSplitSize(Math.max(minSize, Math.min(maxSize, blocksize))) = blocksize = 128M
    • 默认情况下,切片大小=blocksize
    • 开始切
      • 形成第一个切片:0:128M
      • 第二个切片:128:256M
      • 第三个切片:256:300M
      • (每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍则只划分一块切片)
    • 将切片信息写入到一个切片规划文件中
    • 整个切片的核心过程在getSplit()方法中完成
    • InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等
  • 提交切片规则到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask的个数

3.1.3 FileInputFormat 切片机制

3.1.3.1 切片机制
  • 简单地按照文件的内容长度进行切片
  • 切片大小默认等于block大小
  • 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
3.1.3.2 FileInputFormat 切片大小的参数配置
  • 源码中计算切片大小的公式
    • Math.max(minSize, Math.min(maxSize, blocksize))
    • mapreduce.input.fileinputformat.split.minsize = 1
    • mapreduce.input.fileinputformat.split.maxsize = Long.MAXValue
    • 因此默认情况下,切片大小=blocksize
  • 切片大小设置
    • maxsize(切片最大值):参数如果调得比block小,则会让切片变小,而且就等于配置的这个参数的值
    • minsize(切片最小值):参数调得比blocksize大,则可以让切片变得比blocksize大
  • 获取切片信息的API
    • 获取切片的文件名称
      • String name = inputSplit.getPath().getName();
    • 根据文件类型获取切片信息
      • FileSplit inputSplit = (FileSplit) context.getInputSplit();

3.1.4 TextInputFormat

  • FileInputFormat 的实现类
    • FileInputFormat 常见的接口实现类包括:TextInputFormat, KeyValueInputFormat, NLineInputFormat, CombineInputFormat 和自定义InputFormat等
  • TextInputFormat
    • 默认的FileInputFormat实现类,按行读取每条记录。
    • 键是存储该行在整个文件中的起始字节偏移量,LongWritable类型
    • 值是这一行的内容,不包括任何终止符(换行或回车),Text类型

3.1.5 CombineTextInputFormat

默认框架的TextInputFormat切片机制是对任务按照文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量的小文件,就会产生大量的MapTask,处理效率极其低下

  • 应用场景:用于小文件过多的场景。

    • 它可以将多个小文件从逻辑上规划到一个切片中,这样多个小文件就可以交给一个MapTask处理
  • 虚拟存储切片的最大值设置

    • CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); //4M
  • 切片机制

    • 生成切片的过程包括:虚拟存储过程和切片过程两部分

    • 虚拟存储过程:

      • 将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值相比较

        • 如果不大于设置的最大值,逻辑上划分为一个块;

        • 如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;

        • 当剩余数据大小超过设置的最大值且不大于最大值的两倍,此时将文件均分成两个虚拟存储块(均分是为了防止出现太小的切片)

          (例如:setMaxInputSplitSize值为4M,输入文件大小为8.02M,则先逻辑上分成一个4M,剩余的大小为4.02M,将剩余的4.02M均分为两个2.01M的文件)

    • 切片过程:

      • 判断虚拟存储的文件大小是否大于setMaxInputSplit值,大于等于则单独形成一个切片

      • 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片

        (例如:有4个小文件:1.7M,5.1M,3.4M,6.8M,则虚拟存储后形成6个文件块:1.7M,2.55M+2.55M,3.4M,3.4M+3.4M。最终会形成3个切片:1.7M+2.55M,2.55M+3.4M,3.4M+3.4M)

3.2 MapReduce 工作流程

  • 8 中快排:指对key的索引进行快排(按照字典顺序)

3.3 Shuffle

3.3.1 Shuffle 机制

Map方法之后,Reduce方法之前的数据处理过程称为Shuffle(混洗),若没有Reduce阶段,则没有Shuffle阶段

3.3.2 Partition 分区

  • 默认Partitioner分区
    • 默认分区是根据key的hashCode对ReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区
  • 自定义Partitioner步骤
    • 自定义类继承Partitioner,重写getPartition()方法
    • 在Job驱动中,设置自定义Partitioner job.setPartitionerClass(CustomPatitioner.class)
    • 自定义Partition后,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask job.setNumReduceTasks(5)

3.3.3 Partition 分区案例

数据文件:

1,13726230503,120.196.100.82,2481,24681,200
2,13826544101,120.197.40.4,264,0,200
3,13926435656,120.196.100.99,132,1512,200
4,13926251106,120.197.40.4,240,0,200
5,18211575961,120.196.100.99,1527,2106,200
6,18584138413,120.197.40.4,4116,1432,200
7,13560439658,120.196.100.99,1116,954,200
8,15920133257,120.197.40.4,3156,2936,200
9,13719199419,120.196.100.82,240,0,200
10,13660577991,120.197.40.4,6960,690,200
11,15013685858,120.197.40.4,3659,3538,200
12,15989002119,120.196.100.99,1938,180,200
13,13560439658,120.196.100.99,918,4938,200
14,13480253104,120.197.40.4,180,180,200
15,13602846565,120.197.40.4,1938,2910,200
16,13922314466,120.196.100.82,3008,3720,200
17,13502468823,120.196.100.99,7335,110349,200
18,18320173382,120.196.100.99,9531,2412,200
19,13925057413,120.196.100.55,11058,48243,200
20,13760778710,120.196.100.82,120,120,200
21,13560436666,120.196.100.82,2481,24681,200
22,13560436666,120.196.100.99,1116,954,200

需求:按照手机号前三位分区

3.3.3.1 Partitioner 类
package YARN.Partitioner;

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

public class PPartitioner extends Partitioner<Text, Phone> {
	public int getPartition(Text text, Phone phone, int i) {

		String p = text.toString();
		String prefix = p.substring(0, 3);

		int partition;

		if ("136".equals(prefix)){
			partition = 0;
		}else if("137".equals(prefix)){
			partition = 1;
		}else if("138".equals(prefix)){
			partition = 2;
		}else if("139".equals(prefix)){
			partition = 3;
		}else{
			partition = 4;
		}

		return partition;
	}
}
3.3.3.2 Driver 类中增加代码
		job.setPartitionerClass(PPartitioner.class);
		// 分区数和 ReduceTask 个数相同
		job.setNumReduceTasks(5);
3.3.3.3 测试运行

其余类的代码与 2.3 相同,打包上传后运行:

hadoop jar ./HadoopStu-PP.jar YARN.Partitioner.PDriver

运行结果(按照5个分区存储结果):

[hadoop@node102 jar]$ hdfs dfs -ls /output3
Found 6 items
-rw-r--r--   3 hadoop supergroup          0 2021-05-04 18:57 /output3/_SUCCESS
-rw-r--r--   3 hadoop supergroup         53 2021-05-04 18:56 /output3/part-r-00000
-rw-r--r--   3 hadoop supergroup         75 2021-05-04 18:57 /output3/part-r-00001
-rw-r--r--   3 hadoop supergroup         22 2021-05-04 18:57 /output3/part-r-00002
-rw-r--r--   3 hadoop supergroup        105 2021-05-04 18:56 /output3/part-r-00003
-rw-r--r--   3 hadoop supergroup        273 2021-05-04 18:57 /output3/part-r-00004
[hadoop@node102 jar]$ hdfs dfs -cat /output3/*
2021-05-04 18:58:56,146 INFO sasl.SaslDataTransferClient: SASL encryption trust check: localHostTrusted = false, remoteHostTrusted = false
13602846565     1938    2910    4848
13660577991     6960    690     7650
13719199419     240     0       240
13726230503     2481    24681   27162
13760778710     120     120     240
13826544101     264     0       264
13922314466     3008    3720    6728
13925057413     11058   48243   59301
13926251106     240     0       240
13926435656     132     1512    1644
13480253104     180     180     360
13502468823     7335    110349  117684
13560436666     3597    25635   29232
13560439658     2034    5892    7926
15013685858     3659    3538    7197
15920133257     3156    2936    6092
15989002119     1938    180     2118
18211575961     1527    2106    3633
18320173382     9531    2412    11943
18584138413     4116    1432    5548
[hadoop@node102 jar]$ hdfs dfs -cat /output3/part-r-00000
2021-05-04 18:59:18,347 INFO sasl.SaslDataTransferClient: SASL encryption trust check: localHostTrusted = false, remoteHostTrusted = false
13602846565     1938    2910    4848
13660577991     6960    690     7650

3.3.4 分区数和 Reduce 个数总结

  • ReduceTask数 > getPartition的结果数,则会多产生几个空的输出文件part-r-000xxx
  • ReduceTask数 < getPartition的结果数,则有一部分分区数据无处安放,会报IO异常
  • ReduceTask数 = 1,则最终只会产生一个结果文件part-r-00000
  • 分区号从0开始,逐一累加

3.3.5 WritableComparable 排序

  • MapTask和ReduceTask均会对数据按照key进行排序,因此key必须可排序
  • 该操作属于Hadoop的默认行为,任何应用程序中的数据均会被排序,而不管逻辑上是否需要。
  • 默认排序是按照字典顺序排序,且实现方法是快速排序
3.3.5.1 Map 阶段:
  • 对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序
3.3.5.2 Reduce 阶段:
  • 对于ReduceTask,它从每个MapTask上远程拷贝(拉取)相应的数据文件,如果文件大小超过一定阈值,则溢写到磁盘上,否则存储在内存中。
  • 如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大的文件
  • 如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。
  • 当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序
3.3.5.3 排序分类:
  • 部分排序
    • MR根据输入记录的键,对数据集进行排序,保证输出的每个文件内部有序
  • 全排序
    • 最终输出结果只有一个文件,且文件内部有序,实现方式是只设置一个ReduceTask。
    • 但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MR所提供的并行架构
  • 辅助排序(GroupingComparator分组)
    • 在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不同)的key进入到同一个reduce方法时,可以采用分组排序
  • 二次排序(自定义排序)
    • 在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序

3.3.6 WritableComparable 排序案例(全排序)

数据文件(之前统计流量产生的文件):

13480253104	180	180	360
13502468823	7335	110349	117684
13560436666	3597	25635	29232
13560439658	2034	5892	7926
13602846565	1938	2910	4848
13660577991	6960	690	7650
13719199419	240	0	240
13726230503	2481	24681	27162
13760778710	120	120	240
13826544101	264	0	264
13922314466	3008	3720	6728
13925057413	11058	48243	59301
13926251106	240	0	240
13926435656	132	1512	1644
15013685858	3659	3538	7197
15920133257	3156	2936	6092
15989002119	1938	180	2118
18211575961	1527	2106	3633
18320173382	9531	2412	11943
18584138413	4116	1432	5548

需求:按照总流量倒序排序

3.3.6.1 Bean 类

实现WritableComparable接口:

package YARN.Comparable;

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

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

public class Phone implements WritableComparable<Phone> {

	private long upFlow;
	private long downFlow;
	private long sumFlow;

	// 空参构造
	public 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;
	}

	public void setSumFlow() {
		this.sumFlow = this.upFlow + this.downFlow;
	}

	// 序列化
	public void write(DataOutput dataOutput) throws IOException {
		dataOutput.writeLong(upFlow);
		dataOutput.writeLong(downFlow);
		dataOutput.writeLong(sumFlow);
	}

	// 反序列化
	public void readFields(DataInput dataInput) throws IOException {
		this.upFlow = dataInput.readLong();
		this.downFlow = dataInput.readLong();
		this.sumFlow = dataInput.readLong();
	}

	@Override
	public String toString() {
		return upFlow+"\t"+downFlow+"\t"+sumFlow;
	}

	// 按照总流量的倒序排序
	public int compareTo(Phone o) {

		if(this.sumFlow > o.sumFlow){
			return -1;
		}else if(this.sumFlow < o.sumFlow){
			return 1;
		}
		return 0;
	}
}

3.3.6.2 Mapper
package YARN.Comparable;

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

import java.io.IOException;

public class PhoneMapper extends Mapper<LongWritable, Text, Phone, Text> {

	private Phone outK = new Phone();
	private Text outV = new Text();


	@Override
	protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
		String line = value.toString();

		String[] words = line.split("\t");

		outK.setUpFlow(Long.parseLong(words[1]));
		outK.setUpFlow(Long.parseLong(words[2]));
		outK.setSumFlow();

		outV.set(words[0]);

		context.write(outK, outV);
	}
}
3.3.6.3 Reducer
package YARN.Comparable;

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

import java.io.IOException;

public class PhoneReducer extends Reducer<Phone, Text, Text, Phone> {

	@Override
	protected void reduce(Phone key, Iterable<Text> values, Context context) throws IOException, InterruptedException {

		for (Text value : values) {
			context.write(value, key);
		}
	}
}
3.3.6.4 Driver
package YARN.Comparable;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

public class PDriver {

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

		Job job = new Job(new Configuration());

		job.setJarByClass(PDriver.class);

		job.setMapperClass(PhoneMapper.class);
		job.setReducerClass(PhoneReducer.class);

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

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

		FileInputFormat.setInputPaths(job, new Path("D:\\Program Files (x86)\\WorkspaceIDEA\\HadoopStu\\src\\main\\java\\YARN\\pro_phone_data.txt"));
		FileOutputFormat.setOutputPath(job, new Path("output2"));

		boolean b = job.waitForCompletion(true);

		System.exit(b ? 0 : 1);
	}
}
3.3.6.5 运行结果
13502468823	110349	0	110349
13925057413	48243	0	48243
13560436666	25635	0	25635
13726230503	24681	0	24681
13560439658	5892	0	5892
13922314466	3720	0	3720
15013685858	3538	0	3538
15920133257	2936	0	2936
13602846565	2910	0	2910
18320173382	2412	0	2412
18211575961	2106	0	2106
13926435656	1512	0	1512
18584138413	1432	0	1432
13660577991	690	0	690
13480253104	180	0	180
15989002119	180	0	180
13760778710	120	0	120
13719199419	0	0	0
13926251106	0	0	0
13826544101	0	0	0

3.3.7 Combiner 合并

  • Combiner是MR程序中额外的组件
  • Combiner组件的父类是Reducer
  • Combiner和Reducer的区别在于运行的位置
    • Combiner是在每一个MapTask所在的节点运行
    • Reducer是接收全局所有的Mapper的输出结果
  • Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量
  • Combiner能够应用的前提是:不能影响最终的业务逻辑,而且Combiner的输出KV应该和Reducer的输入KV对应
3.3.8 Combiner 案例
3.3.8.1 Combiner 类继承 Reducer
package YARN.Combiner;

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

import java.io.IOException;

public class WCCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {

	private IntWritable outV = new IntWritable();

	@Override
	protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {

		int sum = 0;
		for (IntWritable value : values) {
			sum += value.get();
		}

		outV.set(sum);

		context.write(key, outV);
	}
}
3.3.8.2 Driver 类

添加:job.setCombinerClass(WCCombiner.class);

3.4 OutputFormat 输出数据

3.4.1 OutputFormat 接口实现类

  • OutputFormat是MR输出的基类,所有实现MR输出都实现的OutputFormat接口

  • 默认的输出格式:TextOutputFormat

  • 自定义步骤:

    • 继承FileOutputFormat
    • 改写RecordWriter,具体改写输出数据的方法write()

3.4.1 自定义 OutputFormat 案例

数据文件:

https://www.baidu.com/index
https://www.baidu.com/index.html
https://www.ali.com
https://www.sohu.com
https://www.sina.com
https://www.csdn.com/index.html
https://www.baidu.com
https://www.tencent.com
https://www.baidu.com
https://www.csdn.com/xiaobai
https://www.csdn.com?name=xiaobai
https://www.csdn.com
https://www.baidu.com
https://www.csdn.com
https://www.xiaobai.com
https://www.xiaoming.com

需求:将其中包含CSDN的筛选出来

3.4.1.1 Mapper
package YARN.OutputFormat;

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

import java.io.IOException;

public class LogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {

	@Override
	protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
		// 不做任何处理
		context.write(value, NullWritable.get());
	}
}
3.4.1.2 Reducer
package YARN.OutputFormat;

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

import java.io.IOException;

public class LogReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
	@Override
	protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
		for (NullWritable value : values) {
			context.write(key, NullWritable.get());
		}
	}
}
3.4.1.3 OutputFormat
package YARN.OutputFormat;


import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {

	public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {

		LogRecordWriter lof = new LogRecordWriter(taskAttemptContext);

		return lof;
	}

}
3.4.1.4 LogRecordWriter
package YARN.OutputFormat;

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.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;

import java.io.IOException;

public class LogRecordWriter extends RecordWriter<Text, NullWritable> {

	private FSDataOutputStream csdnLog;
	private FSDataOutputStream otherLog;

	public LogRecordWriter(TaskAttemptContext taskAttemptContext) {
		try {
			FileSystem fs = FileSystem.get(taskAttemptContext.getConfiguration());

			csdnLog = fs.create(new Path("logs/output6-csdn.log"));
			otherLog = fs.create(new Path("logs/output6-other.log"));

		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public void write(Text text, NullWritable nullWritable) throws IOException, InterruptedException {
		String line = text.toString();
		if(line.contains("csdn")){
			csdnLog.writeBytes(line+"\n");
		}else{
			otherLog.writeBytes(line+"\n");
		}
	}

	public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
		IOUtils.closeStream(csdnLog);
		IOUtils.closeStream(otherLog);
	}
}
3.4.1.5 Driver
package YARN.OutputFormat;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

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

		Job job = Job.getInstance(new Configuration());

		job.setJarByClass(LogDriver.class);

		job.setMapperClass(LogMapper.class);
		job.setReducerClass(LogReducer.class);

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

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

		job.setOutputFormatClass(LogOutputFormat.class);

		FileInputFormat.setInputPaths(job, new Path("D:\\Program Files (x86)\\WorkspaceIDEA\\HadoopStu\\src\\main\\java\\YARN\\text.txt"));
		// 还需指定一个目录用于输出 _SUCCESS 文件
		FileOutputFormat.setOutputPath(job, new Path("output6"));

		boolean b = job.waitForCompletion(true);

		System.exit(b ? 0 : 1);

	}
}
3.4.1.6 运行结果

output6-csdn.log:

https://www.csdn.com
https://www.csdn.com
https://www.csdn.com/index.html
https://www.csdn.com/xiaobai
https://www.csdn.com?name=xiaobai

output-other.log:

https://www.ali.com
https://www.baidu.com
https://www.baidu.com
https://www.baidu.com
https://www.baidu.com/index
https://www.baidu.com/index.html
https://www.sina.com
https://www.sohu.com
https://www.tencent.com
https://www.xiaobai.com
https://www.xiaoming.com

3.5 MR 内核

3.5.1 MapTask 工作机制

3.5.2 ReduceTask 工作机制

  • Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中
  • Sort阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台进程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照MR语义,用户编写reduce()函数输入数据是按照key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现了对自己的处理结果进行了局部排序,因此educeTask只需对所有数据进行一次归并排序即可
  • Reduce阶段:reduce()函数将计算结果写到HDFS上

3.5.3 ReduceTask 并行度决定机制

(MapTask并行度由切片个数决定,切片个数由输入文件和切片规则决定)

  • 设置ReduceTask并行度:job.setNumReduceTasks(4)
  • 注意事项
    • ReduceTask = 0,表示没有Reduce阶段,输出文件个数和Map个数一致
    • ReduceTask个数默认为1,所以输出文件个数为1
    • 如果数据分布不均匀,就有可能导致Reduce阶段产生数据倾斜
    • ReduceTask数量并不是任意设置,还要考虑业务逻辑需求。需要计算全局汇总结果时,就只能有1个ReduceTask
    • 具体多少个ReduceTask,需要根据集群性能,实验而定
    • 如果分区数不是1,但是ReduceTask为1,是否执行分区过程?
      • 不执行分区过程。因为在MapTask源码中,执行分区的前提示先判断ReduceNum是否大于1,不大于1肯定不执行。(所有数据都写到0号分区)

3.5.4 MapTask & ReduceTask 源码解析

3.6 Join

3.6.1 Reduce Join 案例

数据文件:

order.csv:

1001,01,5
1002,01,2
1003,02,1
1004,01,2
1005,03,4
1006,01,2
1007,02,1
1008,03,2
1009,01,4
1010,02,5
1011,03,2
1012,03,3

product.csv:

01,xiaomi
02,huawei
03,pingguo

需求:reduce端进行join,输出的行表示为:1001 xiaomi 5

3.6.1.1 创建两张表合并的 bean
package YARN.JoinReduce;

import org.apache.hadoop.io.Writable;

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

public class TableBean implements Writable {

	private String id;
	private String pid;
	private int num;
	private String name;
	private String flag; // 标记是什么表 order/product

	public TableBean() {
	}

	public String getId() {
		return id;
	}

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

	public String getPid() {
		return pid;
	}

	public void setPid(String pid) {
		this.pid = pid;
	}

	public int getNum() {
		return num;
	}

	public void setNum(int num) {
		this.num = num;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getFlag() {
		return flag;
	}

	public void setFlag(String flag) {
		this.flag = flag;
	}

	// 序列化
	public void write(DataOutput dataOutput) throws IOException {
		dataOutput.writeUTF(id);
		dataOutput.writeUTF(pid);
		dataOutput.writeInt(num);
		dataOutput.writeUTF(name);
		dataOutput.writeUTF(flag);
	}

	// 反序列化
	public void readFields(DataInput dataInput) throws IOException {
		this.id = dataInput.readUTF();
		this.pid = dataInput.readUTF();
		this.num = dataInput.readInt();
		this.name = dataInput.readUTF();
		this.flag = dataInput.readUTF();
	}

	@Override
	public String toString() {
		// 写入 id    name    num
		return id+"\t"+name+"\t"+num;
	}
}
3.6.1.2 Mapper
package YARN.JoinReduce;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;

import java.io.IOException;

public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean> {

	private String fileName;
	private Text outK = new Text();
	private TableBean outV = new TableBean();

	// 初始化,每个文件执行一次,获取文件名
	@Override
	protected void setup(Context context) throws IOException, InterruptedException {
		FileSplit inputSplit = (FileSplit) context.getInputSplit();
		// 得到文件名
		fileName = inputSplit.getPath().getName();
	}

	@Override
	protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
		String line = value.toString();
		if (fileName.startsWith("order")){ // 处理订单表

			String[] split = line.split(",");

			outK.set(split[1]); // key是pid

			// 必须全部set,否则序列化报错
			outV.setId(split[0]);
			outV.setPid(split[1]);
			outV.setNum(Integer.parseInt(split[2]));
			outV.setName("");
			outV.setFlag("order");

		}else { // 处理产品表

			String[] split = line.split(",");

			outK.set(split[0]); // key是pid

			outV.setId("");
			outV.setPid(split[1]);
			outV.setNum(0);
			outV.setName(split[1]);
			outV.setFlag("product");
		}
		context.write(outK, outV);
	}
}
3.6.1.3 Reducer
package YARN.JoinReduce;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;

public class TableReducer extends Reducer<Text, TableBean, TableBean, NullWritable> {
	@Override
	protected void reduce(Text key, Iterable<TableBean> values, Context context) throws IOException, InterruptedException {

		// 创建集合用于存储
		ArrayList<TableBean> orderBeans = new ArrayList<TableBean>();
		// 只有一个pd
		TableBean pdBean = new TableBean();

		// 循环遍历
		for (TableBean value : values) {


			if ("order".equals(value.getFlag())){

				TableBean tempTableBean = new TableBean();

				// hadoop迭代器会传地址,减少new,此时应新建一个临时对象,使用该对象进行添加,否则地址会覆盖导致只传进了一个对象
				try {
					BeanUtils.copyProperties(tempTableBean, value);
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				} catch (InvocationTargetException e) {
					e.printStackTrace();
				}

				orderBeans.add(tempTableBean);
			}else {

				try {
					BeanUtils.copyProperties(pdBean, value);
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				} catch (InvocationTargetException e) {
					e.printStackTrace();
				}
			}
		}

		// 循环遍历orderBeans,赋值pdname,然后输出
		for (TableBean orderBean : orderBeans) {
			orderBean.setName(pdBean.getName());
			context.write(orderBean, NullWritable.get());
		}
	}
}
3.6.1.4 Driver
package YARN.JoinReduce;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;

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

		Job job = Job.getInstance(new Configuration());

		job.setJarByClass(TableDriver.class);

		job.setMapperClass(TableMapper.class);
		job.setReducerClass(TableReducer.class);

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

		job.setOutputKeyClass(TableBean.class);
		job.setOutputValueClass(NullWritable.class);

		FileInputFormat.setInputPaths(job, new Path("D:\\Program Files (x86)\\WorkspaceIDEA\\HadoopStu\\src\\main\\java\\YARN\\tableInput"));
		FileOutputFormat.setOutputPath(job, new Path("output7"));

		boolean b = job.waitForCompletion(true);

		System.exit(b ? 0 : 1);
	}
}
3.6.1.5 运行结果

join成功:

1009	xiaomi	4
1006	xiaomi	2
1004	xiaomi	2
1002	xiaomi	2
1001	xiaomi	5
1010	huawei	5
1007	huawei	1
1003	huawei	1
1012	pingguo	3
1011	pingguo	2
1008	pingguo	2
1005	pingguo	4

3.6.2 Map Join 案例

避免Reduce端处理数据量过大,适用于一张表很小,一张表很大的场景

具体办法:

  • 在Mapper的setup阶段,将文件读取到缓存集合中
  • 在Driver类中加载缓存
    • job.addCacheFile(new URI(...));
3.6.2.1 bean

(与3.6.1.1相同)

3.6.2.2 Mapper
package YARN.JoinMap;

import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.fs.FSDataInputStream;
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.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.HashMap;

public class TableMapper extends Mapper<LongWritable, Text, Text, NullWritable> {

	private HashMap<String, String> pdMap = new HashMap<String, String>();
	private Text outK = new Text();

	@Override
	protected void setup(Context context) throws IOException, InterruptedException {
		// 获取缓存的文件,并把文件内容封装到集合
		URI[] files = context.getCacheFiles();

		FileSystem fs = FileSystem.get(context.getConfiguration());
		FSDataInputStream fis = fs.open(new Path(files[0]));

		// 从流中读取数据
		BufferedReader br = new BufferedReader(new InputStreamReader(fis, "UTF-8"));

		String line;
		while (StringUtils.isNotEmpty(line=br.readLine())){
			// 切割
			String[] split = line.split(",");

			// pid,pname
			pdMap.put(split[0], split[1]);
		}

		IOUtils.closeStream(br);

	}

	@Override
	protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
		// 处理order.csv
		String line = value.toString();
		String[] split = line.split(",");

		String pname = pdMap.get(split[1]);

		outK.set(split[0]+"\t"+pname+"\t"+split[2]);

		context.write(outK, NullWritable.get());
	}
}
3.6.2.3 没有Reducer
3.6.2.4 Driver

添加缓存文件,需要注意URI规范

package YARN.JoinMap;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

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

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

		Job job = Job.getInstance(new Configuration());

		job.setJarByClass(TableDriver.class);

		job.setMapperClass(TableMapper.class);

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

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

		job.addCacheFile(new URI("file:///D:/HadoopFiles/product.csv"));

		job.setNumReduceTasks(0);

		FileInputFormat.setInputPaths(job, new Path("D:\\Program Files (x86)\\WorkspaceIDEA\\HadoopStu\\src\\main\\java\\YARN\\tableInput\\order.csv"));
		FileOutputFormat.setOutputPath(job, new Path("output8"));

		boolean b = job.waitForCompletion(true);

		System.exit(b ? 0 : 1);
	}
}
3.6.2.5 运行结果

join成功

1001	xiaomi	5
1002	xiaomi	2
1003	huawei	1
1004	xiaomi	2
1005	pingguo	4
1006	xiaomi	2
1007	huawei	1
1008	pingguo	2
1009	xiaomi	4
1010	huawei	5
1011	pingguo	2
1012	pingguo	3

3.7 ETL 数据清洗

(Map阶段清洗)

3.8 总结

  • InputFormat
    • 默认:TextInputFormat
      • K为偏移量,V为一行内容
    • 处理小文件:CombineTextInputFormat
      • 把多个文件合并到一起,统一切片
  • Mapper
    • setup初始化 =》 map业务逻辑 =》 clearup关闭资源
  • 分区
    • 默认:HashPartitioner
      • 默认按照 key 的 hash 值 % numReduceTask
      • 自定义分区
  • 排序
    • 部分排序
      • 每个输出文件内部有序
    • 全排序
      • 只有一个reduce,对所有数据排序
    • 二次排序(自定义排序)
      • 实现WritableComparable,重写compareTo方法
  • Combiner
    • map阶段预聚合(解决数据倾斜的一个方法)
    • 前提:不影响最终的业务逻辑
  • Reducer
    • setup初始化 =》 reduce业务逻辑 =》 clearup关闭资源
  • OutputFormat
    • 默认:TextOutputFormant
      • 按行输出到文件
    • 自定义输出

四、Hadoop 数据压缩

  • 压缩的优缺点
    • 优点:减少磁盘IO,减少磁盘存储空间
    • 缺点:增加CPU开销
  • 压缩的原则
    • 运算密集型的job,少用压缩
    • IO密集型的job,多用压缩

4.1 MR支持的压缩算法

压缩格式Hadoop自带?算法文件扩展名是否可切片换成压缩格式后,原来的程序是否需要修改其他
DEFLATE是,直接使用DEFLATE.deflate和文本处理一样,不需要修改
Gzip是,直接使用DEFLATE.gz和文本处理一样,不需要修改压缩率高,压缩速度一般
bzip2是,直接使用bzip2.bz2和文本处理一样,不需要修改压缩率高,压缩速度慢
LZO否,需要安装LZO.lzo需要建索引,还需要指定输入格式压缩率一般,压缩速度快
Snappy是,直接使用Snappy.snappy和文本处理一样,不需要修改压缩率一般,压缩速度快

4.2 生产环境中的使用

  • 输入端采用压缩
    • 数据量少于块大小,重点考虑压缩和解压缩速度比较快的LZO/Snappy
    • 数据量非常大,重点考虑支持切片的Bzip2/LZO
  • Mapper输出采用压缩
    • 为了减少MapTask和ReduceTask之间的网络IO,重点考虑压缩和解压缩快的LZO/Snappy
  • Reducer输出采用压缩
    • 永久保存,考虑压缩率比较高的Bzip2/Gzip
    • 如果作为下一个MR输入,需要考虑数据量和是否支持切片

4.3 压缩案例

4.3.1 参数配置

压缩格式编码/解码器
DEFLATEorg.apache.hadoop.io.compress.DefaultCodec
Gziporg.apache.hadoop.io.compress.GzipCodec
bzip2org.apache.hadoop.io.compress.BZip2Codec
LZOcom.hadoop.compression.lzo.LzopCodec
Snappyorg.apache.hadoop.io.compress.SnappyCodec

4.3.2 Map 端压缩

Driver类中配置:

		// 开启Map端输出压缩
		conf.setBoolean("mapreduce.map.output.compress",true);

		// 设置Map端输出压缩方式
		conf.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);

4.3.3 Reduce 端压缩

Driver类中配置:

		// 开启Reduce端输出压缩
		FileOutputFormat.setCompressOutput(job, true);

		// 设置Reduce端压缩的方式
		FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

搬金砖的小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值