HDFS 小文件处理全攻略:问题到底在哪?怎么治?
HDFS 对大文件很友好,但小文件是它的”天敌”。
几 KB 的日志、几 MB 的图片、一堆零散的小 JSON——如果数量到了百万级,HDFS 的性能就会急剧下降。不是磁盘不够,是 NameNode 的内存扛不住。
小文件为什么是问题?根源在 NameNode
小文件的危害,根源是 HDFS 的架构设计。
1. NameNode 内存被撑爆
每个文件/目录的元数据在 NameNode 内存中大约占 150 字节。看起来不大,但数量上去就恐怖了:
- 1 亿个小文件 → 约 15GB 内存
- 10 亿个小文件 → 约 150GB 内存
NameNode 的内存是有限的(通常 64GB-256GB),元数据占满了,整个集群就不稳定了。
2. 计算框架的任务爆炸
MapReduce、Spark 通常按块划分任务。1 亿个 1MB 小文件 → 1 亿个 Map 任务。任务启动、调度、销毁的开销远超实际计算时间,作业跑不动。
3. 存储浪费
1MB 文件在 128MB 块大小下,磁盘只占 1MB,但元数据按一个块算(约 150 字节)。浪费的是 NameNode 内存,不是磁盘空间。
治本之道:源头不要产生小文件
这是最高效的方案——数据进 HDFS 之前就合并好。
1. 应用程序批量写入
业务程序不要频繁创建小文件。攒到一定大小再写,比如每 128MB 或每 5 分钟写一个文件。
2. Flume 配置滚动策略
日志采集阶段就做好合并,别让几 KB 的小文件直接进 HDFS:
1 | # 文件达到 128MB 或 300 秒后滚动 |
3. Sqoop 控制输出文件数量
导入数据时控制并行度,别生成一堆小文件:
1 | sqoop import --split-by id --num-mappers 4 |
事后补救:已经存在的小文件怎么处理
如果小文件已经在了,就得靠工具合并。
方案1:HAR(Hadoop Archive)——归档,不改数据格式
HAR 把多个小文件打包成一个归档文件,NameNode 只存一条元数据。
1 | 创建归档 |
优点: 操作简单,不用改数据格式。
缺点: 归档后不能修改文件(只读),只能删除重建。
适合场景: 冷数据归档,不常访问的小文件。
方案2:SequenceFile——二进制合并,支持随机读写
SequenceFile 是 Hadoop 自带的二进制格式,把多个小文件以 key-value 形式写到一个大文件里。
适合场景: 需要频繁读写的小文件集合(如图片、文档)。
缺点: 需要写代码,不是纯文本格式,不便于直接查看。
示例代码(Java)
1 | // 合并小文件到 SequenceFile |
方案3:ORC / Parquet——列式存储,结构化数据首选
把多个小文件的结构化数据合并到一个 ORC/Parquet 文件里,既解决了小文件问题,又提升了查询性能。
1 | -- Hive 里把文本表转成 ORC |
适合场景: 结构化数据(日志、数据库表),需要频繁查询分析的场景。
优点: 压缩率高、支持索引、查询快。
缺点: 只适合结构化数据,不是通用文件格式。
计算阶段临时处理:不动 HDFS,只优化任务
如果小文件已经存在,但你不改存储,也可以在计算阶段做优化。
CombineTextInputFormat:把多个小文件合并成一个 Split
MapReduce 默认一个文件一个 Split。换成 CombineTextInputFormat 后,多个小文件会被合并成一个虚拟 Split,减少 Map 任务数量。
1 | // 在 Driver 类中设置输入格式 |
优点: 不改存储,配置简单。
缺点: 只优化计算阶段,NameNode 内存问题没解决。
适合场景: 一次性处理任务,不想动存储。
JVM 重用:减少任务启动开销
小文件多 → Map 任务多 → JVM 频繁启停。开 JVM 重用,一个 JVM 处理多个任务。
1 | <property> |
注意: 取值别太大(建议 10-20),否则 JVM 内存可能泄漏。