Flink Savepoint vs 数据库 Savepoint

前不久笔者在 Flink 社区讨论 Flink SQL 中 Savepoint 的 SQL 语法时(见 FLIP-222 [3]),曾提议过参考数据库的 Savepoint 语法。虽然最终未能获得 Flink 社区的大多数赞成,但也引发了笔者的好奇心: Flink Savepoint 和传统的数据库 Savepoint 究竟有何异同?于是经过笔者一番调研与思考,便有了这篇文章。

Savepoint 功能

就功能而言,由两者共用 Savepoint 一词可见其语义之相近,均用于保存状态数据。Flink Savepoint 用于持久化作业当前的状态,以便后续用于恢复作业状态到该时间点;数据库 Savepoint 用于保存当前事务当前的状态,后续可用于事务回滚。

从表面上看,除了对象不同,两者最大的不同点在于 Savepoint 作用的范围。Flink Savepoint 可以作用于作业的整个运行周期,而数据库 Savepoint 只能作用于单个事务中。然而,若我们将一个 Flink 作业视为一个超长事务,那么两种 Savepoint 的作用范围也是一致的。

图1. Savepoint 范围

更为有趣的是,数据库 Savepoint 常与 Nested Transaction (内嵌事务)一起出现。顾名思义,Nested Transaction 即事务中的事务,可独立进行 Commit 或者 Rollback,不会与外层事务的状态相互影响。大多数常见的数据库,包括 MySQL、PostgresSQL 等等,并没有直接支持 Nested Transaction,而是提供 Savepoint 作为替代方案。具体来说,即用户可以在一个事务的范围里声明一个 Savepoint,此后的所有操作可以被视为一个 Nested Transaction。若有需要,用户直接回滚事务到该 Savepoint,因为后续操作也被回滚,因此看起来的效果就跟 Nested Transaction 被回滚了一样。

熟悉 Flink 的读者一定很快联想到一个概念。没错,就是 Flink Checkpoint。Flink Checkpoint 本身是两阶段提交的事务(2PC),对于作为外层事务的 Flink 作业而言,无疑是 Nested Transaction(当然,其中还有很多细节问题,留待下文再谈)。

综上所述,我们可以得到如下的相似映射 。

Flink 数据库
作业 事务
Savepoint Savepoint
Checkpoint 内嵌事务

Savepoint 度量

Flink Savepoint 与数据库 Savepoint 保存作业或事务的状态数据,但由于 Flink 作业为流式执行、数据库事务为批式执行,两者的状态基于不同的度量指标: Flink Savepoint 以数据为基准,而数据库事务则以操作(Operation,即 SQL statement)为基准。

图2. Flink Savepoint 执行流程

具体而言,Flink 作业可能包含多个 Operation (Flink 对应概念为 Task),在流式计算模式下,这些 Operation 会被同时调度、同时执行,并且可能永远不会终止。因此,Flink Savepoint 是独立于作业之外的,并不需要等待某个 Operation 完成。Flink Savepoint 会对所有的操作进行快照,记录它们正在处理中的数据和中间状态(见图 2)。

一般而言,快照通常需要 Stop-The-World (STW),不过 Flink 采用 Chandy-Lamport 算法,向数据注入特殊的 Barrier 并以之为基础进行快照,避免了 STW,因此可以说 Flink Savepoint 是以数据为基准的。

图3. 数据库 Savepoint 执行流程

而另一方面,数据库事务可视为是合并到一个逻辑单位中的操作(数据库中一般称为 Statement)序列,这些操作会像批计算一样顺序执行。显然,两个 Operation 之间是很好的 Savepoint STW 快照时机,所以数据库 Savepoint 是以特殊的 Statement 的方式嵌入到事务里面,因此可以说数据库 Savepoint 是以操作为基准的。

DDL 支持

除了常见的 DML (Data Manipulation Language,比如 insertupdatedelete),数据库事务还可能包含 DDL (Data Definition Language,比如 alter tablecreate index),这个特性被称为 Transactional DDL [3]。Transactional DDL 在系统发版升级场景非常有用,但只有少数的主流数据库支持。比如 MySQL、Oracle 不支持 Transactional DDL,而 PostgreSQL 则支持。

由于 Flink 作业只涉及 DML,不涉及 DDL (DDL 在 Flink 中属于非作业操作 Non-Job Operation),因此以作业运行周期为范围的 Flink Savepoint 自然无法被纳入到 Savepoint 里。回滚到某个 Savepoint 并不能回滚 Savepoint 以后的 DDL 造成的 Schema 变更,无论是在流计算模式还是批计算模式。

Savepoint 权属

数据库 Savepoint 属于事务的一部分,其数据主要存储在数据库内部的内存或者日志里,由数据库管理系统控制,不会暴露给用户。相比之下,Flink Savepoint 则开放得多: 我们不仅可以声明 Savepoint 的权属(在 Flink 1.14 及之前版本,Savepoint Owner 只能为用户),还可以将 Savepoint 搬来搬去,甚至对其进行修改或直接生成一个新 Savepoint。

笔者认为 Flink 与传统数据库截然不同的 Savepoint 管理方式主要有两点原因:

  • Flink 支持执行任意用户代码,并且用户代码可以将任意状态信息以任意序列化方式存到 Savepoint 中,这意味着 Flink 并不掌握所有恢复 Savepoint 需要的信息,因此只能将控制权交给用户。
  • Flink 并不存储数据(撇开尚不成熟的 Table Store 不谈),通常会依赖外部数据存储,因而外部数据存储的变更可能导致 Savepoint 的不兼容,导致 Flink 不能做到 Savepoint 的 Self Contained。

总结

Flink Savepoint 与传统数据库的 Savepoint 在功能定位十分接近,都用于对状态进行快照,而且与 Savepoint 相关的概念都大同小异。然而,在 Savepoint 特性和实现细节方面却大相径庭,其中主要体现在 Savepoint 度量(基于数据或基于操作)、DDL 支持、Savepoint 权属三个方面。

参考

[1] Wikipidia: Savepoint
[2] Wikipidia: Nested Transaction
[3] FLIP-222: Support full job lifecycle statements in SQL client