5.2 基於壓縮的高效存儲
(僅包括技術25,和技術26)
數據壓縮可以減小數據的大小,節約空間,提高數據傳輸的效率。在處理文件中,壓縮很重要。在處理Hadoop的文件時,更是如此。為了讓Hadoop更高效處理文件,就需要選擇一個合適的壓縮編碼器,加快作業運行,增加集群的數據存儲能力。
技術25 為待處理數據選擇正確的壓縮編碼器
在HDFS上使用壓縮並不像ZFS文件系統上那樣透明,特別是在處理那些可分塊的壓縮文件時。(這些將在本章中稍后介紹。)由於Avro和SequenceFiles等文件格式提供了壓縮的內置支持,使用它們就相對透明。但是如果使用它們,會導致沒有辦法使用其他的文件格式。
問題
需要為待處理數據評估並選擇最優壓縮格式。
方案
Snappy,一個來自於Google的壓縮編碼器,在壓縮大小和讀寫時間上取得了最佳平衡。然而,如果要考慮到處理超大文件時對可分塊特性的支持,LZOP將是最佳的編碼器。
討論
首先看看Hadoop中可用的壓縮編碼器,如表5.1所示:
表5.1 壓縮編碼器
編碼器 | 背景 |
Deflate | Deflate和zlib類似,使用gzip的壓縮算法,但不包含gzip的文件頭。 |
gzip | gzip文件格式包含一個文件頭和文件體。文件體的壓縮算法和defalte一樣。 |
bzip2 | bzip2是空間優先的壓縮編碼器。 |
LZO | LZO是基於塊的壓縮算法。壓縮文件可以分塊。 |
LZOP | LZOP是在LZO上加上一個文件頭。曾經LZO和LZOP都是Hadoop的一部分。后來由於GPL授權限制被移除了。 |
Snappy | Snappy是Hadoop中新近支持的編碼器選項。(參考http://code.google.com/p/hadoop-snappy/。)它是Google的開源壓縮算法。Google在MapReduce和BigTable中使用它來壓縮數據。它的最大問題是無法分塊。如果文件和HDFS塊大小相當,那么就可以使用Snappy。如果文件大於HDFS塊,那么效率就會很低。CDH3中提供了Snappy。但是Apache Hadoop中對Snappy的支持還不完全能滿足生產環境的要求。(參考 https://issues.apache.org/jira/browse/HADOOP-7206。) |
在評估編碼器之前,先要明確評估標准。評估標准是基於功能性需求和性能需求制定的。評估標准可能包括如下內容:
- 空間/時間取向 —— 一般來說,壓縮文件越小(並且消耗時間越長),壓縮比率越好。
- 可分塊特性 —— 壓縮文件是否能夠被分塊,以便在多個map任務中處理?如果壓縮文件不能分塊,那么只有一個map任務可以處理它。如果文件占據了很多HDFS塊,將損失數據的本地性。也就是說,map任務很可能需要從遠程數據節點讀取苦塊,增大網絡IO的負擔。
- 是否是本地庫內置的壓縮支持 —— 采用的壓縮編碼器是否是本地的庫。如果采用非本地的JAVA編碼器,性能很可能下降。
運行自定義測試 在進行評估的時候,最好能夠編寫測試壓縮性能的測試。測試環境與生產環境類似為佳。通過測試可以了解壓縮率和編碼器的壓縮時間。 |
接下來看看可用的壓縮編碼器的橫向對比。如表5.2所示:(空間/時間的比較將在下一部分)
表5.2 壓縮編碼器橫向比較
編碼器 | 擴展名 | 版權 | 可分塊 | 只支持JAVA壓縮 | 本地庫壓縮支持 |
Deflate | .deflate | zlib | No | Yes | Yes |
gzip | .gz | GNU GPL | No | Yes | Yes |
bzip2 | .gz | BSD | Yes | Yes | No |
lzo | .lzo_deflate | GNU GPL | No | No | Yes |
lzop | .lzo | GNU GPL | Yes | No | Yes |
Snappy | .gz | New BSD | No | No | Yes |
那么在考慮空間/時間取向的時候,這些編碼器表現如何呢?以下是基於128MB文本文件的壓縮性能測試。結果見表5.3:
表5.3 基於128MB文本文件的壓縮性能測試橫向比較
編碼器 | 壓縮時間(秒) | 解壓縮時間(秒) | 壓縮文件大小 | 壓縮比率 |
Deflate | 6.88 | 6.80 | 24,866,259 | 18.53% |
gzip | 6.68 | 6.88 | 24,866,271 | 18.53% |
bzip2 | 3,012.34 | 24.31 | 19,270,217 | 14.36% |
lzo | 1.69 | 7.00 | 40,946,704 | 30.51% |
lzop | 1.70 | 5.62 | 40,946,746 | 30.51% |
Snappy | 1.31 | 6.66 | 46,108,189 | 34.45% |
圖5.3用柱狀圖描述了壓縮文件大小。
圖5.4用柱狀圖表示壓縮時間。由於硬件差異,這些時間差別很大。給出這些時間只是為了說明它們之間的相關性。
那么可以從時間空間的結果數據中得到什么呢?如果需要在集群中處理盡可能多的數據,並可以忍受很長的壓縮時間,那么bzip2是最佳選擇。如果需要盡可能減少讀寫壓縮文件時的CPU消耗,那么應該選擇Snappy。如果需要在空間和時間中取得平衡。那么就要考慮bzip2之外的選擇了。
如果可分塊特征非常重要,那么就需要在bzip2和LZOP之間選擇。由於bzip2實在太慢,可能就要選擇LZOP了。另外,在hadoop中使用LZOP不如bzip2方便。接下來將介紹如何使用bzip2。
小結
最優的解碼器的選擇完全取決於選擇標准。如果不考慮可分塊性,Snappy是最佳選擇。如果考慮可分塊性,LZOP是很不錯的選擇。
文本文件和二進制文件的壓縮大小不同。不同文件內容的壓縮不同。在使用壓縮前進行測試得到壓縮比率是很有必要的。
在HDFS中壓縮數據有很多好處,減小文件大小,加快MapReduce作業運行。在Hadoop中有多個壓縮編碼器可以選擇。它們的特性和性能各部相同。接下來就介紹如何在MapReduce,Pig和Hive中使用數據壓縮。
技術26 在HDFS,MapReduce,Pig和Hive中使用數據壓縮
由於HDFS並不提供對壓縮的內置支持,在Hadoop中使用壓縮就不那么容易,需要手動實現數據壓縮。另外,可分塊壓縮並不適合心靈脆弱的人,因為Hadoop並不直接支持它。如果只是處理和HDFS塊大小相當的中等大小的文件,本技術將是即快又簡單。
問題
需要在HDFS中讀寫壓縮文件,並在MapReduce,Pig和Hive中使用它們。
方案
在MapReduce中處理壓縮文件需要配置Mapred-site.xml,注冊壓縮編碼器。然后可以在MapReduce將壓縮文件作為輸入文件。如果需要在MapReduce中輸出壓縮文件,需要配置MapReduce屬性mapred.output.compress和mapred.output.compression.codec。
討論
第一步是如何在MapReduce中用前述的編碼器讀取並寫入文件。除了LZO,LZOP和Snappy,其它的編碼器都都由Hadoop提供了。如果需要使用沒有提供的編碼器,就需要手動下載並編譯。
表5.4種給出了可以用到的編碼器的類名。
表5.4 編碼器的類
編碼器 | 類名 |
Deflate | org.apache.hadoop.io.compress.DeflateCodec |
gzip | org.apache.hadoop.io.compress.GzipCodec |
bzip2 | org.apache.hadoop.io.compress.BZip2Codec |
lzo | com.hadoop.compression.lzo.LzoCodec |
lzop | com.hadoop.compression.lzo.LzopCodec |
Snappy | org.apache.hadoop.io.compress.SnappyCodec |
HDFS
以下代碼給出了如何壓縮一個已經在HDFS中的文件:
1 Configuration config = new Configuration(); 2 FileSystem hdfs = FileSystem.get(config); 3 Class<?> codecClass = Class.forName(args[2]); 4 CompressionCodec codec = (CompressionCodec) 5 ReflectionUtils.newInstance(codecClass, config); 6 InputStream is = hdfs.open(new Path(args[0])); 7 OutputStream os = hdfs.create(new Path(args[0] + codec.getDefaultExtension())); 8 OutputStream cos = codec.createOutputStream(os); 9 IOUtils.copyBytes(is, cos, config, true); 10 IOUtils.closeStream(os); 11 IOUtils.closeStream(is);
緩存編碼器 壓縮編碼器的創建成本很高。使用Hadoop的ReflectionUtils類可以緩存創建的實例,以加速后續的編碼器實例創建。一個更好的選項是使用CompressionCodecFactory。它提供了編碼器的緩存功能。 |
以下代碼實現了讀取HDFS中的壓縮文件:
1 InputStream is = hdfs.open(new Path(args[0])); 2 Class<?> codecClass = Class.forName(args[1]); 3 CompressionCodec codec = (CompressionCodec) 4 ReflectionUtils.newInstance(codecClass, config); 5 InputStream cis = codec.createInputStream(is); 6 IOUtils.copyBytes(cis, System.out, config, true); 7 IOUtils.closeStream(is);
接下來介紹如何在MapReduce中使用壓縮文件。
MAPREDUCE
在MapReduce中使用壓縮文件,需要設置作業的一些參數。簡單起見,假定map和reduce之間沒有任何過濾和轉換。
1 Class<?> codecClass = Class.forName(args[2]); 2 conf.setBoolean("mapred.output.compress", true); // Compress the reducer output. 3 conf.setBoolean("mapred.compress.map.output", true); // Compress the mapper output 4 conf.setClass("mapred.output.compression.codec", codecClass, CompressionCodec.class); 5 // The compression codec for compressing mapper output.
在MapReduce作業中處理壓縮文件和非壓縮文件的區別在於上述代碼中的三行注釋。
不僅僅是作業的輸入和輸入可以被壓縮,中間過程的map輸出也可以被壓縮。因為map先輸出到磁盤上,然后通過網絡傳輸給reduce。壓縮map的輸出的效率的高低取決於被壓縮對象的數據類型。一般來說,數據壓縮都可以帶來一定程度的效率提升。
為什么在代碼中不需要指定輸入文件的壓縮編碼器?FileInputFormat類使用CompressionCodecFactory來根據輸入文件擴展名決定使用相匹配的已注冊編碼器。
MapReduce如何知道使用哪個編碼器?相應的配置文件時mapred-site.xml。以下代碼說明了如何注冊前述編碼器。注意,除了gzip,Deflate和bzip2,其他的編碼器在注冊前都需要手動編譯並存放在集群上。
1 <property> 2 <name>io.compression.codecs</name> 3 <value> 4 org.apache.hadoop.io.compress.GzipCodec, 5 org.apache.hadoop.io.compress.DefaultCodec, 6 org.apache.hadoop.io.compress.BZip2Codec, 7 com.hadoop.compression.lzo.LzoCodec, 8 com.hadoop.compression.lzo.LzopCodec, 9 org.apache.hadoop.io.compress.SnappyCodec 10 </value> 11 </property> 12 <property> 13 <name> 14 io.compression.codec.lzo.class 15 </name> 16 <value> 17 com.hadoop.compression.lzo.LzoCodec 18 </value> 19 </property>
到這里就是全部的如何在MapReduce中處理壓縮文件了。Pig和Hive是高層次語言,抽象了MapReduce中的一些底層細節。接下來介紹如何在Pig和Hive中使用壓縮文件。
PIG
和MapReduce不同的是,Pig並不需要額外的代碼來處理壓縮文件。在使用Pig的本地庫時,要保證Pig的JVM的java.library.path包括了本地庫的地址。下列腳本用於為Pig設置正確的路徑以讀取本地庫:
$ bin/pig-native-opts.sh
export PIG_OPTS="$PIG_OPTS -Djava.library.path=/usr/lib/..."
$ export PIG_OPTS="$PIG_OPTS -Djava.library.path=/usr/..."
為了讓Pig能夠處理壓縮文件,還需要為壓縮文件添加正確的擴展名。以下例子給出了如何壓縮文件並讀取到Pig中:
$ gzip -c /etc/passwd > passwd.gz $ hadoop fs -put passwd.gz passwd.gz $ pig grunt> A = load 'passwd.gz' using PigStorage(':'); grunt> B = foreach A generate $0 as id; grunt> DUMP B; (root) (bin) (daemon) ...
Pig中輸出到gzip文件的過程類似,同樣需要指定文件擴展名。以下例子將Pig的關系B的結果存儲到HDFS的一個文件中,並把它們復制到本地文件系統中以查看內容:
grunt> STORE B INTO 'passwd-users.gz'; # Ctrl+C to break out of Pig shell $ hadoop fs -get passwd-users.gz/part-m-00000.gz . $ gunzip -c part-m-00000.gz root bin daemon ...
很簡單。希望Hive中也是一樣。
HIVE
和Pig中一樣,需要做的就是在定義文件名時制定和編碼器相匹配的擴展名:
hive> CREATE TABLE apachelog (...); hive> LOAD DATA INPATH /user/aholmes/apachelog.txt.gz OVERWRITE INTO TABLE apachelog;
上述例子講gzip壓縮文件裝載到了Hive中。在這個例子中,Hive將裝載的文件移動到了Hive的倉庫目錄中,並使用raw文件作為表的存儲。如何創建另外一個表,並制定表必須被壓縮。以下代碼通過Hive的配置啟用了MapReduce壓縮:(MapReduce作業將在最后一個語句中被調用來讀取新表)
hive> SET hive.exec.compress.output=true; hive> SET hive.exec.compress.intermediate = true; hive> SET mapred.output.compression.codec = org.apache.hadoop.io.compress.GzipCodec; hive> CREATE TABLE apachelog_backup (...); hive> INSERT OVERWRITE TABLE apachelog_backup SELECT * FROM apachelog;
通過以下腳本可以檢測Hive中apachelog_backup表的壓縮結果(在HDFS上):
$ hadoop fs -ls /user/hive/warehouse/apachelog_backup
/user/hive/warehouse/apachelog_backup/000000_0.gz
需要注意的是,Hive推薦使用SequenceFile作為表的輸出格式,因為SequenceFile可以分塊壓縮。
小結
這個技術提供了在Hadoop中壓縮了的簡便方法。對於不大的文件,這也是一個相對透明的方法。
如果壓縮文件大小大於HDFS塊的大小,就需要參考技術27中的分塊壓縮技術了。
(譯注:本節原文較長,剩余部分將在下一篇文章翻譯。)