【大数据】Hadoop_MapReduce(二)

MapReduce框架原理

在这里插入图片描述

1. InputFormat数据输入

InputFormat是一个抽象类,它有很多实现类,例如 FileInputFormat、CombineFileInputFormat、TextInputFormat等。

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

MapTask 的并行度决定 Map 阶段的任务处理并发度,进而影响到整个 Job 的处理速度。

数据块:Block 是 HDFS 物理上把数据分成一块一块。数据块是 HDFS 存储数据单位。
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是 MapReduce 程序计算输入数据的单位,一个切片会对应启动一个 MapTask。

数据切片与MapTask并行度决定机制:
在这里插入图片描述

Job提交流程源码解析
在这里插入图片描述

1.2 FileInputFormat

(1)切片机制

  • 简单地按照文件的内容长度进行切片
  • 切片大小,默认等于Block大小
  • 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片

(2)案例分析
输入数据有两个文件:

file1.txt 320M
file2.txt 10M

经过FileInputFormat的切片机制运算后,形成的切片信息如下:

file1.txt.split1-- 0~128
file1.txt.split2-- 128~256
file1.txt.split3-- 256~320
file2.txt.split1-- 0~10M

(3)FileInputFormat切片大小的参数配置
在这里插入图片描述

(4)FileInputFormat切片源码解析
在这里插入图片描述

1.3 TextInputFormat

FileInputFormat 常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat 和自定义 InputFormat 等。

TextInputFormat 是默认的 FileInputFormat 实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable 类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text 类型。
在这里插入图片描述

1.4 CombineTextInputFormat

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

(1)应用场景:
CombineTextInputFormat 用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个 MapTask 处理。

(2)虚拟存储切片最大值设置

CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m

注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。

(3)切片机制
生成切片过程包括:虚拟存储过程和切片过程二部分。
在这里插入图片描述

虚拟存储过程阶段:将输入目录下所有文件,依次和设置的setMaxInputSplitSize的值进行比较

  • 如果不大于设置的最大值,那逻辑上分为一个块
  • 如果数据大小超过设置的最大值,但是不超过最大值的2倍,则将文件均匀切分成2个块(防止出现太小的切片)
  • 如果大于设置的最大值的2倍,则按照最大值切割出一块

切片过程:判断虚拟存储的文件大小是否大于 setMaxInputSplitSize

  • 如果大于等于,则单独形成一个切片
  • 如果小于,则和下一个虚拟存储文件进行合并,共同形成一个切片

(4)案例实操

需求:将输入的大量小文件合并成一个切片统一处理。准备四个小文件,期望一个切片处理四个文件。

实现过程:
在 WordcountDriver 中增加如下代码,运行程序,并观察运行的切片个数为 3

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

2. MapReduce工作流程

Map阶段:
在这里插入图片描述

  1. 把输入目录下的文件按照一定的标准逐个进行 逻辑切片,形成切片规划。 默认切片大小等于数据块大小。每一个切片由一个MapTask进行处理。

  2. 对切片中的数据按照一定的规则读取解析,并返回 key-value 键值对。默认使用的 TextInputFormat进行按行读取数据。key是每一行的起始位置偏移量,value是本行的内容

  3. 调用Mapper类的map()方法处理数据。TextInputFormat每读取解析出一个 key-value, 就会调用一次 Mapper 的map()方法

  4. 按照一定的规则,对Mapper输出的键值对进行分区(patition)。分区的数量就是ReduceTask运行的数量。默认不分区,因为只有一个ReduceTask。

  5. Mapper输出数据如果直接写到磁盘效率比较慢,所以会先写入内存缓冲区,达到比例时溢出(spill)到磁盘上。溢出的时候会根据key进行排序(sort)。
    默认根据key进行字典排序

  6. 对所有溢出文件进行最终的合并(merge),成为一个文件

Reduce阶段:
在这里插入图片描述

  1. ReducetTask会主动从MapTask复制拉取(fetch)属于自己分区的数据
  2. 把拉取来的数据,全部进行合并(merge),把分散的数据合并成一个大的数据。再对合并后的数据进行排序
  3. 对排序后的键值对调用reduce()方法。 键相等的键值对会被合并成{ key1: [value1_1, value1_2, value1_3], key2:[value2_1, value2_2] }的形式,每个键调用一次reduce()方法。
  4. 最后把这些输出的键值对写入到 hdfs 文件中

