如何在Hadoop中處理小文件-續

Fayson在前面的文章《如何在Hadoop中處理小文件》和《如何使用Impala合併小文件》中介紹了什麼是Hadoop中的小文件,以及常見的處理方法。這裏Fayson再補充一篇文章進行說明。

HDFS中太多的小文件往往會帶來性能下降以及擴展性受限問題,爲了避免這個問題,我們一般需要控制每個文件儘可能的接近HDFS block大小比如256MB,或者是block size的幾倍。

在抽取數據時,應儘可能調整抽取管道以保存較少數量的大文件,而不是大量的小文件。如果你做不到,比如實時場景在抽數的時候總是一小批一小批,那隻能事後定期的去合併這些小文件。

本文Fayson主要介紹如何最小化小文件生成以及如何合併小文件。

1

小文件是如何產生的

以下是產生小文件的典型場景:

1.滴漏數據(Trickling data) - 數據是以小批量的形式進行增量抽取會導致小文件的產生,那隻能事後定期使用一些額外的作業去合併這些小文件。

2.大量的map或者reduce任務 - 大量map或者reduce任務的MapReduce作業或Hive查詢很多文件,比如Map-Only的作業有多少個map就會生成多少個文件,如果是Map-Reduce作業則有多少個reduce就會生成多少個文件。

3.過度分區的表 - 比如一個Hive表有太多分區,每個分區下只有幾個文件甚至只有一個小文件,這時考慮降低分區的粒度比如從按照天分區改爲按照月份分區。

4.上述情況的組合 - 如果上面三種情況組合出現,會加劇小文件問題。比如過度分區的Hive表,每個分區下都是很多個小文件而不是大文件。

2

分區設計

分區是指將大型Hive/Impala表物理拆分爲多個更小的,容易管理的部分。當根據分區進行查詢時,只需要掃描必要分區的數據,從而顯著提升查詢性能。

在HDFS中儘量保存大文件的原則同樣適用於分區表的每個分區,我們應儘量保證每個分區對應的HDFS目錄下的文件都較大。所以在設計表分區時,應該注意一下幾點:

1.避免過度分區表。在確定分區的粒度時,請考慮每個分區將存儲的數據量。確保每個分區保存的文件都是大文件(256MB的文件或者更大),即使這樣設計會導致分區粒度變得更粗,比如從按天分區變爲按月分區。

2.對於數據量較小(幾百MB)的表,請考慮創建一個非分區表。這樣即使我們只掃描單個文件夾下的所有文件,也會比處理分散在數個分區中的數百甚至數千個文件性能要好。

3

文件格式和壓縮

根據過往的經驗,有些大的集羣碰到小文件問題,往往是大量的Hive/Parquet表以未壓縮的方式存儲,並使用TEXTFILE文件格式。

從本質上說,HDFS中的文件或者Hive/Impala的表文件你選擇何種文件格式,對於小文件問題沒有直接關係。然而,使用低效的文件格式(比如TEXTFILE)和沒有壓縮的數據會從側面影響小文件問題甚至是加劇,從而影響集羣的性能和可擴展性,具體包含以下幾個方面:

1.使用低效的文件格式,尤其是未壓縮的文件格式,會導致HDFS空間使用量的增加以及NameNode需要跟蹤的塊數量的增加。如果文件很小,由於要存儲的原始數據量較大,可能會有更多的小文件。

2.由於讀取和寫入大量數據而導致更高的IO爭用。

3.從非常寬的表(具有大量字段的表)中讀取非列式存儲格式(TextFile,SequenceFile,Avro)的數據要求每個記錄都要從磁盤中完全讀取,即使只需要幾列也是如此。像Parquet這樣的列式格式允許僅從磁盤讀取所需的列,這樣可以顯著提高性能。

爲了確保性能和高效存儲之間的良好平衡,答應Fayson,請儘量使用PARQUET格式創建表,並確保在向其寫入數據時啓用數據壓縮(除非對Hive / Impala表使用的存儲格式有特定要求)。

