功能点 8:Tiering 分层存储与 Lakehouse

功能点 8:Tiering 分层存储与 Lakehouse ------ 源码阅读笔记

对应源码阅读计划功能点 8:TieringService、ArrowParquetCompactor、LakeUnionRead、Iceberg/Paimon/Lance 集成。


笔记 8.1:TieringService ------ 分层存储核心

文件:TieringService.javaLakeTableTieringManager.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 的完整实现。

相关推荐
头茬韭菜2 天前
功能点 6:Arrow 列式格式
fluss
头茬韭菜2 天前
功能点 4:LogStore 日志存储引擎
fluss
头茬韭菜5 天前
功能点 3:Bucket、Partition 与数据分布
fluss
头茬韭菜7 天前
第 9 篇:「Fluss 运维实战」—— 部署、监控与故障排查
运维·fluss
头茬韭菜7 天前
功能点 1:项目骨架与启动流程 —— 源码阅读笔记
运维·笔记·debian·fluss
头茬韭菜8 天前
第 10 篇:「Fluss 实战案例集」—— 完整解决方案与最佳实践
c#·linq·fluss
头茬韭菜10 天前
第 5 篇:「Fluss 列式流处理」—— Arrow 格式与查询优化
fluss
头茬韭菜10 天前
第 4 篇:「Fluss + Flink 集成实战」—— Catalog、Source 与 Sink
大数据·python·flink·fluss
头茬韭菜14 天前
第 2 篇:「Fluss 架构深入」—— CoordinatorServer、TabletServer 与存储引擎
架构·fluss