线上作业的 Checkpoint 突然从 30 秒涨到 4 分钟,第一反应肯定是翻日志,可日志里干干净净,连个 WARN 都没有。这种时候唯一能救场的就是 Flink 的监控体系——Metrics、Reporter、反压指标、Checkpoint 统计,这套东西平时不起眼,出事时是唯一的信息来源。《Flink 官方文档 - Operations - Monitoring》这一页看着像配置项清单,实际上它是整个可观测性的地基:metric 类型怎么分层、scope 怎么拼、reporter 怎么接、哪些指标能翻译成排障语言。这篇笔记我按自己实际搭监控链路的顺序重新梳理了一遍,把文档里没写透的取舍和踩过的坑都补上,适合正在给 Flink 作业搭监控、或者已经搭了但看板一直不准的同学对照着看。
1. 监控之前先认清坐标系:Metrics 类型、Scope 层级与命名规则
很多人接监控的第一步是抄一份 Prometheus 配置贴进 flink-conf,然后打开页面发现指标一大堆但根本对不上号。问题不在 reporter,在于没搞清楚 Flink 的指标是用什么规则命名的。这一层不理解,后面所有的看板、告警、排障都是在猜。
1.1 四种 Metric 类型与它们各自适合承载什么
Flink 的 Metric 接口下面挂了几个实现,官方文档里列得比较简略,但对排障来说差别巨大。
Counter是单调递增的计数器,典型代表是numRecordsIn、numRecordsOut、numberOfCompletedCheckpoints。它的特点是只增不减,除非作业重启归零。这一点在实际用的时候必须记住:如果你在看板上直接画 Counter 的绝对值,得到的是一条永远向上的斜线,毫无意义;你要么画它的导数(Prometheus 里用rate()),要么直接看它对应的 Meter 版本。
Gauge是瞬时值,最灵活也最容易被滥用。lastCheckpointDuration、currentInputWatermark、堆内存占用这些都属于 Gauge。Gauge 的问题是它只在被采集的那一刻有意义,两次采集之间的抖动完全丢失。所以判断一个 Gauge 是否异常,绝对不能看单点,必须看持续性——我的习惯是任何基于 Gauge 的告警都加一个"持续 N 分钟"的条件,单点超阈值的告警基本都是噪音。
Meter是速率统计,Flink 内部用它算每秒记录数,暴露出来的是numRecordsInPerSecond这类指标。它内部维护了多个时间窗口的滑动平均,好处是平滑,坏处是迟钝——流量真的掉零之后,Meter 还会带着历史均值缓慢下滑好几分钟,所以判断"作业是否卡死"不要用 Meter,用 Counter 的rate()更干脆。
Histogram是分布统计,端到端延迟、状态大小这类需要看长尾的场景才用它。Histogram 的坑最多,后面 6.3 会专门讲它在 Prometheus 侧的形态问题。
一句话总结选型:想统计总量用 Counter,想知道此刻状态用 Gauge,想看速度用 Meter,想看长尾和分位数用 Histogram。选错了类型,看板画出来就是错的,而且错得很隐蔽。
1.2 Scope 是怎么一层层拼出来的,为什么它会决定你查不查得到数
Flink 的每个指标都带一个 Scope,Scope 是一个用分隔符连接的层级字符串。默认情况下,TaskManager 上的任务级指标大致长这样:
<host>.taskmanager.<tm_id>.<job_name>.<task_name>.<subtask_index>.numRecordsInJobManager 侧则是<host>.jobmanager.<job_id>...这种形态。这套拼接规则由一组配置项控制,metrics.scope.jm、metrics.scope.tm、metrics.scope.task、metrics.scope.operator分别定义四个层级的模板,模板里可以用${Variable}这样的占位符。
理解这一点之后,很多"玄学问题"就有解释了。比如为什么同一个numRecordsIn在存储里会有成百上千条时间序列?因为每个 subtask 都有一条,并行度 200 的作业就是 200 条。再比如为什么重构时给算子改了个名字,历史曲线就断成两截?因为 scope 里带的是task_name/operator_name,名字一变,时间序列的名字就变了,存储那边视作两条全新的序列。
注意:scope 里拼接的是算子的名字,不是
uid()。给算子设置 uid 能保证状态恢复时的兼容性,但救不了指标断链。想让看板稳定,要么提前把算子命名规范定死,要么在看板层做一层名字映射。
另外还有一个容易被忽略的点:metrics.scope.task和metrics.scope.operator是两个独立的模板,同一个逻辑位置可能同时产出两套指标(一套以算子链里的 task 名命名,一套以 operator 名命名)。并行度上来之后指标量直接翻倍,这也是后面讲基数治理时要处理的对象。
1.3 Variables 与默认过滤:指标"消失"的第一类原因
配好 reporter 之后最常见的抱怨是"文档里说有这个指标,我这里查不到"。绝大多数情况不是 bug,是被默认配置过滤掉了。
第一个高频例子是主机层面的 CPU、内存指标。这类系统资源指标默认是关的,需要显式打开对应开关,否则你只能看到 JVM 堆,看不到容器实际吃了多少内存,误判率极高。第二个例子是 Job 状态类的 Gauge,它们受一个单独开关控制,某些版本默认只暴露部分状态,想让所有状态都参与告警就得手动调整。
第三个例子更隐蔽:scope 的变量集是可以扩展的。部分版本支持往 scope 里注入额外变量(比如从系统属性里取值),这样你能给不同集群、不同环境打上标签。但键名在不同大版本之间有过调整,我不建议背,直接用的时候去 Configuration 页面搜scope关键词,以你手上那个版本的说明为准,五分钟的事。
还有一类是 reporter 级别的过滤。较新版本给 reporter 提供了 include / exclude 形式的过滤能力,可以按指标名前缀裁剪;老版本没有这个能力,只能在采集端或者聚合层做减法。这一条直接决定了你的监控系统会不会在半年后被指标量压垮。
2. Reporter 选型的真实取舍:JMX、Slf4j 与 Prometheus 的边界
文档里列了一长串 reporter,看着像多选题,实际上每个都有明确的使用场景。选错了不是不能跑,是会在关键时刻掉链子。
2.1 几种常见 Reporter 的能力对照
| Reporter | 数据流向 | 适合场景 | 明显短板 |
|---|---|---|---|
| JMX | 进程内 MBean | 单机本地排查、临时看某个指标 | 远程连接配置繁琐,无长期存储 |
| Slf4j / 输出到日志 | 写日志文件 | 快速验证某个指标是否存在 | 高频写入污染日志,无法聚合 |
| Prometheus | HTTP 端点被拉取 | 生产环境长期监控 | 需要处理端口分配与抓取配置 |
| 时序数据库类(推送型) | 主动上报 | 没有抓取通道的网络环境 | 需要额外维护写入链路 |
我自己的习惯是:本地起单机调试时用 JMX,因为它零依赖,JConsole 连上去就能看;一旦进入联调环境,立刻换成 Prometheus 形态,哪怕只是单机,因为"能被curl出来的指标"和"必须开图形客户端才能看的指标",在排障效率上差着一个数量级。凌晨三点出问题的时候,你不会想打开 JConsole。
JMX 还有一个隐形成本:远程访问需要开一堆启动参数,而且 RMI 会占用不止一个端口。在容器化环境里这意味着要额外开放端口、处理主机名解析,稍有不慎就连不上。这不是不能做,是性价比太低。
2.2 Prometheus Reporter 的配置项逐条拆解
生产上主流方案是让 Flink 暴露一个 HTTP 端点,由采集侧定期拉取。配置大致长这样:
# 新版用 factory.class,老版是 class,升级时最容易在这里翻车 metrics.reporter.prom.factory.class: org.apache.flink.metrics.prometheus.PrometheusReporterFactory # 端口支持区间写法,多实例同机部署时必须用区间 metrics.reporter.prom.port: 9249-9269 # 采集间隔,默认 10s,不要无脑调小 metrics.reporter.prom.interval: 10 s逐条说下为什么这么配。
port用区间而不是固定值,是踩过大坑的。同一台机器上跑多个 TaskManager 是常态,如果都配 9249,第二个进程的 reporter 会因为端口被占而启动失败,而这个失败往往只打一条不显眼的日志,等你发现某个 TM 完全没指标的时候,作业可能已经跑了半天。用区间写法让每个实例自己找空位,虽然端口不固定会给抓取配置带来麻烦,但至少不会静默丢数据。
interval的默认 10 秒是有道理的。这个值决定了两件事:一是采集的精度,二是指标系统自身的开销。把间隔调到 1 秒,指标量不变但写入频率翻十倍,在高并行度作业上会让 reporter 线程吃掉可观的 CPU。我一般保持 10 秒,只有在定点排查某个瞬时问题时才临时调到 2 秒,排查完立刻改回去。
如果你的版本支持标签提取能力,可以把 scope 里的某一级变量提成一个稳定的 label,同时充当过滤条件。这个功能很好用,但要小心:正则写错了不是"少几个标签",而是那批指标整条消失。配完之后一定要打开/metrics端点确认输出里有你想要的东西,别跳过这一步。
另一个经常被忽略的关键点是:factory.class和老的class两种写法不能混用,新版如果不写factory.class会直接报找不到 reporter。升级大版本时,配置文件里那些看着还能用的旧键,未必还生效。
2.3 插件 jar 放哪里、怎么验证真的生效了
reporter 的实现类不是默认加载的,得把对应的 jar 放到能被加载的位置。发行包里通常在opt/目录下躺着一个,需要你手动挪到plugins/metrics/或者lib/。两种方式都能跑,我更倾向plugins/:类加载器隔离更干净,不会和用户代码里的依赖打架。
验证是否生效有个三步法,我每次都按这个顺序走:
- 启动后直接
curl http://<tm_host>:9249/metrics,有输出说明端点起来了; - 如果 404 或者连接拒绝,去 TaskManager 日志里搜 report 相关的 WARN,通常是类没找到;
- 端点通了但输出为空,说明 reporter 起来了但没匹配到指标,回头检查过滤配置和 scope 模板。
第三步最容易被跳过。端点返回 200 但内容为空,很多人以为是"指标还没产生",实际是过滤条件把全部指标干掉了。等了一晚上才发现是配置问题,这种教训一次就够了。
3. 把官方指标清单翻译成排障语言
官方文档给了完整的指标清单,但清单本身不会告诉你"这个数变大意味着什么"。这一节是我自己踩坑之后整理出来的翻译表,按排障场景组织。
3.1 Checkpoint 三兄弟:次数、时长与大小
Checkpoint 相关指标永远是第一优先级。核心看三个维度:成功与失败的次数比、最近一次耗时、最近一次状态大小。
判断逻辑是这样的:如果lastCheckpointDuration持续上涨,但lastCheckpointSize基本稳定,八成不是状态变大,而是 barrier 在链路里走不快——要么是某个算子反压严重,barrier 排在数据后面进不去;要么是状态后端写入慢,比如远端存储抖动。这两种原因的处理方式完全相反,一个要扩容或优化算子,一个要找存储侧。所以必须两个指标一起看。
如果lastCheckpointSize出现跳变式增长,那就要怀疑状态本身出了问题:key 里带了时间戳导致状态无限累积、State TTL 配了但没真正生效、某个ListState只增不清。这类问题不看趋势图是发现不了的,因为作业本身可能跑得好好的,只是状态在慢慢吃掉磁盘和网络带宽。
还有一个很实用的组合:失败次数除以总次数。单次失败在长周期作业里是常态(网络抖动、存储超时),但如果这个比值在爬升,说明是系统性问题而不是偶发。我一般会把"连续失败次数"作为告警条件,比"总失败数大于零"精确得多。
| 观察到的现象 | 首选指标 | 交叉验证 | 常见根因 |
|---|---|---|---|
| 耗时上涨、大小稳定 | 最近一次耗时 | 反压时长占比 | barrier 被反压阻塞或存储写入慢 |
| 大小跳变 | 状态大小 | 各算子状态大小分布 | 状态无界增长、TTL 失效 |
| 失败率爬升 | 失败/总次数比 | 异常日志 | 存储侧不稳定、超时配置过紧 |
| 恢复后耗时异常 | 恢复时间戳 | 恢复后的首个小 checkpoint 耗时 | 冷启动、状态反序列化开销 |
恢复之后的第一两个 checkpoint 通常偏慢,这是状态预热和 JIT 还没跑热的正常现象,不要在这个窗口里报警,否则每次重启都会误报一次。
3.2 反压的三种观测口径与各自的局限
反压是 Flink 排障里最容易看错的东西。至少有三套口径在看同一件事,每套的结论都可能不一样。
第一套是任务级别的缓冲池占用率,也就是输入、输出两个池子的使用率。判断逻辑很直观:某个任务的输入池占用率高,说明它的输入队列堆满了,它自己处理不过来,瓶颈在它;输出池占用率高,说明它的输出发不出去,瓶颈在下游。这套指标的好处是能快速定位瓶颈在哪个算子的哪一侧,缺点是它只是"水位",看不出持续时间。
第二套是忙闲统计,也就是每秒内被反压的时间、真正忙的时间、空闲的时间。这三个值加起来约等于 1000 毫秒(每个采集周期),所以你可以直接用比例来判断。被反压时间占比长期高于 50%,基本可以确定这个算子是受害者而不是元凶——它想干活但下游不给机会。反过来,如果一个算子忙时间接近满格、反压时间很低,那它很可能就是瓶颈本身。
第三套是线程栈采样,老版本的 Web UI 里那个反压页面就是干这个的。它在若干次采样里看任务线程停在哪个位置,停在缓冲区写入上的比例高,就判定为反压。这套方式的优点是能直接看到"卡在哪个方法",缺点是需要采样时间,对瞬时抖动不敏感,而且在高版本里这套机制已经被前两套替代。
注意:这三套口径的采样周期和计算方式都不一样,同一时刻得出的结论可能互相矛盾。我的做法是固定用第二套做主判断,第一套做交叉验证,第三套只在需要定位到具体代码位置时才用。
还有一个细节:忙闲统计在旧版本里的指标名带accumulated前缀,新版本去掉了这个前缀,语义也做了调整。如果你的看板是跨版本复用的,这一处一定会出问题,后面 5.2 会专门讲。
3.3 网络缓冲与 detailed metrics 的开启代价
Flink 的网络栈内部是有缓冲池的,缓冲池的容量、每个通道分到多少,都会直接影响上面那套"输入输出池占用率"的基线。同样是 0.6 的占用率,在缓冲充足和缓冲紧张两种配置下代表的风险完全不同。所以看这类指标时必须知道当前作业的缓冲配置,不能拿一个绝对值横着比所有作业。
有些版本提供了网络层的详细指标开关,打开之后能看到每个通道级别的水位、排队长度等细粒度信息。这在定位"到底哪个下游通道堵了"时非常有用,但代价很大:指标数量会从几百条暴涨到几万条甚至更多,采集和存储压力陡增,指标系统自己可能先扛不住。
我自己的规则是:这个开关默认关,只在明确怀疑网络层有问题、并且能限定时间窗口的时候打开,抓到数据立刻关掉。把它当成长开的监控项,是在给自己挖坑。
3.4 端到端延迟与水位滞后
延迟指标是通过在数据流里打标记记录实现的,默认是关闭的。打开需要配两个东西:采集间隔和粒度。
# 每隔 60 秒打一批标记记录,0 表示关闭 metrics.latency.interval: 60000 # 粒度:single / operator / subtask,粒度越细指标越多 metrics.latency.granularity: operator这里有个很多人没意识到的点:标记记录是沿着某一条具体路径走的,如果中间有按 key 分区的操作,它只会走其中一个分支。所以延迟指标反映的是"抽样路径"的耗时,不是全量数据的真实分布。在数据倾斜严重的作业里,这个采样值可能比真实的最坏情况乐观得多。用它做趋势判断可以,用它做SLA 承诺要谨慎。
粒度选择上,subtask级别会把指标数量乘以并行度,除非你在排查某个具体 sub-task 的长尾,否则用operator级别就够了。
水位滞后是另一个经常被忽略的信号。把当前的输入水位和系统时间做差,得到的就是"数据落后了多少"。这个差值是判断作业是否在健康推进的最直接证据——比吞吐量更可靠,因为吞吐量可能因为数据源本身没数据而降下来,水位滞后则直接告诉你处理进度。做法上,把水位作为一个 Gauge 采出来,在看板里用采集时刻减去它即可。源端如果能看到输出水位,还能进一步判断是源头就没数据,还是数据进来了但没推进。
4. 绕开 Metrics 的第二条通路:REST API 与 Web UI
Metrics 是给长期监控用的,REST API 是给"我现在就要知道"用的。两者不是替代关系,是互补关系。有些信息只有 REST 接口能给,比如 checkpoint 的历史列表、异常堆栈、算子的拓扑结构。
4.1 常用端点与调用姿势
| 端点 | 拿什么 | 典型用途 |
|---|---|---|
/overview | 集群概况 | 快速确认 TM 数量、槽位使用 |
/jobs/overview | 所有作业的简要状态 | 巡检脚本的第一站 |
/jobs/:jobid | 单个作业详情与算子列表 | 拿到 vertex id 做后续查询 |
/jobs/:jobid/metrics | 该作业可用的指标名清单 | 升级前后做指标名 diff |
/jobs/:jobid/checkpoints | 历史 checkpoint 统计 | 定位是哪一次开始变慢 |
/jobs/:jobid/checkpoints/config | checkpoint 配置 | 确认间隔、超时、模式 |
/jobs/:jobid/exceptions | 异常与根因 | 重启原因溯源 |
/jobs/:jobid/vertices/:vid/subtasks/:idx/metrics | 单 subtask 指标 | 定位倾斜 |
其中/jobs/:jobid/metrics不带参数时会返回可用指标名列表,这个能力在做版本升级评估时特别值钱——升级前 dump 一份,升级后 dump 一份,直接 diff,哪些指标改名了、哪些没了,一目了然,比翻 changelog 快得多。
/jobs/:jobid/checkpoints返回的是结构化的历史记录,包含已完成、失败、进行中的次数和最近几次的详细数据。想回答"是从哪一次开始变慢的"这种问题,这个接口比时序数据库更好用,因为它保留了每一次的记录。
4.2 用 REST 做巡检脚本
长期跑着的作业,靠人眼看板是不现实的。写个几十行的脚本,每天定时跑一遍,比任何看板都可靠。
#!/usr/bin/env bash JM="http://jm-host:8081" # 1. 列出所有非运行态作业 curl -s "$JM/jobs/overview" \ | jq -r '.jobs[] | select(.state != "RUNNING") | [.jid, .name, .state] | @tsv' # 2. 检查某个作业最近的 checkpoint 情况 JOB="<job-id>" curl -s "$JM/jobs/$JOB/checkpoints" \ | jq '{completed: .counts.completed, failed: .counts.failed, latest_duration: .latest.completed.end_to_end_duration, latest_size: .latest.completed.state_size}'第一段脚本回答"有没有作业挂了",第二段回答"checkpoint 还正常吗"。两段拼起来就是最小可用的巡检。真要落地的话,我会再加两条:一是对比最近三次 checkpoint 耗时的趋势,二是检查失败计数在最近一小时内有没有增长。趋势比绝对值重要,这一点在巡检脚本里同样成立。
这类脚本还有个额外好处:它强迫你把"什么样的状态算异常"想清楚。写不出来的判断条件,说明你对业务还不够了解,这时候上告警系统只会制造噪音。
4.3 火焰图:定位"哪个算子慢"的最后一步
当指标告诉你某个算子反压严重、忙时间接近满格,接下来的问题是:它到底在干什么。这时候火焰图是唯一能给出答案的工具。
相关配置项大致是采样次数、采样间隔、栈深度这几个,开关在部分版本里默认关闭,需要显式打开。请求某个算子的火焰图端点之后,服务端会连续采样若干次线程栈,然后按调用链聚合成一棵树返回。
读火焰图的方法:横向宽度代表该栈帧出现的比例,越宽说明线程停在这里的时间越多。你不能指望它告诉你"这里有 bug",但它能明确区分几种情况:卡在序列化上、卡在用户代码的循环里、卡在等待网络写入、卡在 GC。这四类的处理方向完全不同。
采样期间是有性能损耗的,别在业务高峰期对一个高并行度作业全量抓火焰图。我的习惯是先在指标里锁定一两个可疑的 sub-task,只对它们抓,采样次数控制在百次以内,抓完立刻关闭。
5. 告警规则设计与升级过程中的指标改名
监控搭起来不等于有用,一堆没有阈值的曲线只是装饰。真正产生价值的是告警规则,而告警规则最大的敌人是版本升级。
5.1 一份能落地的告警规则清单
我自己的规则集大概是这样几条,按优先级排:
| 告警项 | 判断思路 | 阈值建议 | 备注 |
|---|---|---|---|
| 作业不在运行态 | 状态查询或状态 Gauge | 持续 1 分钟 | 最高优先级,先打电话 |
| Checkpoint 连续失败 | 失败计数增量 | 连续 3 次 | 比"失败数大于零"精确 |
| Checkpoint 耗时逼近间隔 | 耗时 / 间隔 | 大于 0.6 倍 | 留出余量,别等超时才发现 |
| 持续反压 | 反压时间占比 | 大于 500ms/秒,持续 5 分钟 | 短时抖动不报 |
| 吞吐归零 | 输入速率 | 等于 0,持续 3 分钟 | 需排除源头无数据的情况 |
| 水位停滞 | 当前水位与系统时间差 | 持续增长 10 分钟 | 比吞吐更可靠 |
| TM 数量不足 | 实例数 | 小于期望值 | 防止静默掉节点 |
| GC 时间占比过高 | GC 时间增量 | 大于 20% | 内存压力的早期信号 |
| 老年代占用高 | 堆内存使用率 | 大于 85%,持续 10 分钟 | 配合 GC 指标一起看 |
阈值不是抄来的,是根据作业的 checkpoint 间隔、SLA 要求、历史波动范围算出来的。比如 checkpoint 间隔 3 分钟,那耗时告警的阈值就该设在 100 到 120 秒之间,而不是笼统的"5 分钟"。这一条我吃过亏:早期用了一个拍脑袋的绝对阈值,结果大促期间每次流量上升都报,报了两周之后所有人都开始忽略告警,最后真出事那天没人看。
还有一个经验:告警要分级。作业挂了、连续 checkpoint 失败属于 P0,立刻通知;反压持续、GC 偏高属于 P1,工作时间处理;指标轻微漂移属于 P2,进日报就行。全都设成 P0 的结果等于全都没有。
5.2 版本升级导致的指标重命名
这是整个监控体系里最容易在无声中失效的一环。指标名变了,看板不会报错,只是曲线变成一条平线或者直接消失,而告警规则因为查不到数据也安静下来——这不是"没问题",是监控瞎了。
我印象最深的几处变化:
第一,反压相关指标从带accumulated前缀的版本改成了不带前缀的版本,同时语义从"累计值"调整成了"每秒窗口内的占比"。名字变了、单位也变了,直接平移会把阈值全部搞错。
第二,早期那个布尔型的"是否反压"Gauge 在新版本里不再是推荐做法,甚至被移除。有人的看板还在查它,查到的永远是空。
第三,对齐相关的指标在非对齐检查点普及之后语义发生了变化,部分指标被移除或替换。如果你的作业开启了对齐优化,这些指标的存在与否会直接影响你对 checkpoint 慢的判断路径。
第四,reporter 的配置键从class迁移到factory.class。这一条在升级时几乎必然要改,不改的话 reporter 直接不启动,而且报错信息不够直白。
应对办法没什么捷径:升级前用前面提到的指标名清单接口做一次全量 dump,和升级后做 diff,把消失和改名的指标逐条映射到新的看板表达式里。这个过程枯燥,但比出事之后发现没数据要划算得多。
5.3 在采集层做聚合,别把复杂度留给告警表达式
指标名字很长、维度很多,直接在告警表达式里写sum by (...) (rate(flink_taskmanager_job_task_operator_numRecordsIn[5m]))这种,写一次两次还行,写到第十次就开始出错,而且改一个地方要改十处。
更合理的做法是在采集侧定义一层中间指标,把原始的长名字收敛成业务语义的短名字,比如"作业级别的每分钟输入记录数"。下游的看板和告警都只引用这层中间指标。这样做的价值不只是好看:当底层指标改名时,你只需要改一处映射,上面所有引用都不用动。
代价是多一层维护,而且中间指标的数量要控制住,别把原始指标的所有维度原样带上来——那就等于没做聚合。
6. 采样开销、标签基数与我踩过的坑
这一节是纯经验,都是那种"文档里不会写、但配完就能感觉到"的东西。
6.1 采样周期与精度、开销的三角关系
默认 10 秒的采集间隔是经过权衡的。想更精确,代价是指标系统自身的开销线性增长。这个开销体现在三个地方:应用侧的指标收集与序列化、抓取侧的连接与解析、存储侧的写入与压缩。
高并行度作业上,指标收集不是免费的。每个 subtask 都在往共享的指标注册表里写,采集时要把整张表遍历一遍并格式化输出。并行度上千的时候,这个遍历本身的耗时就不能忽略了。
所以我的建议是分档:核心大盘保持 10 到 15 秒;排查期临时调到 2 到 5 秒,并且用过滤把范围缩小到可疑的算子;排查结束立刻恢复。不要图省事全局改成 1 秒,那是给自己找麻烦。
6.2 标签基数失控是监控系统崩掉的头号原因
基数这个词听起来抽象,换个说法:时间序列的数量。一个numRecordsIn指标,如果 scope 里带了主机名、TaskManager 标识、作业名、算子名、子任务编号五级,那么时间序列的数量就是这些维度取值的笛卡尔积。
一个中等规模的作业,200 个算子、200 并行度、3 个 TaskManager,光这一个指标就能产生 12 万条序列。再乘上几十个指标,百万级很轻松。而每条序列在存储里都是独立的索引和块,写入放大非常严重。给各项指标做减法,比调整保留策略更有效。
治理手段按效果排序:
- 采集端过滤,只保留真正会看的指标;
- 去掉 subtask 维度的原始序列,在采集层就按算子聚合掉,需要定位倾斜时再临时打开;
- 缩短高基数指标的保留时间,原始数据存 7 天,聚合后的存 90 天;
- 关掉不必要的详细指标开关,尤其是网络层的那个。
第二条最有效也最容易忽略。绝大多数看板其实不需要 sub-task 粒度,它们要的是"这个算子整体怎么样"。等到真要排查倾斜,再从 REST 接口按需查单个 sub-task,比常年存着几百万条序列划算得多。
6.3 几个具体踩坑记录
坑一:jar 没放对位置,端点 404,日志里只有一条不起眼的 WARN。那次排查了两个小时,一直在怀疑网络和端口,最后发现 reporter 的类压根没加载。教训是:配完 reporter 的第一件事永远是curl那个端口,而不是去看板里找数据。
坑二:多个实例共用固定端口,静默丢了一个节点的全部指标。端口被占之后 reporter 启动失败,但作业本身跑得好好的,所以完全没人发现。直到某天需要看那个节点的堆内存,才发现查不到。后来统一改成端口区间写法,才算根治。
坑三:算子改名之后看板断成两截。重构时觉得名字改得更语义化了是好事,结果所有以算子名做维度的曲线全部重来,历史的基线数据全废。后来定了个规矩:算子的对外名称当成接口来管理,改之前必须同步改看板映射。
坑四:打开了延迟统计,却发现用不了分位数查询。原因是 Histogram 类型在 Prometheus 侧的暴露形式是摊平成一串带后缀的数值(平均值、各个分位点等),而不是原生的 bucket 结构,所以常规的分位数聚合函数直接套上去会得到空结果。要么直接用已经算好的分位点数值,要么自己在应用侧做统计。这个差异文档里提得比较隐蔽,配之前最好先去端点上确认一下实际输出长什么样。
坑五:Job 级和 Task 级的同名指标搞混。状态大小这个指标在两个层级都有,但含义完全不同:一个是整个作业的状态总量,一个是单个任务的状态量。看板上把两者画在一起,或者告警引用了错误的层级,得出的结论会完全相反。我现在配置任何指标之前,都会先在指标名清单里搜一遍,确认它到底挂在哪个 scope 下。
坑六:JMX 远程连接在容器环境里连不上。需要开一堆启动参数,还涉及两个端口的映射和主机名解析,容器里主机名往往是随机串,RMI 回连直接失败。折腾了半天之后彻底放弃,本地用 JConsole 看单机,集群一律走 HTTP 端点。
6.4 我现在的默认配置长什么样
经过几轮折腾,稳定下来的配置大概是这个形态:生产环境只开 Prometheus reporter,端口用区间,采集间隔 10 秒;延迟统计开着但粒度用 operator 级别,间隔 60 秒;网络详细指标关闭;系统资源指标打开,因为容器内存的限制和 JVM 堆的配置经常对不上,没有这层数据根本判断不了是不是被容器 OOM 掉了;火焰图开关打开但只在需要时请求。
告警侧只保留五条 P0:作业不在运行态、连续 checkpoint 失败、checkpoint 耗时逼近间隔、TM 数量不足、吞吐归零且水位停滞。其余全部进日报。这套配置撑过了几次流量高峰,中间只调整过阈值,没再动过结构。
最后分享一个小技巧:把指标名清单的 dump 做成定时任务,每周存一份带日期的快照。哪天发现某个曲线莫名其妙变平了,先把这周的清单和上周的 diff 一下,十有八九能立刻定位到是哪个指标被改了名或者被过滤掉了,比从头查快得多。