maxsize (切片最大值) : 参数如果调得比blockSize小,则会让切片变小,而且就等于配置的这个参数的值
minsize (切片最小值) : 参数调的比blockSize大, 则可以让切片变得比blockSize还大。
//获取切片的文件名称
String name = inputSplit.getPath() . getName () ;
//根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context. getInputSplit() ;
思考:在运行MapReduce程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。那么,针对不同的数据类型,MapReduce是如何读取这些数据的呢?FileInputFormat常见的接口实现类包括: TextInputFormat、 KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat 和自定义InputFormat等
TextInputFormat是默认的FileInputFormat实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text类型。
框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。
需求:将输入的大量小文件合并成一个切片统一处理
在资料给的\inputcombinetextinputformat
文件夹下,有四个小文件案例
同样是之前的程序,Driver下的输入输出路径修改一下。看一下默认是多少个mapTask
//设置输入路径输出路径
FileInputFormat.setInputPaths(job, new Path("F:\\11_input\\inputcombinetextinputformat"));
FileOutputFormat.setOutputPath(job, new Path("F:\\hadoopDemo\\outputCombine1"));
运行得知,默认情况下reduceTask就是1
在源程序修改添加以下代码:
//如果不设置InputFormat,它默认使用的是TextInputFormat
job.setInputFormatClass(CombineTextInputFormat.class);
//虚拟存储切片最大值设置为4m
CombineTextInputFormat.setMaxInputSplitSize(job,4194304);
运行后,观察日志情况:
number of splits:3
如果改成20m呢?
//如果不设置InputFormat,它默认使用的是TextInputFormat
job.setInputFormatClass(CombineTextInputFormat.class);
//虚拟存储切片最大值设置为4m
CombineTextInputFormat.setMaxInputSplitSize(job,20971520);
按照计算规则,切片就会变成1片,1个maptask和1个reducetask
后面在处理小文件的时候经常使用,CombineTextInputFormat,经常用在生产环境下。
上面的流程是整个MapReduce最全工作流程,但是Shuffle过程只是从第7步开始到第16步结束,具体Shuffle过程详解,如下:
MapTask收集我们的map()方法输出的kv对,放到内存缓冲区中
从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
多个溢出文件会被合并成大的溢出文件
在溢出过程及合并的过程中,都要调用Partitioner进行分区和针对key进行排序
ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据
ReduceTask会抓取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)
合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的reduce()方法)
注意:
(1)Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。
(2)缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb默认100M。
Shuffle(混洗/洗牌)在map方法之后,reduce方法之前这段过程。
面试重点!
环形缓冲区100M,如果传过来128M,最多也就2次溢出?
但是环形缓冲区100M并不是只存储数据,它还有一半是元数据,所以100M并不是都用来存文件的。所以会出现溢出(溢写情况)
要求将统计结果按照条件输出到不同文件中(分区)。比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
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() & Integer.MAX_VALUE
在第一个wordcount案例代码基础上加入job.setNumReduceTasks(2);
public class WordCountDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
//获取job
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
//设置jar包路径
job.setJarByClass(WordCountDriver.class);
//关联mapper,reducer
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
//设置map输出的kv类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//设置最终输出的map的kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
job.setNumReduceTasks(2);
//设置输入路径输出路径
FileInputFormat.setInputPaths(job, new Path("F:\\11_input\\inputword"));
FileOutputFormat.setOutputPath(job, new Path("F:\\hadoopDemo\\outputPartitioner"));
//提交job
boolean completion = job.waitForCompletion(true);
System.exit(completion ? 0 : 1);
}
}
运行后发现
文件变成2个了。
里面的内容被分开,如果我希望将banzhang和cls在同一个分区应如何控制?
默认分区是根据key的hashCode对Reduce Tasks个数取模得到的。用户没法控制哪个key存储到哪个分区。
但是我们可以重写getPartition方法自己实现
那么如何自定义?
自定义类继承Partitioner,重写getPartition()方法
public class Cus tomPartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition (Text key, FlowBean value,int numPartitions) {
//控制分区代码逻辑
return partition;
}
}
在Job驱动中,设置自定义Partitioner .
job.setPartitionerClass(CustomPartitioner.class);
自定义Partition后,要根据自定义Partitioner的逻辑设置相应数量的ReduceTask
job.setNumReduceTasks(5);
设置多少个,后面再讲。
需求:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
数据是上一次计算手机流量的数据文件
在完成案例前,我先总结一下Map,分区,Reduce之间K,V的关系
分区的Key,Value应该是Map的K,V
创建ProvincePartitioner类
代码如下:
public class ProvincePartitioner extends Partitioner<Text,FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
return 0;
}
}
完成分区代码
public class ProvincePartitioner extends Partitioner<Text,FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
//text手机号
String phone = text.toString();
//得到前3位
String prePhone = phone.substring(0, 3);
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;
}
return partition;
}
}
第二步:回到Driver驱动类,设置分区类,设置任务数量job.setPartitionerClass(ProvincePartitioner.class); //在ProvincePartitioner里设置的就是5个 job.setNumReduceTasks(5);
/**
* 驱动代码
* @author:whd
* @createTime: 2021/11/12
*/
public class FlowDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
//1.获取job
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2.设置jar
job.setJarByClass(FlowDriver.class);
//3.关联mapper Reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReduce.class);
//4.设置mapper 输出key和value类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
//5.设置最终数据输出的key和value类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
job.setPartitionerClass(ProvincePartitioner.class);
//在ProvincePartitioner里设置的就是5个
job.setNumReduceTasks(5);
//6.设置数据的输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path("F:\\01centos\\资料\\11_input\\inputflow"));
FileOutputFormat.setOutputPath(job, new Path("F:\\hadoopDemo\\outputPartitioner3"));
//7.提交Job,true打印的日志信息更多
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
输出结果,5个!
这就是自定义分区案例,可以输出多个文件。
job.setNumReduceTasks(5);
这里设置的是5,如果我不设置5.设置别的行不行?
假设设置4,会抛出异常Caused by: java. io. IOException Create breakpoint : IlLegal partition for 18271575951
剩下一个不知道往哪个分区写。
那如果设置1呢?job.setNumReduceTasks(1);
可以正常运行
底层是走了partition - 1 的默认分支,压根不走你的分区设置。
也就是说,设置1可以,设置2、3、4报IO异常,设置5正常。
设置6会怎么样?能够正常运行。
但是最后一个文件是个0kb的空文件。
总结:
分区总结
- 如果ReduceTask的数量 > getPartition的结果数, 则会多产生几个空的输出文件art-1-000xx;
- 如果1< ReduceTask的数量 < getPatition的结果数,则有一部分分区数据无处安放, 会Exception;
- 如果ReduceTask的数量 = 1,则不管MapTask端输出多少个分区文件,最终结果都交给这-一个ReduceTask,最终也就只会产生一个结果文件 pat-0000;
- 分区号必须从零开始,逐一 累加。
例如:假设自定义分区数为5,则
- job.setNumReduceTasks(1);会正常运行, 只不过会产生一个输出文件
- job setNumReduceTasks(2);会报错
- job. setNumReduceTasks(6);大于5,程序会正常运行,会产生空文件
回忆MapReduce的过程
在环形缓冲区溢写之前,要对数据进行快排,会产生多次的溢写文件。多次的溢写文件,我们需要再次对它归并。
在整个MapReduce当中,MapTask阶段执行了2次排序。
接下来到Reduce阶段
Map阶段结束之后,Reduce阶段主动的去拉取对应的数据,拉取自己分区的数据过来之后,它需要对拉取过来的数据进行一次归并排序。
总结:在map阶段进行了一次快排一次归并,在reduce进行了一次归并
接下来我们就来研究一下这个排序
排序是MapReduce框架中最重要的操作之一。
MapTask和ReduceTask均会对数据按照key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。
默认排序是按照字典顺序排序,且实现该排序的方法是快速排序
不是进来一条数据,就对你进行排序,它是缓冲区到达一定阈值后,要往磁盘上溢写之前进行一次排序。
这个排序的过程是在内存当中完成的。
对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘.上所有文件进行归并排序。
最终无论你的数据在内存还是在磁盘,都要进行一次统一的归并排序。因为我们要将相同的key传到reducer方法里面去。只有统一的全都排好序,效率才能是最高,否则的话,每一次获取key都要遍历所有。(如果这是10亿个key,每次要遍历10亿遍)
对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成- -一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一-次合并后将数据溢写到磁盘 上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一-次归并排序。
**部分排序:**MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。
比如说手机号,第一个分区文件,里面的手机号内容,137xxx后面是按一定顺序的。它只考虑单个分区文件内部是有序的。
使用场景:XX地区销售额前10
**全排序:**最终输出结果只有一个文件,且文件内部有序。实现方式是只设置-个ReduceTask。 但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。
比如说:wordcount案例,只有一个文件。
这种在生产环境慎用,实际数据量非常大,都在一个reduce里是处理不了的。因此全排序用的可能性不大。
辅助排序 – GroupingComparator分组 , 用的较少。
**二次排序:**在自定义排序过程中,如果compareto(java里的一个可重写的方法)中的判断条件为两个即为二次排序。
比如说:手机总流量排序,如果总流量相同,按上行流量倒序,如果还相同,下行流量倒序。
自定义排序也叫二次排序
bean对象做为key传输,需要实现WritableComparable接口重写compareTo方法,就可以实现排序。
回忆,之前序列化实现的是writable接口
WritableComparable
接口,继承了Writable也继承了Comparable
根据案例2.3序列化案例产生的结果再次对总流量进行倒序排序。
即writable包下的案例
比如说某地区的销售量top10
hadoop框架默认对key进行排序。
如果要对flowBean排序,那么就应该吧flowbean放到key里;如果按照之前,就是将手机号作为key,flowBean作为value。
原先的案例无法实现对flowBean的排序,我们得把flowBean作为key,原先的key作为value。再给到mapreduce。
实现方法:
@Override
public int compareTo (FlowBean o) {
//倒序排列,按照总流量从大到小
return this. sumFlow > o. getSumFlow() ? -1 : 1;
}
Mapper类
context.write(bean,手机号)
Reducer类
//循环输出,避免总流量相同情况
for (Text text : values){
context. write. (text,key) ;
}
先建一个包:writableComparable
将writable包下的类复制过来。
public class FlowBean implements WritableComparable
泛型,是比较对象。跟FlowBean自己比(第一次传进来和第二次传进来的值进行比较),因此写FlowBean
重写CompareTo方法
@Override
public int compareTo(FlowBean o) {
//总流量倒序排序
if (this.sumFlow > o.sumFlow) {
return -1;
} else if (this.sumFlow < o.sumFlow) {
return 1;
}else{
return 0;
}
}
接下来处理Mapper
第二个Text和最后的FlowBean需要互换位置
public class FlowMapper extends Mapper<LongWritable, Text,Text, FlowBean>
修改如下:
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, Mapper<LongWritable, Text, FlowBean, Text>.Context context) throws IOException, InterruptedException {
//获取一行
String line = value.toString();
//切割
String[] split = line.split("\t");
//封装
outV.set(split[0]);
outK.setUpFlow(Long.parseLong(split[1]));
outK.setDownFlow(Long.parseLong(split[2]));
outK.setSumFlow();
//写出
context.write(outK,outV);
}
}
接下来是FlowReduce
输出的key和value同样也需要修改
public class FlowReduce extends Reducer<Text, FlowBean,Text, FlowBean>
修改如下
/**
* 如果有值相同,那么就会进入到同一个reducer
* @author:whd
* @createTime: 2021/11/12
*/
public class FlowReduce extends Reducer<FlowBean, Text,Text, FlowBean> {
@Override
protected void reduce(FlowBean key, Iterable<Text> values, Reducer<FlowBean, Text, Text, FlowBean>.Context context) throws IOException, InterruptedException {
for (Text value : values) {
//key,value颠倒
context.write(value,key);
}
}
}
接下来是Driver
public class FlowDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
//1.获取job
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
//2.设置jar
job.setJarByClass(FlowDriver.class);
//3.关联mapper Reducer
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReduce.class);
//4.设置mapper 输出key和value类型
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
//5.设置最终数据输出的key和value类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//6.设置数据的输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path("F:\\hadoopDemo\\output2"));
FileOutputFormat.setOutputPath(job, new Path("F:\\hadoopDemo\\outputWritableComparable"));
//7.提交Job,true打印的日志信息更多
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
然后上次这个案例作为输入路径(F:\\hadoopDemo\\output2
)
执行,发现安装总流量降序排序
但是发现最后3个,我希望总流量相同时,按照上行流量排序。
修改比较方法即可
@Override
public int compareTo(FlowBean o) {
//总流量倒序排序
if (this.sumFlow > o.sumFlow) {
return -1;
} else if (this.sumFlow < o.sumFlow) {
return 1;
} else {
if (this.upFlow > o.upFlow) {
return 1;
} else if (this.upFlow < o.upFlow) {
return -1;
} else {
return 0;
}
}
}
这就叫二次排序
1)需求
要求每个省份手机号输出的文件中按照总流量内部排序。
2)需求分析
基于前一个需求,增加自定义分区类,分区按照省份手机号设置
新建一个包partitionerandwritablecomparable
将包writableComparable下的内容复制过去
创建类ProvincePartitioner2
public class ProvincePartitioner2 extends Partitioner<FlowBean, Text> {
@Override
public int getPartition(FlowBean flowBean, Text text, int numPartitions) {
String phone = text.toString();
//手机号前3位
String prePhone = phone.substring(0, 3);
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;
}
return partition;
}
}
在FlowDriver下新增
//设置分区
job.setPartitionerClass(ProvincePartitioner2.class);
//设numReduceTasks
job.setNumReduceTasks(5);
Combiner会将你排序好的内容,比如说合并成
好处:现在有一万个的数据,如果不进行处理,向reduce处理需要传输一万条。如果合并了,只需要传输。效率提升一万倍。在大数据场景下,map的个数相对来说较多,reduce聚合的个数要小一些。除此之外,map阶段一个maptask只处理128M的数据,反观reducetask,它要将你map阶段所有数据都汇总过来进行一个聚合。所以reducetask明显更加繁忙,maptask更轻松一些。
所以map阶段能帮reduce提前处理一部分,整体效率就会提升。
所以在前部会有选择性的开启Combiner。有选择性是指,不是所有的mapreduce都能开启(前提条件后面讲)
Combiner完就归并,归并完之后还能进行Combiner,再进行一次Combiner前提条件是前边的溢写条件有很多,第一次进行归并的时候没有一次性归并完,比如30个文件,10个10个归并,变成3个文件,这3个文件仍然需要一次combiner。
总之写到磁盘前的文件,都已经combiner完成了。
总结:
Combiner是MR程序中Mapper和Reducer之外的一种组件
Combiner组件的父类就是Reducer。
Combiner和Reducer的区别在于运行的位置。
Combiner运行于map阶段,maptask非常多,每一个Combiner只单独负责一个maptask的文件。(map是在每一个maptask所在结点运行,reduce是接收全局所有mapper的输出结果)
Combiner的意义就是对每一个MapTask的输出进行局部汇总以减小网络传输量(降级网络传输)
Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv应该跟Reducer的输入kv类型要对应起来。
求和场景是可以的。
统计过程中对每一个MapTask的输出进行局部汇总,以减小网络传输量即采用Combiner功能
增加一个包combiner
将wordcount包下内容复制过去
新建类WordCountCombiner
/**
* Combiner类
* 这里的输入key和输入value是map阶段的输出
* @author:whd
* @createTime: 2021/11/14
*/
public class WordCountCombiner extends Reducer<Text, IntWritable,Text, IntWritable> {
//封装
private IntWritable outV = 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();
}
outV.set(sum);
context.write(key,outV);
}
}
修改WordCountDriver
public class WordCountDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
//1. 获取job
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
//2. 设置jar包路径
job.setJarByClass(WordCountDriver.class);
//3. 关联mapper,reducer
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
//4. 设置map输出的kv类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//5. 设置最终输出的map的kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//设置Cobiner
job.setCombinerClass(WordCountCombiner.class);
//6. 设置输入路径输出路径
FileInputFormat.setInputPaths(job, new Path("F:\\11_input\\inputword"));
FileOutputFormat.setOutputPath(job, new Path("F:\\hadoopDemo\\outputcombiner1"));
//7. 提交job
boolean completion = job.waitForCompletion(true);
System.exit(completion ? 0 : 1);
}
}
观察一下输入数据
运行结果
Map input records=7 Map output records=10
Map-Reduce Framework
Map input records=7
Map output records=10
Map output bytes=95
Map output materialized bytes=88
Input split bytes=102
Combine input records=10
Combine output records=7
Reduce input groups=7
Reduce shuffle bytes=88
Reduce input records=7
Reduce output records=7
Spilled Records=14
Shuffled Maps =1
Failed Shuffles=0
Merged Map outputs=1
GC time elapsed (ms)=0
Total committed heap usage (bytes)=771751936
如果
//我将reduce阶段设为没有
job.setNumReduceTasks(0);
有什么变化?没有聚合!
回忆Shuffle阶段:
如果你将reduce阶段去掉,那么shuffle之后的阶段都没了,因为shuffle阶段是map方法输出,到reduce方法之前这段混洗的过程。如果没有reduce就不需要混洗。就没有combiner了。
观察发现WordCountCombiner和WordCountReducer内容是一模一样的。
直接写这一句,也能正常运行。
job.setCombinerClass(WordCountReducer.class);
所以,以后用Combiner直接用Reducer即可,代码是一样的。