未记录事实核验 · 状态:维护中
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本文适合用于理解主键表的LSM读写路径、快照可见性及读写放大与合并的权衡,不是所有Paimon表类型的统一执行规范。主键更新结果取决于merge engine及序列或写入顺序,追加表bucket=-1示例不代表主键表动态分桶。正文中的默认128MB、Bloom Filter建议、分桶路由示意、具体合并范围、配置项和合并命令尚缺明确的版本、执行环境及来源依据,不能直接作为生产配置规范;文件数量和空间节省示例也应按简化模型理解,而非实测收益或即时物理回收承诺。本次仅作全文结构审阅,未作外部事实核验,生产使用前仍需按目标版本确认功能条件、配置方式和执行效果。
🏗️ Paimon架构原理 - LSM树与读写流程
abstract 📋 本节概览
- 🎯 学习目标:深入理解Paimon的存储架构和数据流转机制
- 📚 前置知识:Paimon核心概念
- ⏱️ 预计用时:25分钟
- 🔧 核心内容:LSM树架构、写入流程、读取流程、合并机制
- 💡 关键收获:理解Paimon如何实现高性能读写
🎯 为什么需要理解架构原理
理解Paimon的架构原理,就像理解汽车引擎的工作原理:
- ✅ 性能调优:知道瓶颈在哪,如何优化
- ✅ 问题诊断:出问题时能快速定位原因
- ✅ 合理使用:根据原理选择最佳使用方式
- ✅ 架构设计:设计系统时做出正确的技术选型
🌳 LSM树存储架构
📖 什么是LSM树
LSM(Log-Structured Merge-tree) 是一种针对写密集场景优化的数据结构,广泛应用于现代存储系统(如RocksDB、HBase、Cassandra)。
核心思想:
1. 新数据先写入内存(MemTable)
2. 内存满后刷写到磁盘(Level0)
3. 后台异步合并,形成有序的大文件
4. 查询时需要合并多个层级的数据
生活化类比:就像学习笔记的整理过程
- 上课记笔记:快速记录在草稿本(内存)
- 课后整理:将笔记誊写到笔记本(Level0)
- 周末总结:将一周笔记归纳整理(Level1+)
- 复习查找:从各个笔记本中找到需要的内容
🏗️ Paimon的LSM架构
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
graph TB
A[写入请求] --> B[内存缓冲MemTable]
B --> C{内存满?}
C -->|是| D[刷写到Level0]
C -->|否| B
D --> E[Level0文件]
E --> F{触发合并?}
F -->|是| G[后台合并任务]
F -->|否| E
G --> H[Level1文件]
H --> I[Level2+文件]
style B fill:#e1f5fe
style E fill:#fff3e0
style H fill:#c8e6c9
style I fill:#a5d6a7
📊 存储层级详解
Level 0 - 新数据层: 特征:✅ 刚从内存刷写的文件 / ✅ 文件较小(默认128MB)/ ✅ 文件内部按键排序,不同文件的键值范围可能重叠 / ❌ 查询需要扫描多个文件
示例文件(bucket-0/):
| 文件名 | 大小 | 时间范围 |
|---|---|---|
| data-level0-001.parquet | 50MB | ts: 10:00-10:05 |
| data-level0-002.parquet | 80MB | ts: 10:05-10:10 |
| data-level0-003.parquet | 120MB | ts: 10:10-10:15 |
Level 1+ - 合并数据层: 特征:✅ 合并后的有序文件 / ✅ 文件较大(可达数GB)/ ✅ 键值范围不重叠 / ✅ 查询性能优秀
示例文件(bucket-0/):
| 文件名 | 大小 | key 范围 |
|---|---|---|
| data-level1-001.parquet | 500MB | key: 1-1000000 |
| data-level1-002.parquet | 500MB | key: 1000001-2000000 |
层级关系:
| 层级 | 特征 |
|---|---|
| Level 0 | 多个小文件,键值范围可能重叠 |
| Level 1 | 少量有序文件,键值范围不重叠(由 Level 0 合并而来) |
| Level 2+ | 大文件,完全有序(继续合并产生) |
✍️ 写入流程详解
📝 完整写入路径
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
sequenceDiagram
participant App as 应用程序
participant Writer as Paimon Writer
participant Mem as 内存缓冲
participant L0 as Level0文件
participant Compactor as 合并器
participant L1 as Level1+文件
App->>Writer: 写入数据
Writer->>Mem: 追加到MemTable
alt 内存满
Mem->>L0: 刷写文件
L0->>Writer: 返回文件信息
end
Writer->>App: 写入调用返回(不代表快照已提交或数据已对读者可见)
Note over L0,Compactor: 后台异步执行
alt 触发合并条件
Compactor->>L0: 读取Level0文件
Compactor->>L1: 合并写入Level1
Note over L0,L1: 合并结果提交后替换当前快照中的文件引用;旧文件物理回收需满足历史快照与过期清理条件
end
🔄 写入步骤详解
步骤1:数据路由
// 根据主键或分区键计算bucket
int bucket = hash(primaryKey) % bucketNum;
// 示例:
user_id = 1001
bucket = 1001 % 8 = 1
→ 数据写入bucket-1
步骤2:内存缓冲
每个bucket维护独立的MemTable:
bucket-0: MemTable-0 (当前大小: 50MB)
bucket-1: MemTable-1 (当前大小: 120MB) ← 接近阈值
bucket-2: MemTable-2 (当前大小: 30MB)
...
当MemTable大小超过阈值(默认128MB)时触发刷写
步骤3:刷写磁盘
1. 对MemTable中的数据排序(按主键)
2. 写入Parquet文件到Level0
3. 清空MemTable,释放内存
4. 更新元数据
生成文件:
/dt=2024-01-01/bucket-1/data-uuid-level0.parquet
步骤4:提交快照
1. 生成Manifest文件,记录新增的数据文件
2. 生成Manifest List,引用所有Manifest
3. 创建新快照,指向Manifest List
4. 快照提交成功,数据可见
⚡ 写入性能优化
批量写入:
-- 不推荐:逐条插入
INSERT INTO my_table VALUES (1, 'a');
INSERT INTO my_table VALUES (2, 'b');
-- 推荐:批量插入
INSERT INTO my_table VALUES
(1, 'a'),
(2, 'b'),
(3, 'c');
写入并行度与缓冲配置示意(不代表启用异步刷写):
// 写入并行度与缓冲配置示意;具体设置方式取决于执行环境
TableConfig config = new TableConfig();
config.set("sink.parallelism", "4"); // 4个并发写入
config.set("write-buffer-size", "256mb"); // 更大的缓冲
生活化理解:写入流程就像餐厅收银
- 接单:服务员记录订单(内存缓冲)
- 汇总:一批订单汇总后交给后厨(刷写Level0)
- 归档:定期整理订单记录(合并到Level1+)
📖 读取流程详解
🔍 查询路径
%%{init: {"theme":"base","themeVariables":{"fontFamily":"PingFang SC, -apple-system, Source Han Serif SC, serif","fontSize":"16px","background":"#FDFBF5","primaryColor":"#FDFBF5","primaryTextColor":"#2D2A26","primaryBorderColor":"#6E6A60","secondaryColor":"#E8EFE8","secondaryBorderColor":"#8FA890","secondaryTextColor":"#2D2A26","tertiaryColor":"#FDFBF5","tertiaryBorderColor":"#9A9488","tertiaryTextColor":"#6E6A60","lineColor":"#6E6A60","textColor":"#2D2A26","mainBkg":"#FDFBF5","nodeBorder":"#2D5A3D","clusterBkg":"#F3EEDF","clusterBorder":"#9A9488","edgeLabelBackground":"#FDFBF5"},"flowchart":{"htmlLabels":true,"curve":"basis","padding":20,"nodeSpacing":60,"rankSpacing":70,"useMaxWidth":false},"themeCSS":".node rect, .node polygon, .node circle { stroke-width: 1.5px; } .edgePath .path { stroke-width: 1.5px; } .cluster-label .nodeLabel, .cluster-label foreignObject { color: #2D2A26 !important; } .edgeLabel, .edgeLabel foreignObject { color: #2D2A26 !important; background-color: #FDFBF5; } .edgeLabel rect { fill: #FDFBF5; }"}}%%
graph TB
A[查询请求] --> B{主键查询?}
B -->|是| C[计算Bucket]
B -->|否| D[扫描所有Bucket]
C --> E[读取Bucket元数据]
D --> E
E --> F[分区裁剪]
F --> G[文件过滤]
G --> H[读取数据文件]
H --> I[Level0文件]
H --> J[Level1+文件]
I --> K[合并结果]
J --> K
K --> L[返回数据]
style C fill:#e1f5fe
style G fill:#fff3e0
style K fill:#c8e6c9
📊 读取优化机制
1. 分区裁剪(Partition Pruning):
-- 查询带分区条件
SELECT * FROM orders
WHERE dt = '2024-01-01' AND user_id = 1001;
优化过程:
示例假设:全部分区(365天) → 裁剪后(1天) → 候选分区数减少到原来的1/365;实际性能提升取决于数据分布、读取量及执行开销
2. 文件过滤(File Filtering):
基于文件元信息过滤:
- 主键范围过滤
- 统计信息过滤(min/max)
- Bloom Filter过滤
示例:
查询 user_id = 1001
文件A: [min=1, max=1000] → 跳过
文件B: [min=1001, max=2000] → 读取 ✓
文件C: [min=2001, max=3000] → 跳过
3. 数据合并(Data Merging):
主键表需要合并多个层级的数据:
Level0: user_id=1001, age=25, city='Beijing'
Level1: user_id=1001, age=24, city='Shanghai'
合并结果:
user_id=1001, age=25, city='Beijing' (示例假设:采用保留最新记录的合并规则,且序列或写入顺序判定上述Level0记录较新;不能仅由文件层级判断)
🎯 查询模式优化
批量扫描:
-- 全表扫描,适合分析场景
SELECT COUNT(*) FROM orders WHERE dt >= '2024-01-01';
优化建议:
✅ 设置合理的并行度
✅ 使用列裁剪,减少读取数据量
✅ 定期合并,减少文件数量
点查询:
-- 主键点查,适合实时查询
SELECT * FROM user_info WHERE user_id = 1001;
优化建议:
✅ 使用主键表
✅ 设置合适的bucket数
✅ 开启Bloom Filter
增量读取:
-- 流式消费,订阅增量数据
SELECT * FROM orders /*+ OPTIONS('scan.mode'='latest') */;
适用场景:
✅ 实时数据同步
✅ 增量ETL
✅ 下游流处理
🔄 合并机制详解
🎯 为什么需要合并
问题1:小文件过多
影响:
❌ 查询需要打开大量文件,性能下降
❌ 文件元数据占用内存增加
❌ 分布式系统中文件数限制
示例:
合并前: 1000个小文件(100MB each) → 打开1000次
合并后: 10个大文件(10GB each) → 打开10次
问题2:读放大
主键查询需要查多个层级:
Level0: 5个文件
Level1: 3个文件
Level2: 1个文件
→ 总共读取9个文件
合并后:
Level1: 1个文件
→ 只读取1个文件
问题3:空间放大
删除的数据仍占用空间:
user_id=1001: [version1, version2, version3]
实际有效: version3
占用空间: 3个版本的总和
合并后:
user_id=1001: [version3]
空间节省: 66%
📊 合并策略
1. Universal Compaction(通用合并):
触发条件:
- sorted run数量达到相关配置阈值(不等同于Level0文件数;具体默认值与触发规则需结合所用版本确认)
- 文件总大小达到阈值
合并范围:
所有Level0文件 + 部分Level1文件
合并结果:
生成新的合并文件;结果提交后替换当前快照中的旧文件引用,旧文件的物理回收还取决于历史快照引用及过期清理条件
配置示例:
'compaction.max-sorted-run-num' = '5'
2. Full Compaction(全量合并):
触发方式:
手动触发或定时任务
合并范围:
所有层级的所有文件
合并结果:
重写合并范围内的数据文件,并按适用的合并规则处理更新和删除;不保证历史快照引用的旧文件或已删除数据立即从存储中物理清除
执行命令:
CALL sys.compact('db.table');
⚙️ 合并配置
合并时机控制:
CREATE TABLE my_table (
...
) WITH (
-- 自动合并
'compaction.max-sorted-run-num' = '5', -- sorted run数量阈值,不等同于Level0文件数
-- 合并策略的大小放大比例阈值,并非并行度
'compaction.max-size-amplification-percent' = '200',
-- 合并文件大小
'compaction.max-file-size' = '1GB'
);
流式写入及内存配置示意(不是独立合并任务示例):
-- Flink流式写入任务示意,不是独立合并任务
INSERT INTO my_table
/*+ OPTIONS(
'sink.use-managed-memory-allocator' = 'true',
'sink.managed.writer-buffer-memory' = '256mb'
) */
SELECT * FROM source_table;
🎯 合并最佳实践
场景1:高频更新的主键表
问题: 频繁更新导致大量Level0文件
方案:
✅ 降低合并阈值: 'compaction.max-sorted-run-num' = '3'
✅ 启动专门的合并任务
✅ 增加写入缓冲: 'write-buffer-size' = '256mb'
场景2:大规模仅追加表
问题: 写入量大,合并跟不上
方案:
✅ 对仅追加表,可评估不分桶模式: 'bucket' = '-1'(不是主键表动态分桶;支持范围与适用条件需结合所用版本确认)
✅ 增加合并并行度
✅ 定期全量合并
场景3:混合读写负载
问题: 查询和写入相互影响
方案:
✅ 读写分离: 查询走历史快照
✅ 错峰合并: 低峰期执行全量合并
✅ 资源隔离: 合并任务独立资源池
📊 架构性能特征
✅ 优势总结
| 特性 | 原理 | 适用场景 |
|---|---|---|
| 高吞吐写入 | 顺序写入,批量刷写 | 日志采集、埋点数据 |
| 支持更新删除 | LSM树合并机制 | 用户画像、CDC同步 |
| 查询优化 | 分区裁剪、文件过滤 | 分析查询、报表 |
| 存储高效 | 列式存储、压缩 | 海量数据存储 |
⚠️ 权衡与局限
写放大:
一份数据可能被多次重写(合并过程)
影响: 增加磁盘写入量
缓解: 调整合并策略,减少合并频率
读放大:
查询可能需要读取多个层级的文件
影响: 点查询性能
缓解: 及时合并,使用Bloom Filter
空间放大:
删除的数据在合并前仍占用空间
影响: 存储成本
缓解: 定期全量合并
📋 本节要点回顾
✅ 核心知识点检查
- [ ] 理解LSM树的基本原理和优势
- [ ] 掌握Paimon的写入流程和优化方法
- [ ] 了解查询的执行路径和优化机制
- [ ] 理解合并的必要性和策略选择
🎯 实践能力检查
- [ ] 能够根据业务选择合适的合并策略
- [ ] 会分析和优化写入性能
- [ ] 能够诊断查询慢的原因
- [ ] 理解读写放大的权衡
🔗 学习路径连接
已完成:✅ 01-概述 → ✅ 02-核心概念 → ✅ 03-架构原理 下一步:Paimon环境搭建 - 动手搭建开发环境
tip 深入学习建议
架构原理是性能调优的基础。建议结合实际场景,观察不同配置下的性能表现,加深对原理的理解。
创建时间:2024-12-04 最后更新:2024-12-04 学习时长:25分钟 难度等级:⭐⭐ (中级)
#Paimon #架构原理 #LSM树 #读写流程 #合并机制 #性能优化