☰
Flink 的 checkpoint 为什么越跑越慢
2026/10/26 13:13:26 网站建设 项目流程

很多 Flink 作业的 checkpoint 间隔设得很短,单次却跑得很久。

本地 RocksDB 刷盘是顺序写,快得多,剩下的大头都在把文件传到对象存储。

这就是这篇要拆的事。

checkpoint 越跑越慢,慢的不是快照,是上传。

先把结论放前面。

增量 checkpoint 传的也不是纯增量。

compaction 会把旧数据重新写成新文件,赶上合并的那一轮就得多传一遍。

对 exactly-once 来说,checkpoint 是刚需,绕不开。

所以问题不是要不要做,是怎么把开销压下来。

要讲清楚这件事,得先看增量 checkpoint 到底存了什么。

RocksDB 是 LSM 树,memtable 刷盘变成 SST,SST 一旦写完就不再改。

刷盘是本地顺序写,跟网络没关系。

慢的那一段,发生在文件离开本地盘之后。

checkpoint 触发时,Flink 让 RocksDB 把这一轮新产生的 SST 挑出来,只传这些,再附一份 metadata。

metadata 里不放数据,只放指向这些 SST 的引用。

目录结构不复杂,一个 job 一个目录,下面分 shared、taskowned 和 chk-N。

shared 放可能被多个 checkpoint 共用的文件,taskowned 放 JobManager 不能删、也不属于某一轮的文件,chk-N 放只属于这一轮的。

共用靠引用计数,认的是 state handle 的物理 ID,不是文件路径。

文件一旦被合并重写,物理 ID 就换了,引用计数也就认不出它是老文件。

所以增量 checkpoint 不是一份完整备份,是一条链,每一轮挂在前一轮后面。

一条链意味着一件事,快照做完,不等于文件已经传完。

增量 checkpoint 的快照阶段只决定这一轮要传哪些文件,真正的落地在异步上传里。

JobManager 收集每个 subtask 的 state handle,全部到齐,这一轮才算完成。

任何一个 subtask 上传翻车,整轮 checkpoint 就失败,上传这关绕不开。

这里还有个容易被忽略的前提。

增量 checkpoint 默认不开,得手动把execution.checkpointing.incremental设成 true,这是 2.3 的正名。

很多人以为用了 RocksDB 就自动增量,其实没有。

全量和增量的差别,不只是传多传少。

全量 checkpoint 写的是 Flink 自己的 key/value 快照格式,一份快照自带全部状态,换后端也能读。

增量传的是 RocksDB 的 SST,恢复的时候省掉了重建本地表那一步。

代价是恢复要顺着链把多个 delta 拉下来,链越长,拉得越多。

这也是为什么增量 checkpoint 的恢复时间,可能比全量还长。

savepoint 走的是另一条路。

它默认写成 canonical 格式,所以不能靠增量链。

要保留换状态后端的能力,就得接受全量写一遍。

canonical 格式的好处是跨后端可读,坏处是每次都要全量写。

链本身没问题,问题在 compaction。

RocksDB 会把小 SST 合并成大 SST,读得少,文件也少。

但合并出来的是新文件,文件名变了,身份也变了。

在 Flink 眼里,这就是一批从没见过的新 SST,得重新传一遍。

官方文档写得很直白,大多数增量状态后端都会周期性 compaction,结果就是把旧状态重新上传一次。

集群一大,几乎每个 checkpoint 里都有一个 task 在上传大量数据。

所以状态越大、更新越频繁、compaction 越勤,增量 checkpoint 就越像全量。

还有一层放大效应,来自数据分布。

状态是按 key 分到 subtask 的,key 一歪,某个 subtask 的 SST 就特别多。

compaction 也是按本地数据跑的,数据多的节点合并得更勤,重传得也更多。

于是每个 checkpoint 都有一两个重活节点,别的节点早就传完了。

上传还不只是总量的问题,是短板问题。

一个 checkpoint 要所有 subtask 都传完才算完成。

九十九个传得快没用,第一百个慢,整体就慢。

更麻烦的是排队。

文档里说得很清楚,checkpoint 跑得比间隔久,下一个不会提前触发,等当前完成之后立刻开始。

于是系统会一直处在 checkpoint 里,资源一直绷着。

execution.checkpointing.min-pause能拉开一点距离,但只是把频率降下来,没解决单次为什么慢。

