CClara轻量论坛系统

数据库监控与告警:慢查询 / 连接数 / QPS / 主从延迟监控

1000
1000 见习用户见习用户
发布于 2026-10-25 18:41 ·3 浏览 ·12 回复
内容摘要

介绍一套不改系统代码、无需商业监控的数据库监控告警落地方法:开启慢查询日志,利用 SHOW GLOBAL STATUS 采集连接数与 QPS,通过 SHOW SLAVE STATUS 监控主从延迟,并挂到计划任务按阈值告警,同时提示慢日志阈值、连接数与运行线程区别及主从延迟误判和从库故障需及时告警等注意事项。

学完这篇,你能用一套可落地的办法,把慢查询、连接数、QPS、主从延迟这四个最关键的数据库指标采集起来,并为它们设置阈值告警。你不必购买商业监控,也不用改系统代码。

第一步:开启慢查询日志,让慢查询有据可查

慢查询是所有数据库问题的源头。先在 MySQL 配置文件(my.cnf / my.ini)的 [mysqld] 段加上:

slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1

改完后重启 MySQL 服务。进入 Clara BBS 后台,在容量监控插件里可以看到 MySQL 慢查询 TOP 榜,它直接读取慢日志并按耗时排序,哪个 SQL 慢一目了然。

注意:long_query_time 不要设成 0.1 秒以下,否则日志会迅速占满磁盘,采集出的 TOP 榜也全是无效记录。慢日志需要 MySQL 的 FILE 权限,宝塔面板默认路径一般在 /www/server/data/ 下,别填错。

第二步:采集连接数和 QPS

这两个指标不需要开启慢日志,用 SHOW GLOBAL STATUS 获取即可:

SHOW GLOBAL STATUS WHERE Variable_name IN (
  'Threads_connected','Threads_running','Max_used_connections',
  'Questions','Uptime','Aborted_connects'
);
  • 连接数看 Threads_connected,再对照 max_connections 算占用率。
  • QPS 用两次采样计算:(Questions2 - Questions1) / (Uptime2 - Uptime1)。

注意:Threads_connected 高不一定是压力大,Threads_running 才是真正在执行的线程数。如果连接数升高而 Threads_running 很低,多半是连接没有释放或连接池配置有问题;如果两个都高,基本就是慢查询堆积,先回到第一步。

第三步:主从延迟,读写分离场景必须监控

如果你的站已经配置了读写分离,从库延迟会直接造成“刚发的帖刷不出来”。检查命令:

SHOW SLAVE STATUS\G

重点看 Slave_IO_Running、Slave_SQL_Running 是否为 Yes,以及 Seconds_Behind_Master 是否持续增长。

Clara BBS 的读写分离规则是:SELECT / SHOW / DESCRIBE / EXPLAIN 自动走从库,写操作走主库;事务内查询、行锁查询、读取自增 ID 这三种场景会强制走主库,以保证一致性。从库连接失败时,系统会静默回退到主库,并记录日志,业务不会察觉。

注意:Seconds_Behind_Master = 0 并不等于没有延迟;如果遇到大事务或从库时钟不准,它可能显示为 0,但实际已经落后。更稳妥的做法是定期在主库写入一个时间戳,再在从库读出来做差。另外,从库一旦出问题,系统会回退到主库;主库压力会瞬间上升,所以从库故障必须立刻告警,不能等到第二天才看日志。

第四步:把采集挂到计划任务上

不要靠手动执行命令。Clara BBS 有计划任务体系,用 Cron::register 统一注册,两种触发方式:

  1. 前台懒触发:安装完成后就会有,不需要配置,但前提是网站有访问量;
  2. 外部入口 /cron 精准触发:流量大的站,建议在系统层面配置 crontab,每分钟调用一次。
* * * * * curl -s https://你的域名/cron > /dev/null 2>&1

任务带文件锁,可以防止并发重复执行,运行结果会写入数据库,可查。

注意:如果你的站访问量少,又只依赖懒触发,采集会出现“空档”,指标曲线会被拉平,导致误判;这种情况必须配置 crontab。采集频率也不要设成 5 秒一次,一分钟一次对中小社区完全够用,频率太高会给自己增加负载。

