今天来介绍下大数据计算引擎MapReduce,MapReduce主要用于离线计算,电商公司的离线计算任务大多数是用Hive将sql转化为MR程序来运行,可见MapReduce的重要性。
MapReduce是一个分布式运算程序的编程框架。
优点:易于编程、有良好的扩展性、具有高容错性、适合PB级以上海量数据的离线处理。
缺点:不擅长实时计算、不擅长流式计算、不擅长DAG计算(DAG有向图MR也可以做,只是每个作业的输出结果都会写入磁盘,这样会造成大量的IO而导致性能降低)。
一个完整的MapReduce程序运行时有三种实例进程:
根据官方文档,WordCount程序应编写类分别继承Mapper、Reducer并重写相应的方法,最后写驱动类来调用和提交。且数据的类型是Hadoop自身封装的序列化类型。
Java类型 | Hadoop Writable类型 |
---|---|
Boolean | BooleanWritable |
Byte | ByteWritable |
Integer | IntWritable |
Float | FloatWritable |
Long | LongWritable |
Double | DoubleWritable |
String | Text |
Map | MapWritable |
Array | ArrayWritable |
需求:统计文本文件中每个单词出现的总次数。
public class WordcountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
Text k = new Text();
IntWritable v = new IntWritable(1);
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//读取一行数据
String line = value.toString();
//按空格切分
String[] words = line.split(" ");
//封装并输出
for (String word : words) {
k.set(word);
context.write(k, v);
}
}
}
public class WordcountReducer extends Reducer<Text, IntWritable, Text, IntWritable>{
int sum;
IntWritable v = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
//累加求和
sum = 0;
for (IntWritable count : values) {
sum += count.get();
}
//输出
v.set(sum);
context.write(key,v);
}
}
public class WordcountDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
//获取配置信息
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
//设置jar加载路径
job.setJarByClass(WordcountDriver.class);
//设置map和reduce类
job.setMapperClass(WordcountMapper.class);
job.setReducerClass(WordcountReducer.class);
//设置map输出
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//设置最终输出kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//提交
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
hadoop jar wc.jar com.robofly.wordcount.WordcountDriver /dgf/input /output
。序列化和反序列化:
为什么要序列化?
为什么不用Java的序列化?
开发中当常用的基本序列化类型不能满足需求时就需要自己实现序列化接口,比如在Hadoop内部传递bean对象时。
自定义bean对象实现序列化接口:
从上面可以知道MapReduce分为Map和Reduce两个阶段,下面就从Input到Output来解析MapReduce。
之前说过块的大小是128M,数据块是HDFS在物理上把数据分成一块一块。而数据切片只是逻辑上对输入进行切片,并不会在磁盘上将其切分进行存储,且切片是对每一个文件单独切片。
每个Split切片都是分配一个MapTask并行实例进行处理,那么MapTask并行度就等于切片数。在默认情况下切片大小等于块的大小,是为了尽量避免跨节点传输数据。
Job提交流程主要有三步:
Job提交源码解析:
//1.1是1方法里面的方法,以此类推
//1.提交Job
job.waitForCompletion();
//1.1.开始提交
submit();
//1.1.1.设置使用新的API
setUseNewAPI();
//1.1.2.建立连接,会创建一个根据不同场景(本地或集群)下提交Job的对象
connect();
//1.1.2.1.创建提交Job的代理
new Cluster(getConfiguration());
//1.1.2.1.1.判断是本地还是集群(是本地则创建LocalJobRunner对象,是集群则创建YarnRunner对象)
initialize(jobTrackAddr, conf);
//1.1.3.提交job
submitter.submitJobInternal(Job.this, cluster);
//1.1.3.1.创建给集群提交数据的Stag路径(是本地就在项目的盘符中创建,是集群就在HDFS上创建)
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
//1.1.3.2.获取jobid,并创建Job路径,路径是根据jobStagingArea和jobId拼接起来的
JobID jobId = submitClient.getNewJobID();
Path submitJobDir = new Path(jobStagingArea, jobId.toString());
//1.1.3.3.拷贝jar包到集群(向集群提交就会向HDFS上传jar包)
copyAndConfigureFiles(job, submitJobDir);
//1.1.3.3.1.上传jar包
rUploader.uploadFiles(job, jobSubmitDir);
//1.1.4.计算切片,生成切片规划文件写到submitJobDir路径中
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
//1.1.5.向submitJobFile路径写XML配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
//1.1.6.提交Job,返回提交状态(是本地submitClient就是LocalJobRunner,是集群submitClient就是YarnRunner)
status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
FileInputFormat切片源码解析:
//FileInputFormat中的getSplits方法是用来生成切片信息的,看这一个方法即可
//获取文件路径
Path path = file.getPath();
//获取文件大小
long length = file.getLen();
//判断文件是否可切
if (isSplitable(job, path)) {
//获取文件的块大小
long blockSize = file.getBlockSize();
//获取切片大小
/*计算切片大小,默认:切片大小=块大小
切片大小>块大小:修改minSize的值;切片大小<块大小:修改maxSize的值
protected long computeSplitSize(long blockSize, long minSize, long maxSize) {
return Math.max(minSize, Math.min(maxSize, blockSize));
}
*/
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
long bytesRemaining = length; //剩余文件大小
/*(double) bytesRemaining)/splitSize > 1.1
好处:让最后一片不会太小,不会浪费MapTask资源
缺点:会造成跨节点读数据(只会对最后一个MapTask造成跨节点读数据)
*/
//计算剩余文件是否可以切片
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
//块的索引
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
//生成切片信息
/*splits:是集合用来装切片信息
makeSplit:该方法用来生成切片信息
参数1-path:文件路径,参数2-length-bytesRemaining:片的起始位置,参数3-splitSize:切片大小(偏移量)
*/
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
//重新计算剩余文件大小
bytesRemaining -= splitSize;
}
//将剩余的文件切成一片
if (bytesRemaining != 0) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts()));
}
}
先来看下InputFormat的继承树:
|----InputFormat(抽象类)
|----|----FileInputFormat(抽象类)
|----|----|----TextInputFormat(默认用来读取数据的类)
|----|----|----CombineFileInputFormat(抽象类)
|----|----|----|----CombineTextInputFormat(可以合并小文件)
InputFormat中的抽象方法:
//获取切片信息
public abstract List<InputSplit> getSplits(JobContext context) throws IOException, InterruptedException;
//获取RecordReader对象,该对象是真正用来读取数据的对象
public abstract RecordReader<K,V> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException;
在FileInputFormat抽象类中重写了InputFormat中的getSplits方法,该方法用来生成切片信息。
TextInputFormat是FileInputFormat的实现类,重写了InputFormat中的createRecordReader方法,该方法返回了LineRecordReader,是RecordReader的子类,由名字可以看出是一行一行的读取数据的。TextInputFormat的键是存储该行在整个文件中的起始字节偏移量(LongWritable类型),值是这行的内容(Text类型)。
@Override
public RecordReader<LongWritable, Text> createRecordReader(InputSplit split, TaskAttemptContext context) {
return new LineRecordReader(recordDelimiterBytes);
}
FileInputFormat切片机制:
CombineTextInputFormat可以将多个小文件合并成一片进行处理。要想使用CombineTextInputFormat需要设置虚拟存储切片最大值(setMaxInputSplitSize)。
假设虚拟存储切片最大值为4M,现在有四个文件大小分别为1.5M、5.1M、3.8M、6.8M,则虚拟存储过程对四个文件划分的块大小如下:
1.5M<4M 划分1块,块1=1.5M
4M<5.1M<8M 划分2块,块1=2.55M,块2=2.55M
3.8M<4M 划分1块,块1=3.8M
4M<6.8M<8M 划分2块,块1=3.4M,块2=3.4M
最终虚拟存储的文件是1.5M、2.55M、2.55M、3.8M、3.4M、3.4M。
切片过程:
判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片,如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个文件。
故上述文件最终会形成3个切片,分别为(1.5+2.55)M,(2.55+3.8)M,(3.4+3.4)M。
代码中实现只需要在驱动类中设置两个参数即可:
//InputFormat默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
//虚拟存储切片最大值设置4M
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
注:这里的合并小文件只是解决了小文件运算的问题,小文件存储的问题依然存在。
Map阶段的后半段和Reduce阶段的前半段称为Shuffle。从源码的角度看,Map阶段为:map–>sort,Reduce阶段为:copy–>sort–>reduce,则Shuffle为sort–>copy–>sort过程。
Shuffle流程:
Partition分区:
//默认Partition分区是根据hashCode对ReduceTasks个数取模得到的,没法指定key到哪个分区,不设置默认只有一个分区
public class HashPartitioner<K, V> extends Partitioner<K, V> {
public int getPartition(K key, V value, int numReduceTasks) {
//key.hashCode() & Integer.MAX_VALUE:用来保证结果一定为正数
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
自定义Partitioner步骤:
Partition分区总结:
WritableComparable排序:
排序是MapReduce框架中最重要的操作之一,MapTask和ReduceTask均会对数据按照key进行排序,如果用对象作为key传输,就需要实现WritableComparable接口并重写compareTo方法才可以实现排序。
Combiner合并:
先来看下OutputFormat的继承树:
|----OutputFormat(抽象类)
|----|----FileOutputFormat(抽象类)
|----|----|----TextOutputFormat(默认使用的OutputFormat类)
OutputFormat中的抽象方法:
//获取RecordWriter对象,该对象是用来写数据的
public abstract RecordWriter<K,V> getRecordWriter(TaskAttemptContext context) throws IOException, InterruptedException;
//检查输出的一些参数,比如输出路径是否设置了,输出路径是否存在
public abstract void checkOutputSpecs(JobContext context) throws IOException, InterruptedException;
在FileOutputFormat抽象类中重写了checkOutputSpecs方法,该方法中做了如下操作:检查输出路径是否设置了,输出路径是否存在。
TextOutputFormat是FileOutputFormat的实现类,重写了getRecordWriter方法,该方法返回了LineRecordWriter的对象,该对象是真正用来写数据的对象。
@Override
public RecordWriter<K,V> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException{
return new LineRecordWriter<>(fileOut, keyValueSeparator);
}
自定义OutputFormat:
有时需要控制最终文件的输出路径和输出格式,可以自定义OutputFormat实现。先自定义一个类继承FileOutputFormat,然后改写RecordWriter,具体改写输出数据的方法write()即可。
Reduce Join:
Map端的工作是为来自不同表或文件的键值对打标签来区别不同的来源。Reduce端的工作是在每一个分组当中将来源于不同文件的记录分开,最后在进行合并就行了。这种方式合并的操作在Reduce阶段完成,Reduce端的处理压力太大,容易在Reduce阶段发生数据倾斜。可以在Map端进行数据合并来解决数据倾斜。
Map Join:
适用于一张表很小另一张很大的场景。Map Join在Map端缓存多张表来提前处理业务逻辑,减少了Reduce端数据的压力,减少了数据倾斜的可能。
MapTask工作流程:
ReduceTask工作流程:
以上就是今天的内容分享了,MapReduce理论知识比较多,几天才整理完毕,喜欢的点个关注吧。