除了String对应Hadoop Writable类型为Text以外,其他基本都是类似boolean -> BooleanWritable
waitForCompletion()
submit();
// 1建立连接
connect();
// 1)创建提交Job的代理
new Cluster(getConfiguration());
// (1)判断是本地yarn还是远程
initialize(jobTrackAddr, conf);
// 2 提交job
submitter.submitJobInternal(Job.this, cluster)
// 1)创建给集群提交数据的Stag路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 2)获取jobid ,并创建Job路径
JobID jobId = submitClient.getNewJobID();
// 3)拷贝jar包到集群
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
// 4)计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
// 5)向Stag路径写XML配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
// 6)提交Job,返回提交状态
status = submitClient.submitJob(jobId, submitJobDir.toString(), job.getCredentials());
程序寻找数据存储目录;
开始遍历处理(规划切片)数据目录下的每一个文件;
遍历第一个文件file.txt(300MB)
2.1:获取文件大小fs.sizeOf(file.txt);
2.2:计算切片大小:computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M
minSize:默认值是0,因此决定切片大小默认情况是受限与blocksize;
mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue
maxsize并未在官网的mapred-default.xml文件中找到参考。【上面的参数我是没找到,尚硅谷是这么说的,大家自己瞅着看看奥】
maxsize(切片最大值):参数如果调得比 blocksize 小,则会让切片变小,就等于配置的这个参数的值。
minsize(切片最小值):参数调的比 blockSize 大,则可以让切片变得比 blocksize 还大
2.3:默认情况下,切片大小=blocksize;
2.4:开始对示例的第一个文件file.txt进行切片
【0,128)区间左闭右开
第一个切片:file.txt-----0:128M
第二个切片:file.txt-----128:256M
第三个切片:file.txt-----256:300M
每次开始切片前,要判断进行这一次切片后,如果剩下的部分大于blocksize的SPLIT_SLOP倍(默认值1.1倍)则可以切片,否则就和 前面的一起作为一个切片
切片大小=blocksize(本地运行默认32M,集群上为128M), 若为32.1M此时比值不大于SPLIT_SLOP(默认值1.1),则不进入while 循环,while循环中的split.add(***)用的参数是blocksize(32M),不进入while循环也就是比值不大于1.1时,使用的split.add(***) 用的 参数 是byteRemaining,此时就开一个大小为32.1的切片。
2.5:将切片信息写道一个切片规划文件中
2.6:整个切片的核心过程在getSplit()
方法中完成
2.7: InputSplit
只记录了切片的元数据信息,比如起始位置,长度,以及所在的节点列表等。
(1)简单地按照文件的内容长度进行切片
(2)切片大小,默认等于blocksize块大小
(3)切片时,不考虑数据集整体,而是针对每一个文件单独进行切片
(1)输入数据:file1.txt:300M;file2.txt:10M;
(2)经过FIleInputFormat切片机制处理后,得到的切片规划信息如下:
切片文件 | 切片范围【集群上bolcksize默认128M,本地32M】 |
---|---|
file1.txt.split1 | 0 ~ 128 M |
file1.txt.split2 | 128 ~ 256 M |
file1.txt.split3 | 256 ~ 300 M |
file2.txt.split1 | 0 ~ 10 M |
(1) 源码中计算切片大小的公式
Math.max(minSize, Math.min(maxSize, blockSize));
mapreduce.input.fileinputformat.split.minsize=1 默 认 值 为 1 mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue 因此,默认情况下,切片大小=blocksize。
(2) 切片大小设置
maxsize(切片最大值):参数如果调得比blockSize小,则会让切片变小,而且就等于配置的这个参数的值。
minsize(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大。
(3) 获取切片信息****API
// 获取切片的文件名称
String name = inputSplit.getPath().getName();
// 根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit) context.getInputSplit();
TextInputFormat这个类继承自FileInputFormat,FileInputFormat抽象类实现了InputFormat接口
FileInputFormat 常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、NLineInputFormat、CombineTextInputFormat 和自定义 InputFormat 等。
TextInputFormat 是默认的 FileInputFormat 实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable 类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text 类型。
框架默认的 TextInputFormat 切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask,这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。
1)应用场景:
CombineTextInputFormat 用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个 MapTask 处理。
2)虚拟存储切片最大值设置
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m=41024kb=41024*1024b
3)切片机制
生成切片过程包括:虚拟存储过程和切片过程二部分。
(1)虚拟存储过程:
将输入目录下所有文件大小,依次和设置的 setMaxInputSplitSize 值比较,
规则1: 文件大小 < setMaxInputSplitSize
:逻辑上单独划分一块(按照setMaxInputSplitSize的大小)
规则2:setMaxInputSplitSize < 文件大小 < 2*setMaxInputSplitSize
:对半切片分成两块。
规则3:文件大小 > 2*setMaxInputSplitSize
:可以完整切割下一块setMaxInputSplitSize,如果setMaxInputSplitSize<剩余的大小<2*setMaxInputSplitSize
则按文件大小对半切片分成两块。
例如 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
RecorderReader读取完文件后,把读到的信息给Mapper
Job的提交
1.Read阶段
红框就是Read阶段,读完之后回将读取到的数据传给Mapper,然后进入Map阶段。
Read阶段,通过InputFormat【默认是用TextInputFormat】去读取数据,通过调用RecordReader里的reader()方法。
读取到的Key是偏移量,Value是每一行的内容。【如果是CombineTextInputFormat,那就是一次读取一个文件】
2.Map阶段
3.Collect阶段
4.spill溢写阶段
5.Merge阶段
Merge阶段,对溢写阶段产生的大量溢写文件进行合并。
MapTask 并行度由切片个数决定,切片个数由输入文件和切片规则决定。
computeSliteSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M
公式,
调整 maxSize 最大值。让 maxSize 低于 blocksize 就可以增加 map 的个数。
Map端:
1.每个输入分片会让一个map任务来处理,默认情况下,以HDFS的一个块的大小(默认为128M)为一个分片,当然我们也可以设置块的大小。map输出的结果会暂且放在一个环形内存缓冲区中(该缓冲区的大小默认为100M,由io.sort.mb属性控制),当该缓冲区快要溢出时(默认为缓冲区大小的80%,由io.sort.spill.percent属性控制),会在本地文件系统中创建一个溢出文件,将该缓冲区中的数据写入这个文件。
2.在写入磁盘之前,线程首先根据reduce任务的数目将数据划分为相同数目的分区,也就是一个reduce任务对应一个分区的数据。这样做是为了避免有些reduce任务分配到大量数据,而有些reduce任务却分到很少数据,甚至没有分到数据的尴尬局面。
其实分区就是对数据进行hash的过程。然后对每个分区中的数据进行排序**【对索引进行排序】**,如果此时设置了Combiner,将排序后的结果进行Combiner操作,这样做的目的是让尽可能少的数据写入到磁盘。
3.当map任务输出最后一个记录时,可能会有很多的溢出文件,这时需要将这些文件合并。合并的过程中会不断地进行排序和Combiner操作,目的有两个:1.尽量减少每次写入磁盘的数据量;2.尽量减少下一复制阶段网络传输的数据量。最后合并成了一个已分区且已排序的文件。为了减少网络传输的数据量,这里可以将数据压缩,只要将mapred.compress.map.out设置为true就可以了。
4.将分区中的数据拷贝给相对应的reduce任务。有人可能会问:分区中的数据怎么知道它对应的reduce是哪个呢?其实map任务一直和其父TaskTracker保持联系,而TaskTracker又一直和JobTracker保持心跳。所以JobTracker中保存了整个集群中的宏观信息。只要reduce任务向JobTracker获取对应的map输出位置就可以了。
每读取一行就执行context.write(K,V),将数据写到环形缓冲区;
write()方法进入后,来到collection()方法,这里将执行Partition的分区策略,写好环形缓冲区中的索引和数据两个部分。
将所有溢写文件进行合并
*.out 是溢写文件
*.out.index 是记录了所有溢写文件的索引信息,这样reduce来磁盘拉取数据的时候,才知道自己拉的是哪个分区的数据。因为我们要求同一个分区的数据最终进入同一个reduce中。
数据已经持久化到了本地磁盘上,等待被reducer拉取
1.Copy阶段
不同的reducer拉取自己自己指定的分区
2.Sort阶段
Merge看需求有没有
3.Reduce阶段
(1) ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致。
(2) ReduceTask默认值就是1,所以输出文件个数为一个。
(3) 如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜
(4) ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个ReduceTask。
(5) 具体多少个ReduceTask,需要根据集群性能而定。
(6) 如果分区数不是1,但是ReduceTask为1,是否执行分区过程。答案是:不执行分区过程。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1。不大于1 肯定不执行。
Reduce端:
1.Reduce会接收到不同map任务传来的数据,并且每个map传来的数据都是有序的。
如果reduce端接受的数据量相当小,则直接存储在内存中(缓冲区大小由mapred.job.shuffle.input.buffer.percent属性控制,表示用作此用途的堆空间的百分比),如果数据量超过了该缓冲区大小的一定比例(由mapred.job.shuffle.merge.percent决定),则对数据合并后溢写到磁盘中。
2.随着溢写文件的增多,后台线程会将它们合并成一个更大的有序的文件,这样做是为了给后面的合并节省时间。其实不管在map端还是reduce端,MapReduce都是反复地执行排序,合并操作,现在终于明白了有些人为什么会说:排序是hadoop的灵魂。
3.合并的过程中会产生许多的中间文件(写入磁盘了),但MapReduce会让写入磁盘的数据尽可能地少,并且最后一次合并的结果并没有写入磁盘,而是直接输入到reduce函数。
getNumMapTasks();在init()的内部中,初始化maptask的数量,从而好让reduce知道去哪儿的分区拉取数据。
分配内存和磁盘空间的初始化
初始化init()结束之后,就开始执行run()方法,进入真正的数据抓取过程。
抓取数据,
对抓取过来的数据进行merge排序,copy阶段结束后,马上进入SORT阶段
后续写出处理
上图中reduce会进入我自己写的reduce代码。
Map 端的主要工作:为来自不同表或文件的 key/value 对,打标签以区别不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为 value,最后进行输出。
Reduce 端的主要工作:在Reduce 端以连接字段作为key 的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在 Map 阶段已经打标志)分开,最后进行合并就 ok 了。
对存储的内容进行封装
package com.atguigu.mapreduce.reducejoin;
import org.apache.hadoop.io.Writable; import java.io.DataInput;
import java.io.DataOutput; import java.io.IOException;
public class TableBean implements Writable { private String id; //订单 id
private String pid; //产品 id
private int amount; //产品数量
private String pname; //产品名称
private String flag; //判断是 order 表还是 pd 表的标志字段
public TableBean() {
}
public String getId() { return id;
}
public void setId(String id) { this.id = id;
}
public String getPid() { return pid;
}
public void setPid(String pid) { this.pid = pid;
}
public int getAmount() { return amount;
}
public void setAmount(int amount) { this.amount = amount;
}
public String getPname() { return pname;
}
public void setPname(String pname) { this.pname = pname;
}
public String getFlag() { return flag;
}
public void setFlag(String flag) { this.flag = flag;
}
@Override
public String toString() {
return id + "\t" + pname + "\t" + amount;
}
@Override
public void write(DataOutput out) throws IOException { out.writeUTF(id);
out.writeUTF(pid); out.writeInt(amount); out.writeUTF(pname); out.writeUTF(flag);
}
@Override
public void readFields(DataInput in) throws IOException {
this.id = in.readUTF();
this.pid = in.readUTF();
this.amount = in.readInt();
this.pname = in.readUTF();
this.flag = in.readUTF();
}
}
package com.atguigu.mapreduce.reducejoin;
import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.InputSplit; import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit; import java.io.IOException;
public class TableMapper extends Mapper{
private String filename; private Text outK = new Text();
private TableBean outV = new TableBean();
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//获取对应文件名称
InputSplit split = context.getInputSplit(); FileSplit fileSplit = (FileSplit) split; filename = fileSplit.getPath().getName();
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//获取一行
String line = value.toString();
//判断是哪个文件,然后针对文件进行不同的操作if(filename.contains("order")){ //订单表的处理
String[] split = line.split("\t");
// 封 装 outK
outK.set(split[1]);
// 封 装 outV
outV.setId(split[0]);
outV.setPid(split[1]);
outV.setAmount(Integer.parseInt(split[2]));
outV.setPname("");
outV.setFlag("order");
}else { //商品表的处理
String[] split = line.split("\t");
// 封 装 outK
outK.set(split[0]);
// 封 装 outV
outV.setId("");
outV.setPid(split[0]);
outV.setAmount(0);
outV.setPname(split[1]);
outV.setFlag("pd");
}
//K:订单ID,
//V:TableBean存储其他信息
context.write(outK,outV);
}
}
package com.atguigu.mapreduce.reducejoin;
import org.apache.commons.beanutils.BeanUtils; import org.apache.hadoop.io.NullWritable; import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException; import java.util.ArrayList;
public class TableReducer extends Reducer {
@Override
protected void reduce(Text key, Iterable values, Context context) throws IOException, InterruptedException {
ArrayList orderBeans = new ArrayList<>();
// 一个reduce接受到的pid都是相同的,因此他们的pname也都是相同的,只要一个pdBean类接收就ok
TableBean pdBean = new TableBean();
for (TableBean value : values) {
//判断数据来自哪个表
if("order".equals(value.getFlag())){ //订单表
//创建一个临时 TableBean 对象接收
value TableBean tmpOrderBean = new TableBean();
try {
BeanUtils.copyProperties(tmpOrderBean,value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
/**
Hadoop不允许直接orderBeans.add(value),要新new一个然后copy过去再放进集合中
*/
//将临时 TableBean 对象添加到集合
orderBeans orderBeans.add(tmpOrderBean);
}else { //商品表
try {
BeanUtils.copyProperties(pdBean,value);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
//遍历集合 orderBeans,替换掉每个 orderBean 的 pid 为 pname,然后写出
for (TableBean orderBean : orderBeans) {
orderBean.setPname(pdBean.getPname());
//写出修改后的 orderBean 对象
context.write(orderBean,NullWritable.get());
}
}
}
1)使用场景
Map Join 适用于一张表十分小、一张表很大的场景。
小表预先读取到缓存中。
2)优点
思考:在 Reduce 端处理过多的表,非常容易产生数据倾斜。怎么办?
在 Map 端缓存多张表,提前处理业务逻辑,这样增加 Map 端业务,减少 Reduce 端数
据的压力,尽可能的减少数据倾斜。
3)操作方法:采用 DistributedCache
1)需求分析
3)代码
1.驱动类中添加缓存文件
// 在driver类中添加下面内容
// 加载缓存数据
job.addCacheFile(new URI("file:///D:/input/tablecache/pd.txt"));
// Map 端 Join 的逻辑不需要 Reduce 阶段,设置 reduceTask 数量为 0
job.setNumReduceTasks(0);
2.在 MapJoinMapper 类中的 setup 方法中读取缓存文件
public class MapJoinMapper extends Mapper {
private Map pdMap = new HashMap<>();
private Text text = new Text();
//任务开始前将 pd 数据缓存进 pdMap
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//通过缓存文件得到小表数据 pd.txt
URI[] cacheFiles = context.getCacheFiles();
Path path = new Path(cacheFiles[0]);
//获取文件系统对象,并开流
FileSystem fs = FileSystem.get(context.getConfiguration());
FSDataInputStream fis = fs.open(path);
//通过包装流转换为 reader,方便按行读取
BufferedReader reader = new BufferedReader(new
InputStreamReader(fis, "UTF-8"));
//逐行读取,按行处理
String line;
while (StringUtils.isNotEmpty(line = reader.readLine())) {
//切割一行
//01 小米
String[] split = line.split("\t");
pdMap.put(split[0], split[1]);
}
//关流
IOUtils.closeStream(reader);
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//读取大表数据
//1001 01 1
String[] fields = value.toString().split("\t");
//通过大表每行数据的 pid,去 pdMap 里面取出 pname
String pname = pdMap.get(fields[1]);
//将大表每行数据的 pid 替换为 pname
text.set(fields[0] + "\t" + pname + "\t" + fields[2]);
//写出
context.write(text,NullWritable.get());
}
}
根据自己的清洗规则,在map()方法中,排除或保留符合规则的数据
1) 压缩的好处和坏处
压缩的优点:以减少磁盘 IO、减少磁盘存储空间。
压缩的缺点:增加CPU 开销。
2) 压缩原则
(1) 运算密集型的 Job,少用压缩
(2) IO 密集型的 Job,多用压缩
3)压缩方式选择
Gzip 压缩
优点:压缩率比较高;
缺点:不支持 Split;压缩/解压速度一般;
Bzip2 压缩
优点:压缩率高;支持 Split;
缺点:压缩/解压速度慢。
Lzo 压缩
优点:压缩/解压速度比较快;支持 Split;
缺点:压缩率一般;想支持切片需要额外创建索引。
Snappy 压缩
优点:压缩和解压缩速度快;
缺点:不支持 Split;压缩率一般;