MapRedecu代码
- pom
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<!--版本号根据自己使用的hadoop自己去maven仓库找对应的坐标,下面一样-->
<version>2.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>2.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>2.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>2.5.2</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-yarn-common</artifactId>
<version>2.5.2</version>
</dependency>
- 代码结构 (骨架)
/*1. Map
k1:map阶段输入的key类型,实际是行偏移量
v1:map阶段输入的value类型,实际是行内容
k2:map输出的key类型
v2:map输出的value类型
*/
// k1 v1 k2 v2
public class MyMaper extends Mapper<LongWritable, Text, Text, IntWritable> {
@Override
protected void map(LongWritable k1, Text v1, Context context) throws IOException, InterruptedException {
//todo
}
}
/*2. Reduce
k1:reduce阶段的key输入类型,实际是map输出的key类型
v1:reduce阶段的value输入类型,实际是map输出的value类型
k2:reduce的输出的key类型
v2:reduce的输出的value类型
*/
// k1 v1 k2 v2
public class MyReduce extends Reducer<Text, IntWritable, Text, IntWritable> {
@Override
protected void reduce(Text k2, Iterable<IntWritable> v2s, Context context) throws IOException, InterruptedException {
//todo
}
}
/*3. Job*/
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "MyFirstJob"); //设置job名称
//作业以jar包形式 运行
job.setJarByClass(MyMapReduce.class);
//文件输入路径
InputFormat Path path = new Path("/src/data");
TextInputFormat.addInputPath(job,path);
//设置map
Map job.setMapperClass(MyMaper.class);
//Map端输出key类型,必须和第一步的map输出key保持一致
job.setMapOutputKeyClass(Text.class);
//Map端输出value类型,必须和第一步的map输出value保持一致
job.setMapOutputValueClass(IntWritable.class);
//shuffle 默认的方式处理 无需设置
//设置reduce
reduce job.setReducerClass(MyReduce.class);
//reduce端输出key类型,和第二步reduce输出key保持一致
job.setOutputKeyClass(Text.class);
//reduce端输出value类型,和第二步reduce输出value保持一致
job.setOutputValueClass(IntWritable.class);
//输出目录一定不能存在,由MR动态创建
Path out = new Path("/dest2");
FileSystem fileSystem = FileSystem.get(conf);
fileSystem.delete(out,true);
TextOutputFormat.setOutputPath(job,out);
// 运行job作业
job.waitForCompletion(true);
- MR运行
必须运行在yarn集群上,但由于每次运行MR代码,都需要先打成jar包,然后上传到集群,在使用bin/yarn jar xxx.jar运行jar包,非常繁琐,所以为了简化操作,要借助一些插件。
//创建变量
<myJobMainClass>job类所在的路径</myJobMainClass>
//linux的ip
<target-host>192.168.0.0</target-host>
//linux上hadoop的安装路径
<target-postition>/opt/install/hadoop-2.5.2</target-postition>
<!--借用wagon插件将jar包上传到linux并执行yarn命令执行jar包-->
<extensions>
<extension>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-ssh</artifactId>
<version>2.8</version>
</extension>
</extensions>
<plugin>
<!--maven插件-->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<!--jar包输出路径,basedir为maven的系统变量,默认输出到项目的根下-->
<outputDirectory>${basedir}</outputDirectory>
<archive>
<manifest>
<!--mainClass-->
<mainClass>${myJobMainClass}</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>wagon-maven-plugin</artifactId>
<version>1.0</version>
<configuration>
<!--jar包名字,系统字段-->
<fromFile>${project.build.finalName}.jar</fromFile>
<!--linux的账号密码及ip、上传路径-->
<url>scp://root:123456@${target-host}${target-position}</url>
<commands>
<!--先杀死原先此jar包的进程-->
<command>pkill -f ${project.build.finalName}.jar</command>
<!--使用hadoop命令运行上传的jar包 nohup不在控制台输出日志,指定输出到/root/nohup.out中-->
<command>nohup ${target-postition}/bin/yarn jar ${target-position}/${project.build.finalName}.jar > /root/nohup.out 2>&1 &</command>
</commands>
<!-- 显示运行命令的输出结果 -->
<displayCommandOutputs>true</displayCommandOutputs>
</configuration>
</plugin>
在idea中自定义maven命:鼠标右键-->new goal
jar:jar wagon:upload wagon:sshexec
在集群上运行wordCount案例步骤
1.写好代码打jar包,将jar包上传到集群
2.在hadoop下使用命令运行jar包
hadoop jar jar包名
或者yarn jar jar包名
访问8088(yarn页面)就可以看到提交的任务页面
如果 MR 造成系统宕机。此时要控制 Yarn 同时运行的任务数,和每个任务申请的最大内存。 调整参数:yarn.scheduler.maximum-allocation-mb(单个任务可申请的最多物理内存量,默认是 8192MB)
MapReduce作业中Map,Reduce的一些细节问题
- MapReduce中是可以没有Reduce
如果MapReduce中,只是对数据进行清洗,而不负责统计、去重的话,就没有Reduce
job.setNumReduceTasks(0); //设置reduce的个数为0,将之前关于reduce的设置注释掉
// job.setReducerClass(MyReducer.class);
// job.setOutputKeyClass(Text.class);
// job.setOutputValueClass(NullWritable.class)
- MapReduce中有多少个Map?
文本文件
处理中,Map的数量由block决定(在不改变切片大小的情况下默认1:1;其他可见下面的切片介绍
)
- MapReduce中有多少个Reduce
3.1:Reduce个数可以设置的;默认情况reduce的个数是 1
mapreduce.job.reduces 1 //配置文件中设置
job.setNumReduceTasks(?) //代码中设置3.2:为什么要设置多个Reduce?
a.提高MR的运行效率
b.多个Reduce的输出结果是多个文件,可以再次进行Map的处理,进行汇总
c.Partion分区 由分区决定Map输出结果,交给那个Reduce处理。默认有HashPartitioner实现 k2.hashCode()%reduceNum(2) = 0,1
- 程序员自定义分区算法
4.1: 自定义Partitioner
public class MyPartitioner<k2, v2> extends Partitioner<k2, v2> {
@Override
public int getPartition(k2 key, v2 value, int numPartitions) {
String k = key.toString();
try {
//key值小于3的分到一个reduce中,否则分到另一个reduce中,这里只是测试,实际情况根据自己需求设置逻辑
int key_i = Integer.parseInt(k);
if (key_i <= 3) {
return 0;
} else {
return 1;
}
} catch (Exception e) {
return -1;
}
}
}
4.2:job作业设置
job.setPartitionerClass(MyPartitioner.class);
(1)如果
ReduceTask的数量> getPartition
的结果数,则会多产生几个空的输出文件part-r-000xx;
(2)如果1<ReduceTask的数量<getPartition
的结果数,则有一部分分区数据无处安放,会Exception;
(3)如果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask,
最终也就只会产生一个结果文件 part-r-00000;
(4)分区号必须从零开始,逐一累加。
例如:假设自定义分区数为5,则
job.setNumReduceTasks(1); 会正常运行,只不过会产生一个输出文件
job.setNumReduceTasks(2); 会报错
job.setNumReduceTasks(6); 大于5,程序会正常运行,会产生空文件
- MapReduce中的技术器 Counter
在map中或者reduce中计数(实际没什么作用)
context.getCounter(); //此方法是重载方法,1.接收String,String 2.枚举
1.context.getCounter(“group-name”,“counter-name”).increment(1);
2.public enum MyCounter {
MY_COUNTER //定义一个枚举
}
context.getCounter(MyCounter.MY_COUNTER).increment(1);
- Combiner
自定义 Combiner 实现步骤:自定义一个 Combiner 继承 Reducer,重写 Reduce 方法
(就是一个reduce,如果和已写的reduce逻辑一样,也可以直接使用reduce)
Combiner能够应用的前提是不能影响最终的业务逻辑
Combiner是Map端的Reduce,提前Map端作合并,从而减少传输的数据,提高效率。
默认情况是Combiner关闭
job.setCombinerClass(MyReducer.class); //设置Combiner
但如果job.setNumReduceTasks(0),即没有reduce,那么即使设置 Combiner也没什么意义,因为数据直接从map输出了,就不会走shuffle阶段
- MapReduce自定义数据类型(序列化和排序)
MapReduce中,Map与Reduce会进行跨JVM,跨服务器的通信,
所以需要MapReduce中的数据类型进行序列化
Writable接口 Compareable接口
WritableComparable (既能排序又能序列化
)
自定义的数据类型 实现WritableComparable 接口即可,然后重写write、readFields方法,实现序列化
重写compareTo 方法,实现排序
write(DataOutput out)
readFields(DataInput in)
compareto()
equals
hashcode
toString
- 自定义OutputFormat
自定义OutputFormat步骤
- 自定义一个类继承FileOutputFormat
- 改写RecordWriter,具体改写输出数据的方法write()
//自定义一个 LogOutputFormat 类
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
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;
}
}
//编写 LogRecordWriter 类
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream baiduOut;
private FSDataOutputStream otherOut;
public LogRecordWriter(TaskAttemptContext job) {
try {
//获取文件系统对象
FileSystem fs = FileSystem.get(job.getConfiguration());
//用文件系统对象创建两个输出流对应不同的目录文件
baiduOut= 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("baidu")) {
baiduOut.writeBytes(log + "\n");
} else {
otherOut.writeBytes(log + "\n");
}
}
@Override
public void close(TaskAttemptContext context) throws IOException,
InterruptedException {
//关流
IOUtils.closeStream(baiduOut);
IOUtils.closeStream(otherOut);
}
}
//job作业设置:设置自定义的 outputformat
job.setOutputFormatClass(LogOutputFormat.class);
// 虽然我们自定义了outputformat,但是因为我们的outputformat继承自fileoutputformat
//而fileoutputformat要输出一个_SUCCESS文件,所以在这还得指定一个输出目录z专门输出_SUCCESS
FileOutputFormat.setOutputPath(job, new Path("D:\\logoutput"));
mapReduce中的数据类型:
IntWritable----对应java中的int
LongWritable----对应java中的long
FloatWritable----对应java中的float
Text----对应java中的string
NullWritable 空值
其他的就不一一列举了,一般都是java类型后缀Writable
Job作业的原理分析
- InputFormat
从HDFS读入数据,并把读入的数据封装Key Value (k1,v1)
// getSplits负责从HDFS(DataNode)中 读入文件的数据
// 把块的数据 封装 split中 保证2者 一一对应实际存的是路径,坐标等等,等生成k、v时才会去读取数据,而不是一开始就把数据读到内存中
public abstract List getSplits(JobContext context ) throws IOException, InterruptedException;
// RecordReader 负责把数据封装成 K,V
public abstract RecordReader<K,V> createRecordReader(InputSplit split, TaskAttemptContext context ) throws IOException, InterruptedException;
- mapReduce的shuffle过程
shuffle细划的话,分为map端shuffle、reduce端shuffle
- Map shuffle
- map端输出的key,value写入环形缓冲区(并且在这个阶段会给每个<k,v>对映射Partition编号,partition编号也会写入环形缓冲区)。
- 内存溢写到文件:环形缓冲区数据量达到阈值后就将数据写入磁盘,每次溢写都会产生一个文件(注意溢写前会
先按照Key进行一次快速排序
,保证数据以分区为单位聚集在一起、且区内有序
;如果设置了combiner,则在写文件之前会先进行聚合)- 溢写文件合并:合并之前生成的诸多临时文件(合并分区文件数据并再次排序)
map shuffle总结成自己话:当map以<key,value>对输出之后,会写入到内存中(环形缓冲区),
环形缓冲区默认大小是100MB,当数据达到缓冲区总容量的80%(阈值)时,会将内存的数据spill到本地磁盘。
数据在环形缓冲区中spill到本地磁盘之前会做分区,排序,之后再spill到本地磁盘。
因为map对数据不断的进行处理,数据会不断spill到本地磁盘,也就在磁盘上生成很多小文件。
由于最终reduce只会拿map输出的一个结果,所以会将spill到磁盘的数据进行一次合并,
将各个分区的数据合并在一起,分区合并之后再次排序。最后会形成一个文件,待reduce任务获取
- Reduce shuffle
当map阶段数据处理完成之后,各个reduce 任务主动到已经完成的map 任务的本次磁盘中,去拉取属于自己要处理的数据,最后会形成一个文件
1)reduce对应partition的数据写入内存,根据reduce端数据内存的阈值,spill到reduce本地磁盘,形成一个个小文件
2)对文件进行合并
3)排序
4)分组(将相同key的value放在一起)
reduce shuffle总结成自己话:当map阶段数据处理完成之后,各个reduce任务主动到已经完成的map任务的
本次磁盘中,去拉取属于自己要处理的数据,最后会形成一个文件,reduce 对应partition的数据写入内存,
根据reduce端数据内存的阈值,spill到reduce本地磁盘,形成一个个小文件,然后对一个个小文件进行合并,
排序,分组(将相同key的value放在一起),最后形成reduce输入端,传递给reduce()函数,得出结果。
Job 提交流程源码详解
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(),
FileInputFormat 切片源码解析(input.getSplits(job))
FileInputFormat 切片机制
CombineTextInputFormat
CombineTextInputFormat 用于小文件过多的场景
,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个 MapTask 处理。
// 如果不设置 InputFormat,它默认用的是 TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
//虚拟存储切片最大值设置 4m
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);