图解 Fluss(五):Flink 接入与数据生命周期 ------ Connector、表生命周期与冷热分层
阅读本文你将了解: Flink 是怎么"发现"Fluss 表的、Split 与 Bucket 的映射关系、一条
CREATE TABLE在集群内部要走几步、Schema Evolution 为什么只支持加列、以及热数据如何自动归档成 Iceberg/Parquet 并被同一条 SQL 查到。配套图表:
class-03-flink-connector、state-03-table-lifecycle、seq-06-tiering难度:⭐⭐⭐⭐ | 适合人群:要做端到端方案设计与湖仓落地的架构师
一、场景:从"能跑"到"能长期跑"
前面四篇解决了性能与稳定性问题。但一个系统要真正上线,还要回答三个"工程琐碎但致命"的问题:
问题一:接入成本
数据团队有 40 个 Flink 作业,每个都要读写 Fluss。如果每个作业都要写一堆 CREATE TABLE ... WITH ('connector'='fluss', 'bootstrap.servers'='...'),那配置改一次要改 40 个地方。
问题二:Schema 演进
业务上线三个月后要加一个字段 promotion_id。加完之后,历史数据里的这一列是什么?查询会不会报错?要不要停服?
问题三:数据留存
业务要求"最近 3 天的数据要秒级可查,3 天前到 2 年的数据要能查但不要求实时"。如果全存 Fluss,成本扛不住;如果全存 Iceberg,实时性达不到。
这三个问题,对应本篇的三张图。
二、图 1:Flink Connector 类图

2.1 Catalog:让 Flink 直接"看见"Fluss 的表
问题一的答案就是 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()是 FlinkSource接口的标准契约方法,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() 判断 (有新数据 && 间隔已到)
两个触发条件:
- 有新增数据 :
LEO > lastTieredOffset - 间隔已到 :距离上次 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_offset 与 local-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 的三个本质差异:
-
从"消息"到"表":Kafka 只有 append-only log,Fluss 同时提供 Log 和 KV 两种语义。这让维表关联、状态外部化成为可能。
-
从"行"到"列":Kafka 是行式字节流,Fluss 用 Arrow 列式格式。这让列裁剪、谓词下推、向量化计算成为可能,宽表场景网络传输降低 99%。
-
从"队列"到"湖仓":Kafka 的数据留存能力弱(成本高),Fluss 通过 Tiering 把冷数据自动归档成 Parquet/Iceberg,并用 Union Read 统一查询。
选型决策:
你的场景是纯日志管道,只需要流式读写?
→ Kafka 更成熟,生态更全
你的场景需要以下任一能力?
✓ 维表关联(点查)
✓ 宽表列裁剪(200 列只读 5 列)
✓ 状态外部化(Flink 无状态化)
✓ 实时 + 历史统一查询(Streaming Lakehouse)
→ Fluss 值得评估