小文件问题剖析
做数据湖仓的人,迟早都会撞上小文件这堵墙。我最早在 Hadoop 时代就被它坑过——一个看似正常的 Hive 表,跑查询时卡了半小时,最后发现是几万个几 KB 的小文件在作祟。今天咱们就把小文件问题彻底拆开,看看它到底从哪来,又怎么拖垮性能和成本。
小文件产生的根源
小文件不是凭空冒出来的。我总结下来,主要有四个源头:
- 流式写入的天然缺陷——Kafka、Pulsar 这类消息系统,数据是一条一条来的。每次微批次提交,如果触发频率高、数据量小,就会生成大量小文件。我见过一个实时数仓,每 30 秒提交一次,一天下来文件数超过 10 万。
- 分区策略过于激进——按小时甚至按分钟分区,每个分区里数据量少得可怜。每个分区至少一个文件,分区一多,文件数就爆炸了。
- 并发写入缺乏合并——多个 Spark Executor 同时写同一个表,每个 Task 独立生成文件。如果 Task 数设得大,文件数直接翻倍。
- 更新删除操作产生碎片——Delta Lake、Iceberg 这类湖仓格式,每次 UPDATE/DELETE 都会生成新的数据文件,旧文件标记为删除。频繁的变更操作会让文件碎片越积越多。
核心结论:小文件的本质是「写入粒度」和「存储粒度」不匹配。写入端希望低延迟,存储端希望大文件,两者天然矛盾。
小文件对查询性能的影响
这个问题我感触特别深。记得有一次帮客户排查一个 Iceberg 表查询慢的问题,表里只有 200GB 数据,但文件数超过 5 万个。跑一个简单的 COUNT(*) 都要 3 分钟。
为什么会这样?说白了,查询引擎在读取数据前,要先做「文件发现」和「元数据操作」:
- 文件列表拉取——HDFS 或对象存储的 List 操作,文件越多耗时越长。5 万个文件 List 一次可能要几十秒。
- 元数据解析——每个文件都有对应的统计信息、Min/Max 值、列索引。文件多了,元数据膨胀,解析开销直线上升。
- Task 调度开销——Spark 或 Trino 会为每个文件生成一个 Task。Task 的调度、序列化、启动都有固定成本。文件越多,Task 越多,但每个 Task 处理的数据量却很少。
- 随机 IO 放大——小文件在 HDFS 上通常分散在不同的 DataNode 上。查询时需要跨多个节点拉取数据,网络开销远大于顺序读取一个大文件。
| 文件数量 | 查询耗时(COUNT *) | Task 数 | 元数据加载时间 |
|---|---|---|---|
| 100 个 | 2 秒 | 10 | 0.1 秒 |
| 1,000 个 | 8 秒 | 50 | 0.8 秒 |
| 10,000 个 | 45 秒 | 200 | 6 秒 |
| 50,000 个 | 180 秒 | 500 | 25 秒 |
避坑指南:我曾经遇到一个案例,表里只有 50GB 数据,但文件数 8 万个。查询性能比同数据量但只有 200 个文件的表慢了 40 倍。这不是夸张,是真实踩过的坑。
小文件对存储成本的浪费
存储成本这块,很多人只算了「数据量」,没算「文件开销」。我给大家算笔账:
- 元数据存储膨胀——每个文件在 Hive Metastore、HDFS NameNode、Iceberg 的 Manifest 里都有记录。5 万个文件,光元数据就要占几百 MB。NameNode 内存有限,文件数一多,直接撑爆。
- 块对齐浪费——HDFS 默认块大小 128MB。一个 10KB 的小文件,实际占用 128MB 的块空间。虽然 HDFS 有「空洞」机制,但 NameNode 的块记录不会少。对象存储虽然按实际大小计费,但请求次数多了,API 调用费也是一笔开销。
- 压缩效率下降——小文件通常无法有效压缩。Snappy 对一个 10KB 的文件压缩,压缩率可能只有 1.2 倍;但同样数据合并成 128MB 文件,压缩率能达到 3-4 倍。存储空间直接翻倍。
- 生命周期管理复杂——文件多了,归档、清理、备份都变得困难。每次操作都要遍历大量文件,耗时耗力。
注意:对象存储虽然按实际大小计费,但 PUT/GET 请求次数是收费的。一个表每天产生 10 万个小文件,光请求费一个月就能多花几百美元。别只看存储量,请求费才是隐藏的大头。
小文件问题的全景图
下面这张图,我把小文件问题的根源、影响和后果串在了一起。你一看就明白:
一个真实的案例
去年我帮一家金融科技公司做数据湖仓优化。他们的交易流水表每天新增 200GB 数据,用的是 Delta Lake 格式。因为流式作业每 1 分钟提交一次,加上 50 个并发 Task,一天下来文件数从 2 万涨到 8 万。
后果是什么?
- 查询延迟从 5 秒飙升到 3 分钟
- NameNode 内存使用率从 60% 涨到 95%
- S3 的 GET 请求费用一个月多了 800 美元
后来我们做了 Compaction,把文件合并到 128MB 左右,文件数降到 2000 以内。查询性能恢复到了 8 秒,NameNode 也稳了。你想想看,这还只是一个表。如果整个湖仓都有小文件问题,那场景得多可怕。
我的建议:小文件问题一定要在架构设计阶段就考虑进去。别等到线上出问题了再补救。Compaction 不是万能的,但没有 Compaction 是万万不能的。