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的算子链和并行度决定。
二、方法流程:从代码到运行的转换链路
以一段典型的“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)
转换的关键动作如下:
- StreamGraph → JobGraph:执行算子链合并。满足“同并行度、同Slot Sharing Group、非Shuffle边”的算子会被合并为一个JobVertex。这一步直接决定Task数量。
- JobGraph → ExecutionGraph:JobManager按每个JobVertex的并行度展开为ExecutionVertex,并建立ExecutionEdge。同时初始化状态机,准备调度。
- 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对齐和网络缓冲。层对了,参数才有意义。