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的决策流程画清楚了。你可以对照自己的场景,看看该选哪条路。

Compaction决策流程 检查文件数量 文件数 > 阈值? 无需合并 否 选择合并策略 是 Binpack策略 Sort策略 None策略 执行Compaction 文件减少,存储优化 注意 合并后记得清理 历史Snapshot

4.8 总结一下

Compaction不是银弹。它帮你省存储钱,但吃CPU钱。关键是要找到平衡点:

  • 文件数量控制在合理范围(500-1000)
  • 目标文件大小256MB-512MB,别太大也别太小
  • 根据写入频率决定合并频率,别盲目跑
  • 别忘了清理历史Snapshot

我个人的习惯是,先跑一周看看数据,再调整策略。别一上来就全量合并,容易出事。嗯,Compaction这件事,慢就是快。