流程大致分为:原始文件----maptask-----reducetask----结果文件
原始文件 -----maptask过程:
1、FileInputFormat抽象类 文件加载器
----默认调用的实现类:TextInputFormat 文本格式
2、RecordReader抽象类 文件读取器
----默认调用是实现类:LineRecordReader 按行读取
读取的时候将文件转化为key/value形式
key:每一行的起始偏移量
value:读取到的每一行的内容
//mapper的keyin和valuein就获取自这里
maptask----reducetask过程(shuffle)
1、排序
----WritableComparable 排序器
LongWritable默认实现了WritableComparable接口,所以内部类型具有排序能力 所以key使用内部类型之后,就会对key进行默认排序
//排序之后相同的单词就在一起了,所以排序在前
2、分组
----WritableComparator 分组器
3、分区
将输出结果进行分类
Partitioner抽象类 分类器
----默认调用是实现类HashPartitioner
*4、Combiner组件:合并组件
小结果统计
reducetask----结果文件
1、FileOutputFormat
----默认调用是实现类:TextOutputFormat
2、RecordWriter
----默认调用是实现类:LineRecordWriter
WordCountMapper.java
package com.lyu.edu.wordcount;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
/**
* Mapper:将每个单词拆出来,并且进行打标签1
* mapper:最终的输出:<单词,1>
* Mapper:类中的四个泛型:
* 你的输入就是需要统计的文件的,这个文件应该以那种方式给你 最好的方式是一行给你一次
* 输入:一行的内容
*
* KEYIN:输入的key的泛型 这里指的是每一行的偏移量 mapreduce底层文件输入依赖流的方式
* 字节流方式 \r\n 记录的是每一行的起始偏移量 一般情况下没啥用 lang
* VALUEIN:输入的值的类型 这里指的是一行的内容 String
* KEYOUT:输出的key的类型 指的就是单词 String
* VALUEOU:输出的value的类型 指的就是标记1 便于统计 Int
* @author wanzhaocheng
* 当数据需要持久到磁盘或者进行网络传输的时候必须进行序列化和反序列化
* 序列化:原始数据----二进制
* 反序列化:二进制----原始数据
*
* mapreduce处理数据的时候必然经过持久化磁盘或者网络传输,那么数据必须序列化 反序列化
* java中的序列化接口serialization 连同类结构一起进行序列化和反序列化的
* java中的序列化和反序列化过于累赘,所以hadoop弃用java中的这一套序列化反序列化的东西
* hadoop为我们提供了一套自己的序列化和反序列化的接口Writable 优点:轻便
* 对应的j8中基本的数据类型和String类都帮我们实现好了Writanle接口
* int----IntWritable
* long---LongWritable
* double---DoubleWritable
* String---Text
* null---NullWritable
*
* java中的数据类型转化为hadoop中的对应数据类型 new 该类型 (java中的值)
* hadoop中的类型转化为java中的类型 .get()
*
*/
//对行的每个单词标记"1"
//<行的起始偏移量,行的内容,输出的单词,输出的标记"1"> 输出:
public class WordCountMapper extends Mapper{
/**这个方法调用频率:一行调用一次
* LongWritable key:每一行的起始偏移量
* Text value:每一行的内容
* Context context:上下文对象 用于传输的对象
*/
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//获取到每一行的内容,进行每个单词的切分 将每个单词加标签"1"
String line = value.toString();
//对这一行内容切分:切分出每一个单词
String[] words = line.split("\t");
//循环遍历单词数组:写入context
for(String w : words){
context.write(new Text(w), new IntWritable(1));//输出样例:
}
}
}
WordCountReducer.java
package com.lyu.edu.wordcount;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
/**
*
* reduce处理的是map的结果
* KEYIN: Reducer输入的类型----Mapper输出的是key的类型 Text
* VALUEIN: Reducer输入的value的类型----Mapper输出的value的类型 IntWritable
* 输出应该是reducer最终处理完的业务逻辑的输出 hello,67 spark,56
* KEYOUT: reducer统计结果的key的类型----这里指的是最终统计完成的单词 Text
* VALUEOUT:reducer统计结果的value的类型----这里指的是单词出现的总次数 IntWritable
* @author wanzhaocheng
*/
//对每个单词进行合并统计
public class WordCountReducer extends Reducer{
/* map端输出的结果:
*
* reduce端想要对这种结果进行统计,最好相同的单词在一起,事实上确实是在一起的
* map端输出的数据到达reduce端之前就会对数据进行一个整理,这个整理是框架做的,这个整理就是分组
* 框架内部会进行一个分组,按照map输出的key进行分组 key相同的为一组
* map端输出的数据中有多少不同的key 就有多少组
*/
/**
* 这个方法的调用频率:
* 每一组调用一次
* Text key:每一组中的那个相同的key
* Iterable values:每一组相同的key对应的所有的value值
* Context context:上下文对象 用于传输 写到hdfs
*/
@Override
protected void reduce(Text key, Iterable values,Context context)
throws IOException, InterruptedException {
//统计结果 循环遍历values 求和
int sum = 0;
for(IntWritable i:values){
sum+=i.get();//hadoop类型转换java
}
context.write(key, new IntWritable(sum));
}
}
Driver.java
package com.lyu.edu.wordcount;
import java.io.IOException;
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.mapred.lib.CombineTextInputFormat;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
/**
* 驱动类:代码的执行和map与reduce的组装
* @author wanzhaocheng
* job:mapreduce中一个计算程序就是一个job
*/
public class Driver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
// 加载配置文件
Configuration conf = new Configuration();
//启动一个Job 作用:封装计算程序的mapper和reducer,封装输入和输出
Job job = Job.getInstance(conf);
//设置的是计算程序的主驱动类 运行的时候会打成jar包运行
job.setJarByClass(Driver.class);
//设置Mapper和Reducer类
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
//设置mapper的输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//设置reducer的输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//设置reduceTask
//设置启动reduceTask的数量 ,将产生三个输出结果 ,,三个文件合并在一起就是最终统计结果
//不设置默认reduceTask是一个
// job.setNumReduceTasks(2);
//设置自定义的分区类
// job.setPartitionerClass(MyPartition.class);
/*
//设置mapTask
//设置文件输入类 CombineFileInputFormat 合并文件进行maptask任务划分
job.setInputFormatClass(CombineTextInputFormat.class);
CombineTextInputFormat.setMaxInputSplitSize(job, 10*1024*1024);
//设置切片的大小 单位Byte 修改最大值最终得到的切片大小《块大小
FileInputFormat.setMaxInputSplitSize(job, 330);
FileInputFormat.setMinInputSplitSize(job, 200*1024*1024);
//切片大小改成200M最终被启动的mapTask个数 8个
*/
//设置输入路径和输出路径
//输入路径:需要统计的单词的路径
FileInputFormat.addInputPath(job, new Path(args[0]));//args[0]代表运行代码的时候控制台手动输入的参数 第一个参数
//输出路径:最终结果输出的路径 注意:输出路径一定不能存在 hdfs怕把原来的文件覆盖了 所有要求传入的路径必须是全新的路径
FileOutputFormat.setOutputPath(job, new Path(args[1]));//第二个参数
//job提交
job.waitForCompletion(true);//true:是否打印日志
}
}
//maptask任务并行度:分布式运行了多少个任务
最终程序运行多少个maptask任务和什么有关??
在提交代码的时候会出现两个进程:
在FileInputFormat中有一个getSplits()方法,作用就是获取每一个split切片,决定每个maptask的任务划分
split:切片 只是进行一个逻辑划分并没有真正的进行数据切分
一个maptask任务最终对应到数据上就是一个split切片
块:128M 不同数据块可能存储在不同的数据节点上
逻辑切片大小是多少合适??
切片和块的关系??
平时用的文件输入类是FileInputFormat,FileInputFormat默认会调用TextInputFormat,TextInputFormat对小于128M的文件不能进行文件合并,,所以使用CombineFileInputFormat中的CombineTextInputFormat实现类
CombineTextInputFormat:能实现合并文件的文本类
在Driver类设置如下:
//设置文件输入类
job.setInputFormatClass(CombineTextInputFormat.class);
//设置切片大小为10M
CombineTextInputFormat.setMaxInputSplitSize(job, 10*1024*1024);//10M小于默认块大小128M,使用setMaxInputSplitSize()方法,反之使用setMinInputSplitSize()方法
//设置输入路径
CombineTextInputFormat.addInputPath(job, new Path(args[0]));
//设置启动reduceTask的数量为3
job.setNumReduceTasks(3);
默认分区:
//实现类中默认的分区方法
/*
*key:map端输出的key
*value:map端输出的value
*numReduceTasks:reducetask的个数 job.setNumReduceTasks(3)
*/
public int getPartition(K key, V value, int numReduceTasks){
//key.hashCode()&Integer.MAX_VALUE key.hashCode()如果超过Integer的最大值则取低32位
return (key.hashCode()&Integer.MAX_VALUE) % numReduceTasks; //结果:0、1、2
//最终通过这个函数,不同的余数进入到不同的reducetask中进行计算
}
通过上面的方法进行分区之后,就可以保证相同的key进入到同一个的reducetask中
自定义分区:
package com.lyu.edu.partition;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
/**
* 自定义分区规则
* 指定key为a-j为一个分区进入reduceTask0 k-z为一个分区进入reduceTask1
* @author wanzhaocheng
*key:map输出的key类型
*value:map输出的value的类型
*/
public class MyPartition extends Partitioner{
/**
* Text key: map输出的key
* IntWritable value: map输出的value
* int numPartitions: 分区个数 job.setNumrReduceTasks()
*/
@Override
public int getPartition(Text key, IntWritable value, int numPartitions) {
String k = key.toString();
char kc = k.charAt(0);//转化为单个字符
if(kc>='a'&&kc<'j'){
return 0;//指定key为a-j为一个分区进入reduceTask0
}else{
return 1;//指定key为 k-z为一个分区进入reduceTask1
}
}
}
在Driver类设置如下:
//设置reduceTask启动数量
job.setNumReduceTasks(2);
//设置自定义的分区类
job.setPartitionerClass(MyPartition.class);
自定义分区时的一些问题:
分区个数为3时:
reduceTask启动1个:最终输出一个文件----所有的统计结果
reduceTask启动3个:三个文件对应每个分区,一个分区对应一个reducetask
reduceTask启动2个:报错! 三个分区的数据2个reducetask进行处理,没有办法分配任务,就汇报非法分区
reduceTask启动数量>3:可以!只有前三个有数据,其他的都是空文件
分区的返回值:和reducetask的id对应
返回0:对应reducetask0,=》对应最终结果文件part-r-0000
返回1:对应reducetask1,=》对应最终结果文件part-r-0001
返回2:对应reducetask2,=》对应最终结果文件part-r-0002
数据倾斜
reducetask不设置的情况下默认只有一个
reducetask并行度最终会影响到程序的总体执行效率,reducetask在进行任务分配的时候一定要特别小心
数据倾斜:每个reducetask处理的数据不均匀
数据倾斜造成的直接后果:影响代码的整体执行效率
怎么避免数据倾斜:合理设计分区
默认情况下没有Combiner组件 map----combiner—reduce
作用:减少reduce端的数据量 减少shuffle过程的数据量
注意:combiner不参与业务逻辑 仅仅相当于map到reduce中间的一个优化组件
public class MyCombiner extends Reducer<前两个泛型map输出,后两个泛型reduce输入>{}
2)重写reduce方法
//Combiner本质上相当于在map端进行了一次reduce操作 通常情况下直接使用reducer的方法
job.setCombinerClass(WordCountReducer.class)//Driver引入自定义Combiner
注意:
combiner针对的是单个maptask----切片
不可以对多个maptask的结果进行合并
什么情况下不能使用Combiner组件??
求和-----可以
最大值----可以
最小值----可以
平均值----不可以
maptask----reducetask之间框架默认加了排序
排序规则:按照map端输出key的字典顺序进行排序
需求:将单词统计中词频出现的次数进行排序,,按照出现的次数从低到高
解决:词频应该放在map输出key的位置
注意:排序尽量避免在reduce端进行排序
自定义计数器:
应用场景:全局变量的时候会使用
用来统计job运行过程的进度和状态,类似于job运行的一个报告/日志
在生产中使用自定义的全局计数器一般用来统计不规则数据的数据量
自定义全局计数器:
需求:运用全局计数器统计以下数据的总行数和总字段数
//自定义计数器是一个枚举类
public enum MyCounter{
LINES,
COUNT
}
//统计总行数和统计总字段数
public class MyMapper extends Mapper{
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//获取计数器
Counter lines_counter = context.getCounter(MyCounter.LINES);
//对计数器进行总行数统计 increment(1L) 类似于累加操作,每次加1
lines_counter.increment(1L);
//获取下一个计数 统计总字段数
Counter lines_counter = context.getCounter(MyCounter.LINES);
String[] datas = value.toString().split("\t");
counts.increment(datas.length);
//NullWritable:由于不需要resucer,所以输出为null
}
}