Hadoop-MapReduce

dr.who是通过http连接的默认用户,可以直接在配置文件里面修改为当前用户,重启之后就可以使用当前用户在页面里面对文件进行相关操作。

MapReduce概述

分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。

MapReduce核心功能是将用户编写的业务逻辑代码自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。

优点

MapReduce ****易于编程它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的PC机器上运行。

好的扩展性当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。

高容错性MapReduce设计的初衷就是使程序能够部署在廉价的PC机器上,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行,不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由Hadoop内部完成的。

适合PB级以上海量数据的离线处理可以实现上千台服务器集群并发工作,提供数据处理能力。

缺点

不擅长实时计算MapReduce无法像MySQL一样,在毫秒或者秒级内返回结果。

不擅长流式计算流式计算的输入数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化。这是因为MapReduce自身的设计特点决定了数据源必须是静态的。

不擅长DAG(有向图)计算多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下,MapReduce并不是不能做,而是使用后,每个MapReduce作业的输出结果都会写入到磁盘,会造成大量的磁盘IO,导致性能非常的低下。

MapReduce核心****思想

1603382857992-b099b6b9-6556-4bb1-a1cc-e8da3ba31d61.png

map阶段MapTask并发实例,完全并行运行,互不相干。

reduce阶段ReduceTask并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出。

MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行。


一个完整的MapReduce程序在分布式运行时有三类实例进程:

(1)MrAppMaster:负责整个程序的过程调度及状态协调。

(2)MapTask:负责Map阶段的整个数据处理流程。

(3)ReduceTask:负责Reduce阶段的整个数据处理流程。

直接使用官方的 Wordcount

[deltaqin@hadoop101 mapreduce]$ pwd                                                                                                                        
/opt/module/hadoop-3.1.3/share/hadoop/mapreduce
[deltaqin@hadoop101 mapreduce]$ yarn jar hadoop-mapreduce-examples-3.1.3.jar wordcount /test.txt /output

1605086621095-bb0a0bd5-3782-4721-823e-934d836d3651.png

常用数据****序列化类型

hadoop包装好的类型,想用必须使用包装好的类型


Java类型 Hadoop Writable类型
Boolean BooleanWritable
Byte ByteWritable
Int IntWritable
Float FloatWritable
Long LongWritable
Double DoubleWritable
String Text
Map MapWritable
Array ArrayWritable

自己实现 WordC****ount

有Map类、Reduce类和驱动类。数据的类型是Hadoop自身封装的序列化类型

在给定的文本文件中统计输出每一个单词出现的总次数

按照MapReduce编程规范,分别编写Mapper,Reducer,Driver。

编写Mapper类

package com.deltqin;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;

// 框架拿到内容变成kv给当前程序,输入类型由框架决定
// 将数据一行行,给该程序来处理,如何拿到数据不用管,只需要管业务(框架下编程)

//LongWritable, Text 输入类型:行号,内容
//Text, IntWritable 输出类型:内容

// LongWritable:开头在文件中位置,位置索引
// Text: 输入类型
// Text:输出类型
// IntWritable:对应个数
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {

//    map里面尽量不要生成对象,垃圾回收压力太大,降低性能
    private Text word = new Text();
    private IntWritable one = new IntWritable(1);

//    框架给一些数据,处理之后交还给框架
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

        String string = value.toString();
        String[] words = string.split(" ");

//        (单词, 1),只是数据类型转变,不负责数
        for (String word : words){
            this.word.set(word);
            context.write(this.word, this.one);
        }
    }
}

编写Reducer类

package com.deltqin;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

import java.io.IOException;

public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {

    private IntWritable intWritable = new IntWritable();

    /**
     * 框架将mapper输出的内容处理,变成(单词,单词所有的1(可迭代变量)),同一单词个数相加
     * @param key 单词
     * @param values 单词所有的1(可迭代变量)
     * @param context 任务自己
     * @throws IOException
     * @throws InterruptedException
     */
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int sum = 0;
        for (IntWritable intWritable1: values){
            sum += intWritable1.get();
        }
        intWritable.set(sum);
        context.write(key, intWritable);

    }
}

编写Driver驱动类

package com.deltqin;

import java.io.IOException;
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 WordCountDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        // 1 获取配置信息以及封装任务job实例
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);

        // 2 设置jar加载路径
        job.setJarByClass(WordCountDriver.class);

        // 3 设置map和reduce类
        job.setMapperClass(WordCountMapper.class);
        job.setReducerClass(WordCountReducer.class);

        // 4 设置map输出
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);

        // 5 设置最终输出kv类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        // 6 设置输入和输出路径
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        // 7 提交
        boolean result = job.waitForCompletion(true);

        System.exit(result ? 0 : 1);
    }
}

mvn pacakage 打成jar包,然后拷贝到Hadoop集群中

在 Hadoop101 执行WordCount程序,jar包提交到集群运行。

yarn jar  wc.jar com.deltaqin.WordcountDriver /test.txt /output1

com.deltaqin.WordcountDriver 注意使用类名的全类型引用

/output1 后面是输入文件以及输出文件,输出路径必须是一个不存在的路径

Hadoop序列化

序列化不代表持久化

  • 序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于存储到磁盘(持久化)和网络传输。序列化可以存储“活的”对象,可以将“活的”对象发送到远程计算机
  • 反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象。

Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),大数据数据量本来就大,不便于在网络中高效传输。所以,Hadoop自己开发了一套序列化机制(Writable)。

  • 紧凑快速:只序列化必要的数据,开销小。
  • 可扩展:随着通信协议升级而升级
  • 互操作:支持多语言的交互

