功能点 9:Flink Connector

发布时间:2026/10/20 19:59:10

功能点 9:Flink Connector 功能点 9Flink Connector —— 源码阅读笔记对应源码阅读计划功能点 9FlussCatalog、FlussSource/SourceEnumerator/SourceReader、FlussSink/Writer/Committer、LookupFunction。笔记 9.1FlussCatalog —— Flink Catalog 实现文件FlussCatalog.java、FlussCatalogFactory.java路径fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/catalog/Catalog 注册入口publicclassFlussCatalogFactoryimplementsCatalogFactory{OverridepublicCatalogcreateCatalog(Stringname,MapString,Stringoptions){// 用户 SQL: CREATE CATALOG fluss WITH (typefluss, bootstrap.servers...)// Flink 调用此 Factory 创建 Catalog 实例returnnewFlussCatalog(name,options.get(bootstrap.servers),FlinkConnectorOptions.fromMap(options));}}getTable 核心实现publicclassFlussCatalogextendsAbstractCatalog{OverridepublicCatalogBaseTablegetTable(ObjectPathtablePath){// 1. ★ 通过 RPC 从 Fluss Coordinator 获取 TableDescriptorTableDescriptorflussDescadminClient.getTable(tablePath);// 2. ★ Fluss Schema → Flink Schema 转换SchemaflinkSchemaSchema.newBuilder().fromRowDataType(toFlinkDataType(flussDesc.getSchema())).build();// 3. 根据表类型创建不同的 Connector TableMapString,StringtableOptionsbuildTableOptions(flussDesc);if(flussDesc.getTableType()TableType.PRIMARY_KEY){// PK 表支持 Changelog Mode (UPSERT)returnCatalogTable.of(flinkSchema,Fluss Primary Key Table: tablePath,flussDesc.getPartitionKeys(),tableOptions);}else{// Log 表仅追加returnCatalogTable.of(flinkSchema,Fluss Log Table: tablePath,flussDesc.getPartitionKeys(),tableOptions);}}/** * ★ 关键Fluss 数据类型 → Flink 数据类型映射 */privateDataTypetoFlinkDataType(SchemaflussSchema){// Fluss Type → Flink Type// ─────────────────────────────────// INT → DataTypes.INT()// BIGINT → DataTypes.BIGINT()// STRING → DataTypes.STRING()// DECIMAL(p,s) → DataTypes.DECIMAL(p,s)// TIMESTAMP → DataTypes.TIMESTAMP(3)// ARRAYT → DataTypes.ARRAY(toFlinkType(T))RowTyperowTypeflussSchema.toRowType();// ...}}笔记 9.2FlussSource —— Source 实现文件FlussSource.java、FlussSourceEnumerator.java、FlussSourceReader.javaSource Split 定义/** * Fluss Source Split TablePath PartitionId BucketId StartOffset */publicclassFlussSourceSplitimplementsSourceSplit{privatefinalTablePathtablePath;privatefinallongpartitionId;privatefinalintbucketId;privatefinallongstartOffset;// 从这个 offset 开始读privatefinallongstopOffset;// 读到这个 offset可选-1 表示无限}Split 发现EnumeratorpublicclassFlussSourceEnumeratorimplementsSplitEnumeratorFlussSourceSplit{Overridepublicvoidstart(){// 1. 从 Coordinator 获取所有 PartitionListPartitionInfopartitionscoordinatorClient.listPartitions(sourceTable);// 2. 对每个 Partition获取所有 Bucket 信息for(PartitionInfopartition:partitions){for(intbucketId0;bucketIdpartition.getBucketCount();bucketId){longleaderServerpartition.getBucketLeader(bucketId);// 3. 创建 SplitFlussSourceSplitsplitnewFlussSourceSplit(sourceTable,partition.getPartitionId(),bucketId,discoverStartOffset(partition,bucketId)// 从 Checkpoint 恢复);pendingSplits.add(split);}}// 4. ★ 批量分配 Split 给 Reader避免逐个分配的开销assignSplitsInBatches();}/** * ★ 本地优先分配策略 * 优先将 Split 分配给与 TabletServer 在同一节点的 Reader */privatevoidassignSplitsInBatches(){MapString,ListFlussSourceSplitreaderAssignmentsnewHashMap();for(FlussSourceSplitsplit:pendingSplits){// 获取 Split 的 Leader TabletServer 地址StringleaderHostgetLeaderHost(split);// 优先分配给同一主机的 ReaderStringpreferredReaderfindLocalReader(leaderHost);readerAssignments.computeIfAbsent(preferredReader,k-newArrayList()).add(split);}// 下发分配for(varentry:readerAssignments.entrySet()){context.assignSplits(newSplitsAssignment(entry.getValue(),entry.getKey()));}}}数据读取ReaderpublicclassFlussSourceReaderimplementsSourceReaderRowData,FlussSourceSplit{OverridepublicvoidpollNext(ReaderOutputRowDataoutput){for(FlussSourceSplitsplit:assignedSplits){// 1. ★ 连接到 Split 对应的 TabletServerLogScannerscannergetOrCreateScanner(split);// 2. 读取一批 Arrow RecordBatchArrowRecordBatchbatchscanner.nextBatch();if(batch!null){// 3. ★ 列裁剪通过 projectedColumns 参数ArrowRecordBatchprojectedbatch.project(projectedColumns);// 4. Arrow → Flink RowData 转换for(inti0;iprojected.getRowCount();i){RowDatarowconvertToRowData(projected,i);output.collect(row);}// 5. 更新 Checkpoint offsetsplit.setCurrentOffset(scanner.getCurrentOffset());}}}}笔记 9.3FlussSink —— Sink 实现与 Exactly-Once文件FlussSink.java、FlussSinkWriter.java、FlussSinkCommitter.javaSink WriterpublicclassFlussSinkWriterimplementsSinkWriterRowData{privatefinalMapInteger,LogWriterbucketWriters;// BucketId → WriterOverridepublicvoidwrite(RowDatarow,Contextcontext){// 1. 确定分桶intbucketIdbucketingFunction.getBucket(row,numBuckets);// 2. 获取或创建对应 Bucket 的 WriterLogWriterwriterbucketWriters.computeIfAbsent(bucketId,id-createWriter(tablePath,partitionId,id));// 3. 序列化并写入ArrowRecordBatchbatchserializer.serialize(Collections.singletonList(row));writer.write(batch);}Overridepublicvoidflush(booleanendOfInput){// Flush 所有 pending 的 Batchfor(LogWriterwriter:bucketWriters.values()){writer.flush();}}}Two-Phase CommitExactly-Once 保证publicclassFlussSinkCommitterimplementsSinkCommitter{/** * ★ Phase 1: PrepareCheckpoint 触发时 * 将所有 Writer 的当前 offset 保存为 pending commit */publicListCommitRequestprepareCommit(){ListCommitRequestcommitsnewArrayList();for(varentry:bucketWriters.entrySet()){intbucketIdentry.getKey();LogWriterwriterentry.getValue();commits.add(newCommitRequest(tablePath,partitionId,bucketId,writer.getCurrentOffset()// ★ 记录当前已写入的 offset));}returncommits;}/** * ★ Phase 2: Commit所有并行 Writer 的 checkpoint 都完成后 * 将所有 pending commit 标记为已完成 */publicvoidcommit(ListCommitRequestcommits){for(CommitRequestreq:commits){// 通知 Fluss Server这批数据已成功写入并 Checkpoint// Server 端推进 Committed OffsetadminClient.commitOffset(req.tablePath,req.partitionId,req.bucketId,req.offset);}}/** * ★ 故障恢复从最近 Checkpoint 恢复 * Sink 自动从上次 Committed Offset 继续写入 * 不会产生重复数据因为 Checkpoint 前的数据已确认为 Committed */}笔记 9.4FlussLookupFunction —— Lookup Join文件FlussLookupFunction.javapublicclassFlussLookupFunctionextendsTableFunctionRowData{privatefinalFlussConnectionconnection;privatefinalCacheRowData,RowDatalookupCache;// ★ LRU 缓存/** * Flink 每来一条主表数据调用一次 eval * 对应 SQL: LEFT JOIN dim_table FOR SYSTEM_TIME AS OF o.time AS d ON o.key d.key */publicvoideval(Object...joinKeys){RowDatakeyGenericRowData.of(joinKeys);// 1. ★ 先查本地 LRU 缓存减少网络调用RowDatacachedlookupCache.getIfPresent(key);if(cached!null){collect(cached);return;}// 2. 缓存未命中 → 向 Fluss 发起 PK Lookupbyte[]lookupKeyserializeKey(joinKeys);byte[]resultconnection.pointLookup(FileSystemTablePath.of(dimTable),lookupKey);if(result!null){RowDatarowdeserializeRow(result);lookupCache.put(key,row);// 写入缓存collect(row);}// 维表中没有匹配的记录 → LEFT JOIN 只输出左表数据}/** * ★ 缓存配置 */publicstaticclassLookupCacheConfig{privatefinalintmaxRows;// 最大缓存行数默认 10000privatefinalDurationttl;// 缓存过期时间默认 10 分钟publicCacheRowData,RowDatacreateCache(){returnCaffeine.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 的状态外部化实现。
延伸阅读

更多相关文章

2026/10/18 7:29:02

大模型如何重构软件价值:从功能增强到架构重生的实战路径

1. 风暴来临:当“颠覆”不再是口号,而是生存抉择最近和几个做企业软件的朋友聊天,话题绕不开一个词:焦虑。这种焦虑不是来自竞争对手,也不是来自市场波动,而是来自一个更根本、更不可逆的趋势——大模型。过…

2026/10/19 20:12:29

音频内容智能审核:4倍速并行架构与工程实践全解析

1. 项目概述:当“耳朵经济”遇上内容安全最近几年,长音频内容(比如播客、有声书、在线课程、直播回放)的爆发式增长,让“耳朵经济”成了一个实实在在的风口。但随之而来的,是平台运营者一个头两个大的难题&…

2026/10/17 16:37:35

AI编程CLI配置管理工具:告别配置地狱,统一管理多模型环境

1. 项目概述:为什么我们需要一个AI编程CLI配置管理工具?如果你和我一样,每天的工作流里充斥着各种命令行工具,尤其是那些与AI编程相关的——比如调用不同模型的API、切换项目环境、管理一堆API密钥和配置文件——那你肯定对“配置…

2026/10/21 3:36:10

Linux网络编程必知:自定义协议设计与Socket收发踩坑全解

做Linux网络编程绕不开的一个话题就是应用层自定义协议。TCP在你面前铺开的只是一条可靠的字节流通道,字节流本身没有任何语义,两个进程之间要理解彼此,必须自己定一套对话的规则。这套规则,就是你应用层的协议。很多新手习惯直接…

2026/10/21 3:36:10

Docker配置hosts导致服务无法启动的排查与修复指南

1. 从一次凌晨的容器启动失败说起凌晨两点,测试环境的部署流水线突然卡住了。运维同事在群里甩了一张截图:Job for docker.service failed because the control process exited with error code。我第一反应是磁盘满了或者内核参数被改坏了,结…

2026/10/21 3:36:10

Jetson部署7B大模型实战:量化与内存优化全攻略

Jetson设备部署7B级别大语言模型,到底是不是一件“绝对可行”的事?我在Orin系列上折腾了一个多月,可以很负责任地说:可行,而且远没有想象中那么玄乎。只要把系统、内存和量化这三件事处理到位,qwen3.5这一代…

2026/10/21 3:36:10

Docker Compose 一键打包多技能 AI Agent 的实践与踩坑

最近帮一个内部小团队把散落一地的 AI 脚本收编成一套容器化服务,感觉这件事比写业务代码本身还值。团队里原本有做问答的、做知识库检索的、做文档摘要的,各自跑在个人笔记本上,Python 版本不一样、依赖互相冲突、模型 API 密钥散落在每个人…

2026/10/21 3:36:10

Android轻量级FTP服务器实现:纯Java无Root嵌入式文件服务

简介:本资源是一个基于 Apache FtpServer 实现的 Android 端轻量级 FTP 服务器 Demo,面向 Android 应用开发初学者与嵌入式网络服务实践者,解决在移动设备上快速搭建本地文件共享服务的实际需求,适用于局域网内调试、IoT 设备文件…

2026/10/21 3:31:10

基于YOLOv8的智慧工地安全防护装备检测系统实战

做工地安全防护检测这个项目之前,我一直在想一个问题:工地安全事故频发,监控探头装了满工地都是,为什么还是防不住?后来接触了不少智慧工地项目才明白,核心卡点根本不是摄像头不够多,而是海量实…

2026/10/19 18:06:35

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/21 0:00:35

交通流仿真实战:IDM跟驰与MOBIL换道模型参数调优与避坑指南

简介:这份资源是面向交通工程学习者与Python开发者的微观交通流仿真项目,核心实现智能驾驶模型(IDM)下的车辆跟驰与换道行为,可用于理解速度、密度与流量之间的动态关系,适合具备Python基础、希望深入交通模…

2026/10/21 0:00:35

揭穿智能体表面顺从:用追踪代码重建任务执行确定性

1. 表面顺从不是智能体的“礼貌”,而是任务执行链路上的隐性断点最近在调试一个跨平台长周期任务调度系统时,我反复遇到一种令人困惑的现象:智能体在用户指令发出后,几乎秒级响应“已收到”“正在处理”“请稍候”,UI上…

2026/10/21 0:00:35

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事:AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里,按宣传口径,本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”,而是这话背后的含金量——它把过去只能…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