资讯动态

MySQL重启误报排查:process_start_time_seconds与Uptime的监控陷阱

发布时间:2026/10/6 3:43:59 来源:尧图企业网站定制
1. 事故现象凌晨的“重启”告警数据库根本没有重启1.1 告警触发的那个凌晨凌晨 2 点 13 分告警突然炸了MySQL 实例发生重启。我一边揉眼睛一边登录数据库习惯性地敲下SHOW GLOBAL STATUS LIKE Uptime结果屏幕上的数字让我彻底清醒——Uptime 17658203换算下来数据库已经连续跑了 204 天。也就是说数据库进程根本没动但 Prometheus 监控却在告警里明确写着“process_start_time_seconds 发生跳变”。这不是第一回碰到这种“谎报”了。在 Prometheus 监控体系里凡是带着process_前缀的指标几乎都是 exporter 进程自己暴露出来的而不是目标数据库进程的。如果告警规则直接写changes(process_start_time_seconds{jobmysql-exporter}[5m]) 0那你监控的其实是 exporter 有没有重启而不是数据库有没有重启。这个顺序一旦搞反后续所有告警都等于在猜。当时值班群里的第一反应也很正常有人直接问“数据库挂了要不要切换”被我一票否决了。原因很简单数据库侧的运行时长、连接数、主从状态全部正常只有 Prometheus 这一个点在喊重启。在监控领域有一个朴素原则——当 Prometheus 和数据库本体给出的结论矛盾时先怀疑 PromQL 写错了而不是先怀疑数据库出了问题。1.2 一连串“正常”的检查结果接到告警后我按标准流程走了一轮mysqladmin ping正常返回 mysqld is alive查看 error log最后一行还是几天前的正常日志没有任何异常关闭记录登录 MySQL 执行SHOW GLOBAL STATUS LIKE Uptime连续运行时间超过 200 天查看主从同步状态Seconds_Behind_Master为 0复制线程正常顺手查了SHOW PROCESSLIST连接数、慢查询都符合日常水位。也就是说从数据库本身能拿到的所有证据都指向同一个结论数据库没有重启。但告警界面里process_start_time_seconds的值确实在凌晨 2 点 11 分前后发生了跳变从 1645000000 左右变成了 1700000000 左右。看起来像“重启”实际是某个进程真的重启了。如果只盯着 Prometheus 界面会很容易相信“数据库重启了”。但把数据库侧的证据拉出来一对比才知道要怀疑的是监控指标的语义而不是数据库的稳定性。这时候我基本锁定了问题方向告警规则里用到的指标大概率是 exporter 自身的启动时间而不是 mysqld 的启动时间。为了坐实这个猜测我去看了部署方式。这套 MySQL 是用 docker compose 部署的mysqld 和 mysqld-exporter 分别跑在两个容器里共享网络但生命周期独立。exporter 容器在 2 点 11 分被 Docker 自动重启过一次原因写的是 OOMKilled健康检查随即恢复正常。数据库容器一直没动。到这里整个事故链其实已经闭合exporter 被 OOM KillDocker 拉起新进程process_start_time_seconds跳变changes()函数判定为“重启”告警发出。数据库全程躺平看戏锅从天上来。2. 抽丝剥茧告警规则里的指标到底在监控谁2.1 process_start_time_seconds 的“身份”问题process_start_time_seconds是 Prometheus 客户端库提供的标准指标由 client_golang、client_python 等库在进程启动时写入记录的是该进程自身的启动时间。对 mysqld_exporter 来说这个指标反映的是 mysqld_exporter 进程的启动时间不是 mysqld 的启动时间。原因很简单exporter 只是一个采集代理它通过 SQL 协议连到 MySQL读取各种状态变量再转成 Prometheus 指标。它并不知道 mysqld 的 pid 是什么也没有理由去读/proc下 mysqld 进程的信息。所以process_start_time_seconds天然属于 exporter 自己。如果两者跑在同一个容器里且容器启动时同时拉起两个进程那在容器生命周期内process_start_time_seconds还能勉强代表一条“服务是否经历过重启”的时间线。但一旦容器只重启了其中某个进程或者 exporter 以独立 sidecar 方式部署这个指标的语义就彻底错位了。这个道理放在其他 exporter 上也适用node_exporter 的process_start_time_seconds是 node_exporter 自己的启动时间不是被监控的某台机器的启动时间postgres_exporter 的process_start_time_seconds是 postgres_exporter 的启动时间不是 PostgreSQL 实例的。任何拿“exporter 进程重启”当“业务进程重启”的告警规则本质上都是在玩塔罗牌。2.2 为什么 exporter 重启会让数据库背锅这次事故里exporter 容器是因 OOMKilled 被 Docker 自动拉起的。client_golang 在新进程里重新生成启动时间戳于是整条时间序列的值发生了一次阶跃。而告警规则当时长这样- alert: MySQLRestarted expr: changes(process_start_time_seconds{jobmysql-exporter}[5m]) 0 for: 0m labels: severity: critical annotations: summary: MySQL instance {{ $labels.instance }} has been restartedchanges()的语义是在 5 分钟窗口内该时间序列的值发生过几次变化。exporter 重启后process_start_time_seconds从旧值跳到新值changes() 1条件满足于是告警。整个过程与 mysqld 本身的运行状态毫无关系。这里有个很容易忽略的细节for: 0m意味着只要一次瞬时评估为 true告警立刻触发。瞬时值对抓取抖动、窗口边界非常敏感哪怕只是一个样本点被切到了窗口之外也可能导致漏判或误判。我后来在团队里定了一个规矩——涉及“状态变化”类的告警至少留for: 5m的观察窗口不要用 0m 去赌瞬时值。遇到这类“谎报”排查时最有效的一招是在 Prometheus 里把两个指标近一小时的曲线放在同一个面板里看。process_start_time_seconds出现阶跃同时mysql_global_status_uptime保持平滑递增那基本可以断定重启的是 exporter而不是数据库。曲线不会说谎但指标会。3. 毫厘之间的真相精度、对齐与时间戳陷阱3.1 Linux 进程启动时间戳的精度上限标题里说“真相藏在毫厘之间”这不只是修辞。进程启动时间的采集精度本身就有隐藏的坑。Linux 下我们通常通过/proc/pid/stat里的 starttime 字段获取进程启动时间单位是 clock ticks即时钟节拍数。典型的 USER_HZ 是 100意味着每个 tick 是 10ms。也就是说/proc暴露的进程启动时间精度最多到 10ms。当 exporter 把 ticks 转换成 Unix 秒时还会发生一次取整最终得到的process_start_time_seconds是整数秒。反过来看另一个方向如果一个进程在秒内发生快速重启比如旧进程在 02:11:07.2 退出新进程在 02:11:07.8 启动两个启动时间在秒级取整后都可能落到 02:11:07。那么process_start_time_seconds的值可能根本没变Prometheus 用changes()检测时就会漏掉这次重启。这是另一种“毫厘之间”的遗憾——不是谎报而是漏报。所以在设计告警时必须清楚process_start_time_seconds是一个秒级精度的状态值不适合用来做毫秒级重启判断。如果你真的需要精确判断数据库恢复时间应该看数据库自身的日志、uptime状态或者像 MySQL 8.0 里那样直接查performance_schema里的进程启动信息而不是用一个被/proc精度和秒级取整双重削弱的指标。3.2 PromQL 窗口边界与抓取对齐的微妙偏差PromQL 计算 range 向量时样本会按时间戳对齐到窗口内。实际抓取时间不是绝对均匀的每个样本都有自己的毫秒级时间戳。就像我们把一根标尺放到一条不均匀的点序列上窗口起点和终点稍微偏移一点落在窗口里的样本可能就不一样。举一个具体场景scrape 间隔 30 秒告警窗口 5 分钟进程在 02:11:00 重启。如果上一次抓取是 02:10:55下一次抓取是 02:11:25那么第一次规则评估时的窗口可能刚好没把“旧值”和“新值”同时包含进来changes()结果为 0未触发下一次评估窗口移动后两个值都进来了才触发判断。这个延迟和抖动会让告警时间看起来随机甚至让人觉得监控不稳定。反过来如果两个样本一个落在窗口起点前 1 毫秒一个落在窗口起点后 1 毫秒这个毫秒级的边界差异就会直接影响判断结果。Grafana 图表里呈现出的“断点”往往也是这个原因——采样点之间并非无缝窗口边界切到了不同状态的两侧。这种误差没法彻底消除但可以缓解。做法是让告警窗口至少大于等于 3 个 scrape 周期并且给告警规则加for。比如 scrape 是 15 秒窗口就不要用 1 分钟最好用 5 分钟for至少给 2 分钟让连续两次评估都确认后再发通知。这样虽然不能保证百分之百准确但能把“毫厘误差”对告警结果的影响压到最低。3.3 时钟跳变与浮点尾数那些被忽略的细节还有一个容易被忽视的“毫厘”来源时间同步。如果监控机或数据库所在机器的系统时钟发生 NTP 小幅度校准PromQL 里的time()返回值会产生一次跳跃。当规则里用到time() - process_start_time_seconds这类表达式时一个几百毫秒的时钟调整就可能让计算结果跨越某个阈值边界从而触发一次本不该触发的告警。浮点数尾数也不能完全忽视。某些 exporter 输出的process_start_time_seconds不一定规规矩矩的整数而是类似1685000000.000001这样的浮点数。用changes()判断变化没问题但如果有人用abs(process_start_time_seconds - process_start_time_seconds offset 5m) 0来判断重启浮点尾数那一点点误差就可能被当成变化产生最典型的“毫厘之差谬以千里”。我的原则是判断状态类指标的跳变优先用changes()、delta()这类带容错语义的函数不要自己写“差值0”这种对精度零容忍的表达式。监控数据天生就带有取样、传输、存储过程中的微小干扰所有过于严苛的条件都可能变成误报的温床。4. 正确姿势如何准确监控数据库是否重启4.1 从数据库侧拿证据mysql_global_status_uptime对于 MySQL最可靠的“是否重启”信号是它自己回报的运行时长mysql_global_status_uptime。这个指标来自SHOW GLOBAL STATUS里的 Uptime单位秒反映的是 mysqld 进程从启动到现在的持续运行时间。正常运行时它的值每秒增加约 1。如果 mysqld 重启这个值会从很小的数字重新开始。用 PromQL 检测它的异常下降就能精准判断数据库重启而与 exporter 是否重启无关。推荐表达式delta(mysql_global_status_uptime{jobmysql}[5m]) -60这个表达式计算 5 分钟内指标值的变化量。正常情况delta约等于 300略小一点也正常一旦小于 -60说明在这 5 分钟里 Uptime 发生了明显回退基本可以断定 mysqld 曾重启。不想受抓取抖动影响可以用 offset 写法mysql_global_status_uptime{jobmysql} (mysql_global_status_uptime{jobmysql} offset 5m) - 300含义是“当前 uptime 比 5 分钟前还低”如果成立就是重启。这两种写法各有取舍delta 对窗口内样本数量更敏感offset 写法对重启点落在窗口外的情况更敏感。实际使用中我会把两条规则同时配上或者用 offset 版本加上for参数来过滤瞬时毛刺。需要注意mysql_global_status_uptime也会存在抓取失败导致的空样本。如果监控目标短暂失联Prometheus 会保留旧的时间序列但不会凭空补齐中间的缺失值。窗口里样本变少delta()的结果仍然能反映首尾两个样本的差值一般情况下不会产生误报但窗口如果短到只剩一个样本计算会直接返回 NaN告警规则自然也不会触发。4.2 容器环境下的容器重启次数指标如果数据库跑在 Kubernetes 里还有更直接的指标。kube-state-metrics 暴露的kube_pod_container_status_restarts_total记录了每个容器的累计重启次数它是一个 counter。只要 mysql 这个容器发生过重启这个计数就会增长。increase(kube_pod_container_status_restarts_total{namespaceprod, containermysql}[10m]) 0这个表达式能精确命中“mysql 容器是否重启”。好处很明显即使 exporter 容器频繁重启只要数据库容器没动就不会触发。如果 mysqld-exporter 作为 sidecar 和 mysql 同 Podexporter 重启只影响containermysqld-exporter那条序列不影响 mysql这一点格外关键。还有一个细节值得说kube-state-metrics 数据本身有传播延迟通常在一分钟以内。如果窗口给得太短比如 2 分钟可能因为延迟导致increase()算不出正值。我建议窗口至少给到 5 到 10 分钟再叠加for这样既能覆盖延迟又能过滤瞬时毛刺。4.3 一套更稳健的告警规则模板综合下来我现在的数据库重启告警模板长这样groups: - name: mysql_alerts rules: - alert: MySQLProcessRestarted expr: delta(mysql_global_status_uptime{jobmysql}[5m]) -60 for: 3m labels: severity: warning annotations: summary: MySQL instance {{ $labels.instance }} uptime has an abnormal drop description: mysql_global_status_uptime decreased abnormally, possible mysqld process restart. - alert: MySQLContainerRestarted expr: increase(kube_pod_container_status_restarts_total{containermysql}[10m]) 0 for: 5m labels: severity: critical annotations: summary: MySQL container {{ $labels.pod }} restarted关键点在于用数据库自己汇报的指标而不是 exporter 的进程指标加上for过滤瞬时毛刺告警描述里写清楚判断依据别只写一句“实例重启”否则半小时后人已经忘了规则是查什么的。此外建议把 recording rule 加进去减轻每次评估的查询压力record: job:mysql_uptime:delta5m expr: delta(mysql_global_status_uptime{jobmysql}[5m])然后在告警规则里引用这个预计算指标。数据量大、实例多的时候这类状态指标虽然单个很小但跨多实例、多集群统一预聚合能让 Prometheus 的评估压力下降不少。我把这种“先记录再告警”的习惯带到所有状态类告警里长期看收益非常明显。5. 常见误报源排查速查与复盘5.1 误报源检查清单到了这一步我把这类“数据库没动、监控谎报重启”的场景整理成一个速查表给团队内部用也分享出来误报表现最可能原因快速验证方法process_start_time_seconds 跳变数据库正常运行告警规则用了 exporter 自身的启动时间戳对比 mysql_global_status_uptime 是否平滑同一条曲线断裂Grafana 里出现新序列Pod/IP 变化导致 instance 标签改变检查 instance 标签值前后是否不一致告警时间不固定感觉像随机触发scrape 间隔与窗口边界不对齐放大看抓取点时间戳和窗口起止点time() 相关表达式偶尔告警NTP 时钟校准导致 time() 跳变检查监控机 chrony/ntpd 日志数值比较越界告警浮点尾数导致差值不为 0改用 changes() 或 delta() 判断exporter 频繁重启导致后续告警exporter 自身内存配额不足或崩溃看 exporter 容器日志与内存限额这张表的核心思路是先把“谁在重启”搞清楚再谈“要不要告警”。如果告警规则的 expr 里连被监控对象都没写对后面所有动作都是白费。5.2 本次事件的后续改进告警规则改完我又做了一轮加固给 exporter 容器加上内存限制和健康检查从根源上避免它因 OOMKilled 反复重启在 Prometheus 抓取配置里固定 job 与 instance 标签减少因 DNS 解析或网络抖动导致的 target 重新发现把所有“进程重启”类告警统一梳理一遍凡是基于process_start_time_seconds的都改成基于目标服务自身运行指标的表达式在 Alertmanager 里把MySQLProcessRestarted的严重级别降为 warning把容器重启类告警保留为 critical避免下次误报直接打扰值班。排查这类问题时我还有一个习惯不只看告警本身而是把告警前后的原始曲线拉出来对比。凡是“某个指标跳变 另一个指标正常”的组合大概率就是指标语义不对而不是服务出了问题。比如说process_start_time_seconds跳了但mysql_global_status_uptime还在稳稳往上走这俩放在同一张图里十秒钟就能定位问题。经过这次改动之后同一套环境里又出现过一次 exporter 因内存问题重启但监控面板没有任何告警弹出。数据库侧的 Uptime 曲线始终是平滑的值班同学甚至没有察觉到 exporter 曾短暂掉线。这个结果恰恰说明告警规则的语义一旦对齐整个监控系统的可信度会提升一个量级。我个人的体会是监控系统里最危险的告警不是“漏报”而是“谎报”。谎报几次之后团队就会对告警失去信任真正出事的时候反而没人响应。把告警规则从process_start_time_seconds换成mysql_global_status_uptime只是一次很小的改动但它让监控从“看进程有没有重启”变成了“看数据库自己的运行状态”语义清晰可信度才能建立起来。如果这篇文章的经历让你想起自己踩过的类似坑不妨先回去翻一翻你的告警规则看看有没有哪条规则用的是 exporter 自己的指标。数据库没有重启本身是个好消息但监控规则里藏着的“毫厘误差”迟早会在某个凌晨用一次响亮的告警提醒你。趁现在还来得及先把尺子校准再等下一次告警。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