注意上面的WordCount,类型都是明确指定,一一设定的,不像java自己的序列化可以自动识别

// 3 设置map和reduce类
job.setMapperClass(WordcountMapper.class);
job.setReducerClass(WordcountReducer.class);

// 4 设置map输出
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);

// 5 设置最终输出kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);

自定义bean对象实现序列化接口(Writable)

统计号码流量

输入数据格式:

7 13560436666 120.196.100.99 1116  954 200
id 手机号码 网络ip 上行流量  下行流量     网络状态

期望输出数据格式

13560436666 1116       954 2070
手机号码     上行流量        下行流量 流量
  • 必须实现Writable接口
  • 反序列化时,需要反射调用空参构造函数 super(); ,所以必须有空参构造(通过反射构造对象一般会使用无参构造器)
  • 重写序列化方法 write(DataOutput out)
  • 重写反序列化方法 readFields(DataInput in),注意反序列化的顺序和序列化的顺序完全一致
  • 要想把结果显示在文件中,需要重写toString(),可用”\t”分开,方便后续用。
  • 如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,因为MapReduce中的Shuffle过程要求对key必须能排序。
@Override
public int compareTo(FlowBean o) {
	// 倒序排列,从大到小
	return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
package com.deltaqin;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.Writable;

// 1 实现writable接口
public class FlowBean implements Writable{

    private long upFlow;
    private long downFlow;
    private long sumFlow;

    //2  反序列化时,需要反射调用空参构造函数,所以必须有
    public FlowBean() {
        super();
    }

    public FlowBean(long upFlow, long downFlow) {
        super();
        this.upFlow = upFlow;
        this.downFlow = downFlow;
        this.sumFlow = upFlow + downFlow;
    }

    public void set(long upFlow, long downFlow) {
        this.upFlow = upFlow;
        this.downFlow = downFlow;
        this.sumFlow = upFlow + downFlow;
    }

    //3  写序列化方法,将数据写到指定的地方
    // DataOutput 数据的容器
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeLong(upFlow);
        out.writeLong(downFlow);
        out.writeLong(sumFlow);
    }

    //4 反序列化方法
    //5 反序列化方法读顺序必须和写序列化方法的写顺序必须一致
    @Override
    public void readFields(DataInput in) throws IOException {
        this.upFlow  = in.readLong();
        this.downFlow = in.readLong();
        this.sumFlow = in.readLong();
    }

    @Override
    public String toString() {
        return upFlow + "\t" + downFlow + "\t" + sumFlow;
    }

    public long getUpFlow() {
        return upFlow;
    }

    public void setUpFlow(long upFlow) {
        this.upFlow = upFlow;
    }

    public long getDownFlow() {
        return downFlow;
    }

    public void setDownFlow(long downFlow) {
        this.downFlow = downFlow;
    }

    public long getSumFlow() {
        return sumFlow;
    }

    public void setSumFlow(long sumFlow) {
        this.sumFlow = sumFlow;
    }
}

编写Mapper类

输出类型是 FlowBean

package com.deltaqin;
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

public class FlowCountMapper extends Mapper<LongWritable, Text, Text, FlowBean>{

    FlowBean v = new FlowBean();
    Text k = new Text();

    @Override
    protected void map(LongWritable key, Text value, Context context)	throws IOException, InterruptedException {

        // 1 获取一行
        String line = value.toString();

        // 2 切割字段
        String[] fields = line.split("\t");

        // 3 封装对象
        // 取出手机号码
        String phoneNum = fields[1];

        // 取出上行流量和下行流量
        long upFlow = Long.parseLong(fields[fields.length - 3]);
        long downFlow = Long.parseLong(fields[fields.length - 2]);

        k.set(phoneNum);
        v.set(downFlow, upFlow);

        // 4 写出
        context.write(k, v);
    }
}

编写Reducer类

package com.deltaqin;
import java.io.IOException;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
 
public class FlowCountReducer extends Reducer<Text, FlowBean, Text, FlowBean> {

	private FlowBean flowBean01 = new FlowBean();

	@Override
	protected void reduce(Text key, Iterable<FlowBean> values, Context context)throws IOException, InterruptedException {
 
		long sum_upFlow = 0;
		long sum_downFlow = 0;
 
		// 1 遍历所用bean,将其中的上行流量,下行流量分别累加
		for (FlowBean flowBean : values) {
			sum_upFlow += flowBean.getUpFlow();
			sum_downFlow += flowBean.getDownFlow();
		}
 
		// 2 封装对象
		flowBean01.set(sum_upFlow,sum_downFlow);
		
		// 3 写出
		context.write(key, flowBean01);
	}
}

编写Driver驱动类

package com.atguigu.mapreduce.flowsum;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
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 FlowsumDriver {

    public static void main(String[] args) throws IllegalArgumentException, IOException, ClassNotFoundException, InterruptedException {

        // 输入输出路径需要根据自己电脑上实际的输入输出路径设置
        args = new String[] { "e:/input/inputflow", "e:/output1" };

        // 1 获取配置信息,或者job对象实例
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);

        // 6 指定本程序的jar包所在的本地路径
        job.setJarByClass(FlowsumDriver.class);

        // 2 指定本业务job要使用的mapper/Reducer业务类
        job.setMapperClass(FlowCountMapper.class);
        job.setReducerClass(FlowCountReducer.class);

        // 3 指定mapper输出数据的kv类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);

        // 4 指定最终输出的数据的kv类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);

        // 5 指定job的输入原始文件所在目录
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        // 7 将job中配置的相关参数,以及job所用的java类所在的jar包, 提交给yarn去运行
        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}

MapReduce****框架原理

