原方案弊端 (Kafka)
Paimon 的优势
UPSERT
(更新或插入) 和 DELETE
操作。正如文档中所描述的,主键表是 Paimon 的核心功能之一,用于支持大规模的实时更新。
# Overview
If you define a table with primary key, you can insert, update or delete records in the table.
Primary keys consist of a set of columns that contain unique values for each record. Paimon enforces data ordering by
sorting the primary key within each bucket, allowing users to achieve high performance by applying filtering conditions
on the primary key. See [CREATE TABLE]({{< ref "flink/sql-ddl#create-table" >}}).
在 Flink SQL 中创建这样的主键表非常简单:
// ... existing code ...
CREATE TABLE my_table (
user_id BIGINT,
item_id BIGINT,
behavior STRING,
dt STRING,
hh STRING,
PRIMARY KEY (dt, hh, user_id) NOT ENFORCED
);
// ... existing code ...
原方案弊端 (Kafka)
UPSERT
,无法对聚合结果进行原地更新。例如,一个按用户ID统计5分钟窗口消费额的 DWS 层,10:00-10:05
的聚合结果和 10:05-10:10
的聚合结果会作为两条独立的消息存在于 Kafka 中。Paimon 的优势
UPSERT
到 Paimon 的 DWS 表中。这张表以用户ID为主键。每次新的聚合结果到来时,它会直接更新对应用户的消费总额,而不是插入一条新纪录。changelog
(变更日志)。下游系统(如 ClickHouse)可以直接消费这个 changelog
流,轻松地同步最新的聚合结果,而无需处理复杂的合并逻辑。Paimon 的 merge-engine
机制甚至允许自定义更新逻辑,例如 partial-update
(部分更新)或 aggregation
(聚合),这为构建 DWS 层提供了极大的灵活性。
// ... existing code ...
- Realtime updates:
- Primary key table supports writing of large-scale updates, has very high update performance, typically through Flink Streaming.
- Support defining Merge Engines, update records however you like. Deduplicate to keep last row, or partial-update, or aggregate records, or first-row, you decide.
// ... existing code ...
原方案弊端 (Kafka)
Paimon 的优势
Paimon 的兼容性矩阵展示了其强大的生态整合能力:
// ... existing code ...
| Engine | Version | Batch Read | Batch Write | Create Table | Alter Table | Streaming Write | Streaming Read | Batch Overwrite | DELETE & UPDATE | MERGE INTO | Time Travel |
| :-------------------------------------------------------------------------------: | :-------------: | :-----------: | :-----------: | :-------------: | :-------------: | :----------------: | :----------------: | :---------------: | :---------------: | :----------: | :-----------: |
| Flink | 1.15 - 1.20 | ✅ | ✅ | ✅ | ✅(1.17+) | ✅ | ✅ | ✅ | ✅(1.17+) | ❌ | ✅ |
| Spark | 3.2 - 3.5 | ✅ | ✅ | ✅ | ✅ | ✅(3.3+) | ✅(3.3+) | ✅ | ✅ | ✅ | ✅(3.3+) |
| Hive | 2.1 - 3.1 | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Trino | 420 - 440 | ✅ | ✅(427+) | ✅(427+) | ✅(427+) | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
// ... existing code ...
原方案弊端 (Lambda 架构)
Paimon 的优势
在ODS层完成数据解析后,可以将数据反向写入到Paimon中,这正是 Paimon 流批一体能力的完美体现。
Paimon 的架构设计就是为了解决这个问题,提供一个统一的存储底座。
// ... existing code ...
Paimon provides table abstraction. It is used in a way that
does not differ from the traditional database:
- In `batch` execution mode, it acts like a Hive table and
supports various operations of Batch SQL. Query it to see the
latest snapshot.
- In `streaming` execution mode, it acts like a message queue.
Query it acts like querying a stream changelog from a message queue
where historical data never expires.
// ... existing code ...
痛点 | Kafka 方案 | Paimon 解决方案 |
---|---|---|
数据重复 | Append-only,导致Failover后数据重复 | 主键表,通过 UPSERT 自动去重 |
DWS层构建 | 无 UPSERT,无法原地更新聚合结果 | 主键表,支持聚合结果的持续更新,可构建DWS层 |
数据共享 | No Schema,下游需重复解析 | 统一Schema,多引擎可直接用SQL查询,提升协作效率 |
架构冗余 | Lambda架构,流批两套代码和存储 | 流批一体,统一存储,简化架构,降低开发运维成本 |
优化策略主要围绕性能、存储和稳定性三个方面展开,这是构建和维护高性能湖仓系统的核心。
性能优化的核心目标是提升数据写入和处理的吞杜量,同时降低延迟。
分析:
Asynchronous Compaction
) 将合并操作与写入操作解耦。写入操作可以快速完成,将合并任务交给独立的机制或在系统负载较低时执行。这极大地提升了写入的吞吐量和稳定性,正如您提到的“节点切换的平均耗时超过 50 秒,而开启后则缩短至 20 秒”。实现与扩展:
// ... existing code ...
num-sorted-run.stop-trigger = 2147483647
sort-spill-threshold = 10
lookup-wait = false
// ... existing code ...
'write-only' = 'true'
),然后启动一个独立的、专用的 Flink 作业来负责 Compaction。这样可以为写入和 Compaction 分配独立的资源,互不干扰,实现更精细的资源管理和调优。 // ... existing code ...
public static final ConfigOption WRITE_ONLY =
key("write-only")
.booleanType()
.defaultValue(false)
.withFallbackKeys("write.compaction-skip")
.withDescription(
"If set to true, compactions and snapshot expiration will be skipped. "
+ "This option is used along with dedicated compact jobs.");
// ... existing code ...
然后通过 Flink Action 启动专用作业: // ... existing code ...
/bin/flink run \
/path/to/paimon-flink-action-{{< version >}}.jar \
compact \
// ... existing code ...
分析:
write-buffer
中累积更多,单次刷写生成的文件更大,从而减少小文件数量和 Checkpoint 开销。实现与扩展:
execution.checkpointing.interval
,还可以调整 execution.checkpointing.max-concurrent-checkpoints
来允许更多的 Checkpoint 并行,提高容错效率。write-buffer-size
,并可以开启 write-buffer-spillable
,当内存 Buffer 写满时,会先溢写到本地磁盘,而不是直接刷到远程存储,这样可以平滑 Checkpoint 峰值压力,生成更大的最终文件。// ... existing code ...
1. Flink Configuration (`'flink-conf.yaml'/'config.yaml'` or `SET` in SQL): Increase the checkpoint interval
(`'execution.checkpointing.interval'`), increase max concurrent checkpoints to 3
(`'execution.checkpointing.max-concurrent-checkpoints'`), or just use batch mode.
2. Increase `write-buffer-size`.
3. Enable `write-buffer-spillable`.
// ... existing code ...
分析:
bucket
数量相匹配或成倍数关系,以确保数据均匀分布到各个 bucket,避免数据倾斜。write-buffer-size
) 也至关重要,它直接影响单次刷盘的文件大小和 Compaction 的效率。实现与扩展:
'sink.use-managed-memory-allocator' = 'true'
。这样 Paimon Writer 会使用 Flink 的托管内存,由 Flink TaskManager 统一管理和分配,可以提高资源利用率和稳定性。// ... existing code ...
INSERT INTO paimon_table /*+ OPTIONS('sink.use-managed-memory-allocator'='true', 'sink.managed.writer-buffer-memory'='256M') */
SELECT * FROM ....;
分析:
dt
),用于数据管理和查询过滤。好的分区设计可以极大地提升查询性能,因为查询引擎可以直接跳过不相关的分区目录。bucket
的数量决定了写入的最大并行度和数据在分区内的分布。bucket
数需要 ALTER TABLE
后通过 INSERT OVERWRITE
重写数据,这是一个成本很高的操作。因此,在表设计之初就必须对未来的数据量和并发量有充分预估。实现与扩展:
dt
是最常见的策略。存储优化的核心是控制文件数量,回收无效数据,降低存储成本。
分析:
snapshot.time-retained
和 snapshot.num-retained.min
控制了快照的保留策略。过长的保留时间会导致大量元数据和数据文件堆积。changelog-producer
产生的 Changelog 文件也有独立的生命周期管理。实现与扩展:
snapshot.time-retained
就不应设置得过长。分析:
precommit-compact
: 在文件提交到快照之前进行一次合并,可以有效减少最终生成的 changelog 文件数量。full-compaction
: 全量合并,可以将一个分区/桶内的所有文件合并成一个或少数几个文件,对查询性能提升最大。可以通过 'full-compaction.delta-commits'
定期触发。实现与扩展:
Z-Order
或普通排序) 对数据进行排序。这可以极大地优化基于这些列的范围查询或点查性能,因为数据在物理上是连续存储的,可以最大化数据跳过的效果。 // ... existing code ...
CALL sys.compact(
`table` => 'database_name.table_name',
partitions => 'partition_name',
order_strategy => 'z-order',
order_by => 'col1,col2'
);
// ... existing code ...
expire_snapshots
和 drop_partition
等 Action,可以用来清理快照和分区。稳定性优化的核心是保障作业在各种异常情况下(尤其是高负载时)的健壮性和可恢复性。
分析:
consumer-id
标识)“锁定”一个快照。这个被锁定的快照及其之后的所有快照都不会被 TTL 机制自动删除,直到 Consumer 前进到新的快照。实现与扩展:
分析:
manifest
文件合并,并生成最终的 snapshot
文件。当一次 Checkpoint 写入的分区和文件非常多时,Committer 会成为瓶颈,需要大量的内存来持有这些元数据信息,也需要足够的 CPU 来完成合并。实现与扩展:
// ... existing code ...
You can use fine-grained-resource-management of Flink to increase committer heap memory only:
1. Configure Flink Configuration `cluster.fine-grained-resource-management.enabled: true`. (This is default after Flink 1.18)
2. Configure Paimon Table Options: `sink.committer-memory`, for example 300 MB, depends on your `TaskManager`.
(`sink.committer-cpu` is also supported)
// ... existing code ...
结合 Paimon 的文档和特性,我们可以看到这些策略背后都有其深刻的技术原理支撑。核心思想可以归纳为:
这些策略共同构成了一套行之有效的 Paimon 湖仓优化方法论。
快照(Snapshot)的本质 是一个元数据文件。
Paimon 的数据组织是一个清晰的层级结构,正如文档中图示的那样: Snapshot
-> Manifest List
-> Manifest
-> Data File
Manifest List
文件,以及其他元数据。您正在查看的 Snapshot.java
文件就定义了它的结构。所以,一个快照通过层层指向,最终“引用”了一批数据文件。
当对表进行更新、删除或执行 Compaction(合并)操作时,Paimon 并不会立即去物理删除旧的数据文件。它会执行一个逻辑删除:
DELETE
,将新的数据文件标记为 ADD
。此时,旧的 Snapshot 依然存在,并且它仍然指向那些被“逻辑删除”的旧数据文件。这就是 Paimon 实现时间旅行(Time Travel) 的基础——只要旧快照还在,就可以随时回到过去的数据版本。
文档 docs/content/learn-paimon/understand-files.md
中对此有清晰的描述:
Paimon maintains multiple versions of files, compaction and deletion of files are logical and do not actually delete files. Files are only really deleted when Snapshot is expired.
简单来说:Compaction 等操作只做标记,不做真删除。真正的删除由快照过期来触发。
只要有一个活跃的、未过期的快照还在引用某个数据文件,这个数据文件就是安全的,绝对不会被删除。物理删除操作只会发生在那些“无主”的文件上——即所有引用它的快照都已经过期并被清除了。
这个机制确保了数据安全性和时间旅行能力,同时通过 TTL 自动回收不再需要的历史数据,从而控制存储成本。
这个过程的实现主要在 SnapshotDeletion.java
这个类中,它负责具体的清理逻辑。
// ... existing code ...
public class SnapshotDeletion extends FileDeletionBase {
// ... existing code ...
@Override
public void cleanUnusedDataFiles(Snapshot snapshot, Predicate skipper) {
if (changelogDecoupled && !produceChangelog) {
// Skip clean the 'APPEND' data files.If we do not have the file source information
// eg: the old version table file, we just skip clean this here, let it done by
// ExpireChangelogImpl
Predicate enriched =
manifestEntry ->
skipper.test(manifestEntry)
|| (manifestEntry.fileSource().orElse(FileSource.APPEND)
== FileSource.APPEND);
cleanUnusedDataFiles(snapshot.deltaManifestList(), enriched);
} else {
cleanUnusedDataFiles(snapshot.deltaManifestList(), skipper);
}
cleanUnusedDataFiles(snapshot.baseManifestList(), skipper);
}
// ... existing code ...
这个类中的方法会遍历过期快照的 deltaManifestList
和 baseManifestList
,收集文件列表,然后执行清理。
快照 TTL(Time-To-Live,生命周期)是如何处理多版本数据删除的?
snapshot.time-retained
(保留时长) 和 snapshot.num-retained.min
(最小保留数量)。ExpireSnapshots
)会启动,它首先会删除这些过期的 snapshot
JSON 文件本身。