4. Compaction 资源模型:CPU、内存、IO、网络带宽的消耗模型与预估
Compaction 调优,说白了就是跟资源打交道。你给多少资源,它干多少活。但给多了浪费,给少了卡死。我这些年踩过的坑,十有八九都跟资源预估不准有关。
这一章,咱们就把 Compaction 吃掉的每一口资源掰开来看。CPU、内存、IO、网络带宽,一个都跑不掉。
4.1 CPU 消耗模型:谁在吃算力?
Compaction 的 CPU 消耗,主要来自三个环节:
- 数据读取与解析:从 Parquet/ORC 文件中读数据,解压、反序列化。这一步吃的是单核性能。
- 排序与合并:多路归并排序(Merge Sort),把多个小文件的数据行按主键排序。这是 CPU 大头,尤其是数据量大的时候。
- 编码与压缩:写新文件时,重新编码、压缩。不同压缩算法(Snappy、Zstd、LZ4)的 CPU 开销差好几倍。
核心结论:Compaction 的 CPU 消耗与 数据行数 × 排序复杂度 成正比。文件数量越多,归并路数越多,CPU 压力越大。
我在项目中遇到过一个问题:某个湖仓表的 Compaction 跑了 2 小时还没结束。一看监控,CPU 利用率只有 30%。为什么?因为任务被分配到了 4 个核,但排序阶段是单线程的,其他核在空转。后来我调整了并行度,把单次 Compaction 拆成多个子任务,CPU 利用率直接拉到 85%。
4.2 内存消耗模型:别让 OOM 找上门
内存是 Compaction 最敏感的资源。给少了,频繁 spill 到磁盘,性能暴跌;给多了,集群其他任务没饭吃。
内存消耗主要分三块:
| 内存用途 | 消耗占比 | 说明 |
|---|---|---|
| 数据缓冲区 | 60% - 70% | 存放待排序的原始数据行,按 page 或 row group 加载 |
| 排序工作区 | 20% - 30% | 归并排序的临时缓冲区,包括指针数组和比较缓存 |
| 元数据与索引 | 5% - 10% | 文件统计信息、Bloom Filter、Min/Max 索引 |
我习惯用一个简单公式来估算:
单次 Compaction 内存 ≈ 输入文件总大小 × 0.3 + 200 MB(固定开销)
举个例子:如果你一次合并 10 个 128MB 的文件,总输入约 1.28GB。那么内存需求大约是 1.28 × 0.3 + 0.2 ≈ 0.58GB。但这是理想情况。如果数据列很多,或者有复杂类型(Map、Array),实际消耗可能翻倍。
避坑指南:我曾经在 Spark 上跑 Compaction,默认给 4GB 堆内存。结果一个 500GB 的大分区直接 OOM。后来发现是 shuffle 阶段的排序缓冲区没控制好。建议设置 spark.sql.adaptive.coalescePartitions.enabled=true,并限制单次 Compaction 的输入文件数量不超过 50 个。
4.3 IO 消耗模型:读写放大是元凶
IO 是 Compaction 最直观的消耗。你读了多少数据,写了多少数据,直接决定了执行时间。
IO 消耗的核心指标是 写放大因子(Write Amplification Factor, WAF):
WAF = 写入的总数据量 / 原始数据量
举个例子:你有 100MB 的小文件,Compaction 后生成 80MB 的新文件。但中间过程可能读了 100MB,写了 80MB,WAF 就是 1.8。如果中间有多次 spill,WAF 可能到 3 甚至 5。
我整理了一个常见场景的 IO 消耗对比:
| 场景 | 读放大 | 写放大 | 典型 WAF |
|---|---|---|---|
| 小文件合并(10个以下) | 1.0 - 1.2 | 0.8 - 1.0 | 1.8 - 2.2 |
| 大文件合并(50个以上) | 1.5 - 2.0 | 1.0 - 1.5 | 2.5 - 3.5 |
| 带删除向量的 Compaction | 2.0 - 3.0 | 1.2 - 1.8 | 3.2 - 4.8 |
经验之谈:如果你的集群 IO 带宽是瓶颈,优先控制 WAF。我一般把目标定在 2.5 以下。超过这个值,就要考虑调整 Compaction 策略,比如减少参与合并的文件数,或者启用增量 Compaction。
4.4 网络带宽消耗:分布式环境下的隐形杀手
在分布式架构中,Compaction 不只在本地跑。数据需要在节点间 shuffle,网络带宽就成了隐形瓶颈。
网络消耗主要来自:
- 数据 Shuffle:按主键或分区键重新分布数据,这是大头。
- 元数据同步:Compaction 完成后,通知 NameNode/ Metastore 更新文件列表。
- 中间结果传输:如果使用 Spark/Flink 引擎,中间结果可能落盘到远端存储。
我习惯用这个公式估算网络开销:
网络传输量 ≈ 输入数据量 × (1 + 冗余因子)
冗余因子通常取 0.2 - 0.5,取决于数据分布是否均匀。如果数据倾斜严重,冗余因子可能到 1.0 以上。
关键点:网络带宽的峰值往往出现在 Shuffle 阶段。如果你的集群是 10Gbps 网络,单次 Compaction 的 Shuffle 数据量超过 50GB,就可能把带宽打满,影响其他查询。
4.5 资源预估实战:一个完整的计算示例
咱们来算一个真实的例子。假设你有以下场景:
- 输入:200 个 128MB 的 Parquet 文件,总大小 25.6GB
- 目标:合并成 20 个 128MB 的文件
- 引擎:Spark 3.4,10 个 Executor,每个 4 核 16GB
我的预估步骤:
- CPU 需求:排序复杂度 O(N log N),200 个文件归并,大约需要 8 - 12 个 vCore 持续运行 15 - 20 分钟。
- 内存需求:25.6GB × 0.3 + 0.2GB ≈ 7.9GB。每个 Executor 分配 4GB 给 Compaction,总共 40GB,绰绰有余。
- IO 需求:读 25.6GB,写 2.56GB(20 × 128MB),WAF ≈ 1.1。如果使用 HDD,预计耗时 30 - 40 分钟;如果使用 SSD,10 - 15 分钟。
- 网络需求:Shuffle 数据约 25.6GB × 1.3 ≈ 33GB。10Gbps 网络下,传输时间约 27 秒,不是瓶颈。
你看,这个场景下 IO 是主要瓶颈。所以我建议优先升级存储介质,而不是加 CPU。
4.6 资源模型全景图
下面这张图,把四个资源的消耗关系和调优方向串起来了。你可以把它当作调优时的检查清单。
这张图你看懂了吗?四个资源不是孤立的。比如你加大内存,就能减少 spill,IO 消耗就降下来了。你优化压缩算法,CPU 消耗上去了,但 IO 和网络带宽省下来了。调优的本质,就是在四个资源之间找平衡。
我的习惯:每次调优前,先画一张类似的资源消耗图。标出当前瓶颈在哪里,然后针对性地调整。不要一上来就改参数,那样容易顾此失彼。