execution.checkpointing.max-concurrent-checkpoints也不是解药,并发多了只会让上传互相抢带宽。

上传是异步的,但完成事件是同步等的。

JobManager 要等所有 handle 到齐才宣布完成,异步只说明它不挡算子处理数据,不代表完成判定可以提前。

把间隔拉长也不是免费。

间隔越长,两次 checkpoint 之间的 delta 越大,恢复点越旧。

RPO 和上传成本,本来就是跷跷板的两头。

指标也别只看一个。

duration 看总时长,size 看这一轮传了多少,full size 看总状态。

三个一起看,才知道是状态大了,还是这一轮改多了。

真正要看的,是单次 checkpoint 里,时间花在哪一段。

看两段就够了,对应 UI 里的 sync 和 async。

快照阶段长,多半是对齐或反压;上传阶段长,才是这篇文章要说的事。

还有一项开销,很多人根本没注意。

checkpoint 目录是会留下来的。

保留几份由execution.checkpointing.num-retained控制,每份增量都引用一批 SST。

引用计数没降到零之前,这些文件不会被删。

所以只要还在产生新 SST,共享目录就会一直长,引用计数归零只回收没人用的旧文件。

FLINK-30461 讲过一个极端场面,上传到一半超时,已经传上去的 SST 没被登记,共享目录里堆到上百万个文件。

这个 bug 在 1.15.4、1.16.2 和 1.17.0 都修了,但它说明了一件事,上传失败不是重来一次就能抹平的,是会留下垃圾的。

本地备份能救恢复,但救不了上传。

execution.checkpointing.local-backup.enabled打开之后,TaskManager 的本地盘还会留一份状态副本,恢复时可以少拉远端。

但 checkpoint 该传的还得传,这份开销不会消失。

还有一个容易看错的数字。

开了增量之后,Web UI 里的 Checkpointed Data Size 就是刚才说的 size,只是这一轮的 delta,不是全量状态。

看到它很小就以为没事,其实总状态可能已经很大,只是这一轮刚好没怎么改。

真要看清,得看上传耗时、checkpoint 目录的增长速度、以及恢复时要拉多少数据。

恢复这事也有反直觉的地方。

网络带宽是瓶颈时,增量 checkpoint 恢复可能比全量更慢,因为要按链把多个 delta 都拉下来。

CPU 或本地 IO 是瓶颈时,增量反而快,因为不用按 Flink 的 key/value 格式重建本地表。

所以增量和全量之间,不是单纯的新比旧好,得看瓶颈在哪。

那怎么办。

我不太喜欢一上来就调间隔。

间隔被吃满,是上传慢的结果,不是原因。

先看三处。

第一处是重写。

compaction 越勤,重传越多。

能少更新、能提前把 key 打散、能让分布更稳,都是在减少重写。

第二处是上传。

本地盘和对象存储之间的带宽、并发、单文件大小,决定了这一轮要多久。

对象存储按请求计费,一个 SST 至少一次请求,文件越小,请求越多。

同一个目录下对象太多,list 也会变慢,清理和恢复都受影响。

小文件多了,光请求开销就够肉疼,让 SST 少而大,往往更划算。

大文件走分片上传,吞吐能上去,但并发和连接数又成了新的上限。

小文件太碎,在状态大的作业里很常见,调参折腾半天,到头来问题还在文件上。

第三处是换路。

ForSt 在 Flink 2.3 里还是实验特性,SST 直接放远端,天生就是增量。

好处是 checkpoint 时不用再整批搬一次。

代价是状态访问要过网络,延迟敏感的场景不一定合适。

但文档明确说不适合生产,只能当方向看。

三条路里,前两条是今天能动的,第三条是明年再看的事。

说实话,这套机制不算复杂,坑都在细节里。

传的是什么,为什么重传,谁在等谁,哪个数字会骗人。

这四个问题里,踩坑最多的是第一个,很多人根本不知道增量传的是 SST。

把这四件事拆开,checkpoint 慢在哪就清楚了。

checkpoint 的时间,最终都花在上传这一步。

本地快照再快,传不出去,这一轮就不算完。

Flink 2.3 的状态后端文档,增量那一节我翻了几遍,最要紧的就两句。

一句是默认不开,得手动打开;另一句是 compaction 会把旧状态重新上传。

这两句读懂了,剩下的都能自己推。

祝大家的 checkpoint,别卡在末尾那个 subtask 上。

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

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

立即咨询