1. 对MapReduce的理解

是什么:Hadoop默认自带的分布式计算框架

做什么:提供一系列接口(核心类:InputFormat、OutputFormat、Mapper、Reducer、Driver),让用户能够实现自定义业务功能的分布式计算任务

【优点】:

  • 高扩展性:计算资源不够,直接增加节点数量即可。质量可能不够,数量一定管够
  • 高容错性:一个节点任务失败,能自动转移到其他空闲节点
  • 适合大数据处理:得益于其扩展性,只要数量足够,能够计算TB级别的数据

【缺点】:

  • 无法进行实时计算:太慢了!!!太慢了!!!
  • 不擅长流式计算:MR的输入数据一般都是静态的,无法处理动态的输入格式
  • 不擅长迭代计算:一个完整的MR计算过程一般为Mapper - Reducer - 写出结果,频繁的迭代计算将要启动多组MR并串联,而每次MR的结果都是会以文件的形式写出,给下一个MR组输入的话又得重新读取,太过繁琐

【个人总结】:

个人认为,MR程序的缺点总结的话就是一点,IO过程太多了。首先map阶段输入需要从HDFS中读取数据(这个过程已经是比较慢的了),然后map阶段业务完成后写出数据(一次IO),而reduce阶段的输入首先得从map节点读取已经写出的结果文件(可能是网络IO),读到reduce节点后如果结果集太大又会写到磁盘(又一次IO),最后才是主要的reduce过程,最后结果仍然是写回磁盘......一组MR的执行,可能要涉及到同节点、异节点的多次IO,如果用来做机器学习之类的复杂迭代计算,可能IO时间比核心业务时间更长,因此MR适合做一些一次性、单步骤、大量级的计算。

2. Mapper & Reducer编程需要注意的点

  • map() 方法是对每个<K, V>键值对调用一次
  • reduce() 方法是对每个key调用一次,每个key可能对应多个value,其中value封装为迭代器对象
  • 不要导错包!!!不要导错包!!!不要导错包!!! 血泪的踩坑之旅 -- mapred(老版本,不用) -- mapreduce(用它!)

3. MR的切片

【什么是切片?】

MR要进行计算,就必须要有输入,而MR是分布式计算模式,因此对于数据量较大的计算,就会启动多个Mapper和Reducer(Reducer数目一般都会远小于Mapper),而作为第一道环节,多个Mapper都接受完整的数据集,那分布式完全就没有意义了,因此在数据进入Mapper前,会首先进行逻辑切分,将整个数据集分成多份,每份送给一个Mapper进行处理。因此,切片数 = Mapper(MapTask) 的个数

【怎么切?】

第一种:根据文件大小切(默认切片方式) -- 附源码解析

  • 根据参数得到SplitSize,即满足多大了就切一块,见源码解析
  • 文件优先级 > 切片,即如果一个文件不够SplitSize,则直接作为一个切片,不是把所有文件当整体
  • 一定要明确一点:这里的切片只是逻辑切片!!相当于隔一段给文件"打一个标记",并不是真正物理上将数据文件划分为几份。实际上Client端做的分片仅仅是提交job.split信息,即将这些标记发送给MR,Map阶段每个Mapper将会根据这些标记去读取各自被分配的那部分数据
protected long getFormatMinSplitSize() { 
    return 1L; 
}
// 获取最小切片大小 -- 默认为 1
public static long getMinSplitSize(JobContext job) { 
    return job.getConfiguration().getLong("mapreduce.input.fileinputformat.split.minsize", 1L);
}

// 获取最大切片大小 -- 默认为Long.MAX
public static long getMaxSplitSize(JobContext context) {
    return context.getConfiguration().getLong("mapreduce.input.fileinputformat.split.maxsize", 9223372036854775807L);
    }

// ......(此处省略一万行代码)

