功能点 9:Flink Connector

对应源码阅读计划功能点 9:FlussCatalog、FlussSource/SourceEnumerator/SourceReader、FlussSink/Writer/Committer、LookupFunction。


文件:FlussCatalog.javaFlussCatalogFactory.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.javaFlussSourceEnumerator.javaFlussSourceReader.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.javaFlussSinkWriter.javaFlussSinkCommitter.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 的状态外部化实现。

相关推荐
JJJennie7771 小时前
MAI Gateway技术揭秘:大模型网关有哪些功能?从原理到落地
大数据·人工智能·大模型·gateway·软件工程·ai网关
天天爱吃肉82181 小时前
XCP协议 Part 1 概述 — 完整学习笔记(商用车主机厂工程师版)
大数据·笔记·python·学习·汽车
starzy19901 小时前
SparkStreaming 之配置参数详解
大数据·spark
故七月1 小时前
基于最新Geo数据的测评行业服务商分析——万域智瞰深度洞察
大数据·人工智能
weixin_469163691 小时前
从提示词到驾驭工程:金融 AI 需求工程的三段式演进路径
大数据·人工智能·金融
海兰1 小时前
【应用】Ubuntu 24 搭建大数据 & AI 应用云原生容器化实践
大数据·人工智能·ubuntu
starzy19901 小时前
SparkStreaming 之 DStream 底层结构剖析
大数据·spark
天行健,君子而铎1 小时前
2026年中国API安全产品综合排名:选型指南与市场趋势解析
大数据·数据库·安全
未来之窗软件服务1 小时前
Web 技能的自主测试系统开好处发架构思维-东方仙盟
大数据·前端·架构·仙盟创梦ide·东方仙盟