2. Flink运行时架构

要理解反压、吞吐与延迟的调优,必须先看清Flink作业在集群中“长什么样”。Flink采用经典的主从(Master-Worker)运行时架构:JobManager负责作业的调度与协调,TaskManager负责实际的任务执行,而Slot则是TaskManager内部最细粒度的资源单位。作业提交后,逻辑上的算子图(Operator Graph)会被翻译成物理执行图(Execution Graph),最终以Task和SubTask的形式落到各个Slot上运行。这一节我们就把这条“从代码到运行”的链路拆开讲清楚。

一、核心组件:JobManager与TaskManager

JobManager是集群的“大脑”,它内部包含三个关键角色:Dispatcher负责接收客户端提交的作业并提供REST接口;ResourceManager负责Slot资源的申请、分配与回收;JobMaster则是单个作业的最高管理者,负责将Execution Graph调度到TaskManager上,并协调Checkpoint、故障恢复等。一个JobManager可以同时管理多个作业(Session模式下),每个作业对应一个独立的JobMaster。

TaskManager是集群的“肌肉”,是真正执行Task的工作节点。每个TaskManager是一个JVM进程,内部通过Slot来划分可用的计算资源。TaskManager会向ResourceManager注册自己的Slot,JobMaster再向ResourceManager申请Slot来部署Task。TaskManager之间通过Netty进行数据交换,这也是反压信号传播的物理通道。

关键点:JobManager不参与实际数据处理,它只做调度与协调;所有算子逻辑、状态、网络缓冲都运行在TaskManager上。因此吞吐与延迟问题,绝大多数要在TaskManager侧定位。

二、Slot、Task与SubTask:资源模型的三层关系

很多初学者会把Slot、Task、SubTask混为一谈,其实它们描述的是不同层次的概念:

  • Slot:TaskManager上的资源子集,代表固定的内存与计算份额(Flink 1.11后Slot主要隔离内存,CPU不再严格隔离)。一个TaskManager有多少个Slot,由taskmanager.numberOfTaskSlots决定。
  • Task:一个算子(或算子链)在一个并发实例上的执行单元,是调度的基本单位。
  • SubTask:Task在并行度下的具体实例。并行度为4的Map算子,会产生4个SubTask,每个SubTask就是一个Task实例。

默认情况下,Flink允许Slot共享(Slot Sharing):同一个作业中不同算子的SubTask可以运行在同一个Slot里。这样做的价值在于,一个作业的并行度等于其最大算子并行度,而不是所有算子并行度之和,极大降低了资源需求。同时,同一Slot内的SubTask共享TCP连接,能减少网络开销。

# flink-conf.yaml 中与资源模型相关的核心参数
taskmanager.numberOfTaskSlots: 4
parallelism.default: 4
taskmanager.memory.process.size: 4096m
slot.sharing.group: default   # 通过 SlotSharingGroup 控制共享范围

三、Operator Chain:算子链的合并逻辑

Flink会把满足条件的相邻算子合并成一个算子链(Operator Chain),合并后的整条链作为一个Task运行在同一个线程中。合并的条件主要有两个:上下游算子并行度一致,且数据分发方式为Forward(一对一,非keyBy、非rebalance)。

算子链的意义非常直接:数据在链内以方法调用的方式传递,不经过序列化和网络,避免了线程切换与缓冲开销,从而显著提升吞吐、降低延迟。但反过来,如果一条链过长,某个算子成为瓶颈,整条链都会受影响,反压也会在链内直接传导。因此调优时经常需要disableChaining()或startNewChain()来打断不合理的链。

DataStream<String> stream = env.addSource(new KafkaSource())
    .map(new ParseMap())          // 与source并行度一致且forward,会合并
    .keyBy(r -> r.userId)          // keyBy引入shuffle,链在此断开
    .window(TumblingEventTimeWindows.of(Time.seconds(10)))
    .aggregate(new CountAgg())
    .disableChaining();            // 强制该算子单独成链,便于定位瓶颈

四、一张图看清运行时结构

下面这张图把JobManager、TaskManager、Slot、Task与Operator Chain的层级关系放在一起,帮助你建立空间感:

Flink 运行时架构与资源层级 JobManager (Master) Dispatcher ResourceManager JobMaster 调度/申请Slot TaskManager (Worker JVM) Slot 1 Task (Operator Chain) Source → Map Task (Operator Chain) Window → Sink SubTask 共享同一 Slot 共享 TCP 连接与内存 Slot 2 Task (Operator Chain) Source → Map Task (Operator Chain) Window → Sink 并行度 = 2 两个 Slot 并行执行 Netty

五、案例:并行度与Slot配置不匹配的复盘

某团队提交作业时设置了parallelism.default: 8,但TaskManager只配置了taskmanager.numberOfTaskSlots: 2,且集群只有一台TaskManager。作业提交后一直处于CREATED状态,日志中反复出现:

org.apache.flink.runtime.jobmanager.scheduler.NoResourceAvailableException:
Could not allocate all requires slots within timeout of 300000 ms.
Slots required: 8, slots allocated: 2

复盘结论:Slot共享虽然能降低需求,但作业的最大并行度仍受限于集群总Slot数。该作业有8个并行度,而集群只有2个Slot,无法满足。解决方案有两种:一是增加TaskManager或提高numberOfTaskSlots;二是降低作业并行度。这里也引出一个常见误区——Slot数不等于CPU核数,一个Slot可以承载多个SubTask,但Slot数量决定了作业能同时运行的Task上限。

调优建议:Slot数量建议设置为TaskManager可用CPU核数,让每个Slot大致对应一个线程,避免过多SubTask争抢同一线程导致调度延迟。同时结合SlotSharingGroup,把资源消耗差异大的算子分到不同组,防止相互干扰。

六、常见误区清单

误区 正确理解
Slot就是CPU核 Slot是资源子集,主要隔离内存,CPU不严格隔离
并行度等于Slot数 并行度是算子实例数,Slot是承载Task的资源单位,二者可不等
算子链越长越好 链过长会掩盖瓶颈,反压传导快,必要时需打断链
JobManager负责数据处理 JobManager只做调度协调,数据全在TaskManager处理
Slot共享会降低性能 合理共享可减少网络开销和资源占用,是Flink的默认优化