一、MR的编写
1. MR的编程规范
MR的编程只需要将自定义的组件和系统默认组件进行组合,组合之后运行即可
编程步骤:
- Map阶段的核心处理逻辑需要编写在Mapper中
- Reduce阶段的核心处理逻辑需要编写在Reducer中
- 将编写的Mapper和Reducer进行组合,组合成一个Job
- 对Job进行设置,设置后运行
2. Mapper
Mapper是MapTask中负责Map阶段核心运算逻辑的类
-
继承Mapper<KEYIN,VALUEIN,KEYOUT,VALUEOUT>
-
KEYIN,VALUEIN 取决于InputFormat中RecordReader的设置
KEYOUT,VALUEOUT由自己定义 -
Mapper的运行流程
由MapTask调用Mapper.run()run(){ setUp(); while(context.nextKeyValue()) //循环调用RR读取下一组输入的key-value { map(key,value,context); } cleanUp(); }
-
在Mapper的map()中编写核心处理逻辑
3. Reducer
Reducer是ReduceTask中负责Reduce阶段核心运算逻辑的类
-
继承Reducer<KEYIN,VALUEIN,KEYOUT,VALUEOUT>
-
KEYIN,VALUEIN 取决于Mapper的输出
KEYOUT,VALUEOUT由自己定义 -
Reducer的运行流程
由MapTask调用Reducer.run()run(){ setUp(); while(context.nextKey()) // 使用迭代器不断迭代key相同的数据 { reduce(key,value,context); } cleanUp(); }
-
在Reducer的reduce()中编写核心处理逻辑
4. Job
-
创建Job
Job job=Job.getInstance(Configuration conf);
-
设置Job
Job.setName("job名"); //设置名称 Job.setJar("jar名"); | Job.setJarByClass(类名) // 设置Job所在的Jar包,只有在YARN上运行时才需要设置
-
配置Job
设置输入和输出格式,如果不设置使用默认 设置Mapper,Reducer 设置Mapper和Reducer的输出类型 // 主要为了方便根据类型获取对应的序列化器 设置输入和输出目录
-
运行Job
Job.waitforCompletion(true); //true为打印详细信息
二、Read阶段的流程
根据InputFormat
- ①切片, getSplit()
- ②使用输入格式的RR读取数据, createRecordReader()
三、Read阶段的几种InputFormat
1.默认的TextInputFormat
-
场景: 普通的文本格式数据来源
-
切片: 采用默认的切片策略,以文件为单位,先判断文件是否可切,如果可切,循环以片大小为单位切片,如果不可切,整个文件作为1片
-
RR : LineRecordReader(将一行封装为一个key-value)
LongWritable key: 行的偏移量 Text value: 行的内容
2. NLineInputFormat
-
场景: 适合一行的内容特别多,在Map阶段map()处理的逻辑非常复杂的情况,此时可以根据行数自定义切片的大小
-
切片: 可以设置以文件为单位,每N行作为一个切片!
-
RR : LineRecordReader(将一行封装为一个key-value)
LongWritable key: 行的偏移量 Text value: 行的内容
3. KeyValueTextInputFormat
-
场景: 一行的内容的格式为 key-value,方便地将key、value拆分封装
-
切片: 采用默认的切片策略,以文件为单位,先判断文件是否可切,如果可切,循环以片大小为单位切片,如果不可切,整个文件作为1片。
-
RR : KeyValueRecordReader(将一行封装为一个key-value)
Text key: 行的分隔符之前的部分内容 Text value: 行的分隔符之后的部分内容
4. CombineTextInputFormat
-
场景: 输入目录中小文件过多,可以将多个小文件设置到一个切片中!
-
切片: ①根据maxSize对每个文件进行逻辑切片,切分为若干part
②将多个part组合,知道超过maxSize,这些part作为一个切片 -
RR : LineRecordReader(将一行封装为一个key-value)
LongWritable key: 行的偏移量 Text value: 行的内容
5. 自定义InputFormat
-
举例
将输入目录中的多个文件的内容读取到一个SequnceFile中 -
SequnceFile
SequnceFile是Hadoop特有的文件格式优点: ①适合key-value类型数据的存储 ②比普通的文件格式节省空间
-
默认的输出格式是TextOutPutFormat(文本格式的输出格式)
将输出格式设置为SequnceFileOutPutFormat -
将输入目录中的文件读取为key-value(bytes)形式
将文件的内容读取封装为bytes类型,写出
将文件的文件名作为key -
需要考虑问题:是否需要Reduce?
①是否需要合并
什么时候不需要合并?
仅有一个MapTask,且MapTask不存在相同key的数据时,需要合并
有多个MapTask,最终期望生成一个结果文件,需要汇总,需要有Reduce
②是否将结果进行排序
没有Reduce: MapTask----map
有Reduce: MapTask----map-----sort—ReduceTask----copy—sort----reduce -
自定义InputFormat
① 继承FileInputFormat
② 重写isSplitable(),返回false,让文件不可切,整个文件作为1片
③ 自己提供RR
在RR中,nextKeyValue()是最重要的方法,返回当前读取到的key-value,如果读到返回true,否则返回false,返回true,调用Mapper的map()来处理
四、切片和块
- 切片: 对文件进行逻辑切分,只有在运行MR任务时,才会对文件切分,切分时,切片的大小不同,每个文件切分的结果也不同
- 块: 文件在上传到HDFS时,在HDFS上存储的最小单位,物理存储
- 关系: MapTask在读取切片的内容时,需要根据切片的metainfo,获取到当前切片属于文件的哪部分,再根据此信息去寻找对应的块,读取数据
注意:默认切片大小等于块大小,主要为了减少在运行MR时,大量的跨机器读取切片内容带来额外的网络IO。
根据默认的策略策略,可以调整切片的大小:
- 调整切片大小 大于 块大小: 调整minSize
- 调整切片大小 小于 块大小: 调整maxSize
五、Job提交流程
1. 提交之前的准备阶段
a) 检查输出目录是否合法
b) 为Job设置很多属性(用户,ip,主机名..)
c) 使用InputFormat对输入目录中的文件进行切片设置Job运行的mapTask的数量为切片的数量
d) 在Job的作业目录生成Job执行的关键文件
job.split (job的切片对象)
job.splitmetainfo(job切片的属性信息)
job.xml(job所有的配置)
e) 正式提交Job
2. 本地模式
a) 在提交Job后,创建LocalJobRunner.Job.Job对象,启动线程
b) 在LocalJobRunner.Job.Job启动的线程中,使用线程池,用多线程的形式模拟MapTask和ReduceTask的多进程运行
c) 执行Map,调用线程池,根据Map的切片信息,创建若干MapTaskRunable线程,在线程池上运行多个线程
流程大致如下:
MapTaskRunable------>MapTask--------->Mapper--------->Mapper.run()------->Mapper.map()
d) Map阶段运行完后,执行Reduce,调用线程池,根据Job设置的ReduceTask的数量,创建若干ReduceTaskRunable线程,在线程池上运行多个线程
流程大致如下:
ReduceTaskRunable------->ReduceTask------>Reducer----->Reducer.run()------>Reducer.reduce()
3. 在YARN上运行
a) 在提交Job后,创建MRAppMaster进程
b) 由MRAppMaster,和RM申请,申请启动多个MapTask、多个ReduceTask
流程大致如下:
Container------>MapTask--------->Mapper--------->Mapper.run()------->Mapper.map()
Container------>ReduceTask------>Reducer-------->Reducer.run()------>Reducer.reduce()
4. Job提交之后MR的核心阶段划分
如果当前Job没有Reduce阶段,MapTask只有map,没有sort
如果当前Job有Reduce阶段,可以将Map-Reduce再详细分为Map---Shuffle---Reduce阶段
Shuffle的含义为洗牌,将Map阶段写出的数据,进行洗牌(将数据整理的有序,方便Reducer进行reduce)
Shuffle阶段横跨MapTask和RedcueTask,在MapTask端也有Shuffle,在RedcueTask也有Shuffle
具体Shuffle阶段指MapTask的map之后到RedcuceTask的reduce之前
六、MapTask流程
1. 定义
MapTask分为:map和sort
map:Mapper.map()中将输出的key-value写出之前
sort::Mapper.map()中将输出的key-value写出之后
2. MapTask的shuffle流程
-
当在map()将输出的key-value写出后,记录是会被Partitioner计算一个分区号
-
计算后,记录被收集到一个缓冲区(MapOutPutBuffer)
-
收集线程负责向缓冲区收集数据,缓冲区初始值为100M,当使用到80%阈值,唤醒溢写线程,溢写线程会将缓冲区已经收集的数据溢写到磁盘
此时注意:如果溢写线程慢收集线程块时,收集线程会因为缓冲区内所有数据还没被溢写线程所溢写而导致无法向缓冲区存入数据, 此时收集线程会被阻塞,直到缓冲区中的所有数据都被溢写出之后被唤醒,继续收集。
-
在溢写前,会对缓冲区中的数据进行排序(快速排序),在排序时,只通过比较key进行排序
-
排序后,按照分区,依次将数据写入到磁盘的临时文件的若干分区中
-
每次溢写都会生成一个临时文件,当所有的数据都溢写完成之后,会将所有的临时文件片段合并为一个总的最终的文件
最后一批数据不满足溢写条件会执行一次flush
-
在合并时,将所有的临时文件的相同分区的数据,进行合并,合并后再对所有的数据进行排序(归并排序)
-
最终生成一个结果文件,这个文件分为若干分区,每个分区的数据已经按照key进行了排序,等待reduceTask的shuffle线程来拷贝数据
七、分区
1.定义
分区是在MapTask中通过Partitioner来计算分区号
2.Partitioner的初始化
- 计算总的分区数partitions,取决于用户设置的reduceTask的数量
- partitions>1,默认尝试获取用户设置Partitioner,如果用户没有定义,那么会使用HashPartitioner,HashPartitioner根据key的hashcode进行计算,相同的key以及hash值相同的key会分到一个区
- partitions<=1,则默认初始化一个Partitioner,这个Partitioner计算的所有的区号都为0
3.注意
通常在Job的设置中,希望将数据分为几个区,就设置reduceTask的数量为对应的数量,partitions=设置的reduceTask的数量。
八、排序
1.定义
排序是在shuffle阶段中自动进行的,在MapTask端发生两次排序中,用户唯一可以控制的是提供一个key的比较器
2.设置key的比较器
- 用户可以自定义key的比较器,自定义的比较器必须是一个RawComparator类型的类,重点是实现compareTo()方法
- 用户通过key,让key实现WritableComparable接口,系统自动提供一个比较器,重点是实现compareTo()方法
3.排序的分类
- 全排序: 对所有的数据进行排序,指生成一个结果文件,这个结果文件整体有序
- 部分排序: 最终生成N个结果文件,每个文件内部整体有序
- 二次排序: 在对key进行比较时,比较的条件为多个
- 辅助排序: 在进入reduce阶段时,通过比较key是否相同,将相同的key分为1组
九、分组
1.定义
分组通过分组比较器,对进入reduce的key进行对比,key相同的分为一组,一次性进入Reducer,被调用reduce方法
2.分组比较器的设置
- 用户可以自定义key的分组比较器,自定义的比较器必须是一个RawComparator类型的类,重点是实现compareTo()方法
- 如果没有设置key的分组比较器,默认采取在Map阶段排序时,key的比较器
3.Reduce的细节
在进入reduce()时,Reducer会自动实例化一个key-value,这个key-value在Redcuer工作期间,一直是一个不变的对象,每次迭代,reducer会把读到的新的key-value的属性值赋值给key-value。
十、压缩
1.压缩的目的
- 压缩的目的是在MR运行期间,提高MR运行的效率。
- 压缩可以减少MR运行期间的磁盘IO和网络IO。
2.压缩的原则
- IO密集型,多用压缩
- 计算密集型,CPU负载过重,少用压缩
3.Hadoop支持的压缩格式
自带: deflate,bzip2,gzip
额外安装的: lzo,snappy
特点:
① bzip2压缩比最高,压缩速度最慢
② snappy压缩速度最快,压缩比凑合
③ deflate,gzip折中
使用便利性:
-
LZO压缩格式最麻烦
①额外安装LZO压缩格式
②如果JOB输入目录中的文件为LZO压缩格式,需要为每个文件创建索引,如果不创建索引,那么输入的文件无法切片,整个文件作为1片,还需要使用LZO特定的输入格式,使用LZOInputFormat! -
其他的压缩格式,和纯文本文件使用一致的,不需要额外设置
可切片的角度:
- 如果Job的输入采用了压缩,只有bzip2和lzo可以切片
使用场景:
-
Bzip2: 对速度没有要求,常作为reduce输出结果的压缩格式,即便job运行后,输出的结果还需要被另一个Job继续处理,Bzip2格式也可以支持切片
-
Lzo: 作为Job输入文件的压缩格式
-
Snappy: 作为shuffle阶段的压缩格式
4.压缩的考虑
①Mapper的输入:主要考虑每个文件的大小,如果文件过大,需要使用可以切片的压缩格式
②Reducer的输出:reducer的输出主要考虑,输出之后,是否需要下一个Job继续处理,如果需要被下个Job继续处理,且单个文件过大,也要使用可以切片的压缩格式
③shuffle阶段:速度快即可
5.压缩的参数
io.compression.codecs : 代表整个Job运行期间,可以使用哪些压缩格式!
- 配置这个参数后,配置的压缩格式会被自动初始化!
- 默认值: deflate,gzip,bzip2
mapreduce.map.output.compress: map阶段输出的key-value是否采用压缩
- 默认值: false
mapreduce.map.output.compress.codec: map阶段输出的key-value采用何种压缩
- 默认值: deflate
mapreduce.output.fileoutputformat.compress: job在reduce阶段最终的输出是否采用压缩
- 默认值: false
mapreduce.output.fileoutputformat.compress.codec: job在reduce阶段最终的输出采用何种压缩
- 默认值: deflate