必须分三步走:先确保slow_query_log真正启用且阈值合理,再用pt-query-digest按时间窗口聚合分析,最后用轻量脚本触发去重告警;否则90%线上环境会漏告、误告或压垮磁盘。

直接上结论:只靠 slow_query_log 文件 + 简单脚本发邮件,90% 的线上环境会漏告、误告或压垮磁盘。必须分三步走——先确保日志真正开着且阈值合理,再用 pt-query-digest 做时间窗口聚合,最后用轻量脚本触发去重告警。
确认 slow_query_log 已真正启用且 long_query_time 合理
别信 SHOW VARIABLES 返回的“ON”就万事大吉。很多实例的 slow_query_log 是 ON,但 log_output 是 TABLE(MySQL 8.0+ 默认),而 pt-query-digest 只读文件,mysql.slow_log 表又不适合实时告警。
- 执行
SHOW VARIABLES LIKE 'log_output';,必须包含FILE;若只有TABLE,加配置log_output = FILE并重启 -
long_query_time设为1.0(不是1)才生效;OLTP 场景建议从0.5起调,但别一开始就设0.1,否则日志写满磁盘 -
slow_query_log_file路径要独立,比如/data/mysql-slow.log,不能和datadir共用磁盘,否则慢查暴增时可能卡住整个实例刷盘 - 检查权限:
ls -l /data/mysql-slow.log确保 mysql 用户可写;df -h /data确保剩余空间 >20%
用 pt-query-digest 按分钟窗口统计峰值,别用 tail + awk
用 tail -f /var/log/mysql-slow.log | awk '/# Time:/ {c++} END{print c}' 这类写法是错的:日志行不是严格按时间顺序写入,多线程刷盘会导致截断、重复或跳行;而且它根本无法区分“最近 1 分钟”和“今天累计”。
- 正确做法是每次运行都指定时间范围:
pt-query-digest --since "$(date -d '1 minute ago' +'%Y-%m-%d %H:%M:%S')" /data/mysql-slow.log - 如果日志被
logrotate切分(如mysql-slow.log.1.gz),必须加--read-per-process参数,否则漏读历史条目 - 提取“过去 1 分钟慢查询总数”最稳的方式是解析 JSON 输出:
pt-query-digest --report-format json --since "$(date -d '1 minute ago' +'%Y-%m-%d %H:%M:%S')" /data/mysql-slow.log | jq '.summary.total_events' - 注意:不要在高峰期加
--filter做复杂正则匹配,会拖慢解析速度,甚至让脚本超时
发邮件前必须做频次去重和内容裁剪
每分钟都发一封“发现 3 条慢查”的邮件,DBA 5 分钟后就会 mute 掉整个邮箱。关键不是“有没有”,而是“是不是新问题”。
- 用 Redis 缓存 SQL 指纹(
md5($normalized_sql))+ 时间戳,5 分钟内同指纹只告一次;避免SELECT * FROM orders WHERE status=1每秒刷屏 - 邮件正文只放 Top 3 高频慢查的摘要:平均耗时、
Rows_examined、数据库名、首次出现时间;完整日志作为附件,用gzip压缩后再发 - SMTP 配置失败最常见原因是本地没装
ssmtp或密码含特殊字符没转义;推荐改用 Python 脚本发,明确控制errors='ignore'防止日志乱码中断解码 - 别在循环里调
sendmail—— 把告警攒成批次,每 5 分钟 flush 一次,既防洪峰也减负载
最易被忽略的一点:应用连接池里的长连接不会自动继承新的 long_query_time 全局值。即使你 SET GLOBAL 改了,老连接仍按旧阈值记慢查。要么让应用重启连接池,要么在应用启动时统一执行 SET SESSION long_query_time = 0.5。这点不处理,监控永远有盲区。



















