SequenceFile 解决hadoop小文件问题

本文深入解析SequenceFile的不同存储格式,包括不压缩、记录压缩和块压缩,并对比了它们的特点及应用场景,特别是针对MapReduce输入输出格式的应用。

SequenceFile Formats
2010-10-27 18:50

 

Overview

SequenceFile is a flat file consisting of binary key/value pairs. It is extensively used in MapReduce as input/output formats. It is also worth noting that, internally, the temporary outputs of maps are stored using SequenceFile.

The SequenceFile provides a Writer, Reader and Sorter classes for writing, reading and sorting respectively.

There are 3 different SequenceFile formats:

  1. Uncompressed key/value records.
  2. Record compressed key/value records - only 'values' are compressed here.
  3. Block compressed key/value records - both keys and values are collected in 'blocks' separately and compressed. The size of the 'block' is configurable.

The recommended way is to use the SequenceFile.createWriter methods to construct the 'preferred' writer implementation.

The SequenceFile.Reader acts as a bridge and can read any of the above SequenceFile formats.

 

SequenceFile Formats

This section describes the format for the latest 'version 6' of SequenceFiles.

Essentially there are 3 different file formats for SequenceFiles depending on whether compression and block compression are active.


However all of the above formats share a common header (which is used by the SequenceFile.Reader to return the appropriate key/value pairs). The next section summarises the header:

SequenceFile Common Header
  • version - A byte array: 3 bytes of magic header 'SEQ', followed by 1 byte of actual version no. (e.g. SEQ4 or SEQ6)

  • keyClassName - String
  • valueClassName - String
  • compression - A boolean which specifies if compression is turned on for keys/values in this file.

  • blockCompression - A boolean which specifies if block compression is turned on for keys/values in this file.

  • compressor class - The classname of the CompressionCodec which is used to compress/decompress keys and/or values in this SequenceFile (if compression is enabled).

  • metadata - SequenceFile.Metadata for this file (key/value pairs)

  • sync - A sync marker to denote end of the header.

All strings are serialized using Text.writeString api.



The formats for Uncompressed and RecordCompressed Writers are very similar:

Uncompressed & RecordCompressed Writer Format
  • Header

  • Record
    • Record length
    • Key length
    • Key
    • (Compressed?) Value
  • A sync-marker every few k bytes or so.

The sync marker permits seeking to a random point in a file and then re-synchronizing input with record boundaries. This is required to be able to efficiently split large files for MapReduce processing.


The format for the BlockCompressedWriter is as follows:

BlockCompressed Writer Format
  • Header

  • Record Block

    • A sync-marker to help in seeking to a random point in the file and then seeking to next record block.

    • CompressedKeyLengthsBlockSize

    • CompressedKeyLengthsBlock

    • CompressedKeysBlockSize

    • CompressedKeysBlock

    • CompressedValueLengthsBlockSize

    • CompressedValueLengthsBlock

    • CompressedValuesBlockSize

    • CompressedValuesBlock

    The compressed blocks of key lengths and value lengths consist of the actual lengths of individual keys/values encoded in ZeroCompressedInteger format.

简介: 
SequenceFile 是一个由二进制序列化过的key/value的字节流组成的文本存储文件,它可以在map/reduce过程中的input/output 的format时被使用。在map/reduce过程中,map处理文件的临时输出就是使用SequenceFile处理过的。
SequenceFile分别提供了读、写、排序的操作类。
SequenceFile的操作中有三种处理方式:
1) 不压缩数据直接存储。 //enum.NONE
2) 压缩value值不压缩key值存储的存储方式。//enum.RECORD
3) key/value值都压缩的方式存储。//enum.BLOCK

SequenceFile提供了若干Writer的构造静态获取。
//SequenceFile.createWriter();

SequenceFile.Reader使用了桥接模式,可以读取SequenceFile.Writer中的任何方式的压缩数据。

三种不同的压缩方式是共用一个数据头,流方式的读取会先读取头字节去判断是哪种方式的压缩,然后根据压缩方式去解压缩并反序列化字节流数据,得到可识别的数据。