Map阶段:

  • MapTask.run 执行map阶段,
    • 调用Mapper的map方法

1603429907651-83c58552-730a-4f77-93f0-cbf354996b51.png

InputFormat数据输入

一般分几份之后就会启动多少个MapTask来执行****

  • 遍历文件,按最小切片大小生成切片
  • 数据变成KV:切片是在客户端完成。对每一个切片获取recordReader,在并行的mapper task完成,recordreader就可以变成KV。实际切KV是recordReader,不是切片实现的。
  • 输出给mapper

切片与MapTask并行度决定机制****

MapTask的并行度决定Map阶段的任务处理并发度,进而影响到整个Job的处理速度。


数据(切)****块:BlockHDFS物理把数据分成一块一块。

数据****切片:数据切片只是逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。

按照块大小来切分数据(物理切块和逻辑切分对应起来),就避免了原本在这个DN的数据还需要传递到其他DN上,减少了网络传输,更多的带宽留给shuffle

1603430560784-61f15f3d-06d7-435a-8b48-fadf916da6bc.png

J****ob提交流程源码

  • jar包
  • 切片信息
  • job配置的xml

1603435099140-35c6f83a-f031-43be-b078-ed1a3f0a977b.png

调试源码:主要代码片段:

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());

切片****源码

对应上面源码解读的第4步。

注意下面的1.1倍长才会切片1倍,怕浪费

1603435284260-081801c4-82b3-4039-8bfb-ae99aedd12ac.png

FileInputFormat(抽象父类)切片机制

源码里面该抽象父类只实现了getSplits也就是上面的切片过程。

还有一个createRecordReader没有实现,需要他的特定子类实现,默认使用的是TextInputFormat

1603435388892-ebe50218-c51b-4842-a7c4-36f3da7d2171.png

1603435411273-3c0a9630-94ac-4a20-ac73-e4060f88f14a.png

TextInputFormat的KV

TextInputFormat是默认的FileInputFormat实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text类型。

1603435913143-b5d9edbd-f427-4990-b449-5f9d9a9460db.png

KeyValueTextInputFormat的KV

每一行一条记录,分隔符分为KV,默认分隔符是tab

NLineInputFormat的KV

map处理的不是按照block划分,而是按照指定的行数去划分

CombineTextInputFormat切片机制

框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小都会是一个单独的切,都会交给一个MapTask,这样如果有大量小文件,就产生大量的MapTask,处理效率极其低下。

CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。


CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m

注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。


生成切片过程包括:虚拟存储过程和切片过程二部分。

(1)虚拟存储过程:

将输入目录下所有文件大小,依次和设置的setMaxInputSplitSize值比较,如果不大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍,那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值2倍,此时将文件均分成2个虚拟存储块(防止出现小切片)

例如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

CombineTextInputFormat案例实操

将输入的大量小文件合并成一个切片统一处理。

//不做任何处理,运行1.6节的WordCount案例程序,观察切片个数为4。
 
//在WordcountDriver中增加如下代码,运行程序,并观察运行的切片个数为3。
//驱动类中添加代码如下:
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
 
//虚拟存储切片最大值设置4m
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
//运行为3个切片。


//在WordcountDriver中增加如下代码,运行程序,并观察运行的切片个数为1。
//驱动中添加代码如下:
// 如果不设置InputFormat,它默认用的是TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
 
//虚拟存储切片最大值设置20m
CombineTextInputFormat.setMaxInputSplitSize(job, 20971520);
//运行如果为1个切片。

自定义inputFormat

继承一些东西(RecordReader),实现一些,重写一些方法

  • 初始化
  • 是否读到
  • 读取K
  • 读取V
  • 获取进度
  • 关闭****

Shuffle机制

Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle。

MapReduce工作流程****

MapTask工作机制

7往缓存区里面写东西,写满之后才会输出,溢出为文件

其中第9步使用的快排,全部在内存完成,

局部排序可以使用快排

归并不需要全部在内存里面,两个指针逐渐遍历即可,没必要全进来内存,但是快排必须全部到内存里面,所以这里最后使用归并而不是快排。

最后每个task都输出一个有序文件。

多个有序文件再归并变成一个有序文件,给reduce 1603435963914-2ab50efa-41ee-438e-8082-cab7ab6362b7.png1603443600046-da256ed5-f5b3-4099-83fd-cbe8028fe9a9.png

  • Read阶段:MapTask通过用户编写的RecordReader,从输入InputSplit中解析出一个个key/value。
  • Map阶段:该节点主要是将解析出的key/value交给用户编写map()函数处理,并产生一系列新的key/value。
  • Collect收集阶段:在用户编写map()函数中,当数据处理完成后,一般会调用OutputCollector.collect()输出结果。在函数内部,它会生成的key/value分区调用Partitioner),并写入一个环形内存缓冲区中。
  • Spill阶段:即“写”当环形缓冲区满后,MapReduce会将数据写到本地磁盘上,生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要对数据进行合并、压缩等操作。
    • 步骤1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。
    • 步骤2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每个分区中的数据进行一次聚集操作。
    • 步骤3:将分区数据的元信息写到内存索引数据结构SpillRecord,其中每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写到文件output/spillN.out.index中。
  • Combine阶段:当所有数据处理完成后,MapTask所有临时文件进行一次合并,以确保最终只会生成一个数据文件。

所有数据处理完后,MapTask将所有临时文件合并成一个大文件保存到文件output/file.out,同时生成相应的索引文件output/file.out.index。在进行文件合并过程中,MapTask分区为单位进行合并。对于某个分区,将采用多轮递归合并的方式每轮合并io.sort.factor(默认10)个文件,并将产生的文件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。

