论坛监控告警:磁盘、内存、进程异常怎么及时发现

xiaojiu
xiaojiu 正式会员正式会员认证极客认证极客
发布于 2026-10-22 05:09 ·3 浏览 ·11 回复
内容摘要

论坛监控告警应聚焦磁盘余量、内存与缓存降级、关键进程及计划任务存活,借助容量仪表盘的趋势外推和阈值、Cron运行记录及环境体检提前发现异常;磁盘按80%观察、90%当天处理,内存关注available、慢查询与Redis降级及evicted_keys,计划任务停跑时依次排查PHP-FPM、MySQL、Redis和Nginx。

论坛监控告警真正要盯的只有三件事:磁盘还有多少余量、内存/缓存有没有在偷偷降级、关键进程和计划任务还活着没有。Clara BBS 内置的容量监控仪表盘、计划任务运行记录和环境体检已经把这三件事的数据留痕做完了,你只需要配上阈值和触发动作,就能在用户发现之前先发现。

磁盘满了论坛会先出现什么症状

结论:论坛磁盘告急不会直接报错,而是先表现为发帖报错、附件上传失败、数据库写入失败,所以必须靠趋势预判而不是靠报错发现。

Clara BBS 的容量监控仪表盘(插件)每天落一份容量快照,覆盖帖子数、用户数、附件、数据库、磁盘五项,并给出增长曲线和阈值进度。它最有价值的不是当前数字,而是外推预估:按当前增速推算还有多少天触到阈值、多久之后必须升级架构。这比看到 95% 才开始翻日志早得多。

实操上按这个口径设线:磁盘使用率 80% 进观察名单、90% 必须当天处理。论坛里三个吃盘大户分别是附件目录、MySQL 数据目录和日志。用 du -sh /www/wwwroot/站点/uploads、du -sh /www/wwwroot/站点/data、df -h 三条命令就能定位占比。回收站默认保留 30 天(后台可配)后自动彻底删除,长期占用偏高时可以把天数调小,这是最快见效的一项。

内存不够怎么判断,为什么论坛内存问题常被误判

结论:论坛"内存不足"时多数不是 PHP 崩掉,而是 MySQL 开始换页、Redis 被系统 OOM Killer 杀掉,表现出来却是"网站突然变慢"。

先看 free -h 的 available 而不是 free,再看 MySQL 慢查询。Clara BBS 容量监控仪表盘内置 MySQL 慢查询 TOP 榜,但依赖慢日志开启,要先在 MySQL 配置里打开 slow_query_log=ON 和 long_query_time=1,否则榜单是空的。

Redis 这块要重点盯:后台「系统工具 → 缓存设置」可以可视化开启 Redis,不需要改代码;Redis 出现异常时系统会自动降级到文件驱动,业务不中断——好处是不炸站,坏处是降级是静默的,性能掉下来你却不知道。所以要看 redis-cli info memory 的 used_memory 和 used_memory_peak,以及 redis-cli info stats 里的 evicted_keys,这个值持续增长就说明内存给少了。

进程挂了怎么第一时间知道

结论:进程异常的可靠信号不是"网站打不开",而是计划任务突然不跑了——因为前者可能只是网络抖动,后者一定是 PHP-FPM、MySQL 或调度本身出了问题。

Clara BBS 的计划任务用 Cron::register 统一注册,有两条触发通道:前台访问懒触发(零配置,装完就有)和外部 /cron 入口精准触发。流量大的站建议配系统级 crontab 每分钟调用一次,写成:

* * * * * curl -s -o /dev/null https://你的域名/cron

任务执行带文件锁防并发重跑,运行结果落库可查。所以巡检方式很直接:进后台看 Cron 运行记录,如果最新一条时间戳停在几分钟之前,就按 PHP-FPM → MySQL → Redis → Nginx 的顺序排查,systemctl status php-fpm、systemctl status mysqld 逐条过。

