数据全生命周期操作
创建表
执行以下创建表的语句将创建一个包含3个字段的Paimon表:
CREATE TABLE T ( id BIGINT, a INT, b STRING, dt STRING COMMENT 'timestamp string in format yyyyMMdd', PRIMARY KEY(id, dt) NOT ENFORCED) PARTITIONED BY (dt);
这将会在路径 /tmp/paimon/default.db/T 下创建一个名为 T 的 Paimon 表,并且其表结构(schema)将存储在 /tmp/paimon/default.db/T/schema/schema-0 中。
写入数据
在 Flink SQL 中执行以下插入语句:
INSERT INTO T VALUES (1, 10001, 'varchar00001', '20230501');
一旦 Flink 作业完成,记录会通过一次成功的提交被写入到 Paimon 表中。可以通过执行查询 SELECT * FROM T 来验证这些记录是否可见,该查询将会返回一条记录。提交过程会在路径 /tmp/paimon/default.db/T/snapshot/snapshot-1 处创建一个快照。snapshot-1 的文件布局描述如下:
snapshot-1 的内容包含快照的元数据,例如清单列表(manifest list)和模式ID(schema id):
{ "version" : 3, "id" : 1, "schemaId" : 0, "baseManifestList" : "manifest-list-4ccc-c07f-4090-958c-cfe3ce3889e5-0", "deltaManifestList" : "manifest-list-4ccc-c07f-4090-958c-cfe3ce3889e5-1", "changelogManifestList" : null, "commitUser" : "7d758485-981d-4b1a-a0c6-d34c3eb254bf", "commitIdentifier" : 9223372036854775807, "commitKind" : "APPEND", "timeMillis" : 1684155393354, "logOffsets" : { }, "totalRecordCount" : 1, "deltaRecordCount" : 1, "changelogRecordCount" : 0, "watermark" : -9223372036854775808}
注意,清单列表(manifest list)包含快照的所有更改,baseManifestList 是基础文件,而 deltaManifestList 中的更改是基于此基础文件进行的。首次提交将生成1个清单文件,并创建2个清单列表。
./T/manifest:manifest-list-4ccc-c07f-4090-958c-cfe3ce3889e5-1 manifest-list-4ccc-c07f-4090-958c-cfe3ce3889e5-0manifest-2b833ea4-d7dc-4de0-ae0d-ad76eced75cc-0
manifest-2b833ea4-d7dc-4de0-ae0d-ad76eced75cc-0 是清单文件(上图中的 manifest-1-0),它存储了快照中数据文件的信息。
manifest-list-4ccc-c07f-4090-958c-cfe3ce3889e5-0 是基础清单列表(上图中的 manifest-list-1-base),实际上是空的。
manifest-list-4ccc-c07f-4090-958c-cfe3ce3889e5-1 是增量清单列表(上图中的 manifest-list-1-delta),它包含一组对数据文件进行操作的清单条目,在此情形下指的是 manifest-1-0。
现在,让我们插入一批跨不同分区的记录,并看看会发生什么。在 Flink SQL 中,执行以下语句:
INSERT INTO T VALUES (2, 10002, 'varchar00002', '20230502'),(3, 10003, 'varchar00003', '20230503'),(4, 10004, 'varchar00004', '20230504'),(5, 10005, 'varchar00005', '20230505'),(6, 10006, 'varchar00006', '20230506'),(7, 10007, 'varchar00007', '20230507'),(8, 10008, 'varchar00008', '20230508'),(9, 10009, 'varchar00009', '20230509'),(10, 10010, 'varchar00010', '20230510');
第二次提交完成后,执行 SELECT * FROM T 将会返回10行记录。一个新的快照被创建,即 snapshot-2,它提供了以下物理文件布局:
% ls -1tR . ./T:dt=20230501dt=20230502 dt=20230503 dt=20230504 dt=20230505 dt=20230506 dt=20230507 dt=20230508 dt=20230509 dt=20230510 snapshotschemamanifest
./T/snapshot:LATESTsnapshot-2EARLIESTsnapshot-1
./T/manifest:manifest-list-9ac2-5e79-4978-a3bc-86c25f1a303f-1 # delta manifest list for snapshot-2manifest-list-9ac2-5e79-4978-a3bc-86c25f1a303f-0 # base manifest list for snapshot-2 manifest-f1267033-e246-4470-a54c-5c27fdbdd074-0 # manifest file for snapshot-2
manifest-list-4ccc-c07f-4090-958c-cfe3ce3889e5-1 # delta manifest list for snapshot-1 manifest-list-4ccc-c07f-4090-958c-cfe3ce3889e5-0 # base manifest list for snapshot-1manifest-2b833ea4-d7dc-4de0-ae0d-ad76eced75cc-0 # manifest file for snapshot-1
./T/dt=20230501/bucket-0:data-b75b7381-7c8b-430f-b7e5-a204cb65843c-0.orc
...# each partition has the data written to bucket-0...
./T/schema:schema-0
截至 snapshot-2 的新文件布局看起来如下图所示:
删除数据
现在我们来删除符合 dt>=20230503 条件的记录。在 Flink SQL 中,执行以下语句:
DELETE FROM T WHERE dt >= '20230503';
第三次提交完成后,它生成了 snapshot-3。现在,列出表中的文件,注意到没有任何分区被移除。取而代之的是,为20230503至20230510的分区创建了一个新的数据文件:
./T/dt=20230510/bucket-0:data-b93f468c-b56f-4a93-adc4-b250b3aa3462-0.orc # newer data file created by the delete statement data-0fcacc70-a0cb-4976-8c88-73e92769a762-0.orc # older data file created by the insert statement
这很合理,因为在第二次提交时我们插入了一条记录(表示为 +I[10, 10010, 'varchar00010', '20230510']),然后在第三次提交时删除了这条记录。执行 SELECT * FROM T 将返回2行数据,具体是:
+I[1, 10001, 'varchar00001', '20230501']+I[2, 10002, 'varchar00002', '20230502']
截至 snapshot-3 的新文件布局看起来如下图所示:
注意,manifest-3-0 包含8个添加(ADD)操作类型的清单条目,对应于8个新写入的数据文件。
压缩表
小文件的数量会随着快照的增加而增多,这可能会导致读取性能下降。因此,为了减少小文件的数量,需要执行一次全面压缩(full-compaction)。
现在,让我们触发全面压缩,并通过 flink run 来运行一个专门的压缩作业:
<FLINK_HOME>/bin/flink run \ -D execution.runtime-mode=batch \ /path/to/paimon-flink-action-0.9.0.jar \ compact \ --warehouse <warehouse-path> \ --database <database-name> \ --table <table-name> \ [--partition <partition-name>] \ [--catalog_conf <paimon-catalog-conf> [--catalog_conf <paimon-catalog-conf> ...]] \ [--table_conf <paimon-table-dynamic-conf> [--table_conf <paimon-table-dynamic-conf>] ...]
举例:
./bin/flink run \ ./lib/paimon-flink-action-0.9.0.jar \ compact \ --path file:///tmp/paimon/default.db/T
所有当前的表文件都将被压缩,并且会生成一个新的快照,即 snapshot-4,它包含以下信息:
{ "version" : 3, "id" : 4, "schemaId" : 0, "baseManifestList" : "manifest-list-9be16-82e7-4941-8b0a-7ce1c1d0fa6d-0", "deltaManifestList" : "manifest-list-9be16-82e7-4941-8b0a-7ce1c1d0fa6d-1", "changelogManifestList" : null, "commitUser" : "a3d951d5-aa0e-4071-a5d4-4c72a4233d48", "commitIdentifier" : 9223372036854775807, "commitKind" : "COMPACT", "timeMillis" : 1684163217960, "logOffsets" : { }, "totalRecordCount" : 38, "deltaRecordCount" : 20, "changelogRecordCount" : 0, "watermark" : -9223372036854775808}
截至 snapshot-4 的新文件布局如下所示:
注意,manifest-4-0 包含20个清单条目(18个 DELETE 操作和2个 ADD 操作):
• 对于20230503至20230510的分区,每个分区有两个数据文件的删除操作。
• 对于20230501至20230502的分区,同一个数据文件有一个删除操作和一个添加操作。
修改表
执行以下语句以配置完整压缩(full-compaction):
ALTER TABLE T SET ('full-compaction.delta-commits' = '1');
这将为 Paimon 表创建一个新的模式(schema),即 schema-1,但直到下一次提交之前,没有快照会使用这个新 schema。
快照过期
请注意,标记的数据文件并不会真正被删除,直到快照过期且没有消费者依赖于该快照。
在快照过期的过程中,首先确定要过期的快照范围,然后标记这些快照中的数据文件以供删除。只有当存在引用特定数据文件的 DELETE 类型清单条目时,数据文件才会被标记为删除。这种标记确保了文件不会被后续快照使用,并可以安全移除。
假设上述图中的所有4个快照即将过期。过期过程如下:
-
首先删除所有标记的数据文件,并记录所有更改的桶(buckets)。
-
然后删除每一个变更日志文件和相关的清单(manifests)。
-
最后,删除快照本身并写入最早的提示文件(hint file)。如果删除过程后有任何目录为空,则这些空目录也会被删除。
假设另一个快照,即 snapshot-5 被创建并且触发了快照过期机制。此时,snapshot-1 到 snapshot-4 将被删除。为了简化说明,我们只关注来自之前快照的文件,在快照过期后的最终布局看起来像:
因此,20230503 至 20230510 的分区在物理上被删除了。
Flinkl流式写入举例
最后,我们将利用 CDC(变更数据捕获)摄入的示例来演示Flink 流写入。这部分内容将涵盖变更数据的捕获与写入到 Paimon 的过程,以及异步压缩和快照提交与过期背后的机制。
首先,让我们先来看一下 CDC 摄入的工作流程,以及各个组件在其中发挥的作用。
-
MySQL CDC 源统一读取快照数据和增量数据,SnapshotReader 负责读取快照数据,而 BinlogReader 负责读取增量数据。
-
Paimon Sink 在桶(bucket)级别将数据写入 Paimon 表。其内部的 CompactManager 将异步触发压缩操作。
-
Committer Operator 是一个单例组件,负责提交和过期快照。
接下来,我们来详细介绍端到端的数据流
MySQL CDC 源读取快照数据和增量数据,并在归一化后将这些数据.emit(发送)到下游。
Paimon Sink 首先在基于堆的 LSM 树中缓冲新记录,当内存缓冲区满时,会将这些记录刷新到磁盘。请注意,每个写入的数据文件都是一个已排序的片段(sorted run)。此时,还没有创建任何清单文件和快照。在 Flink 检查点即将发生之前,Paimon Sink 会刷新所有缓冲的记录,并向下游发送可提交的消息,此消息将在检查点期间由 Committer Operator 读取并提交。
在检查点期间,Committer Operator 会创建一个新的快照,并将其与清单列表关联,从而使该快照包含表中所有数据文件的信息。
稍后可能会进行异步压缩,CompactManager 产生的可提交信息包含关于之前文件和合并文件的信息,使得 Committer Operator 可以构建相应的清单条目。在这种情况下,Committer Operator 在 Flink 检查点期间可能会产生两个快照:一个是用于写入数据的快照(Append 类型),另一个是用于压缩的快照(Compact 类型)。如果在检查点间隔期间没有写入任何数据文件,则只会创建 Compact 类型的快照。Committer Operator 还会检查快照是否过期,并执行已标记数据文件的物理删除。