每个MapTask最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。

ReduceTask工作机制

1605197602887-93206d3f-c232-4317-a492-302b8de90eb1.png

1603435998329-e216d411-1937-4515-a08c-b67e012767d9.png

  • Copy阶段:ReduceTask从各个MapTask上远程拷贝一片数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
  • Merge阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。
  • Sort阶段:按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。
  • Reduce阶段:reduce()函数将计算结果写到HDFS上。

设置ReduceTask****并行度(个数)

ReduceTask的并行度同样影响整个Job的执行并发度和执行效率,但与MapTask的并发数由切片数决定不同,ReduceTask数量的决定是可以直接手动设置:

// 默认值是1,手动设置为4

job.setNumReduceTasks(4);

1605197673691-bb95b06f-1ee0-4ed8-87c0-2b1ef10f78e0.png


shuffle流程

shuffle洗牌过程,一共涉及三次排序(用时间换空间,其实快排比归并更快):

  • 第一次内存里面的快排
  • 第二次是一个task里面的归并
  • 第三次是所有task的有序文件的归并

上面的流程是整个MapReduce最全工作流程,但是Shuffle过程只是从第7步开始到第16步结束,具体Shuffle过程详解,如下:

(1)MapTask收集我们的map()方法输出的kv对,放到内存缓冲区中

(2)从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件

(3)多个溢出文件会被合并成大的溢出文件

(4)在溢出过程及合并的过程中,都要调用Partitioner进行分区针对key进行排序

(5)ReduceTask根据自己的分区号,去各个MapTask机器上取相应的结果分区数据

(6)ReduceTask会取到同一个分区的来自不同MapTask的结果文件,ReduceTask会将这些文件再进行合并(归并排序)

(7)合并成大文件后,Shuffle的过程也就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用用户自定义的reduce()方法)

注意****:

(1)Shuffle中的缓冲区大小会影响到MapReduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。

(2)缓冲区的大小可以通过参数调整,参数:io.sort.mb默认100M。

1603436287685-1022696f-dcb8-4a08-85a8-7d4afd1fbf15.png

看转过来的第二行开始位置,reduce处理的时候是拿每一个map的相同分区上的数据来归并。得到的结果再分组给不同的reduce。

Partition--****分区

一个map被分为很多区,分区是为了给reduce划分数据。reduce处理的时候也是采用并行的机制,

在分区之后才会快排。分区的依据就是有多少reduce在工作

默认分区方式

key的 hashcode对reduce的个数取余,相同的取余运算结果去往同一个分区。

默认分区是根据key的hashCode对ReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区。

public class HashPartitioner<K, V> extends Partitioner<K, V> {

    public int getPartition(K key, V value, int numReduceTasks) {
        // 与是为了去负号,除了第一个其余不变,相当于把符号位变成0
        return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
    }
}

自定义分区方式

自定义类继承Partitioner,重写getPartition()方法

public class CustomPartitioner extends Partitioner<Text, FlowBean> {
    @Override
    public int getPartition(Text key, FlowBean value, int numPartitions) {
        // 控制分区代码逻辑
        … …
            return partition;
    }
}

1603445468121-615df441-45e2-40b2-a2d3-d11749b09796.png

Partition分区案例实操

将统计结果按照手机归属地不同省份输出到不同文件中(分区)

手机号136、137、138、139开头都分别放到一个独立的4个文件中,其他开头的放到一个文件中。

package com.atguigu.mapreduce.flowsum;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;

public class ProvincePartitioner extends Partitioner<Text, FlowBean> {

    @Override
    public int getPartition(Text key, FlowBean value, int numPartitions) {

        // 1 获取电话号码的前三位
        String preNum = key.toString().substring(0, 3);

        int partition = 4;

        // 2 判断是哪个省
        if ("136".equals(preNum)) {
            partition = 0;
        }else if ("137".equals(preNum)) {
            partition = 1;
        }else if ("138".equals(preNum)) {
            partition = 2;
        }else if ("139".equals(preNum)) {
            partition = 3;
        }

        return partition;
    }
}

在驱动函数中增加自定义数据分区设置和ReduceTask设置


不设置的话还是使用默认的hash分区

// 8 指定自定义数据分区

// 不设置的话还是使用默认的hash分区

job.setPartitionerClass(ProvincePartitioner.class);

// 9 同时指定相应数量的reduce task

job.setNumReduceTasks(5);

package com.atguigu.mapreduce.flowsum;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
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 FlowsumDriver {

    public static void main(String[] args) throws IllegalArgumentException, IOException, ClassNotFoundException, InterruptedException {

        // 输入输出路径需要根据自己电脑上实际的输入输出路径设置
        args = new String[]{"e:/output1","e:/output2"};

        // 1 获取配置信息,或者job对象实例
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);

        // 2 指定本程序的jar包所在的本地路径
        job.setJarByClass(FlowsumDriver.class);

        // 3 指定本业务job要使用的mapper/Reducer业务类
        job.setMapperClass(FlowCountMapper.class);
        job.setReducerClass(FlowCountReducer.class);

        // 4 指定mapper输出数据的kv类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(FlowBean.class);

        // 5 指定最终输出的数据的kv类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);

        // 8 指定自定义数据分区
        // 不设置的话还是使用默认的hash分区
        job.setPartitionerClass(ProvincePartitioner.class);

        // 9 同时指定相应数量的reduce task
        job.setNumReduceTasks(5);

        // 6 指定job的输入原始文件所在目录
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        // 7 将job中配置的相关参数,以及job所用的java类所在的jar包, 提交给yarn去运行
        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}

WritableComparable接口--****排序

自定义排序 WritableComparable 原理分析

