☰
Flink JobManager过载排查与治理:全集群提交失败复盘
2026/10/27 2:55:39 网站建设 项目流程

1. 凌晨两点的那次全集群提交失败

Flink 的 JobManager 过载这件事,我在一个 Session 模式的集群上完整经历了一次,从报警到彻底稳住前后折腾了将近两周。事情的发生很有代表性:集群规模不算大,一个 JobManager 配了 2 核 4G 的容器、堆内存只给了 2G,后面挂了十几台 TaskManager,上面跑着六十多个作业,绝大多数是"小作业"——每个作业只有一两个算子、占用一两个 slot、数据量也不大。真正开始出问题是在某次业务改动之后,平台侧把作业状态轮询从 15 秒一次改成了 3 秒一次,同时上线了一批新的定时任务,作业提交频率上去了,JobManager 就开始不对劲了。

最先暴露的症状是 Web UI 打不开。点进去转半天,REST 接口大面积超时,/jobs/overview这种本来毫秒级返回的接口要等好几秒,偶尔直接 500。紧接着是作业提交失败,报akka.pattern.AskTimeoutException之类的超时,现场的人一看提交失败就去重试,越重试越卡,很快形成了一个小型的重启风暴。再往后是 checkpoint 告警刷屏,一批作业的 checkpoint 持续超时,然后 JobManager 日志里开始出现Heartbeat of TaskManager with id xxx timed out,TaskManager 被反反复复地判定失联、重新注册。到这一步,整个集群实际上已经处于半瘫痪状态——跑着的作业勉强在跑,但谁也不敢动,因为一旦有作业失败重启,大概率起不来。

这篇文章我想把这次排查和治理的全过程拆开讲清楚:JobManager 到底在忙什么、为什么会 overuse、怎么用最小的成本定位到是哪个环节在吃资源、以及最后落地了哪些参数和架构层面的改造。如果你手上也管着 Flink 集群,尤其是用 Session 模式承载几十上百个作业的那种,这篇内容应该能帮你提前避开大部分坑。我会把踩过的坑、试错过的参数、以及那些文档里不会写的经验都放进去,涉及具体配置的地方会标注版本差异,因为 Flink 在 1.15 前后对 RPC 和内存模型的改动不小,照抄很容易翻车。

2. JobManager 到底在忙什么:先把职责边界理清楚

2.1 一个 JobManager 里其实塞了五六个组件

很多人对 JobManager 的印象停留在"管调度的大脑",但真到排查性能问题的时候,你必须知道这个"大脑"里具体有哪几个模块在跑,因为它们吃的是同一份 CPU 和内存,互相之间还会抢线程。在一个标准的 JobManager 进程里,通常同时存在这么几个东西:Dispatcher 负责接收作业提交、为每个作业创建一个 JobMaster;ResourceManager 负责 TaskManager 的注册和 slot 的分配回收;JobMaster 每个作业一个,负责生成和调度 ExecutionGraph、协调 checkpoint、处理失败恢复;WebMonitorEndpoint 对外提供 REST API 和 Web UI;BlobServer 负责接收和分发用户 jar 包及依赖文件;如果开了 HA,还有一套基于外部存储的选主和元数据服务。

这套结构在只有几个作业的时候完全没问题,但作业数量一上去,问题就来了。JobMaster 是"每作业一个实例",六十个作业就是六十个 JobMaster 对象,每个 JobMaster 内部还有自己的 checkpoint coordinator、调度线程池、定时器。这些对象本身占的内存不算夸张,但它们产生的调用最终都要汇到共享的 RPC 线程池和共享的 REST 线程池里去。也就是说,作业数量增长带来的负载不是线性摊薄的,而是在几个共享资源点上被放大。

2.2 共享线程池是第一个真正的瓶颈

