1. 从一张作业图说起:Flink运行架构到底在解决什么问题
很多人第一次接触Flink的运行架构,是被一张“作业图”绕晕的。打开官方文档,看到JobGraph、ExecutionGraph、物理执行图这几个词,再配上各种箭头和方框,脑子里就剩一个念头:这些东西到底跟我写的代码有什么关系?
我刚开始用Flink的时候也有同样的困惑。当时跑一个简单的WordCount,代码不到二十行,提交上去也能出结果,但一旦作业变复杂——比如加了KeyBy、开了窗口、设了并行度——就开始出各种问题:数据倾斜、反压、TaskManager内存爆掉、Checkpoint超时。排查这些问题的时候才发现,如果不理解Flink的运行架构,基本就是在盲人摸象。
所以这篇内容的核心,就是把Flink的运行架构从“图”的角度彻底讲清楚。我会沿着逻辑图到物理图的演进链路,把每个阶段的图长什么样、由谁生成、解决什么问题,一层层拆开。同时结合Slot、并行度、算子链、数据交换这几个关键机制,说明它们在实际运行中是怎么配合的。适合已经写过Flink作业、但对底层执行机制还比较模糊的开发者,也适合正在排查性能问题、想搞清楚“作业到底怎么跑起来”的同行。
整篇内容会围绕一条主线:你写的代码,是怎么一步步变成集群上并行执行的任务的。把这条链路走通,后面遇到反压、倾斜、Checkpoint这些问题,排查起来就有方向了。
2. 四层图的演进:从代码到物理执行的完整链路
2.1 为什么Flink要用“多层图”来描述一个作业
先想一个问题:为什么Flink不直接把用户代码扔到集群上跑,非要搞出好几层图来?
原因在于,用户写的代码是逻辑层面的描述,而集群执行需要的是物理层面的调度方案。这两者之间隔着好几道鸿沟。比如你在代码里写了一个keyBy,逻辑上就是“按key分组”,但物理上这意味着数据要跨节点重分布,涉及网络传输、序列化、缓冲区管理。再比如你设了并行度是4,那一个算子就要拆成4个并行子任务,这些子任务怎么分配到不同的Slot上,又是一个调度问题。
Flink用多层图的方式,把“逻辑描述”到“物理执行”的转换过程拆成了几个阶段,每个阶段解决一类问题。这样设计的好处是职责清晰:StreamGraph关注代码逻辑,JobGraph关注优化,ExecutionGraph关注调度,物理执行图关注实际运行。每一层各司其职,出了问题也容易定位是哪一层的毛病。
打个比方,这就像盖房子。StreamGraph是设计图纸,JobGraph是施工方案(把能合并的工序合并了),ExecutionGraph是排班表(谁在什么时候干什么),物理执行图是工地上实际干活的场景。图纸画得好不代表施工没问题,施工方案合理也不代表排班不出错,每一层都有自己要解决的问题。
2.2 StreamGraph:最贴近用户代码的第一层图
StreamGraph是在客户端生成的,也就是你执行env.execute()的时候。这一层图最接近你写的代码,基本上每个算子调用都会对应图上的一个节点。
具体来说,StreamExecutionEnvironment在添加算子的时候,会通过Transformation来记录每个操作。比如你调了source、map、keyBy、window、sink,每一个都会生成对应的Transformation对象。这些Transformation最终被转换成StreamGraph上的StreamNode和StreamEdge。
StreamNode代表一个算子,里面记录了算子的类型、并行度、输入输出等信息。StreamEdge代表数据流,记录了上下游的连接关系、分区方式(比如forward、hash、rebalance)等。
这一层图的特点是忠实反映代码逻辑,不做任何优化。你写了几个算子,图上就有几个节点。所以StreamGraph有时候会显得很“啰嗦”,比如连续两个map操作,在StreamGraph上是两个独立的节点,但实际上它们完全可以合并成一个任务来执行。
提示:StreamGraph是在客户端JVM中生成的,不会传到集群上。它的主要作用是作为后续优化的输入。
2.3 JobGraph:算子链优化发生的地方
JobGraph是StreamGraph经过优化后的产物,也是客户端最终提交给集群的图。这一层最重要的优化就是算子链(Operator Chain)。
算子链的逻辑很简单:如果两个算子之间的数据交换是forward(也就是一对一,不需要跨节点重分布),并且它们可以在同一个线程里执行,那就把它们合并成一个任务。合并之后,这两个算子在物理上就是一个Task,减少了线程切换和序列化的开销。
举个例子,source -> map -> filter这三个算子,如果并行度相同,且都是forward连接,那它们就可以被链接成一个算子链,在一个Task线程里依次执行。数据从source出来,直接传给map,map的结果直接传给filter,中间不需要经过网络传输,也不需要序列化。
但并不是所有算子都能链接。以下几种情况会打断算子链:
- 上下游并行度不同,比如source并行度是1,map并行度是4,那必须做rebalance,不能链接。
- 数据分区方式是hash、rebalance、broadcast等需要跨节点的模式。
- 用户手动调用了
disableChaining()、startNewChain()等方法。 - 算子之间有
keyBy操作,因为keyBy会引入网络传输。
JobGraph上还会做一些其他的优化,比如将多个sink合并(如果它们满足条件),以及确定每个算子的Slot共享组(Slot Sharing Group)。Slot共享组决定了哪些算子可以跑在同一个Slot里,这对资源利用率影响很大。
2.4 ExecutionGraph:调度层的核心数据结构
JobGraph提交到JobManager之后,会被转换成ExecutionGraph。这一层图是调度层的核心,JobManager就是靠它来管理作业的执行的。
ExecutionGraph的结构比JobGraph更细。JobGraph上的一个节点(JobVertex),在ExecutionGraph上会被展开成多个并行子任务(ExecutionVertex)。比如一个并行度为4的算子,在ExecutionGraph上就有4个ExecutionVertex。每个ExecutionVertex代表一个并行子任务,有自己独立的执行状态。
ExecutionGraph上还有几个关键概念:
- ExecutionJobVertex:对应JobGraph上的一个JobVertex,管理一组ExecutionVertex。
- ExecutionVertex:一个并行子任务,是调度的基本单位。
- Execution:一次执行尝试。如果某个子任务失败了需要重试,就会产生新的Execution。
- IntermediateResult:中间结果,记录了上游子任务的输出分区信息。
这一层图的核心作用是支持故障恢复和动态调度。当某个子任务失败时,JobManager可以通过ExecutionGraph快速定位到受影响的子任务,决定是重启全部还是只重启部分。Checkpoint机制也是基于ExecutionGraph来做的,每个ExecutionVertex的状态都会被记录下来。
2.5 物理执行图:真正跑在TaskManager上的样子
物理执行图不是Flink官方文档里的标准术语,但我觉得用它来描述“实际运行时的任务分布”很贴切。这一层图反映的是:哪些Task跑在哪些TaskManager的哪些Slot上,数据在它们之间怎么流动。
物理执行图是在ExecutionGraph的基础上,结合Slot分配情况生成的。JobManager的调度器会把ExecutionVertex分配到具体的Slot上,然后TaskManager根据分配结果启动Task。每个Task就是一个线程,执行算子链里的逻辑。
这一层图最直观的体现就是Flink Web UI上的那个可视化界面。你可以看到每个算子被拆成了几个并行子任务,每个子任务跑在哪个TaskManager上,数据流入流出是多少。排查问题的时候,这个界面是最重要的工具之一。
四层图的关系可以用一个表格来总结:
| 图的层次 | 生成位置 | 核心职责 | 关键概念 |
|---|---|---|---|
| StreamGraph | 客户端 | 忠实反映代码逻辑 | StreamNode、StreamEdge |
| JobGraph | 客户端 | 算子链优化、Slot共享组 | JobVertex、IntermediateDataSet |
| ExecutionGraph | JobManager | 调度、故障恢复 | ExecutionVertex、Execution |
| 物理执行图 | TaskManager | 实际运行 | Task、Slot、数据缓冲区 |
理解了这四层图的演进,就理解了Flink作业从代码到运行的完整链路。后面讲Slot、并行度、数据交换的时候,都会回到这个框架里来。
3. Slot与资源管理:TaskManager里到底怎么分配资源
3.1 Slot的本质:不是CPU核数,而是资源子集
很多人第一次看到Slot这个词,会下意识地把它理解成“CPU核”。比如一个TaskManager有4个Slot,就以为它有4个核。这个理解不完全错,但容易导致资源规划出问题。
Slot的本质是TaskManager资源的一个子集。TaskManager管理的是它所在节点上的内存资源(在较新的版本里,CPU资源也被纳入管理)。Slot是对这些资源的一个划分单位。一个TaskManager有多少个Slot,取决于它的taskmanager.numberOfTaskSlots配置,以及它实际拥有的内存和CPU。
关键在于:Slot之间是内存隔离的,但不是CPU隔离的。也就是说,每个Slot有自己独立的内存配额,但多个Slot可以共享同一个CPU核。Flink不做CPU级别的硬隔离,它假设Slot里的Task不会一直满负荷跑,所以允许CPU超卖。
这个设计的好处是资源利用率高。如果每个Slot都独占一个CPU核,那大部分时候CPU都是闲置的。允许共享之后,多个Task可以错峰使用CPU,整体吞吐量更高。但坏处是,如果某个Task特别吃CPU,可能会影响同一个TaskManager上其他Slot里的Task。
注意:从Flink 1.10开始,引入了细粒度资源管理(Fine-grained Resource Management),允许Slot指定具体的CPU和内存需求。但默认情况下,还是按Slot数量来均分资源。
3.2 Slot共享:为什么一个Slot里能跑多个算子
Slot共享(Slot Sharing)是Flink资源管理里一个很重要的机制。默认情况下,一个作业的不同算子可以共享同一个Slot。也就是说,一个Slot里可能同时跑着source、map、sink等多个算子的并行子任务。
为什么要这样设计?核心目的是提高资源利用率,同时降低对Slot数量的需求。
假设一个作业的并行度是4,有source、map、sink三个算子。如果不做Slot共享,那每个算子都需要4个Slot,总共需要12个Slot。但做了Slot共享之后,每个Slot里可以放source的一个子任务、map的一个子任务、sink的一个子任务,总共只需要4个Slot。
Slot共享的另一个好处是负载均衡。因为一个Slot里包含了作业的各个阶段,所以不会出现“source的Slot很忙,sink的Slot很闲”这种情况。数据在同一个Slot内部流转,也减少了网络传输。
但Slot共享也有代价。如果某个Slot里的一个Task出现了反压,会影响同一个Slot里的其他Task。而且Slot共享组(Slot Sharing Group)的划分需要仔细考虑,不能随便把不相关的算子放在一个组里。
默认的Slot共享组是default,所有算子都属于这个组。你可以通过slotSharingGroup()方法给算子指定不同的组。比如把source和sink放在不同的组里,避免它们互相影响。
3.3 并行度与Slot数量的关系:一个容易踩坑的地方
并行度和Slot数量的关系,是新手最容易搞混的地方之一。我见过不少人把并行度设成8,但TaskManager只给了4个Slot,然后作业一直卡在“等待资源”的状态。
这里的关系是这样的:一个作业需要的Slot数量,等于所有Slot共享组中最大并行度。注意,不是所有算子并行度之和,而是最大值。
举个例子,一个作业有三个算子,并行度分别是2、4、4,都在默认的Slot共享组里。那这个作业需要的Slot数量就是4,因为最大并行度是4。每个Slot里会跑一个并行度为2的算子的子任务,以及两个并行度为4的算子的子任务。
但如果source的并行度是1,map和sink的并行度是4,那source就不能和map、sink共享Slot了,因为并行度不同。这时候source会单独占一个Slot,map和sink共享4个Slot,总共需要5个Slot。
所以配置Slot数量的时候,要先算清楚作业的最大并行度,再考虑有没有并行度不一致的算子需要单独占Slot。一般来说,TaskManager的Slot数量建议设成CPU核数,或者CPU核数的一半,具体要看作业的CPU密集程度。
3.4 资源不足时的表现与排查思路
资源不足的时候,作业会卡在CREATED或者SCHEDULED状态,Web UI上会显示“等待资源”。这时候需要检查几个地方:
第一,看TaskManager有没有注册上来。如果TaskManager根本没启动,或者启动后没连上JobManager,那肯定没有可用资源。
第二,看Slot数量够不够。在Web UI的“Task Managers”页面可以看到每个TaskManager的Slot总数和可用数。如果可用数是0,说明Slot都被占满了。
第三,看有没有Slot共享组冲突。如果某个算子的Slot共享组是独立的,而集群里没有对应组的空闲Slot,那这个算子就会一直等。
第四,看有没有数据倾斜导致某个Slot过载。有时候Slot数量是够的,但某个Slot里的数据量特别大,导致这个Slot处理不过来,其他Slot却很闲。这种情况需要调整keyBy的分区策略或者加盐打散。
排查资源问题的时候,我习惯先看Web UI的概览页面,确认作业状态和Slot使用情况,然后再逐个TaskManager看具体分配。大部分资源问题都能通过这个流程定位到。
4. 数据交换机制:算子之间到底怎么传数据
4.1 数据交换的两种模式:forward与shuffle
算子之间的数据交换,决定了数据怎么从上游流到下游。Flink支持多种分区模式,最常用的两种是forward和shuffle(包括hash、rebalance等)。
Forward是一对一的分区。上游的每个并行子任务,把数据发给下游对应的并行子任务。比如上游并行度是4,下游并行度也是4,那上游的第0个子任务发给下游的第0个子任务,第1个发给第1个,以此类推。这种模式不需要网络传输(如果上下游在同一个TaskManager上),也不需要序列化,效率最高。
Shuffle是多对多的分区。上游的每个子任务,可能把数据发给下游的任意一个子任务。具体发给谁,取决于分区策略。hash分区是按key的哈希值取模,rebalance是轮询,broadcast是发给所有下游子任务。
Shuffle模式必然涉及网络传输(除非上下游恰好在同一个TaskManager上),需要序列化和缓冲区管理。所以shuffle的性能开销比forward大得多。在设计作业的时候,要尽量减少不必要的shuffle。
4.2 网络传输的底层:缓冲区与反压
Flink的网络传输是基于Netty的,数据在发送之前会先写入缓冲区(Buffer)。缓冲区的设计对性能影响很大。
每个Task的输出端有一个发送缓冲区池,输入端有一个接收缓冲区池。数据先写入发送缓冲区,攒够一定量或者等待超时后,才会真正发出去。接收端收到数据后,先放入接收缓冲区,然后由Task线程读取处理。
这个机制的核心目的是提高吞吐量。如果每来一条数据就发一次,网络开销会非常大。攒批发送可以显著减少网络交互次数。但攒批也有代价:如果数据量很小,攒批会导致延迟增加。所以Flink提供了execution.buffer-timeout参数来控制攒批的超时时间,默认是100毫秒。
反压(Backpressure)是网络传输里一个很重要的概念。当接收端的处理速度跟不上发送端的发送速度时,接收端的缓冲区会被填满,然后接收端会停止从网络读取数据。发送端发现数据发不出去,也会停止发送。这个压力会沿着数据流向上游传播,最终导致source也减慢发送速度。
反压本身不是故障,它是Flink的一种自我保护机制。但如果反压持续存在,说明作业的处理能力不足,需要排查是哪个环节成了瓶颈。Web UI上可以看每个算子的反压状态,如果某个算子显示“HIGH”,那它很可能就是瓶颈所在。
4.3 数据倾斜的成因与缓解思路
数据倾斜是shuffle模式下的常见问题。简单说就是:某个key的数据量特别大,导致处理这个key的子任务过载,而其他子任务很闲。
比如你按用户ID做keyBy,结果某个大V用户的数据量是普通用户的几百倍,那处理这个大V的子任务就会成为瓶颈。整个作业的速度取决于最慢的那个子任务,所以数据倾斜会严重拖慢整体性能。
缓解数据倾斜的思路有几种:
第一种是预聚合。在keyBy之前先做一次本地聚合,减少shuffle的数据量。比如map -> localAgg -> keyBy -> agg,这样每个子任务先把自己本地的数据聚合一下,再发给下游。
第二种是加盐打散。给key加一个随机前缀,让原本同一个key的数据分散到多个子任务上。等聚合完成后再去掉前缀,做二次聚合。这种方法会增加一些计算开销,但能有效缓解倾斜。
第三种是调整分区策略。如果hash分区导致倾斜,可以试试rebalance或者rescale。rebalance是全局轮询,rescale是局部轮询,都能让数据分布更均匀。但这两种方式会丢失key的局部性,适合不需要按key聚合的场景。
第四种是两阶段聚合。先按加盐后的key做一次聚合,再按原始key做一次聚合。这是加盐打散的标准化实现,适合聚合操作满足结合律的场景。
提示:数据倾斜的排查可以通过Web UI看每个子任务的输入数据量。如果某个子任务的输入远大于其他子任务,基本可以确定是倾斜。
4.4 算子链对数据交换的影响
算子链的存在,会直接影响数据交换的方式。如果两个算子被链接在一起,它们之间的数据交换就变成了线程内的方法调用,不需要经过网络,也不需要序列化。
这就是为什么算子链能大幅提升性能。数据在同一个线程里从上游算子直接传给下游算子,中间没有任何开销。对于CPU密集型的作业,算子链带来的性能提升非常明显。
但算子链也会影响数据交换的灵活性。如果两个算子被链接了,你就不能单独调整它们的并行度,也不能在它们之间插入新的算子。所以在设计作业的时候,要权衡算子链带来的性能提升和灵活性损失。
一般来说,我会把source -> map -> filter这种轻量级的转换操作链接在一起,把keyBy -> window -> agg这种重量级的操作分开。这样既享受了算子链的性能优势,又保留了调整的灵活性。
5. 作业提交与执行流程:从execute()到任务运行
5.1 客户端提交作业的完整步骤
当你调用env.execute()的时候,客户端会做一系列的事情,最终把作业提交到集群上。
第一步是生成StreamGraph。客户端遍历代码里所有的Transformation,构建出StreamGraph。这一步基本上就是把你写的算子调用翻译成图上的节点和边。
第二步是生成JobGraph。StreamGraph会被优化成JobGraph,包括算子链合并、Slot共享组划分等。这一步是客户端做的,不涉及集群。
第三步是提交JobGraph。客户端通过RPC把JobGraph提交给JobManager的Dispatcher。Dispatcher会启动一个新的JobMaster来管理这个作业。
第四步是JobMaster生成ExecutionGraph。JobMaster收到JobGraph后,会把它转换成ExecutionGraph,然后向ResourceManager申请Slot。
第五步是ResourceManager分配Slot。ResourceManager会检查有哪些TaskManager有空闲的Slot,然后把这些Slot分配给JobMaster。如果没有足够的Slot,作业就会等待,直到有资源释放或者新的TaskManager注册上来。
第六步是JobMaster部署Task。拿到Slot之后,JobMaster会把Task部署到对应的TaskManager上。TaskManager启动Task线程,开始执行算子链里的逻辑。
整个流程走完,作业就真正跑起来了。这个过程涉及客户端、Dispatcher、JobMaster、ResourceManager、TaskManager多个组件的协作,任何一个环节出问题都会导致作业提交失败。
5.2 JobManager与TaskManager的职责划分
JobManager和TaskManager的职责划分,是理解Flink运行架构的关键。
JobManager是集群的“大脑”,负责作业的调度和管理。它包含几个核心组件:
- Dispatcher:接收客户端提交的作业,为每个作业启动一个JobMaster。
- JobMaster:管理单个作业的执行,包括生成ExecutionGraph、申请资源、部署Task、触发Checkpoint、处理故障恢复。
- ResourceManager:管理集群的资源,负责Slot的分配和回收。
TaskManager是集群的“工人”,负责实际执行Task。它包含几个核心组件:
- TaskSlot:资源分配的基本单位,每个Slot可以跑一个或多个Task。
- TaskExecutor:管理Task的生命周期,包括启动、停止、监控。
- MemoryManager:管理TaskManager的内存资源,包括网络缓冲区和排序缓冲区。
JobManager和TaskManager之间通过RPC通信。JobManager下发指令(比如部署Task、触发Checkpoint),TaskManager上报状态(比如Task状态变化、心跳)。如果TaskManager长时间不上报心跳,JobManager会认为它失联,然后触发故障恢复。
5.3 故障恢复:ExecutionGraph如何支撑重启策略
故障恢复是Flink的核心能力之一,而ExecutionGraph是支撑故障恢复的关键数据结构。
当某个Task失败时,TaskManager会向JobMaster报告。JobMaster收到报告后,会根据配置的重启策略决定怎么处理。重启策略有几种:
- 固定延迟重启:尝试重启固定次数,每次间隔固定时间。
- 失败率重启:在固定时间窗口内,如果失败次数超过阈值就放弃。
- 不重启:直接让作业失败。
决定重启之后,JobMaster会通过ExecutionGraph找到受影响的ExecutionVertex,把它们的状态重置为CREATED,然后重新申请Slot、重新部署Task。
如果开启了Checkpoint,JobMaster还会从最近的Checkpoint中恢复状态。恢复的时候,每个ExecutionVertex会从Checkpoint里读取自己对应的状态数据,然后从那个状态继续处理。
这里有一个关键点:故障恢复的范围取决于数据交换模式。如果是forward模式,上下游是绑定的,一个失败可能导致整条链重启。如果是shuffle模式,上游和下游是解耦的,可以只重启失败的部分。这也是为什么Flink在shuffle模式下恢复速度更快。
5.4 一个作业从提交到运行的完整时间线
把上面的流程串起来,一个作业从提交到运行的时间线大概是这样的:
- 客户端生成StreamGraph和JobGraph,提交给Dispatcher。
- Dispatcher启动JobMaster,JobMaster生成ExecutionGraph。
- JobMaster向ResourceManager申请Slot。
- ResourceManager分配Slot,通知TaskManager准备。
- JobMaster部署Task到TaskManager。
- TaskManager启动Task,Task开始处理数据。
- Task定期向JobMaster上报状态和心跳。
- JobMaster定期触发Checkpoint,保存状态。
- 如果Task失败,JobMaster触发故障恢复,重新部署Task。
这个时间线里,每一步都可能出问题。比如Slot申请不到、Task部署失败、Checkpoint超时等。排查问题的时候,可以按照这个时间线逐个环节检查,看是哪一步卡住了。
6. 实操排查:运行架构相关的常见问题与解决思路
6.1 作业一直卡在CREATED状态怎么办
作业卡在CREATED状态,最常见的原因是资源不足。JobMaster申请不到足够的Slot,作业就无法进入RUNNING状态。
排查步骤:
第一,打开Web UI,看“Task Managers”页面有没有注册的TaskManager。如果没有,说明TaskManager没启动或者没连上JobManager。
第二,看每个TaskManager的Slot使用情况。如果所有Slot都被占用了,说明集群资源不够,需要扩容或者等其他作业释放资源。
第三,看作业需要的Slot数量。在Web UI的作业详情页面,可以看到作业的并行度和Slot共享组配置。算一下需要的Slot数量,跟集群可用Slot数量对比。
第四,检查Slot共享组配置。如果某个算子被分配到了独立的Slot共享组,而集群里没有对应组的空闲Slot,那这个算子就会一直等。
如果确认是资源不足,可以尝试降低并行度、调整Slot共享组、或者增加TaskManager。如果是配置问题,修改配置后重新提交即可。
6.2 反压导致作业变慢的排查路径
反压是作业变慢的常见原因。排查反压,关键是找到瓶颈算子。
第一步,打开Web UI的作业详情页面,看每个算子的反压状态。Flink会显示每个算子的反压程度,从OK到HIGH。如果某个算子显示HIGH,那它很可能就是瓶颈。
第二步,看这个算子的输入和输出速率。如果输入速率远大于输出速率,说明这个算子处理不过来。如果输入和输出速率差不多,但都很低,说明瓶颈在更上游。
第三步,看这个算子的并行子任务。如果某个子任务的输入数据量远大于其他子任务,说明存在数据倾斜。数据倾斜会导致单个子任务过载,进而引发反压。
第四步,检查这个算子的资源使用情况。如果CPU使用率很高,说明是计算瓶颈,需要优化算子逻辑或者增加并行度。如果内存使用率很高,可能是状态太大或者缓冲区不够。
第五步,检查Checkpoint配置。如果Checkpoint间隔太短或者超时时间太短,会导致Task频繁暂停处理数据来做Checkpoint,进而引发反压。
找到瓶颈之后,针对性地优化。计算瓶颈就优化代码或者加并行度,数据倾斜就加盐打散,状态太大就调整状态后端或者清理过期状态。
6.3 Checkpoint失败与运行架构的关系
Checkpoint失败是另一个常见问题,而且往往跟运行架构有关。
Checkpoint的核心是让所有Task在同一时刻保存自己的状态。这需要JobMaster向所有Task发送Checkpoint触发信号,Task收到信号后暂停处理数据,把状态快照写到外部存储,然后上报完成。
如果某个Task迟迟不上报完成,Checkpoint就会超时失败。常见原因有:
- 反压导致Task处理不过来:Task忙于处理数据,没时间做Checkpoint。这时候需要先解决反压问题。
- 状态太大导致快照时间过长:如果状态有几十GB,写快照可能需要几分钟。可以调整Checkpoint超时时间,或者改用增量Checkpoint。
- 外部存储性能不足:如果状态存储的写入速度慢,快照就会卡住。可以换更快的存储,或者调整并发写入数。
- 网络问题导致信号丢失:如果JobMaster和TaskManager之间的网络不稳定,Checkpoint信号可能丢失。需要检查网络配置。
排查Checkpoint问题的时候,我习惯先看Web UI的Checkpoint页面,确认是哪个环节超时。如果是某个Task一直没上报,就去看这个Task的反压状态和资源使用情况。如果是整体超时,就检查存储性能和网络配置。
6.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决思路 |
|---|---|---|---|
| 作业卡在CREATED | 资源不足 | 检查TaskManager和Slot数量 | 扩容或降低并行度 |
| 作业频繁重启 | Task失败 | 看TaskManager日志 | 修复代码或调整重启策略 |
| 反压严重 | 某个算子处理慢 | 看反压状态和输入输出速率 | 优化算子或加并行度 |
| Checkpoint超时 | 状态太大或反压 | 看Checkpoint详情 | 调整超时或解决反压 |
| 数据倾斜 | key分布不均 | 看子任务输入数据量 | 加盐打散或预聚合 |
| Slot分配失败 | Slot共享组冲突 | 看Slot共享组配置 | 调整共享组或增加Slot |
这张表覆盖了大部分运行架构相关的问题。实际排查的时候,先根据现象定位到可能的原因,然后按照排查方向逐个检查。大部分问题都能通过Web UI和日志定位到。
7. 个人实操体会:理解架构之后排查问题的思路变化
我刚开始用Flink的时候,遇到问题就是瞎试。作业慢了就加并行度,Checkpoint失败就调大超时时间,反压了就重启作业。有时候能碰巧解决,但更多时候是治标不治本,过两天同样的问题又出现了。
后来花时间把运行架构彻底理了一遍,最大的变化是排查问题有了方向感。以前看到反压,只知道“哦,反压了”,但不知道从哪里下手。现在会先看反压发生在哪个算子,然后看这个算子的输入输出速率、并行子任务的数据分布、资源使用情况,一步步缩小范围。大部分时候,十分钟内就能定位到瓶颈。
另一个体会是,很多问题其实在作业设计阶段就能避免。比如数据倾斜,如果在设计的时候就考虑到key的分布,提前做好加盐打散,后面就不用花时间排查了。再比如算子链,如果提前规划好哪些算子该链接、哪些该分开,后面调整并行度的时候就灵活很多。
还有一点是关于Slot共享组的。默认的Slot共享组把所有算子放在一起,方便是方便,但一个算子出问题会影响整个Slot。我现在会习惯性地把source和sink单独分组,把重计算的算子放在默认组里。这样source和sink出问题不会影响计算逻辑,计算逻辑出问题也不会阻塞数据摄入。
最后分享一个小技巧:Web UI的反压监控页面要经常看。不要等到作业变慢了才去看,平时就养成习惯,每隔一段时间扫一眼。如果发现某个算子的反压状态从OK变成了LOW,就提前介入排查,避免它恶化成HIGH。这个习惯帮我避免了好几次线上故障。