文章目录
- Hadoop进阶篇
-
- MapReduce:Hadoop分布式并行计算框架
-
- MapReduce的理解
- MapReduce的核心思想
- MapReduce 编程模型
- MapReduce编程指导思想【八大步骤】
-
- Map 阶段 2 个步骤
- shuffle 阶段 4 个步骤
- reduce 阶段 2 个步骤
- MapReduce编程入门——单词统计
-
- MapReduce的运行模式
-
- Map Task数量及切片机制
-
- 1. MapTask个数
- 2. 如何控制 mapTask 的个数
- MapReduce 的 InputFormat
-
- 1. FileInputFormat常用类介绍
- 2. 使用CombineTextInputFormat实现切片个数控制
-
- 3. CombineTextInputFormat 示例
- 4. KeyValueTextInputFormat 示例
- 5. NlineInputFormat 示例
- 自定义InputFormat
-
- 第一步:自定义 RecordReader
- 第二步:自定义 InputFormat
- 第三步:定义测试类
Hadoop进阶篇
MapReduce:Hadoop分布式并行计算框架
MapReduce的理解
- MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。
- MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上。
MapReduce的核心思想
- MapReduce 的核心思想是:分而治之。适用于大量复杂的任务处理场景(大规模数据处理场景)。即使是发布过论文实现分布式计算的谷歌也只是实现了这种思想,而不是自己原创。
- Map负责“分”,即把复杂的任务分解为若干个“简单的任务”来并行处理。可以进行拆分的前提是这些小任务可以并行计算,彼此间几乎没有依赖关系。 Reduce负责“合”,即对map阶段的结果进行全局汇总。这两个阶段合起来正是MapReduce思想的体现。
- 举例:我们要数图书馆中的所有书。你数1号书架,我数2号书架。这就是“Map”。我们人越多,数书就越快。然后把所有人的统计数加在一起。这就是“Reduce”。
MapReduce 编程模型
- MapReduce是采用一种分而治之的思想设计出来的分布式计算框架,那什么是分而治之呢?
- 比如一复杂、计算量大、耗时长的的任务,暂且称为“大任务”;
- 此时使用单台服务器无法计算或较短时间内计算出结果时,可将此大任务切分成一个个小的任务,小任务分别在不同的服务器上并行的执行;
- 最终再汇总每个小任务的结果。
- MapReduce 由两个阶段组成:
- Map 阶段:切分成一个个小的任务
- Reduce 阶段:汇总小任务的结果
- Map 阶段:
- map 阶段有一个关键的 map() 函数;
- 此函数的输入是键值对
- 输出是一系列键值对,输出写入本地磁盘。
- Reduce 阶段:
- reduce 阶段有一个关键的函数 reduce() 函数;
- 此函数的输入也是键值对,即 map 的输出(KV对);
- 输出也是一系列键值对,结果最终写入 HDFS。
- Map&Reduce:
MapReduce编程指导思想【八大步骤】
- 通过 MapReduce 编程模型总结,进行 MapReduce 开发一共有八大步骤,其中:
- map 阶段分为 2 个步骤;
- shuffle 阶段分为 4 个步骤;
- reduce 阶段分为 2 个步骤。
Map 阶段 2 个步骤
- 第一步:设置 InputFormat 类,将数据切分成 Key、Value 对,此 K-V 对作为第二步的输入;
- 第二步:自定义 map 逻辑,处理我们第一步传过来的 K-V 对数据,然后转换成新的 Key、Value 对,并输出。
shuffle 阶段 4 个步骤
- 第三步:对上一步输出的 K-V 对进行分区,相同 Key 的 K-V对属于同一分区;
- 第四步:对每个分区的数据按照 Key 进行排序;
- 第五步:对分区中的数据进行规约(combine 操作),降低数据的网络拷贝【可选步骤】;
- 第六步:对排序后的 K-V 对数据进行分组,分组过程中,key 相同的 K-V 对为一组,将同一组的 K-V 对的所有 value 放到一个集合当中,每组数据调用一次 reduce 方法。
reduce 阶段 2 个步骤
- 第七步:对多个 map 的任务进行合并、排序、写 reduce 函数自己的逻辑,对输入的 key、value 对进行处理,转换成新的 key、value 对进行输出;
- 第八步:设置将输出的 key、value 对数据保存到文件中。
MapReduce编程入门——单词统计
hadoop 当中常用的数据类型
- hadoop没有沿用java当中基本的数据类型,而是自己进行封装了一套数据类型,其自己封装的类型与java的类型对应如下,下表是常用的数据类型对应的Hadoop数据序列化类型
Java类型 |
Hadoop Writable类型 |
boolean |
BooleanWritable |
byte |
ByteWritable |
int |
IntWritable |
float |
FloatWritable |
long |
LongWritable |
double |
DoubleWritable |
String |
Text |
Map |
MapWritable |
Array |
ArrayWritable |
byte[] |
BytesWritable |
词频统计
- 需求:现有数据格式如下,每一行数据之间都是使用逗号进行分割,求取每个单词出现的次数。
- 定义 Mapper 类:
public class MyMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private IntWritable intWritable = new IntWritable(1);
private Text text = new Text();
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String line = value.toString();
String[] words = line.split(",");
for (String word : words) {
text.set(word);
context.write(text, intWritable);
}
}
}
public class MyReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int result = 0;
for (IntWritable value : values) {
result += value.get();
}
IntWritable intWritable = new IntWritable(result);
context.write(key, intWritable);
}
}
public class WordCounter extends Configured implements Tool {
@Override
public int run(String[] args) throws Exception {
Configuration conf = super.getConf();
Job job = Job.getInstance(conf, WordCounter.class.getSimpleName());
FileSystem fs = FileSystem.get(conf);
if (fs.exists(new Path(args[1]))) {
fs.delete(new Path(args[1]), true);
}
job.setJarByClass(WordCounter.class);
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job, new Path(args[0]));
job.setMapperClass(MyMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
job.setReducerClass(MyReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job, new Path(args[1]));
job.setNumReduceTasks(Integer.parseInt(args[2]));
boolean result = job.waitForCompletion(true);
return result ? 0 : 1;
}
public static void main(String[] args) throws Exception {
Configuration configuration = new Configuration();
int run = ToolRunner.run(configuration, new WordCounter(), args);
System.exit(run);
}
}
hadoop jar hadoop-demo-1.0.jar com.yw.hadoop.mr.WordCounter /1.txt /wordcount01 3
MapReduce的运行模式
1. 本地模式
- MapReduce 程序是被提交给 LocalJobRunner 在本地以单进程的形式运行,而处理的数据及输出结果可以在本地文件系统,也可以在hdfs上。
- 怎样实现本地运行?写一个程序,不要带集群的配置文件。本质是程序的conf中是否有mapreduce.framework.name=local以及yarn.resourcemanager.hostname=local参数。
- 本地模式非常便于进行业务逻辑的debug
configuration.set("mapreduce.framework.name","local");
configuration.set("yarn.resourcemanager.hostname","local");
TextInputFormat.addInputPath(job,new Path("input"));
TextOutputFormat.setOutputPath(job,new Path("output"));
2. 集群运行模式
- 将 MapReduce 程序提交给 yarn 集群,分发到很多的节点上并发执行,处理的数据和输出结果应该位于hdfs文件系统。
- 提交集群的实现步骤:将程序打成JAR包,然后在集群的任意一个节点上用hadoop命令启动
yarn jar hadoop-demo-1.0.jar com.yw.hadoop.mr.WordCounter /1.txt /wordcount01 3
Map Task数量及切片机制
1. MapTask个数
- 在运行我们的MapReduce程序的时候,我们可以清晰的看到会有多个mapTask的运行,那么 mapTask 的个数究竟与什么有关?
- 是不是 Map Task 越多越好,或者说是不是 mapTask 的个数越少越好呢?
- 我们可以通过MapReduce的源码进行查看 mapTask 的个数究竟是如何决定的。
- 在MapReduce当中,每个mapTask处理一个切片split的数据量,注意切片与block块的概念很像,但是block块是HDFS当中存储数据的单位,切片split是MapReduce当中每个MapTask处理数据量的单位。
- MapTask并行度决定机制:
- 数据块:Block 是 HDFS 物理上把数据分成一块一块
- 数据切片:只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储
- 查看 FileInputFormat 的源码,里面 getSplits 的方法便是获取所有的切片,其中有个方法便是获取切片大小
protected long computeSplitSize(long blockSize, long minSize, long maxSize) {
return Math.max(minSize, Math.min(maxSize, blockSize));
}
- 由以上计算公式可以推算出split切片的大小刚好与block块相等。
- 那么hdfs上面如果有以下两个文件,文件大小分别为300M和10M,那么会启动多少个MapTask?
file1.txt 300M
file2.txt 10M
- 经过FileInputFormat的切片机制运算后,形成的切片信息如下:一共就会有 4 个切片,与我们 block 块的个数刚好相等
file1.txt.split1-- 0~128
file1.txt.split2-- 128~256
file1.txt.split3-- 256~300
file2.txt.split1-- 0~10M
- 如果有 1000 个小文件,每个小文件是 1KB~100MB 之间,那么我们启动 1000 个 MapTask 是否合适,该如何合理的控制 MapTask 的个数?
2. 如何控制 mapTask 的个数
- 如果需要控制 mapTask 的个数,我们只需调整 minSize 和 maxSize 这两个值,那么切片的大小就会改变,切片大小改变之后,mapTask的个数就会改变:
- maxSize(切片最大值):如果比 blockSize 小,则会让切片变小,而且就等于配置这个参数的值;
- minSize(切片最小值):如果比 blockSize 大,则可以让切片变得比 blockSize 还大
MapReduce 的 InputFormat
- InputFormat 是 MapReduce 当中用于处理数据输入的一个组件,是最顶级的一个抽象父类,主要用于解决各个地方的数据源的数据输入问题。
1. FileInputFormat常用类介绍
- FileInputFormat类也是InputFormat的一个子类。如果需要操作hdfs上面的文件,基本上都是通过FileInputFormat类来实现的,我们可以通过FileInputFormat来实现各种格式的文件操作
- FileInputFormat的子实现类的UML类图如下:
类名 |
主要作用 |
TextInputFormat |
读取文本文件 |
CombineFileInputFormat |
在 MR 当中用于合并小文件,将多个小文件合并之后只需要启动换一个mapTask进行运行 |
SequenceFileInputFormat |
处理SequenceFile这种格式的数据 |
KeyValueTextInputFormat |
通过手动指定分隔符,将每一条数据解析成为key,value对类型 |
NLineInputFormat |
指定数据的行数作为一个切片 |
FixedLengthInputFormat |
文件的每个record是固定的长度,用于读取固定的二进制记录 |
2. 使用CombineTextInputFormat实现切片个数控制
- 框架默认的TextInputFormat切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个MapTask。这样如果有大量小文件,就会产生大量的MapTask,处理效率极其低下。
- CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
- 虚拟存储切片最大值设置
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
。注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
切片机制
- 生成切片过程包括 ① 虚拟存储过程,② 切片过程。
虚拟存储过程
- 将输入目录下所有文件按照文件名称字典顺序排序,将每个文件的大小,依次和设置的setMaxInputSplitSize值比较:
- ①如果不大于设置的最大值,逻辑上划分一个虚拟存储块;
- ②如果输入文件大于最大值,小于最大值的2倍,那么会将文件平分为2个虚拟存储块;
- ③如果输入文件大于最大值的两倍,那么以最大值为单位切割出虚拟存储块;
- 当剩余数据大小大于设置的最大值,且小于等于最大值2倍时,此时将剩余数据均分成2个虚拟存储块(防止出现太小切片)。
- 举个例子:setMaxInputSplitSize值为4M
- 例子一:输入文件大小为8.02M,则先逻辑上分成一个4M,剩余的大小为4.02M。如果按照4M逻辑划分,就会出现0.02M的小的虚拟存储文件,所以将剩余的4.02M文件切分成(2.01M和2.01M)两个虚拟存储文件。
- 例子二:输入文件大小为6.02M,那么4 < 6.02 < 8,生成两个虚拟存储文件3.01、3.01
切片过程
- ①判断虚拟存储的文件大小是否大于setMaxInputSplitSize值,大于等于则单独形成一个切片;
- ②如果不大于,则跟下一个虚拟存储文件进行合并;如果合并后,还不大于setMaxInputSplitSize,则继续与下一个虚拟存储文件进行合并;当合并后,大于setMaxInputSplitSize后,共同形成一个切片。
- 举个例子:
- 有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
3. CombineTextInputFormat 示例
job.setInputFormatClass(CombineTextInputFormat.class);
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);
CombineTextInputFormat.addInputPath(job, new Path(args[0]));
- 将我们的切片设置成为4M大小,然后重新打包运行,观察mapTask的个数
4. KeyValueTextInputFormat 示例
- KeyValueTextInputFormat允许我们自己来定义分隔符,通过分隔符来自定义我们的key和value,参见下面的数据,数据之间的分隔符为@zolen@ 数据内容如下
hello@zolen@ input datas today
count@zolen@ hadoop spark
hello@zolen@ input some datas to test
hello 2
count 1
- 查看 KeyValueLineRecordReader 的源码,发现切割参数的配置:
public class KeyValueMain {
static class KeyValueMapper extends Mapper<Text, Text, Text, LongWritable> {
private LongWritable outvalue = new LongWritable(1);
@Override
protected void map(Text key, Text value, Context context) throws IOException, InterruptedException {
context.write(key, outvalue);
}
}
static class KeyValueReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
long result = 0;
for (LongWritable value : values) {
result += value.get();
}
context.write(key, new LongWritable(result));
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
conf.set("key.value.separator.in.input.line", "@zolen@");
Job job = Job.getInstance(conf);
job.setJarByClass(KeyValueMain.class);
job.setInputFormatClass(KeyValueTextInputFormat.class);
KeyValueTextInputFormat.addInputPath(job, new Path(args[0]));
job.setMapperClass(KeyValueMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
job.setReducerClass(KeyValueReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job, new Path(args[1]));
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
5. NlineInputFormat 示例
- NlineInputFormat 允许我们自己定义输入的行数作为一个切片数据
- 代码实现:
public class NLineMain {
static class NLineMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] words = value.toString().split(",");
for (String word : words) {
context.write(new Text(word), new LongWritable(1));
}
}
}
static class NLineReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
long wordNum = 0L;
for (LongWritable value : values) {
wordNum += value.get();
}
context.write(key, new LongWritable(wordNum));
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
job.setJarByClass(NLineMain.class);
NLineInputFormat.setNumLinesPerSplit(job, 3);
job.setInputFormatClass(NLineInputFormat.class);
NLineInputFormat.addInputPath(job, new Path(args[0]));
job.setMapperClass(NLineMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(LongWritable.class);
job.setReducerClass(NLineReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(LongWritable.class);
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job, new Path(args[1]));
job.waitForCompletion(true);
}
}
自定义InputFormat
- MapReduce 框架当中已经给我们提供了很多的文件输入类,用于处理文件数据的输入,如果 MapReduce 提供的文件数据类还不够用的话,我们也可以通过自定义 InputFormat 来实现文件数据的输入
- 需求:现在有大量的小文件,我们通过自定义 InputFormat 实现将小文件全部读取,然后输出成为一个 SequenceFile 格式的大文件,进行文件的合并
第一步:自定义 RecordReader
public class MyRecordReader extends RecordReader<NullWritable, BytesWritable> {
private FileSplit fileSplit;
private Configuration configuration;
private BytesWritable bytesWritable;
private boolean flag = false;
@Override
public void initialize(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
this.fileSplit = (FileSplit) split;
this.configuration = context.getConfiguration();
this.bytesWritable = new BytesWritable();
}
@Override
public boolean nextKeyValue() throws IOException, InterruptedException {
if (!flag) {
int length = (int) fileSplit.getLength();
byte[] splitContent = new byte[length];
Path path = fileSplit.getPath();
FileSystem fileSystem = path.getFileSystem(configuration);
FSDataInputStream fsdis = fileSystem.open(path);
IOUtils.readFully(fsdis, splitContent, 0, length);
bytesWritable.set(splitContent, 0, length);
flag = true;
IOUtils.closeStream(fsdis);
return true;
}
return false;
}
@Override
public NullWritable getCurrentKey() throws IOException, InterruptedException {
return NullWritable.get();
}
@Override
public BytesWritable getCurrentValue() throws IOException, InterruptedException {
return bytesWritable;
}
@Override
public float getProgress() throws IOException, InterruptedException {
return flag ? 1.0f : 0.0f;
}
@Override
public void close() throws IOException {
}
}
第二步:自定义 InputFormat
public class MyInputFormat extends FileInputFormat<NullWritable, BytesWritable> {
@Override
public RecordReader<NullWritable, BytesWritable> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
MyRecordReader recordReader = new MyRecordReader();
recordReader.initialize(split, context);
return recordReader;
}
@Override
protected boolean isSplitable(JobContext context, Path filename) {
return false;
}
}
第三步:定义测试类
public class MyInputFormatMain extends Configured implements Tool {
static class MyMapper extends Mapper<NullWritable, BytesWritable, Text, BytesWritable> {
@Override
protected void map(NullWritable key, BytesWritable value, Context context) throws IOException, InterruptedException {
FileSplit inputSplit = (FileSplit) context.getInputSplit();
String name = inputSplit.getPath().getName();
context.write(new Text(name), value);
}
}
@Override
public int run(String[] args) throws Exception {
Job job = Job.getInstance(super.getConf(), "mergeSmallFile");
job.setJarByClass(MyInputFormatMain.class);
job.setInputFormatClass(MyInputFormat.class);
MyInputFormat.addInputPath(job, new Path(args[0]));
job.setMapperClass(MyMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(BytesWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(BytesWritable.class);
job.setOutputFormatClass(SequenceFileOutputFormat.class);
SequenceFileOutputFormat.setOutputPath(job, new Path(args[1]));
return job.waitForCompletion(true) ? 0 : 1;
}
public static void main(String[] args) throws Exception {
int run = ToolRunner.run(new Configuration(), new MyInputFormatMain(), args);
System.exit(run);
}
}
- github 源代码地址:https://github.com/shouwangyw/bigdata/tree/master/hadoop-demo