做数据管道的人大概都遇到过这种尴尬:上游任务明明显示跑完了,下游消费时却发现数据还缺一块,晚到的记录把整张表搞得对不上。Pinterest 的下一代数据库摄取框架也撞上了同样的墙,而且他们的数据规模是 PB 级、管道上千条,问题被放大了好几个量级。
先说背景。Pinterest 的摄取链路是典型的 CDC(变更数据捕获,就是监听数据库的增删改操作,把每笔变更同步到分析系统)架构:MySQL、TiDB、KVStore 里的在线变更,通过 Kafka 消息队列流出来,交给 Flink 做流式处理,再落到基于 Iceberg 格式的分析表里,供 Spark 之类的引擎查询。这套平台支撑着 PB 级数据和上千条管道,在线变更通常 15 分钟到 1 小时就能出现在分析表中。
问题出在“分区什么时候算写完”这件事上。老方案是批处理:一个批任务跑完,它负责的分区就自然 ready 了,下游任务看到任务结束就知道可以消费。但流式摄取没有“任务结束”这个节点——Flink 持续不断地把变更提交进 Iceberg 表,数据永远在写,下游怎么知道某个时间窗口的数据已经完整、可以安全读取?如果下游提前消费,会漏掉晚到的记录;如果等太久,数据新鲜度又没了。
Pinterest 的解法很巧妙:用事件时间水位线(event time watermark)来标记分区的完成度。事件时间就是数据本身携带的业务时间,比如一条订单记录里的下单时间,而不是它进入管道的时间。Flink 本身就有水位线机制,专门用来在乱序流里判断“到这个点为止,事件时间早于该点的数据基本都到齐了”。Pinterest 把这个机制和 Iceberg 的元数据能力结合起来,在 Flink 写入 Iceberg 的 sink 内部做文章。
具体流程是这样的:每条 CDC 记录进来时,系统提取出配置好的事件时间。每次 Flink 做 checkpoint(流式计算里的定期快照,用于故障恢复)时,收集这批记录事件时间的最小值、最大值和百分位统计,用一个叫 t-digest 的数据结构压缩成几百字节的分布摘要,写进 Iceberg 的 snapshot summary 里。Iceberg 每次提交都会生成一个新的 snapshot(数据文件加元数据的不可变快照),这些统计信息就跟着快照走。
提交之后,系统根据最近几次 checkpoint 的统计来计算分区的 finalization watermark(完成水位线,一个单调递增的时间戳,表示早于该时间的数据已完整)。默认策略是取最近若干次提交中事件时间的最小值,再向下取整到整点。关键设计是水位线单调不减——已经发布的值绝不会因为晚到数据而后退,这就保证了下游看到水位线后,不会出现“之前说好了又反悔”的情况。如果业务想用新鲜度换完整性,也可以配置基于百分位的策略,比如取 P95 而不是最小值。
这个水位线最终作为 Iceberg 表的一个属性存储,下游任务直接读表属性就能拿到,不需要额外的协调服务。Spark 在做 CDC upsert 提交时也会把这个元数据传播到基础表。Pinterest 自己总结得很到位:finalization 不再是任务结束的副产品,而是一个显式的、可查询的信号。
最让我意外的是这个设计的优雅之处:它没有发明新组件,纯粹是把 Flink 和 Iceberg 已有的能力——Flink 的水位线跟踪乱序流事件时间进度,Iceberg 的 snapshot 提供持久化元数据——组合起来,就得到了一个表级别的数据完整性信号。而且实现上只用了 sink 的扩展点:accumulator 收集统计,commit processor 在 Iceberg 提交前后执行 finalization 逻辑。
这个思路对做数据工程的人有直接借鉴价值。任何用流式管道持续写入数据湖或数仓的场景,只要下游需要判断“某段时间的数据是否完整”,都可以照搬这套模式:用事件时间而不是处理时间做判断依据,把完整性信号作为元数据存进表本身,让下游通过读元数据来感知,而不是依赖任务状态或外部协调。要注意的坑是事件时间的选择——如果业务数据本身没有可靠的时间字段,或者乱序程度远超预期,水位线的准确性会打折扣;另外百分位策略虽然能换新鲜度,但得想清楚业务能不能接受那部分缺失。

内容与图片版权归原作者所有 · 原文: https://www.infoq.com/news/2026/10/pinterest-event-time-partition/?utm_campaign=infoq_content&utm_source=infoq&utm_medium=feed&utm_term=global