功能点 8:Tiering 分层存储与 Lakehouse ------ 源码阅读笔记
对应源码阅读计划功能点 8:TieringService、ArrowParquetCompactor、LakeUnionRead、Iceberg/Paimon/Lance 集成。
笔记 8.1:TieringService ------ 分层存储核心
文件:TieringService.java、LakeTableTieringManager.java
路径 :fluss-server/src/main/java/org/apache/fluss/server/coordinator/LakeTableTieringManager.java
Tiering 触发与调度
java
public class LakeTableTieringManager {
/**
* 定期扫描所有启用了 Tiering 的表,决定是否执行 Compaction
*/
public void scheduleTiering() {
for (TablePath tablePath : getTieringEnabledTables()) {
TableDescriptor descriptor = metadataManager.getTable(tablePath);
// 检查该表是否需要 Tiering
if (needsTiering(tablePath, descriptor)) {
// 创建 Tiering 任务
TieringTask task = new TieringTask(
tablePath,
getLastTieredOffset(tablePath),
descriptor.getBucketCount()
);
// 提交到执行器
tieringExecutor.submit(task);
}
}
}
/**
* Tiering 触发条件:
* 1. 距上次 Tiering 已过 commit-interval(默认 5 分钟)
* 2. 有新数据(LEO > lastTieredOffset)
* 3. 本地日志超过 local-retention(默认 3 天)
*/
private boolean needsTiering(TablePath path, TableDescriptor desc) {
long lastTieredOffset = getLastTieredOffset(path);
long currentLEO = getLogEndOffset(path);
long tieringInterval = desc.getTieringCommitInterval();
long lastTieringTime = getLastTieringTime(path);
boolean hasNewData = currentLEO > lastTieredOffset;
boolean intervalElapsed = System.currentTimeMillis() - lastTieringTime > tieringInterval;
return hasNewData && intervalElapsed;
}
}
Tiering 任务执行
java
public class TieringTask implements Runnable {
@Override
public void run() {
// 1. 从 LogTablet 读取 [lastTieredOffset, LEO) 范围的数据
LogScanner scanner = logTablet.newScanner(lastTieredOffset);
ArrowRecordBatch hotBatch = scanner.scan();
// 2. ★ Arrow → Parquet 转换
ParquetFileWriter parquetWriter = arrowParquetCompactor.compact(hotBatch);
// 3. 写入 Lakehouse Storage(通过 LakeFormat)
LakeFormat lakeFormat = getLakeFormat(tablePath); // Iceberg/Paimon/Lance
lakeFormat.committer().commit(parquetWriter.getFile());
// 4. 更新元数据:记录已 Tiered 的 offset
updateLastTieredOffset(tablePath, currentLEO);
// 5. 清理本地已 Tiered 的 Segment
logTablet.deleteSegmentsBefore(currentLEO);
}
}
笔记 8.2:ArrowParquetCompactor ------ Arrow → Parquet
核心转换逻辑
java
public class ArrowParquetCompactor {
/**
* 将 Arrow RecordBatch 转换为 Parquet 文件
*
* Arrow → Parquet 的类型映射:
* Arrow Type → Parquet Type
* ─────────────────────────────────
* IntVector → INT32
* BigIntVector → INT64
* VarCharVector → BYTE_ARRAY (UTF8)
* DecimalVector → FIXED_LEN_BYTE_ARRAY (DECIMAL)
* TimeStampVector → INT64 (TIMESTAMP_MICROS)
*/
public File compact(ArrowRecordBatch batch) {
// 1. 创建 Parquet Writer
MessageType parquetSchema = convertArrowSchema(batch.getSchema());
ParquetWriter writer = ParquetWriter.builder(outputFile)
.withSchema(parquetSchema)
.withCompression(CompressionCodecName.SNAPPY) // Parquet 支持更高压缩比
.withRowGroupSize(128 * 1024 * 1024) // 128MB Row Group
.build();
// 2. 逐行写入(Arrow 列式 → Parquet 列式,列式到列式转换高效)
for (int row = 0; row < batch.getRowCount(); row++) {
writer.write(extractRow(batch, row));
}
writer.close();
return outputFile;
}
}
笔记 8.3:LakeUnionRead ------ 流批联合查询
文件:LakeUnionRead.java
路径 :fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/lake/LakeUnionRead.java
Union Read 实现
java
public class LakeUnionRead {
/**
* ★ 核心:创建一个 Union Read 的查询计划
* 将 Hot Tier (Fluss) 和 Cold Tier (Lakehouse) 的数据合并
*/
public DynamicTableSource createUnionReadSource(
TablePath tablePath,
ResolvedSchema schema,
long hotDataStartTime, // 热数据起始时间(如 now - 3d)
long coldDataEndTime // 冷数据截止时间(如 now - 3d)
) {
// 1. 创建 Hot Tier Source(从 Fluss 读取实时数据)
DynamicTableSource hotSource = new FlussTableSource(
tablePath,
schema,
// ★ 只读 hotDataStartTime 之后的数据
new ScanRange(hotDataStartTime, Long.MAX_VALUE)
);
// 2. 创建 Cold Tier Source(从 Iceberg/Paimon 读取历史数据)
Table coldTable = loadLakeTable(tablePath); // 通过 Lake Catalog 加载
DynamicTableSource coldSource = new IcebergTableSource(
coldTable,
schema,
// ★ 只读 coldDataEndTime 之前的数据
new ScanRange(0, coldDataEndTime)
);
// 3. ★ 返回 Union Source
// Flink 优化器会自动在 Hot 和 Cold 之间加 Union All 算子
return new UnionTableSource(hotSource, coldSource);
}
}
查询计划可视化
SQL: SELECT COUNT(*) FROM orders WHERE order_date > '2026-01-01'
Flink 优化器生成的执行计划:
┌─────────────────────┐
│ HashAggregate │
│ COUNT(*) │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Union All │ ← ★ LakeUnionRead 自动插入
└────┬──────────┬──────┘
│ │
┌────────▼────┐ ┌──▼──────────────┐
│ Hot Scan │ │ Cold Scan │
│ (Fluss) │ │ (Iceberg/Paimon) │
│ Arrow fmt │ │ Parquet fmt │
│ ~3 天数据 │ │ ~90 天数据 │
└─────────────┘ └──────────────────┘
三种 Lake Format 的实现差异
java
// Iceberg: 使用 Iceberg 原生 Table API
public class IcebergCommitter implements LakeCommitter {
public void commit(File parquetFile, TableMetadata meta) {
Table icebergTable = loadIcebergTable(meta.getTablePath());
icebergTable.newAppend()
.appendFile(DataFiles.fromPath(parquetFile.toPath()))
.commit(); // Iceberg 的 Snapshot-based Commit
}
}
// Paimon: 使用 Paimon 的 LSM 写入
public class PaimonCommitter implements LakeCommitter {
public void commit(File parquetFile, TableMetadata meta) {
PaimonTable paimonTable = loadPaimonTable(meta.getTablePath());
paimonTable.newCompact()
.addFile(parquetFile)
.commit(); // Paimon 的 Compaction-based Commit
}
}
// Lance: 面向 AI/向量的列式格式
public class LanceCommitter implements LakeCommitter {
public void commit(File parquetFile, TableMetadata meta) {
// Lance 原生支持向量和变体数据类型
// 可以直接写入 embedding 向量
LanceDataset dataset = LanceDataset.write(meta.getTablePath());
dataset.write(parquetFile);
dataset.commit();
}
}
阅读小结
| 已理解 | 尚未深入 |
|---|---|
| ✅ Tiering 的触发条件和调度流程 | ⬜ RemoteStorageCleaner 孤儿文件清理 |
| ✅ Arrow → Parquet 的类型映射和压缩 | ⬜ Parquet 的 Row Group 和 Page 结构 |
| ✅ Union Read 如何生成 Flink 查询计划 | ⬜ Spark/Trino/StarRocks 的 Union Read 适配 |
| ✅ 三种 Lake Format Committer 的差异 | ⬜ Lake Catalog 与 Fluss 元数据的同步机制 |
下一步:功能点 9------FlussCatalog、FlussSource、FlussSink、FlussLookupFunction 的完整实现。