订阅更新

小文件太多怎么解决?——OPTIMIZE和Compaction

数据湖手册 第 16 题 约 8 分钟读完

本文目录
  1. 先说结论
  2. 真实案例:小文件导致查询超时
  3. 小文件问题详解
  4. Delta Lake OPTIMIZE详解
  5. Hudi Compaction详解
  6. 小文件问题对比
  7. 面试高频追问
  8. 实战踩坑:OPTIMIZE失败
  9. 总结

先说结论

小文件问题的原因:每个分区一个文件、高频小批量写入、动态分区产生大量小文件。一个文件对应一个HDFS block,NameNode内存压力大,Driver打开文件慢。

OPTIMIZE命令:Delta Lake的OPTIMIZE命令合并小文件,默认128MB一个文件。支持Z-Ordering同时进行数据排序。

Compaction:Hudi的Compaction服务,MOR表类型的Log文件定期合并成Parquet文件。支持同步和异步模式。

最佳实践:写入时合理设置分区数、定期OPTIMIZE每周一次、监控文件平均大小、设置autosmallfilesize自动合并.

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

这道题面试官问得很多,因为小文件问题是数据湖最常见的问题之一。我在某出海硬件品牌遇到过因为小文件太多,一个查询花了2小时的问题。很多人只知道需要合并文件,但说不清楚什么时候合并、怎么合并。面试官想听的是你能说清楚原理和实战经验.

真实案例:小文件导致查询超时

2023年在某知名出海硬件品牌,我们的用户行为表user_behavior有10万个分区,每个分区只有一个1MB的小文件.

一个简单的COUNT查询花了2小时,业务部门无法接受.

排查发现:表有10万个分区,每个分区1个文件,Driver需要打开10万个文件。NameNode元数据操作耗时,Driver内存不足。

# 表结构
/data/user_behavior/dt=2024-03-20/hour=00/part-00001.parquet  # 1MB
/data/user_behavior/dt=2024-03-20/hour=01/part-00001.parquet  # 1MB
# ... 共10万个小时分区,每个分区一个1MB文件

解决方案:我们重新设计分区策略,去掉hour分区,只保留dt分区,然后用OPTIMIZE合并文件。

-- 步骤1:重建表,去掉hour分区
CREATE TABLE user_behavior_new
PARTITIONED BY (dt STRING)
AS SELECT * FROM user_behavior;
 
-- 步骤2:OPTIMIZE合并小文件
OPTIMIZE user_behavior_new;
 
-- 结果:10万个文件合并成100个文件,每个文件128MB

改进效果:查询时间从2小时降到3分钟,提升40倍.

小文件问题详解

为什么小文件是问题:每个Parquet文件对应一个HDFS block,默认128MB。1MB的文件浪费127MB空间。NameNode需要为每个文件维护元数据,内存压力大。Spark Driver需要打开所有文件获取统计信息,耗时且容易OOM。

小文件的来源:1)动态分区:每个动态分区值创建一个文件;2)高频写入:每分钟写入产生大量小文件;3)Spark默认:200个shuffle分区,每个分区一个文件。

判断小文件的标准:文件大小小于128MB的视为小文件。可以通过DESCRIBE TABLE EXTENDED查看文件数量和平均大小。

-- 查看表的文件统计
DESCRIBE EXTENDED user_behavior;
-- 输出包含:numFiles、sizeInBytes等

Delta Lake OPTIMIZE详解

基本用法:OPTIMIZE table_name合并整个表的小文件。OPTIMIZE table_name WHERE partition_condition只合并指定分区。

-- 合并整个表
OPTIMIZE user_behavior;
 
-- 只合并特定分区
OPTIMIZE user_behavior WHERE dt = '2024-03-20';
 
-- Z-Ordering同时排序数据
OPTIMIZE user_behavior ZORDER BY (user_id, event_type);

OPTIMIZE原理:Delta Lake读取表的小文件,重写成128MB的大文件。通过bin-packing算法优化,减少文件数量。Z-Ordering按指定列排序数据,提升查询性能。

配置参数:delta.targetFileSize默认128MB、delta.autoOptimize.optimizeAutoEnabled默认false、delta.autoOptimize.autoCompactPartitionSize默认128MB。

-- 启用自动OPTIMIZE
ALTER TABLE user_behavior SET TBLPROPERTIES (
  'delta.autoOptimize.optimizeAutoEnabled' = 'true'
);

Hudi Compaction详解

MOR表类型:Merge On Read表,写入时先写Log文件,定期Compaction合并成Parquet文件。适合高频更新删除场景。

同步Compaction:写入时立即执行Compaction,数据可见但写入延迟高。适合对延迟敏感的场景。

异步Compaction:单独的Compaction服务定期执行,写入快但数据有延迟。适合高吞吐场景。

-- 同步Compaction
hoodie.upsert.shuffle.parallelism = 200
hoodie.compact.inline = 'true'
 
-- 异步Compaction
hoodie.compact.inline = 'false',
hoodie.schedule.compact.inline = 'true',
-- 需要单独运行Compaction任务

Compaction策略:NumCommits:每N次提交执行一次;TimeElapsed:每隔N小时执行一次;CommitsSize:Log文件总大小超过阈值时执行。

小文件问题对比

问题症状原因解决方案
NameNode压力大元数据操作慢文件数量多合并小文件
Driver打开文件慢查询初始化慢Driver逐个打开文件OPTIMIZE合并
网络请求多数据读取慢大量小文件导致多次请求增大文件大小
空间浪费存储利用率低小文件block未满合并到128MB
Task数量多调度开销大每个文件一个Task减少文件数量

面试高频追问

追问一:OPTIMIZE会影响写入吗?

OPTIMIZE不会影响写入,是只读操作。但会消耗集群资源,建议在业务低峰期执行。OPTIMIZE会创建新文件,旧文件由VACUUM清理。

追问二:多久执行一次OPTIMIZE?

根据写入频率决定。每天写入的表建议每周OPTIMIZE一次。高频写入的表可以考虑每天一次。监控文件平均大小,当小于64MB时触发OPTIMIZE。

追问三:Z-Ordering是什么?

Z-Ordering是多维数据排序技术,按多列同时排序数据。查询时可以跳过不相关数据,提升性能。例如按user_id和event_type排序,查询特定用户的特定事件类型时快很多。

实战踩坑:OPTIMIZE失败

2023年在某出海硬件品牌,我们执行OPTIMIZE时遇到过失败的问题。

问题:表有100万个分区,OPTIMIZE命令直接运行,Executor OOM失败。

原因:OPTIMIZE一次性处理所有分区,内存不足。

解决方案:我们改成按天OPTIMIZE,每天单独执行。

# 错误做法:一次OPTIMIZE所有分区
OPTIMIZE user_behavior;
 
# 正确做法:按天OPTIMIZE
for dt in $(seq -f '%04.0f' 1 365); do
  spark-sql -e "OPTIMIZE user_behavior WHERE dt = '2024-$dt'"
done

改进效果:OPTIMIZE成功执行,小文件合并完成。

总结

小文件问题是数据湖最常见的问题,原因是高频写入和动态分区。Delta Lake通过OPTIMIZE命令合并小文件,Hudi通过Compaction服务合并Log文件。最佳实践是:合理设置分区数、定期OPTIMIZE每周一次、监控文件平均大小、设置自动合并。面试时能说清楚小文件的原因、OPTIMIZE和Compaction的区别、最佳实践,就很稳.

下一篇聊一个很重要的问题:分区怎么设计才合理?——分区策略最佳实践

你们生产环境遇到过小文件问题吗?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器