图解 Fluss(五):Flink 接入与数据生命周期 —— Connector、表生命周期与冷热分层

阅读本文你将了解: Flink 是怎么"发现"Fluss 表的、Split 与 Bucket 的映射关系、一条 CREATE TABLE 在集群内部要走几步、Schema Evolution 为什么只支持加列、以及热数据如何自动归档成 Iceberg/Parquet 并被同一条 SQL 查到。

配套图表:class-03-flink-connectorstate-03-table-lifecycleseq-06-tiering

难度:⭐⭐⭐⭐ | 适合人群:要做端到端方案设计与湖仓落地的架构师


一、场景:从"能跑"到"能长期跑"

前面四篇解决了性能与稳定性问题。但一个系统要真正上线,还要回答三个"工程琐碎但致命"的问题:

问题一:接入成本

数据团队有 40 个 Flink 作业,每个都要读写 Fluss。如果每个作业都要写一堆 CREATE TABLE ... WITH ('connector'='fluss', 'bootstrap.servers'='...'),那配置改一次要改 40 个地方。

问题二:Schema 演进

业务上线三个月后要加一个字段 promotion_id。加完之后,历史数据里的这一列是什么?查询会不会报错?要不要停服?

问题三:数据留存

业务要求"最近 3 天的数据要秒级可查,3 天前到 2 年的数据要能查但不要求实时"。如果全存 Fluss,成本扛不住;如果全存 Iceberg,实时性达不到。

这三个问题,对应本篇的三张图。


问题一的答案就是 Catalog。

sql 复制代码
-- 注册一次,全局生效
CREATE CATALOG fluss_catalog WITH (
    'type' = 'fluss',
    'bootstrap.servers' = 'coordinator-server-1:9123,coordinator-server-2:9123'
);

USE CATALOG fluss_catalog;

-- 之后所有作业都不需要再写 WITH 子句
SELECT * FROM orders;        -- 直接用,表结构从 Fluss 拉取

图中 FlussCatalog 的核心方法:

java 复制代码
class FlussCatalog implements Catalog {
    private final FlussConnection connection;

    public CatalogBaseTable getTable(ObjectPath tablePath) {
        // 1. 通过 connection 从 Coordinator 拉取 TableInfo
        // 2. 把 Fluss 的 Schema 转成 Flink 的 TableSchema
        return CatalogTable.of(toFlinkSchema(schema), ...);
    }

    public void createTable(ObjectPath path, CatalogBaseTable table, boolean ignoreIfExists) { /* ... */ }
    public boolean tableExists(ObjectPath tablePath) { /* ... */ }

    private DataType toFlinkDataType(Schema flussSchema) { /* ... */ }
}

Catalog 模式的三个好处:

好处 说明
配置收敛 连接信息只在 Catalog 里配一次
Schema 单一来源 表结构在 Fluss 里定义,Flink 直接读取,不会两边不一致
跨作业共享 40 个作业看到的都是同一份元数据

Java API 方式注册:

java 复制代码
TableEnvironment tEnv = TableEnvironment.create(EnvironmentSettings.inStreamingMode());

tEnv.executeSql(
    "CREATE CATALOG fluss_catalog WITH (\n" +
    "    'type' = 'fluss',\n" +
    "    'bootstrap.servers' = 'localhost:9123'\n" +
    ")"
);
tEnv.executeSql("USE CATALOG fluss_catalog");

2.2 Source:Split 是怎么切的

java 复制代码
// 真实签名:FlussSource 继承 Fluss 封装的 FlinkSource 基类
// 源码路径:fluss-flink/fluss-flink-common/.../flink/source/FlussSource.java
public class FlussSource<OUT> extends FlinkSource<OUT> {
    // OUT 是输出记录类型(RowData / InternalRow 等)

    // 遵循 Flink Source 接口契约,实现两个工厂方法:
    //   createEnumerator() → SplitEnumerator(在 JobManager 侧,负责切分和分配 Split)
    //   createReader()     → SourceReader(在 TaskManager 侧,负责实际拉取数据)
}

说明:createEnumerator()createReader() 是 Flink Source 接口的标准契约方法,FlinkSource 基类对它们做了 Fluss 相关的封装。下面按职责理解即可。

