ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Flink集群部署模式:Standalone、YARN与Kubernetes实战

Flink集群部署模式:Standalone、YARN与Kubernetes实战

“Flink 集群部署模式”这个问题,看着像个运维选择题,实际上决定了后面半年你半夜被叫起来几次。我见过不少团队,实时链路做完 PoC 效果挺好,一上生产就出问题:作业跑着跑着 TaskManager 被系统杀掉、JobManager 重启之后状态找不回来、新上线的作业把老作业的 slot 抢光。往回追,很多时候不是代码写得烂,而是一开始部署模式就选错了,或者选对了但参数没配到位。这篇内容就是把这几种模式从“资源归谁管”这个根上讲清楚,然后落到 Standalone、YARN、Kubernetes 三条主线的实操细节,包括我怎么算 slot、怎么估内存、怎么配高可用和 checkpoint 存储,以及那些文档里不写、只有踩过才知道的坑。适合正在做实时平台选型的技术负责人,也适合刚接手 Flink 集群、需要把一套环境跑起来的数据开发。看完你至少能回答三个问题:我这个场景该选哪种模式、这套配置为什么这么填、出问题时先去哪儿看。

1. 部署模式选型:先搞清楚资源归谁管

1.1 四种形态的差异,本质上是“谁负责拉起进程”

很多人把 Flink 的部署模式当成一个产品功能列表来背,其实换个角度想就通了:JobManager 和 TaskManager 这两个 JVM 进程,总得有东西把它们拉起来。这个“东西”不同,就分出了几种模式。

Local 模式最简单,bin/start-cluster.sh一敲,两个进程在你本机起来,parallelism.default一般设成 1 或者 CPU 核数。它的价值就是本地调试,验证一下算子逻辑、看看 watermark 对不对,别拿它当测试环境用,因为它没有任何资源隔离和故障恢复能力。

Standalone 模式是把进程固定部署在几台机器上,配置文件里写死哪些机器跑 JobManager、哪些跑 TaskManager,启动脚本 ssh 过去挨个起。资源是“静态切分”的,这 4 台机器归 Flink 用,别的框架别来抢。它的好处是简单、没有外部依赖,坏处也明显:扩缩容要手动改配置重启,一个作业把 slot 占满了,另一个作业就只能干等。

YARN 模式把资源管理交给 YARN 的 ResourceManager,Flink 变成“申请者”:要多少容器就申请多少,用完释放,跟 MapReduce、Spark 共用一个资源池。Kubernetes 模式更进一步,JobManager 和 TaskManager 都是 Pod,资源由 K8s 调度器分配,容器化之后镜像即环境,滚动升级、命名空间隔离、配额管理都是现成的。

这四种形态的差别,用一句话概括就是:资源管理者是操作系统、是运维、是 YARN、还是 K8s。想清楚这一层,后面所有的配置项都能对上号。

1.2 集群生命周期与作业生命周期的三种绑定关系

光选资源管理者还不够,还有第二个维度:集群什么时候生、什么时候死。这个维度上有三种模式,理解了它们,你才知道为什么有些团队作业之间互相干扰,有些团队每个作业都要等三分钟才能启动。

Session 模式是先起一个常驻集群,然后往里提交作业。资源提前占好,提交作业快,适合大量小作业、交互式分析、SQL 客户端那种“来一个跑一个”的场景。代价是隔离性差:一个作业把 TaskManager 打挂了,同集群的其它作业一起遭殃;资源也是静态分好的,A 作业闲着的 slot,B 作业用不上。

Per-Job 模式是一个作业一个集群,作业结束集群销毁。隔离性最好,资源按作业实际需要申请,不用预留。缺点是每个作业启动都要走一遍“申请容器、下载依赖、启动 JVM”的流程,冷启动几十秒到几分钟不等,小作业频繁提交会很难受。

Application 模式是 Per-Job 的进化版:main()方法不再在客户端跑,而是挪到集群侧的 JobManager 上执行。这个改动看着小,实际影响很大。以前用 Per-Job,客户端要把依赖解析、作业图生成、jar 打包全做完再上传,客户端压力大,网络抖动一下作业就提交失败;Application 模式下客户端只负责把 jar 和配置传上去,剩下的活集群自己干,多租户环境下客户端可以做得非常轻。

