功能点 10:Delta Join 与状态外部化 ------ 源码阅读笔记
对应源码阅读计划功能点 10:DeltaJoinOperator、JoinStateStore、无状态计算架构。
笔记 10.1:DeltaJoinOperator ------ 无状态 Join 算子
文件:DeltaJoinOperator.java
路径 :fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/delta/DeltaJoinOperator.java
核心设计
传统 Flink Join 的状态模型:
Flink Task
├── LeftOperator ← 左表数据
│ └── RocksDB State (Left Side) ← 左表状态
├── RightOperator ← 右表数据
│ └── RocksDB State (Right Side) ← 右表状态
└── JoinOperator
└── ⚠️ 状态都在 Flink 本地!
Delta Join 的状态模型:
Flink Task Fluss TabletServer
├── JoinOperator ├── JoinStateStore
│ └── ❌ 无本地状态! │ └── KvStore (RocksDB)
│ │ └── JoinKey → RightRow
│ 每次收到左表记录, │
│ 直接查询 Fluss 获取右表数据 │ 右表写入时自动更新 Join 状态
└─────────────────────────────┘
核心实现
java
public class DeltaJoinOperator extends TwoInputStreamOperator<RowData, RowData, RowData> {
// ★ 注意:此 Operator 没有任何状态变量!
// 所有 Join 状态都在 Fluss 服务端的 JoinStateStore 中
private final FlussConnection connection;
private final TablePath rightTable;
private final int[] joinKeyIndexes;
/**
* 处理左表(驱动表)的数据
*/
@Override
public void processElement1(StreamRecord<RowData> leftRecord) {
RowData leftRow = leftRecord.getValue();
// 1. 提取 Join Key
byte[] joinKey = extractJoinKey(leftRow, joinKeyIndexes);
// 2. ★ 向 Fluss 查询右表数据
// 这里没有从任何本地状态读取!
byte[] rightValue = connection.pointLookup(rightTable, joinKey);
// 3. 组合输出
if (rightValue != null) {
RowData rightRow = deserialize(rightValue);
RowData output = combine(leftRow, rightRow);
output.collect(new StreamRecord<>(output));
}
// 注:LEFT JOIN 时,rightValue == null 也输出左表数据
}
/**
* 处理右表(被驱动表)的数据
* 右表数据直接写入 Fluss JoinStateStore
*/
@Override
public void processElement2(StreamRecord<RowData> rightRecord) {
RowData rightRow = rightRecord.getValue();
// 1. 提取 Join Key
byte[] joinKey = extractJoinKey(rightRow, joinKeyIndexes);
// 2. ★ 写入 Fluss JoinStateStore(不是本地 RocksDB!)
byte[] value = serialize(rightRow);
connection.putToStateStore(rightTable, joinKey, value);
// 3. 如果右表更新,通知左表侧刷新
// (通过 changelog 机制或定期刷新)
}
/**
* ★ Checkpoint 回调:无需保存任何本地状态
*/
@Override
public void snapshotState(StateSnapshotContext context) {
// 空实现!所有状态都在 Fluss 端
// Fluss 端的状态有自己的 WAL 和 Checkpoint 机制保证持久性
}
}
笔记 10.2:JoinStateStore ------ 服务端状态存储
文件:JoinStateStore.java
路径 :fluss-server/src/main/java/org/apache/fluss/server/tablet/delta/JoinStateStore.java
服务端实现
java
public class JoinStateStore {
// ★ 使用 KvStore 存储 Join 状态
// Key = join_key 的编码
// Value = 右表对应行的完整数据
private final KvTablet kvTablet;
/**
* 存储右表数据到 Join 状态
*/
public void putJoinState(byte[] joinKey, byte[] rowValue) {
// 1. 先写 WAL(KvTablet 自动处理)
// 2. 再写 RocksDB
kvTablet.put(joinKey, rowValue);
}
/**
* 查询 Join 状态
*/
public byte[] getJoinState(byte[] joinKey) {
return kvTablet.get(joinKey); // 亚毫秒级
}
/**
* 删除 Join 状态(右表删除行时)
*/
public void deleteJoinState(byte[] joinKey) {
kvTablet.delete(joinKey);
}
}
与传统方案的故障恢复对比
传统 Flink 有状态 Join:
故障 → Checkpoint 恢复
├── 下载 RocksDB 状态快照到新 TaskManager(数分钟)
├── 重建 LSM 树(数分钟)
└── 从 Kafka 回放 offset gap(数十秒)
总恢复时间:5-15 分钟
Fluss Delta Join:
故障 → 直接启动新 TaskManager
├── 新算子连接 Fluss(秒级)
├── JoinStateStore 状态已在 Fluss 端持久化(无需下载)
└── 新算子立即查询 Fluss 获取最新状态(毫秒级)
总恢复时间:3-5 秒
笔记 10.3:成本优化分析
状态存储成本
场景:100TB Join 状态,Flink 集群 100 核
传统方案:
├── Flink 计算:100 核
├── 状态存储(本地 SSD × 副本):100TB × 3 = 300TB SSD
├── Checkpoint 存储(S3):~30TB/次 × 10 保留 = 300TB S3
└── 总计:~$15,000 - $25,000/月
Fluss Delta Join:
├── Flink 计算:50 核(无状态后并行度可降低)
├── Fluss TabletServer:统一管理状态存储
├── 无需独立 Checkpoint 存储
└── 总计:~$5,000 - $8,000/月(含 Fluss 集群)
成本节省:~60-75%
计算弹性
场景:凌晨低峰 → 白天高峰
传统方案:
├── 扩缩容需状态重分布 → 不敢频繁操作
└── 高峰配置要覆盖峰值 → 凌晨资源浪费
Fluss Delta Join:
├── 计算无状态 → 秒级弹性扩缩
├── 凌晨:10 核 → 白天:100 核
└── 按需付费,成本再降 40-50%
阅读小结
| 已理解 | 尚未深入 |
|---|---|
| ✅ DeltaJoinOperator 的无状态设计 | ⬜ 多流 Join(N-way Join)的支持计划 |
| ✅ JoinStateStore 如何用 KvStore 存储 Join 状态 | ⬜ 右表更新如何触发左表刷新 |
| ✅ 故障恢复时间从分钟级到秒级的原因 | ⬜ 非等值 Join 的实现可能性 |
| ✅ 成本优化的量化分析 | ⬜ Delta Join 的端到端延迟影响 |
下一步:功能点 11-12------FlussClient 客户端、Netty RPC 通信、监控指标。