Flink 任务调度 Graph 源码深度剖析:从 StreamGraph 到 ExecutionGraph 的三层演进

前面讲了 SlotManager 分配 Slot 和 SlotPool 申请资源的源码实现,这篇聚焦 Flink 任务调度 Graph 的源码实现。Flink 任务从用户代码到实际执行,经历了三层 Graph 的演进:StreamGraph(流图)→ JobGraph(作业图)→ ExecutionGraph(执行图)。你有没有想过,用户写的 DataStream API 是如何变成可执行的任务的?算子链优化是如何合并算子的?并行度是如何展开的?调度器是如何调度 Execution 的?这篇从源码级别跟踪 Flink 任务调度 Graph 的完整演进链路。


一、任务调度 Graph 整体架构

下面这张图是任务调度 Graph 整体架构,包括三层 Graph 层级关系、调度关系和核心类。

Flink 任务调度 Graph 分为三层,从逻辑到物理逐步演进:

  • StreamGraph(流图):用户 API 构建的逻辑执行图,描述算子拓扑。包含 StreamNode(算子节点)和 StreamEdge(数据流边),从 Transformation 列表构建。
  • JobGraph(作业图):提交到 JobManager 的执行图,算子链合并后的物理图。包含 JobVertex(作业顶点,对应算子链)、JobEdge(作业边)、IntermediateDataSet(中间数据集)。
  • ExecutionGraph(执行图):JobMaster 内部调度图,按并行度展开的可执行图。包含 ExecutionJobVertex(执行作业顶点)、ExecutionVertex(执行顶点,对应并行子任务)、Execution(执行实例,具体执行)。

三层 Graph 的演进关系:StreamGraph 经过算子链优化(Operator Chain)生成 JobGraph,JobGraph 经过并行度展开(Parallelism Expansion)生成 ExecutionGraph。ExecutionGraph 构建完成后,SchedulerNG 调度 Execution,通过 SlotPool 申请 Slot,SlotManager 分配 Slot,最终部署 Task 到 TaskManager 执行。


二、JobGraph 构建流程

下面这张图是 JobGraph 构建流程,包括5步构建流程、算子链优化对比、组件关系和核心类。

JobGraph 构建分为5步:

第1步:构建 StreamGraph。用户调用 env.execute() 时,StreamExecutionEnvironment 通过 StreamGraphGenerator 从 Transformation 列表构建 StreamGraph。StreamGraph 包含 StreamNode(算子节点)和 StreamEdge(数据流边),描述用户定义的逻辑执行拓扑。每个 Transformation 对应一个或多个 StreamNode,Transformation 之间的依赖关系对应 StreamEdge。

第2步:算子链优化(Operator Chain)。JobGraphGenerator 遍历 StreamGraph,判断哪些 StreamNode 可以链接成算子链。链接条件:上下游并行度相同、数据分发模式为 forward、没有禁用算子链(disableChaining)、没有 slot sharing group 隔离。可链接的算子合并为一个 JobVertex,减少线程切换和数据序列化开销。

第3步:创建 JobVertex。每个算子链(或独立算子)创建一个 JobVertex,包含算子配置、并行度、输入输出配置、SlotSharingGroup 等信息。JobVertex 是 JobGraph 的基本调度单元,对应一个物理执行任务。JobVertex 包含 JobVertexInput(输入配置)和 JobVertexOutput(输出配置)。

第4步:创建 JobEdge 和 IntermediateDataSet。JobVertex 之间通过 JobEdge 连接,JobEdge 指向源 JobVertex 的 IntermediateDataSet(中间数据集)。IntermediateDataSet 表示一个 JobVertex 的输出结果,包含数据分区策略(forward/rebalance/keyBy/broadcast 等)。JobEdge 定义了数据从上游 JobVertex 到下游 JobVertex 的传输方式。

