troy zhang头像
Flink GitHub 仓库源代码深度分析:从目录结构到运行时内核封面图

Flink GitHub 仓库源代码深度分析:从目录结构到运行时内核

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,越靠下的模块越贴近运行时与资源管理。

用户 API 层
DataStream / Table / SQL / PyFlink

图构建层
StreamGraph / JobGraph

运行时层
flink-runtime:调度、Task、网络

状态与容错层
Checkpoint / StateBackend

资源与部署层
YARN / K8s / Standalone

存储层
HDFS / S3 / RocksDB

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 的执行计划经历了三次转换,这是理解源码的关键线索:

图类型所在模块作用
StreamGraphflink-streaming-java用户 API 直接生成的逻辑图
JobGraphflink-runtime经过算子链优化后的提交图
ExecutionGraphflink-runtime调度器使用的并行执行图

DataStream API

StreamGraph

JobGraph
算子链合并

ExecutionGraph
按并行度展开

Task 调度执行

  • 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 的执行链路是:

SQL 文本

Parser
Calcite 解析

SqlNode 语法树

Validator 校验

RelNode 逻辑计划

优化规则
RBO/CBO

PhysicalPlan

ExecNode 生成

转换为 DataStream 算子

流批一体在源码中的体现:同一套 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 的源码主线

TaskManagerSourceJobManagerTaskManagerSourceJobManager触发 Checkpoint(注入 Barrier)Barrier 随数据流下发对齐 Barrier,快照本地状态上报状态句柄全部完成后标记 Checkpoint 完成

关键类:

  • CheckpointCoordinator:协调整个检查点流程;
  • CheckpointBarrierHandler:处理 Barrier 对齐(Exactly-Once)或跳过(At-Least-Once);
  • StateBackend:状态存储与快照的抽象。

7.1.1 Checkpoint 常见问题排查清单

线上遇到 Checkpoint 相关问题时,先按现象对号入座,再顺着对应的源码入口和日志关键字往下查,往往能少走很多弯路:

典型现象源码排查入口类关键日志关键字
Checkpoint 超时(Checkpoint expired before completing)CheckpointCoordinator、CheckpointFailureManagerCheckpoint expired、Checkpoint ... of job ... expired
Barrier 对齐慢、反压导致 Checkpoint 迟迟不完成CheckpointBarrierHandler、CheckpointBarrierAligner、CheckpointBarrierTrackerReceived barrier、alignment、currentChannel
状态过大、快照耗时过长StateBackend、SnapshotStrategy、RocksDBKeyedStateBackendCompleted checkpoint、state size、snapshot
RocksDB 写放大、磁盘 IO 打满RocksDBKeyedStateBackend、RocksDBResourceContainer、RocksDBOperationUtilsRocksDB、compaction、write stall
Checkpoint 失败后作业重启CheckpointFailureManager、FailoverStrategy、DefaultSchedulerFailed to trigger checkpoint、Restarting job、failover
Checkpoint 恢复失败(Could not restore)CheckpointMetadataInputStream、CompletedCheckpointStore、StateAssignmentOperationCould 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. 源码阅读路线建议

面对这么大的仓库,建议按以下顺序推进,避免一上来就陷进细节:

  1. 先跑通一个 WordCount,用 IDE 断点跟一遍 execute() 的调用栈;
  2. 读 flink-core 的类型系统与 MemorySegment,建立基础认知;
  3. 读三层图模型的转换,重点看 StreamGraphGenerator 与 StreamingJobGraphGenerator;
  4. 读 DefaultScheduler 的调度流程,理解任务如何落到 slot;
  5. 读 CheckpointCoordinator,理解容错机制;
  6. 最后按需深入 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

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--