Apache Paimon 表模式(Table Mode)详解
总结
- 更新时间:2025-12-04
- 主要修改内容:
- 新增了 MOR 模式的详细读写机制说明
- 补充了 MOR 模式中 Compaction 触发时机的详细解释(区分 Flink 和 Spark)
- 明确了读取时的数据合并是内存计算而非物理文件修改
- 重点说明了 Spark 批处理任务中 Compaction 的同步执行机制
- 新增了 Mermaid 工作流程图,更直观展示 Flink 和 Spark 的差异(GitHub/GitLab 原生支持)
- 总结内容:
- 本文档详细介绍了 Paimon 的三种表模式(MOR、COW、MOW),推荐使用 MOW 模式实现读写性能平衡
- MOR 模式在读取时只进行内存中的数据合并计算,不修改物理文件
- Compaction 触发机制因引擎而异:
- Flink 流式任务:真正的后台异步执行
- Spark 批处理任务:在任务提交前同步执行,跨任务累计文件数
- COW 模式每次写入都执行完整压缩,适合读取密集型场景
- MOW 模式通过删除向量技术避免频繁数据重写,适合大多数生产场景
- 文档涵盖了各模式的工作原理、性能对比、配置示例和最佳实践
核心概念
Paimon 主键表采用 LSM 树(Log-Structured Merge-Tree) 结构:
- 表或分区包含多个桶(Buckets)
- 每个桶是一个独立的 LSM 树,包含多层文件
- 写入时,Flink checkpoint 刷新 L0 文件并触发压缩
三种表模式对比
1. MOR(Merge On Read)- 默认模式
特点:
- ✅ 只执行次要压缩(minor compaction),不进行完整合并
- ⚠️ 读取时需要对所有文件进行多路合并(multi-way merge)
- ⚠️ 单个 LSM 树限制为单线程读取,并发性受限
性能:
- 写入性能:⭐⭐⭐⭐⭐ 优秀
- 读取性能:⭐⭐ 较差
适用场景:
- 写入密集型业务
- 可接受读取延迟的场景
详细工作机制
写入时:
- 快速写入增量数据文件(delta files)到 L0 层
- 不进行文件合并和重写
- 每次 Flink checkpoint 刷新一批新文件
- 写入性能极高,无额外开销
读取时(关键):
- ✅ 实时合并计算:根据 Manifest 清单文件,将多个层级的 SST 文件(L0、L1、L2...)的数据在内存中进行合并
- ✅ 不修改物理文件:合并过程仅在计算层面完成,按主键去重和合并后返回结果集
- ❌ 不触发 Compaction:读操作不会触发任何物理文件的压缩和重写
- ⚠️ 性能开销:每次读取都需要扫描和合并多个文件,因此读取性能较低
Compaction 触发时机:
MOR 模式的 Compaction 触发机制因计算引擎而异:
1. Flink 流式任务(真正的后台异步):
- Compaction 在 checkpoint 间隙异步执行
- 写入任务持续运行,Compaction 在后台独立进行
- 不阻塞主写入流程
2. Spark 批处理任务(任务内同步执行):
- ⚠️ 关键差异:Spark 批处理任务执行完成后进程退出,不存在"后台"
- 自动触发时机:
- 根据
num-sorted-run.compaction-trigger配置(默认 5 个文件) - 当 delta 文件数量达到阈值时,在任务提交(commit)前同步执行 Compaction
- Compaction 完成后任务才结束
- 根据
- 跨任务场景示例:
第一次写入:3 个 delta 文件 → 未达阈值 → 不 Compact → 任务结束 第二次写入:3 个 delta 文件 → 累计 6 个 → 达到阈值 → 在第二次任务结束前同步执行 Compaction - 配置建议:
-- 控制 Compaction 频率 'num-sorted-run.compaction-trigger' = '5' -- 默认值 -- 如果每次批处理数据量大,可适当提高阈值 'num-sorted-run.compaction-trigger' = '10'
3. 手动触发(适用所有引擎):
-- 手动执行 Compaction
CALL sys.compact('database.table');
4. 专用 Compaction Job(推荐生产环境):
- 启动独立的 Flink/Spark Job 专门执行 Compaction
- 推荐在多写入任务场景下使用,避免冲突
- 不影响主写入任务性能
- Spark 示例:
// 定期执行的 Compaction Job spark.sql("CALL sys.compact('database.table')")
工作流程图示:
Flink 流式任务:
%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '16px'}}}%%
graph TB
subgraph write["写入流程 (快速)"]
W1[接收数据] --> W2[写入 L0 文件<br/>快速写入]
W2 --> W3[完成]
W3 --> W4[继续运行<br/>流式任务]
W4 -.-> W1
style W1 fill:#90EE90
style W2 fill:#90EE90
style W3 fill:#90EE90
style W4 fill:#90EE90
end
subgraph compact["Compaction 流程 (后台异步)"]
C1[检测文件数<br/>达到阈值] --> C2[checkpoint 间隙<br/>执行 Compaction]
C2 --> C3[生成 L1/L2 文件]
style C1 fill:#FFD700
style C2 fill:#FFD700
style C3 fill:#FFD700
end
subgraph read["读取流程 (内存合并)"]
R1[接收查询] --> R2[读取 Manifest]
R2 --> R3[扫描多层文件<br/>L0, L1, L2...]
R3 --> R4[内存合并数据]
R4 --> R5[返回结果]
style R1 fill:#87CEEB
style R2 fill:#87CEEB
style R3 fill:#87CEEB
style R4 fill:#87CEEB
style R5 fill:#87CEEB
end
W4 -.异步后台.-> C1
classDef noteStyle fill:#FFF9C4,stroke:#F57C00,stroke-width:2px
note1[⚠️ Compaction 与写入并行<br/>不阻塞主写入流程]:::noteStyle
note2[⚠️ 读取不修改物理文件<br/>不触发 Compaction]:::noteStyle
Spark 批处理任务:
%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '16px'}}}%%
graph TB
subgraph task1["第一次任务"]
T1_1[接收数据] --> T1_2[写入 3 个 L0 文件]
T1_2 --> T1_3{达到阈值?<br/>默认 5 个}
T1_3 -->|否| T1_4[不执行 Compaction]
T1_4 --> T1_5[任务结束]
T1_5 --> T1_6[进程退出]
T1_3 -->|是| T1_7[执行 Compaction]
style T1_1 fill:#90EE90
style T1_2 fill:#90EE90
style T1_4 fill:#E8E8E8
style T1_6 fill:#FFB6C1
end
subgraph task2["第二次任务"]
T2_1[接收数据] --> T2_2[写入 3 个 L0 文件]
T2_2 --> T2_3[累计 6 个文件]
T2_3 --> T2_4{达到阈值?<br/>默认 5 个}
T2_4 -->|是| T2_5[任务内同步执行<br/>Compaction]
T2_5 --> T2_6[生成 L1/L2 文件]
T2_6 --> T2_7[任务结束]
T2_7 --> T2_8[进程退出]
T2_4 -->|否| T2_9[任务结束]
style T2_1 fill:#90EE90
style T2_2 fill:#90EE90
style T2_3 fill:#90EE90
style T2_5 fill:#FFD700
style T2_6 fill:#FFD700
style T2_8 fill:#FFB6C1
end
subgraph read["读取流程"]
R1[接收查询] --> R2[读取 Manifest]
R2 --> R3[扫描多层文件<br/>L0, L1, L2...]
R3 --> R4[内存合并数据]
R4 --> R5[返回结果]
style R1 fill:#87CEEB
style R2 fill:#87CEEB
style R3 fill:#87CEEB
style R4 fill:#87CEEB
style R5 fill:#87CEEB
end
T1_6 -.第二次启动.-> T2_1
classDef noteStyle fill:#FFF9C4,stroke:#F57C00,stroke-width:2px
note1[⚠️ 跨任务累计文件数<br/>第二次检测到累计超过阈值]:::noteStyle
note2[⚠️ 读取不修改物理文件<br/>不触发 Compaction]:::noteStyle
简化文本流程:
Flink 流式任务:
写入流程:数据 → L0 文件(快速写入)→ 完成 → 继续运行
↓ (异步后台,checkpoint 间隙)
Compaction → L1/L2 文件
读取流程:查询 → 读取 Manifest → 扫描多层文件 → 内存合并 → 返回结果
(不修改任何文件)
Spark 批处理任务:
第一次任务:数据 → L0 文件(3个)→ 未达阈值 → 任务结束(进程退出)
第二次任务:数据 → L0 文件(3个)→ 累计 6 个文件,达到阈值
↓ (任务内同步)
执行 Compaction → L1/L2 文件
↓
任务结束(进程退出)
读取流程:查询 → 读取 Manifest → 扫描多层文件 → 内存合并 → 返回结果
(不修改任何文件)
2. COW(Copy On Write)
配置方式:
'full-compaction.delta-commits' = '1'
特点:
- ✅ 每次写入都触发完整合并
- ✅ 所有数据合并到最高级别,读取无需合并
- ⚠️ 写入开销巨大
性能:
- 写入性能:⭐ 很差
- 读取性能:⭐⭐⭐⭐⭐ 优秀
适用场景:
- 读取密集型业务
- 数据变更不频繁的场景
- 对查询延迟要求极高的场景
3. MOW(Merge On Write)- 推荐模式 ⭐
配置方式:
'deletion-vectors.enabled' = 'true'
特点:
- ✅ 生成删除向量文件(Deletion Vectors)标记已删除数据
- ✅ 读取时直接过滤不必要的行
- ✅ 相当于在读取时合并,但不影响读取性能
- ✅ 写入和读取性能均衡
性能:
- 写入性能:⭐⭐⭐⭐ 良好
- 读取性能:⭐⭐⭐⭐ 良好
适用场景:
- 通用主键表(默认 merge-engine 为 dedup)
- 读写负载均衡的业务
- 官方推荐的默认选择
最佳实践建议
1. 模式选择
- 优先选择 MOW 模式(开启删除向量)
- 极端读取密集型场景考虑 COW
- 预算充足且读取要求高可使用 MOW + 周期性全量压缩
2. 数据规模控制
- MOR 模式中,推荐单个桶数据量控制在 200MB - 1GB
- 避免单桶数据过大导致读取性能下降
3. 查询优化
- MOR 模式下可使用 read-optimized 系统表提升查询性能
- 该表返回已压缩好的数据,牺牲实时性换取性能
配置示例
MOW 模式(推荐)
CREATE TABLE my_table (
id BIGINT PRIMARY KEY NOT ENFORCED,
name STRING,
age INT
) WITH (
'deletion-vectors.enabled' = 'true'
);
COW 模式
CREATE TABLE my_table_cow (
id BIGINT PRIMARY KEY NOT ENFORCED,
name STRING,
age INT
) WITH (
'full-compaction.delta-commits' = '1'
);
MOR 模式(默认,无需额外配置)
CREATE TABLE my_table_mor (
id BIGINT PRIMARY KEY NOT ENFORCED,
name STRING,
age INT
);
性能对比总结
| 模式 | 写入性能 | 读取性能 | 推荐场景 |
|---|---|---|---|
| MOR | ⭐⭐⭐⭐⭐ 优秀 | ⭐⭐ 较差 | 写入密集型 |
| COW | ⭐ 很差 | ⭐⭐⭐⭐⭐ 优秀 | 读取密集型 |
| MOW ⭐ | ⭐⭐⭐⭐ 良好 | ⭐⭐⭐⭐ 良好 | 通用场景(推荐) |
详细对比
| 维度 | MOR | COW | MOW |
|---|---|---|---|
| 写入行为 | 快速写入 delta 文件 | 写入时立即执行 full compaction | 写入删除向量,不重写文件 |
| 读取行为 | 读时在内存合并多个文件 | 直接读取已压缩文件 | 根据删除向量过滤数据 |
| Compaction 时机 | 后台异步执行 | 每次写入时 | 后台异步执行 |
| 文件修改 | 读取不修改文件 | 写入时修改文件 | 写入不修改数据文件 |
| 适用负载 | 写多读少 | 读多写少 | 读写均衡 |
技术原理
LSM 树结构
表/分区
├── Bucket 0 (LSM 树)
│ ├── L0 (Level 0) - 最新写入的文件
│ ├── L1 (Level 1) - 第一次压缩后的文件
│ ├── L2 (Level 2) - 第二次压缩后的文件
│ └── ...
├── Bucket 1 (LSM 树)
└── Bucket N (LSM 树)
删除向量(Deletion Vectors)原理
- MOW 模式下,不立即物理删除数据
- 生成单独的删除向量文件,标记哪些行已删除
- 读取时根据删除向量过滤数据
- 避免了频繁的数据重写,提升写入性能
核心要点总结
MOR 模式关键理解
- ✅ 读取 = 内存计算合并:读操作只在内存中合并数据,不修改任何物理文件
- ❌ 读取 ≠ 触发 Compaction:读操作不会触发文件压缩
- 📁 Compaction 触发机制因引擎而异:
- Flink:真正的后台异步,在 checkpoint 间隙执行
- Spark:任务内同步执行,在任务提交前完成,跨任务累计文件数
- 如果第一次写入未达阈值,第二次写入会检测累计文件数并触发 Compaction
模式选择建议
- MOW:通用场景首选,通过删除向量技术实现读写平衡
- MOR:写入密集型场景,可接受读取时多路合并的性能开销
- COW:读取密集型场景,每次写入都完整压缩以换取最优读性能
最佳实践
- 默认选择 MOW 模式:适合大多数主键表的更新和删除场景
- MOR 模式优化:控制单桶数据量在 200MB-1GB,避免读取时合并过多文件
- 专用 Compaction Job:多写入任务场景下,使用独立 Job 执行 Compaction 避免冲突
文档更新时间: 2025-12-04
参考文档: Apache Paimon 1.3 官方文档