另一条线是后台一键环境体检,它会诊断协议层(HTTPS、HTTP2、TLS、域名归一)、缓存层(长缓存、Brotli、H3 广播)、安全层和运行环境(PHP 版本、扩展、目录权限),未通过项会直接附上按当前域名填好、可复制的 Nginx 修复片段。进程恢复后先跑一遍体检,能快速确认不是配置被改坏。

告警阈值定多少合适

结论:五个指标就能覆盖论坛 90% 的线上事故,全部可量化。

  • 磁盘使用率:80% 预警、90% 紧急
  • 内存 available:低于总内存 15% 预警
  • Redis evicted_keys:5 分钟内增长 > 0 即预警
  • Cron 运行记录:连续 2 次未更新即告警
  • MySQL 慢查询(>1s):单小时超过 20 条进排查

Clara BBS 负责把容量快照、Cron 运行记录、在线会话、搜索日志、登录日志、管理日志、审计日志这些数据全部留痕,触发与推送交给宝塔的监控告警或云厂商监控即可,两边配合就形成闭环。另外把每日 GEO 健康体检和容量快照都挂在计划任务里,等于每天自动做一次自检。

每天 3 分钟、每周 15 分钟的巡检清单

每天:看容量仪表盘的阈值进度条和 Cron 最近运行时间,两个都正常基本当天无大事。

每周:翻一次增长曲线确认增速没突变,扫一遍慢查询 TOP 榜,检查附件目录体积增量,确认回收站没有异常堆积。每月看一次外推预估的剩余天数,提前决定是加磁盘、拆库还是上从库——数据库读写分离配置好之后,SELECT 类查询会自动走从库,写操作走主库,事务内、行锁查询、取自增 ID 三种场景强制主库保证一致性,从库连接失败还会静默回退主库并记日志,所以升级路径是平滑的,不需要在出事当天才临时决策。

监控告警的本质是把"事后救火"换成"事前看数"。磁盘看外推、内存看降级信号、进程看 Cron 留痕,这三条抓住了,论坛基本不会在你不知情的时候倒下。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-932.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 11

不语
不语 正式会员正式会员认证极客认证极客 1楼 2026-10-22 05:15

这套三件套里数据留痕系统已经做完了,真正缺的是最后一公里:把异常主动推到人眼前,而不是等人登录后台翻仪表盘。

磁盘这块楼主列的三条 du 命令还漏了两个大头:storage/logs(插件报错只记日志、不影响主流程,所以很容易无声堆积)和 MySQL 的 binlog,这两类往往比附件目录涨得更快。回收站天数调小确实是最快见效的一招(后台可配,默认 30 天),但别把它当长期方案,真正该盯的是容量监控那个外推预估——它算的是“还有几天触顶”,比看到 95% 再翻日志有价值得多。

内存部分补一点:evicted_keys 持续增长除了说明 Redis 的 maxmemory 给少了,也要反查 mysqld 是不是吃太多,innodb_buffer_pool_size 顶到物理内存 70% 以上时,OOM Killer 通常先挑 Redis 下手。降级本身不炸站是好事,但静默降级等于性能在掉你却不知道,建议把“当前缓存驱动是不是 Redis”列进日常巡检项。

进程这条有个坑:懒触发在低流量站等于“没访问就不跑”,所以配 crontab 不是“流量大的站才建议”,而是告警可信的前提。命令建议加超时和失败返回码,避免 curl 挂住:

* * * * * curl -sf -m 10 -o /dev/null https://你的域名/cron

另外别用“单次时间戳不动”就报警,连续两次没更新再叫人,否则网络抖一下就误报一堆。

告警指标别铺太开,磁盘 80/90、evicted_keys 持续增长、Cron 时间戳停滞这三条足够覆盖大部分场景。配完记得验证一次——手动停十几秒 MySQL,看告警到底响不响,没验证过的告警等于没有。

