Skip to content

Hive 小文件治理 ​

难度与前置

难度:中级
前置:会看分区路径下文件数;能改写入并行度或跑合并作业。
读完能做什么:定位小文件来源,选预防+治理组合,并用文件数/查询耗时验收。

适用场景 ​

  • NameNode / 元数据压力大,或列存扫描「文件打开」占比高
  • 分区内成百上千个 MB 级以下文件,SQL 无故变慢
  • 流式小批量、动态分区、多 Reduce 写出导致碎片化

不适用:

  • 冷数据即将归档删除(优先生命周期,而不是合并)
  • 单分区总数据量本身极小(强行合成大文件收益有限)
  • 对象存储上已用最优大对象写入且查询引擎已做脚注优化(先测再治)

真实场景(一条) ​

dwd_ad_spend_detail_di 每小时微批写入,一天 24 个作业 × 200 个 task,单分区出现 约 4000 个小 ORC 文件。早报 SQL 扫 7 天:CPU 不高但 任务启动与分片规划极慢,P95 从 3 分钟升到 18 分钟。目标:写入侧控文件数 + 日终合并,分区文件数降到百级以内,查询回落。

怎么做(步骤清单) ​

1. 先量化:什么叫「小」 ​

团队内写死阈值(示例,可按存储调):

指标告警参考
单文件大小中位数 < 64MB(ORC/Parquet)
单分区文件数> 200(热表)或 > 1000(一律要治)
小文件占比size < 32MB 的文件数占比 > 50%
bash
# HDFS:统计某分区文件数与大小
hdfs dfs -count /warehouse/dwd/ad_spend_detail_di/dt=${biz_date}
hdfs dfs -ls /warehouse/dwd/ad_spend_detail_di/dt=${biz_date} | awk '{print $5}' | sort -n | head
sql
-- 若有文件级元数据表(示意)
SELECT
  dt,
  COUNT(*) AS file_cnt,
  AVG(file_bytes) AS avg_bytes,
  PERCENTILE_APPROX(file_bytes, 0.5) AS p50_bytes
FROM meta_table_files
WHERE table_name = 'dwd_ad_spend_detail_di'
  AND dt = '${biz_date}'
GROUP BY dt;

2. 分清来源(决定治哪一侧) ​

来源症状优先手段
写入并行度过高文件数 ≈ map/reduce/task 数降并行、增大分区写入量
动态分区扇出每分区每 task 一个文件优化动态分区 + 合并
流式/近实时小批每批一个小文件微批合并、落地后 compaction
多次 INSERT 追加文件只增不减overwrite 分区或定期合并

3. 写入侧预防(治本) ​

Spark 示意:

scala
// 控制输出文件数:按目标文件大小估算 repartition
// 目标:单文件 ~128MB,分区数据量 Q MB → 文件数 ≈ max(1, Q/128)
df.repartition(numFiles)
  .write.mode("overwrite")
  .option("compression", "snappy")
  .orc(path)

Hive / Spark SQL:

sql
-- 合并小文件相关常见参数(按引擎版本取舍)
SET hive.merge.mapfiles=true;
SET hive.merge.mapredfiles=true;
SET hive.merge.size.per.task=268435456;  -- 256MB
SET hive.merge.smallfiles.avgsize=134217728;

-- Spark:AQE 合并小分区(与 aqe-shuffle 文配合)
-- spark.sql.adaptive.enabled=true
-- spark.sql.adaptive.coalescePartitions.enabled=true

原则:

  • 先估数据量再定并行度,不要「默认 200」写小分区
  • 热分区优先 INSERT OVERWRITE 整分区,避免无限追加
  • 近实时写入:ODS 可碎,DWD 日终合并成查询友好布局

4. 存量治理:合并作业 ​

sql
-- Hive ORC:同分区合并(注意锁与读查询影响)
ALTER TABLE dwd_ad_spend_detail_di
  PARTITION (dt='${biz_date}')
  CONCATENATE;

Spark rewrite 示意:

sql
INSERT OVERWRITE TABLE dwd_ad_spend_detail_di PARTITION (dt='${biz_date}')
SELECT /*+ REPARTITION(32) */ *
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}';

排期:

  • 冷分区:空闲窗口批量合并
  • 热分区:日终必做;合并时与读写互斥或用新路径替换

5. 监控与门禁 ​

  • 表级指标:files_per_partition、p50_file_size
  • 写出作业出门禁:超过阈值则失败或自动触发 compaction
  • 与 慢 SQL 联动:规划时间异常先查小文件

常见坑(至少牢记这些) ​

  1. 只合并不改写入
    第二天又碎。预防 + 治理必须一起上。

  2. 合并并行度仍过大
    rewrite 时 repartition(1000) 越合并越碎。按目标文件大小反算。

  3. 合并窗口撞上早报
    合并占用 IO,早报变慢。放到 SLA 后或独立队列。

  4. 忽略压缩与条纹群组
    文件数下来了但 row group 不合理,收益有限。ORC/Parquet 写时带合理 stripe/block。

  5. 动态分区 + 小维度值
    一个 task 写上百分区各一个小文件。先分布再写,或限制动态分区数。

如何验收(可量化) ​

  • [ ] 热表有文件数/大小阈值与监控
  • [ ] 写入作业有并行度估算或 AQE coalesce
  • [ ] 抽测分区:文件数 < 阈值,p50 文件大小 ≥ 约定
  • [ ] 同一 SQL 合并前后:规划时间与总耗时下降可量化
  • [ ] 合并后行数/金额 checksum 与合并前一致
sql
-- checksum 验收
SELECT COUNT(*) AS cnt, SUM(cost_amount) AS cost, SUM(hash(bill_id)) AS h
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}';
-- 合并前后三值一致(允许 hash 函数差异时改用业务校验和)
bash
# 合并后文件数
hdfs dfs -count /warehouse/dwd/ad_spend_detail_di/dt=${biz_date}
# 期望:文件数显著下降且 CONTENT_SIZE 与合并前接近

相关面试题 ​

占位:与题库小程序对齐后补链接(建议关联:小文件问题、Hive 合并、写入并行度)。


改写说明:结构参考 Wood 原创 Hive 治理实践,已改写成可执行清单;未包含未公开集群路径。