Hadoop笔记——MapReduce分布式计算框架详解

一、概述


MapReduce是一个分布式运算程序的编程框架。这个框架提供的是一套对HDFS里面文件进行分析的编程思路,即MapReduce两步。通过MapReduce提供的接口,我们可以方便地编写实现一个分布式计算任务,MapReduce自带的组件会将我们的代码组装成一个分布式计算程序提交给Yarn进行处理。

  • 优点
    易于编程、良好的扩展性,增加机器就能扩展计算能力、高容错性、适合海量数据的离线计算和批处理。
  • 缺点
    不擅长实时计算;
    不擅长流式计算,即不适合处理动态数据;
    不擅长GAD(有向图)计算,即多步计算,否则MR会产生大量磁盘IO

二、Hadoop序列化


(一)序列化定义
序列化是将内存中的对象,转换成字节序列(或其他数据传输协议)以便存储到磁盘(持久化)和网络传输。
(二)Hadoop序列化优点
编写的MR程序在传输数据时必然要将对象进行序列化和反序列化。然而,Java提供的序列化框架Serializable是一个重量级框架,对象在序列化时会附带很多额外的信息(校验信息、Header、继承体等),序列化的对象需实现Serializable接口并通过DataInputStream/DataOutputStream传输数据。

为此,Hadoop使用自己的序列化框架Writable,提供了部分可序列化的基础类并支持自定义可序列化对象,常用的数据类型对应的Hadoop数据序列化类型如下:

Java类型Hadoop Writable类型
BooleanBooleanWritable
ByteByteWritable
IntIntWritable
FloatFloatWritable
LongLongWritable
DoubleDoubleWritable
StringText
MapMapWritable
ArrayArrayWritable

(三)自定义bean对象实现序列化接口(Writable)
具体实现bean对象序列化步骤如下:
(1)必须实现Writable接口
(2)反序列化时,需要反射调用空参构造函数,所以必须有空参构造

public FlowBean() {
	super();
}

(3)重写序列化方法

@Override
public void write(DataOutput out) throws IOException {
	out.writeLong(upFlow);
	out.writeLong(downFlow);
	out.writeLong(sumFlow);
}

(4)重写反序列化方法

@Override
public void readFields(DataInput in) throws IOException {
	upFlow = in.readLong();
	downFlow = in.readLong();
	sumFlow = in.readLong();
}

(5)注意反序列化的顺序和序列化的顺序完全一致
(6)要想把结果显示在文件中,需要重写toString()(TextOutputFormat特性决定),可用”\t”分开,方便后续用。
(7)如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口,重写compareTo()方法,因为MapReduce框架中的Shuffle过程要求对key必须能排序。

@Override
public int compareTo(FlowBean o) {
	// 倒序排列,从大到小
	return this.sumFlow > o.getSumFlow() ? -1 : 1;
}

三、WordCount案例


  1. 创建Maven工程
  2. 添加依赖
  • pom.xml
<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.apache.logging.log4j</groupId>
			<artifactId>log4j-core</artifactId>
			<version>2.8.2</version>
		</dependency>
		<dependency>
			<groupId>org.apache.hadoop</groupId>
			<artifactId>hadoop-common</artifactId>
			<version>2.7.2</version>
		</dependency>
		<dependency>
			<groupId>org.apache.hadoop</groupId>
			<artifactId>hadoop-client</artifactId>
			<version>2.7.2</version>
		</dependency>
		<dependency>
			<groupId>org.apache.hadoop</groupId>
			<artifactId>hadoop-hdfs</artifactId>
			<version>2.7.2</version>
		</dependency>
</dependencies>
  1. 编写Mapper类、Reducer类和Driver驱动类
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

public class WordcountMapper extends Mapper<LongWritable, Text, Text, IntWritable>{
	
	Text k = new Text();
	IntWritable v = new IntWritable(1);
	
