MapTask的运行机制
MapTask运行机制详解以及Map任务的并行度
整个Map阶段流程大体如上图所示。简单概述:inputFile通过split被逻辑切分为多个split文件,通过Record按行读取内容给map(用户自己实现的)进行处理,数据被map处理结束之后交给OutputCollector收集器,对其结果key进行分区(默认使用hash分区),然后写入buffer,每个map task都有一个内存缓冲区,存储着map的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task结束后再对磁盘中这个map task产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task来拉数据。
详细步骤:
1、 首先,读取数据组件InputFormat(默认TextInputFormat)会通过getSplits方法对输入目录中文件进行逻辑切片规划得到splits,有多少个split就对应启动多少个MapTask。split与block的对应关系默认是一对一,一个文件被分为多少个split就取决于hdfs-site.xml中设置的block的大小。
2、 将输入文件切分为splits之后,由RecordReader对象(默认LineRecordReader)进行读取,以\n作为分隔符,读取一行数据,返回<key,value>。Key表示每行首字符偏移值,value表示这一行文本内容。
3、 读取split返回<key,value>,进入用户自己继承的Mapper类中,执行用户重写的map函数。RecordReader读取一行这里调用一次。
4、 map逻辑完之后,将map的每条结果通过context.write进行collect数据收集。在collect中,会先对其进行分区处理,默认使用HashPartitioner。
MapReduce提供Partitioner接口,它的作用就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数量取模。默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以订制并设置到job上。
5、接下来,会将数据写入内存,内存中这片区域叫做环形缓冲区,缓冲区的作用是批量收集map结果,减少磁盘IO的影响。我们的key/value对以及Partition的结果都会被写入缓冲区。当然写入之前,key与value值都会被序列化成字节数组。
环形缓冲区其实是一个数组,数组中存放着key、value的序列化数据和key、value的元数据信息,包括partition、key的起始位置、value的起始位置以及value的长度。环形结构是一个抽象概念。
缓冲区是有大小限制,默认是100MB。当map task的输出结果很多时,就可能会撑爆内存,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为Spill,中文可译为溢写。这个溢写是由单独线程来完成,不影响往缓冲区写map结果的线程。溢写线程启动时不应该阻止map的结果输出,所以整个缓冲区有个溢写的比例spill.percent。这个比例默认是0.8,也就是当缓冲区的数据已经达到阈值(buffer size * spill percent = 100MB * 0.8 = 80MB),溢写线程启动,锁定这80MB的内存,执行溢写过程。Map task的输出结果还可以往剩下的20MB内存中写,互不影响。
6、当溢写线程启动后,需要对这80MB空间内的key做排序(Sort)。排序是MapReduce模型默认的行为,这里的排序也是对序列化的字节做的排序。
如果job设置过Combiner,那么现在就是使用Combiner的时候了。将有相同key的key/value对的value加起来,减少溢写到磁盘的数据量。Combiner会优化MapReduce的中间结果,所以它在整个模型中会多次使用。
那哪些场景才能使用Combiner呢?从这里分析,Combiner的输出是Reducer的输入,Combiner绝不能改变最终的计算结果。Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景。比如累加,最大值等。Combiner的使用一定得慎重,如果用好,它对job执行效率有帮助,反之会影响reduce的最终结果。
7、合并溢写文件:每次溢写会在磁盘上生成一个临时文件(写之前判断是否有combiner),如果map的输出结果真的很大,有多次这样的溢写发生,磁盘上相应的就会有多个临时文件存在。当整个数据处理结束之后开始对磁盘中的临时文件进行merge合并,因为最终的文件只有一个,写入磁盘,并且为这个文件提供了一个索引文件,以记录每个reduce对应数据的偏移量。
至此map整个阶段结束。
MapTask阶段的调优设置
mapTask的一些基础设置配置(mapred-site.xml当中设置):
变量名 | 默认大小 | 设置作用 |
---|---|---|
mapreduce.task.io.sort.mb | 100 | 设置环型缓冲区的内存值大小 |
mapreduce.map.sort.spill.percent | 0.8 | 设置环型缓冲区溢写数据的比例 |
mapreduce.cluster.local.dir | ${hadoop.tmp.dir}/mapred/local | 设置溢写数据目录(默认设置) |
mapreduce.task.io.sort.factor | 10 | 设置一次最多合并多少个溢写文件 |
ReduceTask的运行机制
Reduce大致分为copy、sort、reduce三个阶段,重点在前两个阶段。copy阶段包含一个eventFetcher来获取已完成的map列表,由Fetcher线程去copy数据,在此过程中会启动两个merge线程,分别为inMemoryMerger和onDiskMerger,分别将内存中的数据merge到磁盘和将磁盘中的数据进行merge。待数据copy完成之后,copy阶段就完成了,开始进行sort阶段,sort阶段主要是执行finalMerge操作,纯粹的sort阶段,完成之后就是reduce阶段,调用用户定义的reduce函数进行处理。
详细步骤:
1、Copy阶段,简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求maptask获取属于自己的文件。
2、Merge阶段。这里的merge如map端的merge动作,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端的更为灵活。merge有三种形式:内存到内存;内存到磁盘;磁盘到磁盘。默认情况下第一种形式不启用。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的文件。
3、合并排序。把分散的数据合并成一个大的数据后,还会再对合并后的数据排序。
4、对排序后的键值对调用reduce方法,键相等的键值对调用一次reduce方法,每次调用会产生零个或者多个键值对,最后把这些输出的键值对写入到HDFS文件中。
MapReduceshuffle过程
map阶段处理的数据如何传递给reduce阶段,是MapReduce框架中最关键的一个流程,这个流程就叫shuffle。
shuffle: 洗牌、发牌——(核心机制:数据分区,排序,分组,规约,合并等过程)。
shuffle是Mapreduce的核心,它分布在Mapreduce的map阶段和reduce阶段。一般把从Map产生输出开始到Reduce取得数据作为输入之前的过程称作shuffle。
MapTask阶段的suffle过程:
1).Collect阶段:将MapTask的结果输出到默认大小为100M的环形缓冲区,保存的是key/value,Partition分区信息等。
2).Spill阶段:当内存中的数据量达到一定的阀值的时候,就会将数据写入本地磁盘,在将数据写入磁盘之前需要对数据进行一次排序的操作,如果配置了combiner,还会将有相同分区号和key的数据进行排序。
3).Merge阶段:把所有溢出的临时文件进行一次合并操作,以确保一个MapTask最终只产生一个中间数据文件。
ReduceTask阶段的suffle过程:
1).Copy阶段:ReduceTask启动Fetcher线程到已经完成MapTask的节点上复制一份属于自己的数据,这些数据默认会保存在内存的缓冲区中,当内存的缓冲区达到一定的阀值的时候,就会将数据写到磁盘之上。
2).Merge阶段:在ReduceTask远程复制数据的同时,会在后台开启两个线程对内存到本地的数据文件进行合并操作。
3).Sort阶段:在对数据进行合并的同时,会进行排序操作,由于MapTask阶段已经对数据进行了局部的排序,ReduceTask只需保证Copy的数据的最终整体有效性即可。(这里的排序要区别上自定义的排序,这里的排序是mapreduce自己对数据进行序列化,使得每个ReduceTask输出的数据都是有序,所以,当要对所有数据进行全局排序时,只能有一个ReduceTask,这个排序发生在ReduceTask阶段,而自定义的排序发生在MapTask到ReduceTask之间)。
Shuffle中的缓冲区大小会影响到mapreduce程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。缓冲区的大小可以通过参数调整, 参数:mapreduce.task.io.sort.mb 默认100M。
对于CDH5.14.0的hadoop的mapred-site.xml的设置可以详细参考http://archive.cloudera.com/cdh5/cdh/5/hadoop-2.6.0-cdh5.14.0/hadoop-mapreduce-client/hadoop-mapreduce-client-core/mapred-default.xml。
MapReduce运行过程中数据的压缩
数据压缩的目的
在shuffle阶段,可以看到数据通过大量的拷贝,从map阶段输出的数据,都要通过网络拷贝,发送到reduce阶段,这一过程中,涉及到大量的网络IO,如果数据能够进行压缩,那么数据的发送量就会少得多。
压缩算法的选择
首先,在Linux中cd到hadoop的安装目录,然后使用bin/hadoop checknative
,查看目前支持的压缩方式,一般的hadoop不会支持全部的压缩方式,需要使用CDH版本hadoop的源码进行编译后,使用编译得到的安装包进行安装才能支持全部的压缩方式。具有工具的压缩方式需要使用yum安装。
以下是hadoop的压缩方式的对比
压缩格式 | 工具 | 算法 | 文件扩展名 | 是否可切分 |
---|---|---|---|---|
DEFLATE | 无 | DEFLATE | .deflate | 否 |
Gzip | gzip | DEFLATE | .gz | 否 |
bzip2 | bzip2 | bzip2 | .bz2 | 是 |
LZO | lzop | LZO | .lzo | 否 |
LZ4 | 无 | LZ4 | .lz4 | 否 |
Snappy | 无 | Snappy | .snappy | 否 |
以上的方法中,Snappy是最优秀的,最常用的
启用压缩的方式
1、在mapr-site.xml中设置
<property>
<name>mapreduce.map.output.compress</name>
<value>true</value>
</property>
<property>
<name>mapreduce.map.output.compress.codec</name>
<value>org.apache.hadoop.io.compress.SnappyCodec</value>
</property>
2、在Java代码中设置
map阶段的输入数据压缩和reduce阶段的输出数据压缩均在主类的main方法进行设置
public static void main(String[] args) throws Exception
{
Configuration configuration=new Configuration();
//开启map阶段的数据压缩
configuration.set("mapreduce.map.output.compress","true"); //设置压缩开启
configuration.set("mapreduce.map.output.compress.codec",
"org.apache.hadoop.io.compress.SnappyCodec"); //设置的压缩方式用到的类的源码路径,这里用Snappy压缩
//开启reduce阶段的数据压缩
configuration.set("mapreduce.output.fileoutputformat.compress","true"); //设置压缩开启
configuration.set("mapreduce.output.fileoutputformat.compress.type","RECORD"); //设置压缩方式,这里是按行记录进行压缩
configuration.set("mapreduce.output.fileoutputformat.compress.codec",
"org.apache.hadoop.io.compress.SnappyCodec"); //设置压缩方式用到的类的源码路径,这里用Snappy压缩
System.exit(ToolRunner.run(configuration,new FlowSortRun(),args));
}
推荐使用Java代码中设置的方法。
Join算法
原始数据:
原始数据有两份,一份是订单数据(oeders)、一份是产品数据(products),可以看到orders与products之间有一个共同字段——形如p000n的这列数据,把它称为pid。
所谓的Join算法,其实就是实现select * from orders o left join products p on o.pid=p.pid;
。
方法一:在Reduce阶段对数据数据进行Join
这个方法的思想是利用reduce阶段的按key对数据进行分组操作,使pid作为传入reduce阶段的key2,然后reduce阶段会自动把pid相同的数据放到一个集合中,然后遍历每个key对应的集合,把value拼接起来,就实现了join操作
代码如下:
1、自定义Mapper,从数据中拿到pid,作为输出的key,整条数据作为输出的value
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
/*
需要注意的是,orders和peoducts中的pid在不同的列,
所以要按不同的文件,对数据进行不同的处理
*/
public class ReduceJoinMapper extends Mapper<LongWritable, Text,Text,Text>
{
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException
{
FileSplit inputSplit = (FileSplit) context.getInputSplit(); //获取数据所来源的split切片
String fileName = inputSplit.getPath().getName(); //获取split切片所来源的文件的文件名
//根据文件名决定如何取pid
String pid;
if (fileName.contains("orders"))
pid=value.toString().split(",")[2];
else
pid=value.toString().split(",")[0];
context.write(new Text(pid),value);
}
}
2、自定义reducer,把相同pid的数据的集合进行遍历,并且按一定格式输出
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class ReduceJoinReducer extends Reducer<Text,Text,Text, NullWritable>
{
@Override
protected void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException
{
String orderLine="";
String produceLine="";
for (Text v:values)
{
if (v.toString().startsWith("p"))
orderLine=v.toString();
else
produceLine=v.toString();
}
context.write(new Text(orderLine+"\t"+produceLine),
NullWritable.get());
}
}
3、主类
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
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.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
public class ReduceJoinRun extends Configured implements Tool
{
@Override
public int run(String[] strings) throws Exception
{
Job job= Job.getInstance(super.getConf(),"reduce-join");
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job,new Path("E:\\reduce_join\\input"));
job.setMapperClass(ReduceJoinMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Text.class);
job.setReducerClass(ReduceJoinReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job,new Path("E:\\reduce_join\\output"));
return job.waitForCompletion(true)?0:1;
}
public static void main(String[] args) throws Exception
{
System.exit(ToolRunner.run(new Configuration(),new ReduceJoinRun(),args));
}
}
方法二:把product做成HashMap,在Map阶段直接完成Join
这个方法的思想是,一般情况下,产品种类是远远少于订单数量的,所以可以把记录产品种类的文件,放到hdfs上作为缓存数据,在使用时,把这个文件的数据转为一个HashMap,直接存在内存即可,这样能大大提高程序的效率,并且在Map阶段就能够完成需求,所以也就不需要再让数据经过Reduce阶段了,直接输出数据即可,这样又能再次提高程序的效率。
1、首先,需要把product.txt发送给Linux机器,然后使用hdfs dfs -put
把文件上传到hdfs上
2、在主类中将hdfs上的product.txt,添加到程序的缓存文件中
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.filecache.DistributedCache;
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.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
import java.net.URI;
public class MapJoinRun extends Configured implements Tool
{
@Override
public int run(String[] strings) throws Exception
{
DistributedCache.addCacheFile(URI.create("hdfs://node01:8020/cachefiles/pdts.txt"), super.getConf());//添加缓存文件
Job job=Job.getInstance(super.getConf(),"map-join");
job.setInputFormatClass(TextInputFormat.class);
TextInputFormat.addInputPath(job,new Path("E:\\map_join\\input"));
job.setMapperClass(MapJoinMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
//由于不需要经过reduce阶段,所以不再设置reducer了
job.setOutputFormatClass(TextOutputFormat.class);
TextOutputFormat.setOutputPath(job,new Path("E:\\map_join\\output"));
return job.waitForCompletion(true)?0:1;
}
public static void main(String[] args) throws Exception
{
System.exit(ToolRunner.run(new Configuration(),new MapJoinRun(),args));
}
}
3、自定义Mapper,将缓存文件product.txt中的数据转为HashMap,并且组成新的数据进行输出
import org.apache.hadoop.filecache.DistributedCache;
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 MapJoinMapper extends Mapper<LongWritable, Text,Text, NullWritable>
{
private Map<String,String> products;
//在Mapper中如果需要读取或处理缓存数据,需要在setup方法中
@Override
protected void setup(Context context) throws IOException, InterruptedException
{
products=new HashMap<>();
//从context中获取在主类中加入的缓存文件的URI
URI[] cacheFiles=DistributedCache.getCacheFiles(context.getConfiguration());
//使用FileSystem登录hdfs,然后读取缓存文件
FileSystem hdfs=FileSystem.newInstance(URI.create("hdfs://node01:8020"),context.getConfiguration());
FSDataInputStream fsInputStream=hdfs.open(new Path(cacheFiles[0]));
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(fsInputStream));
//按行读取product.txt的数据,并且处理并加入到HashMap中
String line=null;
while((line=bufferedReader.readLine()) != null)
{
String pid=line.split(",")[0];
products.put(pid,line);
}
IOUtils.closeStream(bufferedReader);
hdfs.close();
}
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException
{
String pid=value.toString().split(",")[2];
value=new Text(value.toString()+"\t"+products.get(pid));
context.write(value,NullWritable.get());
}
}
涉及到这种少量数据join到大量数据中的情况,推荐使用在Map阶段进行join,可以大大提高效率。