第5步:生成 JobGraph 并提交。所有 JobVertex、JobEdge、IntermediateDataSet 创建完成后,组装成完整的 JobGraph。JobGraph 包含作业 ID、作业名称、配置信息、所有 JobVertex 和依赖关系。通过 Client 提交到 JobManager(Dispatcher),Dispatcher 启动 JobMaster,JobMaster 将 JobGraph 构建为 ExecutionGraph 进行调度执行。

算子链优化的价值

算子链优化是 Flink 重要的性能优化手段。优化前,Source、Map、Filter、Sink 四个算子各自独立,需要4个线程,3次序列化和网络传输,性能开销大。优化后,四个算子合并为一个算子链,只需要1个线程,0次序列化,数据在内存中直接传递,性能显著提升。

算子链的链接条件需要同时满足:

  • 上下游并行度相同
  • 数据分发模式为 forward(一对一)
  • 没有调用 disableChaining() 禁用算子链
  • 上下游属于同一个 SlotSharingGroup

三、ExecutionGraph 构建与调度

下面这张图是 ExecutionGraph 构建与调度,包括6步构建流程、并行度展开、Execution 状态机和配置最佳实践。

ExecutionGraph 构建与调度分为6步:

第1步:接收 JobGraph。Dispatcher 启动 JobMaster,JobMaster 接收 Client 提交的 JobGraph。JobGraph 包含所有 JobVertex、JobEdge、IntermediateDataSet 和作业配置。JobMaster 创建 ExecutionGraphBuilder,准备构建 ExecutionGraph。

第2步:创建 ExecutionJobVertex。遍历 JobGraph 中的每个 JobVertex,创建对应的 ExecutionJobVertex。ExecutionJobVertex 是 ExecutionGraph 的基本调度单元,包含并行度、算子配置、输入输出配置、SlotSharingGroup 等信息。一个 ExecutionJobVertex 对应一个 JobVertex。

第3步:并行度展开创建 ExecutionVertex。每个 ExecutionJobVertex 按并行度展开,创建 parallelism 个 ExecutionVertex。ExecutionVertex 表示一个并行子任务,包含子任务索引、输入输出配置、分配的 Slot 信息。ExecutionVertex 是实际调度和执行的基本单元,对应一个 TaskManager 上的一个 Task。

第4步:连接 ExecutionEdge。根据 JobEdge 和 IntermediateDataSet,创建 ExecutionEdge 连接上下游 ExecutionVertex。ExecutionEdge 定义了数据从上游 ExecutionVertex 到下游 ExecutionVertex 的传输方式(forward/rebalance/keyBy/broadcast 等)。ExecutionGraph 的拓扑结构完整构建,所有依赖关系建立。

第5步:SchedulerNG 调度 Execution。ExecutionGraph 构建完成后,SchedulerNG(DefaultScheduler)开始调度。每个 ExecutionVertex 创建一个 Execution 实例,表示一次具体的任务执行。SchedulerNG 按拓扑顺序调度 Execution,先调度 Source,再调度下游算子。调度时调用 SlotPool.requestSlot() 申请 Slot。

第6步:部署 Task 到 TaskManager。Slot 分配成功后,Execution 通过 TaskExecutorGateway 发送 submitTask 请求,将 Task 部署到对应的 TaskManager。TaskManager 创建 Task 实例,分配到 Slot 上启动执行。Task 执行过程中,ExecutionGraph 跟踪每个 Execution 的状态。

并行度展开机制

并行度展开是 ExecutionGraph 的核心机制。一个 ExecutionJobVertex(对应 JobVertex,并行度=4)按并行度展开为4个 ExecutionVertex(子任务索引0-3),每个 ExecutionVertex 每次执行创建一个 Execution 实例。ExecutionVertex 是静态的(对应一个并行子任务),Execution 是动态的(对应一次具体的执行,可能因故障重试而多次创建)。

Execution 状态机

Execution 有7种核心状态,状态转换严格按顺序执行:

  • CREATED:已创建,Execution 实例创建完成
  • SCHEDULED:已调度,SchedulerNG 已调度,等待 Slot 分配
  • DEPLOYING:部署中,Slot 已分配,正在向 TaskManager 部署 Task
  • RUNNING:运行中,Task 已启动,正在执行
  • FINISHED:已完成,Task 执行成功完成
  • FAILED:失败,Task 执行失败
  • CANCELED:已取消,Task 被取消