在Hive中,使用以下示例創建Parquet表,並確保在插入時使用Snappy壓縮來壓縮數據。

# Hive

# Create table
CREATE TABLE db_name.table_name (
...
)
STORED AS PARQUET
LOCATION '/path/to/table';

# Create table as select
SET parquet.compression=snappy; 

CREATE TABLE db_name.table_name
STORED AS PARQUET
AS SELECT ...;

# Insert into/overwrite table
SET parquet.compression=snappy; 

INSERT INTO TABLE db_name.table_name
SELECT ...;

(可左右滑動)

在Impala中,使用以下語法:

# Impala

# Create table
CREATE TABLE db_name.table_name (
...
)
STORED AS PARQUET
LOCATION '/path/to/table';

# Create table as select
SET compression_codec=snappy; 

CREATE TABLE db_name.table_name
STORED AS PARQUET
AS SELECT ...;

# Insert into/overwrite table
SET compression_codec=snappy; 

INSERT INTO TABLE db_name.table_name
SELECT ...;

(可左右滑動)

4

使用Hive最小化小文件生成

Hive查詢會被轉化爲一串多個Map-Reduce(或Map-Only)作業執行。當查詢處理大量數據時,這些作業會被分解爲大量的map或者reduce來並行執行。

Hive查詢執行的最後一個Map-Reduce作業的task數將決定查詢生成的文件數。如果最後一個作業是Map-Only作業,則文件數將與該作業的map數相同;如果最後一個作業是Map-Reduce作業,則reduce的數量將決定生成的文件數。根據查詢產生的數據量,單個生成的文件可能非常小。

有兩種簡單配置Hive作業的方法,可以最大限度地減少查詢生成的文件數量:

4.1

動態文件合併

通過設置下面表格裏的參數,Hive將在這一串多個Map-Reduce作業的末尾額外增加一個是否滿足條件的比較步驟。此步驟計算作業生成的文件的平均大小,如果小於某個閾值,則會運行自動合併。

這個合併是有代價的,它會使用集羣資源,也會消耗一些時間。總耗時和使用的資源取決於生成的數據量。儘管如此,你現在做這個合併也比以後專門去合併小文件要方便,性能也可能會更好。使用這個參數主要是針對查詢結果有大量的小文件(數百個或更多)生成。

這些參數會動態評估判斷是否需要壓縮以及壓縮文件的最佳數量:

# Enable conditional compaction for map-only jobs
SET hive.merge.mapfiles = true;

# Enable conditional compaction for map-reduce jobs
SET hive.merge.mapredfiles = true;

# Target size for the compacted files – this is a target,
# not a hard limit. Leave a buffer between this number
# and 256 MB (268435456 bytes)
SET hive.merge.size.per.task = 256000000;

# Average size threshold for file compaction – the compaction
# will only execute if the average file size is smaller
# than this value
SET hive.merge.smallfiles.avgsize = 134217728;

(可左右滑動)

4.2

強制文件合併

另外一個強制文件合併的方法是指定Hive作業的Reduce數量。由於每個reducer都會生成一個文件,所以reducer的數量也就代表了最後生成的文件數量。

這樣做有優點也有缺點:

1.優點:

  • 對於那些會被轉換爲多個Map-Reduce作業(與Map-Only相反)的查詢,不需要像上面章節提到的多一些額外的判斷或合併的步驟。我們只需要調整最後一個Map-Reduce作業的reduce的數量即可。

2.缺點

  • 除非你能準確知道查詢結果會產生多少數據,否則你無法決定生成大小合適的文件需要多少個reducer。
  • 如果設置的reducer數量很少,會導致作業性能下降,因爲每個reduce需要處理大量數據。
  • 如果查詢執行之間的數據量不同,則可能很難找到reduce的最佳數量。
  • 如果查詢是Map-Only查詢,則需要修改查詢以強制執行reduce階段(參見下文)。

由於上述因素,只有在你至少粗略地知道查詢生成的數據量時才使用此方法。如果查詢結果生成的文件會非常小(小於256MB),我們只使用1個reduce也還不錯。

