考察点
小文件是 HDFS 经典问题,面试官想确认你理解小文件为什么有害(不只是「文件多」),以及有没有真正动手治理过。追问方向:为什么小文件伤 NameNode、合并会不会影响正在读的任务、Spark 写 Hive 表的小文件怎么处理。
参考答案
小文件为什么有害
小文件一般指远小于 HDFS block 大小(128M 或 256M)的文件,比如几 KB 到几 MB。危害在两端:
- 存储端压垮 NameNode:HDFS 上每个文件、每个 block 的元数据都存在 NameNode 内存里,一个文件(含它的 block)大约占几百字节元数据。一亿个小文件就是几十 GB 的 NameNode 堆内存,加上目录、副本的元数据,NameNode 内存吃满后整个集群读写都变慢,这是集群级事故。
- 计算端放大任务开销:MR/Spark 读数据时按 input split 切任务,一个小文件基本对应一个 split 一个 map 任务。10 万个小文件就是 10 万个 map,每个 map 启动 JVM 或容器都要秒级开销,真正干活的时间可能不到任务启动时间的十分之一。Hive 表的查询还会因为文件数多导致 Driver 端 list 目录、get splits 阶段耗时暴涨。
小文件的三个成因
- reduce 数量与数据量不匹配。每个 reduce 输出一个文件,一个 reduce 只处理几 MB 数据,就产出一堆小文件。reduce 数由
hive.exec.reducers.bytes.per.reducer(默认 256M)推算,但 map 端 map join 后没有 reduce 的任务,文件数等于 map 数;用了distribute by强行指定 reduce 数的场景也容易碎。 - 动态分区放大文件数。动态分区下,每个 reduce 会为它遇到的每个分区值各建一个文件。reduce 数乘分区数就是文件数,100 个 reduce 写 365 个分区就是 36500 个文件,这是数仓最常见的小文件来源。
- 实时/高频写入。Flink、Spark Streaming、Flume 滚动写 HDFS 或 Hive 表,checkpoint 间隔 10 分钟一个文件,一天 144 个文件起步,乘上分区数更多。
写入时治理:从源头控制
- 合理设置 reduce 数:让单个 reduce 输出接近 block 大小。按估算调
hive.exec.reducers.bytes.per.reducer,或者对确定数据量的任务直接set mapreduce.job.reduces=N。 - 开启输出合并:Hive 自带参数
hive.merge.mapfiles=true(合并 map-only 输出)、hive.merge.mapredfiles=true(合并有 reduce 的输出)、hive.merge.size.per.task和hive.merge.smallfiles.avgsize控制合并目标大小。任务跑完自动追加一个合并 job。 - 输入侧也有对应参数:
CombineHiveInputFormat(默认已开)把小文件在读取时打包进一个 split,缓解计算端的问题,但不解决 NameNode 压力。
存量治理:定期合并
已经产生的小文件要靠定期任务收编:
- 最朴实的做法是
insert overwrite table t partition(ds=...) select * from t where ds=...,配合控制 reduce 数,把一堆小文件重写成少量大文件。数据量大的话成本高,一般只合并最近 N 天。 - 滚动归档:把 30 天前的分区用 Hadoop Archive(HAR)打包,元数据减一个数量级,查询还能直接读 har:// 路径,代价是读取要走两层索引。
- 实时写入场景,用 Hudi/Iceberg/Delta 这类表格式替代裸 Hive 表,它们有内置的 compaction 机制,小文件合并是引擎自己管的。
一个注意事项
合并任务用 overwrite 重写分区时,如果同时有查询在读这个分区,可能出现读到的文件被删除的情况(HDFS 上文件被删后已打开的句柄还能读完,但新 list 到的文件集就变了)。生产上合并任务一般安排在低峰期,或者对表加读写隔离。
可能的追问
- 怎么发现集群里哪些表小文件严重? 用
hadoop fs -count或 NameNode 的 fsimage 分析工具(如解析 fsimage 导出文件分布报表),按目录统计文件数和平均文件大小,平均小于 10MB 的表列入治理清单。 - 为什么小文件查询时 get splits 也慢? InputFormat 计算 split 时要对每个文件做一次 RPC 拿 block 位置,10 万文件就是 10 万次 NameNode 交互,Driver 端可能卡几分钟。这也是小文件拖慢查询的隐藏因素。
- Spark 写 Hive 表的小文件怎么处理? 写完前
repartition(N)或coalesce(N)控制输出文件数;也可以用spark.sql.adaptive的合并小分区能力,或者落地后跑 Hive 的合并任务兜底。