提示:较新版本的 Flink 已经把 Per-Job 标记为不推荐,主推 Application 模式。新项目直接按 Application 设计,别在 Per-Job 上花时间了,迁移成本比一开始就选对高得多。

1.3 一张表把选型逻辑定下来

维度StandaloneYARN SessionYARN ApplicationK8s Application
资源来源固定机器YARN 资源池YARN 资源池K8s 集群
隔离性差差好好
作业启动速度快快中等中等偏慢
弹性扩缩容手动手动随作业申请随作业申请 / 自适应
运维复杂度低中中高
适合场景演示、小规模固定集群多小作业共享生产实时链路容器化平台、统一调度

我自己的判断顺序是:如果公司已经有成熟的 YARN 资源池、团队熟悉 Hadoop 生态,走 YARN Application 最省事;如果已经在推容器化、有统一的 K8s 平台和 CI/CD,那就走 K8s Application,前期投入大但后面每个作业的交付都会顺;Standalone 只在明确知道“就这几台机器、作业数量固定、不需要弹性”的时候才值得选,否则迟早要迁。

2. Standalone 模式:最朴素,也最容易出事

2.1 进程拓扑与配置文件的分工

Standalone 的目录结构里,跟部署相关的就三个文件:conf/flink-conf.yaml(新版本叫config.yaml)、conf/masters、conf/workers。前两个管“谁是 JobManager”,最后一个管“哪些机器跑 TaskManager”。

masters文件里默认是localhost:8081,一行一个 JobManager。生产上如果只配一个,那 JobManager 就是单点,进程一挂整个集群失去调度能力,所以至少配两个做 HA,配合 ZooKeeper 选举。

workers文件里一行一台机器的主机名或 IP,start-cluster.sh会 ssh 到每台机器执行taskmanager.sh start。这里有个很容易忽略的前提:部署机到所有 worker 节点必须免密,否则脚本跑到一半就提示输入密码,用nohup挂后台的时候直接静默失败,日志里只留一行看不懂的 ssh 报错。

flink-conf.yaml里最关键的几项:

jobmanager.rpc.address: 10.0.0.11 jobmanager.rpc.port: 6123 jobmanager.memory.process.size: 2048m taskmanager.memory.process.size: 8192m taskmanager.numberOfTaskSlots: 4 parallelism.default: 16 high-availability.type: zookeeper high-availability.storageDir: hdfs:///flink/ha high-availability.zookeeper.quorum: zk1:2181,zk2:2181,zk3:2181

taskmanager.numberOfTaskSlots这里是全局默认值,所有 TaskManager 一样。想给某台机器单独调,得在启动时用-D覆盖,比较麻烦,这也是 Standalone 不够灵活的地方之一。

2.2 起停与作业提交的实际操作

改完配置之后,启动顺序是先 JobManager 后 TaskManager,bin/start-cluster.sh内部就是这个顺序,但如果你是多机部署,建议先确认 JobManager 那台已经把 8081 端口监听起来,再起 TaskManager:

# 在 JobManager 节点 bin/jobmanager.sh start ss -lntp | grep 8081 # 在部署机上批量起 TaskManager bin/taskmanager.sh start

提交作业:

bin/flink run \ -m 10.0.0.11:8081 \ -c com.demo.realtime.OrderDetailJob \ -p 16 \ ./order-detail-job.jar

-p指定并行度,不指定就用parallelism.default。提交之后去 8081 页面看,重点看两处:Overview 里的 Task Slots 总数和 Available 数,以及 Running Jobs 里的 Backpressure 面板。Slot 总数应该是TaskManager 数量 × 每台 slot 数,如果页面显示的数量和你算的对不上,说明有 TaskManager 没起来,去各节点看log/目录下的启动日志。

停的时候bin/stop-cluster.sh会挨个停,但它是先停 TaskManager 再停 JobManager 吗?不是,脚本按workers文件顺序 ssh 停,JobManager 最后停。如果此时还有作业在跑,TaskManager 一停作业就失败重启,然后发现 JobManager 还在,又去申请资源,结果一直申请不到。所以停集群前先停作业,或者给作业配上execution.checkpointing.externalized-checkpoint-retention: RETAIN_ON_CANCELLATION,取消时保留 checkpoint,方便恢复。