# ONLY ONE OF THE PARAMETERS BELOW SHOULD BE USED

# Limit the maximum number of reducers
SET hive.exec.reducers.max = <number>;

# Set a fixed number of reducers
SET mapreduce.job.reduces = <number>;

(可左右滑動)

如果Hive查詢是Map-Only的,則上述參數將不起作用。在這種情況下,我們可以在SQL語句後添加SORT BY 1以實現查詢語句必須執行reduce。

5

合併已有的小文件

有時候,我們其實無法阻止HDFS中小文件的產生。這種時候,我們需要定期運行合併作業以控制小文件的數量。你可以將合併作業獨立於你日常數據採集或生成流程之外作爲單獨作業,也可以直接將合併作業合併到裏日常的數據採集流程中去。

將運行合併作業作爲數據採集管道(ingestion pipeline)的一部分,可以更容易協調數據採集和數據合併:這樣你可以確保寫數到表或分區時,這個表或分區不會同時正在做數據合併的事。如果合併作業是獨立於數據採集管道(ingestion pipeline)運行的,則你需要保證數據採集沒運行的時候才能調度數據合併的作業(基於同一個表或者同一個分區)。

以下方法可用於對錶或分區的文件合併。

5.1

Hive合併

我們可以直接使用Hive的作業來合併已有的Hive表中的小文件。這個方法其實就是使用Hive作業從一個表或分區中讀取數據然後重新覆蓋寫入到相同的路徑下。必須爲合併文件的Hive作業指定一些類似上面章節提到的一些參數,以控制寫入HDFS的文件的數量和大小。

合併一個非分區表的小文件方法1:

SET hive.merge.mapfiles = true;
SET hive.merge.mapredfiles = true;
SET hive.merge.size.per.task = 256000000;
SET hive.merge.smallfiles.avgsize = 134217728;

SET hive.exec.compress.output = true;
SET parquet.compression = snappy; 

INSERT OVERWRITE TABLE db_name.table_name
SELECT *
FROM db_name.table_name;

(可左右滑動)

合併一個非分區表的小文件方法:

SET mapreduce.job.reduces = <table_size_MB/256>;

SET hive.exec.compress.output = true;
SET parquet.compression = snappy;

INSERT OVERWRITE TABLE db_name.table_name
SELECT *
FROM db_name.table_name
SORT BY 1;

(可左右滑動)

合併一個表分區的小文件:

SET mapreduce.job.reduces = <table_size_MB/256>;

SET hive.exec.compress.output = true;
SET parquet.compression = snappy;

INSERT OVERWRITE TABLE db_name.table_name
PARTITION (part_col = '<part_value>')
SELECT col1, col2, ..., coln
FROM db_name.table_name
WHERE part_col = '<part_value>'
SORT BY 1;

(可左右滑動)

合併一個範圍內的表分區的小文件:

SET hive.merge.mapfiles = true;
SET hive.merge.mapredfiles = true;
SET hive.merge.size.per.task = 256000000;
SET hive.merge.smallfiles.avgsize = 134217728;

SET hive.exec.compress.output = true;
SET parquet.compression = snappy;

SET hive.exec.dynamic.partition.mode = nonstrict;
SET hive.exec.dynamic.partition = true;

INSERT OVERWRITE TABLE db_name.table_name
PARTITION (part_col)
SELECT col1, col2, ..., coln, part_col
FROM db_name.table_name
WHERE part_col BETWEEN '<part_value1>' AND '<part_value2>';

(可左右滑動)

5.2

FileCrusher

使用Hive來壓縮表中小文件的一個缺點是,如果表中既包含小文件又包含大文件,則必須將這些大小文件一起處理然後重新寫入磁盤。如上一節所述,也即沒有辦法只處理表中的小文件,而保持大文件不變。

FileCrusher使用MapReduce作業來合併一個或多個目錄中的小文件,而不會動大文件。它支持以下文件格式的表:

  • TEXTFILE
  • SEQUENCEFILE
  • AVRO
  • PARQUET