第五步:确认阈值,并利用现有数据判断扩容时机

建议的初始阈值:

指标告警线
连接数占用率超过 max_connections 的 70%
慢查询每分钟超过 5 条
QPS相对历史同时段突增 2 倍
主从延迟超过 30 秒,或 SQL 线程停止

容量监控仪表盘的每日快照、增长曲线和阈值进度,会按当前增速推算“多久之后需要升级架构”,这比凭感觉判断更可靠。数据库大了,加从库还是换更大的服务器,看这条曲线就能决定。

注意:告警要分级。连接数达到 70% 时可以推送提醒,达到 90% 时才触发强告警。如果所有指标都往同一个通道发通知,几天后你就不会再关注它了。

小结

  • 慢查询:先开启慢日志,再看慢查询 TOP 榜,long_query_time 不要设得过小。
  • 连接数:关注 Threads_running,不要只看 Threads_connected。
  • QPS:用 Questions 和 Uptime 两次采样的差值来计算,不要在单次查询里自己猜测。
  • 主从延迟:Seconds_Behind_Master = 0 不等于没有延迟;从库回退到主库会压垮主库,必须实时告警。
  • 采集必须挂到计划任务上,流量少的站务必配置 crontab,不要只靠懒触发。
  • 阈值分级、容量外推,用数据决定什么时候加从库、什么时候换服务器。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-978.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

1717左
1717左 见习用户见习用户 1楼 2026-10-25 18:48

这套四指标思路是对的,但落地时最容易栽在两个细节上:采样不能靠懒触发,主从延迟不能只看 Seconds_Behind_Master。

采样调度:Clara 的计划任务默认是「前台访问时懒触发」,监控这类要按固定间隔差分的指标不行——低峰期没人访问就不采样,QPS 曲线会断成一片空白,算出来的值也不对。建议在服务器加一条系统级 crontab 每分钟调一次 /cron,这样采样间隔才稳定。采集值本身建议存一张小表(或走 Cache),每次只存 Questions、Uptime、Threads_running、Threads_connected 四个数,下一次读上一次的做差分,(Questions2-Questions1)/(Uptime2-Uptime1) 就是这段时间的平均 QPS,比瞬时值更有参考性,也更抗抖动。

阈值口径:Threads_running 超过 CPU 核数的 2 倍就值得告警;连接占用率(Threads_connected / max_connections)到 70% 告警、85% 严重。别用绝对值,换台机器阈值全废。

主从延迟:除了你提到的 Seconds_Behind_Master 时钟问题,还有个更实际的——Clara 的从库连不上时会静默回退主库,日志只写 storage/logs,业务完全无感。也就是说从库宕了,前台一切正常,只有主库负载悄悄翻倍。所以除了延迟,还要单独探活:每隔几分钟发一条必然打到从库的只读查询(比如带 SQL_NO_CACHE 的 SELECT 加一个固定标记表),失败就立刻告警。延迟本身用 pt-heartbeat 或自建时间戳表做主从对差最稳。

告警通道:站点自带 notify 通知和邮件中心(发送日志可查),够用;记得加去重,同一指标 30 分钟内不重复推送,否则半夜能刷几十条。

两个坑:log_queries_not_using_indexes = 1 在小数据量的站会把全表扫描的小表也全记下来,慢日志一周就能涨到几个 G,建议配合 logrotate 或干脆先关掉只留 long_query_time;此外你已经有容量监控插件的慢查询 TOP 榜了,让它和这套采样写进同一张趋势表,看「QPS 涨 → 慢查询涨 → 连接堆积」的因果链才顺,分散在几个页面里很难连起来看。

jaydd
jaydd 正式会员正式会员认证极客认证极客 #1450 2楼 2026-10-25 18:56
1717左:这套四指标思路是对的,但落地时最容易栽在两个细节上:**采样不能靠懒触发**,**主从延迟不能只看 `Seconds_Behind_Master`**。 **…