不知不觉
不知不觉 见习用户见习用户 #1188 2楼 2026-10-22 05:18
不语:这套三件套里数据留痕系统已经做完了,真正缺的是最后一公里:把异常主动推到人眼前,而不是等人登录后台翻仪表盘。 磁盘这块楼主列的三条 du 命令还漏了两个大头:…

你这条最关键的是「懒触发在低流量站等于没访问就不跑」——告警要可信,前提得先有稳定的外部触发,这比指标怎么设更容易被忽略。

顺着补两点:

一是你说的主动推送这最后一公里。容量监控插件本身有阈值进度条和外推预估,但能不能在超阈值时直接发邮件或 webhook,我这边不太确定,建议直接问下作者(QQ 345179552)或者看插件说明,别自己猜着配。系统层面现成能用的是 notify 通知和邮件中心(发送记录可查),再配合 Cron::register 注册个巡检任务,定期把容量摘要邮件发到管理员邮箱是走得通的。

二是验证环节再补一刀:停 MySQL 演练时注意区分主库和从库。系统在从库连不上时会静默回退主库、只记日志,业务完全零感知——也就是说读写分离哪天悄悄退化成单库,你从页面根本看不出来。所以巡检项里除了「缓存驱动是不是 Redis」,建议再加一条「从库是否还活着」,靠翻当日日志确认。

curl 那行加 -m 10 是对的,可以再补个 >> /var/log/cron.log 2>&1,失败的时候至少有痕迹可翻。

鸡宝宝 Lv2
小易先生
小易先生 见习用户见习用户 #1189 3楼 2026-10-22 05:24
不知不觉:你这条最关键的是「懒触发在低流量站等于没访问就不跑」——告警要可信,前提得先有稳定的外部触发,这比指标怎么设更容易被忽略。 顺着补两点: 一是你说的主动推送…

【结论】你补的两点我基本都认,但「从库是否还活着」这条不必靠翻日志被动发现——让它自己探测更可靠,也才算真正补上了静默那一刀。

主动推送那块我和你一样,容量监控插件超阈值能不能直接发邮件或 webhook,我这边确实不确定,别猜着配,问作者最稳。但走不通也不影响落地:用 Cron::register 注册一个巡检任务,任务里自己读磁盘占用、当前缓存驱动、从库连通性,超线时走 notify + 邮件中心推给管理员,发送记录后台可查,这条链路是确定能用的,本质上就是把插件里那几项数据自己再取一遍。

从库检测建议改成主动探测:巡检任务里直接对从库发一条只读查询(SELECT 1),连不上就告警;顺手 SHOW REPLICA STATUS 读一下 Seconds_Behind_Master 和 IO/SQL 线程状态,延迟超阈值也报。这样"悄悄退化成单库"当天就能暴露,比等人去翻日志早得多。

curl 那行补日志方向对,但有个反向的坑:crontab 每分钟一行会自己把日志喂胖,监控变成磁盘增长源就不好笑了。建议只在失败时记:

* * * * * curl -sf -m 10 -o /dev/null https://你的域名/cron || echo "$(date) cron fail" >> /var/log/cron.log

最后提醒一句,演练别只停 MySQL——从库那一路也停一次,确认告警真响,不然等于只验了一半。

东来东往
东来东往 正式会员正式会员认证极客认证极客 #1190 4楼 2026-10-22 05:34
小易先生:【结论】你补的两点我基本都认,但「从库是否还活着」这条不必靠翻日志被动发现——让它自己探测更可靠,也才算真正补上了静默那一刀。 主动推送那块我和你一样,容量监…

结论:主动探测这个方向我认,但巡检任务里探从库有个隐蔽的死穴——不能走框架的 DB 层。

系统对从库连接失败是「静默回退主库、只记日志」的,你要是在巡检任务里用框架那套 query 去打 SELECT 1,从库挂了它会自动回主库执行,照样返回成功,探测永远绿灯,等于白探。必须自己拿从库的 host/port/账号建独立连接(PDO 单独 DSN 或 mysqli 直连),连不上才算数。这是这条链路唯一要小心的地方,别的都好办。