四、核心类源码剖析

4.1 StreamGraph 流图

java 复制代码
public class StreamGraph {
    private final String jobName;
    private final Map<Integer, StreamNode> streamNodes;
    private final Set<StreamEdge> streamEdges;
    private final Map<Integer, List<StreamEdge>> inEdges;
    private final Map<Integer, List<StreamEdge>> outEdges;

    public void addOperator(
            Integer vertexID,
            String slotSharingGroup,
            String coLocationGroup,
            StreamOperatorFactory operatorFactory,
            TypeInformation inTypeInfo,
            TypeInformation outTypeInfo,
            String operatorName) {
        // 创建 StreamNode 并加入映射
        StreamNode vertex = new StreamNode(
            vertexID, slotSharingGroup, coLocationGroup,
            operatorFactory, operatorName, inTypeInfo, outTypeInfo);
        streamNodes.put(vertexID, vertex);
    }

    public void addEdge(Integer sourceId, Integer targetId, int typeNumber, String outputName) {
        // 创建 StreamEdge 连接两个 StreamNode
        StreamNode source = streamNodes.get(sourceId);
        StreamNode target = streamNodes.get(targetId);
        StreamEdge edge = new StreamEdge(source, target, typeNumber, outputName);
        streamEdges.add(edge);
        outEdges.get(sourceId).add(edge);
        inEdges.get(targetId).add(edge);
    }

    public StreamNode getStreamNode(Integer vertexID) {
        return streamNodes.get(vertexID);
    }
}

public class StreamNode {
    private final int id;
    private final StreamOperatorFactory operatorFactory;
    private final String operatorName;
    private int parallelism;
    private String slotSharingGroup;
    private List<StreamEdge> inEdges;
    private List<StreamEdge> outEdges;
}

StreamGraph 是逻辑执行图,包含 StreamNode(算子节点)和 StreamEdge(数据流边)。addOperator 方法创建 StreamNode 并加入映射,addEdge 方法创建 StreamEdge 连接两个 StreamNode。StreamNode 包含算子工厂、并行度、SlotSharingGroup、输入输出边等信息。

4.2 JobGraph 作业图

java 复制代码
public class JobGraph {
    private final JobID jobID;
    private String name;
    private final Map<JobVertexID, JobVertex> taskVertices;
    private final List<JobEdge> jobEdges;

    public void addVertex(JobVertex vertex) {
        // 添加 JobVertex 到作业图
        taskVertices.put(vertex.getID(), vertex);
    }

    public void addEdge(JobEdge edge) {
        // 添加 JobEdge 到作业图
        jobEdges.add(edge);
    }

    public JobVertex findVertexByID(JobVertexID id) {
        return taskVertices.get(id);
    }
}

public class JobVertex {
    private final JobVertexID id;
    private String name;
    private int parallelism;
    private StreamConfig configuration;
    private String slotSharingGroup;
    private List<JobEdge> inputs;
    private List<IntermediateDataSet> results;

    public void connectNewDataSetAsInput(
            JobVertex input,
            DistributionPattern distributionPattern,
            boolean isBroadcast) {
        // 创建 IntermediateDataSet 和 JobEdge
        IntermediateDataSet dataSet = input.createAndAddResultDataSet(
            this, distributionPattern);
        JobEdge edge = new JobEdge(dataSet, this, distributionPattern);
        inputs.add(edge);
    }

    public IntermediateDataSet createAndAddResultDataSet(
            JobVertex consumer,
            DistributionPattern pattern) {
        // 创建中间数据集
        IntermediateDataSet dataSet = new IntermediateDataSet(
            new IntermediateDataSetID(), pattern, this);
        results.add(dataSet);
        return dataSet;
    }
}

