大数据全知识点讲解之Mapreduce
Mapreduce介绍
MapReduce的思想核心是“分而治之”,适用于大量复杂的任务处理场景(大规模数据处理场景)。
- Map负责“分”,即把复杂的任务分解为若干个“简单的任务”来并行处理。可以进行拆分的
前提是这些小任务可以并行计算,彼此间几乎没有依赖关系。 - Reduce负责“合”,即对map阶段的结果进行全局汇总。
- MapReduce运行在yarn集群:ResourceManager和NodeManager
这两个阶段合起来正是MapReduce思想的体现。
Mapreduce设计思想
MapReduce是一个分布式运算程序的编程框架,核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在Hadoop集群上。
MapReduce设计并提供了统一的计算框架,为程序员隐藏了绝大多数系统层面的处理细节。为程序员提供一个抽象和高层的编程接口和框架。程序员仅需要关心其应用层的具体计算问题,仅需编写少量的处理应用本身计算问题的程序代码。如何具体完成这个并行计算任务所相关的诸多系统层细节被隐藏起来,交给计算框架去处理:
Map和Reduce为程序员提供了一个清晰的操作接口抽象描述。MapReduce中定义了如下的Map和Reduce两个抽象的编程接口,由用户去编程实现Map和Reduce;MapReduce处理的数据类型
是<key,value>键值对。
- Map:
(k1; v1) -> [(k2; v2)]
- Reduce:
(k2; [v2]) -> [(k3; v3)]
一个完整的MapReduce程序在分布式运行时有三类实例进程:
- AppMaster 负责整个程序的过程调度及状态协调
- MapTask 负责map阶段的整个数据处理流程
- ReduceTask 负责reduce阶段的整个数据处理流程
Mapreduce特点
- 优点
易于编程
可扩展性
高容错性
高吞吐量 - 不适用领域
难以实时计算
不适合流式计算
MapReduce编程流程
Map 阶段 2 个步骤
- 设置 InputFormat 类,将数据切分为 Key-Value(K1和V1) 对,输入到第二步
- 自定义 Map 逻辑,将第一步的结果转换成另外的 Key-Value(K2和V2)对,输出结果
Shuffle 阶段 4 个步骤
- 对输出的 Key-Value 对进行分区
- 对不同分区的数据按照相同的 Key 排序
- (可选) 对分组过的数据初步规约,降低数据的网络拷贝
- 对数据进行分组,相同 Key 的 Value 放入一个集合中
Reduce 阶段 2 个步骤
- 对多个 Map 任务的结果进行排序以及合并, 编写 Reduce 函数实现自己的逻辑,对输入的Key-Value 进行处理,转为新的 Key-Value(K3和V3)输出
- 设置 OutputFormat 处理并保存 Reduce 输出的 Key-Value 数据
以WordCount为例,这个图将阐述整个MapReduce流程:
WordCount实例
需求:在一堆给定的文本文件中统计输出每一个单词出现的总次数
数据格式装备
创建一个新的文件
vi wordcount.txt
向其中放入一下内容并保存
hello,world,hadoop
hive,sqoop,flume,hello
kitty,tom,jerry,world
hadoop
上传到HDFS
hdfs dfs -mkdir /wordcount/
hdfs dfs put wordcount.txt /wordcount
Mapper
以下于JAVA中实现:
public class WCMapper extends Mapper<LongWritable, Text, Text, LongWritable>{
@Override
public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException{
String line = value.toString();
String[] split = line.split(",");
for (String word : split) {
context.write(new Text(word), new LongWritable(1));
}
}
}
Reducer
public class WCReducer extends Reducer<Text,LongWritable,Text,LongWritable> {
/**
* 自定义我们的reduce逻辑
* 所有的key都是我们的单词,所有的values都是我们单词出现的次数
*/
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
long count = 0;
for (LongWritable value : values) {
count += value.get();
}
context.write(key, new LongWritable(count));
}
}
定义主类,描述Job并提交
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
//注意以上导包千万不能导错
public class WCDriver {
public static void main(String[] args) throws Exception{
System.setProperty("hadoop.home.dir", "D:\\Hadoop2.6.0");
//建立连接
Configuration cfg = new Configuration();
Job job = Job.getInstance(cfg, "job_wc");
job.setJarByClass(WCDriver.class);
//指定mapper和reducer
job.setMapperClass(WCMapper.class);
job.setReducerClass(WCReducer.class);
//指定mapper输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//指定partitioner,这里不需要
//job.setNumReduceTasks(4);
//job.setPartitionerClass(WCPartitioner.class);
//指定reducer输出类型
job.setOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
//指定输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
//返回执行结果成功与否
boolean result=job.waitForCompletion(true);
System.out.println(result?"成功":"失败");
System.out.println(result?0:1);
}
}
MapReduce分区
在MapReduce中,通过我们指定分区,会将同一个分区的数据发送到同一个Reduce当中进行处理
例如:为了数据的统计,可以把一批类似的数据发送到同一个Reduce当中,在同一个Reduce当中统计相同类型的数据,就可以实现类似的数据分区和统计等
其实就是相同类型的数据,有共性的数据,送到一起处理
以上面的WordCount举例,我们想将单词分区处理,就需要自定义Partitioner
实例代码:
public class WCPartitioner extends Partitioner<Text, IntWritable> {
@Override
public int getPartition(Text text, IntWritable intWritable, int i) {
return Math.abs(text.hashCode()%i);
}
}
// i是自定义分区数,abs函数规避了哈希值为负的可能
在主类里,我们还要加上指定Partitioner
//指定partitioner
job.setNumReduceTasks(4); // 4代表想分成4个区
job.setPartitionerClass(WCPartitioner.class);
MapReduce排序和序列化
- 序列化 (Serialization) 是指把结构化对象转化为字节流
- 反序列化 (Deserialization) 是序列化的逆过程,把字节流转为结构化对象。当要在进程间传递对象或持久化对象的时候,就需要序列化对象成字节流,反之当要将接收到或从磁盘读取的字节流转换为对象,就要进行反序列化
- Java 的序列化 (Serializable) 是一个重量级序列化框架,一个对象被序列化后,会附带很多额外的信息 (各种校验信息,header,继承体系等),不便于在网络中高效传输。所以,Hadoop 自己开发了一套序列化机制(Writable),精简高效。不用像 Java 对象类一样传输多层的父子关系,需要哪个属性就传输哪个属性值,大大的减少网络传输的开销
- Writable 是 Hadoop 的序列化格式,Hadoop 定义了这样一个 Writable 接口。一个类要支持可序列化只需实现这个接口即可
- 另外 Writable 有一个子接口是WritableComparable,WritableComparable 是既可实现序列化,也可以对key进行比较,我们这里可以通过自定义 Key 实现 WritableComparable 来实现我们的排序功能
具体实现
a 1
a 9
b 3
a 7
b 8
b 10
a 5
要求:
- 第一列按照字典顺序进行排列
- 第一列相同的时候,第二列按照升序进行排列
解决思路:
- 将 Map 端输出的 <key,value> 中的 key 和 value 组合成一个新的 key (newKey),value值不变
- 这里就变成 <(key,value),value>,在针对 newKey 排序的时候,如果 key 相同,就再对value进行排序
Step1.自定义类型和比较器:
public class PairWritable implements WritableComparable<PairWritable> {
// 组合key,第一部分是我们第一列,第二部分是我们第二列
private String first;
private int second;
public PairWritable() {
}
public PairWritable(String first, int second) {
this.set(first, second);
}
/**
* 方便设置字段
*/
public void set(String first, int second) {
this.first = first;
this.second = second;
}
/**
* 反序列化
*/
@Override
public void readFields(DataInput input) throws IOException {
this.first = input.readUTF();
this.second = input.readInt();
}
/**
* 序列化
*/
@Override
public void write(DataOutput output) throws IOException {
output.writeUTF(first);
output.writeInt(second);
}
/*
* 重写比较器
*/
public int compareTo(PairWritable o) {
//每次比较都是调用该方法的对象与传递的参数进行比较,说白了就是第一行与第二行比较完了之后的结果与第三行比较,
//得出来的结果再去与第四行比较,依次类推
System.out.println(o.toString());
System.out.println(this.toString());
int comp = this.first.compareTo(o.first);
if (comp != 0) {
return comp;
} else { // 若第一个字段相等,则比较第二个字段
return Integer.valueOf(this.second).compareTo(
Integer.valueOf(o.getSecond()));
}
}
public int getSecond() {
return second;
}
public void setSecond(int second) {
this.second = second;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
@Override
public String toString() {
return "PairWritable{" +
"first='" + first + '\'' +
", second=" + second +
'}';
}
}
Step2.Mapper:
public class SortMapper extends Mapper<LongWritable,Text,PairWritable,IntWritable> {
private PairWritable mapOutKey = new PairWritable();
private IntWritable mapOutValue = new IntWritable();
@Override
public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String lineValue = value.toString();
String[] strs = lineValue.split("\t");
//设置组合key和value ==> <(key,value),value>
mapOutKey.set(strs[0], Integer.valueOf(strs[1]));
mapOutValue.set(Integer.valueOf(strs[1]));
context.write(mapOutKey, mapOutValue);
}
}
Step3.Reducer:
public class SortReducer extends Reducer<PairWritable,IntWritable,Text,IntWritable> {
private Text outPutKey = new Text();
@Override
public void reduce(PairWritable key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
//迭代输出
for(IntWritable value : values) {
outPutKey.set(key.getFirst());
context.write(outPutKey, value);
}
}
}
Step4.Main入口:
public class SecondarySort extends Configured implements Tool {
@Override
public int run(String[] args) throws Exception {
Configuration conf = super.getConf();
conf.set("mapreduce.framework.name","local");
Job job = Job.getInstance(conf, SecondarySort.class.getSimpleName());
job.setJarByClass(SecondarySort.class);
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job,new Path("file:///文件读取路径"));
TextOutputFormat.setOutputPath(job,new Path("file:///文件输出路径"));
job.setMapperClass(SortMapper.class);
job.setMapOutputKeyClass(PairWritable.class);
job.setMapOutputValueClass(IntWritable.class);
job.setReducerClass(SortReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
boolean b = job.waitForCompletion(true);
return b?0:1;
}
public static void main(String[] args) throws Exception {
Configuration entries = new Configuration();
ToolRunner.run(entries,new SecondarySort(),args);
}
}
MapReduce的运行机制详解(重点)
MapTask工作机制
整个Map阶段流程大体如上图所示。
简单概述:inputFile通过split被逻辑切分为多个split文件,通过Record按行读取内容给map(用户自己实现的)进行处理,数据被map处理结束之后交给OutputCollector收集器,对其结果key进行分区(默认使用hash分区),然后写入buwer,每个map task都有一个内存缓冲区,存储着map的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task结束后再对磁盘中这个map task产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task来拉数据
详细步骤:
- 读取数据组件 InputFormat (默认 TextInputFormat) 会通过 getSplits 方法对输入目录
中文件进行逻辑切片规划得到 block,有多少个 block 就对应启动多少个 MapTask - 将输入文件切分为 block 之后, 由 RecordReader 对象 (默认是LineRecordReader) 进 行读取,以 \n 作为分隔符,读取一行数据,返回 <key,value>。Key 表示每行首字符偏移值,Value 表示这一行文本内容
- 读取 block 返回 <key,value>,进入用户自己继承的 Mapper 类中,执行用户重写
的 map 函数,RecordReader 读取一行这里调用一次 - Mapper 逻辑结束之后,将 Mapper 的每条结果通过 context.write 进行collect数据收
集。在 collect 中,会先对其进行分区处理,默认使用 HashPartitioner
MapReduce 提供 Partitioner 接口,它的作用就是根据 Key 或 Value 及 Reducer 的数量来决定当前的这对输出数据最终应该交由哪个 Reduce task 处理,默认对 Key Hash 后再以 Reducer 数量取模。默认的取模方式只是为 了平均 Reducer 的处理能力,如果用户自己对 Partitioner 有需求, 可以订制并设置 到 Job 上
- 接下来,会将数据写入内存,内存中这片区域叫做环形缓冲区,缓冲区的作用是批量收集Mapper 结果,减少磁盘 IO 的影响。我们的 Key/Value 对以及 Partition 的结果都会被写入缓冲区。当然,写入之前,Key 与 Value 值都会被序列化成字节数组
环形缓冲区其实是一个数组,数组中存放着 Key,Value 的序列化数据和 Key,Value 的元数据信息,包括 Partition,Key 的起始位置,Value 的起始位置以及 Value 的长度。环形结构是一个抽象概念
缓冲区是有大小限制,默认是 100MB。当 Mapper 的输出结果很多时,就可能会撑爆内存,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用 这块缓冲区。这个从内存往磁盘写数据的过程被称为 Spill,中文可译为溢写。这个溢写是由单独线程来完成,不影响往缓冲区写 Mapper 结果的线程。溢写线程启动时不应该阻止Mapper的结果输出,所以整个缓冲区有个溢写的比例
spill.percent
,这个比例默认是 0.8,也就是当缓冲区的数据已经达到阈值 buffer size * spill percent = 100MB * 0.8 = 80MB,溢写线程启动,锁定这 80MB的内存,执行溢写过程。Mapper 的输出结果还可以往剩下的 20MB 内存中写,互不影响
- 当溢写线程启动后,需要对这 80MB 空间内的 Key 做排序 (Sort)。排序是 MapReduce 模型
默认的行为,这里的排序也是对序列化的字节做的排序
如果 Job 设置过 Combiner,那么现在就是使用 Combiner 的时候了。将有相同 Key 的 Key/Value 对的 Value 加起来,减少溢写到磁盘的数据量。Combiner 会优化 MapReduce 的中间结果,所以它在整个模型中会多次使用
那哪些场景才能使用 Combiner 呢? 从这里分析,Combiner 的输出是 Reducer 的输入,Combiner 绝不能改变最终的计算结果。Combiner 只应该用于那种 Reduce的输入 Key/Value 与输出 Key/Value 类型完全一致,且不影响最终结果的场景。比如累加,最大值等。Combiner 的使用一定得慎重,如果用好,它对 Job 执行效率有帮助,反之会影响 Reducer 的最终结果
- 合并溢写文件,每次溢写会在磁盘上生成一个临时文件 (写之前判断是否有 Combiner),如果 Mapper 的输出结果真的很大有多次这样的溢写发生,磁盘上相应的就会有多个临时文件存在。当整个数据处理结束之后开始对磁盘中的临时文件进行 Merge 合并,因为最终的文件只有一个, 写入磁盘, 并且为这个文件提供了一个索引文件,以记录每个reduce对应数据的偏移量
配置 | 默认值 | 解释 |
---|---|---|
mapreduce.task.io.sort.mb | 100 | 设置环形缓冲区的内存设置大小 |
mapreduce.map.sort.spill.percent | 0.8 | 设置溢出的比例 |
mapreduce.cluster.local.dir | ${hadoop.tmp.dir}/mapred/local | 溢写数据目录 |
mapreduce.task.io.sort.factor | 10 | 设置一次合并多少个溢写文件 |
ReduceTask工作机制
Reduce 大致分为 copy、sort、reduce 三个阶段,重点在前两个阶段。copy 阶段包含一个 eventFetcher 来获取已完成的 map 列表,由 Fetcher 线程去 copy 数据,在此过程中会启动两个 merge 线程,分别为 inMemoryMerger 和 onDiskMerger,分别将内存中的数据 merge 到磁盘和将磁盘中的数据进行 merge。待数据 copy 完成之后,copy 阶段就完成了,开始进行 sort阶段,sort 阶段主要是执行 finalMerge 操作,纯粹的 sort 阶段,完成之后就是 reduce 阶段,调用用户定义的 reduce 函数进行处理
详细步骤:
-
Copy阶段: 简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求maptask获取属于自己的文件。
-
Merge阶段: 这里的merge如map端的merge动作,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端的更为灵活。merge有三种形式:内存到内存;内存到磁盘;磁盘到磁盘。默认情况下第一种形式不启用。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的文件。
-
合并排序: 把分散的数据合并成一个大的数据后,还会再对合并后的数据排序。
-
对排序后的键值对调用reduce方法: 键相等的键值对调用一次reduce方法,每次调用会产生零个或者多个键值对,最后把这些输出的键值对写入到HDFS文件中。
Shuffle过程
map 阶段处理的数据如何传递给 reduce 阶段,是 MapReduce 框架中最关键的一个流程,这个流程就叫 shuffle:洗牌、发牌 ——(核心机制:数据分区,排序,分组,规约,合并等过程)
shuffle 是 Mapreduce 的核心,它分布在 Mapreduce 的 map 阶段和 reduce 阶段。一般把从 Map 产生输出开始到 Reduce 取得数据作为输入之前的过程称作 shuffle:
- Collect阶段: 将 MapTask 的结果输出到默认大小为 100M 的环形缓冲区,保存的是 key/value,Partition 分区信息等。
- Spill阶段: 当内存中的数据量达到一定的阀值的时候,就会将数据写入本地磁盘,在将数据写入磁盘之前需要对数据进行一次排序的操作,如果配置了 combiner,还会将有相同分区号和 key 的数据进行排序。
- Merge阶段: 把所有溢出的临时文件进行一次合并操作,以确保一个 MapTask 最终只产生一个中间数据文件。
- Copy阶段: ReduceTask 启动 Fetcher 线程到已经完成 MapTask 的节点上复制一份属于自己的数据,这些数据默认会保存在内存的缓冲区中,当内存的缓冲区达到一定的阀值的时候,就会将数据写到磁盘之上。
- Merge阶段: 在 ReduceTask 远程复制数据的同时,会在后台开启两个线程对内存到本地的数据文件进行合并操作。
- Sort阶段: 在对数据进行合并的同时,会进行排序操作,由于 MapTask 阶段已经对数据进行了局部的排序,ReduceTask 只需保证 Copy 的数据的最终整体有效性即可。Shuffle 中的缓冲区大小会影响到 mapreduce 程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快;缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb 默认100M
Reduce端实现Join
需求
假如数据量巨大,两表的数据是以文件的形式存储在 HDFS 中,需要用 MapReduce 程序来实现以下 SQL 查询运算:
select a.id,a.date,b.name,b.category_id,b.price from t_order a left join t_product b on a.pid = b.id
商品表:
id | pname | category_id | price |
---|---|---|---|
P0001 | 小米5 | 1000 | 2000 |
P0002 | 锤子T1 | 1000 | 3000 |
订单数据表:
id | date | pid | amout |
---|---|---|---|
1001 | 20150710 | P0001 | 2 |
1002 | 20150710 | P0002 | 3 |
实现步骤
定义Mapper:
public class ReduceJoinMapper extends Mapper<LongWritable,Text,Text,Text> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1:判断数据来自哪个文件
FileSplit fileSplit = (FileSplit) context.getInputSplit();
String fileName = fileSplit.getPath().getName();
if(fileName.equals("product.txt")){
//数据来自商品表
//2:将K1和V1转为K2和V2,写入上下文中
String[] split = value.toString().split(",");
String productId = split[0];
context.write(new Text(productId), value);
}else{
//数据来自订单表
//2:将K1和V1转为K2和V2,写入上下文中
String[] split = value.toString().split(",");
String productId = split[2];
context.write(new Text(productId), value);
}
}
}
定义Reducer:
public class ReduceJoinReducer extends Reducer<Text,Text,Text,Text> {
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
//1:遍历集合,获取V3 (first +second)
String first = "";
String second = "";
for (Text value : values) {
if(value.toString().startsWith("p")){
first = value.toString();
}else{
second += value.toString();
}
}
//2:将K3和V3写入上下文中
context.write(key, new Text(first+"\t"+second));
}
}
Map端实现join
适用于关联表中有小表的情形;使用分布式缓存,可以将小表分发到所有的map节点,这样,map节点就可以在本地对自己所读到的大表数据进行join并输出最终结果,可以大大提高join操作的并发度,加快处理速度
实现步骤
先在mapper类中预先定义好小表,进行join
引入实际场景中的解决方案:一次加载数据库或者用
Step1.定义Mapper:
public class MapJoinMapper extends Mapper<LongWritable,Text,Text,Text>{
private HashMap<String, String> map = new HashMap<>();
//第一件事情:将分布式缓存的小表数据读取到本地Map集合(只需要做一次)
@Override
protected void setup(Context context) throws IOException, InterruptedException {
//1:获取分布式缓存文件列表
URI[] cacheFiles = context.getCacheFiles();
//2:获取指定的分布式缓存文件的文件系统(FileSystem)
FileSystem fileSystem = FileSystem.get(cacheFiles[0], context.getConfiguration());
//3:获取文件的输入流
FSDataInputStream inputStream = fileSystem.open(new Path(cacheFiles[0]));
//4:读取文件内容, 并将数据存入Map集合
//4.1 将字节输入流转为字符缓冲流FSDataInputStream --->BufferedReader
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
//4.2 读取小表文件内容,以行位单位,并将读取的数据存入map集合
String line = null;
while((line = bufferedReader.readLine()) != null){
String[] split = line.split(",");
map.put(split[0], line);
}
//5:关闭流
bufferedReader.close();
fileSystem.close();
}
//第二件事情:对大表的处理业务逻辑,而且要实现大表和小表的join操作
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//1:从行文本数据中获取商品的id: p0001 , p0002 得到了K2
String[] split = value.toString().split(",");
String productId = split[2]; //K2
//2:在Map集合中,将商品的id作为键,获取值(商品的行文本数据) ,将value和值拼接,得到V2
String productLine = map.get(productId);
String valueLine = productLine+"\t"+value.toString(); //V2
//3:将K2和V2写入上下文中
context.write(new Text(productId), new Text(valueLine));
}
}
Step2.定义主类:
public class JobMain extends Configured implements Tool{
@Override
public int run(String[] args) throws Exception {
//1:获取job对象
Job job = Job.getInstance(super.getConf(), "map_join_job");
//2:设置job对象(将小表放在分布式缓存中)
//将小表放在分布式缓存中
// DistributedCache.addCacheFile(new URI("hdfs://node01:8020/cache_file/product.txt"), super.getConf());
job.addCacheFile(new URI("hdfs://node01:8020/cache_file/product.txt"));
//第一步:设置输入类和输入的路径
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job, new Path("file:///D:\\input\\map_join_input"));
//第二步:设置Mapper类和数据类型
job.setMapperClass(MapJoinMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
//第八步:设置输出类和输出路径
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job, new Path("file:///D:\\out\\map_join_out"));
//3:等待任务结束
boolean bl = job.waitForCompletion(true);
return bl ? 0 :1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
//启动job任务
int run = ToolRunner.run(configuration, new JobMain(), args);
System.exit(run);
}
}