真正卡住我们的第一个点就是 RPC 线程池。Flink 内部组件之间的通信走的是 RPC 框架,在 1.15 之前是基于 Akka 的 actor 模型,默认 dispatcher 的并行度有个下限值;1.15 之后换成了自己实现的 RPC 服务,配置项叫jobmanager.rpc.num-threads,印象里默认值只有 4。这个数值在单作业、低频交互的场景下够用,但在六十个作业同时跑、每个作业的 checkpoint 又都很频繁的情况下,就完全是杯水车薪了。

我抓线程栈的时候看得很清楚:大量线程卡在flink-akka.remote.default-remote-dispatcher或者 RPC handler 的调用栈上,栈顶是序列化、反序列化的方法,往下是 checkpoint 相关的消息处理。这说明线程池的队列已经堆满了,消息在排队等处理,处理不过来就会导致上游的调用超时,超时又会触发重试,重试再加重队列压力,典型的正反馈恶化。更麻烦的是心跳消息也走这条链路,RPC 一堵,心跳就延迟,JobManager 就会误判 TaskManager 失联,然后把上面的作业全部标记失败重新调度,重新调度又要走一遍资源申请和 ExecutionGraph 构建,负载进一步上升。

这里有个很反直觉的点:JobManager 的 CPU 打满并不一定是它在做"重计算",很可能只是线程在频繁地做上下文切换和对象序列化。所以看到 CPU 高先别急着加核,先抓栈看它在干什么,方向错了加多少核都是白搭。

2.3 Session 模式把资源争抢放大了好几倍

我们当时用的是 Session 模式,所有作业共享一个 JobManager。这个模式的好处是资源利用率高、集群启动一次就能跑很多作业,坏处是所有作业的命运绑在一起——一个作业疯狂触发 checkpoint,受影响的是全集群;一个作业提交了个超大 jar 包,BlobServer 的压力全集群承担;一个作业的 JobMaster 出现内存泄漏,整个 JobManager 进程跟着 OOM。

相比之下,Application 模式下每个应用一个集群,作业之间的隔离性好得多,代价是资源碎片化、集群数量变多、运维复杂度上升。这不是说 Session 模式不能用,而是说用 Session 模式就必须接受一个前提:你要对上面跑的作业有足够的约束能力,包括作业数量上限、checkpoint 频率上限、jar 包大小限制、提交频率限制。如果这些约束做不了,那 Session 模式迟早会变成事故现场,我们那次就是典型的约束缺位。

3. 四步定位法:从宏观指标一路挖到线程栈

3.1 第一步看 JVM 宏观指标,先排除内存和 GC

排查任何 Java 进程的过载问题,我的习惯都是先看 GC,因为 GC 问题会伪装成 CPU 问题,让你在错误的方向上浪费大量时间。具体操作就是拿到 JobManager 进程的 PID,然后用jstat连续采样一段时间:

# 每 1 秒打印一次 GC 统计,连续 30 次 jstat -gcutil <jm_pid> 1000 30

重点看几个列:老年代使用率(O)、Full GC 次数(FGC)、Full GC 累计耗时(FGCT)。我们那次的数据是:老年代常年在 60% 到 80% 之间震荡,Full GC 大概每两三分钟来一次,单次耗时 400 到 800 毫秒。这个数据说明内存确实偏紧,但还没到"GC 把 CPU 吃干"的程度——Full GC 的累计耗时占总运行时间的比例不到 3%,显然不是主因。

顺手也看了下堆外内存和 native 内存:

jcmd <jm_pid> VM.native_memory summary

如果没开-XX:NativeMemoryTracking=summary这个参数,这条命令会报错,那就得重启加参数,代价比较大。我们当时是直接看了容器的memory.usage_in_bytes和进程 RSS,发现 RSS 比堆上限高出将近 1G,这部分主要是 Metaspace、direct buffer、JVM 自身开销和线程栈。这也解释了为什么 2G 堆的容器经常逼近 4G 的内存上限,一旦超了就是 OOMKilled,容器重启,所有作业重来。

3.2 第二步抓线程栈,找热点线程名