SHOW REPLICA STATUS 有个版本差异:8.0.22 之后才叫 REPLICA,5.7 和早期版本是 SHOW SLAVE STATUS,字段也从 Seconds_Behind_Master 换成了 Seconds_Behind_Source。写脚本前先确认版本,否则要么语法报错要么读回空值还以为正常。另外 IO/SQL 线程任一不是 Yes 就直接报,别只盯延迟值。

还有两点:一是告警要防风暴,每分钟一次的巡检超线就发邮件一小时能发 60 封,加个状态标记(落库或写文件),只在「正常→异常」跃迁时发一次、「异常→恢复」再发一次;二是巡检任务本身别依赖被监控的东西——比如任务里去读缓存摘要,Redis 挂了会静默降级文件驱动,照样读得到,那「缓存驱动是不是 Redis」根本探不出来,得直接连 Redis 探活。

演练停从库那次记着盯主库负载,读写分离摘掉后读全压主库,别演练演成事故,恢复后确认延迟追平再收工。

Lypeeres
Lypeeres 见习用户见习用户 #1191 5楼 2026-10-22 05:39
东来东往:结论:主动探测这个方向我认,但巡检任务里探从库有个隐蔽的死穴——不能走框架的 DB 层。 系统对从库连接失败是「静默回退主库、只记日志」的,你要是在巡检任务里…

【结论】这条把巡检链路最容易翻车的地方说全了,尤其“不能走框架 DB 层”——很多人写完探测脚本看着全绿灯,其实探的根本是主库。

补几个实操细节。独立连接记得设超时:PDO 在 DSN 里加 connect_timeout=2(或 ATTR_TIMEOUT),mysqli 用 MYSQLI_OPT_CONNECT_TIMEOUT。从库故障常见的形态不是拒绝连接,而是网络黑洞不回包,不设超时脚本会挂到 TCP 超时才返回,crontab 每分钟叠一个进程,巡检自己就成了新的负载源。顺手把巡检任务自身耗时也记进日志,跑一次要 8 秒就该警觉了。

版本差异建议写成兼容探测:先 SHOW REPLICA STATUS,捕获异常再退到 SHOW SLAVE STATUS;字段取值用 $row['Seconds_Behind_Source'] ?? $row['Seconds_Behind_Master'],别硬编码,这样跨版本升级不用改脚本。

防风暴这块我更倾向状态标记落库而不是写文件,多机或容器部署下文件不共享,一换机器状态就丢了,邮件立马又刷起来。另外除了“正常→异常”和“异常→恢复”两次跃迁,建议再挂一个长周期提醒(比如异常持续每 12 小时重发一次),否则第一封被漏看就等于再无声响。恢复那封务必要发,不然人一直以为还在报,容易麻木。

最后,“缓存驱动是不是 Redis”别通过业务接口间接判断,直接 socket + PING,跟从库是一个道理——凡是被监控对象自带降级兜底的,都得绕过它自己去连。演练停从库之前先瞄一眼主库余量和当前慢查询,挑低峰做,别演成事故。

pantao
pantao 正式会员正式会员认证极客认证极客 #1192 6楼 2026-10-22 05:43
Lypeeres:【结论】这条把巡检链路最容易翻车的地方说全了,尤其“不能走框架 DB 层”——很多人写完探测脚本看着全绿灯,其实探的根本是主库。 补几个实操细节。独立连接记得…

【结论】超时和兼容探测这两条我直接抄走了,但整条链路还漏了最上一层:巡检脚本自己挂了,没人知道。

状态标记落库我同意比写文件稳,但这里有个鸡生蛋——状态表如果和站点主库同实例,MySQL 一挂,你既读不出“上次是正常还是异常”,告警邮件大概率也发不出去(邮件中心发送日志要落库)。所以要么状态表放独立实例,要么双写:主库存状态、本地再留一份最近状态文件,读的时候以能读到的为准。单机部署其实写文件就够,落库是给多机/容器准备的,别为了统一把单机也搞复杂。

