map task的map()函数以key-value对作为输入,产生一系列kv对作为中间输出写入本地磁盘。
详细过程:
reduce()函数通过网络将map的输出(kv对)作为输入,产生另一系列kv对作为最终输出写入到hdfs,这时的key-value对是计算结果。
参考以上map和reduce阶段的合并,本文最后会做总结。
block对应一个分配split,一个split对应一个map task
这里主要用到hadoop自带的一个mapper类,原本在传输过程中需要做一个序列化和反序列化,而hadoop直接将这两个步骤给写到了mapper这个类里面,(LongWrite,Text,Text,IntWritable)
package com.kaikeba.hadoop.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;
public class WordCountMap extends Mapper<LongWritable, Text, Text, IntWritable> {
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//dear bear river
String[] words = value.toString().split("\t");
for (String word : words) {
// 每个单词出现1次,作为中间结果输出
context.write(new Text(word), new IntWritable(1));
}
}
}
这里Reducer也是hadoop自带的类,它接受的输入参数类型和mapper的输出参数类型是一致的。
public class WordCountReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
/*
(hello, 1)
(hello, 1)
(hello, 1)
...
(spark, 1)
key: hello
value: List(1, 1, 1)
*/
public void reduce(Text key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable count : values) {
sum += count.get();
}
context.write(key, new IntWritable(sum));// 输出最终结果
};
}
public class WordCountMain { //若在IDEA中本地执行MR程序,需要将mapred-site.xml中的mapreduce.framework.name值修改成local public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { if (args.length != 2 || args == null) { System.out.println("please input Path!"); System.exit(0); } Configuration configuration = new Configuration(); //configuration.set("mapreduce.job.jar","/home/bruce/project/kkbhdp01/target/com.kaikeba.hadoop-1.0-SNAPSHOT.jar"); //调用getInstance方法,生成job实例 Job job = Job.getInstance(configuration, WordCountMain.class.getSimpleName()); // 打jar包 job.setJarByClass(WordCountMain.class); // 通过job设置输入/输出格式 // MR的默认输入格式是TextInputFormat,所以下两行可以注释掉 // job.setInputFormatClass(TextInputFormat.class); // job.setOutputFormatClass(TextOutputFormat.class); // 设置输入/输出路径 FileInputFormat.setInputPaths(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); // 设置处理Map/Reduce阶段的类 job.setMapperClass(WordCountMap.class); //map combine减少网路传出量 job.setCombinerClass(WordCountReduce.class); job.setReducerClass(WordCountReduce.class); //如果map、reduce的输出的kv对类型一致,直接设置reduce的输出的kv对就行;如果不一样,需要分别设置map, reduce的输出的kv类型 //job.setMapOutputKeyClass(.class) // job.setMapOutputKeyClass(Text.class); // job.setMapOutputValueClass(IntWritable.class); // 设置reduce task最终输出key/value的类型 job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); // 提交作业 job.waitForCompletion(true); } }
本地运行前,要把idea项目下的mapred-site.xml的mapreduce.framework.name对应的value改成“local”
[hadoop@node1 ~]# hadoop jar jar包的路径 jar包中类的完整类名 hdfs上需统计的文件路径 输出hdfs路径
[hadoop@node1 ~] hadoop jar com.kaikeba.hadoop-1.0-SNAPSHOT.jar com.kaikeba.hadoop.wordcount.WordCountMain /NOTICE.txt /wordcount01
#假如输入路径有加端口号,那么输出路径也需要加端口号
正常情况下,Hadoop框架使用Mapper将数据处理成一个key-value键值对,在网络节点间对其整理(shuffle洗牌),然后再使用Reduce处理数据并进行最终输出。
但是在中间网络节点整理的这个shuffle过程中,如果数据量很大(假设100亿),而需求只是求一个最大值,那么单个Reduce需要承载的kv对数量也将是庞大的,会降低程序的性能。
于是出现Combiner,就是为了避免map task和reduce task之间的数据传输压力而设置的,它允许用户针对map task的输出指定一个合并函数,减少传输到reduce的数据量,从而减少网络带宽和reduce的负载。
下面是combiner部分的代码:
package com.kaikeba.hadoop.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 WordCountMain {
//若在IDEA中本地执行MR程序,需要将mapred-site.xml中的mapreduce.framework.name值修改成local
public static void main(String[] args) throws IOException,
ClassNotFoundException, InterruptedException {
if (args.length != 2 || args == null) {
System.out.println("please input Path!");
System.exit(0);
}
Configuration configuration = new Configuration();
//configuration.set("mapreduce.job.jar","/home/bruce/project/kkbhdp01/target/com.kaikeba.hadoop-1.0-SNAPSHOT.jar"); 集群调用的方式
//调用getInstance方法,生成job实例
Job job = Job.getInstance(configuration, WordCountMain.class.getSimpleName());
// 打jar包
job.setJarByClass(WordCountMain.class);
// 通过job设置输入/输出格式
// MR的默认输入格式是TextInputFormat,所以下两行可以注释掉
// job.setInputFormatClass(TextInputFormat.class);
// job.setOutputFormatClass(TextOutputFormat.class);
// 设置输入/输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 设置处理Map/Reduce阶段的类
job.setMapperClass(WordCountMap.class);
//map combine减少网路传出量
job.setCombinerClass(WordCountReduce.class);
job.setReducerClass(WordCountReduce.class);
//如果map、reduce的输出的kv对类型一致,直接设置reduce的输出的kv对就行;如果不一样,需要分别设置map, reduce的输出的kv类型
//job.setMapOutputKeyClass(.class)
// job.setMapOutputKeyClass(Text.class);
// job.setMapOutputValueClass(IntWritable.class);
// 设置reduce task最终输出key/value的类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 提交作业
job.waitForCompletion(true);
}
}
hadoop的shuffle过程实际上包含在map阶段和Reduce阶段,即分为Map Shuffle和Reduce Shuffle。
在Map端的shuffle过程是对Map的结果进行分区、排序、分割,然后将属于同一划分(分区)的输出合并在一起并写在磁盘上,最终得到一个分区有序的文件。分区有序的含义是map输出的键值对按分区进行排列,具有相同partition值的键值对存储在一起,每个分区里面的键值对又按key值进行升序排列(默认)。
结合第二张图片,map的shuffle大致流程如下:
在Reduce端,shuffle主要通过网络向磁盘获取reduce需要的数据,一般是把key相同的键值对数据放到一起,然后排序合并,最终输出一个整体有序的数据块。
还是参考第二张图片
但是这里要注意的是,虽然叫做Reduce shuffle,但实际是不包含调用reduce方法的。完整的shuffle是从map输出kv对到内存,再到调用reduce方法之前的这个过程。
默认按字典字母升序排序。
读取文件customPartition.txt,内容如下:
Dear River
Dear River Bear Car
Car Dear Car Bear Car
Dear Car Bear Car
结果如下:
package com.kaikeba.hadoop.partitioner;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
import java.util.HashMap;
//传如的是
public class CustomPartitioner extends Partitioner<Text, IntWritable> {
public static HashMap<String, Integer> dict = new HashMap<String, Integer>();
static{
dict.put("Dear", 0);
dict.put("Bear", 1);
dict.put("River", 2);
dict.put("Car", 3);
}
public int getPartition(Text text, IntWritable intWritable, int i) {
//这里做了一个自定义逻辑,它是直接判断dist上写的value是多少来做分区的
int partitionIndex = dict.get(text.toString());
return partitionIndex;
}
}
MapReduce中,根据key进行分区,排序,分组。MapReduce会按照基本类型对应的key进行排序,比如int类型intwritable,默认升序排序,取反则降序排序。
但如果这时有需求要对工资排序,若工资相同,则继续对年龄排序,则需要自定义key类型,并自定义key的排序规则。
现有需求,需要自定义key类型,并自定义key的排序规则,如按照人的salary降序排序,若相同,则再按age升序排序;若salary、age相同,则放入同一组
详见工程代码
Person.java
//Comparable比较,Writable序列化
public class Person implements WritableComparable<Person> {
private String name;
private int age;
private int salary;
public Person() {
}
public Person(String name, int age, int salary) {
//super();
this.name = name;
this.age = age;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
//设置输出的格式
@Override
public String toString() {
return this.salary + " " + this.age + " " + this.name;
}
//先比较salary,高的排序在前;若相同,age小的在前
//int类型默认升序排序,取反就降序排序
public int compareTo(Person o) {
int compareResult1= this.salary - o.salary;
if(compareResult1 != 0) {
return -compareResult1;
} else {
return this.age - o.age;
}
}
//序列化,将NewKey转化成使用流传送的二进制
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(name);
dataOutput.writeInt(age);
dataOutput.writeInt(salary);
}
//反序列化,使用in读字段的顺序,要与write方法中写的顺序保持一致
public void readFields(DataInput dataInput) throws IOException {
//read string
this.name = dataInput.readUTF();
this.age = dataInput.readInt();
this.salary = dataInput.readInt();
}
}
public class SecondarySort {
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
configuration.set("mapreduce.job.jar","/home/bruce/project/kkbhdp01/target/com.kaikeba.hadoop-1.0-SNAPSHOT.jar");
Job job = Job.getInstance(configuration, SecondarySort.class.getSimpleName());
FileSystem fileSystem = FileSystem.get(URI.create(args[1]), configuration);
if (fileSystem.exists(new Path(args[1]))) {
fileSystem.delete(new Path(args[1]), true);
}
FileInputFormat.setInputPaths(job, new Path(args[0]));
job.setMapperClass(MyMap.class);
job.setMapOutputKeyClass(Person.class);
job.setMapOutputValueClass(NullWritable.class);
//设置reduce的个数
job.setNumReduceTasks(1);
job.setReducerClass(MyReduce.class);
job.setOutputKeyClass(Person.class);
job.setOutputValueClass(NullWritable.class);
FileOutputFormat.setOutputPath(job, new Path(args[1]));
job.waitForCompletion(true);
}
public static class MyMap extends
Mapper<LongWritable, Text, Person, NullWritable> {
@Override
protected void map(LongWritable key, Text value,
Context context)
throws IOException, InterruptedException {
String[] fields = value.toString().split("\t");
String name = fields[0];
int age = Integer.parseInt(fields[1]);
int salary = Integer.parseInt(fields[2]);
//在自定义类中进行比较
Person person = new Person(name, age, salary);
context.write(person, NullWritable.get());
}
}
public static class MyReduce extends
Reducer<Person, NullWritable, Person, NullWritable> {
@Override
protected void reduce(Person key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
context.write(key, NullWritable.get());
}
}
}
什么是数据倾斜?
简单书数据倾斜就是数据的key值 分化严重不均,造成一部分数据很多,一部分数据很少,出现离群值。就拿广东一年四季的气温成正态分布来说,一年的气温集中在25°到35°之间,而其他温度比较少,这个集中值在数据中就容易出现数据倾斜。
数据倾斜会显著的拖慢MR的执行,因为如果有些数据量分布少,就能提前执行完,但它要继续等待数据量多的mr任务。
常见的数据倾斜有以下两类:
阿里的ADS库中也有防止数据倾斜的办法,通常是修改表结构,增加聚集列等。
在map端和reduce端都有可能发生数据倾斜。在reduce端的数据倾斜常常来源于MapReduce的默认分区器。
数据倾斜会导致map和reduce的任务执行时间大为延长,也会让缓存数据操作消耗更多的内存资源。
下面是第一种方法的类似代码,新增maxValueThreshold变量判断。
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.log4j.Logger;
import java.io.IOException;
public class WordCountReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
private int maxValueThreshold;
//日志类
private static final Logger LOGGER = Logger.getLogger(WordCountReduce.class);
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//一个键达到多少后,会做数据倾斜记录
maxValueThreshold = 10000;
}
/*
(hello, 1)
(hello, 1)
(hello, 1)
...
(spark, 1)
key: hello
value: List(1, 1, 1)
*/
public void reduce(Text key, Iterable<IntWritable> values,
Context context) throws IOException, InterruptedException {
int sum = 0;
//用于记录键出现的次数
int i = 0;
for (IntWritable count : values) {
sum += count.get();
i++;
}
//如果当前键超过10000个,则打印日志
if(i > maxValueThreshold) {
LOGGER.info("Received " + i + " values for key " + key);
}
context.write(key, new IntWritable(sum));// 输出最终结果
};
}
Reduce数据倾斜一般是指map的输出数据中存在数据频率倾斜的状况,即部分输出键的数据量远远大于其它的输出键
如何减小reduce端数据倾斜的性能损失?常用方式有:
Hadoop默认的分区器是HashPartitioner,基于map输出键的哈希值分区。这仅在数据分布比较均匀时比较好。在有数据倾斜时就很有问题。
使用分区器需要首先了解数据的特性。TotalOrderPartitioner中,可以通过对原始数据进行抽样得到的结果集来预设分区边界值。TotalOrderPartitioner中的范围分区器可以通过预设的分区边界值进行分区。因此它也可以很好地用在矫正数据中的部分键的数据倾斜问题。
简单来说,就是TotalOrderPartitioner能采样得知哪些键的数据量大,然后按照采样结果寻找key值的最佳分割点,将key-value对均匀的分布到不同分区中。
基于输出键的业务知识进行自定义分区。例如,淘宝一年的销售数据可能集中在双十一活动上,那么就把双十一的销售数据值均分给其他时间段的来分区。
使用Combine可以大量地减小数据频率倾斜和数据大小倾斜。
MR程序读取HDFS的文件
文件分片成block,每一行数据解析成键值对
Map阶段
Reduce阶段
reduce方法输出写入到HDFS文件系统。
(1)MR读取HDFS的文件,将文件分片成一个个block,并且block的每一行解析成key-value键值对。
(2)键值对调用map阶段的map()方法,输出的数据存入一个环形缓冲区,该环形缓冲区为100M大小,当输出数据存满这个缓冲区的80%内存后,就会溢出写入磁盘。
(3)在溢出写入磁盘的过程中,会做以下几个操作:
(4)然后到map阶段的最后一步,在磁盘中将溢出的键值对合并输出。
(5)Reduce task从各个map任务结果中,获取它需要的分区数据,合并写入JVM内存;
(6)同样当内存达到一定阈值后会将数据写入到磁盘,如果指定了combine,运行它会减少写入磁盘的数据量。
(7)随着磁盘文件或者内存文件的增多,还会进行合并排序,将最后一次合并的结果作为输入参数调用reduce()方法。
(8)reduce方法的输出结果写入到HDFS文件系统。
时间 | 内容 |
---|---|
2020年04月10日 | 第一次发布 |
2020年9月13日 | 修改部分图片显示不全问题 |
本文为学习课程做的笔记,如有侵权,请联系作者删除。