关键在 FlussSourceSplit 这个类的四个字段:

java 复制代码
class FlussSourceSplit implements SourceSplit {
    private final TablePath tablePath;      // 哪张表
    private final long partitionId;         // 哪个分区
    private final int bucketId;             // 哪个 bucket
    private final long startOffset;         // 从哪个 offset 开始读
}

Split = (分区, bucket)。 这个映射关系是理解并行度的关键:

复制代码
一张表:bucket.num = 64,无分区
  → 64 个 Split
  → Flink 并行度最大 64

一张表:bucket.num = 32,10 个分区
  → 32 × 10 = 320 个 Split
  → Flink 并行度最大 320

所以 bucket.num 直接决定了 Flink 作业的最大并行度。 第 2 篇给过建议值,这里补充一个约束:

复制代码
bucket.num >= Flink 作业的目标并行度

如果 bucket.num = 8 而 Flink 并行度设成 32,那会有 24 个 subtask 空转------因为只有 8 个 Split 可分配。

FlussSourceEnumerator 负责 Split 分配(JobManager 侧):

java 复制代码
// 实现 Flink 的 SplitEnumerator 契约:
//   SplitEnumerator<SplitT, EnumChkT>
//     - SplitT   = FlussSourceSplit(待分配的 Split)
//     - EnumChkT = 枚举器状态快照类型(用于 Checkpoint / 故障恢复)
class FlussSourceEnumerator implements SplitEnumerator<FlussSourceSplit, /* EnumChkT */ Object> {
    private final List<FlussSourceSplit> pendingSplits;

    public void start() {
        // 从 Coordinator 拉取所有 Tablet 信息,生成 Split
    }

    private void assignSplitsInBatches() {
        // 批量分配给空闲的 Reader,而不是一次全部分完
        // 这样能让负载均衡更好(快的 subtask 多分一些)
    }
}

FlussSourceReader 持有 Map<split, LogScanner>

java 复制代码
class FlussSourceReader implements SourceReader<RowData, FlussSourceSplit> {
    private final Map<String, LogScanner> scanners;   // splitId → scanner

    public InputStatus pollNext(ReaderOutput<RowData> output) {
        // 轮询各个 scanner,拉取数据,转成 RowData 发射
    }
}

2.3 Sink:两种 Writer 与 Exactly-Once

图中 FlussSinkWriter 有两个子类:

复制代码
FlussSinkWriter <|-- UpsertWriter     (PK 表:按主键 Upsert)
FlussSinkWriter <|-- AppendWriter     (Log 表:纯追加)
java 复制代码
class FlussSinkWriter implements SinkWriter<RowData> {
    private final Map<Integer, LogWriter> bucketWriters;   // bucketId → writer

    public void write(RowData row, Context context) {
        int bucketId = bucketingFunction.hash(row) % bucketNum;
        bucketWriters.get(bucketId).write(row);
    }

    public void flush(boolean endOfInput) { /* ... */ }
}

注意 Map<Integer, LogWriter> ------ 每个 bucket 一个 writer。 这是为了减少连接数:同一个 bucket 的数据攒在同一个 writer 里批量发送,而不是每条记录建一次连接。

Exactly-Once 的实现在图下方的 note 里:

复制代码
Exactly-Once 保证:
prepareCommit() 在 Checkpoint 触发
commit() 在所有并行 writer 完成后
java 复制代码
class FlussSinkCommitter implements SinkCommitter {
    public List<CommitRequest> prepareCommit() {
        // Checkpoint barrier 到达时调用
        // 把当前批次的数据标记为"预提交"
        // 此时数据已写入 Fluss,但对外不可见
    }

    public void commit(List<CommitRequest> commits) {
        // JobManager 确认所有并行 subtask 都预提交成功后调用
        // 真正提交,数据对外可见
    }
}

这是标准的 Flink 两阶段提交协议,和 KafkaSink 的实现思路一致。第 3 篇讲过,要配合服务端的幂等写入(WriterStateManager)才能真正做到端到端 Exactly-Once。

2.4 FlussConnection:统一的客户端入口

图中四个组件都指向 FlussConnection