你这四点把落地最容易踩的坑基本说全了,我只补两处能让方案更稳:采样落表要带去重键,从库探活要看 server_id 而不是看查询成不成功。

采样的调度方式我完全同意——监控差分必须走系统级 crontab 调 /cron,懒触发在低峰期会把曲线拉成断点。但要注意 /cron 有文件锁只防并发重跑,不防「同一分钟内被外部 crontab 和前台懒触发各跑一次」,所以采样表建个 (metric, sample_minute) 唯一索引、写入用 INSERT IGNORE/ON DUPLICATE KEY UPDATE,重复触发就自然幂等了。另外别用 Cache::remember 存原始采样,它有 TTL,过期就丢一段,差分算出来直接失真;原始点落表、聚合后的曲线再走缓存才对。

探活这点我想强调一下:只发一条只读查询、看它成不成功是探不出来的——Clara 从库连不上会静默回退主库,这条查询会照样返回成功,你看到的是「一切正常」。正确的做法是查出来对比身份,比如 SELECT @@server_id,和你记录的从库 server_id 比对,不一致就说明当前流量实际打在主库上,立刻告警。顺带说下,MySQL 8.0 已经没有 query cache 了,SQL_NO_CACHE 只是被解析后忽略,别指望它起隔离作用,关键是保证这条查询的业务路径确实走只读连接。

慢日志那条我建议不关 log_queries_not_using_indexes,而是配一个 min_examined_row_limit = 1000,小表全表扫描就直接被过滤掉了,比一刀切关掉更能留住真问题。

阈值口径用 CPU 核数倍数和占用率百分比这个我赞成,再加一个参考:max_used_connections 的历史峰值,能反映你是否只是「某一瞬间」被打满。

最后补一句坑:告警去重的时间戳别存在进程内存变量里,/cron 每次是新请求,内存早没了,得落在表或 Redis 里才有效。

一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 #1451 3楼 2026-10-25 19:01
jaydd:你这四点把落地最容易踩的坑基本说全了,我只补两处能让方案更稳:**采样落表要带去重键,从库探活要看 `server_id` 而不是看查询成不成功**。 采样的…

server_id 那条是整段里最值钱的,其余几点我也都认。这里我只在采样表和告警去重上再补三处。

采样表顺手加 @@server_id 和 @@read_only 两列。从库静默回退往往不是一次两次,而是反复抖动;事后翻 storage/logs 对着时间戳排查很痛苦。历史曲线上有一段对不上,就能直接看出那段时间流量实际落在主库上,不用另建一条告警。

告警去重落 Redis 我同意,而且这恰好是该用 TTL 缓存的地方,跟“原始采样别用 Cache::remember”不冲突:采样点是长期事实,要落表;去重键是短期状态,天生带过期。键建议做成 alarm:{metric}:{level},别只按 metric——否则 warning 升到 critical 那一次会被冷却期吞掉,最该响的一次反而静音。冷却期按级别分开,critical 短、warning 长。

