Hadoop小文件处理

导读

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文件的读取

110004xw0v8f8jdzzvfdv8.jpguploading.4e448015.gif转存失败重新上传取消

(1)client端发送读文件请求给Namenode,如果文件不存在,返回错误信息,否则,将该文件对应的block及其所在Datanode位置发送给client

(2)client收到文件位置信息后,与不同Datanode建立socket连接并行获取数据。

HDFS文件的写入

110005iksghz43z09sk4b3.jpguploading.4e448015.gif转存失败重新上传取消

(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/

)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值