3. 数据流与执行图

很多Flink调优失败,并不是因为不会调参数,而是因为改错了层。Flink从用户写的DataStream API到最终在TaskManager上运行的物理任务,中间要经历四次图转换。每一层图解决不同问题,也对应不同的优化切入点。理解这四层图,是反压、吞吐与延迟优化的“地图”。

一、核心原理:四层图各管什么

Flink的执行计划生成是一条自顶向下的“降维”链路,每一层都在做更贴近物理执行的决策:

  • StreamGraph:由用户代码(DataStream API)直接生成,描述逻辑数据流。它保留算子链的原始意图,节点是StreamNode,边是StreamEdge,包含分区方式、并行度、算子链策略等逻辑信息。
  • JobGraph:由StreamGraph经过算子链合并(Operator Chaining)优化后生成,是提交给JobManager的图。节点是JobVertex,边是IntermediateDataSet。这一层决定了哪些算子会被“绑”在同一个Task中执行。
  • ExecutionGraph:JobManager收到JobGraph后,按并行度展开生成的并行化执行图。每个JobVertex展开为多个ExecutionVertex,边展开为ExecutionEdge,并携带状态机(CREATED、DEPLOYING、RUNNING、FAILED等)。
  • 物理执行图:ExecutionGraph被调度到具体TaskManager后,形成实际运行的Task。每个Task对应一个线程,运行在TaskSlot中,是真正消耗CPU、内存和网络资源的实体。
关键认知:StreamGraph和JobGraph是“逻辑+优化”层,ExecutionGraph和物理执行图是“并行+调度”层。反压发生在物理执行图的边(网络缓冲)上,而吞吐瓶颈往往由JobGraph的算子链和并行度决定。
Flink 四层图转换与优化切入点 StreamGraph 逻辑数据流 · StreamNode JobGraph 算子链合并 · JobVertex ExecutionGraph 并行展开 · ExecutionVertex 物理执行图 Task · Slot · 线程 链化 并行化 调度 优化切入点 ● StreamGraph:算子链策略、分区方式、并行度设置 ● JobGraph:算子链合并/拆分、Slot Sharing Group、资源组 ● ExecutionGraph:并行度调整、失败恢复策略、Checkpoint对齐 反压与吞吐的物理落点 反压:物理执行图边上的网络缓冲(InputChannel / ResultPartition) 吞吐:JobGraph的算子链长度 + ExecutionGraph的并行度 + Slot资源

二、方法流程:从代码到运行的转换链路

以一段典型的“Kafka → Map → KeyBy → Window → Sink”作业为例,观察四层图的转换过程:

// 用户代码(StreamGraph 生成阶段)
DataStream<String> source = env.addSource(new FlinkKafkaConsumer<>(...));
DataStream<Event> mapped = source.map(new ParseMapFunction());
DataStream<Event> keyed = mapped.keyBy(Event::getUserId);
DataStream<Result> windowed = keyed.window(TumblingEventTimeWindows.of(Time.seconds(10)))
                                    .aggregate(new MyAggregate());
windowed.addSink(new MySink());

// 默认算子链策略:source → map 可链化,keyBy 后断开
// JobGraph 中形成:JobVertex(source+map) → JobVertex(window+agg) → JobVertex(sink)

转换的关键动作如下:

  1. StreamGraph → JobGraph:执行算子链合并。满足“同并行度、同Slot Sharing Group、非Shuffle边”的算子会被合并为一个JobVertex。这一步直接决定Task数量。
  2. JobGraph → ExecutionGraph:JobManager按每个JobVertex的并行度展开为ExecutionVertex,并建立ExecutionEdge。同时初始化状态机,准备调度。
  3. ExecutionGraph → 物理执行图:调度器将ExecutionVertex分配到TaskManager的Slot中,每个Task启动一个线程,开始消费网络缓冲中的数据。
实操建议:在Flink Web UI中,“Job Graph”视图展示的是JobGraph层,而“Timeline”和“Backpressure”视图展示的是物理执行图层。调优时先看JobGraph确认算子链是否合理,再看物理执行图定位反压节点。

三、案例/场景:一次吞吐上不去的排查

某实时作业吞吐只有预期的一半,Web UI显示反压持续在Map算子。排查过程:

  • 第一层(StreamGraph):检查并行度,发现source并行度为4,而下游map并行度也是4,但Kafka分区数为16,source消费能力不足。
  • 第二层(JobGraph):发现source和map被链在一起,导致map的耗时阻塞了source的消费线程。将disableChaining()应用于map后,source独立成Task,消费能力恢复。
  • 第三层(ExecutionGraph):检查Slot Sharing Group,发现所有算子挤在同一个Slot中,CPU竞争严重。通过slotSharingGroup("heavy")将重算子隔离。
  • 第四层(物理执行图):调整网络缓冲参数taskmanager.network.memory.fraction,缓解反压。

最终吞吐提升约2.3倍。这个案例说明:不同层的优化手段解决不同层的问题,跨层乱调往往无效。

四、误区与复盘

常见误区 实际影响层 正确做法
只调并行度,不看算子链 JobGraph / ExecutionGraph 先确认算子链是否合理,再调并行度
认为反压是“算子慢” 物理执行图 反压是下游消费慢导致上游缓冲满,需定位最慢算子
忽略Slot Sharing Group JobGraph 重算子隔离到独立Slot,避免CPU争抢
在StreamGraph层调网络参数 物理执行图 网络参数属于物理执行层,应在配置文件中调整
复盘要点:优化前先问“这个问题发生在哪一层”。反压看物理执行图,吞吐看JobGraph和ExecutionGraph,延迟看Checkpoint对齐和网络缓冲。层对了,参数才有意义。