流的存储头字节格式:
Header:
*字节头”SEQ”, 后跟一个字节表示版本”SEQ4”,”SEQ6”.//这里有点忘了 不记得是怎么处理的了,回头补上做详细解释
*keyClass name
*valueClass name
*compression boolean型的存储标示压缩值是否转变为keys/values值了
*blockcompression boolean型的存储标示是否全压缩的方式转变为keys/values值了
*compressor 压缩处理的类型,比如我用Gzip压缩的Hadoop提供的是GzipCodec什么的..
*元数据 这个大家可看可不看的

所有的String类型的写操作被封装为Hadoop的IO API,Text类型writeString()搞定。

未压缩的和只压缩values值的方式的字节流头部是类似的:
*Header
*RecordLength记录长度
*key Length key值长度
*key 值
*是否压缩标志 boolean
*values
package com.netease.hadooplucene.lucene.test;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.lang.String;
import java.lang.Object;
import java.net.URI;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.conf.*;
import org.apache.hadoop.io.*;
import org.apache.hadoop.mapred.*;
import org.apache.hadoop.util.*;
import org.apache.hadoop.io.compress.*;
import org.apache.hadoop.fs.*;
import com.netease.hadooplucene.lucene.util.HadoopService;
public class SequenceFileTest {
    
    
    
    public static void writeSequceFile(String uri)throws IOException{
        FileSystem fs = HadoopService.getInstance().getHDFSClient();
        Path path = new Path(uri);
        IntWritable key = new IntWritable();
        Text value = new Text();
        SequenceFile.Writer writer = null;
        try {
            writer = SequenceFile.createWriter(fs, fs.getConf(), path, key.getClass(), value.getClass());
            for (int i = 0; i < 100; i++) {
                String tt = String.valueOf(100 - i);
                key.set(i);
                value.set(tt);
                System.out.printf("[%s]/t%s/t%s/n", writer.getLength(), key, value);
                writer.append(key, value);
            }
        } finally {
            IOUtils.closeStream(writer);
        }
    }
    
    
//    public static File getFile(String uri){
//        FileSystem fs = HadoopService.getInstance().getHDFSClient();
//        Path path = new Path(uri);
//        SequenceFile.Reader reader=new SequenceFile.Reader(fs, path, fs.getConf());
//        reader.
//        reader.
//    }
    public static void main(String[] args) throws IOException {
        String uri = args[0];
//        Configuration conf = new Configuration();
//        FileSystem fs = FileSystem.get(URI.create(uri), conf);
        
    }
}

前天项目组里遇到由于sequenceFile 的压缩参数设置为record 而造成存储空间的紧张,后来设置为block 压缩方式的压缩方式,存储空间占用率为record 方式的1/5 。问题虽解决了,但是还不是很清楚这两种方式是如何工作以及他们的区别是啥。昨天和今天利用空闲时间,细细的看了一遍sequenceFile 这个类和一些相关类的源码。

sequenceFile 文件存储有三种方式:可以通过在程序调用 enum CompressionType { NONE , RECORD , BLOCK } 指定,或通过配置文件io.seqfile.compression.type 指定,这三种存储方式如下图:

 

 

 

对于Record 压缩这种存储方式,RecordLen 表示的是key 和value 的占用的byte 之和,Record 压缩方式中 key 是不压缩 ,value 是压缩后的值,在record 和非压缩这两种方式,都会隔几条记录插入一个特殊的标号来作为一个同步点Sync ,作用是当指定读取的位置不是记录首部的时候,会读取一个同步点的记录,不至于读取数据的reader “迷失”。 每两千个byte 就插入一个同步点,这个同步点占16 byte ,包括同步点标示符:4byte ,一个同步点的开销是20byte 。

对于block 这种压缩方式, key 和value 都是压缩的 ,通过设置io.seqfile.compress.blocksize 这个参数决定每一个记录块压缩的数据量,默认大小是1000000 byte ,这个值具体指的是key 和value 缓存所占的空间,每要往文件写一条key/value 时,都是将key 和value 的长度以及key 和value 的值缓存在keyLenBuffer keyBuffer valLenBuffer valBuffer 这四个DataOutputStream 中,当keyBuffer.getLength() + valBuffer.getLength() 大于或等于io.seqfile.compress.blocksize 时,将这些数据当做一个block 写入sequence 文件,如上图所示 每个block 之间都会插入一个同步点。

 