2.3 高可用与资源隔离,这两块短板要有心理准备

Standalone 的 HA 靠 ZooKeeper 选主,配置项在 1.1 里已经列了,high-availability.storageDir要指向一个所有节点都能访问的共享存储(HDFS、对象存储都行),里面存的是 JobManager 的元数据和作业的 JobGraph。主 JobManager 挂掉之后,备节点从 ZooKeeper 抢到锁,从 storageDir 读回元数据接管作业,作业从最近一次 checkpoint 恢复。

这套机制能用,但有两个体感很差的地方。第一是恢复时间不短,从选主到 TaskManager 重新注册、再到作业重新拉起,小作业一两分钟,大状态作业更久,这期间数据积压会在上游消息队列里越堆越多。第二是资源隔离靠 slot,而 slot 在 Standalone 里就是静态分好的,A 作业申请了 8 个 slot,剩下的 8 个给 B 作业,B 作业业务高峰想扩到 16 个 slot,没门,只能等 A 释放,或者你手动加机器。

所以我个人对 Standalone 的定位很明确:临时环境、内部演示、作业数量固定且互不影响的小规模场景。但凡遇到“多业务共享集群”或者“要按需扩缩”,直接上 YARN 或 K8s,不要在 Standalone 上打补丁,那样改出来的东西比正经方案还难维护。

3. YARN 模式:资源池里讨生活的常规解

3.1 Session 模式的资源模型与它的适用边界

YARN Session 的启动命令长这样:

bin/yarn-session.sh \ -n 4 \ -jm 2048m \ -tm 8192m \ -s 4 \ -nm flink-realtime-prod \ -d

-n是 TaskManager 数量,-tm是每个 TaskManager 的进程内存,-s是每个 TaskManager 的 slot 数,-d是后台运行。算一下就明白了:总 slot 数 = 4 × 4 = 16,也就是说这个 Session 集群最多同时跑并行度总和 16 的作业(不考虑 slot 共享的复杂情况)。

提交作业到 Session:

bin/flink run -t yarn-session \ -Dyarn.application.id=application_1718000000000_0042 \ -c com.demo.realtime.OrderDetailJob \ -p 8 \ ./order-detail-job.jar

yarn.application.id从启动 Session 时打出来的日志里拿,或者去 YARN 的 ResourceManager 页面看。

Session 模式的优点是快,集群一直活着,提交作业只走作业图生成和分发,没有申请容器那一段,几秒钟就能起来。配合 SQL 客户端、报表类的小作业、临时排查问题的场景,体验很好。

缺点是隔离性和资源利用率都一般。所有作业共用一个 JobManager,一个作业的异常(比如死循环、内存泄漏导致 JobManager OOM)会影响全集群;资源是静态的,16 个 slot 分完就没了,某个作业半夜跑批占满 slot,白天来的实时作业就只能排队。我们在一个内部环境里就遇到过这种情况,最后给批作业单独建了个 Session 集群,按用途拆开才消停。

注意:YARN Session 的 slot 分配是“先到先得”,没有队列概念。如果团队里有多个业务方,务必按业务拆 Session 或者干脆用 Application 模式,否则每月都要处理一次“谁把资源占满了”的扯皮。

3.2 Application 模式:把 main() 挪进集群的好处

Application 模式的提交命令:

bin/flink run-application -t yarn-application \ -Dyarn.application.name=order-detail-job \ -Djobmanager.memory.process.size=2048m \ -Dtaskmanager.memory.process.size=8192m \ -Dtaskmanager.numberOfTaskSlots=4 \ -Dparallelism.default=16 \ -Dstate.checkpoints.dir=hdfs:///flink/checkpoints/order-detail \ -Dexecution.checkpointing.interval=60s \ -c com.demo.realtime.OrderDetailJob \ hdfs:///flink/jars/order-detail-job.jar

