MapReduce
013、014-MapReduce
MapReduce 的思想是后续大数据组件影响是一直存在的。它提供了非常多的接口,能做到分布式。但分布式是无感知的,我们编写的单机应用程序在MapReduce上运行其实是分布式的。但它只适用于离线计算,也叫批计算的场景。不适用实时、流式计算。
MapTask 的个数
某些文件并不是一定能分片,只有一个 task来处理。
能分片成几个 128M,则可以有几个task 并发处理。
不同的文件是在不同的MapTask(一定个数内,有几个文件就几个task)
只有一个文件且小于128M,只有一个task
只有一个文件(大于128M),且不能分片,只有一个task
只有一个文件(大于128M),且可以分片,有几个分片数就有几个task
MapTask 并不是越多越好:MapTask还是ReducerTask都是以JVM的方式进行运行的
MapTask过多,会启动N个JVM,JVM的启动和销毁,是需要一定的成本开销的
一般情况下不去调整MapTask个数,交给框架处理。
使用MR来完成WC统计,按MR的编程规范:
自定义实现Mapper: 拆分单词,每个赋值1。 每一行运行一次mapper方法(不代表一行就有一个Maptask,IntputSplit 也不等同于代码内每行split(","))
自定义实现Reduecer:统计。 每个重复key运行一次reducer方法
自定义实现Driver : 八股文编程,固定的7个步骤:把我们自定义的Mapper Reducer Input Output组装起来 形成一个MR作业 => job
Mapper类四个泛型的意思:
KEYIN: 输入数据的KEY的数据类型
VALUEIN:输入数据的VALUE的数据类型
KEYOUT: 输出数据的KEY的数据类型
VALUEOUT:输出数据的VALUE的数据类型
Mapper的输出是要交给Reducer作为输入的
所以一般情况下:Mapper的输出类型 == Reducer的输入类型
Mapper<K1,V1,K2,V2>
Reduce<K2,V2,K3,V3>
Mapper类中提供了模板方法,管理了Mapper的生命周期:
setup: 每个MapTask开始执行一次
cleanup:每个MapTask结束执行一次
map: 我们自定义的类中要实现的map方法
run:典型的模板模式,定义执行流程:setup map cleanup
每一次进入一个mapper的是一行数据。
Split: 只是一个逻辑概念,并不会存储
Block: 是一个物理概念,数据是按Block存储。128M
Maptask个数:文件按Block分块后,Block块的个数
IntputSplit大小 == Block 大小
IntputSplit个数 == Maptask个数
Reduce:聚合,每个单词出现的次数 累加起来
KEYIN Text
VALUEIN IntWritable
KEYOUT Text
VALUEOUT IntWritable
经过shuffle是按照指定的规则进行的: 相同的key才会发到同一个reducer去执行。所以在reducer中直接将进来的value都求和,最后再统一输出
输入 ==> 处理 ==> 输出
输入:InputFormat
getSplits: 拿到你要处理的输入数据的分片信息(数据的大小、是否能分片。)
createRecordReader:如何去读数据
处理:
map
reduce
输出:OutputFormat
序列化
为什么大数据中类型是 LongWritable:序列化
分布式应用程序是运行不同的机器上的 ==> 网络传输。就需要序列化。
序列化:内存中的对象 ==>字节数组
反序列化:字节数组 ==>内存中的对象
默认Java的序列化效率会低一点
Hadoop序列化的机制kryo:紧凑 快速 扩展性 互操作
引入了自己的序列化机制:Writable
Writable 接口规范了两个方法,write():实现序列化,readField() :实现反序列化
分区
自定义分区器
public class PhonePartitioner extends Partitioner<Text, Access> {
public int getPartition(Text text, Access access, int numPartitions) {
// 继承Partitioner抽象类并重写getPartition,指定分区规则
}
}
默认是一个reduce
MR中,有几个reduce,就表示最终有几个文件输出: part-r-00000、part-r-00001、part-r-00002
设定的reduce个数 = 1 (默认)): 输出只有一个文件(不分区)
设定的reduce个数 > 分区数:输出文件个数==reduce个数,但是存在空文件
设定的reduce个数(但不等于1) < 分区数:会报错,会存在分区无法放入reduce
如果只有map没有reduce,那么最终的文件个数就由map个数决定
Combiner
Combiner 是介于 mapper 和 reducer 之间的,是运行在map阶段时的一次局部聚合(运行在map阶段的reduce),但仅仅是局部,将相同key的数据事先放到一起了,可以减少shuffle传输到reduce时的网络开销。
Combiner 的逻辑和 reducer 逻辑是相关甚至相同的。对于wc来说:统计每个单词的出现的频次。
reducer: 次数累加
所以combiner是直接使用reducer就可以
// 设定Combiner,直接照搬 reducer
job.setCombinerClass(WordCountReducer.class);
并不适合所有业务场景,如平均数计算。如果是在Combiner中做了一次平均数,再传输到reduce做均数结果是不对的。
spark、flink也有。
计数器Counter
计数器的使用
1)处理的数据的行数
2)单词总数
// 在map端获取计数器,并将结果累加1
context.getCounter(COUNTER_GROUP, LINES_GROUP).increment(1L); // 行数
context.getCounter(COUNTER_GROUP, WORDS_GROUP).increment(1L); // 单词总数,写在value的循环中
// 在 driver 中从计数器获取结果[迭代]
CounterGroup counters = job.getCounters().getGroup(COUNTER_GROUP);
排序
在大数据中,全局排序
只有一个reduce
1)挂了
2)非常慢
全局排序在Hive、SparkSQ中: order by
大数据中排序,用的比较多的是:分区排序
分区排序在Hive、SparkSQ中: sort by
二次排序(待补充)。
intputFormat 和 outputFormat
job.setInputFormatClass(KeyValueTextInputFormat.class);
job.setOutputFormatClass(MyOutputFormat.class); // 指定自定义的OutputFormat
group by / distinct
GroupBy 就是一个wc,不再直接用key,而是按value中指定的字段来聚合而已
Distinct 就是在map 里分词后,相同的key只保留一个呗 – 甚至不需要我们写这个去重逻辑,交给shuffle就去重了
Join
reduceJoin
mapper: 将两个表数据一起读取到同一个类,但是要区分来在哪个表,并以期望关联的字段作为key(IntWritable),以对象作为value(Info)输出。
reducer: 经过shuffle后的相同的key已经被放到同一个reducer,所以在reducer中将两个对象们的信息组装到同一个对象中即可。
reduceJoin会经过shuffle,必然有网络IO、磁盘IO的开销,效率是有损耗的。
mapJoin
也叫广播join
driver: 将小表数据在driver端addCacheFile。
mapper: 在Mapper端setup()方法通过上下文拿到缓存的小表:context.getCacheFiles();map()方法中将每一个大表数据都去缓存里比对并关联。对象直接作为key(Info),value(NullWritable)输出。
不需要reducer,也不经过shuffle。
压缩
不同的压缩格式使用不同的压缩算法来实现。不同压缩格式的压缩比、压缩时间都有区别。
解压缩:
有损: 压缩前 解压后 大小可能有丢失
无损: 前后是一样的
压缩、解压缩的静态工具类。
MapReduce整合压缩。
受MR支持压缩的格式可以直接被input。
对于压缩后output
// 一个是开关,一个是指定具体的codec类型 (只是Output)
FileOutputFormat.setCompressOutput(job, true);
FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);
提交作业的源码
016-MapReduce-04
submiter.submitJobInternal(true){
submit(){
connect(){
Cluster // 描述是否要提交到集群(Yarn 和 Local)
}
submiter.submitJobInternal{
checkSpecs(job){
output.checkOutputSpecs(job); // 检查文件是否已存在。通过反射拿到outputFormat来判断
}
addMRFrameworkToDistributedCache(); // 添加到分布式缓存
JobSubmissionFiles.getStagingDir(cluster, conf); // 得到本地的工作空间的目录
InetAddress.getLocalHost() // 获取ip
JobID jobId = submitClient.getNewJobID();
job.setJobID(jobId); // 关联jobId
Path submitJobDir = new Path(jobStagingArea, jobId.toString()); // 提交本作业时的工作空间
copyAndConfigureFiles(job, submitJobDir); //
// 计算分片数
int maps = writeSplits(job, submitJobDir){
writeNewSplits(job, jobSubmitDir){
InputFormat // 反射,根据driver中设置拿到
input.getSplits(job); // 得到分片信息 (抽象类,需要找一个实现如FileInputFormat#getSplits())
getSplits(){
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job)); // Math.max(1, 1) = 1
long maxSize = getMaxSplitSize(job); // maxSize = Long.MAX_VALUE
// 根据文件blockSize计算splitSize
long splitSize = computeSplitSize(blockSize, minSize, maxSize){ // computeSplitSize(32M, 1, Long.MAX_VALUE)
return Math.max(minSize, Math.min(maxSize, blockSize)); // Math.max(1, returnMath.min(Long.MAX_VALUE, 32M)) ==32M
}
}
}
};
writeConf(conf, submitJobFile); // 相关配置写入到该作业
status = submitClient.submitJob(); //
}
}
}
数据本地性
存储计算分离:计算时hdfs的数据节点DN 与 Yarn中NM(Container)不在同一节点,每次获取到Container都要去其他节点获取数据用于计算,增加网络IO和磁盘IO。
让hdfs的数据节点DN 与 Yarn中NM(Container)部署在同一节点,移动计算要远远好于移动数据。
DN与NM部署在同一节点也可能出现 NM去拉取了其他节点DN的数据过来计算。
所以又引入时间降级概念,如果在DN当前节点的container正在被其他节点使用,可以短暂等待container释放,牺牲了一部分时间换来了减少IO。
官网解读
Shuffle 洗牌
在分布式框架中都离不开的概念。
Mapper<K1,V1,K2,V2>
Reduce<K2,V2,K3,V3>
经过shuffle后,相同的key一定在一个reduce,不同的key也有可能在一个reduce(比如只有一个reduce)。注:这里的一个reduce不等于MR编程中的调用一次reducer方法。
MR过程
图示。
MR 优化涉及参数
优化点:
- Input:
压缩,可以节省hdfs空间和io,但是要消耗CPU的 - Mapper:
缓冲区Buffer:
mapred-default.xml
// 用于排序的总的Buffer的大小,单位是M。
mapreduce.task.io.sort.mb = 100
// 百分比。当达到Buffer总大小的80%时,就会开启一个线程将buffer内容写入磁盘。
mapreduce.map.sort.spill.percent = 0.8
- Reducer:
mapreduce.map.maxattempts // map最大重试次数,当任务挂掉后最大可以重试的次数,默认4。
mapreduce.reduce.maxattempts // reduce最大重试次数,默认4。
mapreduce.am.max-attempts // AM 最大重试次数,默认2。一般不改变。
- 为什么Hadoop不适合小文件存储
- 元数据维护压力大
- 分块存储浪费空间
- …
- 小文件:尽可能规避(废话,规避不了)。用Spark读取再写入,就能合并小文件。