SequenceFile文件原理及使用

介紹

SequenceFile是hadoop裏用來存儲序列化的鍵值對即二進制的一種文件格式。SequenceFile文件也可以作爲MapReduce作業的輸入和輸出,hive和spark也支持這種格式。
它有如下幾個優點:

  1. 以二進制的形式存儲數據,所以可以在HDFS裏存儲圖像或者更加複雜的結構作爲KV對。
  2. SequenceFile支持壓縮分片。當你壓縮爲一個SequenceFile時,並不是將整個文件壓縮成一個單獨的單元,而是壓縮文件裏的record或者block of records(塊)。因此SequenceFile是能夠支持分片的,儘管使用的壓縮方式如Snappy, Lz4 or Gzip不支持分片。
  3. SequenceFile也可以用於存儲多個小文件。由於Hadoop本身就是用來處理大型文件的,所以用一個SequenceFile來存儲很多小文件就可以提高處理效率,也能節省Namenode內存,因爲Namenode只需一個SequenceFile的metadata,而不是爲每個小文件創建單獨的metadata。
  4. 由於數據是以SequenceFile形式存儲,所以中間輸出文件即map輸出也會用SequenceFile來存儲。

Sync points(同步點)

SequenceFile文件裏每隔100 bytes會記錄一個sync-marker,基於這些sync pointsSequenceFile就是可以切片的並用作MapReduce的輸入。

SequenceFile的壓縮形式

SequenceFile有三種壓縮選擇

  1. NONE ---- 即key和value都不壓縮。
  2. RECORD ---- 只有value會被壓縮。
  3. BLOCK ---- key和value都會被壓縮。key和value都會以block的形式單獨收集和壓縮。block的大小是可以配置的,在core-site,xml裏定義io.seqfile.compress.blocksize,默認是1000000 bytes。

壓縮的好處:減少磁盤佔用、減少IO、減少shuffle的帶寬。

SequenceFile的文件格式

儘管有三種壓縮選擇,但是header的格式都是一樣的。

SequenceFile文件的header格式

在這裏插入圖片描述

字段 說明
version 3 bytes of magic header SEQ, followed by 1 byte of actual version number (e.g. SEQ4 or SEQ6)
KeyClassName key class
ValueClassName value class
Compression A boolean which specifies if compression is turned on for keys/values in this file
BlockCompression CompressionCodec class which is used for compression of keys and/or values (if compression is enabled)
Metadata SequenceFile.Metadata for this file
Sync A sync marker to denote end of the header

無壓縮的SequenceFile文件格式

在這裏插入圖片描述
sync-marker ---- 每100 bytes一個sync-marker

Record壓縮的SequenceFile文件格式

在這裏插入圖片描述
sync-marker ---- 每100 bytes一個sync-marker

存儲格式如下
在這裏插入圖片描述

Block壓縮的SequenceFile文件格式

  • Header
  • Record Block
    • Uncompressed number of records in the block
    • Compressed key-lengths block-size
    • Compressed key-lengths block
    • Compressed keys block-size
    • Compressed keys block
    • Compressed value-lengths block-size
    • Compressed value-lengths block
    • Compressed values block-size
    • Compressed values block
  • A sync-marker every block
    在這裏插入圖片描述

SequenceFile的相關類

SequenceFile提供了SequenceFile.Writer, SequenceFile.Reader 和 SequenceFile.Sorter用來寫、讀及排序數據。
由於有三種壓縮選擇,故SequenceFile Writer有以下三類:

  • Writer : Uncompressed records.
  • RecordCompressWriter : Record-compressed files, only compress values.
  • BlockCompressWriter : Block-compressed files, both keys & values are compressed.

通常用靜態方法SequenceFile.createWriter去創建Writer,創建時指定Writer.Option來確定是否要壓縮及選擇哪種方式壓縮。

Java API讀寫SequenceFile

將HDFS某個目錄下所有的小文件合併成一個SequenceFile文件(寫)

以小文件的文件路徑作爲key,小文件的內容作爲value。然後將其寫入到SequenceFile文件。

public static void writeHDFSFileTOHDFSSequenceFile() throws IOException {
    String inputDir = "/user/root/input";
    Path path = new Path(inputDir);
    //最後生成的sequenceFile文件
    Path outFile = new Path("/user/root/sequecenfile/out/seq_file");
    //聲明一個byte[]用於後面存放小文件內容
    byte[] buffer;
    //獲取inputDir目錄下的所有文件
    FileStatus[] fileStatusArr = fs.listStatus(path);
    //構造writer, 並使用try獲取資源, 最後自動關閉資源
    try(SequenceFile.Writer writer = SequenceFile.createWriter(conf,
            SequenceFile.Writer.file(outFile),//設置文件名
            SequenceFile.Writer.keyClass(Text.class),//設置keyclass
            SequenceFile.Writer.valueClass(Text.class),//設置valueclass
            SequenceFile.Writer.appendIfExists(false),
            SequenceFile.Writer.compression(SequenceFile.CompressionType.BLOCK, new GzipCodec()) //設置block+gzip的壓縮方式
    )){
        //循環外定義key和value,避免重複定義,因爲序列化時只是會序列化對應的內容
        Text key = new Text();
        Text value = new Text();
        for (FileStatus fileStatus : fileStatusArr){
            System.out.println("the file name is "+fileStatus.getPath());
            //利用FileSystem打開文件
            FSDataInputStream fsDataIn = fs.open(fileStatus.getPath());
            //根據文件大小來定義byte[]的長度
            buffer = new byte[((int) fileStatus.getLen())];
            //將文件內容讀入到buffer這個byte[]裏
            fsDataIn.read(buffer);
            key.set(fileStatus.getPath().toString());
            value.set(buffer);
            //通過append方法寫入到SequenceFile
            writer.append(key, value);
        }

    }

}