java 复制代码
class FlussConnection {
    public byte[] pointLookup(TablePath table, RowData key) { /* ... */ }   // 点查
    public LogScanner newLogScanner(TablePath path, int bucket, long offset) { /* ... */ }
    public LogWriter newLogWriter(TablePath path, int bucket, WriteMode mode) { /* ... */ }
}

对应关系:

组件 用到的方法
FlussCatalog 元数据操作
FlussSourceReader newLogScanner()
FlussSinkWriter newLogWriter()
FlussLookupFunction pointLookup()

连接池是复用的 ------同一个 TaskManager 里的 Source、Sink、Lookup 共享同一个 FlussConnection,避免建立过多 TCP 连接。


三、图 2:Table 生命周期状态机

3.1 四个状态

复制代码
[*] --> Creating : CREATE TABLE
Creating --> Active    : 创建完成
Active   --> Altering  : ALTER TABLE (ADD COLUMN)
Altering --> Active    : 变更完成
Active   --> Dropping  : DROP TABLE
Dropping --> [*]       : 删除完成

3.2 Creating 的六步

图右侧的 note 精确列出了:

复制代码
createTable 六步:
1. 参数校验
2. 计算 Tablet 数
3. 分配 Tablet
4. 写 ZK 元数据
5. 通知 TabletServer
6. 返回成功

第 2 步"计算 Tablet 数"的具体算法:

复制代码
Tablet 数 = 分区数 × bucket.num

无分区表:Tablet 数 = 1 × bucket.num = bucket.num
分区表:  Tablet 数 = 已创建分区数 × bucket.num