3. Shuffle机制

Map 方法之后,Reduce 方法之前的数据处理过程称之为 Shuffle。

Shuffle流程:
在这里插入图片描述

Mapper端的Shuffle:

  • Collect阶段:将MapTask的结果收集输出到默认大小为100M的环形缓冲区,保存之前会对 key 进行分区的计算,默认hash分区
  • Spill阶段:当内存中的数据量达到一定的阈值时,就会将数据写入本地磁盘。在将数据写入磁盘之前需要对数据进行一次排序的操作。如果配置了combiner,还会将有相同分区号和key的数据进行排序。
  • Merge阶段:把所有溢出的临时文件进行一次合并操作,以确保一个MapTask最终只产生一个中间数据文件。

Reducer端的Shuffle:

  • Copy阶段:ReduceTask启动Fetcher线程到已经完成MapTask的节点上复制一个属于自己的数据
  • Merge阶段:在ReduceTask远程复制数据的同时,会在后台开启两个线程对内存到本地的数据进行合并操作
  • Sort阶段:在对数据进行合并的同事,会进行排序操作。由于MapTask阶段已经对数据进行了局部的排序,ReduceTask只需保证Copy的数据最终整体有效性即可。

3.1 Partition 分区

需求:求将统计结果按照条件输出到不同文件中(分区)

默认Partitioner分区

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

默认分区是根据key的hashCode对ReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区。

3.1.1 自定义Partitioner

(1)自定义类继承Partitioner,重写getPartition()方法

public class CustomPartitioner extends Partitioner<Text, FlowBean> {
	@Override
	public int getPartition(Text key, FlowBean value, int numPartitions) {
		// 控制分区代码逻辑
		… …
		return partition;
	}
}

(2)在Job驱动中,设置自定义Partitioner

job.setPartitionerClass(CustomPartitioner.class)

(3)自定义Partition后,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask

job.setNumReduceTasks(5);

分区总结:

  • 如果 ReduceTask的数量 > getPartition的结果数,程序可以正常运行,但是会产生几个空的输出文件,最后几个分配的节点没有处理数据,空耗资源;
  • 如果 1 < ReduceTask的数量 < getPartition的结果数,则有部分数据没有ReduceTask处理,会抛出IOException;
  • 如果 ReduceTask的数量 = 1,则不管有没有设置自定义分区类,最终走的都是Hadoop默认的一个固定返回0的分区类,只会分配一个ReduceTask,也只会产生一个结果文件;
  • 如果ReduceTask的数量 = 0,则表示不进行Reduce汇总,Hadoop也不会再进行Shuffle,直接将MapTask的结果输出到文件
  • 分区getPartition方法中,分区号必须从0开始,逐1增加
3.2.2 Partition 分区案例实操

需求:
在这里插入图片描述

在上一篇序列化案例实操的基础上进行操作,增加一个ProvencePartitioner分区类:

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
 	@Override
 	public int getPartition(Text text, FlowBean flowBean, int numPartitions) 
	{
		//获取手机号前三位 prePhone
 		String phone = text.toString();
 		String prePhone = phone.substring(0, 3);
		//定义一个分区号变量 partition,根据 prePhone 设置分区号
 		int partition;
		if("136".equals(prePhone)){
 			partition = 0;
 		}else if("137".equals(prePhone)){
 			partition = 1;
 		}else if("138".equals(prePhone)){
 			partition = 2;
 		}else if("139".equals(prePhone)){
 			partition = 3;
 		}else {
 			partition = 4;
 		}
 		//最后返回分区号 partition
 		return partition;
 	}
} 

在Driver中配置使用该分区类,加上以下代码,根据分区类中的逻辑设置ReduceTask个数:

// 指定自定义分区器
 job.setPartitionerClass(ProvincePartitioner.class);
 // 同时指定相应数量的 ReduceTask
 job.setNumReduceTasks(5); 

3.3 WritableComparable排序

3.3.1 排序概述

排序是MapReduce框架中最重要的操作之一。

MapTask和ReduceTask均会对数据按照key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。

