MapReduce
一、MapReduce定义
MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。
MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
二、MapReduce优缺点
1、优点
- MapReduce易于编程
它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的PC机器上运行。也就是说你写一个分布式程序,跟写一个简单的串行程序是一模一样的。就是因为这个特点使得MapReduce编程变得非常流行。 - 良好的扩展性
当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。 - 高容错性
其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由Hadoop内部完成的。 - 适合PB级以上海量数据的离线处理
可以实现上千台服务器集群并发工作,提供数据处理能力。
2、缺点
- 不擅长实时计算
MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果。 - 不擅长流式计算
流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化。这是因为MapReduce自身的设计特点决定了数据源必须是静态的。 - 不擅长DAG(有向无环图)计算
多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce并不是不能做,而是使用后,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下。
三、Hadoop序列化
1、序列化概述
1)什么是序列化
序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输。
反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象。
2)为什么要序列化
一般来说,“活的”对象只生存在内存里,关机断电就没有了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。 然而序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算机。
3)为什么不用Java的序列化
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。所以,Hadoop自己开发了一套序列化机制(Writable)。
4)Hadoop序列化特点:
(1)紧凑 :高效使用存储空间。
(2)快速:读写数据的额外开销小。
(3)互操作:支持多语言的交互
2、常用数据序列化类型
Java类型 | Hadoop Writable类型 |
---|---|
Boolean | BooleanWritable |
Byte | ByteWritable |
Int | IntWritable |
Float | FloatWritable |
Long | LongWritable |
Double | DoubleWritable |
String | Text |
Map | MapWritable |
Array | ArrayWritable |
Null | NullWritable |
3、自定义bean对象实现序列化接口(Writable)
在企业开发中往往常用的基本序列化类型不能满足所有需求,比如在Hadoop框架内部传递一个bean对象,那么该对象就需要实现序列化接口。
具体实现bean对象序列化步骤如下7步。
- 必须实现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();
}
- 注意反序列化的顺序和序列化的顺序完全一致
- 要想把结果显示在文件中,需要重写toString(),可用"\t"分开,方便后续用。
- 如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,因为MapReduce框架中的Shuffle过程要求对key必须能排序。可以重写compareTo方法实现自排序
@Override
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
四、MapReduce示例
1、WordCountMapper
map端将文件中的每行数据处理成key、value键值对。
Mapper参数中四个值分别是 输入key类型,输入value类型,输出key类型,输出value类型。
输入key、value类型固定为 LongWritable 和 Text
输出key、value类型为map端输出key、value类型
注意:类型必须是序列化类型
- 主要逻辑写在map方法中,map方法每一行调用一次
- map方法之前可以有 setup 方法,只执行一次,可以用于一些初始化操作
- map方法之后可以有 cleanup 方法,只执行一次,可以用于关闭资源
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
Text k = new Text();
IntWritable v = new IntWritable(1);
String line;
String[] split;
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
line = value.toString();
split = line.split(" ");
for (String s : split) {
k.set(s);
context.write(k, v);
}
}
}
2、WordCountReducer
reduce端对map阶段输出的相同key进行聚合
Reducer四个参数分别是 map端输出key类型、map端输出value类型,reduce输出key类型,reduce输出value类型。
前两个参数与map端输出key、value类型对应,后两个参数为最终输出的key、value类型。
注意:类型必须是序列化类型
- 主要逻辑写在reduce方法中,reduce方法每一组key调用一次
- reduce 方法之前可以有 setup 方法,只执行一次,可以用于一些初始化操作
- reduce 方法之后可以有 cleanup 方法,只执行一次,可以用于关闭资源
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
IntWritable v = new IntWritable();
int sum;
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
v.set(sum);
context.write(key, v);
}
}
3、WordCountDriver
相当于yarn集群的客户端,用于提交整个程序到yarn集群,提交的是封装了MapReduce程序相关运行参数的job对象。
public class WordCountDriver {
public static void main(String[] args)
throws IOException, ClassNotFoundException, InterruptedException {
//获取配置文件对象
Configuration conf = new Configuration();
//获取job对象
Job job = Job.getInstance(conf);
//关联本Driver程序的jar
job.setJarByClass(WordCountDriver.class);
//关联mapper和reducer的jar
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
//设置mapper的输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//设置最终输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//设置输入路径和输出路径
FileInputFormat.setInputPaths(job, new Path("/wc/input"));
FileOutputFormat.setOutputPath(job, new Path("/wc/output"));
//提交job
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
五、MapReduce核心思想
1)分布式的运算程序往往需要分成至少2个阶段。
2)第一个阶段的MapTask并发实例,完全并行运行,互不相干。
3)第二个阶段的ReduceTask并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出。
4)MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行。
六、MapReduce进程
一个完整的MapReduce程序在分布式运行时有三类实例进程:
(1)MrAppMaster:负责整个程序的过程调度及状态协调。
(2)MapTask:负责Map阶段的整个数据处理流程。
(3)ReduceTask:负责Reduce阶段的整个数据处理流程。
七、MapReduce框架原理
1、InputFormat数据输入
(1)切片与MapTask并行度决定机制
数据块:Block是HDFS物理上把数据分成一块一块。数据块是HDFS存储数据单位。
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask。
(2)FileInputFormat切片机制
(一)FileInputFormat源码解析(input.getSplits(job))
IDEA中右上角“放大镜”搜索FileInputFormat类–》ctrl+f–》在左上方出现的搜索框上搜索:computeSplitSize–》回车即可找到此方法。
(1)找到你数据存储的目录。
(2)开始遍历处理(规划切片)目录下的每一个文件
(3)遍历第一个文件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倍就划分一块切片。以128M为例,128M的1.1倍是140.8M,最后一块切片若大于140.8M,则以128M为一片切片,若不大于,则分为一片。这样做可以防止最后一片过小)
-
将切片信息写到一个切片规划文件中
-
整个切片的核心过程在getSplit()方法中完成。
-
数据切片只是在逻辑上对输入数据进行分片,并不会在磁盘上将其切分成分片进行存储。InputSplit只记录了分片的元数据信息,比如起始位置、长度以及所在的节点列表等。
-
注意:block是HDFS物理上存储的数据,切片是对数据逻辑上的划分。
-
切块是物理概念,切片是逻辑概念。
-
130MB:两块一片
(4)提交切片规划文件到yarn上,yarn上的MrAppMaster就可以根据切片规划文件计算开启maptask个数。
(二)FileInputFormat中默认的切片机制
(1)简单地按照文件的内容长度进行切片
(2)切片大小,默认等于block大小
(3)切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
(三)FileInputFormat切片大小的参数配置
通过分析源码,在FileInputFormat的279行中,计算切片大小的逻辑:Math.max(minSize, Math.min(maxSize, blockSize));
切片主要由这几个值来运算决定
mapreduce.input.fileinputformat.split.minsize=1 默认值为1
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue
因此,默认情况下,切片大小=blocksize。
maxsize(切片最大值):参数如果调得比blocksize小,则会让切片变小,而且就等于配置的这个参数的值。
minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blocksize还大。
获取切片信息API
// 根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();
// 获取切片的文件名称
String name = inputSplit.getPath().getName();
(四)FileInputFormat实现类
思考:在运行MapReduce程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。那么,针对不同的数据类型,MapReduce是如何读取这些数据的呢?
FileInputFormat常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat和自定义InputFormat等。
(五)TextInputFormat
TextInputFormat是默认的FileInputFormat实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable类型
。值是这行的内容,不包括任何行终止符(换行符和回车符),是Text类型
。切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。
若要处理大量的小文件,就需要用到 CombineTextInputFormat
(六)CombineTextInputFormat
应用场景:
CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
实现过程:
驱动类中添加代码如下:
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
//设置虚拟存储切片最大值
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
//注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
切片机制:
生成切片过程包括:虚拟存储过程和切片过程二部分。
(1)虚拟存储过程:
将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现太小切片)。
例如setMaxInputSplitSize值为4M,输入文件大小为8.02M,则先逻辑上分成一个4M。剩余的大小为4.02M,如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02M文件切分成(2.01M和2.01M)两个文件。
(2)切片过程:
(a)判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片。
(b)如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
(c)测试举例:有4个小文件大小分别为1.7M、5.1M、3.4M以及6.8M这四个小文件,则虚拟存储之后形成6个文件块,大小分别为:
1.7M,(2.55M、2.55M),3.4M以及(3.4M、3.4M)
最终会形成3个切片,大小分别为:
(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M
2、MapReduce工作流程
(1)MapReduce详细工作流程之Map阶段
1、首先有一个200M的待处理文件
2、切片:在客户端提交之前,根据参数配置,进行任务规划,将文件按128M每块进行切片
3、提交:提交可以提交到本地工作环境或者Yarn工作环境,本地只需要提交切片信息和xml配置文件,Yarn环境还需要提交jar包;本地环境一般只作为测试用
4、提交时会将每个任务封装为一个job交给Yarn来处理(详细见后边的Yarn工作流程介绍),计算出MapTask数量(等于切片数量),每个MapTask并行执行
5、每个mapTask会调TextInputFormat去读这个切片内容,然后把读取到的切片内容存到mapTask里面。InputFormat方法,默认为TextInputFormat方法,在此方法调用LineRecordReader方法,将每个块文件封装为k,v键值对(一行一行读,key为该行在整个文件中的起始字节偏移量,value为 该行内容),传递给map方法
6、MapTask中执行Mapper的map方法,此方法需要k和v作为输入参数
7、map方法首先进行一系列的逻辑操作,执行完成后最后进行写操作(map方法如果直接写给reduce的话,相当于直接操作磁盘,太多的IO操作,使得效率太低,所以在map和reduce中间还有一个shuffle操作)
8、map处理完成相关的逻辑操作之后得到(key,value),需要通过outputCollector将(key,value)写入环形缓冲区(MapOutputBuffer),环形缓冲区主要两部分,一部分写入(key,value)的元数据信息(分区信息、keyStart、valueStart、valueLength),另一部分写入(key,value)的真实内容(key数据、value数据等)
9、outputCollector在将(key,value)对写入到缓冲区之前,需要先根据key进行分区操作(默认分区HashPartitioner,也可自定义分区规则),这样就能把map任务处理的结果发送给指定的reducer去执行,从而达到负载均衡,避免数据倾斜。(也就是对于每个键值对来说,都增加了一个partition属性值,然后连同键值对一起序列化成字节数组写入到缓冲区,即打上分区标签)
MapReduce提供默认的分区类(HashPartitioner),其核心代码如下:
public class HashPartitioner<K, V> extends Partitioner<K, V> {
/** Use {@link Object#hashCode()} to partition. */
public int getPartition(K key, V value,
int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
//Integer.MIN_VALUE=2147483647 转换成二进制值是01111111111111111111111111111111
//0是符号位,0&任何数为0
//key.hashCode() & 2147483647 可以保证结果是非负数
10、环形缓冲区的默认大小是100M(可自定义),当缓冲的容量达到默认大小的80%时,进行反向溢写,溢出到磁盘中(缓冲区采用的就是字节数组)。
11、在溢写之前会将缓冲区的数据按照指定的分区规则进行分区和快速排序,之所以反向溢写是因为这样就可以边接收数据边往磁盘溢写数据(数据读到缓存区时就会打上分区标签,根据分区标签进行分区并根据key快速排序)
12、在分区和排序之后,溢写到磁盘,可能发生多次溢写,溢写到多个文件
13、对所有溢写到磁盘的文件进行合并和归并排序
整个MapTask分为Read阶段,Map阶段,Collect阶段,溢写(spill)阶段和Merge阶段
- Read阶段:MapTask通过用户编写的RecordReader(系统默认的RecordReader是LineRecordReader,TextInputFormat),从输入InputSplit中解析出一个个key/value
- Map阶段:该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value
- Collect收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在该函数内部,它会将生成的key/value分区(调用Partitioner),并写入一个环形内存缓冲区中
- Spill阶段:即“溢写”,当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作
溢写阶段详情:
步骤1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号partition进行排序,然后按照key进行排序(这个排序内部是双层 for循环)。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。
步骤2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
步骤3:将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中。
- Merge阶段:
当所有数据处理完成后,MapTask对所有临时文件进行一次合并,以确保最终只会生成一个数据文件。
当所有数据处理完后,MapTask会将所有临时文件合并成一个大文件,并保存到文件output/file.out中,同时生成相应的索引文件output/file.out.index。
最终生成的文件格式与单个溢出文件一致,也是按分区顺序存储,并且输出文件会有一个对应的索引文件,记录每个分区数据的起始位置,长度以及压缩长度,这个索引文件名叫做file.out.index。
在进行文件合并过程中,MapTask以分区为单位进行合并。对于某个分区,它将采用多轮递归合并的方式。每轮合并io.sort.factor(默认100)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。
让每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
(2)combine
在map阶段还可以有一个Combine合并操作,需要自己定义Combiner类,且在程序中设置combine(程序中通过job.setCombinerClass(myCombine.class)自定义combine操作),意义是对每个MapTask的输出进行局部汇总,以减少网络传输量
- Map阶段的进程数比Reduce阶段要多,所以放在Map阶段处理效率更高
- Map阶段合并之后,传递给Reduce的数据就会少很多
- Combine输入和输出格式一致。因为Combine的输入是Map的输出,Combine的输出是Reduce的输入, 而Map的输出和Reduce的输入是一致的;所以,我们需要确保Combine的输入和输出一致。
- 但是Combiner能够应用的前提是不能影响最终的业务逻辑,而且Combiner的输出kv要和Reduce的输入kv类型对应起来(求和可以,求平均数不行)
程序中有两个阶段可能会执行combine操作:
- map输出数据根据分区排序完成后,在写入文件之前会执行一次combine操作(前提是作业中设置了这个操作);
- 如果map输出比较大,溢出文件个数大于3(此值可以通过属性min.num.spills.for.combine配置)时,在merge的过程(多个spill文件合并为一个大文件)中还会执行combine操作;
Combiner需要继承Reducer,重写reduce方法,在Reducer的输入输出键值对类型一致时,可以直接将Reducer当作Combiner使用,在job指定Reducer类作为 Combiner
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable outV = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
outV.set(sum);
context.write(key,outV);
}
}
job.setCombinerClass(WordCountCombiner.class);
(3)MapReduce详细工作流程之Reduce阶段
1、所有的MapTask任务完成后,启动相应数量的ReduceTask(和分区数量相同),并告知ReduceTask处理数据的范围(告诉他他要处理哪个分区的数据)
HTTP下载服务
2、ReduceTask会将MapTask处理完的数据拷贝一份到磁盘中(从每个MapTask中拷贝指定分区数据),并合并文件和归并排序
3、默认根据key相同的分为一组,也可自定义groupingComparator实现自定义分组。
4、最后将数据传给reduce进行处理,一次读取一组数据(以相同key为一组,因为有排序,所以不用再找数据,直接从上往下开始拿数据,直到该key数据取完)
5、最后通过OutputFormat输出
整个ReduceTask分为Copy阶段,Merge阶段,Sort阶段(Merge和Sort可以合并为一个),Reduce阶段。
- Copy阶段:ReduceTask从各个MapTask上针对某一分区远程拷贝数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中
- Merge阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多
- Sort阶段:按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可
- Reduce阶段:reduce()函数将计算结果写到HDFS上
(3)Shuffle机制
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle
Shuffle翻译成中文的意思为:洗牌、发牌(核心机制:数据分区、排序、缓存)
Shuffle流程
Shuffle的大致流程为:Maptask会不断收集我们的map()方法输出的kv对,添加分区标签并放到内存缓冲区中,当缓冲区达到饱和的时候(默认占比为0.8)就会溢出到磁盘中,如果map的输出结果很多,则会有多个溢出文件,多个溢出文件会被合并成一个大的溢出文件,在文件溢出、合并的过程中,都要调用partitoner进行分组和针对key进行排序(默认是按照Key的hash值对Partitoner个数取模),之后reducetask根据自己的分区号,去各个maptask机器上取相应的结果分区数据,reducetask会将这些文件再进行合并(归并排序)。
合并成大文件后,shuffle的过程也就结束了,后面进入reducetask的逻辑运算过程(从文件中取出每一个键值对的Group,调用UDF函数(用户自定义的方法))
注意
Shuffle 中的缓冲区大小会影响到 mapreduce 程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快,正是因为Shuffle的过程中要不断的将文件从磁盘写入到内存,再从内存写入到磁盘,从而导致了Hadoop中MapReduce执行效率相对于Storm等一些实时计算来说比较低下的原因。
Shuffle的缓冲区的大小可以通过参数调整, 参数:io.sort.mb 默认100M
3、自定义分区
(1)自定义分区步骤
继承Partitioner类,实现getPartition()方法,在方法中编写控制分区逻辑
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
//获取手机号前三位prePhone
String phone = text.toString();
String prePhone = phone.substring(0, 3);
//定义一个分区号变量partition,根据prePhone设置分区号
int partition;
if("136".equals(prePhone)){
partition = 0;
}else if("137".equals(prePhone)){
partition = 1;
}else if("138".equals(prePhone)){
partition = 2;
}else if("139".equals(prePhone)){
partition = 3;
}else {
partition = 4;
}
//最后返回分区号partition
return partition;
}
}
注意:Partitioner类的键值类型即为Mapper的输出类型,分区号必须从零开始
在驱动函数中增加自定义数据分区设置和ReduceTask设置
//指定自定义分区器
job.setPartitionerClass(ProvincePartitioner.class);
//同时指定相应数量的ReduceTask
job.setNumReduceTasks(5);
注意:虽然自定义分区中,分区编号是可以自己定义返回值的,不一定要顺序递增。但是出于性能考虑,分区编号最好是顺序递增的,reducetask的设置和分区个数相同,否则必然有reducetask在执行空跑。
(2)分区总结
- 如果ReductTask的数量>getPartition的结果数,则会有ReductTask在空跑,会多生成几个空的输出文件
- 如果1<ReductTask的数量<getPartition的结果数,则会有一部分分区数无处安放,会报异常
- 如果ReductTask的数量=1,则不管MapTask输出多少个分区文件,最终结果都会交给这一个ReducTask,最终也只会生成一个结果文件
- 分区号必须从零开始,逐一累加
4、Join应用
(1)Reduce Join工作原理
1、Map端的主要工作是:为来自不同表或文件的key/value对,打标签以区别不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。或者直接将整行输出,reduce端处理。
2、Reduce端的主要工作是:在Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组中将那些来源于不同文件的记录(在Map阶段已经打标签)分开,最后进行合并就好了
public class JoinMapReduce {
static class JoinMapper extends Mapper<LongWritable, Text, Text, Text> {
String name = null;
String uid = null;
Text k = new Text();
Text v = new Text();
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//获取切片
FileSplit split = (FileSplit) context.getInputSplit();
//获取切片文件名称
name = split.getPath().getName();
}
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//获取第一行内容
String line = value.toString();
//不同的文件处理逻辑不同
if (name.startsWith("user")) {
String[] split = line.split(",");
uid = split[0];
} else {
String[] split = line.split("\\s+");
uid = split[1];
}
k.set(uid);
v.set(line);
context.write(k, v);
}
}
static class JoinReducer extends Reducer<Text, Text, Text, NullWritable> {
Text k = new Text();
@Override
protected void reduce(Text key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
String line = null;
List<String> orderIds = new ArrayList<String>();
for (Text value : values) {
if (value.toString().contains(",")) {
//user只有一条所以直接赋值
line = value.toString();
} else {
//一个用户有多条订单信息,将他们先保存到一起
orderIds.add(value.toString().split("\\s+")[0]);
}
}
for (String orderId : orderIds) {
k.set(line + "," + orderId);
context.write(k, NullWritable.get());
}
}
}
public static void main(String[] args) throws Exception {
Job job = Job.getInstance(new Configuration());
//连接mapper
job.setMapperClass(JoinMapper.class);
//连接Reducer
job.setReducerClass(JoinReducer.class);
//Mapper输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
//Reducer输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//输入路径
FileInputFormat.setInputPaths(job, new Path("D:\\多易教育\\Hadoop\\joininput"));
//输出路径
FileOutputFormat.setOutputPath(job, new Path("D:\\多易教育\\Hadoop\\output"));
//执行
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
(2)Map Join工作原理
1、使用场景
Map Join适用于一张表十分小、一张表很大的场景。
2、优点
思考:在Reduce端处理过多的表,非常容易产生数据倾斜。怎么办?
在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。
3、具体办法:采用DistributedCache
(1)在Mapper的setup阶段,将文件读取到缓存集合中。
(2)在Driver驱动类中加载缓存。
Mapper代码
static class JoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
String uid = null;
Text k = new Text();
Map<String, String> map = new HashMap<String, String>();
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//已经将user.txt缓存到当前 mapTask 运行的工作目录,所以可直接访问当前目录的user.txt
BufferedReader read = new BufferedReader(new FileReader("user.txt"));
String line = "";
while ((line = read.readLine()) != null) {
String[] split = line.split(",");
map.put(split[0], line);
}
read.close();
}
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
//获取第一行内容
String line = value.toString();
String[] split = line.split("\\s+");
uid = split[1];
String s = map.get(uid);
k.set(s + "," + split[0]);
context.write(k, NullWritable.get());
}
}
job代码
//缓存普通文件到task运行节点的工作目录
job.addCacheFile(new URI("/user.txt"));
// Map端Join的逻辑不需要Reduce阶段,设置reduceTask数量为0
job.setNumReduceTasks(0);
注意:mapper的输出key、value类型与reducer的输出key、value类型一致,或只有mapper没有reducer时,都可以不在job设置mapper的输出key、value类型,直接设置最终输出key、value类型
5、OutputFormat数据输出
(1)OutputFormat接口实现类
- 文本输出TextOutputFormat:默认的输出格式是TextOutputFormat,它把每条记录写为文本行。它的键和值可以是任意类型,因为TextOutputFormat调用toString()方法把它们转换为字符串。
- SequenceFileOutputFormat:将SequenceFileOutputFormat输出作为后续 MapReduce任务的输入,这便是一种好的输出格式,因为它的格式紧凑,很容易被压缩。(可以将多个小文件合并成一个SequenceFile文件)
- 自定义OutputFormat:根据用户需求,自定义实现输出。
(2)自定义OutputFormat实操
(一)自定义一个××OutputFormat类继承FileOutputFormat
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
//创建一个自定义的RecordWriter返回
LogRecordWriter logRecordWriter = new LogRecordWriter(job);
return logRecordWriter;
}
}
实现getRecordWriter方法,创建一个自定义的RecordWriter返回
这里的FileOutputFormat的键值对与Reducer输出键值对一致
(二)编写××RecordWriter类继承RecordWriter
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream atguiguOut;
private FSDataOutputStream otherOut;
public LogRecordWriter(TaskAttemptContext job) {
try {
//获取文件系统对象
FileSystem fs = FileSystem.get(job.getConfiguration());
//用文件系统对象创建两个输出流对应不同的目录
atguiguOut = fs.create(new Path("d:/hadoop/atguigu.log"));
otherOut = fs.create(new Path("d:/hadoop/other.log"));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void write(Text key, NullWritable value) throws IOException, InterruptedException {
String log = key.toString();
//根据一行的log数据是否包含atguigu,判断两条输出流输出的内容
if (log.contains("atguigu")) {
atguiguOut.writeBytes(log + "\n");
} else {
otherOut.writeBytes(log + "\n");
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
//关流
IOUtils.closeStream(atguiguOut);
IOUtils.closeStream(otherOut);
}
}
在构造方法中获取文件系统对象,用文件系统对象创建输出流(FileSystem.get(job.getConfiguration())
实现write方法,写输出逻辑
关闭流
这里的RecordWriter的键值对与Reducer输出键值对一致
(三)在job中设置自定义的outputformat
//设置自定义的outputformat
job.setOutputFormatClass(LogOutputFormat.class);
//虽然我们自定义了outputformat,但是因为我们的outputformat继承自fileoutputformat
//而fileoutputformat要输出一个_SUCCESS文件,所以在这还得指定一个输出目录
//数据还是会输出到我们自定义的RecordWriter的输出目录中,_SUCCESS中不会有数据
FileOutputFormat.setOutputPath(job, new Path("D:\\logoutput"));
八、Hadoop数据压缩
1、概述
(1)压缩的好处和坏处
- 压缩的优点:以减少磁盘IO、减少存储占用的磁盘空间。
- 压缩的缺点:增加CPU开销。
(2)压缩原则
- 运算密集型的Job,少用压缩
- IO密集型的Job,多用压缩
2、MapReduce支持的压缩编码
(1)压缩算法对比介绍
压缩格式 | Hadoop自带? | 算法 | 文件扩展名 | 是否可切片 | 换成压缩格式后,原来的程序是否需要修改 |
---|---|---|---|---|---|
DEFLATE | 是,直接使用 | DEFLATE | .deflate | 否 | 和文本处理一样,不需要修改 |
Gzip | 是,直接使用 | DEFLATE | .gz | 否 | 和文本处理一样,不需要修改 |
bzip2 | 是,直接使用 | bzip2 | .bz2 | 是 | 和文本处理一样,不需要修改 |
LZO | 否,需要安装 | LZO | .lzo | 是 | 需要建索引,还需要指定输入格式 |
Snappy | 是,直接使用 | Snappy | .snappy | 否 | 和文本处理一样,不需要修改 |
(2)压缩性能的比较
压缩算法 | 原始文件大小 | 压缩文件大小 | 压缩速度 | 解压速度 |
---|---|---|---|---|
gzip | 8.3GB | 1.8GB | 17.5MB/s | 58MB/s |
bzip2 | 8.3GB | 1.1GB | 2.4MB/s | 9.5MB/s |
LZO | 8.3GB | 2.9GB | 49.3MB/s | 74.6MB/s |
在64位模式的核心i7处理器的单个核心上,Snappy以大约250MB/sec或更高的速度压缩,以大约500MB/sec或更高的速度解压缩。
3、压缩方式选择
压缩方式选择时重点考虑:压缩/解压缩速度、压缩率(压缩后存储大小)、压缩后是否可以支持切片。
(1)Gzip压缩
- 优点:压缩率比较高;
- 缺点:不支持Split;压缩/解压速度一般;
- 应用场景:应用场景:当每个文件压缩之后在130M以内的(1个块大小内),都可以考虑用gzip压缩格式。例如说一天或者一个小时的日志压缩成一个gzip文件,运行mapreduce程序的时候通过多个gzip文件达到并发。hive程序,streaming程序,和java写的mapreduce程序完全和文本处理一样,压缩之后原来的程序不需要做任何修改。
(2)Bzip2压缩
- 优点:压缩率高;支持Split;
- 缺点:压缩/解压速度慢。
- 应用场景:适合对速度要求不高,但需要较高的压缩率的时候,可以作为mapreduce作业的输出格式;或者输出之后的数据比较大,处理之后的数据需要压缩存档减少磁盘空间并且以后数据用得比较少的情况;或者对单个很大的文本文件想压缩减少存储空间,同时又需要支持split,而且兼容之前的应用程序(即应用程序不需要修改)的情况。
(3)Lzo压缩
- 优点:压缩/解压速度比较快;支持Split;
- 缺点:压缩率一般;想支持切片需要额外创建索引。
- 应用场景:一个很大的文本文件,压缩之后还大于200M以上的可以考虑,而且单个文件越大,lzo优点越越明显。
(4)Snappy压缩
- 优点:压缩和解压缩速度快;
- 缺点:不支持Split;压缩率一般;
- 应用场景:当Mapreduce作业的Map输出的数据比较大的时候,作为Map到Reduce的中间数据的压缩格式;或者作为一个Mapreduce作业的输出和另外一个Mapreduce作业的输入。
4、压缩位置选择
压缩可以在MapReduce作用的任意阶段启用。
在mapper阶段开启压缩不会影响最终输出
5、压缩参数配置
(1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器
压缩格式 | 对应的编码/解码器 |
---|---|
DEFLATE | org.apache.hadoop.io.compress.DefaultCodec |
gzip | org.apache.hadoop.io.compress.GzipCodec |
bzip2 | org.apache.hadoop.io.compress.BZip2Codec |
LZO | com.hadoop.compression.lzo.LzopCodec |
Snappy | org.apache.hadoop.io.compress.SnappyCodec |
(2)要在Hadoop中启用压缩,可以配置如下参数
参数 | 默认值 | 阶段 | 建议 |
---|---|---|---|
io.compression.codecs(在core-site.xml中配置) | 无,在命令行输入hadoop checknative查看支持哪些压缩 | 输入压缩 | Hadoop使用文件扩展名判断是否支持某种编解码器 |
mapreduce.map.output.compress(在mapred-site.xml中配置) | false | mapper压缩输出是否开启 | 这个参数设为true启用压缩 |
mapreduce.map.output.compress.codec(在mapred-site.xml中配置) | org.apache.hadoop.io.compress.DefaultCodec | mapper输出默认使用哪种压缩 | 企业多使用LZO或Snappy编解码器在此阶段压缩数据 |
mapreduce.output.fileoutputformat.compress(在mapred-site.xml中配置) | false | reducer压缩输出是否开启 | 这个参数设为true启用压缩 |
mapreduce.output.fileoutputformat.compress.codec(在mapred-site.xml中配置) | org.apache.hadoop.io.compress.DefaultCodec | reducer输出默认使用哪种压缩 | 使用标准工具或者编解码器,如gzip和bzip2 |
6、压缩实操案例
MapReduce压缩基本原则:
(1)运算密集型的job,少用压缩
(2)IO密集型的job,多用压缩
- 开启map端输出压缩
在 job中添加
configuration.setBoolean("mapreduce.map.output.compress", true);
// 设置map端输出压缩方式
configuration.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
- 设置reduce端输出压缩开启
在 job中添加
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
//FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);
//FileOutputFormat.setOutputCompressorClass(job, DefaultCodec.class);
九、数据倾斜
上游数据分发到下游任务,数据分配不均匀
解决数据倾斜的几种办法:
- 1、Combine
- 2、mapjoin
- 3、在map阶段处理数据时在key后面加随机数将key打散,需要再写一个mapReduce对有随机数的key做处理。
Random random = new Random();
//这里将随机数范围设为reduceTask个数,这样就可以将数据均匀的分布到每个分区中
int i = random.nextInt(numReduceTasks);
for (String split : splits) {
k.set(split + "-" + i);
context.write(k, v);
}