public class IntermediateDataSet {
    private final IntermediateDataSetID id;
    private final DistributionPattern distributionPattern;
    private final JobVertex producer;
    private final List<JobEdge> consumers;
}

public class JobEdge {
    private final IntermediateDataSet source;
    private final JobVertex target;
    private final DistributionPattern distributionPattern;
}

JobGraph 是提交到 JobManager 的物理执行图,包含 JobVertex(作业顶点)、JobEdge(作业边)、IntermediateDataSet(中间数据集)。JobVertex.connectNewDataSetAsInput 方法创建 IntermediateDataSet 和 JobEdge,连接上下游 JobVertex。IntermediateDataSet 表示一个 JobVertex 的输出结果,包含数据分区策略。JobEdge 定义了数据传输方式。

4.3 ExecutionGraph 执行图

java 复制代码
public class ExecutionGraph {
    private final JobID jobID;
    private String jobName;
    private final Map<JobVertexID, ExecutionJobVertex> tasks;
    private final List<ExecutionVertex> allVertices;
    private SchedulerNG scheduler;

    public void attachJobGraph(List<JobVertex> topologiallySorted) throws Exception {
        // 遍历 JobVertex,创建 ExecutionJobVertex
        for (JobVertex jobVertex : topologiallySorted) {
            ExecutionJobVertex ejv = new ExecutionJobVertex(
                this, jobVertex, jobVertex.getParallelism());
            tasks.put(jobVertex.getID(), ejv);
        }

        // 连接 ExecutionEdge
        for (ExecutionJobVertex ejv : tasks.values()) {
            ejv.connectToPredecessors();
        }
    }
}

public class ExecutionJobVertex {
    private final ExecutionGraph graph;
    private final JobVertex jobVertex;
    private final int parallelism;
    private final ExecutionVertex[] taskVertices;

    public ExecutionJobVertex(
            ExecutionGraph graph,
            JobVertex jobVertex,
            int parallelism) {
        this.graph = graph;
        this.jobVertex = jobVertex;
        this.parallelism = parallelism;
        // 按并行度展开,创建 ExecutionVertex 数组
        this.taskVertices = new ExecutionVertex[parallelism];
        for (int i = 0; i < parallelism; i++) {
            taskVertices[i] = new ExecutionVertex(
                this, i, jobVertex.getConfiguration());
        }
    }

    public void connectToPredecessors() {
        // 连接上下游 ExecutionVertex
        for (int i = 0; i < parallelism; i++) {
            taskVertices[i].connectSource();
        }
    }

    public ExecutionVertex[] getTaskVertices() {
        return taskVertices;
    }
}

public class ExecutionVertex {
    private final ExecutionJobVertex jobVertex;
    private final int subTaskIndex;
    private final StreamConfig configuration;
    private Execution currentExecution;
    private SlotAndLocality assignedSlot;

    public ExecutionVertex(
            ExecutionJobVertex jobVertex,
            int subTaskIndex,
            StreamConfig configuration) {
        this.jobVertex = jobVertex;
        this.subTaskIndex = subTaskIndex;
        this.configuration = configuration;
    }

    public void deploy() throws Exception {
        // 创建 Execution 实例并部署
        currentExecution = new Execution(this, subTaskIndex);
        currentExecution.deploy();
    }

    public Execution getCurrentExecution() {
        return currentExecution;
    }
}

public class Execution {
    private final ExecutionVertex vertex;
    private final int attemptNumber;
    private ExecutionState state;
    private CompletableFuture<Void> releaseFuture;

    public Execution(ExecutionVertex vertex, int attemptNumber) {
        this.vertex = vertex;
        this.attemptNumber = attemptNumber;
        this.state = ExecutionState.CREATED;
    }

    public void deploy() throws Exception {
        // 状态转换:CREATED → SCHEDULED → DEPLOYING → RUNNING
        checkState(state == ExecutionState.CREATED);
        state = ExecutionState.SCHEDULED;
        // 申请 Slot,部署 Task
        state = ExecutionState.DEPLOYING;
        // TaskManager.submitTask()
        state = ExecutionState.RUNNING;
    }