	@Override
	protected void map(LongWritable key, Text value, Context context)	throws IOException, InterruptedException {
		
		// 1 获取一行
		String line = value.toString();
		
		// 2 切割
		String[] words = line.split(" ");
		
		// 3 输出
		for (String word : words) {
			
			k.set(word);
			context.write(k, v);
		}
	}
}
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

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

int sum;
IntWritable v = new IntWritable();

	@Override
	protected void reduce(Text key, Iterable<IntWritable> values,Context context) throws IOException, InterruptedException {
		
		// 1 累加求和
		sum = 0;
		for (IntWritable count : values) {
			sum += count.get();
		}
		
		// 2 输出
       v.set(sum);
		context.write(key,v);
	}
}
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 获取配置信息以及封装任务
		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);
	}
}

在Hadoop集群上运行MR程序,需将程序打包成jar包。
添加maven依赖(注意修改其中Driver类的全类名):

  • pom.xml
<build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <artifactId>maven-assembly-plugin </artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <mainClass>com.bessen.mapreduce.WordCountDriver</mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

然后导出jar包:
在这里插入图片描述

在Hadoop集群上运行WordCount程序:

 $ hadoop jar  MyWordCount.jar com.bessen.mapreduce.WordCountDriver /input /output

四、MapReduce工作流程


一个MapReduce程序就是一个job,而Driver类相当于Yarn的客户端,Driver将MR程序的各项配置和数据切片信息提交给Yarn,Yarn会创建一个MR appmaster,负责整个job的资源调度。一个job包括mapreduce两步,具体流程如下。

(一)Map阶段详细流程

Map阶段

  1. 准备数据
  2. 切片
    切片数决定了MapTask任务数量,一般默认切片大小为数据块大小,切片机制和文件分块机制类似。默认由FileInputFormat负责切片,为避免小文件问题导致切片过多,可以换成CombineTextInputFormat
  3. 提交job信息
    Driver向Yarn提交job的各类配置和切片信息,Yarn生成对应的Mr appmaster管理该job后续的一切。
  4. 根据切片数生成对应个数的MapTask
  5. 读入数据
    MR默认使用TextInputFormat,按行的方式读取数据,该行在整个文件的起始偏移量为键,整行数据(不包括终止符)为值,也可以在Driver类中配置使用其他方式读取。
  6. 循环对每行数据进行map逻辑运算
  7. 收集器(OutputCollector)将输出的键值对写入环形缓冲区
    (该环形缓冲区位于内存,用于过渡,一侧存储元数据,包括索引、分区、k、v起始位置,另一侧存储实际的k、v数据,环形缓冲区默认大小为100M,占用80%后溢写到磁盘。)
  8. 持续对环形缓冲区溢写的小部分数据进行分区以及分区内排序
  9. 分区、排序后的数据溢写到磁盘
  10. 对磁盘的数据进一步归并(Merge )排序(毕竟之前每次只排序一小部分数据),归并其实就是继续完成之前没做完的分区和区内排序工作。
  11. Combiner合并
    这一步只有在Driver类中配置过的情况下才会进行。Reduce阶段是收集所有Map输出后进行合并操作,而Combiner相当于将合并提前,即对单个MapTask的数据进行提前合并。

(二)Reduce阶段流程

Reduce阶段
12. 根据分区数决定ReduceTask的个数
ReduceTask个数也可以自己设置,在Driver类中添加:

// 默认值是1,手动设置为3
job.setNumReduceTasks(3);
  1. 各ReduceTask收集数据,将所有对应分区的数据下载到磁盘,合并文件并排序
  2. 循环对key相同的每组数据进行reduce逻辑运算
  3. 通过OutputFormat(默认TextOutputFormat)写出键值对。

五、InputFormat和OutputFormat


MapReduce框架的输入输出采用键值对的格式,这其中就涉及到了框架自带的InputFormat和OutputFormat,根据不同需求对key和value的选择,可以使用MR自带的几个实现类,也可以自定义InputFormat和OutputFormat.

