第 01 篇:「架构鸟瞰与进程启动链路」—— JobManager / TaskManager 从零长出来的完整调用链

仓库: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 从零长出来的完整调用链

阅读本文你将了解:

  1. Flink 集群的"出生过程"分几层:runClusterEntrypointstartClusterrunCluster → 组件 onStart,每层职责边界在哪(flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:738 / :225 / :288)。
  2. DispatcherResourceManagerComponentFactory.create(...) 一行调用,背后装配了 Dispatcher、ResourceManager、JobMaster、BlobServer 等十几个组件(flink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:301)。
  3. 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)。
  4. 一个 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 → JobMastersubmitJob 表示作业提交入口,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_CODEstartCluster 里就炸了,集群根本没起来)和 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.setFromConfigurationflink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:229)发生在一切之前 ,因为后续加载的文件系统插件、用户代码都需要在安全沙箱里运行。installSecurityContextflink-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 挂接关停回调
    }
}

关键源码事实initializeServicesflink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:291)与组件装配(:301)被刻意拆成两步,因为它们有严格的时序依赖------组件构造需要 commonRpcServicehaServicesblobServer 这些共享服务先就绪。而 createDispatcherResourceManagerComponentFactory:299)是 ClusterEntrypoint 唯一的抽象方法,正是它把"装配哪些组件"这个可变点隔离出来:standalone、YARN、K8s 三类 Entrypoint 的差异,全部沉淀在这个工厂方法的实现里。这是整个启动链路的"分水岭"。

01.2.4 JM 侧:Dispatcher.onStart 的恢复语义

组件装配完成后,RPC 系统会回调每个 RpcEndpointonStart。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
}

关键源码事实startRecoveredJobsflink-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,其余情况要么提前终止、要么抛异常。紧接着的 createUserCodeClassloaderflink-runtime/src/main/java/org/apache/flink/runtime/taskmanager/Task.java:637)是整条链路的"用户代码边界":从这里开始,用户 JAR 里的 StreamTask/算子逻辑才被加载进 Flink 的执行环境。读懂 doRun 的开头几行,就读懂了 Flink 执行模型的最内层

01.3 生产实践(Production)

01.3.1 启动时序:一张图看懂全链路

把上面六段串起来,就是一次完整的进程启动时序:

图示讲解 :这张时序图回答"一次进程启动,控制面与数据面各自经历了什么"。上半部分是控制面:mainrunClusterEntrypointflink-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)。注意 mainrunClusterEntrypoint 里是阻塞 的(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 启动失败的三个高频坑

  1. 端口冲突runClustercommonRpcService 若绑定失败,startCluster 会捕获 Throwable 并清理半初始化状态后抛 ClusterEntrypointExceptionflink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:244-266),最终由 runClusterEntrypoint 转成非零退出码(:747)。排查日志里找 Could not start cluster entrypoint 关键字即可定位。
  2. HA 服务连不上 :若配置了 ZooKeeper HA 但 ZK 不可达,initializeServices 阶段就会失败------这是 startCluster 里"服务初始化"和"组件装配"被拆开的原因,日志会明确停在 Initializing cluster servicesflink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:345)之后。
  3. TM 注册超时 :TM 起来后若一直找不到 RM,startRegistrationTimeoutflink-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.onStartstartRecoveredJobs Driver 重新提交、依赖外部调度器
边缘节点启动 TM 反向发现 RM 再注册(:487 Executor 反向注册 CoarseGrainedExecutorBackend
退出码语义 显式区分启动失败/运行失败(:747/:757 依赖 System.exit + 容器编排

01.4.3 一个容易被误读的点

很多人以为 startCluster 返回就代表"集群可用了"。实际上 startCluster 只是完成了组件装配 ,真正的"就绪"要等到各组件 onStart 完成------尤其 Dispatcher 要跑完 startRecoveredJobsflink-runtime/src/main/java/org/apache/flink/runtime/dispatcher/Dispatcher.java:361)才算恢复完历史作业。因此容器健康探针若只 ping 进程存活,会过早把"还在恢复作业"的 JM 判为就绪 ,这是生产里作业重复拉起或状态错乱的常见根源。正确做法是探针打 REST 的 /overview 端点,等待它返回 running

01.5 小结与下一篇

本篇打穿了 Flink 进程启动的完整调用链:runClusterEntrypointflink-runtime/src/main/java/org/apache/flink/runtime/entrypoint/ClusterEntrypoint.java:738)负责退出码语义,startCluster:225)负责安全边界,runCluster:288)负责组件装配,Dispatcher.onStartflink-runtime/src/main/java/org/apache/flink/runtime/dispatcher/Dispatcher.java:349)与 TaskExecutor.onStartflink-runtime/src/main/java/org/apache/flink/runtime/taskexecutor/TaskExecutor.java:470)分别完成 JM 的作业恢复与 TM 的反向注册,最终落到 Task.doRunflink-runtime/src/main/java/org/apache/flink/runtime/taskmanager/Task.java:581)的状态机。这条链是理解后续所有篇章的地基------因为调度、状态、检查点、网络,全部运行在由这条链启动起来的组件之上

下一篇进入《有状态流处理范式与时间语义》,切入点从"进程怎么起来"转到"数据怎么被有状态地处理":我们将从 AbstractKeyedStateBackend 的 KeyedState 访问链路出发,讲清 Flink 为何能把 Exactly-Once 语义建立在状态快照之上,以及 Watermark 如何在算子间传播(对应本系列的 02 篇)。

相关推荐
CaseyWei1 小时前
Harness 架构 Multi‑Agent(多智能体)完整深度解析
人工智能·ai·架构·harness
源代码•宸3 小时前
前置准备:定时微服务背景和现状
开发语言·经验分享·后端·微服务·云原生·架构·golang
天远数科3 小时前
零信任架构实战:基于天远身份证OCR构建自动化高并发移动支付网关
人工智能·架构·自动化·ocr
ThornArmor4 小时前
《沼泽巨鳄驯养手册》
架构
YWamy4 小时前
远程医疗音视频系统技术选型与全场景落地架构
架构·音视频
名字还没想好☜4 小时前
Docker buildx 多架构镜像实战:一次构建 amd64+arm64,告别 exec format error
运维·docker·eureka·架构·kubernetes
Dawson Zhu5 小时前
多Agent系统共享记忆架构:从存储范式到分布式共识的技术剖析
人工智能·语言模型·架构·aigc·agi
lincats6 小时前
大白话讲解Addy Osmani 的循环工程Loop Engineering工作流
人工智能·驱动开发·架构·prompt·状态模式·知识图谱
starzy19906 小时前
Flink基础之有界与无界流详解:批流一体的底层逻辑
大数据·flink