它還可以壓縮合並後的文件,不管這些文件以前是否被壓縮,從而減少佔用的存儲空間。默認情況下FileCrusher使用Snappy壓縮輸出數據。

FileCrusher不依賴於Hive,而且處理數據時不會以Hive表爲單位,它直接工作在HDFS數據之上。一般需要將需要合併的目錄信息以及存儲的文件格式作爲輸入參數傳遞給它。

爲了簡化使用FileCrusher壓縮Hive表,我們創建了一個“包裝腳本”(wrapper script)來將Hive表的相關參數正確解析後傳遞給FileCrusher。

crush_partition.sh腳本將表名(也可以是分區)作爲參數,並執行以下任務:

  • 在合併之前收集有關表/分區的統計信息
  • 計算傳遞給FileCrusher所需的信息
  • 使用必要參數執行FileCrusher
  • 在Impala中刷新表元數據,以便Impala可以查看合併後的文件
  • 合併後蒐集統計信息
  • 提供合併前和合並後的摘要信息,並列出原始文件備份的目錄位置

腳本的方法如下所示:

Syntax: crush_partition.sh <db_name> <table_name> <partition_spec> [compression] [threshold] [max_reduces] 

(可左右滑動)

具體參數解釋如下:

db_name - (必須)表所存儲的數據庫名

table_name -(必須)需要合併的表名

partition_spec -(必須)需要合併的分區參數,有效值爲:

  • “all” – 合併非分區表,或者合併分區表的所有分區內的文件
  • 指定分區參數,參數必須用引號引起來,例如:
    • "year=2010,state='CA'"
    • "pt_date='2016-01-01'"

compression -(可選,默認Snappy)合併後的文件寫入的壓縮格式,有效值爲:snappy, none (for no compression), gzip, bzip2 and deflate。

threshold -(可選,默認0.5)符合文件合併條件的相對於HDFS block size的百分比閾值,必須是 (0, 1] 範圍內的值。默認的0.5的意思是小於或等於HDFS block size的文件會被合併,大於50%的則會保持不變。

max_reduces -(可選,默認200)FileCrusher會被分配的最大reduce數,這個限制是爲了避免在合併非常大的表時分配太多任務而佔用太多資源。所以我們可以使用這個參數來平衡合併文件的速度以及它在Hadoop集羣上造成的開銷。

當FileCrusher運行時,它會將符合壓縮條件的文件合併壓縮爲更大的文件,然後使用合併後的文件替換原始的小文件。合併後的文件格式爲:

“crushed_file-<timestamp>-<some_numbers>”

原始文件不會被刪除,它們會被移動的備份目錄,備份目錄的路徑會在作業執行完畢後打印到終端。原始文件的絕對路徑在備份目錄中保持不變,因此,如果需要回滾,則很容易找出你想要拷貝回去的目錄地址。例如,如果原始小文件的目錄爲:

/user/hive/warehouse/prod.db/user_transactions/000000_1
/user/hive/warehouse/prod.db/user_transactions/000000_2

(可左右滑動)

合併後會成爲一個文件:

/user/hive/warehouse/prod.db/user_transactions/crushed_file-20161118102300-0-0

(可左右滑動)

原始文件我們會移動到備份目錄,而且它之前的原始路徑我們依舊會保留:

/user/admin/filecrush_backup/user/hive/warehouse/prod.db/user_transactions/000000_1
/user/admin/filecrush_backup/user/hive/warehouse/prod.db/user_transactions/000000_2

(可左右滑動)

FileCrusher的github地址:

https://github.com/asdaraujo/filecrush

本文提到的crush_partition.sh全路徑爲:

https://github.com/asdaraujo/filecrush/tree/master/bin

提示:代碼塊部分可以左右滑動查看噢

爲天地立心,爲生民立命,爲往聖繼絕學,爲萬世開太平。 溫馨提示:如果使用電腦查看圖片不清晰,可以使用手機打開文章單擊文中的圖片放大查看高清原圖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章