流式数据湖Paimon探秘之旅 (五) 写入流程全解析

第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到磁盘:

  1. 内存满 - 达到目标大小(512MB)
  2. 提交时 - 应用调用 prepareCommit()
  3. 关闭时 - 应用调用 close()

Flush的结果是什么?

markdown 复制代码
WriteBuffer Flush
    ↓
Sorted(按主键排序,仅KeyValue表)
    ↓
Compressed(压缩,默认snappy)
    ↓
DataFile(Parquet文件)
    ↓
添加到LSM Tree或Raw文件集合

2.2 RecordWriter(记录写)

RecordWriter的职责

  1. 缓冲管理 - 维护WriteBuffer
  2. 文件生成 - 定期将buffer flush成DataFile
  3. 合并管理 - 对于KeyValue表,触发压缩
  4. 统计收集 - 记录行数、大小等信息

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表的压缩目标

  1. 合并小文件 - 几个128MB的文件合并成一个512MB的大文件
  2. 排序 - 如果有bucket-key,按key排序
  3. 删除向量处理 - 如果启用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个

关键特性

  1. 写优化 - 新数据先写到Level 0(内存),无需立即排序
  2. 读时合并 - 查询时从Level 0-3依次读取并合并结果
  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何时会阻塞应用?

两种情况:

  1. prepareCommit()时 - 等待压缩完成
java 复制代码
List<CommitMessage> msgs = write.prepareCommit(
    waitCompaction = true,  // ← 等待Compaction
    commitId
);
// 如果有未完成的Compaction,这里会阻塞
  1. 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合并等,敬请期待!

相关推荐
TDengine (老段)36 分钟前
TDengine 时区函数 TIMEZONE 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
语落心生38 分钟前
流式数据湖Paimon探秘之旅 (九) Compaction压缩机制
大数据
语落心生39 分钟前
流式数据湖Paimon探秘之旅 (十) Merge Engine合并引擎
大数据
en-route39 分钟前
深入理解数据仓库设计:事实表与事实宽表的区别与应用
大数据·数据仓库·spark
语落心生40 分钟前
流式数据湖Paimon探秘之旅 (八) LSM Tree核心原理
大数据
Light6042 分钟前
智慧办公新纪元:领码SPARK融合平台如何重塑企业OA核心价值
大数据·spark·oa系统·apaas·智能办公·领码spark·流程再造
智能化咨询1 小时前
(66页PPT)高校智慧校园解决方案(附下载方式)
大数据·数据库·人工智能
忆湫淮1 小时前
ENVI 5.6 利用现场标准校准板计算地表反射率具体步骤
大数据·人工智能·算法
lpfasd1231 小时前
现有版权在未来的价值:AI 泛滥时代的人类内容黄金
大数据·人工智能