外观
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 | headsql
-- 若有文件级元数据表(示意)
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 联动:规划时间异常先查小文件
常见坑(至少牢记这些)
只合并不改写入
第二天又碎。预防 + 治理必须一起上。合并并行度仍过大
rewrite 时repartition(1000)越合并越碎。按目标文件大小反算。合并窗口撞上早报
合并占用 IO,早报变慢。放到 SLA 后或独立队列。忽略压缩与条纹群组
文件数下来了但 row group 不合理,收益有限。ORC/Parquet 写时带合理 stripe/block。动态分区 + 小维度值
一个 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 治理实践,已改写成可执行清单;未包含未公开集群路径。