    public void markFinished() {
        checkState(state == ExecutionState.RUNNING);
        state = ExecutionState.FINISHED;
    }
}

public enum ExecutionState {
    CREATED, SCHEDULED, DEPLOYING, RUNNING, FINISHED, FAILED, CANCELED
}

ExecutionGraph 是 JobMaster 内部的可执行调度图。attachJobGraph 方法遍历 JobVertex,创建 ExecutionJobVertex,然后连接 ExecutionEdge。ExecutionJobVertex 按并行度展开创建 ExecutionVertex 数组,每个 ExecutionVertex 对应一个并行子任务。ExecutionVertex.deploy 方法创建 Execution 实例,Execution 管理任务执行的状态机(CREATED→SCHEDULED→DEPLOYING→RUNNING→FINISHED)。


五、关键配置参数

yaml 复制代码
# 默认并行度
parallelism.default: 1

# 调度器类型
jobmanager.scheduler: adaptive

# 算子链优化
pipeline.operator-chaining: true

# 故障恢复策略
jobmanager.execution.failover-strategy: region

# 重启策略类型
restart-strategy.type: fixed-delay

# 固定延迟重启次数
restart-strategy.fixed-delay.attempts: 3

# 固定延迟重启间隔
restart-strategy.fixed-delay.delay: 10s

# 执行模式
execution.attached: true

# Slot 请求超时
slotmanager.request-timeout: 5min

# 每个 TaskManager 的 Slot 数
taskmanager.numberOfTaskSlots: 1

关键配置说明:

  • parallelism.default:默认并行度,所有算子的默认并行度,可在代码中单独设置。
  • jobmanager.scheduler:调度器类型,adaptive 支持动态调整并行度,根据可用 Slot 数自动调整。
  • pipeline.operator-chaining:算子链优化,默认 true,启用后可链接的算子合并为一个 JobVertex。
  • jobmanager.execution.failover-strategy:故障恢复策略,region 只重启受影响的 Task 区域,减少故障恢复时间。
  • restart-strategy:重启策略,fixed-delay 固定延迟重试,failure-rate 失败率重试。
  • slotmanager.request-timeout:Slot 请求超时,默认5分钟,大作业可调大到10-15分钟。

六、常见问题与最佳实践

常见问题

  1. 算子链未生效

    • 原因:并行度不同、非 forward 分发、禁用算子链、不同 SlotSharingGroup。
    • 解决:检查并行度配置,确认分发模式为 forward,移除 disableChaining(),确认 SlotSharingGroup 配置。
  2. JobVertex 数量过多

    • 原因:算子链优化未生效,每个算子独立 JobVertex,导致调度开销大。
    • 解决:启用算子链,合并可链接算子,减少 JobVertex 数量。
  3. Execution 调度延迟

    • 原因:Slot 不足、调度器配置不当、依赖关系复杂。
    • 解决:增加 TM 数量,使用 adaptive 调度器,优化算子拓扑,检查 Slot 分配状态。
  4. 部署 Task 失败

    • 原因:TaskManager 无响应、网络异常、资源不足。
    • 解决:检查 TM 心跳,确认网络连通,增加资源,查看 TM 日志。
  5. Execution 状态异常

    • 原因:状态转换非法、并发问题、故障恢复失败。
    • 解决:检查日志,重启 JM,配置重启策略,查看 ExecutionGraph 状态。
  6. SlotSharingGroup 隔离

    • 原因:不同 SlotSharingGroup 的算子不能共享 Slot,导致资源浪费。
    • 解决:合理配置 SlotSharingGroup,避免不必要的隔离,提高资源利用率。

