功能点 9:Flink Connector ------ 源码阅读笔记
对应源码阅读计划功能点 9:FlussCatalog、FlussSource/SourceEnumerator/SourceReader、FlussSink/Writer/Committer、LookupFunction。
笔记 9.1:FlussCatalog ------ Flink Catalog 实现
文件:FlussCatalog.java、FlussCatalogFactory.java
路径 :fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/catalog/
Catalog 注册入口
java
public class FlussCatalogFactory implements CatalogFactory {
@Override
public Catalog createCatalog(String name, Map<String, String> options) {
// 用户 SQL: CREATE CATALOG fluss WITH ('type'='fluss', 'bootstrap.servers'='...')
// Flink 调用此 Factory 创建 Catalog 实例
return new FlussCatalog(
name,
options.get("bootstrap.servers"),
FlinkConnectorOptions.fromMap(options)
);
}
}
getTable 核心实现
java
public class FlussCatalog extends AbstractCatalog {
@Override
public CatalogBaseTable getTable(ObjectPath tablePath) {
// 1. ★ 通过 RPC 从 Fluss Coordinator 获取 TableDescriptor
TableDescriptor flussDesc = adminClient.getTable(tablePath);
// 2. ★ Fluss Schema → Flink Schema 转换
Schema flinkSchema = Schema.newBuilder()
.fromRowDataType(toFlinkDataType(flussDesc.getSchema()))
.build();
// 3. 根据表类型创建不同的 Connector Table
Map<String, String> tableOptions = buildTableOptions(flussDesc);
if (flussDesc.getTableType() == TableType.PRIMARY_KEY) {
// PK 表:支持 Changelog Mode (UPSERT)
return CatalogTable.of(
flinkSchema,
"Fluss Primary Key Table: " + tablePath,
flussDesc.getPartitionKeys(),
tableOptions
);
} else {
// Log 表:仅追加
return CatalogTable.of(
flinkSchema,
"Fluss Log Table: " + tablePath,
flussDesc.getPartitionKeys(),
tableOptions
);
}
}
/**
* ★ 关键:Fluss 数据类型 → Flink 数据类型映射
*/
private DataType toFlinkDataType(Schema flussSchema) {
// Fluss Type → Flink Type
// ─────────────────────────────────
// INT → DataTypes.INT()
// BIGINT → DataTypes.BIGINT()
// STRING → DataTypes.STRING()
// DECIMAL(p,s) → DataTypes.DECIMAL(p,s)
// TIMESTAMP → DataTypes.TIMESTAMP(3)
// ARRAY<T> → DataTypes.ARRAY(toFlinkType(T))
RowType rowType = flussSchema.toRowType();
// ...
}
}
笔记 9.2:FlussSource ------ Source 实现
文件:FlussSource.java、FlussSourceEnumerator.java、FlussSourceReader.java
Source Split 定义
java
/**
* Fluss Source Split = TablePath + PartitionId + BucketId + StartOffset
*/
public class FlussSourceSplit implements SourceSplit {
private final TablePath tablePath;
private final long partitionId;
private final int bucketId;
private final long startOffset; // 从这个 offset 开始读
private final long stopOffset; // 读到这个 offset(可选,-1 表示无限)
}
Split 发现(Enumerator)
java
public class FlussSourceEnumerator implements SplitEnumerator<FlussSourceSplit> {
@Override
public void start() {
// 1. 从 Coordinator 获取所有 Partition
List<PartitionInfo> partitions = coordinatorClient.listPartitions(sourceTable);
// 2. 对每个 Partition,获取所有 Bucket 信息
for (PartitionInfo partition : partitions) {
for (int bucketId = 0; bucketId < partition.getBucketCount(); bucketId++) {
long leaderServer = partition.getBucketLeader(bucketId);
// 3. 创建 Split
FlussSourceSplit split = new FlussSourceSplit(
sourceTable,
partition.getPartitionId(),
bucketId,
discoverStartOffset(partition, bucketId) // 从 Checkpoint 恢复
);
pendingSplits.add(split);
}
}
// 4. ★ 批量分配 Split 给 Reader(避免逐个分配的开销)
assignSplitsInBatches();
}
/**
* ★ 本地优先分配策略:
* 优先将 Split 分配给与 TabletServer 在同一节点的 Reader
*/
private void assignSplitsInBatches() {
Map<String, List<FlussSourceSplit>> readerAssignments = new HashMap<>();
for (FlussSourceSplit split : pendingSplits) {
// 获取 Split 的 Leader TabletServer 地址
String leaderHost = getLeaderHost(split);
// 优先分配给同一主机的 Reader
String preferredReader = findLocalReader(leaderHost);
readerAssignments.computeIfAbsent(preferredReader, k -> new ArrayList<>())
.add(split);
}
// 下发分配
for (var entry : readerAssignments.entrySet()) {
context.assignSplits(
new SplitsAssignment<>(entry.getValue(), entry.getKey())
);
}
}
}
数据读取(Reader)
java
public class FlussSourceReader implements SourceReader<RowData, FlussSourceSplit> {
@Override
public void pollNext(ReaderOutput<RowData> output) {
for (FlussSourceSplit split : assignedSplits) {
// 1. ★ 连接到 Split 对应的 TabletServer
LogScanner scanner = getOrCreateScanner(split);
// 2. 读取一批 Arrow RecordBatch
ArrowRecordBatch batch = scanner.nextBatch();
if (batch != null) {
// 3. ★ 列裁剪(通过 projectedColumns 参数)
ArrowRecordBatch projected = batch.project(projectedColumns);
// 4. Arrow → Flink RowData 转换
for (int i = 0; i < projected.getRowCount(); i++) {
RowData row = convertToRowData(projected, i);
output.collect(row);
}
// 5. 更新 Checkpoint offset
split.setCurrentOffset(scanner.getCurrentOffset());
}
}
}
}
笔记 9.3:FlussSink ------ Sink 实现与 Exactly-Once
文件:FlussSink.java、FlussSinkWriter.java、FlussSinkCommitter.java
Sink Writer
java
public class FlussSinkWriter implements SinkWriter<RowData> {
private final Map<Integer, LogWriter> bucketWriters; // BucketId → Writer
@Override
public void write(RowData row, Context context) {
// 1. 确定分桶
int bucketId = bucketingFunction.getBucket(row, numBuckets);
// 2. 获取或创建对应 Bucket 的 Writer
LogWriter writer = bucketWriters.computeIfAbsent(bucketId, id ->
createWriter(tablePath, partitionId, id)
);
// 3. 序列化并写入
ArrowRecordBatch batch = serializer.serialize(Collections.singletonList(row));
writer.write(batch);
}
@Override
public void flush(boolean endOfInput) {
// Flush 所有 pending 的 Batch
for (LogWriter writer : bucketWriters.values()) {
writer.flush();
}
}
}
Two-Phase Commit(Exactly-Once 保证)
java
public class FlussSinkCommitter implements SinkCommitter {
/**
* ★ Phase 1: Prepare(Checkpoint 触发时)
* 将所有 Writer 的当前 offset 保存为 pending commit
*/
public List<CommitRequest> prepareCommit() {
List<CommitRequest> commits = new ArrayList<>();
for (var entry : bucketWriters.entrySet()) {
int bucketId = entry.getKey();
LogWriter writer = entry.getValue();
commits.add(new CommitRequest(
tablePath, partitionId, bucketId,
writer.getCurrentOffset() // ★ 记录当前已写入的 offset
));
}
return commits;
}
/**
* ★ Phase 2: Commit(所有并行 Writer 的 checkpoint 都完成后)
* 将所有 pending commit 标记为已完成
*/
public void commit(List<CommitRequest> commits) {
for (CommitRequest req : commits) {
// 通知 Fluss Server:这批数据已成功写入并 Checkpoint
// Server 端推进 Committed Offset
adminClient.commitOffset(
req.tablePath, req.partitionId, req.bucketId, req.offset
);
}
}
/**
* ★ 故障恢复:从最近 Checkpoint 恢复
* Sink 自动从上次 Committed Offset 继续写入
* 不会产生重复数据(因为 Checkpoint 前的数据已确认为 Committed)
*/
}
笔记 9.4:FlussLookupFunction ------ Lookup Join
文件:FlussLookupFunction.java
java
public class FlussLookupFunction extends TableFunction<RowData> {
private final FlussConnection connection;
private final Cache<RowData, RowData> lookupCache; // ★ LRU 缓存
/**
* Flink 每来一条主表数据,调用一次 eval
* 对应 SQL: LEFT JOIN dim_table FOR SYSTEM_TIME AS OF o.time AS d ON o.key = d.key
*/
public void eval(Object... joinKeys) {
RowData key = GenericRowData.of(joinKeys);
// 1. ★ 先查本地 LRU 缓存(减少网络调用)
RowData cached = lookupCache.getIfPresent(key);
if (cached != null) {
collect(cached);
return;
}
// 2. 缓存未命中 → 向 Fluss 发起 PK Lookup
byte[] lookupKey = serializeKey(joinKeys);
byte[] result = connection.pointLookup(
FileSystemTablePath.of(dimTable),
lookupKey
);
if (result != null) {
RowData row = deserializeRow(result);
lookupCache.put(key, row); // 写入缓存
collect(row);
}
// 维表中没有匹配的记录 → LEFT JOIN 只输出左表数据
}
/**
* ★ 缓存配置
*/
public static class LookupCacheConfig {
private final int maxRows; // 最大缓存行数(默认 10000)
private final Duration ttl; // 缓存过期时间(默认 10 分钟)
public Cache<RowData, RowData> createCache() {
return Caffeine.newBuilder()
.maximumSize(maxRows)
.expireAfterWrite(ttl)
.recordStats() // 记录缓存命中率
.build();
}
}
}
阅读小结
| 已理解 | 尚未深入 |
|---|---|
| ✅ FlussCatalog 如何将 Fluss Schema 转为 Flink Schema | ⬜ $changelog 和 $binlog 虚拟表的实现 |
| ✅ SourceEnumerator 的 Split 发现和本地优先分配 | ⬜ 动态分区发现(运行时新增分区) |
| ✅ Sink 的 Two-Phase Commit Exactly-Once 保证 | ⬜ SinkCommitter 的 Globally Committed 生命周期 |
| ✅ LookupFunction 的 LRU 缓存和点查询实现 | ⬜ Lookup Join 对 Flink 执行计划的优化影响 |
下一步:功能点 10------DeltaJoinOperator、JoinStateStore 的状态外部化实现。