大数据学习笔记(三)

一、MapReduce

1.1 MapReduce介绍

MapReduce是Hadoop框架的核心之一,它主要负责数据的分布式计算问题。MapReduce的核心思想是“分而治之”。“分”,即把一个复杂的任务分解成若干个简单任务来处理,但前提是这些任务是可以并行计算的。“合”,即对map阶段的结果进行全局汇总。
大数据学习笔记(三)_第1张图片
MapReduce将整个并行计算过程划分为三个阶段:

  • Map:对一些独立元素组成的列表中的每个元素并行地执行指定操作;
  • Shuffle:对Map阶段得到的数据进行清洗操作;
  • Reduce:对Shuffle后的数据进行合并,得到最终结果;

1.2 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;

1.3 入门示例

需求:在给定文本文件中统计并输出每一个单词出现的次数。

实现步骤:

  • 第一步:将文本文件上传到HDFS上;
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
  • 第二步:定义一个Mapper的子类,并重写map方法;
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));
        }
    }
}
  • 第三步:定义一个Reducer的子类,并重写reduce方法;
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));
    }
}
  • 第四步:定义一个主类,负责定义和启动job任务;
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);
    }
}
  • 第五步:将工程打成jar包,然后提交到Yarn集群上运行;

可以在任意节点上执行如下命令:

# 命令格式: hadoop jar jar文件路径 主类完整路径 

当把jar文件提交给Yarn集群后,Yarn集群会将该文件分发到其他节点上并发执行。执行完成后,最终计算结果会保存在HDFS上。

1.4 自定义分区

在实际应用中,我们可以把一批类似的数据放在同一个分区里面。在执行统计的时候,通过我们指定的分区,可以把同一个分区的数据发送给同一个Reduce进行处理。

Reduce默认只有一个分区。如果要定义多个分区,可以按照以下步骤来实现:

  • 第一步:定义一个分区类,继承Partitioner,并重写getPartition方法;
/**
 * 这里的输入类型与我们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类中设置分区类,以及指定reducetask的个数(必须要与分区数保持一致);
// 设置分区类
job.setPartitionerClass(MyPartitioner.class);
// 设置reducetask个数,与分区个数相同
job.setNumReduceTasks(2);

运行效果:
大数据学习笔记(三)_第2张图片

1.5 序列化和排序

Hadoop提供了Writable接口,用于实现序列化。另外,Writable接口有一个子接口WritableComparable,它既可以实现序列化,也可以通过自定义Key实现p排序功能。

假设需要对下面数据进行排序:

a	1
a	9
b	3
a	7
b	8
b	10
a	5

要求先对第一列按照字典进行排列,如果第一列相同时,再对第二列数字的升序排列。

解决思路:

  • 因为需要同时对key和value进行排序,所以在map阶段将输入的key和value组成一个新的key作为k2,value作为v2;
  • 对新的k2进行排序,如果k2中的key相同,再对k2中的value进行排序;

具体实现步骤:

  • 第一步:自定义类型PairWritable,该类型继承了WritableComparable。该类用于封装了K2和V2。如果要实现排序,需要重写compareTo方法;
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);
        }
    }
}
  • 第二步:实现Mapper;
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);
    }
}
  • 第三步:实现Reducer;
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);
    }
}

1.6 计数器

1.6.1 什么是计数器

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

1.6.2 自定义计数器

通过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);
        }
    }
}

1.7 Combiner规约

1.7.1 什么是规约

每一个 map 都可能会产生大量的本地输出,规约的作用就是对map阶段的输出先做一次合并,以减少在map和reduce之间的数据传输量,以提高网络IO 性能。Combiner组件的父类是Reducer,因此Combiner实际上也是Reduce。但是不同的是,Combiner是在每一个运行maptask任务的节点上运行,而Reducer是用于接收所有Mapper的输出结果。

1.7.2 实现步骤

  • 第一步:自定义Combiner类,继承Reducer,重写reduce方法;
  • 第二步:在job中设置Combiner;
job.setCombinerClass(CustomCombiner.class)

值的注意是,Combiner使用的前提是不能够影响的最终的业务逻辑。而且Combiner输出的kv应该跟reducer输入的kv类型要相匹配。

1.8 多文件的Join操作

如果数据是以多文件形式存放在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

如果需要同时查询订单和商品的数据,我们的实现思路:

  • 在map阶段判断数据是来自哪一个文件,不同文件的商品ID的位置不一样;
  • 将商品ID作为map阶段输出的Key,文件每一个行的内容作为输出的Value;
  • 在reduce阶段遍历Value,然后将订单和商品数据合并一起作为Value输出,Key保持不变;

实现步骤:

  • map实现:
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);
        }
    }
}
  • reduce实现:
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);
    }
}

你可能感兴趣的:(大数据)