GC 排除掉之后,下一步就是抓线程栈。这个方法看着原始,但在 Flink 这种组件众多的进程里极其有效,因为线程名本身就带语义。我的做法是连续抓五到十份,间隔两三秒,然后对比哪几个线程一直在忙:

for i in $(seq 1 10); do jcmd <jm_pid> Thread.print > /tmp/jstack_$i.txt sleep 2 done

抓完以后用grep统计一下状态为RUNNABLE的线程都在哪些名字下面聚集:

grep -A 1 "RUNNABLE" /tmp/jstack_*.txt | grep "^\"" | awk -F'"' '{print $2}' | sed 's/-[0-9]*$//' | sort | uniq -c | sort -rn | head -20

我们那次的统计结果非常集中,排在前面的线程名大概是这么几类:flink-akka.actor.default-dispatcher系列(RPC 调用处理)、checkpoint-thread系列(checkpoint 协调)、jobmanager-future-thread-pool(异步任务回调)、flink-metrics-reporter(指标上报)。其中 RPC dispatcher 那一类的数量最多,而且基本都处在 RUNNABLE 状态,栈顶停在序列化相关的方法上。这一下就锁定了方向:RPC 链路被打满了。

顺带还发现了一个细节:WebMonitorEndpoint相关的 handler 线程也一直是满负荷的,栈顶是 JSON 序列化。这跟我们平台侧改成 3 秒轮询一次对上了——轮询请求打到 REST 接口上,每个请求都要序列化一份作业状态快照,作业数量六十多个,等于每 3 秒要序列化六十份作业详情,这个开销在默认只有 4 个线程的 REST 服务里根本扛不住。

3.3 第三步用火焰图确认 CPU 时间去向

线程栈只能看到某个瞬间的快照,想看一段时间内的 CPU 分布还得靠火焰图。Flink 自己内置了火焰图功能,前提是在flink-conf.yaml里打开开关:

rest.flamegraph.enabled: true

打开后可以通过 Web UI 或者 REST 接口触发一段采样,产物是 SVG。不过我们当时 UI 已经卡到打不开了,所以用的是 async-profiler 手动采:

# 采样 60 秒,输出 CPU 火焰图 ./profiler.sh -d 60 -e cpu -f /tmp/jm-cpu.html <jm_pid>

火焰图出来的结果比线程栈更直观:最宽的那一块落在 JSON 序列化和 Akka 消息的编解码上,其次是 checkpoint 相关的状态处理,再往后是 metrics 的采集与上报。真正的"业务逻辑"(比如调度算法)占比非常小。这个结论其实很有价值——它说明 JobManager 的 CPU 不是在算东西,而是在"搬运和转换数据",那优化方向就很明确了:减少消息总量、减少序列化次数、减少无谓的轮询。

3.4 第四步翻日志,找高频事件的时间线

日志是所有证据里最耗时间但也最能说明因果关系的。我们当时重点搜了几个关键词:

# 心跳超时 grep -c "Heartbeat of TaskManager" jobmanager.log # checkpoint 相关异常 grep -c "Checkpoint.*timed out" jobmanager.log # 作业提交/取消 grep -cE "Submitting job|Job .* switched from state" jobmanager.log # 重启 grep -c "Restarting job" jobmanager.log

统计下来触目惊心:一天之内心跳超时记录了一千多次,checkpoint 超时告警三千多条,而作业状态切换的记录有上万条。再按时间维度聚合一下,发现这些事件高度集中在几个时间段——正好跟平台侧的批量部署任务和定时任务触发时间重合。这就把"偶然抖动"的假设排除掉了,问题是结构性的、必然的。

4. 根因拆解:四个把 JobManager 拖垮的具体写法

4.1 高频 checkpoint 与小作业数量叠加

