仓库:https://github.com/apache/flink
官方文档:https://nightlies.apache.org/flink/flink-docs-lts/
技术栈:Java 11 / Scala(少量兼容层)/ Akka-Pekko RPC / Calcite
解读版本:release-1.20.5(commit
0980485)解读视角:总架构师评审(架构 / 源码 / 生产 / 进阶)
第 01 篇:「架构鸟瞰与进程启动链路」------ JobManager / TaskManager 从零长出来的完整调用链
阅读本文你将了解:
- Flink 集群的"出生过程"分几层:
runClusterEntrypoint→startCluster→runCluster→ 组件onStart,每层职责边界在哪(flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:738/:225/:288)。DispatcherResourceManagerComponentFactory.create(...)一行调用,背后装配了 Dispatcher、ResourceManager、JobMaster、BlobServer 等十几个组件(flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:301)。- JobManager 与 TaskManager 的启动不对称:JM 靠
Dispatcher.onStart恢复历史作业(flink-runtime/src/main/java/org/apache/flink/runtime/dispatcher/Dispatcher.java:349),TM 靠TaskExecutor.onStart反向去"找" ResourceManager 注册(flink-runtime/src/main/java/org/apache/flink/runtime/taskexecutor/TaskExecutor.java:470)。- 一个 Task 真正跑起来前,要先在
doRun的自旋状态机里完成CREATED→DEPLOYING迁移并加载用户 JAR 类加载器(flink-runtime/src/main/java/org/apache/flink/runtime/taskmanager/Task.java:581/:637)。
01.0 一句话定性
Flink 的进程启动代码,本质是一套模板方法 + 组件工厂 的装配器:ClusterEntrypoint 用模板方法锁死启动骨架(安全上下文 → 服务初始化 → 组件装配 → 退出码),把"具体装配哪些组件"这个可变点下放给子类实现的 createDispatcherResourceManagerComponentFactory;而真正干活的组件(Dispatcher、ResourceManager、TaskExecutor)全是 RpcEndpoint 子类,靠 onStart 钩子在自己的 RPC 主线程里各自就位。理解这条链,就理解了"为什么 Flink 能同时支持 standalone / YARN / K8s 三种部署模式却共享同一套启动骨架"。
01.1 架构视角(Architect)
01.1.1 组件拓扑:控制面与数据面的分工
Flink 运行时由两类进程组成:一个(或 HA 下多个)JobManager 进程承载控制面,多个 TaskManager 进程承载数据面。JobManager 内部再细分四个协作组件,各自是一个独立的 RpcEndpoint:

图示讲解 :这张类图回答"JobManager 进程内部,谁负责把组件攒到一起"。
ClusterEntrypoint只持有DispatcherResourceManagerComponentFactory,通过它的create(...)一次拿到打包好的DispatcherResourceManagerComponent(内含 Dispatcher、ResourceManager、JobMaster 三个核心组件,flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:301-316传入了 13 个依赖:ioExecutor、commonRpcService、haServices、blobServer、heartbeatServices、metricRegistry 等)。这种"工厂返回一个聚合组件"的设计,让ClusterEntrypoint不必关心组件间错综复杂的构造依赖,也解释了为何 JM 启动参数如此之多。箭头Dispatcher → JobMaster的submitJob表示作业提交入口,JobMaster ..> TaskExecutor的虚线则表示跨进程的 RPC 部署。
01.1.2 启动骨架是"模板方法",装配细节是"抽象方法"
ClusterEntrypoint 是 JM 进程的骨架类,它定义了一个不可覆写的启动顺序,把可变点留给子类。这个设计的价值在于:YARN 的 YarnJobClusterEntrypoint、K8s 的 KubernetesSessionClusterEntrypoint、本地 StandaloneSessionClusterEntrypoint 走的是同一套 startCluster 骨架,只是各自实现的 createDispatcherResourceManagerComponentFactory 返回不同的组件工厂。这是 Flink 支持多种部署模式却不大改启动代码的根本原因------改动点被收敛到工厂这一层。
01.2 源码侦探(Source Sleuth)
01.2.1 第一层:静态启动壳 runClusterEntrypoint
进程的 main 方法最终都会收敛到 ClusterEntrypoint.runClusterEntrypoint 这个静态方法。它的职责只有一个:把 startCluster 抛出的异常翻译成进程退出码,并阻塞等待集群终止:
java
// flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:738
public static void runClusterEntrypoint(ClusterEntrypoint clusterEntrypoint) {
final String clusterEntrypointName = clusterEntrypoint.getClass().getSimpleName();
try {
clusterEntrypoint.startCluster();
} catch (ClusterEntrypointException e) {
LOG.error(
String.format("Could not start cluster entrypoint %s.", clusterEntrypointName), e);
System.exit(STARTUP_FAILURE_RETURN_CODE); // 启动失败:非零退出码
}
int returnCode;
Throwable throwable = null;
try {
returnCode = clusterEntrypoint.getTerminationFuture().get().processExitCode();
} catch (Throwable e) {
throwable = ExceptionUtils.stripExecutionException(e);
returnCode = RUNTIME_FAILURE_RETURN_CODE; // 运行期失败:另一套退出码
}
LOG.info("Terminating cluster entrypoint process {} with exit code {}.", ...);
System.exit(returnCode);
}
关键源码事实 :
runClusterEntrypoint把失败分成两类------STARTUP_FAILURE_RETURN_CODE(startCluster里就炸了,集群根本没起来)和RUNTIME_FAILURE_RETURN_CODE(起来之后才炸),两者的退出码不同,flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:747与:757分别对应。这个区分对容器环境(YARN/K8s)意义重大:容器编排器正是靠退出码判断该"原地重启"还是"标记应用失败"。这是"进程生命周期被显式编码为退出码"的典型范例。
01.2.2 第二层:startCluster 的安全上下文与服务初始化
startCluster 是骨架的第一段,它做的第一件事不是启动组件,而是先建立安全边界:
java
// flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:225
public void startCluster() throws ClusterEntrypointException {
LOG.info("Starting {}.", getClass().getSimpleName());
try {
FlinkSecurityManager.setFromConfiguration(configuration); // :229 安全上下文
PluginManager pluginManager =
PluginUtils.createPluginManagerFromRootFolder(configuration);
configureFileSystems(configuration, pluginManager); // :232 文件系统插件
SecurityContext securityContext = installSecurityContext(configuration); // :234
ClusterEntrypointUtils.configureUncaughtExceptionHandler(configuration);
securityContext.runSecured(
(Callable<Void>) () -> { runCluster(configuration, pluginManager); return null; });
} catch (Throwable t) {
// 清理半初始化状态,转成 ClusterEntrypointException 抛出
}
}
关键源码事实 :
startCluster的骨架顺序是"安全 → 插件 → 文件系统 → 服务",其中FlinkSecurityManager.setFromConfiguration(flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:229)发生在一切之前 ,因为后续加载的文件系统插件、用户代码都需要在安全沙箱里运行。installSecurityContext(flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:234)返回的SecurityContext再用runSecured把真正的runCluster包进去------这个"先装护栏、再跑业务"的顺序,是启动代码里最容易被忽略、却决定安全边界是否真正生效的细节。
01.2.3 第三层:runCluster 的组件装配
runCluster 是启动的真正核心,它完成两件事:先 initializeServices 初始化一批跨组件共享的服务(RPC 服务、HA 服务、BlobServer、心跳服务、指标注册表),再调用工厂把组件攒起来:
java
// flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:288
private void runCluster(Configuration configuration, PluginManager pluginManager) throws Exception {
synchronized (lock) {
initializeServices(configuration, pluginManager); // :291 共享服务
// 把本机地址写回配置,供后续组件使用
configuration.set(JobManagerOptions.ADDRESS, commonRpcService.getAddress()); // :294
configuration.set(JobManagerOptions.PORT, commonRpcService.getPort()); // :295
final DispatcherResourceManagerComponentFactory
dispatcherResourceManagerComponentFactory =
createDispatcherResourceManagerComponentFactory(configuration); // :299
clusterComponent = dispatcherResourceManagerComponentFactory.create( // :301 装配
configuration, resourceId.unwrap(), ioExecutor, commonRpcService,
haServices, blobServer, heartbeatServices, delegationTokenManager,
metricRegistry, executionGraphInfoStore,
new RpcMetricQueryServiceRetriever(...), failureEnrichers, this);
clusterComponent.getShutDownFuture().whenComplete(...); // :318 挂接关停回调
}
}
关键源码事实 :
initializeServices(flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:291)与组件装配(:301)被刻意拆成两步,因为它们有严格的时序依赖------组件构造需要commonRpcService、haServices、blobServer这些共享服务先就绪。而createDispatcherResourceManagerComponentFactory(:299)是ClusterEntrypoint唯一的抽象方法,正是它把"装配哪些组件"这个可变点隔离出来:standalone、YARN、K8s 三类 Entrypoint 的差异,全部沉淀在这个工厂方法的实现里。这是整个启动链路的"分水岭"。
01.2.4 JM 侧:Dispatcher.onStart 的恢复语义
组件装配完成后,RPC 系统会回调每个 RpcEndpoint 的 onStart。Dispatcher 的 onStart 有一段很关键的逻辑------恢复上次未完成的作业:
java
// flink-runtime/src/main/java/org/apache/flink/runtime/dispatcher/Dispatcher.java:349
public void onStart() throws Exception {
try {
startDispatcherServices(); // :351 注册指标
} catch (Throwable t) {
onFatalError(exception); throw exception;
}
startCleanupRetries(); // :360 清理残留 JobResult
startRecoveredJobs(); // :361 恢复历史作业
this.dispatcherBootstrap = this.dispatcherBootstrapFactory.create(...); // :363
}
关键源码事实 :
startRecoveredJobs(flink-runtime/src/main/java/org/apache/flink/runtime/dispatcher/Dispatcher.java:361)的存在,解释了 Flink 的 HA 恢复入口为何在 Dispatcher 而非 JobMaster------Dispatcher 是唯一在启动时就能拿到executionGraphInfoStore(持久化的作业图存储)的组件。它会遍历recoveredJobs列表,逐个runRecoveredJob把上次崩溃前还没跑完的作业重新拉起来(flink-runtime/src/main/java/org/apache/flink/runtime/dispatcher/Dispatcher.java:397-401)。这一行是"作业级容错"的起点,与后面第 06 篇的 Checkpoint 状态恢复是两回事------前者恢复"作业还在跑",后者恢复"算到哪一步了"。
01.2.5 TM 侧:TaskExecutor.onStart 的"反向寻址"
与 JobManager 不同,TaskManager 启动后不知道 ResourceManager 在哪,所以它的 onStart 是先启动一个 leader 监听器,等着发现 ResourceManager 再主动注册:
java
// flink-runtime/src/main/java/org/apache/flink/runtime/taskexecutor/TaskExecutor.java:470
public void onStart() throws Exception {
try {
startTaskExecutorServices();
} catch (Throwable t) {
onFatalError(...); throw exception;
}
startRegistrationTimeout(); // :481 注册超时兜底
}
// :484
private void startTaskExecutorServices() throws Exception {
resourceManagerLeaderRetriever.start(new ResourceManagerLeaderListener()); // :487 找 RM
taskSlotTable.start(new SlotActionsImpl(), getMainThreadExecutor()); // :490 启动 Slot 表
jobLeaderService.start(getAddress(), getRpcService(), haServices, ...); // :493 Job Leader
fileCache = new FileCache(...); // :496 文件缓存
tryLoadLocalAllocationSnapshots(); // :501 恢复本地 Slot 分配
}
关键源码事实 :TM 启动的第一个动作是
resourceManagerLeaderRetriever.start(...)(flink-runtime/src/main/java/org/apache/flink/runtime/taskexecutor/TaskExecutor.java:487)------这揭示了 JM 与 TM 启动的根本不对称 :JM 是"中心",组件靠工厂直接注入就位;TM 是"边缘",必须通过 leader 发现机制去"找"ResourceManager,找到后由ResourceManagerLeaderListener触发注册。startRegistrationTimeout(:481)则是兜底:如果一直找不到 RM,超时后 TM 会转入失败处理,避免进程永远空转。这个"反向寻址 + 超时兜底"的模式,是分布式系统里边缘节点启动的通用范式。
01.2.6 终点:Task.run 的状态机与用户代码加载
当一个 Task 被部署到 TM 的 Slot 上,最终会走到 Task.run。它的核心是一个自旋状态机 + 用户代码类加载:
java
// flink-runtime/src/main/java/org/apache/flink/runtime/taskmanager/Task.java:581
private void doRun() {
while (true) { // :585 自旋状态机
ExecutionState current = this.executionState;
if (current == ExecutionState.CREATED) {
if (transitionState(ExecutionState.CREATED, ExecutionState.DEPLOYING)) break;
} else if (current == ExecutionState.FAILED) {
notifyFinalState(); metrics.close(); return;
} else if (current == ExecutionState.CANCELING) {
if (transitionState(ExecutionState.CANCELING, ExecutionState.CANCELED)) {
notifyFinalState(); metrics.close(); return;
}
} else {
throw new IllegalStateException("Invalid state for beginning of operation ...");
}
}
// ... 后续获取用户代码类加载器
userCodeClassLoader = createUserCodeClassloader(); // :637 加载用户 JAR
}
关键源码事实 :
doRun里的while(true)自旋(flink-runtime/src/main/java/org/apache/flink/runtime/taskmanager/Task.java:585)不是为了忙等,而是用 CAS 式的transitionState处理"部署指令到达时状态可能已经被外部改掉"的竞态------比如 Task 刚部署就收到取消信号。这个循环只在CREATED→DEPLOYING迁移成功时break,其余情况要么提前终止、要么抛异常。紧接着的createUserCodeClassloader(flink-runtime/src/main/java/org/apache/flink/runtime/taskmanager/Task.java:637)是整条链路的"用户代码边界":从这里开始,用户 JAR 里的StreamTask/算子逻辑才被加载进 Flink 的执行环境。读懂doRun的开头几行,就读懂了 Flink 执行模型的最内层。
01.3 生产实践(Production)
01.3.1 启动时序:一张图看懂全链路
把上面六段串起来,就是一次完整的进程启动时序:

