1. 为什么值得花时间把 Flink 源码跑起来
很多人用 Flink 写业务作业,API 用得很溜,DataStream、KeyedProcessFunction、Checkpoint配置张口就来,但一旦遇到反压、状态不一致、作业重启后数据重复这类问题,就只能靠搜索引擎和猜。我自己也经历过这个阶段——直到有一次线上作业频繁触发全量 Checkpoint 超时,翻遍文档都找不到根因,最后硬着头皮去读源码里CheckpointCoordinator的调度逻辑,才定位到是某个算子状态过大导致对齐阶段卡死。那次之后我就养成了一个习惯:凡是核心组件,一定要把源码拉下来、编译通过、能打断点调试。
Flink 作为流批一体的分布式计算引擎,它的源码规模不算小,模块划分也比较细,flink-core、flink-runtime、flink-streaming-java、flink-clients这些模块之间的依赖关系如果不亲手编译一遍,光看文档是理不清的。而且 Flink 的构建体系用的是 Maven 多模块 + Shade 插件重定位,中间还夹杂着 Scala 和 Java 混编,第一次编译踩坑几乎是必然的。这篇内容就是把我自己从零开始下载、导入、编译 Flink 源码的完整过程梳理出来,包括版本选择、环境准备、IDE 配置、编译命令、常见报错和排查思路。适合两类人看:一类是想深入理解 Flink 内部机制但不知道从哪下手的开发者,另一类是准备给 Flink 提 PR 或者做二次开发的同学。整个过程我会尽量把"为什么这么做"讲清楚,而不是只丢几条命令。
2. 动手之前先把版本和环境这件事想明白
2.1 选哪个版本:别一上来就追最新
Flink 的版本迭代节奏比较快,主分支上经常有正在重构的代码,直接拉 master 编译大概率会遇到依赖还没发布、测试用例挂掉的情况。我的建议是优先选一个稳定的 release 分支,比如release-1.18、release-1.17这种。原因有三点:第一,release 分支的依赖都是正式发布的,不会出现 SNAPSHOT 拉不到的问题;第二,这个分支的代码和官方文档、社区讨论是对得上的,你读源码时遇到疑问容易找到对应资料;第三,如果你是想验证某个 bug 或者学习某个特性,release 分支的行为是确定的,不会因为主干重构而"跑出来的结果和预期不一样"。
具体操作上,我一般会先去 GitHub 的 releases 页面看一眼最近的稳定版本号,然后 clone 的时候直接指定分支:
git clone -b release-1.18 https://github.com/apache/flink.git这里有个细节:Flink 仓库完整 clone 下来体积不小,如果你网络条件一般,可以加--depth 1只拉最近一次提交,能省不少时间和空间。但要注意,浅克隆之后你没法直接切到别的历史提交去对比代码,如果打算做源码考古,还是老老实实全量拉。
提示:不要用 GitHub 网页上 "Download ZIP" 的方式下载源码包。ZIP 包里没有
.git目录,后续你想看某行代码的提交历史、想切分支、想打 patch 都会很麻烦。源码学习这件事,git 仓库是刚需。
2.2 JDK 和 Maven:版本对不上,编译必挂
Flink 不同版本对 JDK 的要求不一样。1.18 之前的版本官方主推 JDK 8 和 JDK 11,1.18 开始对 JDK 17 的支持逐渐完善,但如果你编译的是 1.14 到 1.17 之间的版本,用 JDK 17 去编大概率会在 Scala 编译阶段报模块访问相关的错误。我自己的做法是:编译 Flink 1.18 及以下版本,统一用 JDK 11,这是兼容性最好的选择。
Maven 方面,建议用 3.8.x 以上的版本。低版本 Maven 在处理 Flink 这种多模块 + Shade 的工程时,偶尔会出现插件版本解析异常。装好之后记得确认一下:
java -version mvn -version两个命令的输出里 JDK 版本和 Maven 版本都要对得上你的预期。另外,Maven 的settings.xml里最好配置一个稳定的镜像仓库,Flink 编译过程中要下载的依赖包数量相当可观,用默认中央仓库在国内环境下会非常慢,甚至中途超时导致构建失败。
还有一个容易被忽略的点:内存。Flink 的编译过程,尤其是 Scala 模块和 Shade 阶段,对内存消耗比较大。如果你在容器或者内存较小的机器上编译,建议给 Maven 调大堆内存:
export MAVEN_OPTS="-Xmx3g -XX:MaxMetaspaceSize=512m"这个配置我踩过坑——有一次在一台 4G 内存的云主机上编译,跑到flink-runtime模块时直接 OOM,报的还是莫名其妙的插件错误,排查了半天才发现是内存不够。
2.3 提前把 Scala 版本这件事搞清楚
Flink 的很多模块是 Scala 和 Java 混编的,比如flink-streaming-scala、flink-scala。Flink 1.15 之前同时支持 Scala 2.11 和 2.12,通过 Maven profile 来切换;1.15 之后逐步收敛到 Scala 2.12。如果你编译时报 "object xxx is not a member of package scala" 这类错误,八成是 Scala 版本没对上。
查看当前工程默认用哪个 Scala 版本,可以看根目录pom.xml里的scala.version属性,或者直接看flink-scala模块的 pom。切换版本一般用:
mvn clean install -Dscala.version=2.12.7 -Dscala.binary.version=2.12不过说实话,除非你有明确的跨版本需求,否则直接用工程默认的 Scala 版本就行,别自己折腾。我见过有同学为了"统一团队技术栈"强行改 Scala 版本,结果 shade 之后的包和运行时对不上,作业跑起来各种NoSuchMethodError,得不偿失。
3. 导入 IDE 之前,这些配置不改会很难受
3.1 用 IDEA 还是 Eclipse:我为什么坚定选 IDEA
Flink 官方文档里对 IDEA 和 Eclipse 都有说明,但实际体验下来,IDEA 对 Maven 多模块、Scala 混编、Shade 插件的支持明显更好。Eclipse 在导入 Flink 这种规模的工程时,经常出现模块识别不全、Scala 插件版本冲突的问题。如果你还没装 IDEA,社区版就够用了,不需要 Ultimate。
导入方式上,不要用 "Open" 直接打开根目录,而是用 "Open" 选中根目录的pom.xml,让 IDEA 以 Maven 工程的方式导入。这样 IDEA 会自动识别所有子模块,并按照 pom 里声明的依赖关系建立模块依赖图。导入过程中 IDEA 会去下载依赖,这一步比较耗时,耐心等它跑完,中途别乱点。
3.2 几个必须手动调整的 IDEA 设置
导入完成之后,有几处默认配置需要改,否则你读代码和调试都会很别扭。
第一处是Scala 编译器的 target JVM 版本。在Settings -> Build, Execution, Deployment -> Compiler -> Scala Compiler里,把 target 设成和你的 JDK 一致,比如 JDK 11 就设 11。不设的话,Scala 代码可能按 JDK 8 编译,和 Java 模块的字节码版本不一致,运行时会报UnsupportedClassVersionError。
第二处是Maven 的自动导入和离线模式。Flink 依赖多,IDEA 默认会在后台频繁检查更新,很吃资源。可以在 Maven 面板里把 "Auto-Import" 关掉,需要的时候手动 Reload。如果你已经把依赖都下好了,可以勾上 "Work offline",能明显减少卡顿。
第三处是代码风格和 Checkstyle。Flink 对代码格式有严格要求,根目录下有tools/maven/checkstyle.xml和.editorconfig。如果你打算改代码提 PR,一定要在 IDEA 里装上 Checkstyle 插件并导入这个配置,否则本地编译能过,提交上去 CI 会因为格式问题挂掉。我自己第一次提 PR 就是被 Checkstyle 拦下来的,什么 import 顺序、行尾空格、Javadoc 格式,全是细节。
3.3 模块太多看花眼?先理清依赖层次
Flink 根目录下有几十个模块,刚导入的时候很容易懵。我的经验是先把模块按层次分一下类,读代码时按需深入,不要试图一次全看懂。
| 层次 | 代表模块 | 作用 |
|---|---|---|
| 基础层 | flink-core、flink-annotations | 核心数据结构、类型系统、注解 |
| 运行时层 | flink-runtime、flink-rpc | 作业调度、Task 执行、RPC 通信 |
| API 层 | flink-streaming-java、flink-streaming-scala | DataStream API 实现 |
| 客户端层 | flink-clients、flink-java | 作业提交、环境构建 |
| 连接器层 | flink-connector-* | 各类外部系统连接器 |
| 部署层 | flink-yarn、flink-kubernetes | 集群部署相关 |
读源码的时候,建议从flink-streaming-java入手,因为这是你写业务代码最常打交道的模块,从这里往下追到flink-runtime,再往下追到flink-core,路径比较自然。反过来从flink-runtime硬啃,很容易被各种内部抽象劝退。
4. 编译命令怎么敲:从全量到按需
4.1 第一次编译:老老实实全量走一遍
第一次编译我建议全量执行,把整个工程编一遍,确保环境没问题:
mvn clean install -DskipTests -Dfast这里几个参数解释一下。clean install是清理并安装到本地仓库,这样模块之间能通过本地仓库互相引用。-DskipTests跳过测试用例执行,Flink 的测试用例非常多,全跑一遍可能要一两个小时,第一次编译没必要。-Dfast是 Flink 提供的一个 profile,它会跳过一些耗时的检查,比如 checkstyle、spotbugs、rat 检查等,能显著加快编译速度。
如果你连测试代码的编译都想跳过,可以再加-Dmaven.test.skip=true,注意这个和-DskipTests的区别:-DskipTests是编译测试代码但不运行,-Dmaven.test.skip=true是连测试代码都不编译。第一次编译为了快,用后者也行。
编译成功的标志是最后看到BUILD SUCCESS,并且各个模块的 jar 都装进了本地仓库。如果中途失败,先别慌,看报错信息定位是哪个模块、哪个插件出的问题,下面我会讲常见错误。
4.2 只编译你关心的模块
全量编译通过之后,后续你改代码再编译就没必要全量了。比如你只改了flink-runtime,可以这样:
mvn clean install -pl flink-runtime -am -DskipTests -Dfast-pl指定要编译的模块,-am表示同时编译它依赖的模块。这样 Maven 只会编译flink-runtime以及它上游依赖的模块,速度快很多。反过来,如果你改了flink-core,那所有依赖它的模块理论上都要重新编译,这时候用-amd(also make dependents)把下游也带上:
mvn clean install -pl flink-core -amd -DskipTests -Dfast这个技巧在调试时特别有用。比如你想验证某个flink-core里的类型系统改动,用-amd把flink-streaming-java一起编了,然后跑一个简单的 DataStream 作业,就能快速看到效果。
4.3 编译产物在哪:别找错地方
编译完成后,各模块的 jar 包在各自模块目录的target/下。但要注意,Flink 有些模块会通过 Shade 插件生成一个"重定位"后的 jar,比如flink-dist模块最终会打出一个包含所有依赖的大包,这个包在flink-dist/target/flink-<version>-bin/目录下。如果你是想本地起一个 MiniCluster 跑作业,用 IDE 直接运行 main 方法就行,不需要这个 dist 包;如果你是想部署到集群,那就要用 dist 包。
我见过有同学编译完找不到"可执行文件",在flink-runtime/target里翻半天,其实他要找的是flink-dist的产物。记住这个对应关系就不会迷路。
5. 编译报错排查:我踩过的那些坑
5.1 依赖下载失败:九成是仓库配置问题
最常见的报错就是某个依赖Could not resolve dependencies或者Failed to read artifact descriptor。这类问题九成出在 Maven 仓库配置上。Flink 的依赖里有一部分在中央仓库,有一部分在 Apache 的 snapshot 仓库,如果你的settings.xml里镜像配置把*全部指向了某个不全的镜像,就会拉不到。
排查思路是:先看报错里具体是哪个 groupId 和 artifactId 拉不到,然后手动去中央仓库搜一下这个包存不存在。如果存在,那就是镜像问题,把镜像的mirrorOf改成central而不是*,让非中央仓库的请求走原地址。如果不存在,那可能是版本号写错了,或者这个包只在 snapshot 仓库里有,需要额外加 repository 配置。
注意:改完
settings.xml后,记得在 IDEA 里 Reload Maven 工程,或者命令行加-U强制更新,否则 Maven 会用本地缓存的失败记录,还是报同样的错。
5.2 Scala 编译报错:版本和内存双杀
Scala 相关的报错主要有两类。一类是版本不匹配,报Scala signature xxx does not match或者object xxx is not a member of package,这种就是前面说的 Scala 版本没对上,检查scala.binary.version属性。另一类是内存不足,Scala 编译器(scalac)本身很吃内存,报java.lang.OutOfMemoryError: Java heap space或者GC overhead limit exceeded,这时候就要调大MAVEN_OPTS里的-Xmx。
还有一个比较隐蔽的坑:Scala 编译器和 JDK 版本的兼容性。比如 Scala 2.11 在 JDK 11 上编译会有问题,Scala 2.12.7 之前的版本对 JDK 11 支持也不完善。如果你用的是 JDK 11,Scala 版本最好在 2.12.8 以上。这个组合问题在官方文档里不一定写得很清楚,但实际编译时经常遇到。
5.3 Shade 插件报错:重定位冲突
Flink 大量使用 Shade 插件把依赖包重定位到org.apache.flink.shaded包下,避免和用户作业的依赖冲突。这个过程中偶尔会报Error creating shaded jar: Invalid signature或者duplicate entry之类的错误。这类问题通常是因为某个依赖 jar 带了签名文件(比如META-INF/*.SF),Shade 之后签名失效。
解决办法是在对应模块的 pom 里配置 shade 插件过滤掉签名文件:
<filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters>不过 Flink 官方工程一般已经处理好了这些,如果你是在自己二次开发的模块里遇到,才需要手动加。如果是官方模块报这个错,先检查是不是本地仓库里的某个依赖包损坏了,删掉对应目录重新下载试试。
5.4 编译过了但运行报错:类找不到或版本冲突
有时候编译能过,但你在 IDE 里跑一个测试类或者 main 方法时,报NoClassDefFoundError或者NoSuchMethodError。这通常是依赖冲突导致的——同一个类在 classpath 上有多个版本,JVM 加载了错误的那个。
排查这类问题,IDEA 自带的 Maven 依赖分析很好用。在pom.xml上右键,选 "Diagrams -> Show Dependencies",能看到整个依赖树,冲突的依赖会用红色标出来。找到冲突的包之后,用<exclusions>排除掉不需要的版本,或者用<dependencyManagement>统一版本。
我印象比较深的一次是调试flink-runtime里的某个类,运行时一直报NoSuchMethodError,查了半天发现是本地仓库里有一个旧版本的flink-core没被覆盖,classpath 上同时存在新旧两个版本。解决办法就是mvn clean install重新装一遍,确保本地仓库里的版本和源码一致。
6. 编译通过之后,怎么开始读和调
6.1 从测试用例入手,比读 main 代码高效
源码编译好之后,很多人第一反应是打开某个核心类从头读,比如StreamGraph、JobGraph、ExecutionGraph。但说实话,这些类动辄几千行,直接读很容易迷失。我的建议是先找对应的测试用例,从测试用例反推代码逻辑。
比如你想搞懂 Checkpoint 的触发流程,可以搜CheckpointCoordinatorTest,看它怎么构造场景、怎么断言结果。测试用例的好处是:它把输入、输出、预期行为都写清楚了,你顺着测试跑一遍,打断点跟一下,比干读代码快得多。而且 Flink 的测试覆盖做得不错,大部分核心逻辑都有对应的测试类。
6.2 打断点调试 MiniCluster
Flink 提供了一个MiniCluster,可以在本地 JVM 里模拟一个完整的集群,非常适合调试。你可以在测试代码里找到MiniClusterWithClientResource的用法,或者自己写一个简单的 main 方法:
Configuration conf = new Configuration(); MiniCluster miniCluster = new MiniCluster( MiniClusterResourceConfiguration.Builder .fromConfigurationAndNumberTaskManagers(conf, 1) .build()); miniCluster.start();然后在TaskExecutor、Task、StreamTask这些类的关键方法上打断点,跑一个简单的 WordCount 作业,就能看到整个执行链路。这种"跑起来看"的方式,比静态读代码理解深得多。我第一次跟完一个作业从提交到 Task 执行的完整流程后,才对 Flink 的运行时有了真正的体感。
6.3 读源码时带着问题,别漫无目的
最后分享一个我自己的习惯:每次读源码都带着一个具体问题。比如"作业提交后,JobManager 是怎么把 Task 分配到 TaskManager 的"、"Checkpoint 的对齐阶段具体做了什么"、"状态后端是怎么把状态写到 RocksDB 的"。带着问题读,你会主动去追调用链,遇到看不懂的地方也会想办法查资料,效率比漫无目的地翻代码高很多。
而且 Flink 的代码注释写得比较规范,很多关键类和方法上都有 Javadoc 说明设计意图。遇到看不懂的抽象,先看类注释和接口定义,往往能豁然开朗。如果注释也看不懂,那就去搜对应的 FLIP(Flink Improvement Proposal),FLIP 里通常有详细的设计文档和讨论,能帮你理解"为什么这么设计"。
编译 Flink 源码这件事,第一次做确实会踩不少坑,但一旦跑通,后面读代码、调试、改代码的体验会完全不一样。我自己的感受是,编译通过的那一刻,Flink 从一个"黑盒框架"变成了一个"可以拆开看的机器",这种掌控感是看多少篇博客都换不来的。