导读
HDFS作为Hadoop生态系统的分布式文件系统,设计是用来存储海量数据,特别适合存储TB、PB量级别的数据。但是随着时间的推移或者处理程序的问题,HDFS上可能会存在大量的小文件,进而消耗NameNode大量的内存,并且延长程序的运行时间。下面我就把对小文件的处理经验总结一下,供大家参考。
引言
先来了解一下Hadoop中何为小文件:小文件指的是那些文件大小要比HDFS的块大小(在Hadoop1.x的时候默认块大小64MB,可以通过dfs.blocksize来设置;但是到了Hadoop 2.x的时候默认块大小为128MB了,可以通过dfs.block.size设置)小的多的文件。而HDFS的问题在于无法很有效的处理大量小文件。在HDFS中,任何一个文件、目录和block,在HDFS中都会被表示为一个object存储在Namenode的内存中,每一个object占用150 bytes的内存空间。所以,如果有10million个文件,每一个文件对应一个block,那么就将要消耗Namenode 3G的内存来保存这些block的信息。如果规模再大一些,那么将会超出现阶段计算机硬件所能满足的极限。不仅如此,HDFS并不是为了有效的处理大量小文件而存在的。它主要是为了流式的访问大文件而设计的。对小文件的读取通常会造成大量从Datanode到Datanode的seeks和hopping来retrieve文件,而这样是非常的低效的一种访问方式。
一、概述
HDFS存储特点:
(1)流式读取方式,主要是针对一次写入,多次读出的使用模式。写入的过程使用的是append的方式。
(2)设计目的是为了存储超大文件,主要是针对几百MB,GB,TB,甚至PB的文件。
(3)该分布式系统构建在普通PC机组成的集群上,大大降低了构建成本,并屏蔽了系统故障,使得用户可以专注于自身的操作运算。
(4)HDFS适用于高吞吐量,而不适合低时间延迟的访问。如果同时存入1million的files,那么HDFS 将花费几个小时的时间。
(5)流式读取的方式,不适合多用户写入,以及任意位置写入。如果访问小文件,则必须从一个Datanode跳转到另外一个Datanode,这样大大降低了读取性能。
二、HDFS文件操作流程
HDFS体系结构
HDFS采用master/slave架构。一个HDFS集群是由一个Namenode和一定数目的Datanodes组成。Namenode是一个中心服务器,负责管理文件系统的名字空间(namespace)以及客户端对文件的访问。集群中的Datanode一般是一个节点一个,负责管理它所在节点上的存储。HDFS暴露了文件系统的名字空间,用户能够以文件的形式在上面存储数据。从内部看,一个文件其实被分成一个或多个数据块,这些块存储在一组Datanode上。Namenode执行文件系统的名字空间操作,比如打开、关闭、重命名文件或目录。它也负责确定数据块到具体Datanode节点的映射。Datanode负责处理文件系统客户端的读写请求。在Namenode的统一调度下进行数据块的创建、删除和复制。
HDFS文件的读取
转存失败重新上传取消
(1)client端发送读文件请求给Namenode,如果文件不存在,返回错误信息,否则,将该文件对应的block及其所在Datanode位置发送给client
(2)client收到文件位置信息后,与不同Datanode建立socket连接并行获取数据。
HDFS文件的写入
转存失败重新上传取消
(1)client端发送写文件请求,Namenode检查文件是否存在,如果已存在,直接返回错误信息,否则,发送给client一些可用Datanode节点
(2)client将文件分块,并行存储到不同节点上Datanode上,发送完成后,client同时发送信息给Namenode和Datanode
(3)Namenode收到的client信息后,发送确信信息给Datanode
(4)Datanode同时收到Namenode和Datanode的确认信息后,提交写操作。
三、HDFS小文件解决方案
1、 HDFS上的小文件问题
现象:在现在的集群上已经存在了大量的小文件和目录。
方案:文件是许多记录(Records)组成的,那么可以通过调用HDFS的sync()方法和append方法结合使用,每隔一定时间生成一个大文件。或者可以通过写一个程序来来合并这些小文件。
2、 MapReduce上的小文件问题
现象:
Map任务(task)一般一次处理一个块大小的输入(input)(默认使用FileInputFormat)。如果文件非常小,并且拥有大量的这种小文件,那么每一个map task都仅仅处理非常小的input数据,因此会产生大量的map tasks,每一个map task都会额外增加bookkeeping开销。一个1GB的文件,拆分成16个块大小文件(默认block size为64M),相对于拆分成10000个100KB的小文件,后者每一个小文件启动一个map task,那么job的时间将会十倍甚至百倍慢于前者。
方案:
I**、Hadoop Archive:**
Haddop Archive是一个高效地将小文件放入HDFS块中的文件存档工具,它能够将多个小文件打包成一个HAR文件,这样同时在减少Namenode的内存使用。
II**、Sequence file:**
sequence file由一系列的二进制key/value组成。key为小文件名,value为文件内容,可以将大批小文件合并成一个大文件。
I、II这里不做介绍可以参考(http://blog.cloudera.com/blog/2009/02/the-small-files-problem)
III**、CombineFileInputFormat:**
Hadoop内置提供了一个 CombineFileInputFormat 类来专门处理小文件,其核心思想是:根据一定的规则,将HDFS上多个小文件合并到一个 InputSplit中,然后会启用一个Map来处理这里面的文件,以此减少MR整体作业的运行时间。CombineFileInputFormat类继承自FileInputFormat,主要重写了List getSplits(JobContext job)方法;这个方法会根据数据的分布,mapreduce.input.fileinputformat.split.minsize.per.node、mapreduce.input.fileinputformat.split.minsize.per.rack以及mapreduce.input.fileinputformat.split.maxsize 参数的设置来合并小文件,并生成List。其中mapreduce.input.fileinputformat.split.maxsize参数至关重要,如果用户没有设置这个参数(默认就是没设置),那么同一个机架上的所有小文件将组成一个InputSplit,最终由一个Map Task来处理。如果用户设置了这个参数,那么同一个节点(node)上的文件将会组成一个InputSplit。同一个 InputSplit 包含了多个HDFS块文件,这些信息存储在 CombineFileSplit 类中,它主要包含以下信息:
折叠源码
1
2
3
4
5
private``Path[] paths;
private``long``[] startoffset;
private``long``[] lengths;
private``String[] locations;
private``long``totLength;
从上面的定义可以看出,CombineFileSplit类包含了每个块文件的路径、起始偏移量、相对于原始偏移量的大小以及这个文件的存储节点。因为一个CombineFileSplit包含了多个小文件,所以需要使用数组来存储这些信息。CombineFileInputFormat是抽象类,如果我们要使用它,需要实现createRecordReader方法,告诉MR程序如何读取组合的InputSplit。内置实现了两种用于解析组合InputSplit的类:org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat 和 org.apache.hadoop.mapreduce.lib.input.CombineSequenceFileInputFormat,我们可以把这两个类理解是 TextInputFormat 和 SequenceFileInputFormat。为了简便,这里主要来介绍CombineTextInputFormat。
在 CombineTextInputFormat 中创建了 org.apache.hadoop.mapreduce.lib.input.CombineFileRecordReader,具体如何解析CombineFileSplit中的文件主要在CombineFileRecordReader中实现。CombineFileRecordReader类中其实封装了 TextInputFormat的RecordReader,并对CombineFileSplit中的多个文件循环遍历并读取其中的内容,初始化每个文件的RecordReader主要在initNextRecordReader里面实现;每次初始化新文件的RecordReader都会设置mapreduce.map.input.file、mapreduce.map.input.length以及mapreduce.map.input.start参数,这样我们可以在Map程序里面获取到当前正在处理哪个文件。
样例代码如下:
折叠源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package``com.analysys.test;
import``org.apache.commons.logging.Log;
import``org.apache.commons.logging.LogFactory;
import``org.apache.hadoop.conf.Configuration;
import``org.apache.hadoop.conf.Configured;
import``org.apache.hadoop.fs.Path;
import``org.apache.hadoop.io.LongWritable;
import``org.apache.hadoop.io.Text;
import``org.apache.hadoop.mapreduce.Job;
import``org.apache.hadoop.mapreduce.MRJobConfig;
import``org.apache.hadoop.mapreduce.Mapper;
import``org.apache.hadoop.mapreduce.lib.input.CombineTextInputFormat;
import``org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import``org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import``org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import``org.apache.hadoop.util.Tool;
import``org.apache.hadoop.util.ToolRunner;
import``java.io.IOException;
public``class``TestCombineInput?``extends``Configured?``implements``Tool {
????``private``static``final``Log LOG = LogFactory.getLog(TestCombineInput.``class``);
????``private``static``final``long``ONE_MB =?``1024``* 1024L;
????``static``class``TextFileMapper?``extends``Mapper<LongWritable , Text, Text, Text> {
????????``@Override
????????``protected``void``map(LongWritable key, Text value, Context context)
????????????????``throws``IOException, InterruptedException {
????????????``Configuration configuration = context.getConfiguration();
????????????``LOG.info(``"MAP_INPUT_FILE??? "``+ configuration.get(MRJobConfig.MAP_INPUT_FILE));
????????????``Text filenameKey =?``new``Text(configuration.get(MRJobConfig.MAP_INPUT_FILE));
????????????``context.write(filenameKey, value);
????????``}
????``}
????``public``static``void``main(String[] args)?``throws``Exception {
????????``int``exitCode = ToolRunner.run(``new``TestCombineInput(), args);
????????``System.exit(exitCode);
????``}
????``@Override
????``public``int``run(String[] args)?``throws``Exception {
????????``Configuration conf =?``new``Configuration(getConf());
????????``// 设置splitFile文件大小
????????``conf.setLong(``"mapreduce.input.fileinputformat.split.maxsize"``, ONE_MB *?``32``);
????????``Job job = Job.getInstance(conf);
????????``FileInputFormat.setInputPaths(job, args[``0``]);
????????``FileOutputFormat.setOutputPath(job,?``new``Path(args[``1``]));
????????``job.setJarByClass(TestCombineInput.``class``);
????????``job.setInputFormatClass(CombineTextInputFormat.``class``);
????????``job.setOutputFormatClass(TextOutputFormat.``class``);
????????``job.setOutputKeyClass(Text.``class``);
????????``job.setOutputValueClass(Text.``class``);
????????``job.setMapperClass(TextFileMapper.``class``);
????????``return``job.waitForCompletion(``true``) ??``0``:?``1``;
????``}
}
日志输出:
折叠源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
18``/``09``/``21``15``:``05``:``23``INFO client.RequestHedgingRMFailoverProxyProvider: Looking?``for``the active RM in [rm1, rm2]...
18``/``09``/``21``15``:``05``:``23``INFO client.RequestHedgingRMFailoverProxyProvider: Found active RM [rm2]
18``/``09``/``21``15``:``05``:``24``INFO input.FileInputFormat: Total input paths to process :?``152
18``/``09``/``21``15``:``05``:``24``INFO input.CombineFileInputFormat: DEBUG: Terminated node allocation with : CompletedNodes:?``57``, size left:?``651281
18``/``09``/``21``15``:``05``:``24``INFO mapreduce.JobSubmitter: number of splits:``1
18``/``09``/``21``15``:``05``:``32``INFO mapreduce.Job:? map?``0``% reduce?``0``%
18``/``09``/``21``15``:``05``:``42``INFO mapreduce.Job:? map?``100``% reduce?``0``%
18``/``09``/``21``15``:``05``:``48``INFO mapreduce.Job:? map?``100``% reduce?``100``%
18``/``09``/``21``15``:``05``:``49``INFO mapreduce.Job: Counters:?``49
????``File System Counters
????????``FILE: Number of bytes read=``7177482
????????``FILE: Number of bytes written=``14656921
????????``FILE: Number of read operations=``0
????????``FILE: Number of large read operations=``0
????????``FILE: Number of write operations=``0
????????``HDFS: Number of bytes read=``667468
????????``HDFS: Number of bytes written=``7163032
????????``HDFS: Number of read operations=``157
????????``HDFS: Number of large read operations=``0
????????``HDFS: Number of write operations=``2
????``Job Counters
????????``Launched map tasks=``1
????????``Launched reduce tasks=``1
????????``Other local map tasks=``1
????????``Total time spent by all maps in occupied slots (ms)=``61968
????????``Total time spent by all reduces in occupied slots (ms)=``72656
????????``Total time spent by all map tasks (ms)=``7746
????????``Total time spent by all reduce tasks (ms)=``4541
????????``Total vcore-milliseconds taken by all map tasks=``7746
????????``Total vcore-milliseconds taken by all reduce tasks=``4541
????????``Total megabyte-milliseconds taken by all map tasks=``63455232
????????``Total megabyte-milliseconds taken by all reduce tasks=``74399744
????``Map-Reduce Framework
????????``Map input records=``2407
????????``Map output records=``2407
????????``Map output bytes=``7167847
????????``Map output materialized bytes=``7177482
????????``Input split bytes=``16187
????????``Combine input records=``0
????????``Combine output records=``0
????????``Reduce input groups=``152
????????``Reduce shuffle bytes=``7177482
????????``Reduce input records=``2407
????????``Reduce output records=``2407
????????``Spilled Records=``4814
????????``Shuffled Maps =``1
????????``Failed Shuffles=``0
????????``Merged Map outputs=``1
????????``GC time elapsed (ms)=``614
????????``CPU time spent (ms)=``18970
????????``Physical memory (bytes) snapshot=``2913640448
????????``Virtual memory (bytes) snapshot=``25466003456
????????``Total committed heap usage (bytes)=``3401056256
????``File Input Format Counters
????????``Bytes Read=``0
????``File Output Format Counters
????????``Bytes Written=``7163032
可以从日志中很清楚的看出input文件为_Total input paths to process : 152,_通过CombineFileInputFormat处理后splits为_mapreduce.JobSubmitter: number of splits:1,_Map数为_Launched map tasks=1。_注意体会mapreduce.input.fileinputformat.split.maxsize
参数的设置,大家可以不设置这个参数并且和设置这个参数运行情况对比,观察Map Task的个数变化。
3、Hive上的小文件问题
现象1:
输入文件过多,而Hive对文件创建的总数是有限制的,这个限制取决于参数:hive.exec.max.created.files,默认值是10000。如果现在你的表有60个分区,然后你总共有2000个map,在运行的时候,每一个mapper都会创建60个文件,对应着每一个分区,所以60*2000> 120000,就会报错:exceeds 100000.Killing the job 。最简单的解决办法就是调大hive.exec.max.created.files参数。但是如果说数据文件只有400G,那么你调整这个参数比如说40000。平均下来也就差不多每一个文件10.24MB,这样的话就有40000多个小文件,不是一件很好的事情。
方案1:
设置 mapper 输入文件合并参数
折叠源码
1
2
3
4
5
6
7
8
-- mapper执行前进行小文件的合并
hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;
-- 每一个mapper最大的输入大小
mapred.``max``.split.``size``= 256000000
-- 一个节点上split的至少的大小(这个值决定了多个DataNode上的文件是否需要合并)
mapred.``min``.split.``size``.per.node = 100000000
-- 一个机架下split的至少的大小(这个值决定了该机架下的文件是否需要合并)
mapred.``min``.split.``size``.per.rack = 100000000
现象2:
hive执行中间过程生成的文件过多
方案2:
设置中间过程合并参数
折叠源码
1
2
3
4
5
6
7
8
-- 在Map-only的任务结束时合并小文件
hive.merge.mapfiles =?``true
-- 在Map-Reduce的任务结束时合并小文件
hive.merge.mapredfiles =?``true
-- 合并文件的大小
hive.merge.``size``.per.task = 25610001000
-- 当输出文件的平均大小小于该值时,启动一个独立的map-reduce任务进行文件merge
hive.merge.smallfiles.avgsize=16000000
现象3:
hive结果文件过多
方案3:
设置 reducer 参数(一种是调整reducer个数,另一种是调整reducer大小)
折叠源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 设置reduce的个数
set mapreduce.job.reduces=500;
insert``into``table``xxx
select
??``*
from
?``xxx
distribute?``by``rand();
-- 设置reduce的大小
set``hive.``exec``.reducers.bytes.per.reducer=5120000000;
insert``into``table``xxx
select
??``*
from
?``xxx
distribute?``by``rand();
-- distribute by rand()保证了reduce中的数据随机分配,大小大致相同
参考文章:
1、https://hadoop.apache.org/docs/r2.7.1/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html
2、https://cwiki.apache.org/confluence/display/Hive/Configuration+Properties
3、http://blog.cloudera.com/blog/2009/02/the-small-files-problem/
)