Published on

为什么 Checkapoint 没成功,下游却有数据?

Authors
  • avatar
    Name
    Charles Chen
    Twitter

一、 灵异开端:为什么 Checkpoint 没过,数据却入库了?

当你盯着 MacBook M4 Pro 的屏幕,看着 Flink 任务因为网络抖动 Checkpoint 失败时,你可能以为数据也会跟着“回滚”。现实却给了你一个响亮的耳光。 ckpt-effectively-Once-transaction.mdx

1. JDBC Sink 的“快枪手”行为

在你的配置中,sink.buffer-flush.max-rows = 1000sink.buffer-flush.interval = 2s 是罪魁祸首。

  • 真相:JDBC Sink 内部有个“攒批”的小仓库。一旦攒够 1000 条或者过了 2 秒,它就迫不及待地执行 commit 写入 OceanBase 了。
  • 脱钩:这个过程完全不看 Flink 检查点的脸色。哪怕 Checkpoint 此时正在难产,数据已经完成了“物理越狱”。
  • 代价:这叫 At-Least-Once(至少一次)。如果任务重启,Flink 会从上一个成功的位点重跑,刚才“抢跑”的数据就会被重复写入。

这个提交动作是独立的定时器或计数器触发的,它完全不等待 Flink 的 Checkpoint 完成。

2. Kafka Sink 的“薛定谔事务”

如果你觉得换成 Kafka 就能解决问题,那就太天真了。

  • 默认状态:Kafka Sink 默认也是“给我就发”。
  • 隔离级别陷阱:就算你开启了 Kafka 事务,由于 Kafka 默认的消费者隔离级别是 read_uncommitted,下游依然能读到那些“尚未通过 Checkpoint 确认”的数据。这在架构上被称为脏读

二、数据库事务隔离级别(Transaction Isolation Levels)

既然聊到数据写入,就必须深挖数据库的事务隔离。隔离级别决定了一个事务中所做的修改,哪些在何时对其他事务可见。

隔离级别脏读 (Dirty Read)不可重复读 (Non-repeatable Read)幻读 (Phantom Read)备注
Read Uncommitted可能可能可能性能最高,几乎不用
Read Committed不可能可能可能主流数据库默认(如 Oracle, SQL Server, OceanBase)
Repeatable Read不可能不可能可能MySQL 默认
Serializable不可能不可能不可能性能最低,串行化执行
  • 在 Read Committed 下: 你在一个事务里执行两次相同的 SELECT,如果中间有别人 COMMIT 了修改,你两次看到的结果是不一样的。这叫“看到事实”,但牺牲了“事务内的一致性”。
  • 在 Repeatable Read 下: 数据库给你打了一个“时间静止”的快照。无论别人怎么改,只要你的事务没结束,你看那几行数据永远是最初的样子。整个事务期间共用同一个视图,实现了“可重复读”。

深度解析:

  • 脏读:你还没 commit,我就能看到你的数据。万一你回滚了,我就读到了“假”数据。
  • 不可重复读:我一个事务内两次读同一行,你中间改了并提交了,我发现两次结果不一样。
  • 幻读:我一个事务内按范围查(比如 id > 10),你中间插入了一行新的,我再查发现多了一行。

虽然现代数据库多用 MVCC,但在传统的封锁协议里,这两者隔着锁的释放时机

  • RC: 读完数据立刻释放 S锁 (Shared Lock)
  • RR: 读完数据后,S锁必须持有到事务结束 (Commit/Rollback) 才能释放。

架构师视角:OceanBase 默认支持 Read Committed,并且通过其 LSM-Tree 存储引擎和多版本并发控制(MVCC)实现了高性能的快照隔离。


三、 50w 刀架构师的修养:如何优雅地处理“抢跑”?

面对 Checkpoint 失败与数据入库的不一致,成熟的架构师不会去死磕“百分之百不漏不重”,而是根据业务场景做权衡(Trade-off)。

1. At-Most-Once(至多一次)

这是最容易实现的语义。

  • 语义:消息可能丢失,但绝对不会重复。
  • 实现:Producer 发完消息就不管了(Fire-and-Forget),不管 Broker 有没有收到。
  • 场景:对延迟极其敏感,但对数据丢失有一定容忍度的场景。比如:
    • 监控指标采样(丢一个点不影响趋势)。
    • 日志收集(丢几条访问日志不会导致系统崩溃)。
    • UDP 协议通信。
  • 槽点:在追求数据质量的 Flink 任务里,这通常是由于没开 Checkpoint 导致的“事故”。

2. Effectively-Once(等效一次)

我们从不迷信“Exactly-once(精确一次)”,因为它的成本(如 XA 事务)极其昂贵。我们追求的是 “最终一致性”。 这是一种“曲线救国”的智慧,解决分布式重复写入最经济、最稳健的方法,也是大厂生产环境中最常用的方案。

  • 核心At-Least-Once + 幂等性(Idempotency) = Exactly-Once。
  • 原理:虽然消息可能发了多次,但由于下游(如 MySQL、Redis、HBase)具备幂等处理能力(比如 INSERT ON DUPLICATE KEY UPDATE 或通过主键去重),最终的结果就像只处理了一次一样。
  • 价值点:对于架构师来说,能用幂等解决的,绝不用分布式事务(2PC)。因为 2PC 会极大地拉低吞吐量。

幂等写入是行业内最通用、性能最优的生产级方案。

3. End-to-End Exactly-Once(端到端精确一次)

这是你目前 Flink 18.1 + Kafka 3.7.0 组合追求的“圣杯”。

  • 深度解析
    • 仅仅 Flink 内部状态一致是不够的。真正的端到端要求:Source 可回溯 + Flink 状态一致性 + Sink 支持事务(或幂等)
    • 实现机制:Flink 使用的是 两阶段提交(2PC) 结合 Chandy-Lamport 算法(分布式快照)。
    • 架构思考:Kafka 在 0.11 版本后引入了 TransactionalId,配合 Flink 的 TwoPhaseCommitSinkFunction,才能真正实现端到端的闭环。
    • 代价:你会发现数据延迟变高了(延迟取决于 Checkpoint 周期),系统复杂度翻了一倍。

四、 结语:别被一致性的表象蒙蔽

在分布式系统的架构中,一致性从来不是免费的午餐

你看到的“Checkpoint 失败但数据入库”,本质上是系统在性能(吞吐量)与正确性(精准一次)之间选择了性能。作为未来的高薪架构师,你的价值在于:在数据“抢跑”的混沌中,通过幂等设计或合理的事务配置,为业务构建出一套即便底层“乱套”但结果依然“正确”的系统。

默认情况下,Flink 和外部系统是“脱钩”的,各自奔跑。想要同步,必须支付“延迟”和“复杂度”作为代价。

下次如果有人问你为什么数据多出来了,请优雅地打开你的 IDE,指着那两行 buffer-flush 配置告诉他:“这是我为了系统吞吐量,故意留下的‘性能后门’。”