#46 大数据计算 中等

Hive/Spark 里的小文件问题是什么?怎么产生、有什么危害、怎么治理?

小文件动态分区NameNode文件合并

🎯 考察意图

考察候选人是否把「小文件」理解成系统问题(元数据压力 + 任务启动开销),而不是一句「文件碎了不好」。要能说来源(动态分区、流式小批、错误分区粒度)、危害,以及写侧治理和读侧兜底怎么分工。

📐 回答框架

  1. 先定义:相对 HDFS 块大小或目标文件大小,大量远小于块的文件才叫小文件问题。
  2. 说危害:NameNode/Driver 元数据膨胀,任务数爆炸,读放大。
  3. 说来源:动态分区过细、流式/高并发各写一小份、repartition 过多、上游没合并。
  4. 给治理:控分区粒度、写时合并(coalesce/目标文件数)、定期 compact;读时 combine 只是止痛。

💬 参考答案(口述版)

一句话:小文件伤的是元数据和调度,不是磁盘上看起来「碎」。文件数远大于必要、每个文件远小于块大小(常见 128MB),作业就会又慢又容易把 NameNode 或 Driver 打满。

危害三件套。
1. 元数据:HDFS 上每个文件、每个 block 都占 NameNode 内存;表有几百万个文件时,NN 先撑不住,Spark Driver 列分区文件也会 OOM。
2. 调度:Map/Task 默认一个 split 一份文件(或接近),十万小文件就是十万任务,启动和心跳比计算还贵。
3. 读放大:列存 footer、压缩块、连接开销都被放大,扫描同样数据比大文件慢一个数量级。

怎么来的。 最常见是动态分区写:INSERT ... PARTITION (dt, hour, city) 再叠并发,每个 task 每个分区各落一个文件,一天轻松几十万文件。其次是流式微批每分钟写一次、上游 Kafka 分区过多直接落到表、错误地把用户 id 当分区键。Spark 里 repartition(2000) 再写一张小表,也会故意造小文件。

治理分写侧和读侧。 写侧才是根治:
- 分区粒度对齐查询:按天够用就不要按小时+城市;高基数维放字段别放分区。
- 控制输出文件数:按目标大小估 目标文件数 ≈ 分区数据量 / 128MB,用 coalesce 或 repartition 落到这个数,而不是 task 数有多少写多少。
- Hive 可用 hive.merge.mapfiles / combinehiveinputformat;Spark 写完对热分区做 compact(读出再按目标文件数覆盖写回)。

Spark 写日分区的口述级伪代码:

-- 先估:该 dt 约 20GB → 约 160 个 128MB 文件,不要按 2000 task 直写
INSERT OVERWRITE TABLE ads_gmv PARTITION (dt='2026-10-18')
SELECT ... ;  -- 上游先 coalesce 到合理文件数再落盘

读侧 CombineFileInputFormat / 小文件合并读只能减轻本次扫描,文件还在 NN 上。口述收尾:先讲「分区粒度和输出文件数」,再主动提「compact 是定期作业,coalesce(1) 会变成超大文件另一个坑」。

🔍 关键词

小文件NameNode 元数据动态分区coalescecompact

💭 面试官可能的追问

追问 1:动态分区为什么特别容易制造小文件?怎么改写入方式?

每个 Spark/Hive task 都会在它碰到的每个分区目录下各写一个文件。分区键越细、并发越高,文件数 ≈ task 数 × 非空分区数。改法:能静态分区就按 dt 一次写一个分区;必须动态分区时,先按分区键 shuffle 再在分区内 coalesce,让每个分区只有少数文件。禁止「千并发直写小时+城市」这种组合。

追问 2:coalesce 和 repartition 合并小文件有什么区别?什么时候必须 shuffle?

coalesce 只减少分区、尽量不 shuffle,适合「文件略多、数据已经比较均匀」;repartition 会 shuffle,能打散倾斜也能精确指定文件数,代价是一次全量交换。数据严重倾斜或要从 10000 份合成均匀的 200 份,用 repartition;刚刚写完、只是每个 task 多了一个小尾巴,用 coalesce。千万别 coalesce(1) 把整天数据塞进单文件、单 task。

追问 3:小文件合并会不会越合并越好?和下游读取怎么平衡?

不会。文件太大(几个 GB 甚至撑满单 task 内存)会让重试和推测执行变贵,列存 row group 也不好切。目标是接近块大小或略大于扫描并行度需要的 split。并发查询多的 ADS 可以略多几个中等文件;一次性离线重跑的 DWD 可以更靠近 128–256MB。合并作业本身也要幂等:按分区覆盖写,别在原目录 append 出更多碎片。

⚠️ 常见踩坑

  • 把用户 id、订单 id 当分区键,分区数随数据爆炸,小文件无法避免
  • 用 coalesce(1) 当「合并」,变成单文件单 task,把小文件问题换成超大文件问题
  • 只在读作业开 combine,不治理表上的文件,NameNode 和下游作业仍然爆炸
  • 流式每分钟 append 一个文件却不做分区 compact,一周后表不可用

🧠 记忆锚点

小文件伤的是元数据和调度;分区粒度和输出文件数一起控,读时合并只止痛、不治本。