bean对象做为key传输,需要实现WritableComparable接口重写compareTo方法,就可以实现排序。

只要实现了此接口,就可以动态调用你。

@Override
public int compareTo(FlowBean o) {

    int result;

    // 按照总流量大小,倒序排列
    if (sumFlow > bean.getSumFlow()) {
        result = -1;
    }else if (sumFlow < bean.getSumFlow()) {
        result = 1;
    }else {
        result = 0;
    }

    return result;
}

WritableComparable排序案例实操(全排序)****

根据案例2.3产生的结果再次对总流量进行排序。

13509468723 7335 110349 117684

13736230513 2481 24681 27162

13956435636 132 1512 1644

13846544121 264 0 264

。。 。。。


FlowBean对象在在需求1基础上增加了比较功能

package com.atguigu.mapreduce.sort;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.WritableComparable;

public class FlowBean implements WritableComparable<FlowBean> {

    private long upFlow;
    private long downFlow;
    private long sumFlow;

    // 反序列化时,需要反射调用空参构造函数,所以必须有
    public FlowBean() {
        super();
    }

    public FlowBean(long upFlow, long downFlow) {
        super();
        this.upFlow = upFlow;
        this.downFlow = downFlow;
        this.sumFlow = upFlow + downFlow;
    }

    public void set(long upFlow, long downFlow) {
        this.upFlow = upFlow;
        this.downFlow = downFlow;
        this.sumFlow = upFlow + downFlow;
    }

    public long getSumFlow() {
        return sumFlow;
    }

    public void setSumFlow(long sumFlow) {
        this.sumFlow = sumFlow;
    }	

    public long getUpFlow() {
        return upFlow;
    }

    public void setUpFlow(long upFlow) {
        this.upFlow = upFlow;
    }

    public long getDownFlow() {
        return downFlow;
    }

    public void setDownFlow(long downFlow) {
        this.downFlow = downFlow;
    }

    /**
	 * 序列化方法
	 * @param out
	 * @throws IOException
	 */
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeLong(upFlow);
        out.writeLong(downFlow);
        out.writeLong(sumFlow);
    }

    /**
	 * 反序列化方法 注意反序列化的顺序和序列化的顺序完全一致
	 * @param in
	 * @throws IOException
	 */
    @Override
    public void readFields(DataInput in) throws IOException {
        upFlow = in.readLong();
        downFlow = in.readLong();
        sumFlow = in.readLong();
    }

    @Override
    public String toString() {
        return upFlow + "\t" + downFlow + "\t" + sumFlow;
    }

    @Override
    public int compareTo(FlowBean bean) {

        int result;

        // 按照总流量大小,倒序排列
        if (sumFlow > bean.getSumFlow()) {
            result = -1;
        }else if (sumFlow < bean.getSumFlow()) {
            result = 1;
        }else {
            result = 0;
        }

        return result;
    }
}

编写Mapper类

package com.atguigu.mapreduce.sort;
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
 
public class FlowCountSortMapper extends Mapper<LongWritable, Text, FlowBean, Text>{
 
	FlowBean bean = new FlowBean();
	Text v = new Text();
 
	@Override
	protected void map(LongWritable key, Text value, Context context)	throws IOException, InterruptedException {
 
		// 1 获取一行
		String line = value.toString();
		
		// 2 截取
		String[] fields = line.split("\t");
		
		// 3 封装对象
		String phoneNbr = fields[0];
		long upFlow = Long.parseLong(fields[1]);
		long downFlow = Long.parseLong(fields[2]);
		
		bean.set(upFlow, downFlow);
		v.set(phoneNbr);
		
		// 4 输出
		context.write(bean, v);
	}
}

编写Reducer类

package com.atguigu.mapreduce.sort;
import java.io.IOException;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

public class FlowCountSortReducer extends Reducer<FlowBean, Text, Text, FlowBean>{

    @Override
    protected void reduce(FlowBean key, Iterable<Text> values, Context context)	throws IOException, InterruptedException {

        // 循环输出,避免总流量相同情况
        for (Text text : values) {
            context.write(text, key);
        }
    }
}

编写Driver类

package com.atguigu.mapreduce.sort;
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
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 FlowCountSortDriver {

    public static void main(String[] args) throws ClassNotFoundException, IOException, InterruptedException {

        // 输入输出路径需要根据自己电脑上实际的输入输出路径设置
        args = new String[]{"e:/output1","e:/output2"};

        // 1 获取配置信息,或者job对象实例
        Configuration configuration = new Configuration();
        Job job = Job.getInstance(configuration);

        // 2 指定本程序的jar包所在的本地路径
        job.setJarByClass(FlowCountSortDriver.class);

        // 3 指定本业务job要使用的mapper/Reducer业务类
        job.setMapperClass(FlowCountSortMapper.class);
        job.setReducerClass(FlowCountSortReducer.class);

        // 4 指定mapper输出数据的kv类型
        job.setMapOutputKeyClass(FlowBean.class);
        job.setMapOutputValueClass(Text.class);

        // 5 指定最终输出的数据的kv类型
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(FlowBean.class);

        // 6 指定job的输入原始文件所在目录
        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        // 7 将job中配置的相关参数,以及job所用的java类所在的jar包, 提交给yarn去运行
        boolean result = job.waitForCompletion(true);
        System.exit(result ? 0 : 1);
    }
}

WritableComparable排序案例实操(区内排序)

1****)需求

要求每个省份手机号输出的文件中按照总流量内部排序。

2)需求****分析

基于前一个需求,增加自定义分区类分区按照省份手机号设置。

3)****案例实操

(1)增加自定义分区类
package com.atguigu.mapreduce.sort;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
 
