Hadoop之MapReduce基础知识
一、MapReduce的概念
MapReduce是一个处理海量数据的分布式计算框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。
二、MapReduce的优缺点
优点:
- MapReduce易于编程
- 良好的扩展性
- 高容错性
- 适合PB级别的海量数据的离线处理分析
缺点:
- 不擅长实时计算
- 不擅长流式计算
- 不擅长DAG(有向图)计算
三、MapReduce的核心思想(Map和Reduce)
- job(作业):一个MapReduce程序称为一个job
- MRAppMaster(MapReduce的主节点):一个job在运行 时,会先启动MRAppMaster,它负责监控job的运行状态,和ResourceManager申请资源,提交任务等
- Map(Map阶段):Map是MapReduce程序运行的第一个阶段;Map阶段的目的就是把输入的文件进行切分,每个部分称为一个切片,每个切片对应一个MapTask任务,每个MapTask并行运行
- Reduce(Reduce阶段):Reduce是MapReduce程序运行的第二个阶段,也是最后一个阶段;Reduce阶段的目的是把Map阶段每个MapTask任务处理输出的数据进行一个汇总,该阶段是可选的,不一定要存在
- 一个MapReduce程序只能有一个Map和一个Reduce,如果业务逻辑非常复杂,需要编写多个MapReduce程序,然后串行运行
四、MapReduce进程
一个完整的MapReduce程序有三大实例进程:
- MRAppMaster:负责整个过程调度以及状态协调
- MapTask:负责Map阶段整个数据处理的流程
- ReduceTask:负责Reduce阶段整个数据处理的流程
五、MapReduce编程规范
用户编写的程序分成三个部分:Mapper、Reducer和Driver。
- Mapper:创建一个类继承Mapper,主要实现里面的map()方法,还有setup()、cleanup()
- Reducer:创建一个类继承Reducer,主要实现里面的reduce()方法,还有setup()、cleanup()
- Driver:
1. 创建数据的输入路径和输出路径(例:Path inputPath=new Path(“args[0]”))
2. 创建conf和FileSystem
3. 判断输出路径是否存在,若存在就删除
4. Job job=Job.getInstense(conf)
5. 设置Mapper和Reducer所在的类名
6. 设置Mapper和Reducer的各自输出数据的类型(如果一样就只设置Reducer的)
7. 设置数据输入路径和输出路径
8. 最后编写等待程序执行的代码
9. 如果程序有分区,需要设置reduce个数等
六、序列化
- 序列化定义:序列化是把内存中的对象转为字节序列,以便于存储和传输
- 反序列化定义:把收到的字节序列或者是硬盘的持久化数据,转换为内存中的对象
- 为什么不用Java的序列化:java中的序列化,在序列化过程中,会附带很大额外的信息(比如各种校验信息、继承体系等),这些是不方便在网络中高效传输的,所以Hadoop自带序列化机制
- Hadoop序列化机制的特点:紧凑、高效、可扩展、互操作、快速等
- 序列化和反序列化代码编写(实现Writable接口)
//重写序列化方法
@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();
}
- 常用数据序列化类型
Java类型 | Hadoop Writable类型 |
boolean | BooleanWritable |
byte | ByteWritable |
int | IntWritable |
float | FloatWritable |
long | LongWritable |
double | DoubleWritable |
String | Text |
map | MapWritable |
array | ArrayWritable |
七、MapReduce工作流程(简述)
- InputFormat阶段:InputFormat调用RecordReader方法读取输入文件中的每一行,把它们以k-v的形式输入到Map
- Map阶段:Mapper调用map方法,把接收到的k-v经过逻辑处理后,封装为k-v形式的数据输出
- shuffle阶段:系统排序的过程(后面详解)
- Reduce阶段:Reducer调用Reduce方法,把map输出的k-v经过逻辑处理后,然后再以k-v形式写出
- OutputFormat阶段:OutputFormat调用RecordWriter,把Reduce输出的k-v写出到文件之中
八、InputFormat数据输入(框架默认的是TextInputFormat)
- 数据块和数据切片的区别:
- 数据块是Hadoop在存储文件时把文件按照系统默认的块大小(128M)进行物理上的切分,然后一块一块的存储在HDFS上
- 数据切片是MapReduce程序在InputFormat输入阶段把输入文件按照系统默认或者自己设置的切片大小进行一个逻辑上的切分,并不会在磁盘上切分成片来存储
- 切片与MapTask并行度决定机制:(MapTask的并行度决定Map阶段的任务处理并发度,进而影响到整个Job的处理速度。)
- 一个job的Map阶段的并行度由客户端提交job时的切片数决定
- 切片的多少决定MapTask的多少,一个切片分配一个MapTask并行实例处理
- 默认情况下,切片大小等于块大小(128M)
- 切片时不考虑数据集整体,而是针对每一个文件单独切片(所以小文件过多情况下,会导致MapTask过多,从而MapReduce的性能就降低)
- FileInputFormat切片源码解析(input.getSplits(job))
- 程序先找到你数据存储的目录
- 开始遍历处理目录下的每一个文件
- 遍历第一个文件ss.txt:
- 获取文件大小 fs.sizeOf(ss.txt)
- 计算切片大小 computeSliteSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M(默认情况下,切片大小=blocksize)
- 开始切,形成第1个切片:ss.txt—0:128M 第2个切片ss.txt—128:256M 第3个切片ss.txt—256M:300M(每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片)
- 将切片信息写到一个切片规划文件中
- 整个切片的核心过程在getSplit()方法中完成
- InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等
- 提交切片规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数
- FileInputFormat切片机制:简单地按照文件的内容长度进行切片
- FileInputFormat的实现类
- TextInputFormat:框架默认的,每条记录是一行输入。键是LongWritable类型,存储该行在整个文件中的起始字节偏移量。值是这行的内容,不包括任何行终止符(换行符和回车符)。
- CombineTextInputFormat:适用于小文件过多的场景。它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
job.setInputFormatClass(CombineTextInputFormat.class);
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); //4M
虚拟存储切片最大值设置可以根据实际的小文件大小情况来设置具体的值。
- KeyValueTextInputFormat:每一行均为一条记录,被分隔符分割为key,value,默认分隔符是tab(\t)。
job.setInputFormatClass(KeyValueTextInputFormat.class);
conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR, "\t");
- NLineInputFormat:如果使用NlineInputFormat,代表每个map进程处理的InputSplit不再按Block块去划分,而是按NlineInputFormat指定的行数N来划分。即输入文件的总行数/N=切片数,如果不整除,切片数=商+1。
//使用NLineInputFormat处理记录数
job.setInputFormatClass(NLineInputFormat.class);
//设置每个切片InputSplit中划分三条记录
NLineInputFormat.setNumLinesPerSplit(job, 3);
- 自定义InputFormat:
- 自定义一个类继承FileInputFormat
- 重写isSplitable()方法,返回false不可切割
- 重写createRecordReader()方法,创建自定义的RecordReader对象,并初始化
- 改写RecordReader,实现一次读取一个完整的文件并封装为k-v
- 采用IO流一次读取一个文件输出到value中,因为设置了不可切片,最终把所有文件都封装到value
- 获取文件路径信息+名字,并设置key
- 设置Driver
job.setInputFormatClass(MyInputFormat.class);
九、OutputFormat数据输出(OutputFormat是MapReduce输出的基类)
- TextOutputFormat:默认的输出格式,它把每条记录写为文本行。它的键和值可以是任意类型,因为TextOutputFormat调用toString()方法把它们转换为字符串。
- SequenceFileOutputFormat:将SequenceFileOutputFormat输出作为后续 MapReduce任务的输入,这便是一种好的输出格式,因为它的格式紧凑,很容易被压缩。
job.setOutputFormatClass(SequenceFileOutputFormat.class);
- 自定义OutputFormat:(使用场景:为了实现控制最终文件的输出路径和输出格式)
- 自定义一个类继承FileOutputFormat,重写getRecordWriter()方法,返回一个自定义的RecordWriter
- 自定义一个RecordWriter继承RecordWriter,具体重写输出数据的方法write()
- 设置Driver
job.setOutputFormatClass(MyOutputFormat.class);
十、MapTask工作机制
- read阶段:利用Inputformat调用RecordReader来读取输入文件的每一行,从输入InputSpilt中解析出一个个k-v
- map阶段:将解析的k-v交给Mapper的map方法,进行逻辑处理后,以k-v形式输出
- collect阶段:map数据处理完后,一般会调用OutputCollector.collect()输出结果。该函数内部会调用patitioner将map生成的k-v进行分区,然后写入到一个环形缓冲区中
- spill阶段:当环形缓冲区中达到其内存大小的80%,mapreduce会将文件溢出,溢出时会对文件进行本地排序,必要时对数据进行合并或者压缩操作,然后再写到磁盘上,生成一个临时文件。
- combine阶段:当所有文件溢出处理完毕后,MapTask会对所有临时文件按照分区进行合并,确保只有一个数据文件
十一、ReduceTask工作机制
- copy阶段:把MapTask阶段生成并写在磁盘上的文件拷贝过来,如果数据大小超过一定的阈值,则写到磁盘中,否则直接放在内存中
- merge阶段:把从不同MapTask拷贝过来的文件按照分区号进行合并,防止内存和磁盘使用过多
- sort阶段:每个ReduceTask对应一个分区,每个分区内按照key进行排序
- reduce阶段:reduce()函数将计算结果写到HDFS上
十二、ReduceTask并行度
- ReduceTask的并行度同样影响整个job的执行并发度和执行效率,但与MapTask的并发数由切片数决定不同,Reducetask数量的决定是可以直接手动设置
//默认值是1,手动设置为4
job.setNumReduceTasks(4);
- 注意事项
- ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致
- ReduceTask默认值就是1,所以输出文件个数为一个
- 如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜
- ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个ReduceTask
- 具体多少个ReduceTask,需要根据集群性能而定
- 如果分区数不是1,但是ReduceTask为1,是否执行分区过程?答案是:不执行分区过程。因为在MapTask的源码中,执行分区的前提是先判断reduceNum个数是否大于1,不大于1肯定不执行
十三、shuffle机制
- Mapreduce确保每个Reducer的输入都是按key排序的。系统执行排序的过程(即将Mapper输出作为输入传给Reducer)称为Shuffle。
- shuffle工作流程:(三次排序;map shuffle有一次快速排序和一次归并排序;reduce shuffle有一次归并排序)
- MapTask收集map输出的k-v对,放到环形内存缓冲区中(默认100M)
- 从内存缓冲区中不断溢出文件到本地磁盘,可能会溢出多个文件;多个溢出的文件最终会合并一个大文件
- 溢出前,会调用Partitioner分区并且对每个分区中按照key进行排序;溢出完毕所有文件合并完成之后,还会对整个MapTask中数据进行一个排序
- ReduceTask会根据分区号去各个MapTask去相应的结果分区数据
- ReduceTask会把从不同MapTask但是属于同一个分区的数据进行一次合并(归并排序)
- 合并成大文件后,shuffle过程也就结束了,后面就进入了ReduceTask的逻辑运算过程。
- 注意:Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。
- Partiton分区
- 有默认实现 HashPartitioner,逻辑是根据key的哈希值和numReduces来返回一个分区号;(key.hashCode()&Integer.MAXVALUE % numReduces)
- 设置的ReduceTask的个数必须大于等于自定义的分区数,否则会报错,默认的ReduceTask只有一个
- 自定义分区:
- 创建一个类继承Partitioner,重写getPartiton()方法
- 在job驱动设置自定义分区类
- 在job驱动设置ReduceTask的个数,和分区数一致
- WritableComparable排序
- 默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
- 部分排序:对最终输出的每一个文件进行内部排序(只针对有分区的,每个分区内排序)
- 全排序:对所有数据进行排序,通常只有一个Reduce(只有一个ReduceTask,分区只有一个,最终只输出一个文件)
- 辅助排序:GroupingComparator分组,Mapreduce框架在记录到达Reducer之前按键对记录排序,但键所对应的值并没有被排序。一般来说,大多数MapReduce程序会避免让Reduce函数依赖于值的排序。但是,有时也需要通过特定的方法对键进行排序和分组等以实现对值的排序(对Reduce阶段的数据根据某一个或几个字段进行分组)
- 二次排序:在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序
- 自定义排序:
- bean对象实现WritableComparable接口重写compareTo方法
@Override
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
- Combiner合并
- combiner是mapper和reducer之外的一个组件
- combiner父类也是reducer,自定义时和自定义Reducer一样,都是继承Reducer
- combiner和reducer区别在于执行位置,combiner是在每个MapTask阶段进行数据合并,reducer是在reduce阶段进行数据汇总
- Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量
- Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv应该跟Reducer的输入kv类型要对应起来
- 做法:
- 自定义一个Combiner继承Reducer,重写Reduce方法
- 在Job驱动类中设置
job.setCombinerClass(WordcountCombiner.class);
十四、Join多种应用
- Reduce join
- 原理:对文件在Reduce端进行合并操作,在Map端不进行
- 缺点:会造成Shuffle阶段出现大量的数据传输,效率很低
- Map join
- 原理:把文件提前在Map阶段进行合并
- 适用场景:合适一张小表和一张大表的场景
- 优点:在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜
- 具体做法:采用DistributedCache
- 在Mapper的setup方法中,将文件读取到缓存集合中
- 在Driver函数中加载缓存
// 缓存普通文件到Task运行节点。
job.addCacheFile(new URI("file://e:/cache/pd.txt"));
十五、计数器应用(了解)
- 采用枚举的方式统计计数
enum MyCounter{MALFORORMED,NORMAL}
//对枚举定义的自定义计数器加1
context.getCounter(MyCounter.MALFORORMED).increment(1);
- 采用计数器组、计数器名称的方式统计
context.getCounter("counterGroup", "counter").increment(1);
//组名和计数器名称随便起,但最好有意义
- 计数结果在程序运行后的控制台上查看
十六、ETL数据清洗
编写MapReduce程序,对不符合要求的数据进行拦截。清理的过程往往只需要运行Mapper程序,不需要运行Reduce程序。