### 小文件问题的成因与影响 Hadoop 的 HDFS(Hadoop Distributed File System)最初设计用于处理大规模数据集,适合存储和处理大文件。当系统中存在大量小文件时,HDFS 会面临性能瓶颈[^3]。 每个文件、目录和数据块在 HDFS 中都对应一定的元数据信息,这些信息都存储在 NameNode 的内存中。当系统中存在大量小文件时,NameNode 需要维护的元数据条目急剧增加,导致内存消耗过大,影响集群稳定性[^3]。 此外,HDFS 的设计目标是高效处理大文件,对于小文件而言,打开文件所需的寻址时间可能远大于实际读取数据的时间,造成资源浪费并降低整体性能[^3]。 --- ### 解决方案概述 #### 1. 文件合并策略 Hadoop 提供了多种工具用于将多个小文件合并为较大的文件,从而减少 NameNode 的元数据负担,并提高数据访问效率。 - **Hadoop Archive(HAR)**:HAR 是一种将多个小文件打包成一个归档文件的机制。使用 `hadoop archive` 命令可以创建 HAR 文件,它本质上是一个 MapReduce 作业,将多个小文件合并为一个大文件,并保留原始路径结构。用户仍然可以通过类似 `/user/output/data.har#filename` 的方式访问原始文件内容[^2]。 - **SequenceFile**:这是一种键值对形式的二进制文件格式,适用于将多个小文件合并为一个或多个 SequenceFile 文件。这种方式不仅减少了文件数量,还能提升后续 MapReduce 任务的处理效率。例如,可以编写 Java 程序将本地文件逐个读取,并以文件名作为 Key、文件内容作为 Value 存储SequenceFile 中。 ```java Configuration conf = new Configuration(); FileSystem fs = FileSystem.get(conf); Path outputPath = new Path("hdfs:///output/sequencefile"); SequenceFile.Writer writer = SequenceFile.createWriter(fs, conf, outputPath, Text.class, BytesWritable.class); File dir = new File("local_dir/"); for (File file : dir.listFiles()) { FileInputStream fis = new FileInputStream(file); byte[] content = IOUtils.toByteArray(fis); writer.append(new Text(file.getName()), new BytesWritable(content)); } writer.close(); ``` - **CombineFileInputFormat**:该输入格式允许单个 Map 任务处理多个小文件,从而减少 Map 任务的数量,提高处理效率。相比 SequenceFile 和 HAR,它更适合于直接处理原始文件而不进行预合并的场景。 #### 2. 使用 HDFS Federation 分散元数据压力 HDFS Federation 允许多个 NameNode 管理不同的命名空间,将原本集中在单一 NameNode 上的压力分散到多个节点上。这样可以有效缓解因小文件过多导致的 NameNode 内存瓶颈问题[^4]。 #### 3. 使用外部系统管理小文件 某些场景下,可以将小文件存储在专门处理海量小文件的系统中,如 HBase、Ozone 或对象存储系统(如 Amazon S3、阿里云 OSS),仅在需要批量处理时导入 HDFS。这种架构方式可以避免直接在 HDFS 中处理小文件带来的性能问题。 #### 4. Hive 小文件自动合并 在 Hive 表中启用 ACID 特性后,可以通过合并小文件的方式优化存储和查询性能。Hive 提供了自动合并小文件的功能,支持将多个小文件合并为较大的 ORC 或 Parquet 文件。以下是一些常用配置参数: ```sql SET hive.compactor = true; SET hive.compactor.mapreduce.map.memory.mb = 2048; SET hive.compactor.mapreduce.reduce.memory.mb = 2048; SET hive.compactor.num mapper.tasks = 10; SET hive.compactor.num reducer.tasks = 5; ``` 启用 Hive Compactor 后,系统会定期检查表中的小文件,并将其合并为较大的文件,提升查询效率。 --- ### 小文件合并技术对比 | 技术 | 优点 | 缺点 | |------|------|------| | HAR | 减少 NameNode 元数据压力,保留原始路径结构 | 访问需通过特殊路径,不支持追加写入 | | SequenceFile | 支持高效的 MapReduce 处理,便于序列化 | 无法直接查看文件内容,需额外程序解析 | | CombineFileInputFormat | 不改变文件结构,减少 Map 任务数 | 对非 MapReduce 场景帮助有限 | | HDFS Federation | 横向扩展 NameNode,缓解内存瓶颈 | 架构复杂度增加,运维难度上升 | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值