这是最主要的因素,也是最容易被忽视的。单个作业的 checkpoint 间隔设成 10 秒,看起来没什么问题,因为数据量小、状态也小,每次 checkpoint 完成得很快。但 checkpoint 的开销不只是"做快照"那一下,它包含一整套交互:JobManager 的 checkpoint coordinator 触发 checkpoint,给所有 source 发 RPC,source 把 barrier 往下游推,各个算子在完成后上报,JobManager 收集所有 ack、做统计、记录元数据。这一整套下来,单个作业每次 checkpoint 至少要产生几十条 RPC 消息。

六十个作业,每个 10 秒触发一次,意味着平均每秒有 6 次 checkpoint 在流水线上跑,换算成 RPC 消息就是每秒几百条。这些消息全部要经过共享的 RPC 线程池,而且 checkpoint 的状态元数据还要在 JobManager 的内存里保存和更新。数量一旦上去,RPC 线程池的队列开始积压,checkpoint 的完成时间被拉长,超时告警出现,然后重试机制又把负载往上推。这就是我们看到的"checkpoint 越积越多、越来越慢"的恶性循环。

4.2 提交和取消作业的频率远超预期

第二个因素是作业的提交/取消频率。我们平台侧的 CI 流水线做得很"勤快",每次代码合并都会触发一次全量作业的重新部署,一批作业先 cancel 再 submit。六十个作业批量重启一次,对 JobManager 来说就是一百二十次状态变更,每次都要创建或销毁 JobMaster、构建或清理 ExecutionGraph、向 ResourceManager 申请或释放 slot。

关键在于 Dispatcher 处理作业提交的能力是有限的。在某些版本里,Dispatcher 是用单个线程逐个处理提交请求的,也就是说六十个作业同时提交,是排队一个一个来的。每个作业提交的时候还要从 BlobServer 拉取 jar 包、解析、生成 JobGraph。我们的业务 jar 包带了一堆依赖,单个将近 200MB,六十个作业同时提交,等于瞬间要把十几 G 的文件流转一遍。BlobServer 的磁盘 IO 和网络带宽瞬间打满,提交超时,流水线重试,负载再翻一倍。

4.3 平台侧的 REST 轮询和指标上报在悄悄放血

第三个因素很隐蔽,因为它藏在"监控"这个看起来无害的需求里。平台侧为了展示作业状态,做了一套轮询机制,最初是 15 秒一次,后来为了"实时性"改成了 3 秒一次。轮询调的是/jobs/overview再加上每个作业的详情接口,等于每 3 秒要遍历一遍所有作业、序列化一份完整状态。

同时 Flink 自身的 metrics reporter 也在上报,默认间隔 10 秒,上报的指标包括每个作业、每个算子、每个 TaskManager 的几十上百个指标。这两股流量叠加在一起,WebMonitorEndpoint 的线程池被长期占满,进而影响到正常的管理请求——你想手动 cancel 一个作业,请求排在几百个轮询请求后面,等到超时都轮不上。

4.4 ExecutionGraph 规模失控带来的序列化开销

第四个因素跟作业本身的写法有关。有些作业为了图省事,把大量逻辑平铺在同一个作业里,算子数量做到了几百个,并行度还不低。ExecutionGraph 的规模跟算子数、并行度是乘法关系,几百个算子乘以并行度,顶点和边的数量能到几千甚至上万。这个图在 JobManager 内存里要完整保存一份,每次状态变更、每次 checkpoint 都要遍历和序列化。

我们在火焰图上看到的那个特别宽的序列化热点,有一部分就是它贡献的。而且这个问题很隐蔽,因为单个作业看起来"能跑",只是有点慢,你不会意识到它在持续消耗 JobManager 的资源。等到作业数量多了,这些开销叠加起来就非常可观了。

5. 治理方案:从止血到结构性改造

5.1 第一步先止血:把最要命的参数调上去

在没搞清楚根因之前,先做了一些成本最低的调整,保证集群能正常对外服务。这里列一下我们实际改的参数,注意版本差异,我标注的是 1.15 之后的配置名,更早的版本用的是 Akka 那套参数,名字完全不一样:

参数原值调整后说明
jobmanager.memory.process.size4g8g容器总内存,给堆外留足空间
jobmanager.memory.heap.size2g4g堆内存翻倍,缓解 GC 压力
jobmanager.rpc.num-threads4(默认)16RPC 处理线程,直接缓解队列积压
rest.server.numThreads4(默认)16REST 处理线程,扛住平台轮询
heartbeat.timeout1000060000心跳超时放宽,避免误判失联
heartbeat.interval1000020000心跳间隔拉长,减少消息量
web.timeout10min5min缩短前端等待,快速失败

这里要重点说两个。jobmanager.rpc.num-threads这个参数调大之后效果立竿见影,提交超时的问题当场缓解了大半。但要注意它不能无脑调,线程数上去之后上下文切换和内存占用也会上去,一般给到 CPU 核数的两到四倍比较合理,我们给 16 是因为这个 JobManager 的负载确实重。heartbeat.timeout更是个双刃剑,放宽能减少误判,但如果真的有 TaskManager 挂了,发现时间也会延后,所以配合着把heartbeat.interval一起调,让心跳消息总量降下来,才是比较稳的做法。

有个细节值得提醒:调大堆内存之后一定要同步调大容器内存,而且要给堆外留足空间。我们第一次只把堆从 2G 调到 4G,容器还是 4G,结果跑了半天直接被 OOMKilled,因为 Metaspace 加上 direct memory 加上线程栈轻松超过 1G。后来容器给到 8G 才稳下来。

5.2 第二步做架构隔离:把鸡蛋从一个篮子里拿出来

参数调整只能延缓问题,治不了根。根本问题是六十个作业共享一个 JobManager,只要作业数量还在涨,早晚还会撞墙。所以我们做了集群拆分:按业务线拆成三个集群,每个集群承载二十个左右的作业;核心的、对延迟敏感的几个作业单独用 Application 模式部署,给独立集群;剩下的小作业、定时任务归到一个"杂项集群"里,这个集群的定位就是可以容忍一定程度的抖动。

拆分的时候有个取舍要想清楚:拆得越细,隔离性越好,但资源碎片和运维成本越高。我们的做法是先按"重要性"分层,再按"资源特征"分组——把 checkpoint 特别频繁的作业聚在一起,把 jar 包特别大的作业聚在一起,避免它们互相伤害。这个思路比单纯按业务线拆更有效,因为同类型的作业负载特征接近,放在一起反而好做针对性的参数优化。

拆完之后,单个 JobManager 上的作业数从六十多降到二十左右,RPC 消息量直接砍掉三分之二,UI 立刻就流畅了。这一步做完,我们才有余力去处理更深层的问题。

5.3 第三步统一 checkpoint 策略,把频率降下来

checkpoint 这块我们定了一套强制的规范,写进了作业模板,新作业默认就用这套:

配置项规范值出发点
execution.checkpointing.interval60s小作业 10 秒太奢侈,60 秒足够恢复
execution.checkpointing.min-pause30s两次之间留间隙,避免背靠背
execution.checkpointing.timeout5min超时设短,快速失败快速重试
execution.checkpointing.max-concurrent-checkpoints1禁止并发,减少同时占用的资源
execution.checkpointing.unaligned视情况开反压严重时开启,减少对齐等待

min-pause这个参数很多人不设,但它在新版本里其实很重要。不设的话,如果一次 checkpoint 做得快,下一次马上就来了,等于实际间隔远小于配置的 interval。设了 30 秒,能保证两次之间至少有半分钟喘息,对 JobManager 来说就是实打实地减少了触发次数。

另外我们还推行了"状态瘦身":定期检查作业的 state 大小,超过阈值的必须做 TTL 或者换用增量 checkpoint。有个作业的状态从 8G 降到 300M 之后,它的 checkpoint 耗时从四十多秒降到两秒,对 JobManager 的压力完全不是一个量级。

5.4 第四步掐掉平台侧的无效流量

