
做实时数据平台久了你就会发现Flink本身并不难上手真正难的是“跑起来之后”。我手上维护的几十个流作业里被问得最多的不是“这个算子怎么写”而是“作业是不是挂了”“延迟怎么又涨了”“为什么反压那么高”。这些问题的答案全都藏在监控指标里。这篇内容想聊的就是我实际搭建的一套Flink监控体系——从Web UI查看到Prometheus指标接入再到规则告警设置把大数据作业日常健康检查这件事完完整整捋一遍。不管你是刚开始接触Flink还是已经在线上一线救火我认为这里面的经验都值得对照着看。1. 为什么Flink作业比批处理作业更需要“日常体检”1.1 流作业没有“结束”这个概念批处理拿一个SQL一跑出数了就是成功。流作业不一样从启动那一天起它就在无边界数据流上持续运行。即使当前完全没数据进来JobManager、TaskManager之间还要维持心跳协调器还要定期触发检查点。你没法用“这分钟成功了”来判断健康只能通过连续观测来判断“当前状态是否偏离预期”。这就决定了监控设计思维的差异批处理关注的是结果流处理关注的是过程。Flink的健康检查本质上是对整个过程做持续抽样看数据是否在被及时消化、状态是否在被稳定保存、资源是否还有余量。这个概念不建立清楚后面看指标就很容易陷入“看到数字却不知道意味着什么”的困境。1.2 健康检查到底查什么我把一个Flink作业的“健康”定义为在不丢数据、不明显增加延迟的前提下作业稳定推进并且在故障发生后可以快速恢复。拆开看大概包含四层含义进程和资源健康JobManager、TaskManager都活着CPU、内存、GC没有出现异常。计算进度健康数据处理速度跟得上数据进入速度Kafka消费Lag没有持续上涨。状态持久化健康检查点定期成功状态后端读写没有报错。业务正确性健康计算结果落在预期范围内脏数据比例没有突变。这四层对应着不同的指标来源也对应着不同的告警级别。日常巡检时我习惯于先看进程层再看计算层最后结合业务自定义指标判断是否真的“健康”。1.3 监控体系失效的几个典型场景有些团队的Flink作业天天挂着监控大屏但事故并没少出。我总结过几个典型症状监控图上全是尖刺和虚线看不出当前哪个环节在吃紧反压从60%到80%没人报警直到Kafka消费Lag冲破天际检查点连续失败三次却没人把失败日志和OOM异常日志关联起来作业上线一个月没看过指标某次发布后重启了八次直到业务方投诉才发现。这些症状的本质不是“缺监控”而是缺少一套把指标转化为动作的规则。监控的价值不在于图好看而在于能缩短定位问题的路径。我的目标很简单让排障时间从小时级变成分钟级让预警在用户发现之前到达。2. 健康检查需要盯住的四层指标2.1 系统资源层别让问题从底层开始Flink作业跑在容器或物理机上底层的资源波动哪怕只有一秒钟都可能引发连锁反应。系统资源层我主要看CPU使用率、内存使用率、磁盘IO和Full GC次数。CPU和内存相对直观重点说GC。Flink是JVM系状态后端无论是RocksDB还是堆内存GC都会直接影响吞吐和响应时间。你可以通过JMX接入观察TaskManager的GarbageCollector相关指标。如果Full GC每分钟超过3次且持续上升一般就说明堆内存压力极大要么是状态存储过大要么是对象分配过于频繁。现在Flink 1.17版本之后对RocksDB的内存管理做了不少优化但底层资源水位该看还是要看。磁盘IO也容易踩坑尤其任务在把检查点写入远程存储比如S3或者HDFS时写带宽会突然飙高。如果后端存储性能跟不上检查点Duration会明显拉长看上去像是Flink自身卡顿实际上问题出在存储侧。所以系统资源监控不能只看机器还要覆盖Flink所依赖的外部存储。2.2 Flink运行时层JobManager与TaskManager的各自视角Flink自带的Metrics体系分三组JobManager、TaskManager和Job/Task/Operator。每组关心的角度不同。JobManager组重点看jobRestarts作业重启次数、uptime作业运行时长、numberOfFailedJobs失败作业数量。这些数值一旦出现异常跳动说明作业整体稳定性有问题。TaskManager组重点看Status.JVM.Memory.Heap.Used堆内存使用、Status.Shuffle.Netty.TotalMemoryUsed网络缓冲内存、Status.Flink.Memory.MemoryUsedFlink托管内存。当Shuffle内存使用逼近上限时往往意味着数据交互量巨大容易触发反压。Task/Operator组则是日常巡检最常用的比如numRecordsInPerSecond、numRecordsOutPerSecond、currentFetchEventTimeLag、watermarkLag。这些指标直接反映了一个算子当前处理数据的速度和延迟。用一个表格整理一下我日常巡检的核心字段指标含义健康参考范围jobRestarts作业重启次数24小时内0次lastCheckpointDuration最近一次检查点耗时小于1分钟且稳定numRecordsInPerSecond每秒输入记录数在合理吞吐区间内无异常突降numRecordsOutPerSecond每秒输出记录数接近输入速率无明显堆积currentFetchEventTimeLag当前事件时间与最新事件时间差值与预期处理延迟一致不持续上涨watermarkLag水印滞后程度满足业务容忍上限backPressureLevel算子反压等级无持续HIGH状态2.3 数据正确性层水位线与处理延迟很多团队监控做了一堆却在“数据到底算得对不对”上缺少手段。这一层核心是看事件时间和水印。你可以通过算子暴露的currentInputWatermark、currentOutputWatermark对比上游判断事件时间是否在持续往前推。如果两个值差距越来越大就说明事件时间处理速度跟不上数据摄入延迟在积累。这里有一个比较容易误判的地方如果上游一段时间没有新数据水印会停滞。此时你会看到numRecordsInPerSecond降到0但作业并没挂。要注意区分“源端没有数据”和“作业处理不过来”。前者是上游问题后者才是Flink自身的瓶颈。2.4 业务自定义层把业务语义变成可监控Flink的指标如果只在系统维度就很难回答“这次业务结果对不对”。我强烈建议在业务算子内部埋一些自定义指标。比如一个订单金额汇总任务可以自定义一个Counter记录“异常订单数”再自定义一个Gauge记录“最近5分钟订单平均金额”。当平均金额出现断崖式下跌或异常订单数飙升时告警能立刻触发而不必等下游报表异常才回过头来排查。埋点方式不复杂在RichFunction里通过getRuntimeContext().getMetricGroup()新增指标即可。后文我会给出具体示例。2.5 阈值是怎么定出来的阈值的设定没有标准答案但有一个原则不要按“平均值”直接定要按分位数。比如检查点耗时用P99而不是平均值三者差异可能很大。另一个原则是阈值要随作业业务量动态调整不能一个值吃遍所有作业。我一般先无告警观察两周记录正常波动范围再取正常值的1.5倍作为报警线这样能降低误报率。3. Web UI是最快的“现场体检工具”3.1 打开Web UI先看哪些页面Flink自带的Web UI很少会用到全部功能但有几个页面是健康检查必须熟悉的Dashboard页最上面一行显示JobManager相关状态重点看运行中的Job数量、TaskManager数量和可用槽位。如果TaskManager数量与配置不符或者槽位余量长期为0说明资源规划出了问题。Jobs列表页每个作业的状态一目了然。RUNNING不代表健康还要点进去看Checkpoints标签页和Metrics页。单个作业的运行监控页依次看Dataflow Graph、Metrics、Checkpoints、BackPressure四个子页。在实际排障时我通常先看作业有没有在重启再看每个节点的numRecordsOutPerSecond是否递减。如果中间某个节点的输出速率明显低于输入速率瓶颈就锁定在这条链路上。3.2 判断一次重启是否异常在“Job Overview”页面Restarting Time和Job Restarts这两项要结合起来看。注意不是所有重启都算事故。比如因人工修改并行度触发的重启属于正常操作但如果是检查点失败触发的自动重启就要立刻追查。一个快速判断方法重启之前是否有大量Checkpoint expired before completing日志。如果有说明检查点超时配合看lastCheckpointDuration和checkpointAlignmentTime基本能定位到是状态体量太大还是对齐时间过长。3.3 Web UI的局限Web UI的指标是内存态的重启后历史数据就没了无法做趋势分析也不能主动报警。它更像一个“现场体检工具”适合坐台诊断不适合长期监测。要让监控真正变成“健康检查体系”必须把指标导出到外部时序数据库中。4. 把指标接入Prometheus搭建长期观测体系4.1 开启Metrics Reporter现在Flink社区最常见的接法是Prometheus配置过程不复杂。先准备一个包含Flink prometheus依赖的镜像或者在lib目录放好对应jar包然后在flink-conf.yaml里加三段配置metrics.reporter.prom.class: org.apache.flink.metrics.prometheus.PrometheusReporter metrics.reporter.prom.port: 9249 metrics.reporter.prom.scope.variables.excludes: flink_job_id;_default第一行指定Reporter类第二行指定暴露指标的HTTP端口第三行是变量过滤。flink_job_id默认会出现在指标标签里但在查询时很碍事ID随机且不可读我建议排除掉改用job_name来区分作业。4.2 让Prometheus抓取指标在Prometheus的配置文件中加一个抓取任务每个TaskManager和JobManager都是一个目标地址scrape_configs: - job_name: flink scrape_interval: 15s static_configs: - targets: [flink-hm-01:9249, flink-tm-01:9249, flink-tm-02:9249]15秒的抓取间隔够用太频繁会对Flink产生额外压力太稀疏又会漏掉短时尖峰。抓取路径默认是/metrics如果你的网络环境比较复杂可以在Flink Reporter配置中追加metrics.reporter.prom.port和metrics.reporter.prom.scope.variables.excludes之外的参数比如metrics.reporter.prom.filterLabelValueCharacters这个参数可以控制标签是否过滤特殊字符默认值会限制标签内容自定义指标名一旦使用中文或特殊符号就会出现问题。4.3 Grafana看板怎么搭指标进入了Prometheus之后Grafana只是最后一步。我会在Dashboard里放这几块核心面板作业存活状态直接用flink_jobmanager_job_uptime做时间序列图如果曲线归零或消失说明作业挂了。重启次数用delta(flink_jobmanager_job_restarts[5m])画出增量一旦出现大于0的点立刻能看出重启时间点。逐算子吞吐用sum by (task_name, operator_name) (rate(flink_taskmanager_job_task_operator_numRecordsInPerSecond[1m]))形成每个算子的读入速率曲线。检查点耗时画出lastCheckpointDuration的P95线观察是否存在持续抬升。反压状态将backPressureLevel映射成数值画出每个Task的反压曲线。这些面板的核心目标是“一眼看出哪个环节异常”不需要面面俱到但要保证最关键的十几个指标都在。4.4 自定义指标实例如果你想让业务状态也进入Prometheus可以在算子里面埋点。以Counter为例public class XxxProcessFunction extends RichProcessFunctionEvent, Result { private transient Counter invalidCount; private transient GaugeDouble avgAmount; Override public void open(Configuration parameters) { MetricGroup group getRuntimeContext().getMetricGroup(); invalidCount group.counter(custom.invalidCount); avgAmount new GaugeDouble() { private double currentAvg 0.0; Override public Double getValue() { return currentAvg; } }; group.gauge(custom.avgAmount, avgAmount); } Override public void processElement(Event value, Context ctx, CollectorResult out) { if (!value.isValid()) { invalidCount.inc(); } // 更新 avgAmount 的逻辑略 } }这种自定义指标会以flink_taskmanager_job_task_operator_custom_invalidCount这样的名字暴露出来。实际使用中我建议名前面统一加custom_前缀和系统指标区分开查询时一眼能认出来。5. 三大“命门”指标检查点、反压与水印延迟5.1 检查点是作业的心电图检查点是Flink状态持久化的核心机制也是我最先看的一个指标。它为什么重要因为检查点记录着算子的状态和数据源消费位点。检查点长期失败意味着作业一旦发生故障恢复时会把数据算重复或者丢状态。看检查点我主要看两张图。第一张是Completed Checkpoints数量曲线如果长时间没有新的Completed检查点说明检查点被卡住。第二张是失败检查点的错误信息去TaskManager日志中搜CheckpointException能看到具体是什么原因导致快照失败。这里最常见的坑是状态体量太大导致检查点超时。默认的超时时间可能设置得太短比如1分钟而状态有几个GB这个配置显然不合理。需要综合lastCheckpointDuration和状态大小来调整state.checkpoints.timeout。另一个坑是多个Task向同一文件系统写快照造成短时间写入瓶颈这时要考虑把检查点存储进行分区。5.2 反压是最容易被误读的指标反压本身是Flink流控机制的一部分不是什么bug。当某个算子的处理速度跟不上输入速度数据会在网络缓冲区和算子内部堆积反压会从下游往上传导。你在Web UI看到某个Task显示High BackPressure真正要思考的不是“怎么去掉反压”而是“瓶颈在哪”。我看过不少新手踩坑一看到反压为100%就盲目增加并行度加完毫无效果。道理很简单如果瓶颈是外部依赖的写入吞吐比如Sink到下游数据库增加并行度只会增加连接数量甚至把下游数据库打挂。增加并行度前先看是哪个算子产生了背压再判断是算子计算耗时高还是输出到外部的链路慢。反压采样时有个操作细节尽量在业务低峰和高峰分别采样不要只看一次结果。一次采样只能代表那一瞬间的状态持续采几次才能区分是偶发抖动还是持续积压。5.3 水印延迟与“假死”作业流处理里最怕的不是作业报错而是作业不报错但也不往前推进。这种假死状态用常规报警很难发现所以我特别强调看水印延迟指标。以事件时间处理为例每个算子都会生成或透传水印。当currentInputWatermark长时间不前进说明事件时间停滞了。这时要排查是上游源没有新数据还是某个keyBy节点出现了数据倾斜个别subtask进度被拖死再或者是有几个key上的数据量极大导致单线程处理不过来。有一种情况特别容易迷惑人如果上游存活时间很长但没有数据水印会被判定为idle从而不参与整体水印推进。如果业务上该时段本来就没数据那算正常但如果是预期的数据没来就要立刻查源头。所以水印阈值要配合业务流量表来定义脱离了业务量谈水印很难有判断标准。6. 把健康检查变成自动告警6.1 告警级别划分不是所有异常都值得立刻呼叫在线人员。告警噪音是监控体系的最大杀手指标暴涨但业务没受影响发一堆告警反而让值班的人越来越疲。我把告警分三级级别典型场景通知方式P0作业挂掉、JobManager失联、检查点连续失败电话短信IM持续升级P1反压持续High、Kafka消费Lag上涨、处理延迟超阈值IM通知自动工单P2单节点CPU偏高、内存占用攀升、GC变频繁早晨合并报表不做实时呼叫P0对应的是“作业不可用”或“数据安全受影响”必须秒级反馈。P1是作业还在跑但质量在恶化给几个小时的响应窗口。P2是潜在风险不用立刻处理但要有趋势跟踪。6.2 一条从指标到IM通知的链路Prometheus配规则配合Alertmanager推送给钉钉或企业微信是常见的组合。一个P0规则的示例groups: - name: flink-alerts rules: - alert: FlinkJobDown expr: flink_jobmanager_job_uptime 0 or absent(flink_jobmanager_job_uptime) for: 1m labels: severity: critical annotations: summary: Flink作业 {{ $labels.job_name }} 已停止运行 description: 作业在主机 {{ $labels.host }} 上失去心跳请立即查看。absent函数用于检测指标是否存在。一旦JobManager重启或指标被清空flink_jobmanager_job_uptime消失absent返回1告警就会触发。for: 1m表示持续1分钟才告警防止瞬时抖动造成误报。6.3 日志监控也不能省指标会撒谎但日志不会。尤其当作业出现罕见异常比如某个网络库的偶发Timeout时指标可能只显示反压上升不能告诉你根因。所以我还习惯将TaskManager日志接入ELK或Loki对关键词做正则匹配告警比如java.lang.OutOfMemoryError、CheckpointException、Exception in thread、Failed to promote等。日志告警的问题是噪音大尤其错误日志在异常恢复阶段会大量出现。我会对日志告警加一个“连续出现N次”的条件比如5分钟内同一关键字出现超过10次才触发。这样既保住信号又过滤了偶发错误。6.4 告警之后要有“操作预案”光有告警不够告警之后还要能快速恢复。我给每个作业都准备了一个“恢复卡”里面写清重启命令、检查点路径、Kafka消费组位置、可能的Sink影响顺序。这个恢复卡平时不用但真正出P0告警时值班人员可以照着执行不用临时翻文档。这其实是监控体系中最容易被忽略的一环。7. 常见问题速查与排障技巧7.1 现象与排查顺序速查现象可能原因排查顺序作业频繁自动重启检查点失败、反压过大导致超时、内存溢出先看JobManager日志再看检查点失败原因最后看GC吞吐骤降但无异常日志上游无数据、taskmanager卡顿、反压传导看numRecordsInPerSecond变化曲线对比水印再看反压延迟越来越高下游Sink慢、外部依赖抖动、算子计算量突增看Sink算子的处理时间压测外部依赖再看数据倾斜检查点耗时暴涨状态体量变大、后端存储变慢、网络抖动查看检查点大小对比存储IO检查网络带宽某个subtask吞吐远低于其他keyBy数据倾斜、单key热点按key统计频次调整并发策略或加salting水印停滞但有数据流过某个source被标记idle、事件时间倾斜看各并行源的水印差排查空闲分区7.2 排障中的几个心得第一看到反压先别看数字先看上下游的吞吐曲线。背压的传导有一个方向从Sink往Source蔓延。你看到某个顶层的任务反压很高多半不是它自身的问题而是地下游堵了。要从DAG最下游开始逐层往前查。第二检查点失败不一定是Flink问题。很多次排查到最后发现是HDFS在做滚动或NameNode出现抖动检查点写入超时。排查时不要只看Flink还要看外部存储的监控。第三日志采集中TaskManager的日志经常是分散的。如果用了容器编排注意把taskmanager.log汇聚到一个统一存储里否则排查时要一台台登录机器效率极低。第四注意区分背压和直播间的概念。Kafka消费Lag高反映的是源头数据积压而反压反映的是Flink内部处理不过来的压力。两回事处置方案完全不同。前者优先考虑扩容消费并行度后者优先定位处理瓶颈。第五监控指标要和代码版本绑定。有一次我发现线上作业一天的吞吐趋势异常查了很久才发现是上线了新版本扩大了窗口大小。如果每个版本发布时能自动记录指标基线这类排查就能省掉一大半力气。7.3 关于资源分配的监控陷阱还有一个不算低级但很常见的错误看到numRecordsInPerSecond比预期高很多就忙着加并行度。请注意并行度提高了每个subtask的本地状态会分散检查点时间可能变长Kafka分区到算子的连接方式也会变。如果没有做好分区映射增加并行度反而让处理慢了下来。所以每次调整资源后至少要观察一整天完整指标曲线不能只看10分钟就下结论。8. 我个人在实操中积累的几点体会先说一个最深刻的教训监控图不是越多越好。我自己曾经把Grafana叠了二十多张图结果真正用到最多不超过十张。后来我砍掉了大量重复的磁盘IO和网络包指标只保留直接对应故障场景的十几个指标反而更管用。核心原因是监控的终点是“快速决策”不是“追求全面”。指标一旦多到看不完它就不是助力而是噪音。还有一个经验是给阈值留“观察期”。新指标上线头两周我都不会配告警只做记录。因为很多指标在真实运行环境中的波动远超预期如果一开始就按书本值配报警只会收获一堆误报。等到跑了一两周把P95和P99摸清楚再确定报警线准确度会高很多。最后的建议是监控体系要跟着事故走。每次出现线上故障解决之后都要问一个问题如果当时监控里多了一个指标或者多了一条规则这次事故是不是能更早发现我把这个过程当做监控体系迭代的触发点。慢慢地监控建的规则越来越精准排障也越来越顺手。希望这篇Flink监控实战经验能让你少走几段弯路。