第 3 步"分配 Tablet"用到 `TabletBalancer"(第 1 篇提到过):

java 复制代码
// 分配策略:轮询 + 容量感知
// 1. 按 TabletServer 的剩余容量排序
// 2. 优先分配到容量充足的节点
// 3. 同一个 Tablet 的多个副本必须分散在不同节点

第 4 步和第 5 步的顺序很关键先写 ZK 元数据,再通知 TabletServer。

为什么?反过来会出问题:

复制代码
错误顺序:先通知 TabletServer 加载 Tablet,再写 ZK
  → 如果写完通知后 Coordinator 宕机,ZK 里没有元数据
  → 新 Leader 不知道有这个 Tablet,但 TabletServer 已经加载了
  → 产生"孤儿 Tablet",占着资源没人管

正确顺序:先写 ZK,再通知
  → 如果写完 ZK 后宕机,新 Leader 接管
  → 发现 Tablet 元数据存在但没有对应副本 → 重新分配
  → 不会产生孤儿

这是分布式系统里"先持久化意图,再执行动作"的典型实践(类似 WAL 的思想)。

3.3 Altering:Schema Evolution 的边界

图右侧第二个 note:

复制代码
当前仅支持 ADD COLUMN:
- 新列对旧数据为 NULL
- 支持 DEFAULT 默认值
- Arrow Schema 动态补 NULL

为什么只支持加列,不支持改列类型、删列、改列顺序?

因为底层是 Arrow 列式格式 ,而且数据在 LogStore 里是按批次独立存储的。

考虑"把列 age 从 INT 改成 BIGINT":

复制代码
批次 1(改之前写入):Arrow Schema = [id: BIGINT, age: INT(32bit)]
批次 2(改之后写入):Arrow Schema = [id: BIGINT, age: BIGINT(64bit)]

读取时要合并两个批次的 Arrow Vector
  → 类型不同,无法直接合并
  → 要么做运行时转换(复杂且慢),要么重写历史数据(代价大)

所以 Fluss 只支持向后兼容的变更:

sql 复制代码
-- 支持:加列(在最后追加)
ALTER TABLE orders ADD COLUMN promotion_id STRING;

-- 支持:加列带默认值
ALTER TABLE orders ADD COLUMN channel STRING DEFAULT 'app';

-- 不支持(会报错):
ALTER TABLE orders MODIFY COLUMN amount STRING;   -- 改类型
ALTER TABLE orders DROP COLUMN promotion_id;      -- 删列
ALTER TABLE orders RENAME COLUMN amount TO amt;   -- 改列名

"新列对旧数据为 NULL"是怎么实现的?

答案在 note 的第三行:"Arrow Schema 动态补 NULL"。

复制代码
读取批次 1(Schema 里没有 promotion_id 列):
  1. 读到 Arrow RecordBatch,只有 [id, age]
  2. 用当前表的 Schema [id, age, promotion_id] 去投影
  3. 发现 promotion_id 在批次 1 中不存在
  4. 动态生成一个全 NULL 的 FieldVector 补上
  5. 返回给上层,看起来就像所有数据都有这一列

这个机制让加列操作完全不需要重写历史数据,是 O(1) 的元数据操作

3.4 回答"问题二"

业务要加 promotion_id 字段:

sql 复制代码
-- 1. Fluss 侧加列(毫秒级,纯元数据操作)
ALTER TABLE orders ADD COLUMN promotion_id STRING;

-- 2. Flink 作业侧:如果是 Catalog 模式,什么都不用做
--    下次作业启动时 FlussCatalog.getTable() 自动拉到新 Schema
SELECT order_id, promotion_id FROM orders;

-- 3. 历史数据的 promotion_id 是 NULL,业务侧要处理
SELECT order_id, COALESCE(promotion_id, 'N/A') FROM orders;

不需要停服,不需要重写数据,不需要改 40 个作业。

3.5 Dropping 的三步

复制代码
Dropping : 停止写入
Dropping : 清理 Tablet 数据
Dropping : 删除 ZK 元数据

注意顺序也是先停写,再清数据,最后删元数据------和 Creating 的顺序相反,同样是"先保证状态安全,再清理"的思路。


四、图 3:Tiering 分层存储时序图

4.1 问题三:冷热分层

业务需求回顾:

最近 3 天的数据要秒级可查,3 天前到 2 年的数据要能查但不要求实时。

Fluss 的解法:

复制代码
Hot Tier(Fluss TabletServer)
  格式:Arrow(列式,内存友好)
  新鲜度:秒级
  保留:1-3 天
  介质:本地 SSD
  成本:高

Cold Tier(Iceberg / Paimon / Lance)
  格式:Parquet(列式,压缩率高)
  新鲜度:分钟级
  保留:数月 ~ 数年
  介质:S3 对象存储
  成本:低(约为 SSD 的 1/10)

两者通过 Union Read 统一查询

4.2 触发阶段

复制代码
Mgr -> Mgr : scheduleTiering() 定期扫描 tiering-enabled 表
Mgr -> Mgr : needsTiering() 判断 (有新数据 && 间隔已到)

两个触发条件:

  1. 有新增数据LEO > lastTieredOffset
  2. 间隔已到 :距离上次 tiering 超过了 commit-interval

配置:

sql 复制代码
ALTER TABLE orders SET (
    'table.tiering.enabled' = 'true',
    'table.tiering.commit-interval' = '5min',
    'table.tiering.compaction.max-memory' = '2gb',
    'table.tiering.local-retention' = '3d'
);
参数 含义 调优建议
commit-interval 多久做一次 tiering 越短冷数据越新鲜,但小文件越多。建议 5-30 min
compaction.max-memory 单次压缩的内存上限 大表可调到 4-8GB
local-retention 热层保留多久 等于业务的"实时查询窗口"

4.3 执行阶段:读热数据

复制代码
Mgr -> Task : 创建 TieringTask(tablePath, lastTieredOffset)
Task -> Hot : 读取 [lastTieredOffset, LEO) 数据
Hot --> Task : Arrow RecordBatch (热数据)

lastTieredOffset 是增量的关键:每次只处理上次结束位置之后的新数据,不重复处理。

4.4 Arrow → Parquet 转换

复制代码
Task -> Comp : compact(arrowBatch)
Comp -> Comp : Arrow Schema → Parquet Schema 逐列类型映射
Comp -> Comp : 写入 Parquet (SNAPPY 压缩, 128MB Row Group)
Comp --> Task : Parquet 文件

为什么这一步是"零转换"的?

因为 Arrow 和 Parquet 都是列式格式,内存布局和文件布局高度对应

复制代码
Arrow 内存:                    Parquet 文件:
  col_id:     [1,2,3,...]  →     column chunk: id
  col_name:   ['a','b',..] →     column chunk: name
  col_amount: [10.5, ...]  →     column chunk: amount

转换 ≈ 把内存里的连续数组按 Parquet 的编码格式序列化,
      不需要做行→列的转置,也不需要类型转换

这就是 Fluss 选择 Arrow 作为热层格式的深层原因------它让"热转冷"变成了近乎纯粹的序列化操作

两个关键参数:

  • SNAPPY 压缩:压缩比和 CPU 开销的平衡点(相比 GZIP,SNAPPY 快 5 倍,压缩比低 20%)
  • 128MB Row Group:Parquet 的最小读取单元。设太小 → 元数据膨胀;设太大 → 谓词下推效果差

4.5 提交到 Lakehouse

复制代码
Task -> Lake : committer.commit(parquetFile)
Lake -> S3   : 写入 Parquet 文件到对象存储
Lake -> Lake : 更新 Snapshot (Iceberg) 或 LSM 元数据 (Paimon)
Lake --> Task : Commit 完成

"更新 Snapshot"这一句是湖格式的核心价值

复制代码
Iceberg 的提交是原子的:
  1. 写入新的 Parquet 文件到 S3
  2. 生成新的 manifest 文件
  3. 原子地更新 metadata 指针,指向新的 snapshot

效果:
  - 读取方要么看到旧 snapshot,要么看到新 snapshot,不会看到中间状态
  - 支持 time travel:可以查询任意历史 snapshot
  - 支持并发写入的乐观锁

配置 Iceberg 作为冷层:

sql 复制代码
CREATE TABLE orders (
    order_id    BIGINT,
    user_id     BIGINT,
    amount      DECIMAL(18, 2),
    order_time  TIMESTAMP(3)
) PARTITIONED BY (order_date)
WITH (
    'bucket.num' = '32',

    -- Iceberg 冷存储配置
    'table.tiering.enabled' = 'true',
    'table.tiering.lake.format' = 'iceberg',
    'table.tiering.lake.catalog' = 'hadoop',
    'table.tiering.lake.warehouse' = 's3://my-bucket/warehouse/',

    -- S3 配置
    's3.endpoint' = 'https://s3.amazonaws.com',
    's3.access-key' = 'YOUR_ACCESS_KEY',
    's3.secret-key' = 'YOUR_SECRET_KEY'
);

4.6 收尾:推进 offset 并释放空间

复制代码
Task -> Task : 更新 lastTieredOffset 元数据
Task -> Hot  : deleteSegmentsBefore(offset) 释放本地 SSD 空间
Hot --> Task : 清理完成
Task --> Mgr : Tiering 完成

注意顺序 :必须先确认冷层提交成功,才能删热层数据。如果反过来,tiering 失败会导致数据永久丢失。

4.7 Union Read:一套 SQL 查冷热

图中 note 的最后一句:

复制代码
两者共享 Schema, 通过 Union Read 统一查询

Union Read 的执行过程:

sql 复制代码
SELECT COUNT(*), SUM(amount) FROM orders;
复制代码
执行计划:

┌─────────────────┐
│   Union Read    │
└────────┬────────┘
         │
    ┌────┴─────┐
    ▼          ▼
┌────────┐  ┌──────────┐
│Hot Tier│  │Cold Tier │
│(Fluss) │  │(Iceberg) │
│最近3天 │  │3天前~2年 │
└────┬───┘  └────┬─────┘
     │           │
  秒级数据     分钟级数据
     └─────┬─────┘
           ▼
     合并去重(按 offset / 主键)
           ▼
      返回完整结果

对用户完全透明:同一条 SQL,既能查到刚写入的秒级数据,也能查到两年前的历史数据。不需要写两条 SQL 再 UNION。

这就是 Fluss 宣传的 Streaming Lakehouse 的核心:一套存储,同时满足实时性和低成本留存。


五、动手验证

5.1 完整端到端 Demo

sql 复制代码
-- ========== 1. 注册 Catalog ==========
CREATE CATALOG fluss_catalog WITH (
    'type' = 'fluss',
    'bootstrap.servers' = 'coordinator:9123'
);
USE CATALOG fluss_catalog;

-- ========== 2. 建表(开启 tiering)==========
CREATE TABLE orders (
    order_id    BIGINT,
    user_id     BIGINT,
    amount      DECIMAL(18, 2),
    order_time  TIMESTAMP(3),
    order_date  STRING,
    PRIMARY KEY (order_id) NOT ENFORCED
) PARTITIONED BY (order_date)
WITH (
    'bucket.num' = '32',
    'table.merge-engine' = 'deduplicate',
    'table.tiering.enabled' = 'true',
    'table.tiering.lake.format' = 'iceberg',
    'table.tiering.lake.warehouse' = 's3://my-bucket/warehouse/',
    'table.tiering.local-retention' = '3d'
);

-- ========== 3. 写入数据 ==========
INSERT INTO orders SELECT * FROM source_table;

-- ========== 4. Union Read(自动跨冷热)==========
SELECT COUNT(*), SUM(amount) FROM orders;

-- ========== 5. Schema Evolution ==========
ALTER TABLE orders ADD COLUMN promotion_id STRING;
SELECT order_id, COALESCE(promotion_id, 'N/A') FROM orders;

5.2 观察 Tiering 进度

bash 复制代码
# Coordinator metrics
curl http://coordinator:9124/metrics | grep tiering

# 关注指标
fluss_tiering_task_count          # 累计 tiering 任务数
fluss_tiering_last_tiered_offset  # 已归档的 offset
fluss_tiering_lag_offset          # 距 LEO 还有多少未归档(应该周期性归零)
fluss_tiering_duration_ms         # 单次 tiering 耗时

5.3 验证冷热数据

bash 复制代码
# 热层(本地 SSD)
du -sh /data/fluss-server/data/log/fluss/orders/

# 冷层(S3)
aws s3 ls s3://my-bucket/warehouse/orders/ --recursive --summarize

# 对比:3 天前的数据应该只在 S3 上,本地已清理

5.4 验证 Iceberg Time Travel

sql 复制代码
-- 查询某个历史 snapshot
SELECT * FROM orders /*+ OPTIONS('snapshot-id'='1234567890') */;

六、生产实践要点

6.1 Catalog 使用规范

sql 复制代码
-- 推荐:一个集群一个 Catalog,命名统一
CREATE CATALOG fluss_prod WITH (...);
CREATE CATALOG fluss_test WITH (...);

-- 作业里显式指定,避免歧义
SELECT * FROM fluss_prod.`default`.orders;

不要在作业里硬编码 bootstrap.servers ------ 用 Catalog 或配置中心管理。

6.2 Schema 演进规范

sql 复制代码
-- ✅ 推荐:只在末尾加列,新列可空
ALTER TABLE orders ADD COLUMN promotion_id STRING;

-- ✅ 推荐:加列时给默认值(业务语义更清晰)
ALTER TABLE orders ADD COLUMN channel STRING DEFAULT 'unknown';

-- ❌ 避免:加列后立即要求非空
--    (历史数据是 NULL,会导致下游 NPE)

-- ❌ 禁止:改类型、删列、改列名
--    (需要重建表 + 数据迁移)

变更流程:

复制代码
1. 先在 Fluss 侧 ALTER TABLE(秒级完成)
2. 下游作业逐个发版(读取新列)
3. 业务侧用 COALESCE 兜底历史 NULL

6.3 Tiering 调优

场景 建议配置
实时性优先(分钟级新鲜度) commit-interval: 1min,接受小文件多
成本优先 commit-interval: 30min,文件更少更大
大表(TB 级) compaction.max-memory: 8gb,按分区并行
小表(GB 级) 默认配置即可

小文件问题 :如果 commit-interval 太短,S3 上会堆积大量小 Parquet 文件,查询性能会下降。解决:

sql 复制代码
-- 定期做 Iceberg 的 compaction(在 Spark/Trino 侧执行)
CALL hadoop_prod.system.rewrite_data_files('db.orders');

6.4 冷层格式选型

格式 优势 适用场景
Iceberg 生态最成熟、Snapshot 语义完善、Time Travel 通用首选
Paimon LSM 结构,支持高频更新、主键表语义 CDC 场景、需要按主键更新冷数据
Lance 针对向量检索优化 AI/ML 特征、多模态数据

七、排障手册

现象 可能原因 排查方向
Flink 找不到表 Catalog 未注册或 database 不对 SHOW CATALOGS / SHOW DATABASES / SHOW TABLES
加列后下游报 NPE 历史数据是 NULL 下游用 COALESCE 兜底
加列报 "unsupported" 试图改类型/删列 只支持末尾加列;其他变更要重建表
Tiering 一直不执行 没开 enabled 或没新数据 检查表配置;看 tiering_lag_offset
冷层没有数据 S3 配置错误或权限不足 检查 s3.access-key;手动测试 S3 连通性
Union Read 结果偏少 热层数据已被清理但冷层未提交 检查 last_tiered_offsetlocal-retention 是否匹配
Union Read 很慢 冷层小文件太多 在 Spark/Trino 侧定期 rewrite_data_files
磁盘空间不释放 tiering 卡住 检查 tiering 任务日志;确认 S3 写入正常

八、五篇总结:Fluss 的全景回顾

到这里,14 张图全部讲完。回顾整个系列:

篇目 核心问题 关键结论
一、架构与核心服务 集群由什么组成? 四层架构;Coordinator 不存数据;两阶段启动 + ZK Fence 防脑裂
二、存储引擎 数据怎么存? LogStore(稀疏索引 + mmap)+ KvStore(RocksDB + 共享 WAL);三种 Merge Engine
三、读写全链路 读写怎么优化? 写入:本地路由 + 顺序 WAL;扫描:服务端列裁剪 + 谓词下推;点查:LRU → Bloom → RocksDB
四、分布式协调 集群怎么不出事? 顺序节点选举 + 链式 watch + epoch fence;ISR + HW;渐进式 Rebalance
五、接入与生命周期 数据怎么进来、怎么归档? Catalog 统一元数据;加列 O(1);Arrow → Parquet → Iceberg + Union Read

Fluss 相对 Kafka 的三个本质差异:

  1. 从"消息"到"表":Kafka 只有 append-only log,Fluss 同时提供 Log 和 KV 两种语义。这让维表关联、状态外部化成为可能。

  2. 从"行"到"列":Kafka 是行式字节流,Fluss 用 Arrow 列式格式。这让列裁剪、谓词下推、向量化计算成为可能,宽表场景网络传输降低 99%。

  3. 从"队列"到"湖仓":Kafka 的数据留存能力弱(成本高),Fluss 通过 Tiering 把冷数据自动归档成 Parquet/Iceberg,并用 Union Read 统一查询。

选型决策:

复制代码
你的场景是纯日志管道,只需要流式读写?
  → Kafka 更成熟,生态更全

你的场景需要以下任一能力?
  ✓ 维表关联(点查)
  ✓ 宽表列裁剪(200 列只读 5 列)
  ✓ 状态外部化(Flink 无状态化)
  ✓ 实时 + 历史统一查询(Streaming Lakehouse)
  → Fluss 值得评估

相关推荐
IanSkunk22 分钟前
视光中心信息化建设的关键问题与路径
大数据·人工智能
ACP广源盛1392462567333 分钟前
端侧 AI 硬件架构实践@ACP#中端边缘整机 PCIe 扩展与 IX7012 器件分析
大数据·数据库·人工智能·嵌入式硬件·开源·硬件架构
ACP广源盛1392462567334 分钟前
端侧 AI 硬件架构探讨@ACP#多外设高密度整机 PCIe 扩展与 IX7024 器件分析
大数据·数据库·人工智能·嵌入式硬件·开源·硬件架构
ZGi.ai42 分钟前
ZGI 文件产物:生成报告后怎样交付
大数据·运维·workflow·权限管理·zgi·智能体交付·文件产物
StarRocks_labs44 分钟前
StarRocks × Fluss × Paimon:流湖仓分析与 ETL 闭环
starrocks·flink·jni·native·paimon·湖仓·fluss
企业数字化服务商1 小时前
深圳企业大模型私有化部署后,如何与现有OA/CRM系统无缝集成
大数据·人工智能
数智启示录1 小时前
Doris精讲篇(六) 从第一批小文件到 -235:Doris 是怎样被正常写入拖死的
大数据·数据库·经验分享·面试·flink
whcyhhh1 小时前
头歌实践教学平台:大数据存储2023(十四下答案)
大数据·数据库·python
学着改变2751 小时前
2026电磁流量计选型全维度指南:励磁技术、衬里材质与工况适配深度解析
大数据·人工智能·科技·产品运营·能源·材质