Iceberg 湖表维护:小文件合并与快照清理实践
用 rewrite_data_files 到 expire_snapshots 编排 Iceberg 生产级表维护流水线
Iceberg 每次写入提交都会生成一个新的快照(Snapshot),快照里是清单(Manifest)和清单列表(Manifest List),真正数据存在 data 文件里。流式写入的问题在于:提交频繁 → 快照多、清单多、data 文件小,查询规划器要读的元数据越来越多,规划越来越慢。解决思路是把四类维护操作编排成一条流水线:rewrite_data_files(合并小文件)→ rewrite_manifests(压缩清单)→ expire_snapshots(过期快照)→ remove_orphan_files(清理孤儿文件),按固定频率在生产窗口里跑。本文给出每步的做法、参数与边界,完整机制可参考 Iceberg 官方维护文档。
先理解元数据层级:小文件为什么拖慢规划
Iceberg 的元数据分三层:表元数据(table metadata)→ 清单列表(manifest list)→ 清单(manifest)→ data 文件条目。查询规划时,引擎要扫描清单来定位需要的 data 文件。于是:
- 每次 commit 增加一个快照,快照里记录着该次写入涉及的清单;
- 流式写入每个批次一个文件,一段时间后 data 文件上万、清单几百个;
- 规划器虽然会按分区和列做元数据裁剪,但清单数量大、条目多,规划时间仍然上涨,最终表现为"查询大部分时间花在 planning 而不是扫描"。
所以维护的目标很明确:减少 data 文件数(合并)、减少清单数(压缩)、控制快照数(过期)、清掉死文件(孤儿清理)。
先算账:什么程度值得合并
不是所有小文件都值得立刻合并。判断依据是文件数、清单数与规划耗时的变化:当单表 data 文件超过 1 万、清单文件超过 50 个,或查询 planning 时间占比明显上升时,合并收益就大于重写开销。Iceberg 提供元数据表可以快速摸底:
SELECT count(*) FROM ods.event_log.files; -- data 文件数
SELECT count(*) FROM ods.event_log.manifests; -- 清单数小表重写成本低可以勤合并;大表用 where 限定分区,避免全表重写。
维护流水线总览与执行窗口
推荐把四项操作串成一条定时流水线(示例为 Spark 3 加 Iceberg Spark 扩展):
-- 1. 合并小数据文件(binpack 策略)
CALL iceberg.system.rewrite_data_files(
table => 'ods.event_log',
strategy => 'binpack',
options => map('target-file-size-bytes','536870912') -- 512MB
);
-- 2. 压缩 manifest 文件
CALL iceberg.system.rewrite_manifests('ods.event_log');
-- 3. 过期快照,保留最近 3 天
CALL iceberg.system.expire_snapshots(
table => 'ods.event_log',
older_than => TIMESTAMP '2026-07-28 00:00:00',
retain_last => 3
);
-- 4. 清理一周前的孤立数据文件
CALL iceberg.system.remove_orphan_files(
table => 'ods.event_log',
older_than => TIMESTAMP '2026-07-24 00:00:00'
);执行窗口选业务低峰,且不要在活跃写入期间跑合并与清理——合并会重写数据文件,孤儿清理可能误删正在写入的临时文件,两者都需要"相对静默"的表状态。
大数据基础平台 EasyMR
统一存储、计算与运维,支撑批、流、OLAP 与湖仓一体的国产化大数据底座平台
第一步:rewrite_data_files 合并小文件
这是最核心的一步,把多个小 data 文件重写为接近目标大小的大文件。Spark 的 procedure 支持两种策略:
- binpack(默认):把同一分区的文件打包合并到目标大小,不改变数据顺序。开销小、适合日志、流水类追加型表;
- sort:按指定列排序后重写(用
sort_order指定排序列),重写后数据有序,点查、范围过滤的 min-max 裁剪效果更好。代价是排序需要 shuffle,计算开销明显高于 binpack。
参数用 options 控制:target-file-size-bytes(目标文件大小,默认约 512MB)、min-file-size-bytes(小于该值的文件才参与合并,默认约为目标大小的 75%)、max-file-size-bytes(上限,防止重写后文件过大)。还可以用 where 限定只合并某些分区:
CALL iceberg.system.rewrite_data_files(
table => 'ods.event_log',
where => 'dt >= ''2026-07-28''',
strategy => 'binpack',
options => map('target-file-size-bytes','536870912')
);选择建议:追加为主、查询按时间范围过滤的表用 binpack 加分区限定;查询大量依赖某列等值或范围过滤的表,用 sort 策略让排序收益覆盖重写成本。合并频率按写入量定:日增几个 GB 的表一天一次即可,流式大表可以每小时对当天分区合并一次。
第二步:rewrite_manifests 压缩清单
合并 data 文件后,清单文件本身也可能堆积(每次提交产生新清单,小清单可能只有几个条目)。rewrite_manifests 把小清单合并成少量大清单,直接减小规划时读取的元数据量:
CALL iceberg.system.rewrite_manifests('ods.event_log');这一步开销小、风险低,建议在每次合并 data 文件后顺带执行,或至少每天一次。注意:清单压缩只改元数据,不碰数据文件,可以和合并解耦。
第三步:expire_snapshots 快照过期
Iceberg 的快照机制支撑时间旅行与回滚,但快照也是元数据:快照越多,清单列表越长,规划越慢,且被过期快照引用的旧数据文件一直占着存储。expire_snapshots 用于删除指定时间之前、且不在保留范围内的快照:
older_than:只过期该时间点之前的快照;retain_last:额外保留最近 N 个快照,即使它们早于 older_than。
保留边界要想清楚:快照是时间旅行的依据,也是下游读取(如 Flink、Hive 从特定快照读)的入口。留得太少,出问题时无法回滚、时间旅行查不到历史;留得太多,元数据又持续膨胀。实践经验:按数据保留策略倒推,T+1 离线分析通常保留 3-7 天,实时或近实时场景保留 1-3 天即可;做过回滚演练后再缩短保留窗口。过期快照只删除"不再被任何保留快照引用"的数据文件,安全性有保障,但仍建议先小范围试跑再全量开启。
数据治理
统一标准、质量、安全与资产管理,沉淀高质量数据集,支撑经营决策、监管合规与 AI 应用。
第四步:remove_orphan_files 孤儿文件清理
写入过程中崩溃、任务重试、合并重写失败都可能留下"不在任何元数据里"的孤儿文件,它们不占元数据、不影响查询,但占存储。remove_orphan_files 按 older_than 删除指定时间之前且无元数据引用的文件:
- 时间窗口要给足:新写入的临时文件如果还没提交进元数据,可能被误判为孤儿,所以
older_than至少留出比最长写入事务更长的窗口(如 7 天); - 不要在活跃写入期间执行,这是四条操作里风险最高的一条;
- 建议每周一次,先在小表验证再铺开。
失败模式:维护任务自己也会制造问题
维护操作如果编排不当,反而会引入新问题:
- 合并与写入并发:rewrite 重写期间新写入又产生小文件,合并永远追不上;把合并排在写入低峰,或对热分区按小时小步合并;
- 多个维护任务并发执行:rewrite 与 expire 基于不同快照并发跑,可能出现文件被误删的竞态;严格按流水线顺序串行执行;
- older_than 设得太短:remove_orphan_files 或 expire_snapshots 的窗口小于最长写入事务时长,会把刚写完还没提交进元数据的文件当孤儿删掉;窗口要给足(至少数天);
- 快照保留过短:发生误删需要回滚时,发现快照早已过期;保留策略要与数据安全要求对齐,做过回滚演练后再收紧;
- 只合并 data 文件不压清单:文件数降了,清单还是几百个,规划依旧慢;两步要连着做。
无论用 Spark 的 CALL 语法还是 Flink 侧的维护 Action(如 FlinkActions 的 rewrite 与 expire 能力),原则一致:合并、过期、清理按固定窗口串行执行。
频率、窗口与边界的排布建议
- rewrite_data_files:按写入量定,日级或每小时对当天分区;低峰窗口执行;
- rewrite_manifests:每次合并后或每日;低峰执行;
- expire_snapshots:每日,与数据保留策略对齐;低峰执行;
- remove_orphan_files:每周;必须避开活跃写入。
整体节奏:先合并 data 文件(减少条目),再压清单(减少元数据文件),再过期快照(释放存储与元数据),最后清孤儿(兜底)。四步串起来跑,Iceberg 表才能长期保持"文件数受控、规划快速"的生产状态。上线时先在小表跑通参数与频率,观察规划时长和文件数趋势后再推广到核心表。