public class ProvincePartitioner extends Partitioner<FlowBean, Text> {
 
	@Override
	public int getPartition(FlowBean key, Text value, int numPartitions) {
		
		// 1 获取手机号码前三位
		String preNum = value.toString().substring(0, 3);
		
		int partition = 4;
		
		// 2 根据手机号归属地设置分区
		if ("136".equals(preNum)) {
			partition = 0;
		}else if ("137".equals(preNum)) {
			partition = 1;
		}else if ("138".equals(preNum)) {
			partition = 2;
		}else if ("139".equals(preNum)) {
			partition = 3;
		}
 
		return partition;
	}
}
(2)在驱动类中添加分区类
// 加载自定义分区类
job.setPartitionerClass(ProvincePartitioner.class);
 
// 设置Reducetask个数
job.setNumReduceTasks(5);

Combiner--合并

1605196752290-e9473b75-f7c9-4897-bafa-5f3e79a0587d.png

(6)自定义Combiner实现步骤

自定义一个Combiner继承Reducer,重写Reduce方法

public class WordcountCombiner extends Reducer<Text, IntWritable, Text,IntWritable>{

    @Override
    protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {

        // 1 汇总操作
        int count = 0;
        for(IntWritable v :values){
            count += v.get();
        }

        // 2 写出
        context.write(key, new IntWritable(count));
    }
}

Job驱动类中设置:  

job.setCombinerClass(WordcountCombiner.class);

Combiner合并案例实操****

统计过程中对每一个MapTask的输出进行局部汇总,以减小网络传输量即采用Combiner功能。

期望Combine输入数据多,输出时经过合并,输出数据降低。

(1)增加一个WordcountCombiner类继承Reducer

package com.atguigu.mr.combiner;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
 
public class WordcountCombiner extends Reducer<Text, IntWritable, Text, IntWritable>{
 
IntWritable v = new IntWritable();
 
	@Override
	protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
 
        // 1 汇总
		int sum = 0;
 
		for(IntWritable value :values){
			sum += value.get();
		}
 
		v.set(sum);
 
		// 2 写出
		context.write(key, v);
	}
}

(2)在WordcountDriver驱动类中指定Combiner

// 指定需要使用combiner,以及用哪个类作为combiner的逻辑
job.setCombinerClass(WordcountCombiner.class);

1605197135723-0bebe915-608b-426e-b990-c1cf9340978d.png

OutputFormat数据输出

OutputFormat接口****实现类

1605197780928-ab8a197d-4bdd-4baa-95e0-edccb2d04756.png

自定义Out****putFormat

1605197888418-ea6c01a6-b763-4b83-89e5-25b523b838d9.png

1605198011698-39e752bb-212c-456e-aff7-aa4edb914f41.png

编写FilterMapper类

package com.atguigu.mapreduce.outputformat;
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
 
public class FilterMapper extends Mapper<LongWritable, Text, Text, NullWritable>{
	
	@Override
	protected void map(LongWritable key, Text value, Context context)	throws IOException, InterruptedException {
 
		// 写出
		context.write(value, NullWritable.get());
	}
}

编写FilterReducer类

package com.atguigu.mapreduce.outputformat;
import java.io.IOException;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
 
public class FilterReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
 
Text k = new Text();
 
	@Override
	protected void reduce(Text key, Iterable<NullWritable> values, Context context)		throws IOException, InterruptedException {
 
       // 1 获取一行
		String line = key.toString();
 
       // 2 拼接
		line = line + "\r\n";
 
       // 3 设置key
       k.set(line);
 
       // 4 输出
		context.write(k, NullWritable.get());
	}
}

(3)自定义一个OutputFormat类

package com.atguigu.mapreduce.outputformat;
import java.io.IOException;
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;
 
public class FilterOutputFormat extends FileOutputFormat<Text, NullWritable>{
 
	@Override
	public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job)			throws IOException, InterruptedException {
 
		// 创建一个RecordWriter
		return new FilterRecordWriter(job);
	}
}

(4)编写RecordWriter类

package com.atguigu.mapreduce.outputformat;
import java.io.IOException;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;

public class FilterRecordWriter extends RecordWriter<Text, NullWritable> {

    FSDataOutputStream atguiguOut = null;
    FSDataOutputStream otherOut = null;