这一块纯粹是自己人给自己挖的坑。我们把平台侧的轮询从 3 秒改回 10 秒,并且把"拉全量作业详情"改成了"拉一次列表 + 增量更新",只在状态真正变化时才去查详情。同时把 Flink 侧的 metrics 上报间隔从 10 秒放宽到 30 秒,关掉了一批没人看的指标。

# 只保留真正用到的 reporter 和指标范围 metrics.reporter.prom.interval: 30 SECONDS metrics.reporter.prom.factory.class: org.apache.flink.metrics.prometheus.PrometheusReporterFactory

这里的心得是:监控本身是有成本的,而且成本经常被低估。每多一个指标、每缩短一次间隔,都是在向 JobManager 收税。做监控方案的时候一定要问自己一句"这个指标真的会有人看吗、出问题真的会用它定位吗",答案是否定的就别上。我们关掉的那批指标里,有一半从来没人查过,但它们每天都在稳定地消耗着 CPU。

6. 参数之外,那些真正决定稳定性的细节

6.1 jar 包管理和提交方式也影响很大

前面提到过 jar 包大小的问题,这块值得单独说。BlobServer 在 JobManager 里,负责接收和分发用户代码。一个 200MB 的 jar,六十个作业并发提交,瞬间就是十几 G 的 IO。我们的做法是把公共依赖抽出来,做成集群级别的依赖库,业务 jar 只保留自己的代码,体积从 200MB 降到 5MB 左右。这一招的效果比调参数还明显,提交耗时从平均四十多秒降到三秒以内。

具体操作是把公共依赖放到 Flink 安装目录的lib/下面,或者在提交的时候通过-C参数指定额外的 classpath。要注意的是,放在lib/下的包会被所有作业共享,版本冲突的风险要自己控制好,我们的做法是把版本锁死,业务侧不允许擅自升级公共依赖。

6.2 定期重启和状态清理不能省

JobManager 是个长期运行的进程,跑久了会有各种累积:元数据缓存、类加载器引用、metrics 对象、日志里的历史状态。这些东西不会一次性把内存吃光,但会让 GC 越来越吃力。我们的经验是,Session 集群的 JobManager 每周做一次滚动重启,挑业务低峰期,先把作业 savepoint 下来,重启后再恢复。

这个操作听着粗暴,但很有效。重启之后老年代使用率能从 70% 多回到 30% 以下,UI 响应速度也会明显改善。当然前提是你的作业都支持从 savepoint 恢复,而且恢复时间在可接受范围内。如果某个作业恢复要半小时,那它就不适合放在 Session 集群里,应该单独部署。

6.3 失败恢复策略的选择会放大或抑制雪崩

jobmanager.execution.failover-strategy这个参数默认是region,意思是失败时只重启受影响的区域。这个策略比full好,但在某些拓扑下区域划分未必理想,一个小算子挂了可能带崩一大片。我们针对几个关键作业做了验证,发现改成region之后单点失败的影响面确实小了很多。

更重要的是要防止重启风暴。我们配了restart-strategy的退避策略,让失败的作业不要立刻重试:

restart-strategy: exponential-delay restart-strategy.exponential-delay.initial-backoff: 10s restart-strategy.exponential-delay.max-backoff: 5min restart-strategy.exponential-delay.backoff-multiplier: 2.0

这个配置的意义在于:如果一个作业是因为集群资源不足起不来,立刻重试只会让它再失败一次,白白消耗 JobManager 的调度资源。用指数退避,第一次等 10 秒,第二次 20 秒,慢慢往后推,给集群留出恢复的时间窗口。

7. 常见问题速查与避坑实录

7.1 问题速查表

把这次遇到的和之前踩过的坑整理成一张表,出问题的时候可以对着排查:

现象最可能的原因优先排查手段
UI 打不开、REST 超时REST 线程池被轮询打满线程栈看 WebMonitor 相关线程
作业提交超时RPC 线程池队列积压抓栈看 RPC dispatcher 状态
心跳超时、TM 频繁重注册RPC 拥堵导致心跳延迟统计日志中心跳超时的时间分布
checkpoint 持续超时触发频率过高或状态过大看 checkpoint 的 duration 和 size 指标
JobManager CPU 持续 90%序列化开销大或 GC 频繁火焰图 + jstat 双向确认
容器被 OOMKilled堆外内存没算够对比 RSS 和堆上限
作业反复重启起不来重启风暴或资源不足看 restart 策略和 slot 可用数
提交时卡在 Blob 传输jar 包过大或并发提交看 BlobServer 日志和 jar 体积

7.2 几个容易走错的方向

第一个误区是"CPU 高就加核"。我们最早的反应就是把 JobManager 从 2 核加到 4 核,结果 CPU 使用率确实从 95% 降到 70%,但过了两天又回到 95%,因为作业数量也在涨。加资源只是在延后问题,不解决结构性的负载集中。

第二个误区是"内存不够就加堆"。堆从 2G 加到 4G 之后 GC 频率确实降了,但如果容器内存没同步加,就会变成 OOMKilled,而 OOMKilled 比 GC 慢要糟糕得多,因为它是硬重启,所有作业全部重来。加堆之前先算清楚堆外需要多少。

第三个误区是"监控越密越好"。我们平台侧那次改动就是典型的例子,出发点是想让用户看到更实时的状态,结果是把自己打挂了。监控的本质是抽样,采样的频率要和业务的实际需求匹配,不是越高越好。

7.3 几条我认为最有价值的经验

第一,排查顺序永远是"先排除 GC,再看线程,最后看业务"。顺序错了会浪费大量时间在错误的方向上,我见过有人对着火焰图分析了半天,最后发现是 Full GC 导致的 STW,根本没到业务逻辑那一层。

第二,Session 模式的集群必须设置作业数量上限。这个上限不是拍脑袋定的,要根据 JobManager 的规格、平均 checkpoint 频率、平均算子数量算出来。我们的经验值是 2 核 4G 的 JobManager 承载不超过 15 个中等作业,超过就要拆。

第三,任何关于 JobManager 的参数调整,都要在预发环境压测验证。尤其是线程池相关的参数,调大之后的表现跟作业的负载特征强相关,别人的经验值在你这里未必成立。我们的做法是准备一个压测作业集,模拟真实的作业数量和 checkpoint 频率,每次改参数先跑一遍。

第四,把 JobManager 的关键指标纳入告警。我们后来加了这么几条:RPC 线程池队列长度、REST 接口 P99 延迟、JobManager 老年代使用率、单位时间内的作业状态变更次数。这几条指标能在问题爆发前十几分钟就发出预警,给了足够的处置时间。

8. 我个人在这件事上的几点体会

这件事之后我对"资源过载"这个词的理解变了不少。以前觉得过载就是资源不够,加就完事了;现在更倾向于把它看成"负载结构和资源配置不匹配"。我们那个集群的 CPU 和内存按绝对值算都不算紧张,问题出在负载高度集中在几个共享的、容量有限的组件上——RPC 线程池、REST 线程池、Dispatcher 的处理能力。这些组件的容量是固定的,跟作业数量的增长不成比例,所以只要作业数往上走,早晚会撞上。

另一个体会是关于"小作业"的。很多人觉得小作业不占资源,实际上从 JobManager 的角度看,小作业的固定开销一点也不小:一个 JobMaster 对象、一套 checkpoint 协调逻辑、一套 metrics 上报、一份 ExecutionGraph。作业越小,这些固定开销占比越高,性价比越差。所以后来我们推了一波作业合并,把几十个定时小任务合成了几个大作业,JobManager 的负载直接降了一半。

最后想说的是,这类问题的定位过程其实没什么高科技,就是老老实实地看指标、抓栈、翻日志,把时间线对上,把因果链理清楚。工具都是现成的,难的是判断力——知道先看什么、看到某个现象该往哪个方向想、哪些证据可以互相印证。这个能力只能靠一次次真实的排查积累,看再多的文档也替代不了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询