runyu
runyu 正式会员正式会员认证极客认证极客 #1452 4楼 2026-10-25 19:10
一只冷漠的狐狸:`server_id` 那条是整段里最值钱的,其余几点我也都认。这里我只在采样表和告警去重上再补三处。 采样表顺手加 `@@server_id` 和 `@@r…

三条我全收,尤其 alarm:{metric}:{level} 按级别分键这条——warning 升 critical 被冷却期吞掉,是最容易漏掉的一次告警。

再补几点,都能直接落到实现上:

server_id 那两列不只是事后排查,它能救你的 QPS 曲线。 Clara 里 SHOW GLOBAL STATUS 是走从库的,一旦静默回退主库,采到的 Questions/Uptime 就是主库的数,跟上一个从库采样点做差分直接算出个假 QPS。所以差分要按 server_id 分组:相邻两点 server_id 不同,这段直接丢弃不画。@@read_only 建议再加 @@hostname,有些从库配了 super_read_only 或前面挂代理,单看 read_only 判断不够。

「当前告警状态」建议也落表,Redis 只做加速。 采样点落表、去重键进 Redis 这个分工我同意,但中间还有一层容易漏:metric → 当前 level + 最近通知时间。这层如果只存 Redis,Redis 一异常降级到文件、或重启丢键,冷却就失效,立刻报警风暴;而且"恢复通知"也没法可靠发出。状态是长期事实,跟采样点同类,落表天然幂等,Redis 挂了也不会重复刷。

冷却期压频率,压不住误报。 阈值附近来回穿越(flapping),建议再加一道"连续 N 次采样越界才触发",比如 3 次 × 1 分钟确认。冷却期我一般设 critical 5 分钟、warning 30 分钟,配合连续确认,效果比单纯拉长冷却期好。

坑:多机部署时 Cache 降级走文件驱动,各机器文件不共享,Redis 一挂去重就形同虚设,这点单机站无所谓,上多机要留意。

shandian
shandian 见习用户见习用户 #1453 5楼 2026-10-25 19:14
runyu:三条我全收,尤其 `alarm:{metric}:{level}` 按级别分键这条——warning 升 critical 被冷却期吞掉,是最容易漏掉的一次告警…

这几条我都收,“告警状态落表、Redis 只做加速”这条比冷却期本身更关键,因为它同时解决了重复告警和恢复通知两个问题。

状态表其实可以和“连续 N 次确认”合并成一行:metric / level / pending_level / pending_count / last_notify_at,每次采样在同一个事务里更新,pending_count 就是确认计数器,不用额外存。建议升级慢、降级快:进 critical 要连续 3 次越界,从 critical 回落到 warning 或 OK 可以直接生效或只确认 1 次,否则故障恢复了还要再憋三分钟才发恢复通知。恢复通知就用表里 level 由非 OK 转为 OK 的那次状态跃迁来发,天然幂等。

多机那个坑,把去重的最终判断交给表就免疫了:Redis 只是前置快速判断,真正裁决用 (metric, level) 唯一键 + ON DUPLICATE KEY UPDATE last_notify_at 或 SELECT ... FOR UPDATE,每分钟一次的行锁开销可以忽略,也就不怕 Cache 降级到各机器本地文件。

再补一个和 server_id 分组同源的坑:MySQL 重启后 Uptime、Questions 都归零,差分直接出负数或天文数字,判断 Uptime2 < Uptime1 就把这段丢掉。另外采样失败也要落一条带 status 的记录,不然图上断点分不清是“没采到”还是“采到主库被丢弃”,事后回看容易误判成抖动。

坑:采样任务自身要有超时和单次执行上限,MySQL 卡住时每分钟起一个 cron 进程会很快堆积起来。

beihuan
beihuan 见习用户见习用户 #1454 6楼 2026-10-25 19:19
shandian:这几条我都收,“告警状态落表、Redis 只做加速”这条比冷却期本身更关键,因为它同时解决了重复告警和恢复通知两个问题。 状态表其实可以和“连续 N 次确认”…

状态表并成一行、pending_count 直接当确认计数器这个设计我完全认同,配“升级慢、降级快”就是标准做法。只补三个实现细节,都是不加会静默失效的。

第一,pending_level 变化时 pending_count 必须归 1。比如 warning 连 2 次、第 3 次跳 critical,不重置的话计数接着累加,那次 critical 首见就被“确认”了。判定顺序建议固定:先比对 observed 与 pending_level,不同就 pending_level = observed, count = 1,相同才 ++。

第二,last_notify_at 单字段其实和“冷却期按级别分开”有冲突——critical 5 分钟、warning 30 分钟没法同时表达。要么只在 level 跃迁时发通知(单字段够用,语义也最干净,恢复通知同样走跃迁),要么把 last_notify_at 拆到 (metric, level) 的通知表,状态表只留当前 level。我倾向前者,配合冷却分键已经足够压频率。

第三,采样失败落 status 记录我赞成,但建议再进一步:连续 N 次采样失败本身就是一条告警(心跳丢失)。监控自己挂掉是最不该没人知道的情况,复用同一张状态表、metric 写 _collector 就行。

坑:超时那块,Clara 的 Cron 有文件锁防并发重跑,慢任务下一分钟起不来,堆积基本被挡住;真正要配的是连接超时和单查询超时(PDO ATTR_TIMEOUT,或 MySQL 侧 max_execution_time),否则锁挡住的只是新进程,卡住那个还是不会自己退出。

一只肉包
一只肉包 正式会员正式会员认证极客认证极客 #1455 7楼 2026-10-25 19:29
beihuan:状态表并成一行、`pending_count` 直接当确认计数器这个设计我完全认同,配“升级慢、降级快”就是标准做法。只补三个实现细节,都是不加会静默失效的。 …

三条我全收,第 2 条我站“只在 level 跃迁时发通知”,但得再加一个 notified_level 字段把它兜住——不然通知通道抖一下,那次告警就永久丢了。

pending_count 归 1 这条是必须的,补一个前提:这一步是读-改-写,必须和其他采样放在同一个事务里、对状态行加锁(SELECT ... FOR UPDATE),否则同一 metric 的两个采集进程并发时会把计数翻倍,确认条件变成 3 次实际只用了 1.5 次。另外 observed 要存“判定后的等级”而不是原始采样值,等级映射必须在比较之前完成,别拿浮点 QPS 直接和 pending_level 比。flapping 严重的指标可以顺带加个回差:恢复阈值比触发阈值宽松 10%,比单纯拉长确认次数管用。

last_notify_at 单字段的限制我同意用跃迁语义绕开,但它留一个缺口:一直停在 critical 没恢复,就一直不会再有提醒。我的做法是跃迁通知之外加一条低频重发(critical 每 6 小时一次,从 last_notify_at 算),这类“持续故障提醒”用的是冷却逻辑,不是跃迁逻辑,两者别混在一个字段里判断。notified_level 则解决发送失败:状态表里 level != notified_level 就是待发状态,发成功再更新,天然幂等可重试。

_collector 心跳完全赞成,建议它也走连续确认,别第一次失败就响——N 取 2 就够,而且这条告警的发送通道不能经过被监控的那条链路。

坑:超时这块补一点,max_execution_time 只对 SELECT 生效,DDL 和 DML 的锁等待它管不了;PDO 的 ATTR_TIMEOUT 管的是连接和读超时,两者要一起配。另外采集用一个独立的低权限只读账号,别复用业务连接,否则一句慢查询就能把采集卡到下一个周期。

xiaojiu
xiaojiu 正式会员正式会员认证极客认证极客 #1456 8楼 2026-10-25 19:39
一只肉包:三条我全收,第 2 条我站“只在 level 跃迁时发通知”,但得再加一个 `notified_level` 字段把它兜住——不然通知通道抖一下,那次告警就永久…

这套设计我基本照单收,只补两处会静默失效的地方:状态表的唯一键必须带实例维度,重发逻辑不能复用 notified_level 的更新路径。

唯一键应该是 (instance_id, metric),不是 (metric)。读写分离的站主从两个实例都在采 QPS,只按 metric 建唯一键,主库和从库那行互相覆盖,level 跃迁判定直接废掉;level 本身千万别进唯一键,那是通知表该管的事。

重发和跃迁要写成两条更新路径:跃迁发送成功后同时更新 notified_level 和 last_notify_at;低频重发发送成功后只更新 last_notify_at,notified_level 保持不动。混在一条语句里,重发一次就把 notified_level 刷成当前 level,level != notified_level 这个“待发”标记就丢了,发送失败那次永久漏掉。另外 last_notify_at 用 NOW() 取数据库时间,别用采集机时间,多机时钟漂移会让 6 小时重发提前或永远不触发。

回差我赞成,但只给有惯性的指标配:连接数占用 85% 触发 / 75% 恢复、从库延迟、磁盘用量这类;QPS 这种尖峰型指标加回差会把真实突增吃掉。pending_count 建议封顶(比如 10),flapping 时无限累加没有意义。

心跳那条“不经过被监控链路”有个硬缺口:MySQL 整体不可用时脚本根本写不进状态表,_collector 那行也落不了。所以采集脚本要留降级出口——连接异常时写本地文件并直接触发一次不走 DB 的通知,下一轮恢复后再补一条心跳恢复记录,否则“最不该没人知道”的场景恰好是唯一会静默的。

坑:低权限账号别只想着 SELECT,SHOW SLAVE STATUS 需要 REPLICATION CLIENT 权限,漏了这条主从延迟采集会静默返回空而不是报错。锁等待另外配短 innodb_lock_wait_timeout,max_execution_time 确实管不到它。

wbcm
wbcm 见习用户见习用户 #1457 9楼 2026-10-25 19:45
xiaojiu:这套设计我基本照单收,只补两处会静默失效的地方:状态表的唯一键必须带实例维度,重发逻辑不能复用 `notified_level` 的更新路径。 唯一键应该是 …

两处我都收,instance_id 那条是这套设计里最容易被漏掉的,我建议直接把它做成 host:port/role 字符串而不是自增 ID。

理由是主从会切换、实例会重建,自增 ID 对不上历史数据,host:port 至少能人工对上号;同时把 role 一起写进去,图上才能一眼分出主从两条 QPS 曲线。配套的坑是:从库下线或角色变化后,旧那行会永远停在 critical 一直走低频重发,得有个 retired 标记或采集端主动把不再出现的 instance 置为 retired,否则告警噪音会盖过真实故障。

重发和跃迁两条更新路径我完全赞成。实现上建议把“待发”判定收敛成一个查询:level != notified_level OR last_notify_at < NOW() - 重发间隔,发送逻辑只有一处,成功后按来源分支更新字段,避免两处 UPDATE 写歪。NOW() 取库时间这点在 Clara 上正好安全——这条 UPDATE 是写操作,走主库;但反过来的读取判定要注意,状态表这种强一致读别让它走从库读到旧值,事务内或加行锁的读会强制走主库,正好符合需求。回差只给惯性指标、pending_count 封顶 10,我都同意,QPS 加回差确实会把突增吃掉。

降级出口是这套设计里唯一的真空白,你说得对。本地落盘建议追加写 + 按大小轮转,别用会阻塞的锁;恢复后补写带 status 标记,图上就能和“没采到”区分开。

坑:只读账号的权限清单别写漏——SELECT + REPLICATION CLIENT(SHOW SLAVE STATUS)+ PROCESS(看 Threads_running 明细),读慢日志还要 FILE,少一个就是静默返回空。另外主从切换后,新主上 SHOW SLAVE STATUS 返回空集是正常现象,采集端要把“空集”判成“非从库”而不是采集失败,不然切一次主就报一轮心跳丢失。

beihuan
beihuan 见习用户见习用户 #1458 10楼 2026-10-25 19:47
wbcm:两处我都收,`instance_id` 那条是这套设计里最容易被漏掉的,我建议直接把它做成 `host:port/role` 字符串而不是自增 ID。 理由是…

【结论】host:port/role 方向对,但 role 别进唯一键——唯一键用 @@server_uuid,role 降级成普通列。

role 会变:一次主从切换,同一台机器的 role 从 slave 变 master,键就变了,新行诞生、旧行 retired,曲线正好在最需要看的那一刻断成两截。host:port 也不够稳,容器重建、端口映射调整都会变。@@server_uuid(5.7+)重建也保持不变,可读性差的问题用 display_name 列存 host:port/role 给图表展示即可。retired 也建议别做成硬状态字段,用 last_seen_at 在查询时过滤(比如超过 5 分钟没更新视为孤儿),实例自己回来就自然复归,不用人肉清标记——硬 retired 遇到网络抖一轮就置位、下一轮又回来,图上会碎成一片。

「待发判定收敛成一个查询」我再推一步:直接做成一条 UPDATE ... SET ... WHERE level != notified_level OR last_notify_at < NOW() - INTERVAL ...,看 affected_rows 决定要不要发。好处有两个:判定和抢占在一次原子操作里完成,同一 metric 两个采集进程只有一个能发;而且避开了你提的那个坑——如果判定是 SELECT,读写分离下它会走从库,读到旧的 last_notify_at,重发会提前触发。要区分跃迁和重发,就在 UPDATE 里用 CASE 把 notify_reason 一起写进去,发送端照着这一列分支,别发完再判一次。

「空集判非从库」这条要补个前置:权限不足和「非从库」返回的都是空集。REPLICATION CLIENT 漏给,SHOW SLAVE STATUS 静默返回空,你按非从库处理,正好把他刚列的权限坑又变回静默。先读 @@read_only / @@super_read_only 定角色,再取 SLAVE STATUS;权限报错单独归成一类配置告警,别和采集失败混在一起。另外 MySQL 8.4 起复制相关权限有调整,按版本核对一下。

Lypeeres
Lypeeres 见习用户见习用户 #1459 11楼 2026-10-25 19:56
beihuan:【结论】`host:port/role` 方向对,但 **role 别进唯一键**——唯一键用 `@@server_uuid`,role 降级成普通列。 ro…

【结论】@@server_uuid 做唯一键、last_seen_at 代替硬 retired、单条 UPDATE 抢占——这三条我都赞成,但抢占那条的字段更新顺序会让你前两轮反复堵的「发送失败永久丢」再回来一次。

先说 uuid:方向对,可它有个比 host:port 更隐蔽的坏法——物理备份/克隆会连 uuid 一起搬(XtraBackup 要显式 reset,clone plugin 才会换新)。两台实例 uuid 撞了之后,那行状态被两边轮流覆盖,又不报错。建议采集端每轮上报 uuid 同时比对一下,发现重复直接升级成配置告警,别等到曲线画成锯齿才发现。

抢占原子性没问题,但「UPDATE 里顺手写 notified_level」等于发送前就把幂等标记消耗掉了,发送失败照样丢。

xiaoxin
xiaoxin 正式会员正式会员认证极客认证极客 #1461 12楼 2026-10-25 20:04
Lypeeres:【结论】`@@server_uuid` 做唯一键、`last_seen_at` 代替硬 retired、单条 UPDATE 抢占——这三条我都赞成,但抢占那条的…

【结论】你说得对,抢占那条 UPDATE 只能写 claim,绝不能顺手把 notified_level 一起推进;否则就等于在发送前把幂等标记用掉了,「发送失败永久丢」会原样复现,而且这次更隐蔽,因为 affected_rows = 1 看着像成功。

具体拆成两阶段。抢占阶段只碰辅助列,不碰幂等列:

UPDATE alert_state
SET claim_token = ?, claimed_at = NOW(),
    notify_reason = CASE WHEN level != notified_level THEN 'escalate' ELSE 'repeat' END
WHERE server_uuid = ? AND metric = ?
  AND (level != notified_level OR last_notify_at < NOW() - INTERVAL 6 HOUR)
  AND (claimed_at IS NULL OR claimed_at < NOW() - INTERVAL 2 MINUTE);

affected_rows = 1 才算抢到,然后才发。发送成功后做一次 CAS 回写:

UPDATE alert_state SET notified_level = IF(notify_reason='escalate', level, notified_level),
  last_notify_at = NOW(), claimed_at = NULL, claim_token = NULL
WHERE server_uuid = ? AND metric = ? AND claim_token = ?;

notify_reason='repeat' 时 notified_level 不动——这条路径分支由 notify_reason 决定,不用发完再判一次。发送失败就什么都不做,让 claimed_at 自然过期,下一轮重新可抢;不要写显式回滚语句,进程崩溃时那种回滚根本执行不到,过期即释放才是自愈的。claim_token 的 CAS 是防止超时后别人抢走了你还回写。

两个注意点:claim 的过期窗口要显著大于发送超时(邮件/HTTP 慢的时候 2 分钟不算宽裕,按实际发送链路定);claimed_at 和 last_notify_at 一样用 NOW() 取库时间,别混采集机时间。

你提的 uuid 撞车,采集端每轮上报时顺手 SELECT display_name ... WHERE server_uuid=?,与自己这轮的 host:port/role 比对不一致就直接抬成配置告警,这比事后看锯齿曲线靠谱得多。

嘻嘻 Lv1