第5章:写入流程全解析
导言:从应用到文件的完整旅程
在第4章,我们了解了FileStore及其两种实现。但是真正的写入操作发生在哪里?数据如何从应用到达磁盘?
本章将深入讲解完整的写入链路:
scss
应用程序
↓
write(record)
↓
TableWrite(表层包装)
↓
FileStoreWrite(核心写入引擎)
↓
RecordWriter(分区/桶级别的writer)
↓
WriteBuffer(内存缓冲)
↓
Flush到磁盘
↓
生成DataFile
↓
prepareCommit(生成CommitMessage)
↓
FileStoreCommit.commit(生成Snapshot)
第一部分:写入层次结构
1.1 三层写入架构
scss
┌─────────────────────────────┐
│ TableWrite(表写) │ ← 应用直接调用
│ - write(record) │
│ - prepareCommit() │
│ - close() │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ FileStoreWrite(文件写) │ ← 路由和管理
│ - getOrCreateWriter() │
│ - sync() │
│ - finish() │
│ - prepareCommit() │
└──────────────┬──────────────┘
│
┌──────────────▼──────────────┐
│ RecordWriter(记录写) │ ← 真正执行
│ - write(record) │
│ - prepareCommit() │
│ - close() │
└─────────────────────────────┘
1.2 TableWrite vs FileStoreWrite
| 层级 | 负责 | 实现 |
|---|---|---|
| TableWrite | 行类型转换、权限检查 | TableWriteImpl |
| FileStoreWrite | 分区/桶路由、并发管理 | AbstractFileStoreWrite、KeyValueFileStoreWrite、BaseAppendFileStoreWrite |
| RecordWriter | 文件生成、LSM/Raw写入 | MergeTreeWriter、RawFileWriter |
第二部分:写入的三个关键概念
2.1 WriteBuffer(写缓冲)
什么是WriteBuffer?
WriteBuffer是内存中的一个有序缓冲区,用于暂存写入的记录:
scss
WriteBuffer(512MB)
┌────────────────────────────────┐
│ │
│ Record1 → Record2 → Record3 → │
│ │
│ Size: 120MB / 512MB │
│ Status: WRITING │
│ │
└────────────────────────────────┘
何时Flush?
WriteBuffer满足以下任何一个条件就会Flush到磁盘:
- 内存满 - 达到目标大小(512MB)
- 提交时 - 应用调用 prepareCommit()
- 关闭时 - 应用调用 close()
Flush的结果是什么?
markdown
WriteBuffer Flush
↓
Sorted(按主键排序,仅KeyValue表)
↓
Compressed(压缩,默认snappy)
↓
DataFile(Parquet文件)
↓
添加到LSM Tree或Raw文件集合
2.2 RecordWriter(记录写)
RecordWriter的职责:
- 缓冲管理 - 维护WriteBuffer
- 文件生成 - 定期将buffer flush成DataFile
- 合并管理 - 对于KeyValue表,触发压缩
- 统计收集 - 记录行数、大小等信息
RecordWriter的生命周期:
arduino
创建 → 写入 → Flush → 写入 → Flush → ... → 关闭
↓ ↓ ↓ ↓ ↓ ↓
init write flush write flush close
↓
prepareCommit()
返回 DataFileMeta
DataFileMeta包含什么?
java
public class DataFileMeta {
String fileName; // 文件名:20240101-partition-bucket-seqno.parquet
long fileSize; // 文件大小:126MB
long rowCount; // 行数:1,234,567
Statistics rowStatstics; // 统计信息:min/max/null_count
long minSequenceNumber; // 序列号范围
long maxSequenceNumber;
Long embeddedIndex; // 嵌入式索引(可选)
}
2.3 Compaction(压缩)
为什么需要压缩?
没有压缩的LSM Tree:
css
Level 0(最新数据):[F1] [F2] [F3] [F4] [F5] ... ← 很多小文件
Level 1:[F6] [F7] [F8]
Level 2:[F9]
Level 3:[F10]
问题:
- 读取时扫描Level 0需要打开50+个文件
- 同一主键可能在多个文件中出现
- 点查性能差(需要merge多个版本)
压缩后:
css
Level 0:[F1]
Level 1:[F2]
Level 2:[F3]
Level 3:[F4]
优点:
- 文件数少,查询快
- 同一主键只在一个文件中
- 合并操作完全
Compaction是异步的吗?
是的!Paimon采用后台压缩:
css
应用写入 → WriteBuffer Flush → 生成DataFile → 返回
↓
[后台线程] Compaction
↓
生成新的大文件
应用不需要等待压缩完成,可以继续写入。
第三部分:AppendOnlyFileStore的写入流程
3.1 BUCKET_UNAWARE模式的写入
scss
写入数据
↓
[没有分桶]所有数据→partition目录/bucket-0
↓
WriteBuffer(单个)
↓
RecordWriter
├── buffer满 → Flush
│ ↓
│ 排序(可选,只在压缩时)
│ ↓
│ 压缩(snappy/gzip)
│ ↓
│ 生成ParquetFile
└── 继续写入...
↓
prepareCommit()
├── 等待所有Flush完成
├── 收集所有DataFileMeta
└── 返回CommitMessage
BUCKET_UNAWARE的写入特点:
java
// 代码片段:AppendFileStoreWrite
public void write(InternalRow record) {
// 1. 提取分区值
BinaryRow partition = partitionComputer.compute(record);
// 2. 获取分区的RecordWriter(单个bucket-0)
RecordWriter<InternalRow> writer =
getOrCreateWriter(partition, 0); // 始终是bucket 0
// 3. 写入
writer.write(record);
// 4. WriteBuffer满?自动Flush
if (writer.getWriteBuffer().size() > 256MB) {
writer.flush(); // 生成DataFile
}
}
3.2 HASH_FIXED模式的写入
scss
写入数据
↓
计算bucket = Hash(bucketKey) % numBuckets
↓
路由到对应bucket的RecordWriter
↓
多个WriteBuffer(每个bucket一个)
↓
各自独立Flush
↓
生成多个DataFile(分散在不同bucket目录)
↓
prepareCommit()
└── 收集所有bucket的DataFileMeta
HASH_FIXED的写入特点:
java
// 代码片段:BucketedAppendFileStoreWrite
public void write(InternalRow record) {
// 1. 计算桶号
BinaryRow partition = partitionComputer.compute(record);
int bucket = bucketFunction.bucket(
bucketKey(record), numBuckets); // 0-7之间
// 2. 获取该bucket的RecordWriter
RecordWriter<InternalRow> writer =
getOrCreateWriter(partition, bucket);
// 3. 写入
writer.write(record);
// 4. 如果这个bucket的buffer满,flush这个bucket的数据
// (不影响其他bucket)
}
多个Bucket带来的优势:
yaml
Bucket 0: Record1, Record3, Record5, ... → File_0.parquet (100MB)
Bucket 1: Record2, Record4, Record6, ... → File_1.parquet (100MB)
Bucket 2: Record7, Record9, ... → File_2.parquet (95MB)
...
Bucket 7: Record8, Record10, ... → File_7.parquet (105MB)
总计:8个bucket,8个文件
同时写入8个bucket → 写入吞吐翻8倍!
3.3 Append表的压缩
AppendOnly表的压缩目标:
- 合并小文件 - 几个128MB的文件合并成一个512MB的大文件
- 排序 - 如果有bucket-key,按key排序
- 删除向量处理 - 如果启用DV,重新计算
scss
压缩前:
bucket-0/
├── File_1.parquet (120MB) [seq 1-100]
├── File_2.parquet (125MB) [seq 101-200]
├── File_3.parquet (100MB) [seq 201-250]
└── File_4.parquet (90MB) [seq 251-280]
压缩(合并File_1-4):
bucket-0/
├── File_1.parquet (120MB) [seq 1-100] ← 保留
├── File_2.parquet (125MB) [seq 101-200] ← 保留
└── File_compact.parquet (390MB) [seq 201-280] ← 新生成
prepareCommit()返回:
- ADD: File_compact.parquet
- DELETE: File_3.parquet, File_4.parquet
第四部分:KeyValueFileStore的写入流程
4.1 概览
KeyValue表的写入比Append表复杂,因为要维护主键约束:
scss
写入数据
↓
数据转换为 KeyValue(key + value + flag)
↓
计算bucket(根据BucketMode)
↓
获取RecordWriter(MergeTreeWriter)
↓
WriteBuffer + LSM Tree
├── Level 0(最新)
├── Level 1
├── Level 2
└── Level 3(最旧)
↓
后台Compaction线程
├── 监测Level 0文件数
├── 触发压缩条件
└── 执行Compaction
↓
prepareCommit()
├── 等待压缩完成
├── 收集所有DataFileMeta(包括压缩产生的)
└── 返回CommitMessage
4.2 KeyValue的写入实现
java
public class KeyValueFileStoreWrite extends MemoryFileStoreWrite<KeyValue> {
// 管理所有bucket的writer
protected final Map<BinaryRow, Map<Integer, WriterContainer<KeyValue>>> writers;
public void write(KeyValue record) throws Exception {
// 1. 提取分区
BinaryRow partition = partitionComputer.compute(record);
// 2. 计算bucket(根据BucketMode)
int bucket = bucketFunction.bucket(record.getKey(), numBuckets);
// 3. 获取该分区/bucket的writer
WriterContainer<KeyValue> container =
writers.computeIfAbsent(partition, p -> new HashMap<>())
.computeIfAbsent(bucket, b -> {
// 创建新的MergeTreeWriter
return createWriter(partition, bucket);
});
// 4. 写入记录
MergeTreeWriter writer = container.writer;
writer.write(record);
// 5. WriteBuffer满?自动Flush(但不涉及Compaction)
if (writer.writeBufferSize() > options.writeBufferSize()) {
writer.flush(); // 只flush WriteBuffer,不压缩
}
}
}
4.3 MergeTreeWriter(LSM核心)
MergeTreeWriter是KeyValue表的真正执行者,管理整个LSM Tree:
java
public class MergeTreeWriter implements RecordWriter<KeyValue> {
private WriteBuffer writeBuffer; // 内存缓冲
private Levels levels; // LSM层级
private CompactManager compactManager; // 压缩管理
@Override
public void write(KeyValue kv) throws Exception {
// 1. 写入WriteBuffer
writeBuffer.add(kv);
// 2. 检查Compaction触发条件
// (后续详细讲解)
}
@Override
public CommitIncrement prepareCommit(boolean ignorePreviousFiles)
throws Exception {
// 1. Flush WriteBuffer到Level 0
flushWriteBuffer();
// 2. 等待后台Compaction完成?
if (waitCompaction) {
compactManager.waitCompactionDone();
}
// 3. 返回提交增量
return new CommitIncrement(
getAllDataFiles(),
getDeletedFiles(),
...
);
}
}
4.4 LSM Tree的层级结构
什么是LSM Tree?
LSM(Log-Structured Merge)Tree是一种分层存储结构,优化了写入性能:
ini
Level 0(热层):频繁写入
├── [File_1][File_2][File_3] ← 可能有重复的key
├── Size: 20MB + 20MB + 20MB = 60MB
└── 文件数:3个
Level 1(温层):逐渐合并
├── [File_4][File_5]
├── Size: 200MB + 200MB = 400MB
└── 文件数:2个
Level 2(冷层)
├── [File_6]
├── Size: 1.6GB
└── 文件数:1个
Level 3(冻层)
├── [File_7]
├── Size: 6.4GB
└── 文件数:1个
关键特性:
- 写优化 - 新数据先写到Level 0(内存),无需立即排序
- 读时合并 - 查询时从Level 0-3依次读取并合并结果
- 定期压缩 - 后台线程逐层合并文件
压缩的层级提升:
markdown
Level 0 触发Compaction
↓
合并Level 0的所有文件到Level 1
↓
Level 1变大,超过阈值
↓
合并Level 1的所有文件到Level 2
↓
逐级向下传递...
4.5 Compaction触发条件
何时触发Compaction?
Paimon采用UniversalCompaction策略(类似RocksDB):
ini
Level 0: [F1] [F2] [F3] [F4] [F5] ← 文件数 > 4个
↓
触发Level 0→Level 1的Compaction
↓
Level 1: [F6] [F7] ← 大小超过阈值
↓
触发Level 1→Level 2的Compaction
具体触发条件:
yaml
# 文件数触发
num-sorted-run-compaction-trigger: 4 # Level N文件数 > 4
# 大小触发
sorted-run-size-ratio: 2 # 相邻层级大小比例
# 例如:
# Level 0: 4个文件 × 100MB = 400MB
# Level 1应该 >= 400MB × 2 = 800MB
# 如果Level 1只有700MB,触发Compaction
压缩过程中能否继续写入?
是的!这是LSM Tree的核心优势:
scss
Thread 1(应用):
└── write(record) → WriteBuffer(Level 0)
Thread 2(Compaction):
└── merge(Level 0 → Level 1)(Level 1)
不会阻塞!因为写的是Level 0,压缩处理的是其他层
第五部分:写入的三个阶段详解
5.1 阶段1:内存写入
scss
write(record)
↓
WriteBuffer.add(record)
↓
record进入内存缓冲
WriteBuffer的实现:
java
public class WriteBuffer {
// 核心是一个 InternalRow[]
private InternalRow[] records;
private int size;
public void add(InternalRow record) {
if (size >= records.length) {
// 需要Flush
triggerFlush();
}
records[size++] = record;
}
public void flush() {
// 将records序列化到磁盘
// 生成DataFile
}
}
性能特点:
- ✅ 极快 - O(1)时间复杂度
- ✅ 内存消耗 - 512MB WriteBuffer存储几百万条记录
- ⚠️ 数据丢失风险 - 如果进程崩溃,内存数据丢失
5.2 阶段2:Flush到磁盘
lua
WriteBuffer.flush()
↓
1. 排序(仅KeyValue表)
├─ 按主键排序
└─ 便于查询和压缩
↓
2. 压缩(所有表)
├─ snappy(快速)或gzip(高压缩率)
└─ 默认snappy
↓
3. 编码(所有表)
├─ Parquet格式
└─ 添加统计信息(min/max/null_count)
↓
4. 写入文件系统
└─ 生成DataFile
Flush的时间复杂度:
scss
排序:O(n log n) - 只排序WriteBuffer中的记录
压缩:O(n) - 线性扫描
编码:O(n) - 线性编码
文件IO:O(n/8) - 512MB数据 → ~64MB磁盘IO(8倍压缩率)
总耗时:500ms-2s(取决于WriteBuffer大小)
5.3 阶段3:Compaction(可选)
对于KeyValue表,Flush后可能触发Compaction:
ini
flush() → 生成DataFile_0
Level 0文件数检查:
Level 0: [File_1] [File_2] [File_3] [File_0] ← 4个文件
↓
文件数 > 4?
↓
触发 Level 0→Level 1 Compaction
↓
Merge & Sort:
├─ 读取Level 0的4个文件(已排序)
├─ 读取Level 1的相关文件(已排序)
├─ 合并(多路归并)
├─ 去重(同主键只保留最新)
└─ 生成新的Level 1文件
↓
更新Levels结构:
├─ Level 0: 清空
└─ Level 1: 添加新文件
Compaction的开销:
diff
假设:
- 4个Level 0文件,每个100MB(共400MB)
- Level 1的相关文件,500MB
- 压缩率:8:1
处理数据量:900MB
输出数据量:112MB
CPU时间:2-5秒
磁盘IO:900MB读 + 112MB写
应用感知:
- 写延迟:无影响(异步进行)
- 读延迟:Compaction期间可能慢0-10%
Compaction何时会阻塞应用?
两种情况:
- prepareCommit()时 - 等待压缩完成
java
List<CommitMessage> msgs = write.prepareCommit(
waitCompaction = true, // ← 等待Compaction
commitId
);
// 如果有未完成的Compaction,这里会阻塞
- Level 0堆积 - 如果压缩跟不上写入
markdown
写入速度 > 压缩速度
↓
Level 0文件堆积到10个
↓
应用阻塞(必须等待Compaction清理Level 0)
↓
现象:写入延迟突增
第六部分:实战案例
6.1 案例1:电商订单写入(KeyValue + HASH_FIXED)
场景:
- 日均新增订单:50M条
- 订单更新率:10%(修改订单状态等)
- 并发writer:8个
写入流程:
bash
50M条订单/天
↓
8个writer并行处理,每个处理6.25M条
↓
每个writer:
├── 分配给16个bucket
│ ├── Bucket 0: 390K条/writer → 256MB WriteBuffer
│ ├── Bucket 1: 390K条/writer → 256MB WriteBuffer
│ └── ...
│ └── Bucket 15: 390K条/writer → 256MB WriteBuffer
│
├── 每个bucket的WriteBuffer
│ └── 满了就Flush → 生成~512MB DataFile
│
└── 后台Compaction
├── 监测Level 0(新写入的文件)
├── 触发条件:Level 0 > 4个文件
└── 合并到Level 1
性能指标:
| 指标 | 实际值 |
|---|---|
| 写入吞吐 | 400MB/s(8writer × 50MB/s) |
| 数据落盘 | ~6小时(50M条 × 1KB/条 ÷ 2.5MB/s) |
| WriteBuffer数量 | 8writer × 16bucket = 128个 |
| 每日DataFile数 | ~100个(12000MB ÷ 120MB/file) |
| 压缩频率 | 每10分钟触发一次 |
调优要点:
yaml
# 配置
write-buffer-size: 256MB # 中等大小
write-buffer-spill-disk-size: 2GB # spillable磁盘
target-file-size: 512MB # 目标file大小
num-sorted-run-compaction-trigger: 4 # 4个文件触发
sorted-run-size-ratio: 2 # 层级比例2:1
num-levels: 4 # 4层LSM
# 预期效果
- Level 0: 128MB × 4 = 512MB
- Level 1: 512MB × 2 = 1GB
- Level 2: 1GB × 2 = 2GB
- Level 3: 2GB × 2 = 4GB
总容量: 7.5GB(一天的数据都能放在LSM中)
6.2 案例2:日志写入(Append + BUCKET_UNAWARE)
场景:
- 日均日志:500M条
- 数据量:2TB/天
- 并发writer:4个
写入流程:
yaml
500M条日志/天 = 2TB
↓
4个writer,每个处理125M条
↓
每个writer:
├── 无分桶(BUCKET_UNAWARE)
│ └── 所有数据→partition/bucket-0
├── WriteBuffer: 1GB(大buffer处理日志流)
│ └── 1小时flush一次
├── 生成DataFile
│ ├── File_1: 2GB
│ ├── File_2: 2GB
│ └── ...
└── 后台压缩(合并小文件)
性能指标:
| 指标 | 实际值 |
|---|---|
| 写入吞吐 | 800MB/s |
| WriteBuffer数量 | 4(每个writer一个) |
| 每日DataFile数 | ~30个(2TB ÷ 64MB) |
| 压缩频率 | 每2小时触发一次 |
调优要点:
yaml
# 配置
write-buffer-size: 1GB # 大buffer适合日志
write-buffer-spillable: true # spillable避免OOM
target-file-size: 2GB # 大文件减少数量
compression: snappy # 快速压缩
compaction-min-file-num: 10 # 宽松压缩条件
# 原因
- 日志吞吐大,写入优先于读优化
- BUCKET_UNAWARE无需路由,直接写
- 大buffer减少flush频率
- 宽松压缩条件减轻后台压力
6.3 案例3:维度表写入(KeyValue + KEY_DYNAMIC)
场景:
- 用户维表:100M条
- 分区:dt(按日期)
- 更新:某些用户跨日期更新(如修改用户等级)
- 并发:1个写入job
写入流程:
ini
100M条用户数据
↓
跨日期分布:
├── dt=2024-01-01: 50M条
├── dt=2024-01-02: 50M条(包括来自01-01的更新)
└── ...
↓
KEY_DYNAMIC模式:
├── 初始化时读取所有已存在的主键
│ └── 建立(user_id) → (partition, bucket)的映射
├── 写入:
│ ├── UPDATE的user_id 在 01-02
│ ├── 查询映射:user_id在01-01的bucket
│ └── 在原bucket中更新(跨分区)
└── 后台Compaction
└── 合并多个版本
初始化开销:
100M条用户,每条映射~100字节
索引大小:10GB
初始化耗时:5-10分钟(全表扫描)
性能指标:
| 指标 | 实际值 |
|---|---|
| 写入吞吐 | 150MB/s(单writer) |
| 索引大小 | 10GB |
| 启动延迟 | 5-10分钟 |
| 点查延迟 | <100ms(包括lookup) |
何时选择KEY_DYNAMIC?
diff
DO ✅
- 维度表(数据不频繁变化)
- 小表(< 1GB)
- 有跨分区更新需求
DON'T ❌
- 大表(> 10GB)
- 高频率更新(索引维护开销大)
- 多并发writer(索引非线程安全)
第七部分:写入的高级特性
7.1 事务性写入
Paimon保证原子性的提交:
scss
write(records) → 内存缓冲,可回滚
↓
↓
↓(应用决定:提交或回滚)
↓
prepareCommit() → 生成临时文件
↓
↓
commit() → 原子重命名 → 提交成功!
实现原理:
java
// 如果commit失败,应用可以回滚
List<CommitMessage> messages = write.prepareCommit(...);
try {
commit.commit(messages, ...); // 原子操作
} catch (Exception e) {
// 提交失败,可以重试
write.restore(checkpointedState); // 恢复状态
write.close();
}
7.2 恢复与容错
什么是Checkpoint?
Paimon支持保存写入的中间状态(Checkpoint),用于故障恢复:
erlang
写入records...
↓
Checkpoint 1(保存状态)
├── partition_1/bucket_0: DataFile_1.parquet
├── partition_2/bucket_1: DataFile_2.parquet
└── ...
↓
继续写入...
↓
突然故障!
↓
恢复时:
├── 恢复从Checkpoint 1的状态
├── 继续完成未提交的写入
└── 无数据重复或丢失
Checkpoint的API:
java
FileStoreWrite<KeyValue> write = fileStore.newWrite("user1");
// 定期保存状态
List<FileStoreWrite.State<KeyValue>> state = write.checkpoint();
// 保存state到持久化存储(HDFS等)
// 故障后恢复
write.restore(savedState);
7.3 内存管理
WriteBuffer如何避免OOM?
ini
场景:16个并发writer,每个256MB WriteBuffer
总内存:16 × 256MB = 4GB
如果机器只有8GB内存?
↓
Paimon会让部分WriteBuffer spillable到磁盘
↓
┌─────────────────────┐
│ 内存: 256MB │
│ 磁盘: 500MB spillable │
└─────────────────────┘
配置spillable:
yaml
write-buffer-size: 256MB # 内存部分
write-buffer-spillable: true # 启用spillable
write-buffer-spill-disk-size: 4GB # 磁盘部分上限
第八部分:性能调优参考
8.1 写入性能瓶颈诊断
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 写入吞吐<100MB/s | WriteBuffer太小 | 增加到512MB+ |
| 写入吞吐<100MB/s | 压缩阻塞 | 优化compaction参数 |
| 内存持续增长 | spillable未启用 | write-buffer-spillable: true |
| 提交延迟>30s | 等待Compaction | prepareCommit(false) 不等待 |
8.2 WriteBuffer大小选择
bash
小WriteBuffer(64MB):
├── 优点:内存占用少,适合低端机器
├── 缺点:频繁flush,IO压力大
└── 吞吐:200MB/s
中等WriteBuffer(256MB):
├── 优点:平衡性能和内存
├── 缺点:无
└── 吞吐:400MB/s(推荐)
大WriteBuffer(1GB):
├── 优点:吞吐最高,适合日志系统
├── 缺点:内存占用多
└── 吞吐:800MB/s
8.3 Compaction参数调优
yaml
# 快速写入,后期再优化(延迟压缩)
num-sorted-run-compaction-trigger: 8 # 宽松条件
sorted-run-size-ratio: 4 # 大比例
max-size-amplification-percent: 200 # 允许大幅度膨胀
# 实时性优先(频繁压缩)
num-sorted-run-compaction-trigger: 2 # 严格条件
sorted-run-size-ratio: 2 # 小比例
max-size-amplification-percent: 50 # 严格限制
# 平衡方案(推荐)
num-sorted-run-compaction-trigger: 4 # 中等条件
sorted-run-size-ratio: 2 # 标准比例
max-size-amplification-percent: 100 # 中等限制
8.4 并发writer调优
ini
假设:单个writer吞吐 50MB/s
目标吞吐:400MB/s
所需writer:8个
但注意:
8 × 256MB WriteBuffer = 2GB内存
8 × 16 bucket = 128个并发flush操作
需要充足的磁盘IO能力
第九部分:常见问题
Q1: prepareCommit(true) vs prepareCommit(false)
java
// 方案1:等待Compaction
List<CommitMessage> msgs = write.prepareCommit(
waitCompaction = true,
commitId
);
// 特点:
// - 提交延迟可能5-30秒
// - 最终数据最优化
// - 适合批处理(每小时提交一次)
// 方案2:不等待Compaction
List<CommitMessage> msgs = write.prepareCommit(
waitCompaction = false,
commitId
);
// 特点:
// - 提交延迟<100ms
// - 后台继续Compaction
// - 适合流式处理(实时提交)
Q2: finish() vs close()的区别
java
// finish():刷出所有数据,但不关闭
write.finish();
// 结果:所有DataFile已生成,可以提交
// 应用仍可继续写入(会从新buffer开始)
// close():彻底关闭
write.close();
// 结果:writer销毁,不能再写入
Q3: 如何处理故障时的未提交数据?
java
// 方案1:手动abort
try {
List<CommitMessage> msgs = write.prepareCommit(false, id);
commit(msgs);
} catch (Exception e) {
// 提交失败,放弃这批数据
write.abort(msgs); // 删除临时文件
}
// 方案2:自动恢复
write.restore(previousCheckpoint);
// 恢复后继续写入,无数据丢失
总结
写入链路总结
scss
应用程序
↓ write(record)
TableWrite(转换)
↓ write(KeyValue)
FileStoreWrite(路由)
↓ getOrCreateWriter(partition, bucket)
RecordWriter(执行)
├─ 内存: WriteBuffer
├─ 磁盘: DataFile
└─ 后台: Compaction
↓
prepareCommit()
↓ 返回CommitMessage
FileStoreCommit
↓ commit()
生成Snapshot(一致性保证)
关键数据结构
- WriteBuffer - 内存缓冲,大小可配置(64MB-1GB)
- DataFileMeta - 文件元数据,含统计信息
- CommitIncrement - 提交增量,含新增/删除文件
- Levels - LSM树的层级结构
性能特点
| 表类型 | 吞吐 | 压缩开销 | 适用场景 |
|---|---|---|---|
| AppendOnly | 800MB/s | 低 | 日志、事件 |
| KeyValue | 300MB/s | 中等 | 数据库、维表 |
下一章:第6章将讲解提交流程与事务保证,包括两阶段提交、冲突检测、Manifest合并等,敬请期待!