ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Prometheus监控误报排查:MySQL假重启背后的指标误用

Prometheus监控误报排查:MySQL假重启背后的指标误用 数据库凌晨三点收到告警MySQL节点疑似重启。值班同事第一时间跳起来去查库——show global status like uptime一看Uptime 还是 48 天连接数正常慢查询没波动binlog 断裂也没有。数据库压根没动过。但 Prometheus 监控页面上那个运行时长的曲线确实突然归零了然后又以一个固定速率重新爬升视觉上跟重启别无二致。我翻了翻 alert 规则告警条件写的是time() - process_start_time_seconds{jobmysql} 300。看到这里心里基本有数了问题不在数据库而在我们监控了一台假数据库。1. 误报现场数据库明明没动监控却拉响了重启红警先还原一下当时的监控画面。我们的 Grafana 看板上有一个MySQL Instance Uptime面板查的是 PromQLtime() - process_start_time_seconds{jobmysql, instance10.0.2.15:9104}原本这条曲线应该是一条接近水平的线因为process_start_time_seconds是进程启动时的 Unix 时间戳只要进程不重启它就是一个固定的数字time() - 它就是从这个时间点到现在经过的秒数持续变大。告警规则阈值定为小于 300 秒意思是如果这个进程启动时间距今不足五分钟就认为数据库刚重启过。这在很长时间里都非常可靠数据库崩溃重启、节点断电重启都能第一时间抓到。但这套逻辑有一个前提假设 process_start_time_seconds 描述的进程就是数据库服务进程本身。实际情况是Prometheus 从mysqld_exporter暴露的/metrics端口拉取数据process_start_time_seconds这个指标是由client_golangPrometheus 官方的 Go 客户端库自动注册、自动采集的。它读取的是exporter 进程自己的/proc/self/stat跟 MySQL 的服务进程/proc/mysqld_pid/stat完全是两码事。也就是说我们在用监控程序自己的心跳去推断数据库的心跳。这是一个典型的代理指标误用。当告警亮起时exporter 确实重启了一次于是process_start_time_seconds从一个月前的旧值跳到了刚刚的新值运行时长归零。MySQL 本尊稳如老狗毫发无损。这类误报最坑的地方在于它把监控系统自身的故障伪装成了被监控基础设施的故障轻则熬夜看日志重则在业务高峰期错误触发切换操作造成更大的事故。2. 疑点解剖告警规则到底在检测谁的状态要定位这种问题核心是去理解 Prometheus 里每个指标的身份。用 Prometheus 自带的表达式浏览器Graph 页查一眼process_start_time_seconds{jobmysql}返回结果的标签里通常有一个instance值是10.0.2.15:9104。这个 9104 端口是mysqld_exporter的监听端口不是 MySQL 的 3306 端口。所以这条时间序列描述的启动时间只可能是监听 9104 端口的那个进程的启动时间。为了确认这一点可以到那台机器上做三件事。第一步查 mysqld_exporter 进程实际的启动时间ps -eo pid,lstart,cmd | grep mysqld_exporter | grep -v grep第二步查 MySQL 自身的启动时间mysql -e SHOW GLOBAL STATUS LIKE Uptime;第三步查 Prometheus 中该 job 的process_start_time_seconds数值并做比对。你会发现process_start_time_seconds跟ps里 exporter 的 lstart 完全对得上而跟 MySQL 的Uptime没半毛钱关系。如果 exporter 确实是在告警时间附近重启的那真凶就有了。这里顺便说一下process_start_time_seconds这个指标本身不是 Prometheus 服务器计算的也不是 exporter 特意伪造的它是 Go 运行时导出自身的进程启动时间。所有用 Go 写的 exporternode_exporter、mysql_exporter、redis_exporter、postgres_exporter 等都会自动带出这个指标。所以这类误报在各类监控里都可能发生不是 MySQL 专有。搞清楚谁在说话之后另一个很常见的坑也随之浮现有些监控系统的数据库运行时长面板根本不是基于数据库自身的状态变量而是基于这个通用的进程启动时间指标。一旦 exporter 被杀掉重建、被 systemd 扔进 cgroup 重启、被 K8s 滚动更新这个面板就会表演一次数据库重启。3. 锁定真凶毫厘之间是Exporter进程的狸猫换太子为什么这类问题会拖那么久才排查清楚因为在很多设计方案里exporter 和数据库被部署在同一台主机上甚至同一个容器里。运维人员直觉上认为进程都在同一台机器上时间应该一致于是把两个进程混为一谈。但毫厘之间的误差恰恰就藏在进程身份和时间精度上。client_golang在读取/proc/self/stat时拿到的starttime字段第 22 个值并不是直接的 Unix 时间戳而是距内核启动以来的节拍数jiffies。它需要结合/proc/stat中的btime系统启动的 Unix 时间戳才能换算成人类可读的启动时间。# 粗略演示换算式 btime 1700000000 # 系统开机时间 starttime 123456789 # 进程启动时距开机的节拍数 hertz 100 # 通常 USER_HZ100 start_time_epoch btime starttime / hertz问题就出在换算边界上如果读取/proc/stat和读取/proc/self/stat之间发生了跨秒的窗口btime starttime/HZ会在数值上下出现一个秒级抖动。如果主机做过 NTP 时间跳变stepbtime记录的是跳变前还是跳变后的时间可能会让process_start_time_seconds产生看起来像重启又穿越的怪异变化。在容器运行时host /proc与container /proc的视图差异也可能导致 starttime 在不同的 PID namespace 里表现不同。这些误差通常只有一两秒肉眼不容易察觉但对于运行时长 300 秒这种告警一个秒级突变就可能跨越阈值让告警毫无征兆地触发。回到那天的现场。我们抓到了真正的证据链systemctl status mysqld_exporter # 输出显示Active: active (running) since Tue 2025-07-08 # 但手动对比后发现 Active 时间是 2025-07-08 03:12:33 # 而告警时间是 03:12:35只差 2 秒再看 exporter 日志journalctl -u mysqld_exporter --since 03:10 --until 03:15日志里出现了levelinfo msgStarting mysqld_exporter version(version0.15.1)这说明 exporter 进程确实在那个时间点重启过。继续查重启原因发现是某次配置管理服务批量推送了 exporter 的 systemd unit 文件配置更新导致 watchdog 触发服务重启。数据库没参加这次变更但它躺枪了。这里要强调一个排查态度不要因为监控曲线像重启就去查数据库的二进制日志、redo log 崩溃恢复记录那是缘木求鱼。正确的顺序是先确认被监控指标的采集对象再讨论被监控对象本身的健康状态。如果process_start_time_seconds变了第一反应应该是exporter 是不是重启了而不是数据库是不是重启了。4. 容器化和时钟偏移让假重启雪上加霜的两个放大器第一起误报定位后我们以为只是个案谁知一个月后又来了一波更凶猛的假重启。这次不是单台机器而是 K8s 集群里一个数据库中间件所有实例同时报重启。检查发现真正发生的是 Prometheus 的instance标签在滚动更新后变了。原先的 instance 是10.96.0.5:9104更新后变成了10.96.0.9:9104。Prometheus 的规则对旧 IP 的时间序列有新数据就继续评估新旧 IP 在规则上完全视为两个不同的序列。假如告警用了这种写法min_over_time(process_start_time_seconds{jobmysql-replication}[5m]) ! max_over_time(process_start_time_seconds{jobmysql-replication}[5m])意思是在 5 分钟内同一个序列内出现了多个启动时间值就认为进程重启过。但在 IP 改变、Pod 重建后新序列只有一个值这条规则根本不会触发反而是对旧序列的 stale 数据处理在某些聚合场景下会产生强行补零、差值巨大等行为看起来像全部重启。另一类放大器是时钟偏移。我们有一个海外节点云主机默认没有启用 NTP 服务系统时钟跑偏了一个多小时。Prometheus 服务器执行time() - process_start_time_seconds时使用的是Prometheus 服务器本地的时间而process_start_time_seconds的值取决于目标机节点的系统时间。一旦目标节点时间比 Prometheus 服务器慢time() - process_start_time_seconds就会偏小甚至变成负数。规则time() - process_start_time_seconds 300对时钟偏移极其敏感。只要目标节点时钟慢了 5 分钟以上任何进程都会变成刚启动的。这就是标题里毫厘之间的另一个注脚时间戳的毫厘误差经过 PromQL 一放大就会变成一次几十厘米的误报。遇到这类环境排查方向也更明确# 在目标节点看当前时间 date # 在 Prometheus 服务器上看该 node 导出的 node_time_seconds node_time_seconds{instance10.0.2.15:9100} # 与 node_boot_time_seconds 对比 node_boot_time_seconds{instance10.0.2.15:9100}如果node_time_seconds和 Prometheus 服务器本地时间差太多就先不要谈什么重启先修时钟同步。5. 当再次遇到假重启五步排查法直接抄作业经历这两次误报后我总结了一套简单到不行的排查流程遇到数据库没动但监控说重启的情况按顺序走一遍基本能在十五分钟内给出结论。第一步确认数据库真实状态。登录数据库执行SHOW GLOBAL STATUS LIKE Uptime查 MySQL 错误日志看有没有Normal shutdown、Starting MySQL、ready for connections这类字样再查last_query_cost之类的活跃度指标确认业务没有中断。第二步锁定变化的具体指标。Prometheus 告警详情里会给出哪条 PromQL 触发拿到后再看这条表达式中的所有指标分别属于哪个 exporter。在 Prometheus 页面执行process_start_time_seconds{jobmysql}确认指标实例标签对应的端口是 9104 还是 3306。这一步基本能确认是否为进程级误报。第三步对比 exporter 与 MySQL 的启动时间。用ps -eo pid,lstart,cmd | grep mysqld_exporter对比 Prometheus 里的数值再用mysql -e SELECT NOW() - INTERVAL (SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAMEUptime) SECOND AS mysql_start_time拿到数据库精确的启动时间。两者如果相差巨大说明告警里的启动是 exporter 的启动。第四步查 exporter 的重启原因。journalctl -u mysqld_exporter如果是容器则kubectl logs mysqld-exporter -n db-ns --previous找不到就直接看 docker inspect 的 StartedAt 和 OOMKilled 字段。多数情况下是配置变更、OOM、健康检查失败后被重新拉起。第五步复盘告警逻辑。把告警规则里可疑的process_start_time_seconds整体替换为语义更明确的指标或者加上up{job...} 0 and on(instance) (mysql_global_status_uptime 0)之类条件。至于怎么替换下一节展开。为了更直观我把几个核心指标的身份整理如下指标名数据来源含义能否证明数据库重启process_start_time_secondsexporter 自身 /procexporter 进程启动时间否node_boot_time_secondsnode_exporter 采集主机 /proc/stat操作系统开机时间否但可证明节点重启mysql_global_status_uptimemysqld_exporter 连接 MySQL 后获取的 SESSION/STATUS 变量数据库实例已运行秒数是mysql_up / mysqld_upmysqld_exporter 探活标记exporter 能否连上数据库间接相关upPrometheus 拉取 exporter 的健康标记exporter 的 HTTP 接口是否可访问否注意表格里mysql_up与up的区别mysql_up表示 exporter 是否成功从 MySQL 拿到响应up表示 Prometheus 是否成功拉取到 exporter 自身的 metrics。一台数据库如果挂了但 exporter 还活着mysql_up0、up1如果 exporter 挂了up0而mysql_global_status_uptime则直接消失。这三者组合才构成完整的监控判断链。6. 治本之策把能直接证明数据库没死的指标纳入告警误报的根因是我们在用代理进程的时间戳代替数据库自身的时间戳。治本思路也很简单告警直接看数据库自己的状态变量而不是看代理进程的 procs。以 MySQL 为例最可靠的两个监依据是mysql_global_status_uptime数据库自己维护的运行秒数如果数据库真的重启它才会归零重新增长。mysql_global_status_threads_connected连接数。如果数据库真重启连接会瞬间掉光再重新建立。针对数据库疑似重启的告警我推荐这样改# 数据库刚启动不足5分钟 mysql_global_status_uptime 300但要注意一个细节mysqld_exporter 默认会从performance_schema.global_status采样 Uptime 变量如果它连不上数据库或者采集超时这个指标会消失不会出现归零的效果。所以同时必须搭配探活指标# exporter 无法连接数据库的告警 mysql_up 1如果既不想放过真重启又想过滤掉 exporter 重启可以用组合规则# 触发条件数据库自身 uptime 出现 明显变小 或 归零 increase(mysql_global_status_uptime[1m]) 0这条 PromQL 的含义是一分钟后uptime 的数值反而变小了。正常情况下 uptime 只会增加不会减少一旦它减少就意味着这个变量本身被重置了也就是数据库确实重启过。exporter 重启不会导致mysql_global_status_uptime变化因为新 exporter 从同一个数据库重新拿到的还是那个巨大的 Uptime 值。这里有一个例外要特别留意如果你用的是mysqld_exporter的--collect.global_status同时搭配 MySQL 的主从复制环境从库的 Uptime 反映的是从库实例的启动时间不代表主库状态。别把主从搞混。同理如果业务通过数据库中间件连接中间件返回的 Uptime 可能来自后端不同实例同一套监控表达式在不同库上会读出完全不同的身份。在容器/K8s 环境中还建议在告警里加上instance或pod的稳定标识。如果 Prometheus 的抓取目标使用了relabel_configs把pod_name保留为标签最好统一以pod_name而不是instance大概率是 Pod IP来做聚合否则每次滚动更新都会制造新的时间序列间接干扰changes()、increase()等函数的计算。如果确实希望用进程启动时间来监控 exporter 自身是否频繁重启那就单独建一条职业告警别把它塞进数据库重启规则里。区分职责这是监控设计里最值得花时间的地方。以我个人的经验这类告警误报最大的危害不是把人吵醒而是会逐步侵蚀对监控体系的信任。排查流程走完、规则修好之后我还会在每个 exporter 的 systemd unit 里增加Restarton-failure、RestartSec10并且给进程加上合理的ulimit和 OOM Score 调整从源头上减少 exporter 自己被系统干掉的概率。毕竟代理进程稳定了冯京当马凉的机会也就少了很多。对了修这个问题的当天我在告警备注里写了一句process_start_time_seconds说的是我什么时候醒不是数据库什么时候醒。此后团队再没人把这两件事看成一件事。
返回列表