將本地某個目錄下所有的小文件合併成一個SequenceFile文件(寫)

public static void writeLocalFileToHDFSSequenceFile() throws IOException {
    String inputDir = "/root/test";
    java.nio.file.Path localDir = Paths.get(inputDir);
    Path outFile = new Path("/user/root/sequecenfile/out/seq_file");
    //構造writer, 並使用try獲取資源, 最後自動關閉資源
    try(SequenceFile.Writer writer = SequenceFile.createWriter(conf,
            SequenceFile.Writer.file(outFile),//設置文件名
            SequenceFile.Writer.keyClass(Text.class),//設置keyclass
            SequenceFile.Writer.valueClass(Text.class),//設置valueclass
            SequenceFile.Writer.appendIfExists(false),
            SequenceFile.Writer.compression(SequenceFile.CompressionType.BLOCK, new GzipCodec()) //設置block+gzip的壓縮方式
    )){
        Text key = new Text();
        Text value = new Text();

//            Collection<File> fileList = FileUtils.listFiles(new File(inputDir),null,false);
        try(Stream<java.nio.file.Path> stream = Files.list(localDir)){

            //遍歷所有以.txt結尾的小文件寫入到SequenceFile
            stream.filter(x->x.toString().endsWith(".txt")).forEach(localPath->{
                try {
                    System.out.println(localPath.toString());
                    byte[] bytes = Files.readAllBytes(localPath);
                    key.set(localPath.toString());
                    value.set(bytes);
                    writer.append(key, value);
                } catch (IOException e) {
                    e.printStackTrace();
                    System.err.println("read "+localPath.toString()+" error");
                }
            });
        }

    }

}

讀HDFS上SequenceFile文件的內容(讀)

通過SequenceFile.Reader的構造方法並指定參數創建Reader來讀取SequenceFile文件

public static void readSequenceFile() throws IOException {
    Path seqPath = new Path("/user/root/sequecenfile/out/seq_file");
    try(SequenceFile.Reader reader = new SequenceFile.Reader(conf,
            SequenceFile.Reader.file(seqPath), //設置SequenceFile文件路徑
            SequenceFile.Reader.bufferSize(1024*8)) //設置bufferSize
    ){
        Text key = new Text();
        Text value = new Text();
        while (reader.next(key, value)){
            System.out.println("------------key="+key+"----------");
            System.out.println(value);
            System.out.println("-------------------------------");
        }
    }
}

將其打包成具體的jar文件後執行hadoop jar sequenceFile.jar com.utstar.patrick.hadoop.data.format.SequenceFileDemo
其中sequenceFile.jar就是打包的jar名,com.utstar.patrick.hadoop.data.format.SequenceFileDemo就是需要運行的main類。
部分結果如下圖所示
在這裏插入圖片描述

使用SequenceFile作爲MapReduce的輸入和輸出

使用SequenceFile作爲MapReduce的輸入

SequenceFile文件作爲輸入很簡單,只需要在driver代碼裏設置job的inputformat即可。

job.setInputFormatClass(SequenceFileInputFormat.class);

我們以hadoop提供的example爲例,我通過-Dproperty=value的方式設置了inputformat來讀取上面生成的SequenceFile文件

hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-2.6.5.jar wordcount -Dmapreduce.job.inputformat.class=org.apache.hadoop.mapreduce.lib.input.SequenceFileInputFormat /user/root/sequecenfile/out/seq_file /out_99

能正常的進行單詞統計功能,如下圖所示
在這裏插入圖片描述

使用SequenceFile作爲MapReduce的輸出

driver代碼裏設置如下參數即可,需要強調的是要通過SequenceFileOutputFormat.setOutputCompressionType方式來說明SequenceFile選取哪種壓縮方式。

//設置outputformat
job.setOutputFormatClass(SequenceFileOutputFormat.class);

FileInputFormat.addInputPath(job, new Path("/user/root/input"));
FileOutputFormat.setOutputPath(job, new Path(outpath));
//設置壓縮
FileOutputFormat.setCompressOutput(job, true);
//設置用哪種算法進行壓縮
FileOutputFormat.setOutputCompressorClass(job, Lz4Codec.class);
//必須通過SequenceFileOutputFormat.setOutputCompressionType來指定SequenceFile文件的壓縮類型
SequenceFileOutputFormat.setOutputCompressionType(job, SequenceFile.CompressionType.BLOCK);

順帶提下小文件的處理方式

hadoop自帶有3種小文件解決方式

Hadoop Archive雖然解決了小文件佔用namenode內存的問題,但是提交任務時還是一個小文件一個task;
CombineFileInputFormat是一個抽象類,需要繼承該類實現對應的抽象方法public abstract RecordReader<K, V> createRecordReader(InputSplit split, TaskAttemptContext context) throws IOException,比較麻煩;
所以解決小文件更好的方式還是 SequenceFile

提到小文件順便提下小job,對於小型job可以開啓jvm重用讓每個jvm跑幾個task,這樣可以減少進程關閉和創建的開銷,能有效提高小job的運行效率。
mapreduce.job.jvm.numtasks的默認值是1

mapreduce.job.jvm.numtasks=10

但是對於大型job,該參數就不合適了,因爲在長期運行的job裏會有內存碎片,最好是重新創建jvm,而且創建jvm的時間和整個job的運行時間相比就很微不足道了。說白了,這都是一個trade-off的過程。

參考網址

sequence-file-hadoop
how-to-read-and-write-sequencefile-in-hadoop
what-is-sequencefileinputformat-in-hadoop-mapreduce
reuse-jvm-in-hadoop-mapreduce-jobs

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