    public FilterRecordWriter(TaskAttemptContext job) {

        // 1 获取文件系统
        FileSystem fs;

        try {
            fs = FileSystem.get(job.getConfiguration());

            // 2 创建输出文件路径
            Path atguiguPath = new Path("e:/atguigu.log");
            Path otherPath = new Path("e:/other.log");

            // 3 创建输出流
            atguiguOut = fs.create(atguiguPath);
            otherOut = fs.create(otherPath);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void write(Text key, NullWritable value) throws IOException, InterruptedException {

        // 判断是否包含“atguigu”输出到不同文件
        if (key.toString().contains("atguigu")) {
            atguiguOut.write(key.toString().getBytes());
        } else {
            otherOut.write(key.toString().getBytes());
        }
    }

    @Override
    public void close(TaskAttemptContext context) throws IOException, InterruptedException {

        // 关闭资源
        IOUtils.closeStream(atguiguOut);
        IOUtils.closeStream(otherOut);	}
}

(5)编写FilterDriver类

package com.atguigu.mapreduce.outputformat;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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 FilterDriver {
 
	public static void main(String[] args) throws Exception {
 
// 输入输出路径需要根据自己电脑上实际的输入输出路径设置
args = new String[] { "e:/input/inputoutputformat", "e:/output2" };
 
		Configuration conf = new Configuration();
		Job job = Job.getInstance(conf);
 
		job.setJarByClass(FilterDriver.class);
		job.setMapperClass(FilterMapper.class);
		job.setReducerClass(FilterReducer.class);
 
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(NullWritable.class);
		
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(NullWritable.class);
 
		// 要将自定义的输出格式组件设置到job中
		job.setOutputFormatClass(FilterOutputFormat.class);
 
		FileInputFormat.setInputPaths(job, new Path(args[0]));
 
		// 虽然我们自定义了outputformat,但是因为我们的outputformat继承自fileoutputformat
		// 而fileoutputformat要输出一个_SUCCESS文件,所以,在这还得指定一个输出目录
		FileOutputFormat.setOutputPath(job, new Path(args[1]));
 
		boolean result = job.waitForCompletion(true);
		System.exit(result ? 0 : 1);
	}
}

Join多种****应用

Reduce Join

Map端的主要工作:为来自不同表或文件的key/value对,打标签以区别不同来源的记录。然后用连接字段作为key,其余部分和新加的标志作为value,最后进行输出。

Reduce端的主要工作:在Reduce端以连接字段作为key的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在Map阶段已经打标志)分开,最后进行合并就ok了。


通过将关联条件作为Map输出的key,将两表满足Join条件的数据并携带数据所来源的文件信息,发往同一个ReduceTask,在Reduce中进行数据的串联。


(1)创建商品和订合并后的Bean类

package com.atguigu.reducejoin;
 
import org.apache.hadoop.io.WritableComparable;
 
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
 
public class OrderBean implements WritableComparable<OrderBean> {
    private String id;
    private String pid;
    private int amount;
    private String pname;
 
    @Override
    public String toString() {
        return id + "\t" + pname + "\t" + amount;
    }
 
    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;
    }
 
    //按照Pid分组,组内按照pname排序,有pname的在前
    @Override
    public int compareTo(OrderBean o) {
        int compare = this.pid.compareTo(o.pid);
        if (compare == 0) {
            return o.getPname().compareTo(this.getPname());
        } else {
            return compare;
        }
    }
 
    @Override
    public void write(DataOutput out) throws IOException {
        out.writeUTF(id);
        out.writeUTF(pid);
        out.writeInt(amount);
        out.writeUTF(pname);
    }
 
    @Override
    public void readFields(DataInput in) throws IOException {
        id = in.readUTF();
        pid = in.readUTF();
        amount = in.readInt();
        pname = in.readUTF();
    }
}

(2)编写TableMapper类

package com.atguigu.reducejoin;
 
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
 
import java.io.IOException;
 
public class OrderMapper extends Mapper<LongWritable, Text, OrderBean, NullWritable> {
 
    private String filename;
 
    private OrderBean order = new OrderBean();
 
    @Override
    protected void setup(Context context) throws IOException, InterruptedException {
        
        //获取切片文件名
        FileSplit fs = (FileSplit) context.getInputSplit();
        filename = fs.getPath().getName();
    }
 
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String[] fields = value.toString().split("\t");
        
        //对不同数据来源分开处理
        if ("order.txt".equals(filename)) {
            order.setId(fields[0]);
            order.setPid(fields[1]);
            order.setAmount(Integer.parseInt(fields[2]));
            order.setPname("");
        } else {
            order.setPid(fields[0]);
            order.setPname(fields[1]);
            order.setAmount(0);
            order.setId("");
        }
 
        context.write(order, NullWritable.get());
    }
}

(3)编写TableReducer类

package com.atguigu.reducejoin;
 
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Reducer;
 
import java.io.IOException;
import java.util.Iterator;
 
public class OrderReducer extends Reducer<OrderBean, NullWritable, OrderBean, NullWritable> {
 
    @Override
    protected void reduce(OrderBean key, Iterable<NullWritable> values, Context context) throws IOException, InterruptedException {
        
        //第一条数据来自pd,之后全部来自order
        Iterator<NullWritable> iterator = values.iterator();
        
        //通过第一条数据获取pname
        iterator.next();
        String pname = key.getPname();
        
        //遍历剩下的数据,替换并写出
        while (iterator.hasNext()) {
            iterator.next();
            key.setPname(pname);
            context.write(key,NullWritable.get());
        }
    }
}

(4)编写TableDriver类

package com.atguigu.reducejoin;
 
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
 
import java.io.IOException;
 
public class OrderDriver {
    public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
        Job job = Job.getInstance(new Configuration());
        job.setJarByClass(OrderDriver.class);
 
        job.setMapperClass(OrderMapper.class);
        job.setReducerClass(OrderReducer.class);
        job.setGroupingComparatorClass(OrderComparator.class);
 
        job.setMapOutputKeyClass(OrderBean.class);
        job.setMapOutputValueClass(NullWritable.class);
 
        job.setOutputKeyClass(OrderBean.class);
        job.setOutputValueClass(NullWritable.class);
 
        FileInputFormat.setInputPaths(job, new Path("d:\\input"));
        FileOutputFormat.setOutputPath(job, new Path("d:\\output"));
 
        boolean b = job.waitForCompletion(true);
 
        System.exit(b ? 0 : 1);
 
    }
}

缺点:这种方式中,合并的操作是在Reduce阶段完成,Reduce端的处理压力太大,Map节点的运算负载则很低,资源利用率不高,且在Reduce阶段极易产生数据倾斜。

Map Join****

Map Join适用于一张表十分小、一张表很大的场景。

Reduce处理过多的表,非常容易产生数据倾斜。怎么办?

Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。

Mapper的setup阶段,将文件读取到缓存集合中

驱动函数中加载缓存。

(1)先在驱动模块中添加缓存文件

