Apache Paimon 表模式(Table Mode)详解

Apache Paimon 表模式(Table Mode)详解

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:读取密集型场景,每次写入都完整压缩以换取最优读性能

最佳实践

  1. 默认选择 MOW 模式:适合大多数主键表的更新和删除场景
  2. MOR 模式优化:控制单桶数据量在 200MB-1GB,避免读取时合并过多文件
  3. 专用 Compaction Job:多写入任务场景下,使用独立 Job 执行 Compaction 避免冲突

文档更新时间: 2025-12-04
参考文档: Apache Paimon 1.3 官方文档

域名转发架构文档 2025-12-06
Paimon 表 Index 文件详解 2025-12-04

评论区