最佳实践

  1. 合理设置并行度:根据数据量和资源设置并行度,避免并行度过高导致调度开销大,或过低导致处理能力不足。Source 并行度受数据源分片数限制。

  2. 启用算子链:默认启用算子链,减少线程切换和序列化开销,提升性能。只有在需要隔离或调试时才禁用算子链。

  3. 使用 adaptive 调度器:adaptive 调度器支持动态调整并行度,根据可用 Slot 数自动调整,提高资源利用率和作业稳定性。适合资源波动较大的环境。

  4. 配置 region 故障恢复:region 策略只重启受影响的 Task 区域,而不是整个作业,减少故障恢复时间和资源消耗。适合大多数流处理作业。

  5. 监控 Execution 状态:关注 Execution 状态变化,及时发现调度延迟和部署失败问题。监控 SCHEDULED 状态持续时间,过长说明 Slot 分配延迟。

  6. 合理配置 SlotSharingGroup:默认所有算子共享同一个 SlotSharingGroup,提高资源利用率。只有在需要资源隔离时才配置独立的 SlotSharingGroup。

  7. 优化算子拓扑:减少不必要的 shuffle,使用 forward 分发模式,促进算子链合并。避免数据倾斜,合理设计 keyBy 字段。


七、总结

Flink 任务调度 Graph 经历三层演进:StreamGraph(流图,逻辑执行图)→ JobGraph(作业图,算子链合并后的物理图)→ ExecutionGraph(执行图,按并行度展开的可执行图)。StreamGraph 从 Transformation 构建,包含 StreamNode 和 StreamEdge。JobGraph 经过算子链优化,包含 JobVertex、JobEdge、IntermediateDataSet。ExecutionGraph 按并行度展开,包含 ExecutionJobVertex、ExecutionVertex、Execution,是实际调度和执行的图。

核心类包括 StreamGraph(逻辑执行图)、JobGraph(物理执行图)、JobVertex(作业顶点)、JobEdge(作业边)、IntermediateDataSet(中间数据集)、ExecutionGraph(可执行调度图)、ExecutionJobVertex(执行作业顶点)、ExecutionVertex(执行顶点,并行子任务)、Execution(执行实例,状态机跟踪)。算子链优化合并可链接的算子,减少线程切换和序列化开销。并行度展开将 ExecutionJobVertex 展开为多个 ExecutionVertex。Execution 状态机跟踪 CREATED→SCHEDULED→DEPLOYING→RUNNING→FINISHED。

理解任务调度 Graph 的三层演进和核心类实现,有助于排查算子链未生效、JobVertex 数量过多、调度延迟、部署失败、状态异常等常见问题,为 Flink 作业的稳定运行提供保障。合理设置并行度、启用算子链、使用 adaptive 调度器、配置 region 故障恢复、监控 Execution 状态,可以显著提高作业的性能和稳定性。

相关推荐
Eyfcom1 小时前
〖实战〗快运系统 Oracle 性能优化:从“能用”到“扛住双11”的 Java + Spring Boot 实战
java·oracle·性能优化·快递管理系统
m0_587383001 小时前
碰碰好评卡功能落地全流程实战指南 附核心逻辑与常见问题解决方案
java·前端·数据库·后端·struts·需求分析
cheems95271 小时前
从统一响应到 Swagger:一篇讲清微服务接口文档设计未命名
java·微服务·架构·状态模式
Y3815326621 小时前
SerpBase + RAG 实战:用搜索 API 给知识库接上实时数据,回答不再过期
java·服务器·数据库
开开心心就好1 小时前
文件关联被破坏怎么修?免费工具一键修复
java·服务器·开发语言·pdf·ocr·启发式算法·宽度优先
不停喝水1 小时前
【前端转全栈java速通课】 项目实战④12-14节 拦截器过滤器和全局配置-cors跨域设置-token体系-利用session做验证码
java·开发语言·前端·ai·容器
鱼宵1 小时前
Spring AI RAG 问答助手:回答带引用,AI 不再睁眼说瞎话
java·人工智能·spring·springai
徐礼昭|商派软件市场负责人1 小时前
如何打通2B与2C库存?如何破解“线上缺货、线下积压”的内耗?——OMS全渠道「库存一盘货」策略详解
大数据·库存管理·订单管理·商派oms·oms系统