if (this.isSplitable(job, path)) {
    long blockSize = file.getBlockSize();
    // 实际切片的大小 -- computeSplitSize函数见下
    long splitSize = this.computeSplitSize(blockSize, minSize, maxSize);

    long bytesRemaining;
    int blkIndex;
    // 如果剩下的文件大于 1.1 * SplitSize,那么继续切分;否则直接作为一个分片; 防止出现过小文件
    for(bytesRemaining = length; (double)bytesRemaining / (double)splitSize > 1.1D; bytesRemaining -= splitSize) {
        blkIndex = this.getBlockIndex(blkLocations, length - bytesRemaining);
        splits.add(this.makeSplit(path, length - bytesRemaining, splitSize, blkLocations[blkIndex].getHosts(), blkLocations[blkIndex].getCachedHosts()));
    }
    
// ......(此处省略一万行代码)
// 核心公式
protected long computeSplitSize(long blockSize, long minSize, long maxSize) {
    // 默认情况下就是块文件的大小 -- 128M
        return Math.max(minSize, Math.min(maxSize, blockSize));
}

第二种:根据文件行数切

  • 相当于将评判标准从文件大小换成了行数,设置多少行为一个切片
  • 同样不会跨文件切

第三种:消灭小文件切法

当有大量小文件时,如果用上面两种机制,则会导致切片很多,启用非常多的Mapper,资源利用率极低,因此设置了一种切片方式专门用于小文件过多的场景

重要参数: CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 默认4m

  • Step1:虚拟存储,机制见伪代码
if (文件A.size() <= MaxInputSplitSize){
    A文件不做处理,直接为一个虚拟文件;
}else if(文件A.size() <= MaxInputSplitSize * 2){
    A平均切分为两个等大小的虚拟文件[A1,A2];
}else{
    A切掉大小为MaxInputSplitSize的一块,剩下的部分继续判断;
}
  • Step2:重新切片
if(文件B1 >= MaxInputSplitSize){
    B1作为一个切片;
}else{
    B1等待和下一个虚拟文件合并;
    if(合并后的大小 >= MaxInputSplitSize){
        合并文件成为一个切片;
    }else{
        继续等待合并;
    }
}

4. MR常用的输入类型

全部都是继承了FileInputFormat抽象类,如果自定义输入类型,需要继承该抽象类并重写RecordReader方法(实现具体自定义读的逻辑)

InputFormatSplit_TypeKey -- TypeValue -- Type
TextInputFormat按文件大小切(默认)每行偏移量offset <LongWritable>该行文本<Text>
NLineInputFormat按固定行数切每行偏移量offset <LongWritable>该行文本<Text>
CombineTextInputFormat消灭小文件切法每行偏移量offset <LongWritable>该行文本<Text>
KeyValueInputFormat按文件大小切(默认)每行Split后的field[0] <Text>field[0] <Text>

这四种主要的输入类型都是按行读取数据,每一行会形成一个KV对,调用一次map方法

【千万别忘了!!!】

除了默认输入格式,其他三种输入格式都需要在Driver类中设置相关参数:

// 设置Job的输入类型
job.setInputFormatClass(......)

// 对于NLineInputFormat,要设置多少行一个切片
NLineInputFormat.setNumLinesPerSplit(job, 3);

// 对于CombineTextInputFormat要设置最大切片大小
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); // 默认4m

// 对于KeyValueInputFormat要指定对应的分隔符 -- 每行的Key和Value用什么分割的
// 特别注意:这个参数是在Configuration中进行设置的!!
conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR, "\t");  // 以制表符分割

5. MR常用的输出类型

同理,MR的输出类型(Reducer写出的文件类型)也都继承了FileOutputFormat类,同样支持自定义输出格式,需要继承FileOutputFormat类并重写RecordWriter方法

OutputFormatDescription
TextOutputFormatReducer的结果直接以字符串格式按行写出
SequenceFileOutputFormatReducer的结果按照KV键值对格式写出的序列化文件

SequenceFileOutputFormat中写入的是Reducer输出的键值对,且进行了序列化,因此更方便进行压缩;此外,一个MR流程得到的结果如果是SequenceFileOutputFormat格式写出,那么该结果能够很便利地作为下一个MR流程的输入,因为已经序列化封装好了KV对,作为输入时只需要进行反序列化读入就可以了

PS

思否什么时候能够支持自定义字体颜色!!!好想记出五颜六色而又骚气的笔记......o(╥﹏╥)o


Chord_Gll
51 声望74 粉丝

每一个领跑者,都曾是优秀而虔诚的追赶者