(一)几种FileInputFormat的子类

  • TextInputFormat
    默认使用的类,按行读取数据,该行在整个文件的起始字节偏移量为键,LongWritable类型,该行内容为值,不包括终止符(回车和换行符),Text类型。
  • KeyValueTextInputFormat
    按分隔符将一行分隔为key、value,设置使用该类读取数据,需在Driver类中添加:
// 设置切割符
configuration.set(KeyValueLineRecordReader.KEY_VALUE_SEPERATOR,"\t");

// 设置输入格式
job.setInputFormatClass(KeyValueTextInputFormat.class);
  • NLineInputFormat
    使用NLineInputFormat时,切片机制将改变为N行一个切片。数据读取方法与TextInputFormat保持一致。在Driver类中设置方法如下:
// 设置3行为一个切片(InputSplit)
NLineInputFormat.setNumLinesPerSplit(job, 3);
          
// 设置输入格式
job.setInputFormatClass(NLineInputFormat.class);
  • 自定义InputFromat
    步骤:
  1. 自定义RecordReader类继承RecordReader
  2. 自定义InputFromat类继承FileInputFromat
  3. 在输出时使用SequenceFileOutputFormat输出合并文件
  4. 在Driver类中设置:
// 7设置输入的inputFormat
job.setInputFormatClass(MyFileInputformat.class);

// 设置输出的outputFormat
job.setOutputFormatClass(SequenceFileOutputFormat.class);

(二)几种FileOutputFormat的子类

MapReduce计算任务最终得到的是键值对类型的数据,在Reducer类里我们直接写一个write()方法进行输出。

context.write(key, value);

但这些数据最终是要输出到磁盘文件里面的,背后负责写出数据的就是OutputFormat。

  • TextOutputFormat
    MR默认使用的是TextOutputFormat,会直接调用key和value的toString()方法,把输出的一条条数据写成文本行,格式如下:
    在这里插入图片描述
  • SequenceFileOutputFormat
    MR还提供了另一个子类SequenceFileOutputFormat,该类输出的格式紧凑,容易压缩,一般使用这个类时,输出作为下一个MapReduce任务的输入。
  • 自定义OutputFormat
    如果我们不喜欢TextOutputFormat提供的输出格式,而是想要输出其他的格式,例如对数据简单处理、将结果输出到数据库等,我们也可以自定义OutputFormat,其实就是自己写个类定义输出规则,告诉MapReduce拿到最终key和value后怎么操作。具体步骤如下:
  1. 自定义一个类继承RecordWriter,重写其中的构造方法和write()、close()方法。
  2. 自定义一个类继承FileOutputFormat,重写getRecordWriter()方法
@Override
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job)			throws IOException, InterruptedException {

	// 返回一个RecordWriter
	return new MyRecordWriter(job);
}
  1. 在Driver类中添加:
// 要将自定义的输出格式组件设置到job中
job.setOutputFormatClass(MyOutputFormat.class);

六、Shuffle机制


在前面的MapReduce工作流程中,每个MapTask,在map之后的写入环形缓冲区、分区、区内排序、溢写磁盘、归并、(合并)、压缩的整个过程称为Shuffle(洗牌)。

(一)分区(Partition)

分区数直接决定了ReduceTask个数,MapReduce默认使用HashPartitioner进行分区,分区个数默认为1,通常我们需要自定义分区。
自定义分区步骤如下:
自定义一个类继承Partitioner,重写getPartition()方法。

public class MyPartitioner extends Partitioner<Text, TEXT> {

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

		// 分区逻辑
		// ...
		return partition;
	}
}

在Driver中设置:

job.setPartitionerClass(MyPartitioner.class);

//根据分区逻辑设置ReduceTask数量
job.setNumReduceTasks(3);

(二)排序