默认排序是按照字典顺序排序,且实现该排序的方法是快速排序

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

对于ReduceTask,它从每个MapTask上远程拷贝(拉取fetch)相应的数据文件。

  • 如果文件大小超过一定阈值,则溢写到磁盘上,否则存储在内存中:
  • 如果内存中文件大小或数目超过一定阈值,则进行一次合并后溢写到磁盘上;
  • 如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大的文件;
  • 当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
3.3.2 排序分类

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

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

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

(4)二次排序
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。如果使用自定义的 JavaBean 作为key传输,那么这个 JavaBean 需要实现 WritableComparable 接口并重写compareTo方法。

3.3.3 全排序案例

需求:对序列化案例产生的结果再次对总流量进行倒序排序。

在这里插入图片描述

(1)FlowBean 对象在在需求 1 基础上增加了比较功能

@Override
 public int compareTo(FlowBean o) {
	//按照总流量比较,倒序排列
 	if(this.sumFlow > o.sumFlow){
 		return -1;
 	}else if(this.sumFlow < o.sumFlow){
 		return 1;
	}else {
 		return 0;
 	}
}

(2)编写 Mapper 类

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, FlowBean, Text> 
{
 	private FlowBean outK = new FlowBean();
 	private Text outV = new Text();
 	@Override
 	protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
 		//1 获取一行数据
 		String line = value.toString();
 		//2 按照"\t",切割数据
 		String[] split = line.split("\t");
 		//3 封装 outK outV
 		outK.setUpFlow(Long.parseLong(split[1]));
 		outK.setDownFlow(Long.parseLong(split[2]));
 		outK.setSumFlow();
 		outV.set(split[0]);
 		//4 写出 outK outV
 		context.write(outK,outV);
 	}
}

(3)编写 Reducer 类

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowReducer extends Reducer<FlowBean, Text, Text, FlowBean> 
{
 	@Override
 	protected void reduce(FlowBean key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
		//遍历 values 集合,循环写出,避免总流量相同的情况
 		for (Text value : values) {
 			//调换 KV 位置,反向写出
 			context.write(value,key);
 		}
	}
}

(4)修改Driver类,添加以下内容:

// 设置 Map 端输出数据的 KV 类型
 job.setMapOutputKeyClass(FlowBean.class);
 job.setMapOutputValueClass(Text.class); 
3.3.4 区间排序案例

需求:要求每个省份手机号输出的文件中按照总流量内部排序。
需求分析:基于前一个需求,增加自定义分区类,分区按照省份手机号设置。
在这里插入图片描述

(1)增加自定义分区类ProvincePartitioner

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

public class ProvincePartitioner2 extends Partitioner<FlowBean, Text> {

	@Override
 	public int getPartition(FlowBean flowBean, Text text, int numPartitions) 
	{
 		//获取手机号前三位
 		String phone = text.toString();
 		String prePhone = phone.substring(0, 3);
 		//定义一个分区号变量 partition,根据 prePhone 设置分区号
 		int partition;
 		if("136".equals(prePhone)){
 			partition = 0;
 		}else if("137".equals(prePhone)){
 			partition = 1;
 		}else if("138".equals(prePhone)){
 			partition = 2;
 		}else if("139".equals(prePhone)){
 			partition = 3;
 		}else {
 			partition = 4;
 		}
 		//最后返回分区号 partition
 		return partition;
 	}
}

(2)在Driver驱动类中添加分区类

// 设置自定义分区器
job.setPartitionerClass(ProvincePartitioner2.class);
// 设置对应的 ReduceTask 的个数
job.setNumReduceTasks(5);

3.4 Combiner合并

特点

  • Combiner 是MR程序中 Mapper 和 Reducer 之外的一种组件。即 Combiner不是必须的,可以选择性的添加
  • 自定义Combiner组件的父类也是Reducer
  • Combiner和Reducer的区别在于运行的位置: Combniner是在MapTask所在的节点运行; Reducer是接收全局所有Mapper的输出结果;
  • Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小Reducer从Mapper拉取时的网络传输流量
  • Combiner能够应用的前提是:不能影响最终的业务逻辑。而且 Combiner 输出的 key-value 应该能够跟Reducer的输入 key-value 类型对应起来。
