一、InputFormat数据输入
1.1 切片于MapTask并行度决定机制
MapTask的并行度决定Map阶段的任务处理并发度,进而影响到整个Job的处理速度。
那么:
1G的数据,启动8个MapTask,可以提高集群的并发处理能力。
那么1K的数据,也启动8个MapTask,会提高集群性能吗?
MapTask并行任务是否越多越好呢?哪些因素影响了MapTask并行度?
先了解两个概念:
- 数据块:数据块(Block)是HDFS物理上把数据分成一块一块的。
- 数据切片:数据切片只是逻辑上对输入数据进行分片,并不会在磁盘上将其切分成片进行存储。
我们有一个300M的文件,分别存储在节点1(128M)、节点2(128M)、节点3(44M)上
- 假设切片大小设置为100M
集群物理上存储的一个Block是128M(hadoop2.x默认128M,hadoop1.x默认64M,本地是32M)。那么当一个节点1运行MapTask时,处理100M,那么剩下的28M就要传输到节点2运行处理,节点2处理这传输过来的28M,还要处理本地72M,好要将本地的56M传输到节点3上运行处理,节点3就处理这传输过来的56M还要处理自身的44M。当切片大小设置为100M时,会涉及大量的网络IO。- 假设切片大小设置为128M时
这样每个节点都只负责处理自身的那128M,不用网络传输到其他节点上处理。
经过上述分析,我们得出当我们的切片大小于数据块大小一致时,运行效果是最好的。
事实上:
- 默认情况下,切片大小=BlockSize
- 一个Job的Map阶段的并行度由客户端在提交Job时的切片数决定
- 每个切片分配一个MapTask并行处理
- 切片时,不考虑数据集整体,而是逐个针对每一个文件单独切片
1.2 Job提交流程源码解析
// 提交运行job,true:打印信息
job.waitForCompletion(true);
// 1.提交:
// public static enum JobState {DEFINE, RUNNING};
// DEFINE代表没有正在运行的job,当有job运行时,将 DEFINE 置为 RUNNING
if (state == JobState.DEFINE) {
submit();
}
// 1.1 确保当前集群状态为 DEFINE ,否则会抛出异常
ensureState(JobState.DEFINE);
// 1.2 默认使用新的api替换旧的api,除非显式设置它们,或者使用旧的mapper或reduce属性。
setUseNewAPI();
// 1.3 建立连接
connect();
// 1.3.1 创建集群
new Cluster(getConfiguration());
// 1.3.1.1 集群初始化,判断连接的是yarn还是本地
initialize(jobTrackAddr, conf);
// 1.4 提交job的详情信息
submitter.submitJobInternal(Job.this, cluster);
// 1.4.1 校验输出路径:确认输出路径已经被设置好并不存在(存在或没设置则抛出异常)
checkSpecs(job);
// 1.4.1.1 创建给集群提交数据的Stag路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 1.4.2 获取jobid
JobID jobId = submitClient.getNewJobID();
// 1.4.3 拷贝jar包到集群(本地不需要拷贝)
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
// 并创建Job路径(名字是上面获取到的jobid)
FileSystem.mkdirs(jtFs, submitJobDir, mapredSysPerms);
// 1.4.4 计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
// 进行切片(InputSplit中)只记录了切片的元数据信息,比如起始位置、长度以及所在节点列表等
List<InputSplit> splits = input.getSplits(job);
// 判断是否可以切割(文件的解码方式是否支持切割,并不是所有的文件都可以切割)
isSplitable(job, path)
// 数据块大小(hadoop2.x默认128M,hadoop1.x默认64M,本地是32M)
long blockSize = file.getBlockSize();
// getFormatMinSplitSize() = 1;
// getMinSplitSize(job) = job.getConfiguration().getLong(SPLIT_MINSIZE, 1L);
// SPLIT_MINSIZE:mapreduce.input.fileinputformat.split.minsize(mapred-default.xml, 默认为0)
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
// getMaxSplitSize(job) = context.getConfiguration().getLong(SPLIT_MAXSIZE, Long.MAX_VALUE);
// SPLIT_MAXSIZE: mapreduce.input.fileinputformat.split.maxsize
// Long.MAX_VALUE = 9223372036854775807
long maxSize = getMaxSplitSize(job); // 9223372036854775807
// 计算切片大小
computeSplitSize(blockSize, minSize, maxSize);
// splitSize 计算公式
// 当 mapreduce.input.fileinputformat.split.minsize 比 blockSize 大时,切片会变大
// 当 mapreduce.input.fileinputformat.split.maxsize 比 blockSize 小时,切片会变小
Math.max(minSize, Math.min(maxSize, blockSize));
// 当前文件大小
long bytesRemaining = length;
// SPLIT_SLOP = 1.1;每次切片时,要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就会分成一块
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP)
// 1.4.5 Write job file to submit dir(写job文件到提交目录)
writeConf(conf, submitJobFile);
// 向Stag路径写XML配置文件(所有的配置信息都在这里面)
conf.writeXml(out);
// 1.4.6 提交job并执行,返回提交状态
status = submitClient.submitJob(
jobId, submitJobDir.toString(), job.getCredentials());
// 1.4.7 删除之前创建的文件(split(切片信息)、xml配置文件、jar包)
jtFs.delete(submitJobDir, true);
// 2.打印信息:verbose为 waitForCompletion 提交的值
if (verbose) {
monitorAndPrintJob();
}
画作图:
简而言之步骤如下:
1、提交job
2、建立链接(初始化集群、判断是本地运行还是yarn运行)
3、提交job的详细信息
- 校验输出路径:确认输出路径已经被设置好并不存在,创建给集群提交数据的Stag路径
- 获取jobid
- 拷贝jar包到集群(本地不需要拷贝)
- 计算切片,生成切片规划文件(遍历目录下的每一个文件进行切片,将切片信息写入切片规划文件中)
数据块大小(hadoop2.x默认128M,hadoop1.x默认64M,本地是32M)
splitSize 计算公式:Math.max(minSize, Math.min(maxSize, blockSize));
SPLIT_SLOP = 1.1;每次切片时,要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就会分成一块
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP)
- 写job文件到提交目录(向Stag路径写XML配置文件)
- 提交job并执行,返回提交状态
- 除之前创建的文件(split(切片信息)、xml配置文件、jar包)
1.3 FileInputFormat切片机制
上面1.2源码中1.4.4步中已经提到了,切片规则:
- 简单的按照文件内容长度进行切片
- 默认情况下:切片大小 = Block大小
- 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
切片大小计算公式:
Math.max(minSize, Math.min(maxSize, blockSize));
mapreduce.input.fileinputformat.split.minsize=1
默认值为1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue
;默认值Long.MAXValue
切片大小设置:
- 当 mapreduce.input.fileinputformat.split.minsize 比 blockSize 大时,切片会变大,且等于这个值
- 当 mapreduce.input.fileinputformat.split.maxsize 比 blockSize 小时,切片会变小,且等于这个值
1.4 CombineTextInputFormat切片机制(处理小文件)
框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,
都会是一个单独的切片,都会交给一个MapTask,
这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。
CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
虚拟存储切片最大值设置
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
// 4m
虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
1、虚拟存储过程
将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较
如果不大于设置的最大值,逻辑上划分一个块
如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块
当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)。
最后将每一块文件进行存储例如setMaxInputSplitSize值为4M,输入文件大小为8.02M
则先逻辑上分成一个4M。剩余的大小为4.02M,
如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02M文件切分成(2.01M和2.01M)两个文件。
最后存储的文件:4M、2.01M、2.01M
2、切片过程
- 判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片。
- 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
如:有4个小文件大小分别为1.7M、5.1M、3.4M以及6.8M这四个小文件,则虚拟存储之后形成6个文件块,大小分别为:
1.7M,(2.55M、2.55M),3.4M以及(3.4M、3.4M)
最终会形成3个切片,大小分别为:
(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M
代码案例:
只需要在Driver类中添加以下代码:
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
//虚拟存储切片最大值设置20m
CombineTextInputFormat.setMaxInputSplitSize(job, 20971520);
如我有两个几十K的小文件,不设置默认number of splits:2
,设置之后number of splits:1
1.5 FileInputFormat实现类
在运行MapReduce程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。针对不同的数据类型,有不同是实现类来读取数据;FileInputFormat常见的接口实现类包括:TextInputFormat
、KeyValueTextInputFormat
、NLineInputFormat
、CombineTextInputFormat
和自定义InputFormat
等。
1.5.1 TextInputFormat
TextInputFormat是默认的FileInputFormat实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text类型。
如一个文件以下四条记录:
Rich learning form
Intelligent learning engine
Learning more convenient
From the real demand for more close to the enterprise
经过Map后:
(0,Rich learning form)
(19,Intelligent learning engine)
(47,Learning more convenient)
(72,From the real demand for more close to the enterprise)
1.5.2 KeyValueTextInputFormat
每一行均为一条记录,被分隔符分割为<key,value>。可以通过在驱动类中设置conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR, “\t”);来设定分隔符。默认分隔符是tab(\t)。
如一个文件有以下内容:
line1 ——>Rich learning form
line2 ——>Intelligent learning engine
line3 ——>Learning more convenient
line4 ——>From the real demand for more close to the enterprise
——>代表制表符
则经过map后的键值对:
(line1,Rich learning form)
(line2,Intelligent learning engine)
(line3,Learning more convenient)
(line4,From the real demand for more close to the enterprise)
key为每行排在制表符之前的Text序列
代码案例:
只需要在Driver添加以下代码:
Configuration conf = new Configuration();
// 设置切割符
conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR, " ");
// 设置输入格式
job.setInputFormatClass(KeyValueTextInputFormat.class);
1.5.3 NLineInputFormat
如果使用NlineInputFormat,代表每个map进程处理的InputSplit不再按Block块去划分,而是按NlineInputFormat指定的行数N来划分。即输入文件的总行数/N=切片数,如果不整除,切片数=商+1。
如一个文件的内容如下:
Rich learning form
Intelligent learning engine
Learning more convenient
From the real demand for more close to the enterprise
指定函数N=2,每个输入分片包含两行,开始两个MapTask:
(0,Rich learning form)
(19,Intelligent learning engine)
另一个mapper则收到后两行
(47,Learning more convenient)
(72,From the real demand for more close to the enterprise)
key与value和TextInputFormat生成的一样
代码案例:
只需要在Driver添加以下代码:
// 7设置每个切片InputSplit中划分三条记录
NLineInputFormat.setNumLinesPerSplit(job, 3);
// 8使用NLineInputFormat处理记录数
job.setInputFormatClass(NLineInputFormat.class);
1.6 自定义InputFormat
步骤:
- 自定义类继承InputFormat
- 改写RecordReader,实现一次读取一个完整文件封装为KV
- 在输出时使用SequenceFileOutPutFormat输出合并文件
自定义InputFormat实现小文件的合并
需求:将多个小文件合并成一个SequenceFile文件(SequenceFile文件是Hadoop用来存储二进制形式的key-value对的文件格式),SequenceFile里面存储着多个文件,存储的形式为文件路径+名称为key,文件内容为value。
分析:
1、自定义类继承FileInputFormat
- 重写isSplitable(),返回false不可切割
- 重写createRecordReader(),创建自定义的RecordReader对象,并初始化
2、改写RecordReader,实现一次读取一个完整文件封装为KV
- 采用IO流一次读取一个文件来输出到value中,因为设置了不可切片,最终把所有文件都封装到了value中。
- 获取文件路径信息+名称,设置为key
3、设置Driver
// 设置输入输出
job.setInputFormatClass(WholeFileInputformat.class);
job.setOutputFormatClass(SequenceFileOutputFormat.class);
代码实现:
-
自定义InputForamt
public class WholeFileInputformat extends FileInputFormat<Text, BytesWritable> { @Override protected boolean isSplitable(JobContext context, Path filename) { return false; } @Override public RecordReader<Text, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException { WholeRecordReader wholeRecordReader = new WholeRecordReader(); wholeRecordReader.initialize(split, context); return wholeRecordReader; } }
-
自定义RecordReader类
public class WholeRecordReader extends RecordReader<Text, BytesWritable> { Text k = new Text(); BytesWritable v = new BytesWritable(); boolean isProgress = true; FileSplit split; Configuration configuration; @Override public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException { this.split = (FileSplit) split; this.configuration = context.getConfiguration(); } @Override public boolean nextKeyValue() throws IOException, InterruptedException { // 具体业务逻辑 if (isProgress){ FSDataInputStream fis = null; try { // 1.获取文件系统 Path path = split.getPath(); FileSystem fileSystem = path.getFileSystem(configuration); // 2.读取数据 fis = fileSystem.open(path); // 3.读取文件内容 byte[] bytes = new byte[(int)split.getLength()]; // 缓冲 IOUtils.readFully(fis, bytes, 0, bytes.length);// 将文件读取到缓冲数组 // 4.输出v v.set(bytes, 0, bytes.length); //将缓冲数组的内容写入V // 5.获取文件路径及名称 String name = split.getPath().toString(); k.set(name); }catch (Exception e){ }finally { IOUtils.closeStream(fis); } isProgress = false; return true; } return false; } @Override public Text getCurrentKey() throws IOException, InterruptedException { return k; } @Override public BytesWritable getCurrentValue() throws IOException, InterruptedException { return v; } @Override public float getProgress() throws IOException, InterruptedException { return 0; } @Override public void close() throws IOException { } }
-
mapper阶段
public class SequenceFileMapper extends Mapper<Text, BytesWritable, Text, BytesWritable> { @Override protected void map(Text key, BytesWritable value, Context context) throws IOException, InterruptedException { context.write(key, value); } }
-
reduce阶段
public class SequenceFileReducer extends Reducer<Text, BytesWritable, Text, BytesWritable> { @Override protected void reduce(Text key, Iterable<BytesWritable> values, Context context) throws IOException, InterruptedException { for (BytesWritable value : values) { context.write(key, value); } } }
-
Driver代码
public class SequenceFileDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { args = new String[]{"e:/input/", "e:/output/"}; // 1.获取job对象 Configuration configuration = new Configuration(); Job job = Job.getInstance(configuration); // 设置输入输出 job.setInputFormatClass(WholeFileInputformat.class); job.setOutputFormatClass(SequenceFileOutputFormat.class); // 2.设置jar包路径 job.setJarByClass(SequenceFileDriver.class); // 3.设置mapper与reducer job.setMapperClass(SequenceFileMapper.class); job.setReducerClass(SequenceFileReducer.class); // 4.设置mapper的输出 job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(BytesWritable.class); // 5.设置最终的输出 job.setOutputKeyClass(Text.class); job.setOutputValueClass(BytesWritable.class); // 6.设置输入输出路径 FileInputFormat.setInputPaths(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); // 7.提交job boolean b = job.waitForCompletion(true); System.exit(b ? 0 : 1); } }
二、Shuffle机制
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。
2.1 Partition分区
在开发中,遇到要将结果按照不通的条件输出到不同的文件中(分区),则需求重写分区方法;默认的分区方法:
public class HashPartitioner<K, V> extends Partitioner<K, V> {
// numReduceTasks 默认为1
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K key, V value,
int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
默认分区是根据key的hashCode对ReduceTasks个数取模得到的,用户无法控制那个key在那个分区。
自定义Partition分区:
- 自定义类继承
Partitioner
类,泛型为Map阶段的输出类型 - 重写
getPartition()
方法 - 设置
job
的Partitioner
为自定义 - 自定义
Partitioner
后,根据自定义的Partitioner
逻辑设置job
的ReduceTask数量。
修改7.3 统计流量案例:
需求:将手机号136、137、138、139开始的归为一个文件,其他的归为一个文件。
- 自定义
Partitioner
public class ProvincePartitioner extends Partitioner<Text, FlowBean> { @Override public int getPartition(Text k, FlowBean v, int numPartitions) { String head = k.toString().substring(0, 3); int partition = 4; if ("136".equals(head)){ partition = 0; }else if ("137".equals(head)){ partition = 1; }else if ("138".equals(head)){ partition = 2; }else if ("139".equals(head)){ partition = 3; } return partition; } }
- Driver添加如下代码:
// 设置分区为自定义分区 job.setPartitionerClass(ProvincePartitioner.class); // 指定reduce task数量 job.setNumReduceTasks(5);
分区总结:
- 如果ReduceTask的数量 > getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
- 如果1 < ReduceTask的数量 < getPartition的结果数,则有一部分分区数据无处安放,会Exception;
- 如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,最终也就只会产生一个结果文件 part-r-00000;
- 分区号必须从零开始,逐一累加。
2.2 WritableComparable排序
2.2.1 排序概述
MapTask和ReduceTask都会按照key进行排序,此操作属于Hadoop的默认行为。任何应用程序中均会被排序,而不管逻辑上是否需要。默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
MapTask排序:
对于MapTask,它会将处理的结果暂时放到环形缓冲区,当环形缓冲区使用率达到一定阈值后(80%),再对缓冲区的数据进行一次快速排序,并将这些数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
ReduceTask:
对于RedUceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序生成一个更大的文件;如果内存中文件大小或者数据超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
2.2.2 排序分类
- 部分排序
MapReduce根据输入记录的键值对数据集排序,保证输出的每个文件内部有序 - 全排序
最终输出结果只有一个文件,且文件内部有序。此方式实现需要只设置一个ReduceTask,但是处理大型文件效率极低。 - 辅助排序(GroupingComparator分组)
在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段不相同)的key进入到同一个reduce方法时,可以采用分组排序。 - 二次排序
在自定义排序的过程中,如果compareTo中的判断条件为两个即未二此排序(3个判断条件则为三次排序,以此类推)
2.2.3 WritableComparable排序案例
需求:对7.3 统计流量为例的输出结果,进行按照总流量大小降序排序,并将手机号136、137、138、139开始的归为一个文件,其他的归为一个文件。文件内部仍然有序。
分析:
- 使用的数据是7.3 统计流量为例的输出结果
- 按照总流量大小进行排序,而Hadoop默认会对key排序,故需要自定义序列化,包含上行流量、下行流量、总流量,而value为电话号码
- 排序需要使用CompareTo方法,故不再是实现Writable 接口,而是实现WritableComparable接口
- 将结果需要写入不同分区,故需要自定义Partition分区
- 设置相应的Reduce Task数量
代码:
- 自定义序列化
public class FlowBean implements WritableComparable<FlowBean> {
private long upFlow;
private long downFlow;
private long sumFlow;
public FlowBean(long upFlow, long downFlow) {
this.upFlow = upFlow;
this.downFlow = downFlow;
this.sumFlow = upFlow + downFlow;
}
public FlowBean() {
}
// 省略get与set方法
@Override
public int compareTo(FlowBean flowBean) {
int result;
if (sumFlow > flowBean.getSumFlow()){
result = 1;
}else if (sumFlow < flowBean.getSumFlow()){
result = -1;
}else {
result = 0;
}
return result;
}
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
@Override
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
}
- 自定义Partition分区
public class ProvincePartitioner extends Partitioner<FlowBean, Text> {
@Override
public int getPartition(FlowBean k, Text v, int numPartitions) {
String head = v.toString().substring(0, 3);
int partition = 4;
if ("136".equals(head)){
partition = 0;
}else if ("137".equals(head)){
partition = 1;
}else if ("138".equals(head)){
partition = 2;
}else if ("139".equals(head)){
partition = 3;
}
return partition;
}
}
- mapper阶段代码
public class FlowCountSortMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
FlowBean flowBeanKey = new FlowBean();
Text v = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
// 1.读一行数据
String line = value.toString();
// 2.分割数据
String[] fields = line.split("\t");
// 3.电话号码
String phoneNum = fields[0];
// 3.1获取上行流量、下行流量、总流量
long upFlow = Long.parseLong(fields[1]);
long downFlow = Long.parseLong(fields[2]);
long sumFlow = Long.parseLong(fields[3]);
flowBeanKey.setUpFlow(upFlow);
flowBeanKey.setDownFlow(downFlow);
flowBeanKey.setSumFlow(sumFlow);
v.set(phoneNum);
context.write(flowBeanKey, v);
}
}
- reducer阶段代码
public class FlowCountSortReducer extends Reducer<FlowBean, Text, Text, FlowBean> {
@Override
protected void reduce(FlowBean key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
for (Text value : values) {
context.write(value, key);
}
}
}
- driver代码
public class FlowCountSortDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
args = new String[]{"e:/input/flowsort.txt", "e:/output/"};
// 1.获取job
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 设置分区
job.setPartitionerClass(ProvincePartitioner.class);
// 设置reduce个数
job.setNumReduceTasks(5);
// 2.设置jar包
job.setJarByClass(FlowCountSortDriver.class);
// 3.设置mapper与reducer
job.setMapperClass(FlowCountSortMapper.class);
job.setReducerClass(FlowCountSortReducer.class);
// 4.设置mapper的输出
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
// 5.设置最终的输出
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 6.设置输入输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7.提交job
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
2.3 Combiner合并
概述:
- Combiner是MR程序中Mapper和Reducer之外的一种组件
- Combiner的组件父类就是Reducer
- Combiner和Reducer的区别在于运行的位置:Combiner是在每一个MapTask所在阶段运行,Reducer是接收全局所有Mapper的输出结果。
- Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量
- Combiner能够应用的前提是不能影响最终的业务逻辑,而且Combiner的输出kv应该与Reducer的输入KV对应。
- 自定义Combiner的步骤:自定义一个类继承Reducer,重新Reduce方法(其实就是和Reduce阶段一样,只是可能存在的处理逻辑有所区别)
将WordCount案例增加Combiner,在Reduce之前,对每个MapTask进行一次合并,僵尸网络传输。
添加类:
public class WordcountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
IntWritable v = 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();
}
v.set(sum);
context.write(key, v);
}
}
在driver中设置:
// 指定需要使用combiner,以及用哪个类作为combiner的逻辑
job.setCombinerClass(WordcountCombiner.class);
2.4 GroupingComparator分组(辅助排序)
对Reduce阶段的数据根据某一个或几个字段进行分组。
步骤:
- 自定义类继承WritableComparator
- 重写compare()方法
@Override public int compare(WritableComparable a, WritableComparable b) { // 比较的业务逻辑 return result; }
- 创建一个构造器将比较对象的类传递给父类
protected OrderGroupingComparator() { super(OrderBean.class, true); }
上手案例:
有如下数据:
订单id 商品id 成交金额
0000001 Pdt_01 222.8
0000002 Pdt_05 722.4
0000001 Pdt_02 33.8
0000003 Pdt_06 232.8
0000003 Pdt_02 33.8
0000002 Pdt_03 522.8
0000002 Pdt_04 122.4
需求:需要求出每一个订单中最贵的商品。
分析:
- 利用“订单id和成交金额”作为key,可以将Map阶段读取到的所有订单数据按照id升序排序,如果id相同再按照金额降序排序,发送到Reduce
- 在Reduce端利用groupingComparator将订单id相同的kv聚合成组,然后取第一个即是该订单中最贵商品
上手代码:
-
自定义bean,实现接口WritableComparable
public class OrderBean implements WritableComparable<OrderBean> { private int orderId; // 订单id号 private double price; // 价格 public OrderBean(int orderId, double price) { this.orderId = orderId; this.price = price; } public OrderBean() { } //省略get与set // 用于排序 @Override public int compareTo(OrderBean o) { // id升序 int i = this.orderId - o.getOrderId(); if (i == 0){ // 价格倒序 i = price > o.getPrice() ? -1 : 1; } return i; } @Override public void write(DataOutput out) throws IOException { out.writeInt(orderId); out.writeDouble(price); } @Override public void readFields(DataInput in) throws IOException { orderId = in.readInt(); price = in.readDouble(); } @Override public String toString() { return orderId + "\t" + price; } }
-
创建分组类,实现分组
public class OrderGroupingComparator extends WritableComparator { public OrderGroupingComparator(){ super(OrderBean.class, true); } // 分组 @Override public int compare(WritableComparable a, WritableComparable b) { OrderBean aBean = (OrderBean) a; OrderBean bBean = (OrderBean) b; // 订单id相同,key就相同 return aBean.getOrderId() - bBean.getOrderId(); } }
-
mapper
public class OrderMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable> { OrderBean orderBean = new OrderBean(); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { // 1/获取一行 String line = value.toString(); // 2.分割 String[] split = line.split("\t"); // 3.获取单号与价格 String orderId = split[0]; String price = split[2]; orderBean.setOrderId(Integer.parseInt(orderId)); orderBean.setPrice(Double.parseDouble(price)); context.write(orderBean, NullWritable.get()); } }
-
reducer
public class OrderReducer extends Reducer<OrderBean, NullWritable, OrderBean, NullWritable> { @Override protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException { context.write(key, NullWritable.get()); } }
-
driver中添加一下代码
// 设置为自定义的分组类 job.setGroupingComparatorClass(OrderGroupingComparator.class);
2.5 Shuffle机制
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。
Shuffle流程:
- Map方法之后,每个MapTask都会将数据写入环形缓冲区
缓冲区默认大小100M,双向写入(一个方向写数据kv,一个方向写元数据信息(包括index、partition、keystart、valuestart)) - 当环形缓冲区使用率达到80%,会对环形缓冲区的数据进行一次快速排序,而环形缓冲区则又反向缓冲,
- 经历过排序的数据,如果存在combiner,则会进行对分区内部的数据进行合并,当一个MapTask执行完毕后,将这个MapTask的所有溢出的数据进行归并排序(归并排序之后还可以进行Combiner),将归并的分区进行压缩写道磁盘
- 第三步结束的数据就是整个Map阶段的输出数据
- Reduce远程拷贝Map阶段输出的数据,将每个MapTask的相同分区合并成一个文件,写入自身的内存中,内存不够时溢出到磁盘,当所有数据拷贝完毕后,ReduceTask会统一对内存和磁盘上的所有数据进行一次归并排序
- 之后再根据相同的key进行分组(GroupingComparator分组就是再这个阶段完成)
- 之后再执行Reduce方法
Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。缓冲区的大小可以通过参数调整,参数:io.sort.mb默认100M。
三、MapReduceTask工作流程
3.1 MapTask工作机制
1. Read阶段:
MapTask通过的RecordReader,从输入InputSplit中解析初一个个key/value
2. Map阶段
该节点主要是将解析出的key/value交给用户编写的map()函数进行处理,并产生一系列新的key/value
3. Collect阶段
在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value进行分区(调用Partitioner),并写入一个环形内存缓冲区中。
4. Spill阶段
溢写阶段,当环境缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
溢写阶段详情:
- 利用快速排序算法对缓冲区数据进行排序,排序方式是:先按照Partition进行分区,然后按照key进行排序,这样经过排序后,数据以分区为单位聚集在一起,且同一分区类所有数据按照key有序。
- 按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out中(N表示当前溢写次数)。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
- 将分区数据的元信息写到内存索引结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1M,则将内存索引写到文件output/ spillN.out中。
5. Combine阶段
当所有数据处理完成后,MapTask对所有临时文件进行一次合并,形成一个大文件,保存在output/file.out中,同时生成索引文件output/file.out.index。
在文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并io.sort.factor(默认10个)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
3.2 ReduceTask工作机制
1. Copy阶段
ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写道磁盘上,否则直接放到内存中。
2. Merge阶段
在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件使用过多。
3. Sort阶段
按照MapReduce语意,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚集在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。
4. Reduce阶段
reduce()函数计算结果写到HDFS上。
四、OutputFormat数据输出
4.1 OutputFormat实现类
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了OutputFormat接口。
-
TextOutputFormat
默认输出格式是TextOutputFormat,它把每条记录写为文本行,它的键和值可以是任意类型,因为TextInputFormat调用toString()方法把它们转换为字符串。
-
SequenceFileOutputFormat
SequenceFileOutputFormat是一种二进制文件输出,可以作为后续MapReduce任务的输入。其格式紧凑,容易压缩
-
自定义OutputFormat
4.2 自定义OutputFormat
步骤:
- 自定义一个类集成FileOutputFormat
- 改写RecordWriter,改写具体输出数据方法write().
案例要求:过滤输入的log日志,包含atguigu的网站输出到e:/atguigu.log,不包含atguigu的网站输出到e:/other.log。
http://www.google.com
http://cn.bing.com
http://www.atguigu.com
http://www.sohu.com
http://www.sina.com
http://www.sin2a.com
http://www.sin2desa.com
http://www.sindsafa.com
1.自定义一个OutputFormat类:
public class FilterOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext context) throws IOException,
InterruptedException {
return new FilterRecordWriter(context);
}
}
2.编写RecordWriter类
public class FilterRecordWriter extends RecordWriter<Text, NullWritable> {
FileSystem fs;
FSDataOutputStream fosAtguigu;
FSDataOutputStream fosOther;
public FilterRecordWriter(TaskAttemptContext context) {
try {
fs = FileSystem.get(context.getConfiguration());
// 创建文件输出路径以及输出流
fosAtguigu = fs.create(new Path("e:/output/atguigo.log"));
fosOther = fs.create(new Path("e:/output/other.log"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
if (key.toString().contains("atguigu")){
fosAtguigu.write(key.toString().getBytes());
}else {
fosOther.write(key.toString().getBytes());
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
// 关闭资源
IOUtils.closeStream(fosAtguigu);
IOUtils.closeStream(fosOther);
IOUtils.closeStream(fs);
}
}
3.编写FilterMapper类
public class FilterMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
context.write(value, NullWritable.get());
}
}
4.编写FilterReducer类
public class FilterReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
Text k = new Text();
@Override
protected void reduce(Text key, Iterable<NullWritable> values, Context context) throws IOException,
InterruptedException {
// 进行换行
String line = key.toString() + "\r\n";
// 设置包含换行符的key
k.set(line);
for (NullWritable value : values) {
context.write(k, NullWritable.get());
}
}
}
5.编写FilterDriver类
public class FilterDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
args = new String[]{"e:/input/log.txt","e:/output"};
// 1.获取job对象
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2.设置jar包路径
job.setJarByClass(FilterDriver.class);
// 3.设置mapper与reducer
job.setMapperClass(FilterMapper.class);
job.setReducerClass(FilterReducer.class);
// 4.设置Mapper的输出
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
// 5.设置最终的输出
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 6.设置输出格式
job.setOutputFormatClass(FilterOutputFormat.class);
// 7.设置输入输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 8.提交job
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
五、MapReduce开发总结
1、输入数据接口:InputFormat
- 默认实现类四TextInputFormat
- TextInputFormat的功能座机四:一次读一行文本,然后讲该行的起始偏移量作为key,行内容为value返回。
- KeyValueTextInputFormat每一行均为一条记录,被分割符分割为key,value。默认分隔符为tab
- NLineInputFormat按照指定函数N来划分切片。
- CombineTextInputFormat可以把多个小文件合并在成一个切片处理,提高处理效率
- 用户自定义InputFormat
2、逻辑处理接口:mapper
根据业务需求实现其中三个方法:setupt()、map()、cleanup()
3、Partitioner分区
- 默认实现HashPartitioner。根据key的hash值和numReduces返回一个分区号
(key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
- 可以自定义分区
4、Comparable排序
- 当需要用自定义的对象作为key输出时,就必须要实现
WritableComparable
接口,重写compareTo()
方法。- 部分排序:对最终的输出的每一个文件进行内部的排序
- 全排序:对所有数据进行排序,通常只有一个Reduce
- 二此排序:排序条件只有两个
5、Combiner合并
Combiner合并可以提高程序的执行效率,减少IO传输。但是使用时必须不能影响原有的业务结果
6、Reduce端分组:GroupingCompartor
在Reduce端对key进行分组。应用于:在接收的key作为bean对象时,想让一个或几个字段相同key进入同一个reduce方法时,可以采用分区排序。
7、逻辑处理接口:Reducer
用户根据业务要求实现:setup()、reduce()、cleanup()
8、输出数据接口:OutputFormat
- 默认的实现类是TextOutputFormat,功能逻辑是:将没一个kv对,像目标文件输出一行
- 自定义OutputFormat