Flink监控实战:Metrics指标、背压与Checkpoint排障指南
1. 先把“监控”这件事拆开看第一次翻 Flink 官方文档里 Operations 下面的 Monitoring 那一页我其实是有点失望的。整页内容不长大段都是“有哪些指标”“指标叫什么名字”“怎么配置 reporter”看着像一份指标清单不太像一份能直接拿来用的排障手册。真正跑起来之后我才慢慢意识到这一页更像索引而不是整套监控方案的完整说明它告诉你 Flink 内部会把哪些数值暴露出来但“这些数值背后对应的到底是什么环节、哪个数值变了该往哪查、采集本身会不会反过来拖垮作业”全得自己补上。Flink 监控笔记、Metrics 指标、背压、Checkpoint、指标上报这几个词基本就是这篇笔记的主线。我把自己的理解归纳成一句话Flink 的监控体系本质是把“一个分布式流式作业内部发生了什么”翻译成一组能被外部系统持续拉取的数值而你要做的是判断“哪几个数值足以让你在用户投诉之前发现问题”。这篇内容适合两类人一类是刚接手 Flink 作业、看到一堆flink_taskmanager_job_task_operator_xxx指标不知道重点看哪个的同学另一类是已经在用 Flink 但监控面板靠“能显示就行”搭起来、告警经常误报或漏报的人。我会从指标的产生机制讲到实际配置、告警规则再到我自己踩过的几个比较典型的坑。文中涉及的版本差异会标出来因为 Flink 在 1.13 到 1.18 这段跨度里监控相关的改动不算少照抄某篇旧文章的配置大概率会卡住。2. Metrics 体系指标到底是怎么长出来的2.1 MetricGroup 决定了指标名而不是你刚接触的时候我有个误解以为指标名是固定的一串字符串。实际上 Flink 的指标名是运行时由层层嵌套的 MetricGroup 拼出来的。一个 TaskManager 上的算子指标完整路径大致是host.taskmanager.tm_id.job_name.operator_name.subtask_index.metric_name这个结构解释了为什么指标名会这么长也解释了两个很常见的困惑。第一个困惑是“为什么我本地跑出来的指标名和线上不一样”。因为host和tm_id是变量换机器、扩缩容之后这些段就变了。所以在做告警规则的时候绝对不能把 TM ID 写进匹配条件否则一重启就失效。正确的做法是用 PromQL 的正则或者sum by把变量段聚合掉。第二个困惑是“为什么我在代码里注册了一个counter(parseError)在监控里搜不到”。大概率是被淹没在几百个子任务的同名指标里了。这时候要么用getRuntimeContext().getMetricGroup().addGroup(biz)主动加一层分组要么在上报端做过滤。经验给自定义指标加一个独有前缀组比如addGroup(biz).addGroup(order)后期在几万个指标里定位会轻松非常多。这是不写进文档但极省时间的习惯。另外 Flink 还提供了几组 scope 配置来裁剪指标名长度比如是否带上 host、是否带上 task 名字相关参数在metrics.scope.*这一族里。指标名太长不只是难看的问题它会直接推高后端的存储基数。2.2 五种指标类型选错了后面全是麻烦文档里列了Counter、Gauge、Meter、Histogram、Distribution几种类型我当时扫了一眼就过去了后来发现选型选错会直接导致面板算不出正确结果。类型语义典型用途使用注意Counter只增不减的累计值错误总数、记录总数求速率必须用rate()/increase()直接看值没意义Gauge瞬时值可上可下队列长度、内存占用、水位采样点之间发生了什么看不到Meter由框架计算的平均速率每秒记录数内部是滑动窗口短时抖动会被抹平Histogram分桶统计延迟分布Flink 内置实现是近似分桶分位数不精确Distribution可交给后端算分位需要较准分位的场景依赖 reporter 是否支持换版本容易翻车说几个具体的坑。Counter是最容易被误用的我在面板上直接画了numRecordsIn的原始值结果是一条一直往上爬的斜线看不出任何信息量正确姿势是rate(xxx[1m])或者sum(increase(xxx[5m]))。Meter看着很省事numRecordsInPerSecond直接就是速率但我实测下来它对突发的响应偏慢因为内部有窗口平滑。真正的瞬时压力我宁可看busyTimeMsPerSecond这类按秒统计的指标。Histogram的问题更隐蔽。Flink 自带的 Histogram 是简单分桶实现落到 Prometheus 上一般输出成 summary 形式附带 0.5、0.95、0.99 之类的分位。这些分位是近似值我曾经拿它去对账业务侧的 P99 延迟差了将近一倍后来才明白两边统计口径和分桶精度都不一样。如果你的 SLA 需要严格分位就得考虑用Distribution类型并把聚合交给后端但前提是你用的 reporter 和版本支持配之前先在测试环境看一眼实际输出长什么样。2.3 自定义指标十行代码换一个能报警的信号内置指标覆盖的是框架视角业务视角的东西它不知道。比如“解析失败的订单数”这个只能自己埋。最小可用的写法是这样public class OrderParseMapper extends RichMapFunctionString, Order { private transient Counter parseError; private transient Counter parsed; Override public void open(Configuration parameters) { MetricGroup biz getRuntimeContext() .getMetricGroup() .addGroup(biz) .addGroup(order); parseError biz.counter(parse_error_total); parsed biz.counter(parsed_total); } Override public Order map(String value) { try { Order o parse(value); parsed.inc(); return o; } catch (Exception e) { parseError.inc(); return Order.invalid(value); } } }这段代码里有几个细节值得说。指标句柄必须在open()里初始化不能写成成员变量直接new因为getRuntimeContext()只有在算子初始化之后才可用早期版本在构造函数里调用会直接抛异常。transient关键字是必须的Flink 算子会被序列化分发到各个 TaskManager非 transient 字段会导致序列化失败。还有一个观念上的问题别把高基数标签塞进指标名或者 MetricGroup。我见过有人在 group 里拼订单 ID 或者用户 ID几秒钟就把后端的时序数量打爆了。指标的维度应该是有限枚举的比如状态码、渠道、错误类型这个边界必须守住。3. 内置指标体系哪几个数值真正值得盯3.1 进程与资源层先确认机器没被自己拖死这一层的指标来自 JVM 和操作系统名字里通常带Status.JVM、Status.Flink.Memory之类。我常看的有这几类堆内存使用Heap_Used、Heap_Max。看绝对值意义不大要看的是使用曲线是否呈锯齿状并且 GC 之后能降回去。如果 GC 后基线一路上移那就是泄漏的信号。GC 次数与耗时老年代 full GC 的Count和Time。这两个指标一涨作业的处理延迟基本同步上涨。Managed Memory 使用RocksDB 状态后端场景下这一项非常关键超过配额之后会触发溢写甚至 OOM而堆内存指标看起来一切正常很容易误判。网络缓冲池使用率outPoolUsage、inPoolUsage。这组指标是理解反压的第一现场。这里有个很实在的经验JVM 指标和 Flink 自身的阈值判断要放在一起看。单看堆内存 80% 就报警规则会很吵改成“堆内存持续 5 分钟高于 85% 且 full GC 次数在增加”误报率会降一个数量级。3.2 作业与算子层吞吐和忙碌度算子层最核心的几个指标是numRecordsIn、numRecordsOut、numRecordsInPerSecond、numRecordsOutPerSecond以及numRecordsOutErrors。前四个指标在排查“哪个算子变慢了”时构成一条完整的证据链上游流出速率正常、下游流入速率掉下来问题就锁定在中间这个算子。但我现在更依赖的是busyTimeMsPerSecond、idleTimeMsPerSecond、backPressuredTimeMsPerSecond这一组毫秒级指标较新版本提供。它们的好处是分母固定为 1000 毫秒可以直接做百分比理解busy长期接近 1000算子在满负荷跑CPU 是瓶颈。busy很低但backPressured很高算子在等下游压力来自后面。idle很高上游没数据可能不是你的问题。这组指标比numRecordsInPerSecond更能说明“算子到底在忙什么”因为后者只是吞吐吞吐低可能是没数据也可能是处理慢这两种情况的处理方式完全相反。注意这组指标在不同版本里的可用性和命名有过调整升级前先去实际指标端点确认一下名字别直接照搬面板配置。3.3 网络与背压别把“反压”当成一个独立故障背压这个事文档给了采样机制说明但没讲清楚怎么用它做判断。Flink 的背压检测是通过采样 Task 的输出缓冲区占用情况实现的处于high状态意味着下游消费不过来。我踩过的坑是看到背压就去加并行度结果越加越糟。因为背压只是症状根因可能在三个完全不同的地方下游算子真的算得慢CPU 打满busy高下游在做同步外部调用比如逐条写数据库线程被阻塞数据倾斜某一个 subtask 承担了绝大部分数据。区分方法也不复杂看背压是均匀分布还是集中在少数 subtask。如果是第 3 种所有 subtask 的背压指标会呈现极端的两极分化这时加并行度只是把数据分到更多空闲的 subtask 上热点依旧存在控制手段应该是改 key 或者加本地预聚合。另外网络层的numBytesInLocal、numBytesInRemote也值得看一眼。Remote 比例突然上升说明数据在节点之间发生了大量重分布常见于 keyBy 之后的热点或者上游并行度变更后的 rebalance。3.4 Checkpoint 与状态层正确性问题的唯一信号吞吐掉了用户能感知到但 checkpoint 出问题往往是静默的等到需要恢复的时候才会暴露那时候就晚了。JobManager 侧这几个指标是我必看的lastCheckpointDuration完成一次 checkpoint 的耗时。它和 checkpoint 间隔的比值是关键超过 50% 就该警惕超过 80% 基本就是在排队。lastCheckpointSize/lastCheckpointFullSize状态大小趋势。注意版本差异早期版本里这个字段含义有过调整别拿新旧数据直接对比趋势。numberOfFailedCheckpoints/numberOfCompletedCheckpoints这两个要和numRestarts一起看连续失败通常意味着状态后端或者外部存储出了问题。lastCheckpointRestoreTimestamp确认作业是不是刚发生过重启恢复。还有一个容易被忽略的点checkpoint 对齐时间。早期版本有checkpointAlignmentTime这类指标后来因为 unaligned checkpoint 的引入部分对齐相关指标被调整甚至移除。如果你升级后发现面板某条线消失了先别怀疑采集坏了很可能是指标本身被删了。这种情况我一般会去翻对应版本的 release notes 确认。4. 动手搭链路从配置到告警4.1 把指标出口打开以 Prometheus 生态为例配置大致是下面这样参数名以你手上版本为准新旧版本在class和factory.class上有区别# flink-conf.yaml metrics.reporters: prom # 较新版本用 factory.class metrics.reporter.prom.factory.class: org.apache.flink.metrics.prometheus.PrometheusReporterFactory metrics.reporter.prom.port: 9249 # 只上报需要的指标控制基数 metrics.reporter.prom.filter.includes: jobmanager.job.*;taskmanager.job.task.*这里有三件事必须提前想清楚。第一reporter 的 jar 包要在lib/下。Prometheus reporter 不是默认打包进去的只改配置不扔 jar启动时日志里会有Reporter factories相关的提示但作业照常跑指标就是没有很容易被忽略。我第一次配置就卡在这里半小时。第二端口会飘。多个 TaskManager 跑在同一台机器上时配置的端口被占用后 reporter 会自动往后找可用端口。如果你在抓取配置里写死了端口就会出现“有的 TM 采到了有的没有”的情况。稳妥的做法是让每个 TM 使用不同的端口区间或者干脆走服务发现。第三上报内容要做过滤。默认情况下所有指标全开一个中等规模作业的指标数量轻松上万采集开销和后端存储都会受影响。用filter.includes/filter.excludes精确控制前提是你得先跑一轮看看有哪些指标真的有用。4.2 抓取配置与标签重写scrape_configs: - job_name: flink scrape_interval: 30s scrape_timeout: 10s static_configs: - targets: [jm-host:9249] labels: role: jobmanager - targets: [tm-host-1:9249, tm-host-2:9249] labels: role: taskmanagerscrape_interval设成 30 秒是个折中。设太短比如 5 秒采集本身会消耗可观的 CPU尤其是指标数量大的时候设太长短时的抖动会被漏掉。我一般让 JVM 和核心业务指标走短间隔长尾的算子级指标走长间隔分两个 job 抓。标签重写这一步很关键。Flink 上报的指标名里已经带了 host 和 TM ID 这些变量段如果不做处理扩缩容之后同一个逻辑指标会分裂成多条时间线。处理方式是用metric_relabel_configs把不需要的标签去掉或者用relabel_configs把role这类固定标签补上方便后续按角色聚合。提示改动抓取配置之后先在 Prometheus 的 target 页面确认所有实例都是 UP再看 Graph 里有没有数据。跳过这一步直接配告警后面排查会分不清是“规则写错”还是“根本没采到”。4.3 几条真正救过场的告警规则告警规则我遵循一个原则宁可少每条都要能对应一个明确的动作。下面几条是我实际在用的# 1. 重启次数在最近 10 分钟内增加 increase(flink_jobmanager_job_numRestarts[10m]) 0 # 2. checkpoint 在最近 15 分钟内出现失败 increase(flink_jobmanager_job_numberOfFailedCheckpoints[15m]) 0 # 3. checkpoint 耗时占比过高接近间隔的一半 flink_jobmanager_job_lastCheckpointDuration / on(job_name) group_left flink_jobmanager_job_lastCheckpointDuration 0.5第三条规则实际写的时候不能这么草率因为 Flink 没有直接把“checkpoint 间隔”当成一个指标暴露出来间隔是配置项。我的做法是在业务侧自己加一个 gauge把checkpointInterval作为常量写进去或者干脆用固定阈值——先确认间隔配置是 60 秒那么lastCheckpointDuration 30000就直接告警。这种“用已知配置代替动态计算”的做法很土但长期维护成本最低。再补两条针对背压和延迟的# 4. 背压时间占比持续偏高 avg by (job_name, operator_name) ( flink_taskmanager_job_task_backPressuredTimeMsPerSecond ) 600 # 5. 作业端到端延迟分位超阈值需开启 latency tracking flink_taskmanager_job_latency_source_id_operator_id_quantile{quantile0.99} 1000第五条里的指标名我故意写得模糊因为 latency 指标的实际名字取决于metrics.latency.granularity的配置operator/subtask/job三档粒度会生成完全不同的指标名。这个配置项直接决定了指标名长什么样改档位等于改面板改动之前一定要评估影响。顺带说 latency tracking 的原理开启之后source 端周期性地往数据流里插入一个标记算子处理到这个标记时和本地时间对比算出延迟。所以它测的是端到端链路的近似延迟不是单条记录的真实耗时而且只在开启后才有数据。metrics.latency.interval设成 60000 意味着每分钟才插一个标记采样密度很低别指望它捕捉秒级抖动。4.4 REST API最快的一次现场检查面板没配好或者来不及配的时候REST API 是最快的入口# 指定作业的指定指标 curl -s http://jm:8081/jobs/jobid/metrics?getlastCheckpointDuration,numRestarts # 某算子的背压状态 curl -s http://jm:8081/jobs/jobid/vertices/vid/backpressure # 最近一次 checkpoint 的详细统计 curl -s http://jm:8081/jobs/jobid/checkpoints # 所有 TM 的资源概览 curl -s http://jm:8081/taskmanagers我在处理后一个问题时习惯的顺序是先/jobs/overview看整体状态和重启次数再/jobs/id/checkpoints看最近几次 checkpoint 是大是小、耗时分布如何然后/jobs/id/vertices/vid/backpressure定位是哪个算子在堵。这三步走完八成的线上问题已经有了方向。还有两个冷门但非常好用的端点线程转储和火焰图。算子卡死或者 CPU 异常飙高时线程转储能直接看到卡在哪个方法上火焰图需要在配置里显式开启rest.flamegraph.enabled: true采集几十秒就能出图比凭经验猜高效得多。这两个我建议平时就打开出事的时候再改配置往往来不及。5. 排障实录几个反复出现的坑5.1 指标列表是空的这是最高频的问题排查顺序我固化成了四步看 reporter 的 jar 有没有放进lib/。没有的话配置写了也没用日志里不会有明显报错。看端口通不通。直接在 TM 机器上curl localhost:9249/metrics如果连不上说明 HTTP server 没起来。看端口是不是飘了。同机多 TM 的情况下去日志里搜 reporter 启动时的实际端口。看 filter 是不是写错了。过滤规则是包含式的配了一个错误的 pattern 会静默地把所有指标过滤掉。还有一种情况指标端点能访问但返回内容极短。这通常是因为查询的作业没有运行中的 task只有 JobManager 层级的少数指标。5.2 采集把自己拖垮了这个坑比较隐蔽。我们有一次指标数量增长到六位数Prometheus 的抓取请求本身开始超时导致数据出现空洞值班同学看到“指标断断续续”以为是作业不稳定白白排查了半天。根因是subtask 级指标 × 算子数 × 作业数的组合爆炸。一个 100 并发的作业每个算子的每个 subtask 都贡献一份指标多几个作业就是天文数字。处理方式有三条按性价比排序砍掉 subtask 维度绝大多数告警只需要作业级或算子级的聚合值subtask 级只在排查数据倾斜时临时打开。用过滤规则精确保留只放行面板上真正画了的指标。分级采集核心指标高频抓长尾指标低频抓甚至只在排障时临时开启。5.3 把背压、延迟、空闲混为一谈这三个概念经常被混着用实际含义完全不同现象观测指标真实含义常见处理背压backPressuredTimeMsPerSecond高下游消费不过来查下游算子的 busy 和外部调用延迟高latency 分位高数据在链路里停留久查是否是某个算子排队空闲idleTimeMsPerSecond高上游没数据多半不是故障别误报我见过最典型的误判是作业吞吐掉了一看有背压就扩容扩完还是慢。真正原因是某次外部接口超时变长导致下游算子的处理线程被阻塞背压只是这个阻塞向上传导的结果。背压是一条线索不是结论。5.4 一份随手可查的速查表症状优先查看大概率原因作业频繁重启numRestarts、异常日志OOM、外部依赖不可用checkpoint 越来越慢lastCheckpointDuration、状态大小状态膨胀、外部存储变慢吞吐下降但 CPU 不高backPressuredTimeMsPerSecond下游阻塞或数据倾斜部分 subtask 明显更慢各 subtask 的busyTimeMsPerSecond热点 key、分区不均延迟分位突然抬高latency 指标、GC 时间长 GC、算子排队指标数据断档reporter 端口、抓取超时端口冲突、采集过载6. 最后说几句实际体会这套东西我从头到尾配过三遍每次换环境都会重新踩几个老坑。最深的一点体会是Flink 的监控能力其实是够的难点从来不在“有没有指标”而在“你有没有提前想清楚哪些指标对应哪些故障”。官方文档给的是原料清单真正有价值的是你自己那套“看到这个数变了我知道该去查什么”的条件反射。另一个建议是别一上来就追求大而全。先挑五个指标——重启次数、checkpoint 耗时、背压时间、算子忙碌度、JVM 老年代 GC——把这五个配成能真正触发并且有人响应的告警比堆一屏没人看的面板有用得多。等这套跑顺了再往上加 latency、加业务埋点、加数据倾斜分析节奏会舒服很多。再补一个小技巧每次版本升级前先把当前环境的关键指标名导出成一份快照升级后逐个对比。指标改名或者被移除是最容易被忽略的变更等告警失效了才发现代价往往不小。