3.4.1 自定义Combiner

(1)自定义一个 Combiner 继承 Reducer,重写 Reduce 方法

public class WordCountCombiner 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);
 	}
}

(b)在 Job 驱动类中设置:

job.setCombinerClass(WordCountCombiner.class); 
3.4.2 Combiner 合并案例实操

(1)需求:统计过程中对每一个 MapTask 的输出进行局部汇总,以减小网络传输量即采用Combiner 功能。
在这里插入图片描述

(2)实现
编写自定义Combiner类:

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

import java.io.IOException;

/**
 * Combiner就是一个小的Reducer,继承的类也是Reducer
 * 只在当前的MapTask上运行,对当前MapTask上的结果进行汇总
 */
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
    IntWritable outValue = new IntWritable();
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context) throws IOException, InterruptedException {
        int sum = 0;
        for (IntWritable value : values) {
            sum += value.get();  // value是IntWritable类型,需要调用get()进行类型转换
        }
        outValue.set(sum);
        context.write(key, outValue);
    }
}

在 WordcountDriver 驱动类中指定 Combiner

// 指定需要使用 combiner,以及用哪个类作为 combiner 的逻辑
job.setCombinerClass(WordCountCombiner.class); 

4. OutputFormat数据输出

4.1 OutputFormat 接口实现类

OutputFormat是MapReduce输出的基类,所有MapReduce的输出类都实现了 OutputFormat接口。

默认使用的是OutputFormat下的 FileOutputFormat下的TextOutputFormat。

自定义OutputFormat: 例如在FileOutputFormat基础上自定义

  • 自定义类,继承FileOutputFormat
  • 改写FileOutputFormat中实现RecordWrite的内部类FilterRecordWriter

4.2 自定义 OutputFormat 案例实操

在这里插入图片描述

(1)编写 LogMapper 类

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 {
 		//不做任何处理,直接写出一行 log 数据
 		context.write(value,NullWritable.get());
 	}
}

(2)编写 LogReducer 类

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)自定义一个 LogOutputFormat 类

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> 
{
 	@Override
 	public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
 		//创建一个自定义的 RecordWriter 返回
 		LogRecordWriter logRecordWriter = new LogRecordWriter(job);
 		return logRecordWriter;
 	}
}

(4)编写 LogRecordWriter 类

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 atguiguOut;
	private FSDataOutputStream otherOut;
 
	public LogRecordWriter(TaskAttemptContext job) {
 		try {
 			//获取文件系统对象
 			FileSystem fs = FileSystem.get(job.getConfiguration());
 			//用文件系统对象创建两个输出流对应不同的目录
 			atguiguOut = fs.create(new Path("d:/hadoop/atguigu.log"));
 			otherOut = fs.create(new Path("d:/hadoop/other.log"));
 		} catch (IOException e) {
 			e.printStackTrace();
 		}
 	}
 
	@Override
 	public void write(Text key, NullWritable value) throws IOException, InterruptedException {
 		String log = key.toString();
 		//根据一行的 log 数据是否包含 atguigu,判断两条输出流输出的内容
 		if (log.contains("atguigu")) {
 			atguiguOut.writeBytes(log + "\n");
 		} else {
 			otherOut.writeBytes(log + "\n");
 		}
 	}
 
	@Override
 	public void close(TaskAttemptContext context) throws IOException, InterruptedException {
 		//关流
 		IOUtils.closeStream(atguiguOut);
 		IOUtils.closeStream(otherOut);
 	}
} 

(5)编写 LogDriver 类

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 {
 		Configuration conf = new Configuration();
 		Job job = Job.getInstance(conf);
 
		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);
 
		//设置自定义的 outputformat
		job.setOutputFormatClass(LogOutputFormat.class);
		FileInputFormat.setInputPaths(job, new Path("D:\\input"));
		// 虽 然 我 们 自 定 义 了 outputformat , 但 是 因 为 我 们 的 outputformat 继承自fileoutputformat
		//而 fileoutputformat 要输出一个_SUCCESS 文件,所以在这还得指定一个输出目录
		FileOutputFormat.setOutputPath(job, new Path("D:\\logoutput"));

		boolean b = job.waitForCompletion(true);
		System.exit(b ? 0 : 1);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值