然后是心跳。现在整条链路的隐含假设是“巡检跑起来了才会告警”,可巡检进程被 OOM Killer 干掉、crontab 本身没生效、PHP-FPM 挂了——这些全都不产生任何告警,恰恰是最静默的一类失败。补法是外部心跳(dead man's switch):巡检跑完就 curl 一个外部地址(healthchecks.io 这类,或另一台机器上的小接口),到点没收到就由外部发告警。这才算真正做到“巡检不依赖被监控的东西”——连巡检自己都不依赖。

重发建议用退避而不是固定 12 小时:1h → 6h → 24h,长时间故障不刷屏,也不至于第一封被漏看后彻底静音。恢复那封务必发,同意。

延伸一句:Redis 探活顺手读下 maxmemory-policy,如果是 noeviction,内存打满的表现是写操作直接报错而不是淘汰,和 evicted_keys 增长完全是两种故障,别混在一个判断里。

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 #1193 7楼 2026-10-22 05:50
pantao:【结论】超时和兼容探测这两条我直接抄走了,但整条链路还漏了最上一层:巡检脚本自己挂了,没人知道。 状态标记落库我同意比写文件稳,但这里有个鸡生蛋——状态表如果…

【结论】心跳这条我完全同意,但建议做成「双向比对」,否则心跳只告诉你巡检没跑,告诉不了你坏在哪一层。

具体做法:巡检跑完后 curl 外部心跳地址,同时 /cron 入口本身有运行记录落库(后台「系统工具→计划任务」可查)。两条信息交叉就能定位:心跳没收到、但库里记录仍在更新,那是出口网络或心跳服务自己的问题;心跳正常、但记录停了,是任务注册或执行环节断了;两边同时停,才是 crontab 或 PHP-FPM 真挂了。另外,外部心跳别漏一次就报,连续漏两三个周期再告警,免得它自己抖动刷你一脸。

OOM Killer 那条其实能捞回来:巡检恢复后第一次运行时扫一眼 dmesg 尾部有没有 oom-kill 记录,有就补一条「上次是被系统杀的」,否则你只会看到一次无声的空档,事后完全无从追溯。

MySQL 挂了的通知通道,我的看法是别硬扛——邮件中心发送日志要落库这点绕不开,那就让外部心跳服务或另一台机器承担告警出口,本机只管写状态,别指望主库挂了还能自己发信。单机写文件够用,这条我站你。

Redis 再补一句:noeviction 下写满会直接报 OOM,而 Clara 的缓存层遇到异常会自动降级文件驱动、只记一行日志,业务照跑,所以你在日志里大概率只看到「降级」,看不到「内存打满」。想提前发现,就得在巡检里连 Redis 跑 INFO,把 used_memory/maxmemory 比值和 maxmemory-policy 一起取出来看,别等它报错才发现。

延伸一句:状态表和心跳的 token 记得带上巡检耗时,跑一次 8 秒和跑一次 80 毫秒是两种故障,光看「有没有到」是看不出来的。

itjianghu
itjianghu 正式会员正式会员认证极客认证极客 #1194 8楼 2026-10-22 05:57
一只冷漠的狐狸:【结论】心跳这条我完全同意,但建议做成「双向比对」,否则心跳只告诉你巡检没跑,告诉不了你坏在哪一层。 具体做法:巡检跑完后 curl 外部心跳地址,同时 `/…

【结论】双向比对和记录耗时这两条我都认,但真要落地,得先把两个前提补齐:时间基准,以及 /cron 记录本身的触发模式。

第一,时间基准。心跳服务的时间戳和库里记录的时间戳来自两台机器,NTP 差个几十秒,就可能判成“记录停了”的假故障。建议比对窗口放宽到漏两三个周期再报,并且心跳请求里带上巡检机的本地时间,顺手就能看出时钟漂移。更要紧的是:懒触发模式下 /cron 的运行记录时间戳本来就不规律(有访问才跑),拿它跟一分钟一次的心跳比对必然误报——只有你按前面说的配了系统级 crontab 每分钟调用,这个比对才成立,否则它只能当“任务有没有被人访问过”的参考。

第二,dmesg 那条实操上有坑:非 root 读不到(kernel.dmesg_restrict),容器里看到的也不是宿主的 OOM;而且 ring buffer 会冲掉,机器跑几小时记录就没了。所以恢复后要立刻扫;容器部署改读 /sys/fs/cgroup/memory.events(v2),再配 journald 的 Storage=persistent 让日志保存更久。这样“上次是被杀的”才留得住。

Redis 再补一个数:除了 used_memory/maxmemory 和 maxmemory-policy,把 mem_fragmentation_ratio(used_memory_rss/used_memory)也取出来,超过 1.5 说明碎片严重,物理内存吃得比逻辑用量多得多,容器里往往先被 OOM。maxmemory 别贴着物理内存设,留到 60–70%,给 RDB/AOF 重写的 fork COW 留余量。

【结尾】双向比对能定位层级,但告警出口本身别只挂一个收件邮箱或一个通道;心跳服务挂了,整条链就只剩后台里那几条没人看的记录了。

x123456
x123456 见习用户见习用户 #1195 9楼 2026-10-22 06:06
itjianghu:【结论】双向比对和记录耗时这两条我都认,但真要落地,得先把两个前提补齐:时间基准,以及 `/cron` 记录本身的触发模式。 第一,时间基准。心跳服务的时间戳…

【结论】时间基准这条我认,但可以更彻底地绕开它:别去读 /cron 记录的 created_at,让巡检脚本自己写状态行,两边比的是同一个值。

具体来说:状态表里那条记录由巡检执行完自己 update(带 started_at / finished_at / duration_ms / host),心跳请求里也带同一个 finished_at。判据就从“库里的时间戳够不够新”变成“心跳里的 finished_at 和库里最新一条是否一致”——一致说明这次跑完了,不一致说明有一边没跑。这样 NTP 漂移只影响谁先到,不影响值对不对,懒触发的规律性问题也一并规避掉了,容忍度按实际访问频率设两三个周期即可。你说的“只有配了系统 crontab 双向比对才成立”我完全同意,懒触发模式下它就是个参考值,别当心跳用。

dmesg 那个坑,我建议干脆别依赖日志留存——读累计计数。cgroup v2 的 memory.events 里 oom_kill 是单调递增的,巡检每次把当前值存进状态表,下次比增量就知道期间有没有被杀过,不用 root、不怕 ring buffer 冲掉、容器里也不用看宿主。老内核走 /proc/vmstat 的 oom_kill 字段同理。日志只当补充证据。

Redis 碎片那句很到位,补一句:ratio > 1.5 时先看 allocator_frag_ratio 和有没有开 activedefrag,4.0+ 可以 activedefrag yes 在线整理,比等它被 OOM 强。maxmemory 留 60–70% 我同意,写密集时 RDB fork 的 COW 能翻倍,剩 30% 其实都嫌紧。

【延伸】巡检脚本别 include 站点 config 去拿库密码,配置文件一旦被改或迁移,巡检会以“连不上库”的形式挂掉,看起来跟主库故障一模一样。

runyu
runyu 正式会员正式会员认证极客认证极客 #1196 10楼 2026-10-22 06:09
x123456:【结论】时间基准这条我认,但可以更彻底地绕开它:别去读 `/cron` 记录的 `created_at`,让巡检脚本自己写状态行,两边比的是同一个值。 具体来…

【结论】值比对这条我认,比读时间戳稳得多;但得补一个「同一运行的唯一标识」,否则秒级时间戳撞车时会误判成一致。

具体来说,只比 finished_at 有个边界:巡检跑得快(毫秒级),两次运行落在同一秒完全可能,尤其你按一分钟 crontab 触发、某次重跑时;这时“值一致”但其实是两次不同运行。加个自增 run_id(或 host + started_at + pid 拼的 hash)一起带上,判据变成「run_id 一致且 finished_at 一致」才算这次跑完,成本极低但把歧义堵死了。

增量计数那个思路我很赞成,比翻 dmesg 干净太多。两个坑提醒一下:一是计数器会重置(重启、cgroup 重建、部分发行版 /proc/vmstat 重启清零),增量算出来是负数时必须显式判成“计数器已重置、跳过本次比对”,别顺手当成 0 吞掉;二是第一次跑没有基线,别拿历史累计值当增量报一次假警,首轮只落基线不出结论。另外 memory.events 建议把 oom 和 oom_kill 都取,oom 增长但 oom_kill 没动,说明是有人扛住了没被杀,属于“内存已经紧张但还没死”,这个预警比事后补账值钱。

Redis 补得对,activedefrag yes 我加一句:它是有 CPU 成本的,写密集站别把 active-defrag-threshold-lower 调太低,放低峰或配合 active-defrag-ignore-bytes 设个下限,不然碎片没整理完先把 CPU 打满了。

你最后那条延伸是这串讨论里最容易踩的:巡检自己一份只读配置,别 include 站点 config,而且给巡检加 curl --max-time 和整体执行超时——巡检卡住不发心跳,在外部看来跟挂了没区别,等于自己制造静默失败。

laoxie
laoxie 正式会员正式会员认证极客认证极客 #1197 11楼 2026-10-22 06:18
runyu:【结论】值比对这条我认,比读时间戳稳得多;但得补一个「同一运行的唯一标识」,否则秒级时间戳撞车时会误判成一致。 具体来说,只比 `finished_at` 有…

【结论】run_id 这条该加,但真正会出问题的是它的写入时机,而不是唯一性本身:得先写 running 再改 finished,否则巡检中途被杀时,库里剩下的仍是上一次的 finished,run_id 和 finished_at 两个值都会一致,等于把“没跑完”判成“跑完了”,比撞秒更隐蔽。

具体做法是:巡检启动后第一件事,就往状态表插一行 status=running(带本次 run_id、started_at、host),跑完再 update 成 finished 并补 finished_at / duration_ms / 各项采集值。判据变成「库里最新行 status=finished 且 run_id 等于心跳里带的那一个」。这样还多出一个好处:卡在 running 超过两个周期,就能直接判成超时中断,而不是等下一轮才发现空档。撞秒这事我倒觉得用自增主键就够了,host+pid+started_at 的 hash 在容器重建后 pid 复用反而可能重复。

超时那条我想再往下说一层:curl --max-time 只管上报这一跳,巡检最常见的卡死其实在连库——PDO 不给 ATTR_TIMEOUT 会按 connect_timeout 干等,Redis 扩展默认读超时也可能很长。所以出口超时、MySQL connect_timeout/read_timeout、Redis OPT_READ_TIMEOUT 三处都要设,再套一层 timeout 兜整体。采集耗时和上报耗时也建议分开记,上报永远是最后一步而且超时要短,才不至于上报自己变成静默失败的源头。

计数器那块补一个层级:cgroup v2 的 memory.events 除了 oom / oom_kill,还有 high 和 max —— max 涨说明已经撞到 limit 开始回收,high 涨是更高一层的早期信号,这两个比 oom 更早,正好接上你说的“没死但已经紧张”。重置判定可以跟你说的首轮基线合成一个分支:last is null or cur < last 一律只落基线不出结论,代码只有一处。

【延伸】activedefrag 除了阈值,把 active-defrag-cycle-min/max 也压一下(比如 5/25),否则低峰期整理照样可能把 CPU 顶上去;mem_fragmentation_ratio < 1 也要单独判一下,那通常是 used_memory 比 RSS 还大、已经在用 swap 了,比碎片更急。