图示讲解 :这张时序图回答"一次进程启动,控制面与数据面各自经历了什么"。上半部分是控制面:
main调runClusterEntrypoint(flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:738),进入startCluster先装安全上下文(:225),再进runCluster初始化共享服务并写回主机信息(:288/:294),随后通过工厂create(:301)一次装配出 Dispatcher 等组件。下半部分是两条并行的onStart:Dispatcher 走startRecoveredJobs恢复历史作业(flink-runtime/src/main/java/org/apache/flink/runtime/dispatcher/Dispatcher.java:361),TaskExecutor 走 leader 发现去找 ResourceManager 注册(flink-runtime/src/main/java/org/apache/flink/runtime/taskexecutor/TaskExecutor.java:487)。注意main在runClusterEntrypoint里是阻塞 的(getTerminationFuture().get()),这意味着 JM 主线程启动后并不会退出,而是等待集群终止信号------这是与"一次性批处理进程"最本质的差别。
01.3.2 启动相关的关键配置
| 配置项 | 作用 | 源码锚点 |
|---|---|---|
jobmanager.rpc.address / jobmanager.rpc.port |
JM 对外 RPC 地址,runCluster 会写回配置 |
flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:294-295 |
jobmanager.memory.process.size |
JM 进程总内存,影响 JVM 堆与非堆划分 | JobManagerOptions |
rest.address / rest.port |
Web UI/REST 端点 | RestOptions |
high-availability.type |
决定 haServices 是 ZooKeeper/K8s/Standalone |
HighAvailabilityOptions |
taskmanager.registration.timeout |
TM 注册 RM 的超时,对应 startRegistrationTimeout |
flink-runtime/src/main/java/org/apache/flink/runtime/taskexecutor/TaskExecutor.java:481 |
01.3.3 启动失败的三个高频坑
- 端口冲突 :
runCluster里commonRpcService若绑定失败,startCluster会捕获Throwable并清理半初始化状态后抛ClusterEntrypointException(flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:244-266),最终由runClusterEntrypoint转成非零退出码(:747)。排查日志里找Could not start cluster entrypoint关键字即可定位。 - HA 服务连不上 :若配置了 ZooKeeper HA 但 ZK 不可达,
initializeServices阶段就会失败------这是startCluster里"服务初始化"和"组件装配"被拆开的原因,日志会明确停在Initializing cluster services(flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:345)之后。 - TM 注册超时 :TM 起来后若一直找不到 RM,
startRegistrationTimeout(flink-runtime/src/main/java/org/apache/flink/runtime/taskexecutor/TaskExecutor.java:481)会触发,进程进入失败处理。这是 K8s 环境下 Service 未就绪、或网络分区时最常见的症状。
01.4 进阶向导(Advanced)
01.4.1 扩展点:如何加一种新的部署模式
Flink 支持新部署模式(如自研容器平台)的最小改动,就是继承 ClusterEntrypoint 并实现 createDispatcherResourceManagerComponentFactory 。这条扩展路径之所以干净,正是因为 ClusterEntrypoint 把启动骨架锁死、把装配细节隔离成单一抽象方法(flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:299)。对比之下,Spark 的部署模式扩展要动 SparkContext/SchedulerBackend 多处,扩展成本更高------这是 Flink 启动层设计优于 Spark 的一个具体证据。
01.4.2 与 Spark 启动模型的对比
| 维度 | Flink | Spark |
|---|---|---|
| 进程入口抽象 | ClusterEntrypoint 模板方法骨架 |
SparkSubmit + DriverWrapper 脚本驱动 |
| 组件装配 | 工厂 create(...) 返回聚合组件 |
Driver 内按需 new 各组件 |
| 作业恢复入口 | Dispatcher.onStart 的 startRecoveredJobs |
Driver 重新提交、依赖外部调度器 |
| 边缘节点启动 | TM 反向发现 RM 再注册(:487) |
Executor 反向注册 CoarseGrainedExecutorBackend |
| 退出码语义 | 显式区分启动失败/运行失败(:747/:757) |
依赖 System.exit + 容器编排 |
01.4.3 一个容易被误读的点
很多人以为 startCluster 返回就代表"集群可用了"。实际上 startCluster 只是完成了组件装配 ,真正的"就绪"要等到各组件 onStart 完成------尤其 Dispatcher 要跑完 startRecoveredJobs(flink-runtime/src/main/java/org/apache/flink/runtime/dispatcher/Dispatcher.java:361)才算恢复完历史作业。因此容器健康探针若只 ping 进程存活,会过早把"还在恢复作业"的 JM 判为就绪 ,这是生产里作业重复拉起或状态错乱的常见根源。正确做法是探针打 REST 的 /overview 端点,等待它返回 running。
01.5 小结与下一篇
本篇打穿了 Flink 进程启动的完整调用链:runClusterEntrypoint(flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:738)负责退出码语义,startCluster(:225)负责安全边界,runCluster(:288)负责组件装配,Dispatcher.onStart(flink-runtime/src/main/java/org/apache/flink/runtime/dispatcher/Dispatcher.java:349)与 TaskExecutor.onStart(flink-runtime/src/main/java/org/apache/flink/runtime/taskexecutor/TaskExecutor.java:470)分别完成 JM 的作业恢复与 TM 的反向注册,最终落到 Task.doRun(flink-runtime/src/main/java/org/apache/flink/runtime/taskmanager/Task.java:581)的状态机。这条链是理解后续所有篇章的地基------因为调度、状态、检查点、网络,全部运行在由这条链启动起来的组件之上。
下一篇进入《有状态流处理范式与时间语义》,切入点从"进程怎么起来"转到"数据怎么被有状态地处理":我们将从 AbstractKeyedStateBackend 的 KeyedState 访问链路出发,讲清 Flink 为何能把 Exactly-Once 语义建立在状态快照之上,以及 Watermark 如何在算子间传播(对应本系列的 02 篇)。