这里有个关键变化:jar 包放在 HDFS 上,不是本地路径。因为main()是在 JobManager 侧执行的,JobManager 所在的容器要能读到 jar;如果用本地路径,YARN 的ship-files机制会帮你上传,但对大 jar 来说上传时间不划算,生产上统一丢 HDFS 或对象存储更靠谱。

Application 模式解决了 Session 的两个痛点。第一,隔离性:每个作业独享一个 JobManager,一个作业出问题不影响别人;第二,资源按需:作业需要多少 slot,集群就申请多少个 TaskManager,作业结束容器释放,资源还给资源池。中间那条“申请容器”的链路是慢的,但如果作业是长期跑的实时链路,这几十秒的启动时间完全可以接受。

还有一个容易被忽略的收益:客户端变轻了。Per-Job 模式时代,客户端要负责依赖解析、作业图生成,如果作业依赖几十个 jar,客户端内存小了直接 OOM;Application 模式把这些活儿交给集群,客户端只是“递交材料”的角色,在 CI/CD 流水线里跑会舒服很多。

3.3 Slot 和内存怎么算,这部分别拍脑袋

我见过最多的配置错误,是taskmanager.memory.process.size和 YARN 的容器上限对不上。YARN 有个参数yarn.scheduler.maximum-allocation-mb,默认值通常不大(不同发行版差异很大,有的 8G,有的 16G)。如果你的 TaskManager 进程内存设成 12G,超过这个上限,作业提交时就会报类似Deployment failed in YarnClusterDescriptor或者容器申请被拒,日志里能看到 “Required more than the maximum allocation” 字样。

正确的算法是:

  1. 先定单个 slot 的资源。实时链路一般按每个 slot 2~4GB 内存估,具体看状态大小和算子复杂度,RocksDB 状态大的给到 4GB 以上。
  2. 再定每台 TaskManager 的 slot 数。常见 2~8 个,核数多、内存大的机器可以多给几个,但别超过物理核数,否则线程争抢严重,checkpoint 会超时。
  3. taskmanager.memory.process.size= 单 slot 内存 × slot 数 + 框架开销。Flink 1.10 之后的统一内存模型里,process.size是总进程内存,内部再切成框架堆、任务堆、托管内存、JVM 元空间、JVM 开销几块。托管内存默认占process.size的 0.4(taskmanager.memory.managed.fraction),RocksDB 用的就是这一块。

举个具体的例子:目标配置是 4 个 slot、每个 slot 分配 2GB 有效内存,那么process.size大致给到 8192m,托管内存约 3.2GB 留给 RocksDB,任务堆和框架堆分剩下的部分。这个配置下每个 TaskManager 占 YARN 一个 8GB 容器,作业并行度 16 就需要 4 个 TaskManager。

提示:taskmanager.memory.process.size一定要大于taskmanager.memory.flink.size(Flink 自身内存)加上 JVM 元空间和开销,否则启动时内存校验直接报错。懒得算就用process.size一档配到底,让 Flink 自己切分,比自己拼flink.size、managed.size出错概率低。

3.4 提交之后先看哪几个指标

作业提交上去,别急着走,花三分钟看几个地方,能省掉后面几小时的排查。

第一看 YARN 页面上容器状态,是不是所有 TaskManager 都 RUNNING 了,有没有容器反复 FAILED 又重试。如果 TaskManager 起来就挂,多半是内存超限被 NodeManager kill,去看容器的日志尾部,通常有Container killed by YARN for exceeding memory limits。

第二看 Flink Web UI 的 Checkpoint 页面,间隔、耗时、大小、是否对齐。刚启动时 checkpoint 慢是正常的,但如果稳定运行后单次 checkpoint 耗时接近间隔时间,说明状态太大或者反压严重,需要调整。

第三看 TaskManager 的 GC 情况,如果老年代频繁 Full GC,说明任务堆给小了,或者状态没走 RocksDB 全堆在堆内,这时候调整内存分配比加机器更有效。

4. Kubernetes 模式:容器化之后的落地细节

4.1 原生模式与 Operator,怎么选

