TL;DR
- Flink 的模块划分基本就是它的分层架构,目录即分层。
- 执行计划三层图:StreamGraph → JobGraph → ExecutionGraph。
- 调度主线看
DefaultScheduler.startScheduling()的调用链。 - 容错主线看
CheckpointCoordinator与 Barrier 对齐流程。 - 推荐顺序:WordCount → core → 图模型 → 调度 → Checkpoint。
1. 为什么值得读 Flink 源码
Apache Flink 是流批一体的分布式计算引擎,其 GitHub 仓库(apache/flink)是一个超过百万行代码、包含数十个 Maven 模块的巨型工程。很多同学第一次 clone 下来后,面对满屏的 flink-* 目录往往无从下手。
本文不打算逐行讲解某个算子,而是从工程结构 → 模块职责 → 核心抽象 → 运行时链路 → 状态与容错 → 源码阅读路线这条主线,带你建立一张 Flink 源码的"全局地图"。读完你至少能回答三个问题:
- 一个 Flink 作业从提交到运行,代码到底经过了哪些模块?
- 为什么 Flink 能做到流批一体,这个设计在源码里体现在哪?
- 想改一个功能,应该去哪个模块找入口?
说明:本文基于 Flink 1.18/1.19 时代的仓库结构,不同版本目录会有微调,但核心分层长期稳定。
2. 仓库整体结构:先看顶层目录
clone 仓库后,顶层目录大致如下(省略部分辅助文件):
flink/
├── flink-core/ # 核心抽象:类型系统、内存管理、配置
├── flink-runtime/ # 运行时内核:JobGraph、Task、调度、网络栈
├── flink-streaming-java/ # DataStream API 实现
├── flink-table/ # Table API & SQL(含 planner、runtime)
├── flink-connectors/ # 各类连接器(Kafka、JDBC、File 等)
├── flink-clients/ # 客户端提交逻辑
├── flink-statebackend-rocksdb/ # RocksDB 状态后端
├── flink-checkpoint/ # 检查点相关
├── flink-metrics/ # 指标系统
├── flink-java/ # Java DataSet API(旧版批处理)
├── flink-python/ # PyFlink
├── flink-kubernetes/ # K8s 部署相关
├── flink-annotations/ # 公共注解
├── flink-test-utils/ # 测试工具
├── docs/ # 官方文档
└── pom.xml # 根 POM,统一依赖与版本管理
一个非常实用的观察方法:Flink 的模块划分基本就是它的分层架构。越靠上的模块越贴近用户 API,越靠下的模块越贴近运行时与资源管理。
3. flink-core:一切抽象的地基
flink-core 是理解 Flink 的起点,它几乎不依赖其他 Flink 模块,却定义了后续所有模块都要用的基础能力。
3.1 类型系统 TypeInformation
Flink 需要在不依赖 Java 反射序列化的前提下高效处理数据,因此自建了一套类型系统:
TypeInformation<T>:类型描述符的根接口;BasicTypeInfo、TupleTypeInfo、PojoTypeInfo、GenericTypeInfo等具体实现;TypeSerializer<T>:与类型一一对应的序列化器。
关键点在于:Flink 在作业提交前就能确定每个数据的序列化方式,这让它可以在运行时避免反射开销,也为状态序列化、网络传输提供了统一基础。
// 典型用法:显式声明类型信息
TypeInformation<String> info = TypeInformation.of(String.class);
TypeSerializer<String> serializer = info.createSerializer(new ExecutionConfig());
3.2 内存管理 MemorySegment
flink-core 中的 MemorySegment 是 Flink 自管理内存的核心。它把堆内/堆外内存抽象成固定大小的段,配合 MemorySegmentFactory 使用,从而绕开 JVM 对象头开销与 GC 压力。排序、哈希表、网络缓冲都建立在这一层之上。
3.3 配置体系
Configuration、ConfigOption、ConfigOptions 构成了 Flink 的配置骨架。所有 flink-conf.yaml 中的配置项,最终都映射为 ConfigOption 常量,这也是排查"某个参数到底生效没有"时最该看的地方。
4. flink-runtime:真正的运行时内核
如果说 flink-core 是地基,flink-runtime 就是整栋大楼。它是 Flink 最复杂、最值得深读的模块。
4.1 三层图模型
Flink 的执行计划经历了三次转换,这是理解源码的关键线索:
| 图类型 | 所在模块 | 作用 |
|---|---|---|
| StreamGraph | flink-streaming-java | 用户 API 直接生成的逻辑图 |
| JobGraph | flink-runtime | 经过算子链优化后的提交图 |
| ExecutionGraph | flink-runtime | 调度器使用的并行执行图 |
StreamGraph由StreamGraphGenerator从DataStream转换而来;JobGraph由StreamingJobGraphGenerator生成,核心动作是算子链(Operator Chaining)合并;ExecutionGraph由ExecutionGraphBuilder构建,把每个 JobVertex 按并行度展开成ExecutionVertex。
下面用一段 Java 代码把三次转换的入口串起来,注意每一步的输入输出对象:
// ===== 第 1 步:StreamGraph 生成 =====
// 输入:StreamExecutionEnvironment(持有用户构建的 DataStream 拓扑)
// 输出:StreamGraph(逻辑图,节点是 StreamNode,边是 StreamEdge)
StreamGraph streamGraph = env.getStreamGraph(); // 内部调用 StreamGraphGenerator.generate(env, transformations)
// ===== 第 2 步:JobGraph 生成 =====
// 输入:StreamGraph(逻辑图)
// 输出:JobGraph(提交图,节点是 JobVertex,核心动作是算子链合并)
JobGraph jobGraph = StreamingJobGraphGenerator.createJobGraph(streamGraph);
// ===== 第 3 步:ExecutionGraph 生成 =====
// 输入:JobGraph(提交图)+ 调度相关上下文
// 输出:ExecutionGraph(并行执行图,JobVertex 按并行度展开为 ExecutionVertex)
ExecutionGraph executionGraph = ExecutionGraphBuilder.buildGraph(
null, // 复用的旧 ExecutionGraph,首次构建传 null
jobGraph, // 上一步产出的 JobGraph
jobGraph.getJobConfiguration(), // 作业配置
futureExecutor, // 异步执行线程池
ioExecutor, // IO 线程池
userCodeLoader, // 用户代码类加载器
checkpointRecoveryFactory, // Checkpoint 恢复工厂
rpcTimeout, // RPC 超时
metrics, // 指标组
executionGraph -> {}, // 作业状态监听器
new DefaultExecutionGraphFactory(...) // ExecutionGraph 工厂
);
三步的输入输出可以概括为:StreamExecutionEnvironment → StreamGraph、StreamGraph → JobGraph、JobGraph → ExecutionGraph。调试时在这三个入口各打一个断点,就能完整看清一个作业从 API 到可调度执行图的演进过程。
4.2 调度器 Scheduler
flink-runtime 中的 SchedulerNG 接口及其实现(如 DefaultScheduler)负责把 ExecutionGraph 中的任务分配到 TaskManager 上。它内部又拆成几个协作组件:
SchedulerStrategy:决定任务如何分配;SlotPool:管理可用 slot;ExecutionSlotAllocator:为执行顶点申请 slot。
阅读调度相关代码时,建议从 DefaultScheduler 的 startScheduling() 方法顺着调用链往下看。
下面用一段伪代码把 startScheduling() 到 SlotPool、ExecutionSlotAllocator 的关键调用顺序串起来,每一步都标注了它在做什么:
// 入口:JobMaster 收到作业后启动调度
defaultScheduler.startScheduling();
// 1. 校验并切换到 RUNNING 状态,触发 ExecutionGraph 从 CREATED 进入 RUNNING
executionGraph.start(mainThreadExecutor);
// 2. 把 ExecutionGraph 中所有尚未分配的 ExecutionVertex 交给调度策略
// SchedulerStrategy 决定"先调度谁"(如 EagerSchedulingStrategy 一次性全量调度)
schedulingStrategy.startScheduling();
// 3. 为每个待调度的 ExecutionVertex 申请 slot
// ExecutionSlotAllocator 负责把 ExecutionVertex 转成 SlotRequest,并做请求去重与合并
Collection<SlotExecutionVertexAssignment> assignments =
executionSlotAllocator.allocateSlotsFor(executionVertices);
// 4. SlotProvider 把 SlotRequest 转交给 SlotPool
// SlotPool 维护当前 TaskManager 上报的可用 slot,并负责向 ResourceManager 申请新资源
slotProvider.allocateSlot(slotRequest);
// 5. SlotPool 内部:优先匹配已注册的空闲 slot,匹配不到则挂起请求等待新 slot 上报
slotPoolImpl.requestSlotFromAvailableSlots(slotRequest);
// 6. slot 就绪后回调,把 ExecutionVertex 部署到对应 TaskManager 上执行
executionSlotAllocator.allocateSlot(executionVertexId, slotRequestId, slot);
这条链路的核心可以概括为:DefaultScheduler 决定调度时机 → SchedulerStrategy 决定调度顺序 → ExecutionSlotAllocator 负责申请 slot → SlotPool 负责匹配或等待资源 → 最终把 Task 部署到 TaskManager。调试时在 startScheduling() 和 SlotPool.requestSlotFromAvailableSlots() 各打一个断点,就能看清一个作业是如何从"待调度"变成"运行中"的。
4.3 Task 与 TaskManager
Task 是运行时最小的执行单元,它包装了一个 AbstractInvokable(真正干活的算子)。TaskManagerServices 负责在 TaskManager 侧初始化内存管理器、IO 管理器、网络环境等。
4.4 网络栈
flink-runtime 的 io.netty 包实现了基于 Netty 的网络传输。核心概念包括:
NetworkBufferPool:网络缓冲池;ResultPartition/InputGate:上下游数据交换的抽象;CreditBasedPartitionRequestClientHandler:基于信用的反压机制。
Flink 的反压不是靠"限流",而是靠缓冲区耗尽自然传导,这一点在源码里体现得非常直观。
5. flink-streaming-java:DataStream API 的实现
这一层是大多数用户最熟悉的 API 背后的实现。
StreamExecutionEnvironment:作业入口,负责生成 StreamGraph;DataStream及其子类:描述数据流转换;StreamOperator:算子的运行时抽象;OneInputStreamOperator/TwoInputStreamOperator:单/双输入算子接口。
以 map 为例,调用链大致是:
DataStream.map()
→ transform()
→ StreamMap(StreamOperator)
→ 在 StreamGraph 中注册为 StreamNode
理解这条链路后,你就能明白为什么自定义算子需要实现 OneInputStreamOperator 并配合 AbstractUdfStreamOperator 使用。
6. flink-table:流批一体的关键战场
flink-table 是 Flink 最庞大的子工程之一,内部又分为:
flink-table-common:公共接口与数据类型;flink-table-api-java/flink-table-api-scala:用户 API;flink-table-planner:查询计划器(基于 Calcite);flink-table-runtime:运行时算子实现。
SQL 的执行链路是:
流批一体在源码中的体现:同一套 RelNode 逻辑计划,通过不同的 Planner 配置(StreamPlanner / BatchPlanner)生成不同的物理执行计划,最终都落到 DataStream 或 DataSet 算子上。这也是为什么 Flink SQL 能同时跑流和批。
7. 状态管理与容错:Checkpoint 机制
状态与容错是 Flink 区别于普通流处理框架的核心竞争力,相关代码分散在多个模块:
flink-runtime中的checkpoint包:CheckpointCoordinator、CheckpointBarrier;flink-statebackend-rocksdb:RocksDB 状态后端;flink-checkpoint:检查点存储抽象。
7.1 Checkpoint 的源码主线
关键类:
CheckpointCoordinator:协调整个检查点流程;CheckpointBarrierHandler:处理 Barrier 对齐(Exactly-Once)或跳过(At-Least-Once);StateBackend:状态存储与快照的抽象。
7.1.1 Checkpoint 常见问题排查清单
线上遇到 Checkpoint 相关问题时,先按现象对号入座,再顺着对应的源码入口和日志关键字往下查,往往能少走很多弯路:
| 典型现象 | 源码排查入口类 | 关键日志关键字 |
|---|---|---|
Checkpoint 超时(Checkpoint expired before completing) | CheckpointCoordinator、CheckpointFailureManager | Checkpoint expired、Checkpoint ... of job ... expired |
| Barrier 对齐慢、反压导致 Checkpoint 迟迟不完成 | CheckpointBarrierHandler、CheckpointBarrierAligner、CheckpointBarrierTracker | Received barrier、alignment、currentChannel |
| 状态过大、快照耗时过长 | StateBackend、SnapshotStrategy、RocksDBKeyedStateBackend | Completed checkpoint、state size、snapshot |
| RocksDB 写放大、磁盘 IO 打满 | RocksDBKeyedStateBackend、RocksDBResourceContainer、RocksDBOperationUtils | RocksDB、compaction、write stall |
| Checkpoint 失败后作业重启 | CheckpointFailureManager、FailoverStrategy、DefaultScheduler | Failed to trigger checkpoint、Restarting job、failover |
Checkpoint 恢复失败(Could not restore) | CheckpointMetadataInputStream、CompletedCheckpointStore、StateAssignmentOperation | Could not restore、Failed to restore、state handle |
排查思路可以概括为三步:先看日志关键字定位阶段(触发 / 对齐 / 快照 / 上报 / 恢复),再进对应入口类打断点,最后结合 Metrics(如 lastCheckpointDuration、lastCheckpointSize)确认瓶颈。这样即使面对的是分布式环境下的偶发问题,也能快速缩小范围。
7.2 状态后端的选择
| 状态后端 | 特点 | 适用场景 |
|---|---|---|
| HashMapStateBackend | 状态在 JVM 堆内 | 小状态、低延迟 |
| EmbeddedRocksDBStateBackend | 状态在 RocksDB | 大状态、超大规模 |
阅读 RocksDB 后端时,重点看 RocksDBKeyedStateBackend 如何把 key 编码成字节数组并写入列族。
8. 连接器与生态模块
flink-connectors 下按数据源/目的端划分,例如:
flink-connector-kafka:Kafka Source/Sink,实现了新的 Source API;flink-connector-jdbc:关系型数据库连接;flink-connector-files:文件系统连接。
新版 Source API(Source<T> / SplitEnumerator / SourceReader)是理解连接器源码的重点,它把"发现分片"和"读取分片"解耦,天然支持并行与状态恢复。
9. 源码阅读路线建议
面对这么大的仓库,建议按以下顺序推进,避免一上来就陷进细节:
- 先跑通一个 WordCount,用 IDE 断点跟一遍
execute()的调用栈; - 读 flink-core 的类型系统与 MemorySegment,建立基础认知;
- 读三层图模型的转换,重点看
StreamGraphGenerator与StreamingJobGraphGenerator; - 读 DefaultScheduler 的调度流程,理解任务如何落到 slot;
- 读 CheckpointCoordinator,理解容错机制;
- 最后按需深入 Table/SQL 或某个连接器。
配套技巧:
- 用
git log --follow <file>看某个类的演进历史; - 关注
@PublicEvolving/@Internal注解,判断 API 稳定性; - 多读
flink-runtime下的单元测试,测试往往比注释更能说明设计意图。
10. 总结
Flink 的源码规模庞大,但它的分层非常清晰:flink-core 提供地基,flink-streaming-java 与 flink-table 提供用户 API,flink-runtime 承担调度与执行,状态与容错模块保证正确性,连接器与部署模块负责生态对接。
抓住"三层图模型 + 调度 + Checkpoint"这三条主线,再配合模块化的阅读顺序,就能在庞大的代码库中保持方向感。源码阅读从来不是一次性的任务,而是一个持续建立心智模型的过程——先有地图,再走细节,效率会高得多。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2601_96632650/article/details/170563785




