4. 压缩的代价:小文件合并(Compaction)的CPU与存储成本权衡
小文件问题,是每个用Iceberg的人都会撞上的墙。你想想看,数据不停地写,文件越生越多,查询越来越慢,账单越来越贵。这时候你就得做Compaction——把小文件合并成大文件。
但Compaction不是免费的。它吃CPU,吃IO,吃时间。我见过不少团队,Compaction跑得太猛,把集群资源吃光了,业务查询全堵在那儿。也见过Compaction跑得太懒,小文件堆成山,存储成本翻倍。
说白了,这就是一场权衡。你得在CPU开销和存储成本之间,找到那个甜点。
4.1 小文件是怎么产生的?
先看看小文件从哪来。Iceberg的写入模式决定了文件数量:
- 流式写入:每几分钟刷一次,一次就生成一个文件。一天下来,几百上千个小文件很正常。
- 分区写入:每个分区一个文件,分区多了,文件就多了。
- 并发写入:多个任务同时写,每个任务都生成自己的文件。
我在项目中遇到过最夸张的情况:一个表每天写入200GB数据,但文件数量超过10万个。查询时元数据操作就占了30%的时间。存储成本倒还好,但查询性能完全没法看。
核心问题:小文件太多,会导致元数据膨胀、查询计划变慢、文件打开开销剧增。最终结果就是——你付了更多的钱,却得到更差的性能。
4.2 Compaction的成本构成
Compaction不是白干的。它的成本分三块:
| 成本项 | 说明 | 典型占比 |
|---|---|---|
| CPU成本 | 读取小文件、解压、合并、重新压缩、写入大文件 | 40%-60% |
| IO成本 | 读取原始小文件、写入合并后的大文件、删除旧文件 | 30%-40% |
| 元数据成本 | 更新Manifest、提交新Snapshot、清理旧文件 | 10%-20% |
嗯,这里要注意:CPU成本往往被低估。很多人只看到存储省钱了,没看到Compaction作业本身在吃资源。如果你的集群是共享的,Compaction跑起来,业务查询就得等。
4.3 合并策略:怎么选?
Iceberg提供了几种合并策略。我个人的习惯是,根据表的写入模式来选:
- Binpack策略:默认策略。把一堆小文件打包成大文件。适合写入量稳定、文件大小均匀的场景。
- Sort策略:合并时按指定列排序。适合经常按某列过滤查询的表。排序后,查询可以跳过更多文件。
- None策略:不合并。适合写入极少、查询也少的冷数据表。
举个例子,Binpack策略的配置:
-- 设置合并策略为Binpack
ALTER TABLE my_table SET TBLPROPERTIES (
'write.distribution-mode' = 'hash',
'write.target-file-size-bytes' = '268435456', -- 256MB
'compaction.strategy' = 'binpack'
);
-- 手动触发Compaction
CALL system.rewrite_data_files(
table => 'my_table',
strategy => 'binpack',
options => map(
'target-file-size-bytes', '268435456',
'min-file-size-bytes', '134217728'
)
);
我曾经在一个日志表上试过Sort策略。按时间戳排序后,查询时间从15秒降到了3秒。但代价是Compaction时间翻了一倍。值不值?看你的查询模式。
4.4 合并频率:多久跑一次?
这是最让人纠结的问题。跑太勤,CPU浪费;跑太懒,小文件堆积。
我一般按这个原则来:
- 高频写入表(每分钟都有数据):每1-2小时跑一次Compaction。文件数量控制在1000以内。
- 中频写入表(每小时写入一次):每天跑一次。文件数量控制在500以内。
- 低频写入表(每天写入一次):每周跑一次。或者干脆不跑。
我的经验:不要用固定的时间间隔。用文件数量触发Compaction更合理。比如,当文件数超过1000时自动触发。这样能避免在写入低谷期浪费资源。
4.5 成本权衡:一个真实案例
我之前帮一个客户优化过。他们的表每天写入500GB,文件数量从2000涨到了15000。存储成本每月增加了30%。他们决定跑Compaction。
第一次尝试:每小时跑一次,目标文件大小512MB。结果Compaction作业吃掉了集群40%的CPU,业务查询延迟从2秒涨到了8秒。用户投诉了。
第二次尝试:每天跑一次,目标文件大小256MB。Compaction只占10%的CPU,但文件数量还是维持在5000左右。存储成本降了15%,查询性能也回来了。
你看,同样的表,不同的策略,结果天差地别。没有银弹,只有权衡。
4.6 避坑指南
我踩过不少坑,挑几个典型的说说:
- 目标文件大小设太大:我曾经设成1GB,结果Compaction跑了一整夜,还把磁盘IO打满了。小文件合并成大文件,但大文件太大,查询时扫描的数据量反而增加了。
- 忽略分区:Compaction默认按分区合并。如果你的分区策略不合理,合并效果会大打折扣。比如,按天分区,但每天的数据量差异很大,小分区的文件永远合不拢。
- 并发Compaction:多个Compaction作业同时跑,会互相抢资源。我建议用队列控制并发数,一般1-2个就够了。
警告:Compaction会生成新的Snapshot,旧的Snapshot不会立即删除。如果你不清理历史Snapshot,元数据会持续膨胀。记得配合expire_snapshots一起用。
4.7 核心逻辑图
下面这张图,把Compaction的决策流程画清楚了。你可以对照自己的场景,看看该选哪条路。
4.8 总结一下
Compaction不是银弹。它帮你省存储钱,但吃CPU钱。关键是要找到平衡点:
- 文件数量控制在合理范围(500-1000)
- 目标文件大小256MB-512MB,别太大也别太小
- 根据写入频率决定合并频率,别盲目跑
- 别忘了清理历史Snapshot
我个人的习惯是,先跑一周看看数据,再调整策略。别一上来就全量合并,容易出事。嗯,Compaction这件事,慢就是快。