K8s 上部署 Flink 有两条路。一条是Flink 自带的原生 K8s 模式,用bin/kubernetes-session.sh或bin/flink run-application -t kubernetes-application直接与 K8s API 交互,JobManager 通过 Service Account 创建 TaskManager Pod、ConfigMap 等资源。另一条是Flink Kubernetes Operator,装一个 CRD(比如FlinkDeployment),用声明式 YAML 描述整个集群,Operator 负责协调实际状态。

原生模式的好处是“官方原生、少一层抽象”,适合已经有一套成熟的 CI/CD 流水线、能自己拼命令行参数的团队。它的短板是运维能力弱:升级、扩缩容、状态监控这些都得自己写脚本或者靠外部工具。

Operator 的好处是把“部署”变成了“声明”,一个 YAML 里写清镜像、并行度、资源、重启策略、checkpoint 配置,改完kubectl apply就行,版本升级、savepoint 触发的操作也有现成的 CRD 字段。代价是多了一层组件要维护,Operator 本身也需要升级,出问题时排查链路更长。

我的建议是:作业数量超过十个、有多环境(测试/预发/生产)需求,直接上 Operator;只有两三个作业、团队对 K8s 很熟、不想引入额外组件,原生模式够用。

4.2 镜像、依赖和配置挂载

容器化部署 Flink,第一个绕不开的问题是镜像。官方镜像里只有 Flink 二进制,没有你的业务 jar,也没有连接器。通常的做法是自己构建一层:

FROM flink:1.18-scala_2.12-java11 # 业务 jar 内置,提交时用 local:// 引用 COPY target/order-detail-job.jar /opt/flink/usrlib/ # 连接器插件放 plugins 目录,能被插件机制自动加载 COPY lib/flink-s3-fs-hadoop-1.18.1.jar /opt/flink/plugins/s3-fs-hadoop/ COPY lib/flink-connector-kafka-3.1.0-1.18.jar /opt/flink/lib/ USER flink

