湖仓表“每天数据都写成功”,不代表它运行健康。流式任务持续产生小文件,快照和 manifest 逐渐膨胀,失败作业留下未引用文件。查询开始变慢、Catalog 元数据增大时,团队才临时跑一次全表 compaction。
这种做法把表维护当清洁脚本,实际上它和写入、查询一样是持续服务。维护延迟会积累运行负债,维护过猛又会抢资源、制造提交冲突甚至误删在途文件。它需要自己的 SLI、SLO、容量和变更治理。
邓明瑞 / 纯粹
湖仓表“每天数据都写成功”,不代表它运行健康。流式任务持续产生小文件,快照和 manifest 逐渐膨胀,失败作业留下未引用文件。查询开始变慢、Catalog 元数据增大时,团队才临时跑一次全表 compaction。
这种做法把表维护当清洁脚本,实际上它和写入、查询一样是持续服务。维护延迟会积累运行负债,维护过猛又会抢资源、制造提交冲突甚至误删在途文件。它需要自己的 SLI、SLO、容量和变更治理。
湖仓写任务报 CommitFailed,最常见的处理是调高重试次数。这对一部分并发 append 有效,对 overwrite、delete 或 compaction 却可能掩盖真正的语义冲突。更麻烦的是提交超时:客户端认为失败,Catalog 可能已经成功切换了元数据。
诊断并发写入,不能只看异常类名。我会把一次写拆成数据文件生成、基于某个 snapshot 规划变更、冲突校验、Catalog 原子提交和提交结果确认五段,先判断失败发生在哪一段,再决定复用文件、重新规划还是人工核对。
Iceberg 表能被 Spark 和 Flink 正常读写,只能证明接入完成,不能证明可以长期运行。流式任务每天产生几百个 snapshots,小批写入不断增加 data files,失败任务留下 orphan files,time travel 保留策略又阻止旧文件回收。三个月后,表仍能查,但规划越来越慢,存储账单一直涨,维护脚本谁也不敢改。
我认为平台真正要补的是表级运维闭环:从 metadata tables 读取健康信号,按每张表的负载和 SLA 选择维护动作,记录提交与删除证据,再验证查询和成本是否改善。它不是几条全局 cron,而是和写入一样正式的生产链路。
把 Iceberg 表的 format-version 从 1 改成 2,看起来只是一条表属性更新。真正危险的是,这张表往往同时被 Spark、Flink、Trino、离线维护脚本和自研 SDK 访问。创建表的人确认自己的引擎能读 v2,并不能证明整条生产链路已经兼容。
格式版本升级与普通依赖升级不同。旧 reader 遇到自己不理解的新表特性,正确行为应该是拒绝读取;更糟的实现可能加载成功,却没有应用 delete files,悄悄把已删除的行读出来。上线前必须把“有哪些访问者、每个访问者执行什么动作”展开成矩阵。
很多团队第一次治理小文件,会加一个凌晨两点的 Spark 脚本:扫描昨天的分区,把文件合到 512 MB。刚上线时效果明显,过一阵又会遇到任务跑不完、与实时写入冲突、刚合完第二天又碎了,最后脚本变成一个没人敢停的定时黑盒。
我更愿意把 Compaction 看成表的持续维护服务。它不是“每天执行一次重写”,而是一条有输入指标、候选选择、预算、提交证据和效果验收的控制循环。表什么时候需要整理、整理哪部分、允许花多少资源、失败后如何继续,都应该可计算。
在数据库里执行一条 UPDATE orders SET status='PAID' WHERE id=...,我们很容易把它想成“找到这一行,改掉几个字段”。把同样的语句放到湖仓表上,这个直觉会出问题。Parquet 数据文件是不可变的,Iceberg 管的是文件和元数据,不会钻进一个已经提交的 Parquet 文件里原地改几个字节。
于是,更新一行到底有多贵,不能只看 affected rows。它可能触发整个数据文件重写,也可能先增加 delete file,把合并成本推迟到查询和维护阶段。真正要算的是:命中了多少 data file、每个文件多大、产生多少 delete、读端要合并多少层,以及这些文件何时能被重新整理掉。
“Iceberg 支持原子提交”这句话很容易被理解成:两个任务同时写同一张表也不会冲突,失败了自动重试就好。实际边界要窄得多。
Iceberg 的原子点是 table metadata pointer 从旧 metadata file 切换到新文件。数据文件、manifest、manifest list 和新 metadata file 在此之前已经写到存储上;只有 catalog 中的指针成功切换,新 snapshot 才对读者可见。两个 writer 基于同一个旧版本提交时,只能有一个先完成切换,另一个必须刷新表状态、重新验证再尝试。
所以原子性解决的是“读者不会看到半张表”,不是“所有并发操作都会成功”。并发 append、overwrite 和 compaction 的冲突条件不同,失败后的处理也不能统一写成一个无限重试。