MapReduce是Hadoop框架的核心之一,它主要负责数据的分布式计算问题。MapReduce的核心思想是“分而治之”。“分”,即把一个复杂的任务分解成若干个简单任务来处理,但前提是这些任务是可以并行计算的。“合”,即对map阶段的结果进行全局汇总。
MapReduce将整个并行计算过程划分为三个阶段:
MapReduce的开发一共分为八个步骤。其中,Map阶段分为2个步骤,Shuffle阶段分为4个步骤,Reduce阶段也分为2个步骤。
- Map阶段:
1)设置InputFormat类,将数据切分为Key-Value(K1和V1);
2)自定义Map逻辑,将第一个的结果转成K1和V2;
- Shuffle阶段:
1)对K2和V2进行分区;
2)对不同分区的数据按照相同的key进行排序;
3)对分组过的数据进行初步筛选,降低数据的网络拷贝;
4)对数据分组,相同Key的Value放入到Map集合中;
- Reduce阶段:
1)对多个Map任务的结果进行排序和合并,编写Reduce函数实现自己的业务逻辑,将输入的K2和V2转成K3和V3;
2)设置OutputFormat,输出K3和V3;
需求:在给定文本文件中统计并输出每一个单词出现的次数。
实现步骤:
cd /export/servers
# 创建文件
vim wordcount.txt
# 创建HDFS目录
hdfs dfs ‐mkdir /wordcount/
# 上传文件
hdfs dfs ‐put wordcount.txt /wordcount/
文件内容:
hello,world,hadoop
hive,sqoop,flume,hello
kitty,tom,jerry,world
hadoop
public class WordCountMapper 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));
}
}
}
public class WordCountReducer 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));
}
}
public class JobMain extends Configured implements Tool {
@Override
public int run(String[] args) throws Exception {
Job job = Job.getInstance(super.getConf(),
JobMain.class.getSimpleName());
//打包到集群上面运行时候,必须要添加以下配置,指定程序的main函数
job.setJarByClass(JobMain.class);
//第一步:读取输入文件解析成key,value对
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job,new
Path("hdfs://node01:8020/wordcount"));
//第二步:设置我们的mapper类
job.setMapperClass(WordCountMapper.class);
//设置我们map阶段完成之后的输出类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
//第三步:设置我们的reduce类
job.setReducerClass(WordCountReducer.class);
//设置我们reduce阶段完成之后的输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
//第四步:设置输出类以及输出路径
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job,new
Path("hdfs://node01:8020/wordcount_out"));
boolean b = job.waitForCompletion(true);
return b ? 0 : 1;
}
/**
* 程序main函数的入口类
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
Tool tool = new JobMain();
int run = ToolRunner.run(configuration, tool, args);
System.exit(run);
}
}
可以在任意节点上执行如下命令:
# 命令格式: hadoop jar jar文件路径 主类完整路径
当把jar文件提交给Yarn集群后,Yarn集群会将该文件分发到其他节点上并发执行。执行完成后,最终计算结果会保存在HDFS上。
在实际应用中,我们可以把一批类似的数据放在同一个分区里面。在执行统计的时候,通过我们指定的分区,可以把同一个分区的数据发送给同一个Reduce进行处理。
Reduce默认只有一个分区。如果要定义多个分区,可以按照以下步骤来实现:
/**
* 这里的输入类型与我们map阶段的输出类型相同
*/
public class MyPartitioner extends Partitioner<Text, LongWritable>{
/**
* text代表K2,longWritable代表V2,i代表reducetask个数
* 返回值只是一个分区的编号,用来标记所有相同的数据去到指定的分区
*/
@Override
public int getPartition(Text text, LongWritable longWritable , int i) {
// 如果单词长度大于等于5,则进入第一个分区
// 如果单词长度小于5,则进入第二个分区
if (text.toString().length() >= 5){
return 0;
}else{
return 1;
}
}
}
// 设置分区类
job.setPartitionerClass(MyPartitioner.class);
// 设置reducetask个数,与分区个数相同
job.setNumReduceTasks(2);
Hadoop提供了Writable接口,用于实现序列化。另外,Writable接口有一个子接口WritableComparable,它既可以实现序列化,也可以通过自定义Key实现p排序功能。
假设需要对下面数据进行排序:
a 1
a 9
b 3
a 7
b 8
b 10
a 5
要求先对第一列按照字典进行排列,如果第一列相同时,再对第二列数字的升序排列。
解决思路:
具体实现步骤:
public class PairWritable implements WritableComparable<PairWritable> {
private String first; // 第一例数据
private Integer 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 write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(first);
dataOutput.writeInt(second);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
this.first = dataInput.readUTF();
this.second = dataInput.readInt();
}
@Override
public int compareTo(PairWritable other) {
int res = this.first.compareTo(other.first);
if (res != 0) {
return res;
} else {
return this.second.compareTo(other.second);
}
}
}
public class SortMapper extends
Mapper<LongWritable,Text,PairWritable,IntWritable> {
private PairWritable mapOutKey = new PairWritable(); // K2
private IntWritable mapOutValue = new IntWritable(); // V2
@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);
}
}
public class SortReducer extends
Reducer<PairWritable,IntWritable,Text,NullWritable> {
private Text outPutKey = new Text(); // K3
@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, null);
}
}
}
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("hdfs://node01:8020/sort"));
TextOutputFormat.setOutputPath(job,new Path("hdfs://node01:8020/sort_out"));
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计数器是用于获取计算过程中的数据。计数器是收集作业统计信息的有效手段之一,用于质量控制或应用级统计。计数器还可辅助诊断系统故障。在实际应用中,可以使用计数器值来记录某一特定事件的发生,然后根据计数器值统计特定事件的发生次数。
hadoop内置的计数器:
计数器名称 | 计数器类型 |
---|---|
任务计数器 | org.apache.hadoop.mapreduce.TaskCounter |
文件系统计数器 | org.apache.hadoop.mapreduce.FileSystemCounter |
FileInputFormat计数器 | org.apache.hadoop.mapreduce.lib.input.FileInputFormatCounter |
FileOutputFormat计数器 | org.apache.hadoop.mapreduce.lib.output.FileOutputFormatCounter |
作业计数器 | org.apache.hadoop.mapreduce.JobCounter |
通过context上下文对象获取计数器;
// 自定义计数器,第一个参数代表计数器的分类名称,第二个参数代表计数器的名称,它们都可以任意起名
Counter counter = context.getCounter("MR_COUNT", "MapRecordCounter");
// 计数器的值加1
counter.increment(1L);
如果有多个计数器的话,可以使用一个枚举类来管理所有的计数器。
public static enum Counter {
REDUCE_INPUT_RECORDS,
REDUCE_INPUT_VAL_NUMS,
}
context.getCounter(Counter.REDUCE_INPUT_RECORDS).increment(1L);
例如:统计reduce阶段输入的key的数量,以及对应的value的数量。
public class SortReducer extends Reducer<PairWritable, IntWritable, Text, IntWritable> {
private Text outPutKey = new Text();
public static enum Counter {
REDUCE_INPUT_RECORDS,
REDUCE_INPUT_VAL_NUMS,
}
@Override
protected void reduce(PairWritable key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
context.getCounter(Counter.REDUCE_INPUT_RECORDS).increment(1L);
for(IntWritable value : values) {
outPutKey.set(key.getFirst());
context.write(outPutKey, value);
}
}
}
每一个 map 都可能会产生大量的本地输出,规约的作用就是对map阶段的输出先做一次合并,以减少在map和reduce之间的数据传输量,以提高网络IO 性能。Combiner组件的父类是Reducer,因此Combiner实际上也是Reduce。但是不同的是,Combiner是在每一个运行maptask任务的节点上运行,而Reducer是用于接收所有Mapper的输出结果。
job.setCombinerClass(CustomCombiner.class)
值的注意是,Combiner使用的前提是不能够影响的最终的业务逻辑。而且Combiner输出的kv应该跟reducer输入的kv类型要相匹配。
如果数据是以多文件形式存放在HDFS中,例如:
id | date | pid | amount |
---|---|---|---|
1001 | 20150710 | P0001 | 2 |
1002 | 20150710 | P0001 | 3 |
1002 | 20150710 | P0002 | 3 |
id | pname | category_id | price |
---|---|---|---|
P0001 | 小米10 | 1000 | 3000 |
P0002 | 华为meta | 1000 | 3500 |
如果需要同时查询订单和商品的数据,我们的实现思路:
实现步骤:
public class ReduceJoinMapper extends Mapper<LongWritable, Text,Text,Text> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//首先判断数据来自哪个文件
FileSplit fileSplit = (FileSplit) context.getInputSplit();
String fileName = fileSplit.getPath().getName();
if(fileName.equals("orders.txt")){
//获取pid
String[] split = value.toString().split(",");
context.write(new Text(split[2]), value);
}else{
//获取pid
String[] split = value.toString().split(",");
context.write(new Text(split[0]), value);
}
}
}
public class ReduceJoinReducer extends Reducer<Text,Text,Text,Text> {
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
String first = "";
String second = "";
for (Text value : values) {
if(value.toString().startsWith("p")){
first = value.toString();
}else{
second = value.toString();
}
}
if(first.equals("")){
context.write(key, new Text("NULL"+"\t"+second));
}else{
context.write(key, new Text(first+"\t"+second));
}
}
}
public class JobMain extends Configured implements Tool {
@Override
public int run(String[] strings) throws Exception {
//创建一个任务对象
Job job = Job.getInstance(super.getConf(),"mapreduce_reduce_join");
//打包放在集群运行时,需要做一个配置
job.setJarByClass(JobMain.class);
//第一步:设置读取文件的类: K1 和V1
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job, new Path("hdfs://node01:8020/reduce_join"));
//第二步:设置Mapper类
job.setMapperClass(ReduceJoinMapper.class);
//设置Map阶段的输出类型: k2 和V2的类型
//k2为商品的ID,v2为商品或订单数据
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
//第三,四,五,六步采用默认方式(分区,排序,规约,分组)
//第七步 :设置文的Reducer类
job.setReducerClass(ReduceJoinReducer.class);
//设置Reduce阶段的输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
//第八步:设置输出类
job.setOutputFormatClass(TextOutputFormat.class);
//设置输出的路径
TextOutputFormat.setOutputPath(job, new
Path("hdfs://node01:8020/reduce_join_out"));
boolean b = job.waitForCompletion(true);
return b ? 0 : 1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
int run = ToolRunner.run(configuration, new JobMain(), args);
System.exit(run);
}
}