package test;
import java.net.URI;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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 DistributedCacheDriver {
 
	public static void main(String[] args) throws Exception {
		
// 0 根据自己电脑路径重新配置
args = new String[]{"e:/input/inputtable2", "e:/output1"};
 
// 1 获取job信息
		Configuration configuration = new Configuration();
		Job job = Job.getInstance(configuration);
 
		// 2 设置加载jar包路径
		job.setJarByClass(DistributedCacheDriver.class);
 
		// 3 关联map
		job.setMapperClass(DistributedCacheMapper.class);
		
// 4 设置最终输出数据类型
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(NullWritable.class);
 
		// 5 设置输入输出路径
		FileInputFormat.setInputPaths(job, new Path(args[0]));
		FileOutputFormat.setOutputPath(job, new Path(args[1]));
 
		// 6 加载缓存数据
		job.addCacheFile(new URI("file:///e:/input/inputcache/pd.txt"));
		
		// 7 Map端Join的逻辑不需要Reduce阶段,设置reduceTask数量为0
		job.setNumReduceTasks(0);
 
		// 8 提交
		boolean result = job.waitForCompletion(true);
		System.exit(result ? 0 : 1);
	}
}

(2)读取缓存的文件数据

package com.atguigu.mapjoin;
 
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
 
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
 
public class MjMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
 
    //pd表在内存中的缓存
    private Map<String, String> pMap = new HashMap<>();
 
    private Text line = new Text();
 
    //任务开始前将pd数据缓存进PMap
    @Override
    protected void setup(Context context) throws IOException, InterruptedException {
        
        //从缓存文件中找到pd.txt
        URI[] cacheFiles = context.getCacheFiles();
        Path path = new Path(cacheFiles[0]);
 
        //获取文件系统并开流
        FileSystem fileSystem = FileSystem.get(context.getConfiguration());
        FSDataInputStream fsDataInputStream = fileSystem.open(path);
 
        //通过包装流转换为reader
        BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(fsDataInputStream, "utf-8"));
 
        //逐行读取,按行处理
        String line;
        while (StringUtils.isNotEmpty(line = bufferedReader.readLine())) {
            String[] fields = line.split("\t");
            pMap.put(fields[0], fields[1]);
        }
 
        //关流
        IOUtils.closeStream(bufferedReader);
 
    }
 
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String[] fields = value.toString().split("\t");
 
        String pname = pMap.get(fields[1]);
 
        line.set(fields[0] + "\t" + pname + "\t" + fields[2]);
 
        context.write(line, NullWritable.get());
    }
}

计数器应用

Hadoop为每个作业维护若干内置计数器,以描述多项指标。例如,某些计数器记录已处理的字节数和记录数,使用户可监控已处理的输入数据量和已产生的输出数据量。

1605198645793-3f014cc6-eb52-4cd3-899b-d94887df194d.png

数据清洗(ETL)

运行核心业务MapReduce程序之前,往往要先对数据进行清洗,清理掉不符合用户要求的数据。清理的过程往往只需要运行Mapper程序,不需要运行Reduce程序。



(1)编写LogMapper类

package com.atguigu.mapreduce.weblog;
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
 
public class LogMapper extends Mapper<LongWritable, Text, Text, NullWritable>{
	
	Text k = new Text();
	
	@Override
	protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
		
		// 1 获取1行数据
		String line = value.toString();
		
		// 2 解析日志
		boolean result = parseLog(line,context);
		
		// 3 日志不合法退出
		if (!result) {
			return;
		}
		
		// 4 设置key
		k.set(line);
		
		// 5 写出数据
		context.write(k, NullWritable.get());
	}
 
	// 2 解析日志
	private boolean parseLog(String line, Context context) {
 
		// 1 截取
		String[] fields = line.split(" ");
		
		// 2 日志长度大于11的为合法
		if (fields.length > 11) {
 
			// 系统计数器
			context.getCounter("map", "true").increment(1);
			return true;
		}else {
			context.getCounter("map", "false").increment(1);
			return false;
		}
	}
}

(2)编写LogDriver类

package com.atguigu.mapreduce.weblog;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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 LogDriver {
 
	public static void main(String[] args) throws Exception {
 
// 输入输出路径需要根据自己电脑上实际的输入输出路径设置
        args = new String[] { "e:/input/inputlog", "e:/output1" };
 
		// 1 获取job信息
		Configuration conf = new Configuration();
		Job job = Job.getInstance(conf);
 
		// 2 加载jar包
		job.setJarByClass(LogDriver.class);
 
		// 3 关联map
		job.setMapperClass(LogMapper.class);
 
		// 4 设置最终输出类型
		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(NullWritable.class);
 
		// 设置reducetask个数为0
		job.setNumReduceTasks(0);
 
		// 5 设置输入和输出路径
		FileInputFormat.setInputPaths(job, new Path(args[0]));
		FileOutputFormat.setOutputPath(job, new Path(args[1]));
 
		// 6 提交
		job.waitForCompletion(true);
	}
}

总结

1605198759162-3e4dc63f-21d1-4721-9b24-8c39097d0b18.png

1605198776489-dd8cffcb-ed88-47d7-9af7-1dc0fbdbdf4c.png

1605198794104-fd6a3b1f-adee-4eb0-a69c-dd76d67d915d.png

posted on 2025-10-13 01:12  chuchengzhi  阅读(9)  评论(0)    收藏  举报

导航

杭州技术博主,专注分享云计算领域实战经验、技术教程与行业洞察, 打造聚焦云计算技术的垂直博客,助力开发者快速掌握云服务核心能力。

褚成志 云计算 技术博客