Map和Reduce阶段都要进行排序,如MapTask每次溢写磁盘前的区内排序、Map阶段最终的归并排序、ReduceTask从各MapTask节点获取输出后进行的归并排序等。
MapReduce默认对key进行字典排序。为此,如果想要自定义排序,就需要让key对应的Bean对象具备排序功能,具体过程是:
让自定义的Bean对象实现WritableComparable接口并重写compareTo方法。

@Override
public int compareTo(FlowBean bean) {

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

	return result;
}

(三)合并(Combiner)

ReduceTask需要收集所有MapTask节点对应分区的输出,进行汇总操作。如果Map阶段输出的数据量很大,必然会耗费大量的IO和网络传输资源。
如果任务在Map阶段进行提前汇总不会影响最终结果,则可以使用Combiner进行合并,事实上,Combiner组件的父类就是Reducer。
自定义Combiner的步骤:

  1. 自定义一个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));
	}
}
  1. 在Driver驱动类中设置:
job.setCombinerClass(WordcountCombiner.class);

(四)分组排序/辅助排序(GroupingComparator)

Reduce阶段,相同key的数据会进入同一个reduce方法进行汇总,但在很多情况下,我们需要让相同组的数据进入一个reduce方法,同一组的数据key不一定相同,这时候就需要在reduce前添加分组排序。
分组排序的过程是按照key对输入Reduce阶段的数据进行分组,分组后,同一组的数据进入同一个reduce方法,设置如下:

  1. 自定义类继承WritableComparator,重写compare()方法并创建一个构造方法将比较对象的类传给父类
protected OrderGroupingComparator() {
	super(OrderBean.class, true);
}
@Override
public int compare(WritableComparable a, WritableComparable b) {
		
	// 比较的业务逻辑,a、b同一组时返回值为0
	// ...
		
	return result;
}
  1. 在Driver类中设置
// 设置reduce端的分组
job.setGroupingComparatorClass(MyComparator.class);

七、Reduce Join和Map Join


其实,Map Join和Reduce Join没有什么新的东西,不像排序、分组、合并这些有MR框架提供的接口或父类,前面的内容是整个MR框架的基础,Map Join和Reduce Join只是两种实现join的逻辑思路。
首先回顾join,如果表的设计过程合乎规范,需要join的两张表中,一张表的外键应该是另一张表的主键,如下图所示:
自然连接

(一)Reduce Join

Reduce Join的思路是在Map阶段的setup()初始化方法中先区分是哪张表,并在map方法里面打上标签。Map阶段的输出以外键B为key,这样就可以在Reduce阶段将两张表属性B相同的数据汇总到一起进行join操作了。

  • 在Mapper类的setup()方法中获取表名:
	String name;
	
	@Override
	protected void setup(Context context) throws IOException, InterruptedException {

		// 1 获取输入文件切片
		FileSplit split = (FileSplit) context.getInputSplit();

		// 2 获取输入文件名称
		name = split.getPath().getName();
	}

(二)Map Join

如果两张表中有一个是小表,那么可以直接让每一个MapTask都缓存一份小表到内存中,这样一来,在Map阶段就能直接完成join,也就不需要后面的Shuffle和Reduce过程了。

  • 在Driver类中设置缓存文件路径并设置reduceTask个数为0:
// 设置缓存数据路径,HDFS路径为hdfs://
job.addCacheFile(new URI("file:///缓存文件路径"));
		
// 设置reduceTask数量为0,取消reduce阶段
job.setNumReduceTasks(0);
  • 在Mapper类的setup()方法中缓存小表到HashMap:
	Map<String, String> hashMap = new HashMap<>();
	
	@Override
	protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {

		// 1 获取第0个缓存的文件
		URI[] cacheFiles = context.getCacheFiles();
		String path = cacheFiles[0].getPath().toString();
		
		BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "UTF-8"));
		
		String line;
		while(StringUtils.isNotEmpty(line = reader.readLine())){

			// 2 切割
			String[] fields = line.split("\t");
			
			// 3 缓存数据到集合
			hashMap.put(fields[0], fields[1]);
		}
		
		// 4 关流
		reader.close();
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值