1:MapReduce概述
1.1 MapReduce定义
MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。
MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
1.2 MapReduce的优缺点
1.2.1 优点–简单
1.2.1.1 MapReduce易于编程
它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的PC机器上运行。也就是写一个分布式程序,跟写一个简单的串行程序是一样的。
1.2.1.2 良好的扩展性
可以通过简单的增加机器来扩展它的计算能力
1.2.1.3 高容错性
MapReduce设计的初衷就是使程序恩能够部署在廉价的PC机器上,所以容错性很高。如果其中的一台机器gg了,它可以把上面的计算任务转移到另一个节点上运行。这整个过程都由Hadoop内部完成,不需要人工参与。
1.2.1.4 适合PB级以上海量数据的离线处理
1.2.2 缺点–慢
1.2.2.1 不擅长实时计算
MapReduce无法向MySQL一样,在毫秒或者秒级内返回结果
1.2.2.2 不擅长流式计算
流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化。但实际上从静态文件到动态数据流也不是不可以跨越的,例如spark就可以解决。但是因为速度慢,即使解决了数据状态的转换还是没有很大的意义。
1.2.2.3 不擅长DAG(有向图)计算
多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出,在这种情况下,MapReduce也不是不可以做,而是使用之后,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常低下。
1.3 MapReduce核心思想
以wordcount这个程序为例:
简单来说就是:Map就是把数据拆分成键值对的形式存储,Reduce就是把相同的键值对合并成一个。
1.4 MapReduce进程
一个完整的MapReduce程序在分布式运行时有三类实例进程:
- MrAPPMaster:负责整个程序的过程调度及状态协调。
- MapTask:负责Map阶段的整个数据处理流程。
- ReduceTask:负责Reduce阶段的整个数据处理流程。
1.5 常用数据的序列化类型
Java类型 | Hadoop Writable类型 |
---|---|
Boolean | BooleanWritable |
Byte | byteWritable |
Int | IntWritable |
Float | FloatWritable |
Long | LongWritable |
Double | DoubleWritable |
String | Text |
Map | MapWritable |
Array | ArrayWritable |
1.6 MapReduce编程
分三个部分:Mapper、Reducer、Driver
以一个wordcount的程序为例
1.6.1 Mapper阶段
- 用户自定义的Mapper要继承自己的父类
- Mapper的输入数据是KV对的形式
- Mapper中的业务逻辑写在map()方法中
- Mapper的输出数据是KV对的形式
- map()方法(MapTask进程)对每一个<K,V>调用一次
public class WcMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
//LongWritable:行首在文件中的偏移量,也就是当前行第一个字符到行首中间有多少个字符(包括换行符)
//Text:这行的内容
//后两个泛型代表想把数据处理成何种形态的KV对,这里是string-int
//private Text word=new Text(); //2.0修改后
//private IntWritable one=new IntWritable(1); //2.0修改后
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//context:类似于这个框架任务的主线,输入来源于context,输出也传输给context
//map的任务是把输入数据变成一个word-1的形式
//拿到这行数据
String line = value.toString();
//按照空格切分数据
String[] words = line.split(" ");
//遍历数组,把单词变成(word,1)的形式,交给框架
for (String word : words) {
//this.word.set(word); //2.0修改后
//context.write(this.word,this.one); //2.0修改后
context.write(new Text(word),new IntWritable(1));
}
}
上述代码存在的问题是:如果数据量很大的情况下,会一直new Text()生成新对象,导致垃圾回收机制占比越来越大,程序变得越来越慢。具体修改方式为上述代码中注释为“2.0修改后”的部分。
1.6.2 Reducer阶段
public class WcReducer extends Reducer<Text, IntWritable,Text,IntWritable> {
//前两个是Reducer输入的KV,即Mapper输出的KV,后两个为Reducer输出的KV
private IntWritable total=new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
//这里的输入是同一个的key(即word)对应很多个value(即1)
int sum=0;
for (IntWritable value : values) {
sum+=value.get();
}
total.set(sum);
context.write(key,total);
}
}
1.6.3 Driver阶段
public class WcDriver {
//Driver就是对之前写的任务进行一些设置
public static void main(String[] args) throws InterruptedException, IOException, ClassNotFoundException {
//1.获取一个Job实例,job包装一下=context
Job job = Job.getInstance(new Configuration());
//2.设置类路径(classpath)
job.setJarByClass(WcDriver.class);
//3.设置Mapper和Reducer
job.setMapperClass(WcMapper.class);
job.setReducerClass(WcReducer.class);
//4.设置Mapper和Reducer输出的类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
//5.设置输入输出数据
FileInputFormat.setInputPaths(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
//6.提交Job
boolean b = job.waitForCompletion(true);
System.exit(b?0:1);
}
}
1.6.4 运行
设置一下输入输出文件地址:
运行结果:
1.7 打包到集群上运行
如果报错显示:User setting file does not exist …m2\setting.xml这样的问题
把maven的settings.xml文件粘到上述位置中,如果是IntelliJ IDEA的话一般在这个位置:
..\JetBrains\IntelliJ IDEA 2019.1.3\plugins\maven\lib\maven3\conf
把打包好的jar包放到虚拟机上执行,记得要启动yarn不然就会显示一直在尝试连接那台配置yarn的主机。
$ hadoop jar 1.jar com.us.wordcount.WcDriver /hello.txt /koutput #1.jar就是刚刚写的wordcount
$ hadoop fs -cat /koutput/* #查看一下结果,应该和之前的测试的一致
2. Hadoop序列化
2.1 什么是序列化
序列化局势把内存中的对象,转化成字节序列(或其他数据传输协议)以便于存储到硬盘(持久化)和网络传输。
反序列化就是将受到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象。
2.2 为什么要序列化
一般来说,“活的”对象只生存在内存里,关机就没有了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另外一台机器。然而序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算机。
2.3 Hadoop序列化(Writable)的特点
- 紧凑:高效使用存储空间
- 快速:读写数据的额外开销小
- 可扩展:随着通讯协议的升级而升级
- 互操作:支持多语言的交互
java的重量级序列化框架(serializable),在对数据序列化后会带有很多额外的信息,不便于在网络中传输,这也是Hadoop为什么不使用java序列化的原因。
hadoop在序列化的过程中不序列化类的信息,只是把必要的数据信息提取出来了。
例1:以一个计算手机用户上下行总流量的程序为例
input.txt的内容大致如下图,需要的部分为红色方框框出来的,分别是:手机号(可重复)、上行流量和下行流量。
分析:
Key-手机号;Value-{上行流量、下行流量和总流量}
所以这时候就需要一个bean来集合value的三个值,所以这个程序分为四部分:Flowbean、FlowMapper、FlowReducer、FlowDriver。
实现:
Flowbean
public class FlowBean implements Writable {
private long upFlow;
private long downFlow;
private long sunFlow;
public FlowBean() {
}
@Override
public String toString() {
return upFlow+"\t"+downFlow+"\t"+sunFlow;
}
public void set(long upFlow, long downFlow){
this.upFlow=upFlow;
this.downFlow=downFlow;
this.sunFlow=upFlow+downFlow;
}
//为了节省空间,这里就不写出三个变量的set和get方法
//为了节省空间,这里就不写出三个变量的set和get方法
/**
* 序列化方法
* @param dataOutput 框架提供的数据出口
* @throws IOException
*/
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sunFlow);
}
/**
* 反序列化方法
* @param dataInput 框架提供的数据来源
* @throws IOException
*/
public void readFields(DataInput dataInput) throws IOException {
upFlow=dataInput.readLong();
downFlow=dataInput.readLong();
sunFlow=dataInput.readLong();
}
}
FlowMapper
public class FlowMapper extends Mapper<LongWritable,Text,Text,FlowBean> {
private FlowBean flow=new FlowBean();
private Text phone=new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] fields = value.toString().split(" ");
phone.set(fields[1]);
long upflow = Long.parseLong(fields[fields.length - 3]);
long downflow = Long.parseLong(fields[fields.length - 2]);
flow.set(upflow,downflow);
context.write(phone,flow);
}
}
FlowReducer
public class FlowReducer extends Reducer<Text,FlowBean,Text,FlowBean> {
private FlowBean sumflow=new FlowBean();
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
long sumupflow=0;
long sumdownflow=0;
for (FlowBean value : values) {
sumupflow+=value.getUpFlow();
sumdownflow+=value.getDownFlow();
}
sumflow.set(sumupflow,sumdownflow);
context.write(key,sumflow);
}
}
FlowDriver
public class FlowDriver {
public static void main(String[] args) throws InterruptedException, IOException, ClassNotFoundException {
Job job = Job.getInstance(new Configuration());
job.setJarByClass(FlowDriver.class);
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
FileInputFormat.setInputPaths(job,new Path(args[0]));
FileOutputFormat.setOutputPath(job,new Path(args[1]));
boolean b = job.waitForCompletion(true);
System.exit(b?0:1);
}
}
3. MapReduce框架原理
shuffle其实是由Map Task的后半部分和Reduce Task的前半部分组成的
3.1 InputFormat数据输入
InputFormat负责把输入文件变成一个个KV值,也就是说InputFormat决定数据要如何切分。
3.1.1 数据切片与MapTask并行度决定机制
前提:现有一个300M的文件,数据块的存储大小为128M
planA:按照100M的切片大小切
优点:每个MapTask处理的数据都是一样大的,效率上升。
缺点:会造成网络传输。总所周知yarn会尽量把MapReduce启动在当前切片数据量最大的节点上,这样可以避免过多的网络传输。但是在这样切的情况下,如下图,300M的数据造成了84M的网络传输。
planB:按照块大小切(128M)
虽然MapTask1和MapTask2多处理了28%的数据,但是不用网络传输了,提高了整体的集群性能。
总的来说:
对于第四点:如果一个input里面有三个文件,打小分别是:300M、10M和20M。那么切片的时候300M切3片,10M切一片,20M切一片。切片是按文件来切的,不会考虑整个输入的整体。
在框架中,剩余的数据大小要大于切片大小的1.1倍才进行切片,否则是不会切片的。如果大于的话,依旧切1倍的切片大小数据出来。
3.1.2 InputFormat官方提供类
InputFormat主要干了两件事情
- getSplits 切片
- 得到RecordReader<K, V>,也就是把切片转换成KV值
3.1.2.1 TextInputFormat
- 切片方法:直接使用FileInputFormat的切片方法,就是默认的方法,如上述绿色字体所述。
- KV方法:LineRecordReader。键是存储该行在整个文件中的起始字节偏移量,LongWritable类型。值是这行的内容,不包括任何行终止符(换行符和回车符),text类型。
3.1.2.2 KeyValueTextInputFormat
- 切片方法:直接使用FileInputFormat的切片方法,就是默认的方法,如上述绿色字体所述。
- KV方法:KeyValueLineRecordReader。每一行均为一条记录,被分隔符分割为key,value。可以通过在驱动类设置
conf.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR,"\t")
,来设置分隔符,默认是tab。
3.1.2.3 NLineInputFormat
- 切片方法:自定义。代表每个map进程处理的InputSplit不再按照Block块去划分,而是按照NlineInputFomart指定的行数N来划分。即输入文件的总行数/N=切片数,如果不整除,切片数=商+1.
- KV方法:LineRecordReader。
3.1.2.4 CombineTextInputFormat
框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会使一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率低下。 - 应用场景:小文件过多。它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件可以交给一个MapTask来处理。
- 虚拟存储切片最大值设置:
CombineTextInputFormat.setMaxInputSplitSize(job,4194304)//4M
。最好根据实际小文件大小情况来设置具体值。 - 切片机制:
- KV方法:CombineFileRecordReader。其实就是一个跨文件的LineRecordReader。
3.1.2.5 FixedLengthInputFormat
不常用
切片方法:FIF切片方式(默认方法)。
KV方法:FixedLengthRecordReader。前面都是按行读,这是读取一个定长的数据。
3.1.2.6 SequenceFileInputFormat
- 切片规则:FIF切片方式
- KV方法:SequenceFileRecordReader。这个用于接收上一个MapReduce的输出数据,完成两个MapReduce之间的对接。
3.1.3 实现InputFormat自定义类
例2:实现InputFormat的自定义类.
戳->Hadoop之MapReduce—自定义InputFormat
3.2 Shuffle的详细工作流程
字数较多,写在另一篇文章里面了:Shuffle的详细工作流程
3.3 Reduce输入原理
例5中,Reduce方法只写了一句话,输出便是每个组的最大值(只有三行数据):
context.write(key,NullWritable.get());
现把这句话改成:
for (NullWritable value : values) {
context.write(key,value);
}
输出变成了按组排序,并且组内降序排序的文件(七行数据全部在)。
这是因为:
- 在框架中,KEY和Value的对象都只有一个,数据流输入进来,把KV值反序列化赋值到这两个对象上进行操作,结束之后,下个KV依旧赋值到的是这两个对象上。所以此时的Key值是包含了文件的所有数据,随着数据流的都会流入对象中,通过这样的操作来完成对所有数据的遍历。
- 分组也是在这样的过程中完成的,在遍历的过程中,只需判断当前的数据是否和上一个数据是否满足同组的条件,满足即为一组,不满足就代表上一组的数据到此为止了(红色框)。从这个数据开始是新的组的数据了(第三个KV值开始)。
根据这个原理,现在要取出每个组商品价格前两名可以这样写:
@Override
protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
Iterator<NullWritable> iterator = values.iterator();
for (int i = 0; i < 2; i++) {
if (iterator.hasNext()) {
context.write(key,iterator.next());
}
}
}
}
3.4 OutputFormat 数据输出
3.4.1 OutputFormat接口实现类
类型 | 特点 |
---|---|
TextOutputFormat | 每条记录写为文本行,KV可以是任何类型,因为都会被toSting()方法转换成字符串 |
SequenceFileOutputFormat | 将SequenceFileOutputFormat输出作为后续MapReduce任务的输入,它的格式紧凑,容易被压缩 |
自定义OutputFormat | 根据需求自定义 |
例6:自定义OutputFormat
- 需求:过滤输入的log日志,包含"us"的网站输出到D:/us.log,不包含"us"的网站输出到D:/other.log
- 思路:因为要实现自定义的OutputFormat接口,所以在输出的时候把数据分为两个文件输出
- 实现:
- MyRecordWriter
public class MyRecordWriter extends RecordWriter<LongWritable, Text> {
private FSDataOutputStream us;
private FSDataOutputStream other;
/**
* 初始化方法,通过获取job的信息,来使文件可以输入到Driver定义的文件夹中
* @param job
*/
public void initialize(TaskAttemptContext job) throws IOException {
String outdir = job.getConfiguration().get(FileOutputFormat.OUTDIR);
FileSystem fileSystem = FileSystem.get(job.getConfiguration());
us = fileSystem.create(new Path(outdir + "/us.log"));
other=fileSystem.create(new Path(outdir + "/other.log"));
}
/**
* 将KV写出,每对KV调用一次
*/
@Override
public void write(LongWritable key, Text value) throws IOException, InterruptedException {
//读数据的时候会把换行去掉,写的时候要记得把换行加回来
String out = value.toString() + "\n";
if(out.contains("us")){
us.write(out.getBytes());
}else {
other.write(out.getBytes());
}
}
@Override
public void close(TaskAttemptContext context) throws IOException, InterruptedException {
IOUtils.closeStream(us);
IOUtils.closeStream(other);
}
}
- MyOutputFormat
public class MyOutputFormat extends FileOutputFormat<LongWritable, Text> {
@Override
public RecordWriter<LongWritable, Text> getRecordWriter(TaskAttemptContext job) throws IOException, InterruptedException {
MyRecordWriter myRecordWriter = new MyRecordWriter();
myRecordWriter.initialize(job); //这个方法是自定义的,框架不会自动调用,要手动调用
return myRecordWriter;
}
}
- OutputDriver
只需要在基础上增加下面这句,把OutputFormat换成自定义的版本即可:
job.setOutputFormatClass(MyOutputFormat.class);
甚至因为Mapper和Reducer没有做任何操作,可以不必写关于他们set
3.5 ReduceJoin
ReduceJoin就是把两个文件的数据合并成一个文件。
例7的思路就是hive的底层实现,ReduceJoin代表Join的过程在Reduce阶段实现。
例7:ReduceJoin的案例
看看就好,了解一下hive处理join时的方式。
- 需求
把下面两张表中 的数据根据商品pid合并到订单数据表中
最终预期结果如下图:
- 思路
首先,需要把包装一个新的数据类型。
其次,因为需要让order.pid=pd.pid,所以相同pid的数据一定要同一组进入reduce()。所以这里需要一个自定义分组,把pid相同的所有数据放到同一组,再用pd中的pname替换掉该组order数据中的pid。
最后,分组时,应该把从pd中拿到的数据放在第一条,这样后续才能对该组其他数据进行修改。 - 实现
- OrderBean
需要的参数:
compareTo()函数
其他的不打紧,和之前的案例基本一致。 - RJComparator
和例5的思路是一样的,要多申明一个对象,才能进行比较
- RJMapper
因为两个文件要分开处理,不同文件的处理方式不同,所以把获取文件名放在setup()函数中进行。每个MapTask都要进行一遍setup()和很多遍的map()。
map()分别对两个文件进行赋值,每个变量在每次赋值时都赋值,没有就赋空值。
- RJReducer
Reduce主要负责把第一条数据取出,然后把pname的值赋给同组中表order中的数据。
- RJDriver
无特别之处,不写了。
3.7 MapJoin
MapJoin则代表Join的过程在Map阶段实现。
3.7.1 使用场景
MapJoin适合用于一张表很小,一张表很大的场景。这是因为在实现的过程中,要把小的那张表完全的缓存进内存。所以当不止两张表的时候,也是依旧只能有一张比较大的表,其余表都要比较小。
3.7.2 优点
快,如果在Map阶段就完成了Join,就不需要Reduce了,Reduce没有了也就不需要shuffle了。
没有shuffle的不但速度提高了很多,也不会发生数据倾斜了。
数据倾斜:
可以看到数据这样分布的情况下,即使有3个ReduceTask,处理的速度也并没有到达三倍。有一个ReduceTask要处理的数据尤其多。
数据倾斜引起的原因:是因为shuffle对数据进行了重新分配导致的。
例8:MapJoin的案例
看看就好,了解一下hive处理join时的方式。
-
需求
同例7 -
思路:把pd存到缓存中,在读取order数据的时候吧pid换成pname,再写入到框架中。
-
实现
- MJDriver
需要表明特别写出放进缓存的表
- MJMapper
使用hashMap来存储pd中的数据:
把缓存中的数据读出来,写到pMap中去
把pid换成pname,写到框架中
可见每一个MapTask都需要一份文件,所以当MapTask很多的情况下,就需要很大的存储空间。一般控制在15M以下。
3.8 计数器
Hadoop 为每个作业维护若干内置计数器,以描述多项指标。例如,某些计步器记录已处理的字节数和记录数,使用户可监控已处理的输入数据和已产生的输出数据。
3.9 数据清洗
在运行核心业务MapReduce程序之前,往往要先对数据进行清洗,清洗掉不符合用户要求的数据。清洗的过程往往只需要运行Mapper程序,不需要运行Reduce程序。
数据清洗有专门的数据清洗的工具,这里就不演示用MapReduce的方式做数据清洗了。
4.MapReduce总结
4.1 输入数据接口:InputFormat
- 默认使用的实现类是TextInputFormat
- TextInputFormat的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为key,内容作为value返回。
- KeyValueTextInputFormat每一行均为一条记录,被分隔符分割为key和value,默认分隔符是tab
- NlineInputFormat按照指定的行数N来划分切片,到KV值的过程和TextInputFormat一致。
- CombineTextInputFormat可以把读个小文件合并成一个切片处理,提高处理效率
- 自定义,要继承FileInputFormat
4.2 逻辑处理接口:Mapper
用户根据业务需求实现其中三个方法:map(),setup(),cleanup()
4.3 Partitioner分区
- 有默认实现HashPartitioner,逻辑是根据key的哈希值和numReduces来返回一个分区号;
key.hashCode()&Integer.MAXVALUE%numReduces
- 也可自定义
4.4 Comparable排序
- 当我们用自定义的对象作为key来输出时,就必须实现WritableComparable接口,重写其中的compareTo()方法。
- 部分排序:对最终输出的每一个文件进行内部排序(环形缓冲区出来的数据)
- 全排序:对所有数据进行排序,通常只有一个Reduce
- 二次排序:排序的条件有两个
4.5 Combiner合并
Combiner合并可以提高程序执行效率,减少IO传输。但是使用时必须不能影响原有的业务处理结果。
4.6 Reduce端分组:GroupingComparator
在Reduce端对key进行分组。应用于:在接受的key为bean对象时,想让一个或者几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。
4.7 逻辑处理接口:Reducer
用户根据业务需求实现其中的三个方法:reduce(),setup(),cleanup()
4.8 输出数据接口:OutputFormat
- 默认实现类是TextOutputFormat,功能逻辑是:将每个KV对,向目标文本输出一行
- 将SequenceFileOutputFormat输出作为后续MapReduce任务的输入,这便是一种好的输出格式,因为它的格式紧凑,很容易被压缩
- 可自定义
============================================END