这里有两个细节值得说。第一,业务 jar 内置镜像还是外挂存储。内置的好处是启动快、不依赖外部存储;坏处是每次发版都要构建镜像、推仓库、改镜像 tag,迭代慢。外挂(把 jar 放对象存储,提交时用s3://路径)迭代快,但 JobManager 启动时要下载,大 jar 会拖慢启动,而且要考虑存储访问权限。折中的做法是:连接器和 Flink 本身打进镜像,业务 jar 走外挂,业务迭代不用重新构建镜像。

第二,local://和s3://的区别。local:///opt/flink/usrlib/xxx.jar表示从容器本地文件系统读,必须保证镜像里有这个文件;不写 scheme 的路径会被当成“需要上传”,比如 Application 模式下用file://之类的本地路径,客户端会尝试打包上传,在容器里这条路往往走不通。

配置挂载上,环境变量和-D参数都能覆盖flink-conf.yaml里的值。少量参数用-D最直观,参数多了就做成 ConfigMap 挂到/opt/flink/conf/,或者干脆打进镜像分环境。我倾向于用 ConfigMap 管配置、镜像只放代码和依赖,这样改配置不用重新构建镜像。

4.3 Application 模式的提交命令与高可用配置

原生模式提交一个 Application 集群:

bin/flink run-application -t kubernetes-application \ -Dkubernetes.cluster-id=order-detail \ -Dkubernetes.namespace=flink-prod \ -Dkubernetes.container.image=registry.local/flink:1.18.1-custom \ -Dkubernetes.jobmanager.service-account=flink-sa \ -Dkubernetes.rest-service.exposed.type=ClusterIP \ -Dkubernetes.jobmanager.replicas=2 \ -Dhigh-availability.type=kubernetes \ -Dhigh-availability.storageDir=s3://flink-ha/order-detail \ -Dstate.checkpoints.dir=s3://flink-ckp/order-detail \ -Dtaskmanager.numberOfTaskSlots=4 \ -Dtaskmanager.memory.process.size=8192m \ -c com.demo.realtime.OrderDetailJob \ local:///opt/flink/usrlib/order-detail-job.jar

几个参数的作用要说清楚。kubernetes.cluster-id是整个集群的唯一标识,JobManager Pod、Service、ConfigMap 都以它命名,同一个 namespace 下不能重复。kubernetes.jobmanager.service-account必须指定一个至少有创建 Pod、Service、ConfigMap 权限的 Service Account,不指定的话 JobManager 根本没法创建 TaskManager Pod,表现就是作业一直卡在初始化。kubernetes.jobmanager.replicas=2配合high-availability.type: kubernetes才能让 JobManager 做主备,单副本配了 HA 也没意义。

HA 模式下有两件事必须在 K8s 上额外确认。一是存储可达性,high-availability.storageDir和 checkpoint 目录得是集群内所有 Pod 都能访问的存储,对象存储最省事,用 PVC 也行但要考虑读写多挂载的问题。二是对象存储插件,用s3://路径就必须把对应的 FS 插件放进plugins/,没放的话启动时直接报No FileSystem for scheme: s3,这个错误很常见但也好排查。

4.4 弹性伸缩与资源碎片的现实问题

K8s 上最容易让人觉得“容器化真好”的一点,是弹性伸缩。较新版本支持自适应调度,作业先按最小资源起来,运行中根据实际负载申请更多 TaskManager,资源紧张时缩回来。配合 K8s 的节点自动伸缩,业务高峰时集群自动加机器,低峰时缩容,成本能压下来不少。

但真正落地时会撞上几个现实问题。第一是扩容不等于变快:新 TaskManager 从调度、拉镜像、启动 JVM 到注册进集群,链路不短,如果镜像大、节点冷启动慢,扩一个 TaskManager 要一两分钟,对秒级突发的业务帮助有限。所以弹性伸缩更适合“有持续高峰、但峰值持续几十分钟以上”的场景,比如整点促销、日终结算。

第二是资源碎片。TaskManager 申请的资源(CPU、内存)是整数 Pod,K8s 节点上剩余的零散资源拼不出一个 Pod,就会一直 Pending。一个 8GB 的 TaskManager Pod,节点上只剩 6GB,作业就卡着。解决办法是给 Flink 单独划节点池,用节点亲和性和资源配额把这块资源隔离出来,别和在线业务混跑。

第三是状态存储的本地盘。RocksDB 状态默认落在 TaskManager 本地目录,K8s 上就是容器的临时存储,Pod 一重建本地状态就没了。虽然 checkpoint 能恢复,但恢复时要重新加载全部状态,重启一次几分钟。所以要么给/tmp挂emptyDir用节点本地 SSD 提升 IO,要么配置 RocksDB 的本地恢复机制(state.backend.local-recovery),把状态目录放到持久卷上。这一块不配,作业重启的体感会非常差。

5. 常见问题与排查技巧实录

5.1 启动阶段的卡点:从提交到 RUNNING 之间发生了什么

作业提交后一直不 RUNNING,是最常见的求助场景。排查思路是按阶段切:客户端阶段(依赖解析、作业图生成)、资源申请阶段(YARN 容器或 K8s Pod 调度)、进程启动阶段(JVM 起来、注册到 JobManager)、作业初始化阶段(分配 slot、部署算子)。

客户端阶段卡住,通常是 main() 里做了重活儿,比如启动时读数据库、加载大文件,或者依赖冲突导致类加载死锁。Application 模式下这段跑在集群侧,去看 JobManager 的日志就能看到进度。

资源申请阶段卡住,YARN 上看页面上的容器申请是不是 PENDING,常见原因是队列资源不足、单容器内存超上限;K8s 上看 Pod 是不是 Pending,用kubectl describe pod看 Events,通常是资源不足或者镜像拉取失败。

进程启动阶段卡住,TaskManager 的日志里会有内存校验失败、端口冲突、无法连接 JobManager 这类报错。K8s 上还有一个高频问题:TaskManager Pod 起来后立刻重启,kubectl get pod显示 CrashLoopBackOff,看kubectl describe的 Last State 里如果是 OOMKilled,就是 Pod 的内存 limit 设得比 JVM 进程实际需要的小,把 limit 调到process.size之上、留 10%~20% 余量。

5.2 资源类报错:几个一看就知道原因的提示

现象或报错常见原因处理办法
作业一直等待,日志出现 “Could not acquire the minimum required resources”集群 slot 不足或并行度超过总 slot提高 slot 总数、降低并行度,或改用 Application 按需申请
YARN 提交失败,提示超过最大分配taskmanager.memory.process.size超过yarn.scheduler.maximum-allocation-mb调大上限或减小 TM 内存、增加 TM 数量
TaskManager 频繁重启,YARN 容器 FAILED内存超限被 NodeManager kill检查容器日志,调整process.size与内存配额
K8s Pod Pending 不调度节点资源碎片、亲和性冲突、配额不足查看 Events,调整资源请求或节点池
作业报 “No FileSystem for scheme”缺少对应文件系统插件把插件 jar 放进plugins/目录后重启集群
作业卡在 REST 接口无法访问Service 暴露方式或端口未通检查rest-service.exposed.type、Service 和网络策略

这些错误信息本身其实很直白,难的是不知道去哪儿看。我的习惯是把“Flink Web UI → JobManager 日志 → TaskManager 日志 → 资源调度器(YARN/K8s)事件”这套顺序固定下来,从外往里一层层排除,比东看一眼西看一眼有效率得多。

5.3 状态恢复失败:checkpoint 配了不等于能用

配了 checkpoint 目录,作业失败重启却恢复不了,甚至报找不到 checkpoint,这类问题在生产上出现得不少。原因通常有几种。

一是 checkpoint 目录权限或路径不一致。Standalone 时代迁移到 YARN 或 K8s 时,state.checkpoints.dir还写着本地路径file:///data/flink/ckp,集群化之后每个容器都是独立的文件系统,路径根本不存在。必须统一成所有节点都能访问的存储。

二是外部化策略没配。默认情况下作业取消时 checkpoint 会被清理,想恢复就得配execution.checkpointing.externalized-checkpoint-retention: RETAIN_ON_CANCELLATION,把取消时的 checkpoint 保留下来。

三是增量 checkpoint 与 RocksDB 目录。开了state.backend.incremental: true之后,checkpoint 之间是增量的,恢复时依赖完整的元数据链,中间有任何一个 checkpoint 被清理或损坏,恢复就可能失败。生产上建议配state.checkpointing.num-retained: 3到 5,保留几个历史点,同时用 savepoint 做版本升级的锚点。

四是升级兼容。改代码改状态结构之后从旧 savepoint 恢复,如果没给状态加 serializer 或没配state.backend.rocksdb的兼容选项,会直接报StateMigrationException。升级前先打 savepoint,然后本地用一小份数据验证恢复,这个流程不要省。

提示:savepoint 和 checkpoint 用途不同。checkpoint 是运行时自动的内存快照,主要给故障恢复用;savepoint 是手动触发的、格式更稳定的快照,用于版本升级、扩缩容、迁移。生产上两者目录分开配,别共用。

5.4 一套例行检查清单

环境搭完、作业上线前,我会过一遍这张清单,能挡掉八成的后续问题:

  • 单 JobManager 还是主备,HA 的存储目录是否所有节点可写;
  • 作业并行度与总 slot 数的关系是否算过,留了多少余量;
  • taskmanager.memory.process.size是否小于 YARN 容器上限 / K8s Pod limit;
  • checkpoint 间隔、超时、最小间隔是否配,目录是否可写,外部化策略是否开;
  • 重启策略是否配(固定延迟还是指数退避,重试次数上限多少);
  • 日志落盘位置和保留周期,出问题时够不够查;
  • 容器镜像的 tag 是否用了固定版本,避免“latest”带来的不可复现;
  • 是否配了监控,至少要有作业状态、checkpoint 成功率、反压、GC 时间这几项。

我个人在实际操作中的体会是,部署模式这件事没有“最优解”,只有“和团队现状最匹配的解”。早期我特别喜欢追新,什么方案先进就上什么,结果一个三五个人的小团队维护 K8s 上的 Flink 集群,光是排查 Pod 调度问题就耗掉了大半精力,反而不如老老实实用 YARN。后来换了个思路:先看团队最熟什么、现有资源池是什么、作业规模有多大,答案往往自己就浮出来了。真正需要死磕的不是模式选择本身,而是 slot 怎么算、内存怎么分、状态存哪儿这三件事,这三件做扎实了,换哪种模式都能跑得舒服。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进