尚拙

一个分享技术、学习成长的个人博客网站

0%

用 Python 自动化监控 MySQL 主从复制状态:从部署到告警全流程指南

在生产环境中,MySQL 主从同步的稳定性对业务至关重要。一旦从库同步中断或延迟过高,可能导致数据不一致或业务异常。因此,通过自动化脚本监控从库状态,并在异常时及时告警,是运维必备手段。本文将记录我在 Linux 环境下使用 Python 脚本结合 Cron 定时任务实现这一目标的过程。

环境说明

  • 操作系统:Linux
  • Python 版本:Python 3.x
  • MySQL 版本:8.0+
  • 邮件服务:126 邮箱 SMTP
  • 定时任务:Cron,每 3 小时执行一次

一、为什么需要自动化监控?

MySQL主从同步异常(IO/SQL线程停止、延迟过高)会导致:

  • 数据不一致
  • 业务查询延迟
  • 严重时引发服务雪崩

手动检查效率低,自动化是运维刚需


二、解决方案:Python脚本 + Cron定时任务

设计思路

监控 MySQL 主从同步状态,主要关注三个字段:

  1. Slave_IO_Running:从库 IO 线程是否运行
  2. Slave_SQL_Running:从库 SQL 线程是否运行
  3. Seconds_Behind_Master:从库延迟(秒)

脚本主要功能

项目 说明
执行频率 每3小时(Cron实现,避免频繁轮询数据库)
异常检测 3维度:Slave_IO_RunningSlave_SQL_RunningSeconds_Behind_Master
告警方式 126邮箱SMTP邮件(支持手机/企业微信推送)
日志追踪 详细记录异常原因,定位问题更快

脚本逻辑:

  1. 连接从库数据库
  2. 获取 SHOW SLAVE STATUS 信息
  3. 判断 IO/SQL 线程状态及延迟是否正常
  4. 异常时发送告警邮件
  5. 日志记录执行结果,便于追踪历史问题

三、环境准备(关键!)

1. 数据库权限配置

必须创建专用监控账号(避免使用 root):

CREATE USER 'monitor'@'%' IDENTIFIED BY 'your_strong_password';
GRANT REPLICATION CLIENT ON *.* TO 'monitor'@'%';
FLUSH PRIVILEGES;

2. 邮件服务配置

  • 126/163 邮箱:需在邮箱设置中开启 POP3/SMTP 服务,获取SMTP 授权码(非邮箱密码!)
  • 其他邮箱:确认 SMTP 服务器地址和端口(如 Gmail 使用 smtp.gmail.com:587

3. 系统环境

# 安装 Python 依赖
sudo apt-get install -y python3-pip
pip3 install pymysql[rsa] # 注意:pymysql 需要 rsa 支持

验证点:确保能用 mysql -umonitor -p 连接从库,且能执行 SHOW SLAVE STATUS


四、脚本实现(核心逻辑)

1. 参数配置(config.py 隔离配置)

# config.py(建议单独文件,避免硬编码)
DB_CONFIG = {
'host': '192.168.1.100', # 从库IP
'port': 3306,
'user': 'monitor',
'password': 'your_strong_password',
'database': 'mysql',
'charset': 'utf8mb4',
}

MAIL_CONFIG = {
'smtp_server': 'smtp.126.com',
'smtp_port': 465,
'sender': 'your_email@126.com',
'password': 'YOUR_SMTP_AUTH_CODE', # 126 邮箱授权码
'receiver': 'alert@yourcompany.com',
'subject_prefix': '[MySQL同步告警]'
}

ALERT_THRESHOLD = 300 # 延迟超过 5 分钟告警(单位:秒)
LOG_FILE = '/var/log/mysql_slave_monitor.log'

💡 为什么设 300 秒?
业务可接受的延迟通常 < 5 分钟,超过即需人工介入。


2. 日志配置(清晰记录每一步)

import logging
import os

# 确保日志目录存在
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)

logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
handlers=[
logging.FileHandler(LOG_FILE, encoding='utf-8'),
logging.StreamHandler() # cron 执行时输出到 stdout
]
)
logger = logging.getLogger(__name__)

3. 数据库连接与状态检查(关键逻辑)

def check_replication():
"""检查主从同步状态"""
try:
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
cursor.execute("SHOW SLAVE STATUS")
status = cursor.fetchone()

# 检查关键字段
io_run = status['Slave_IO_Running']
sql_run = status['Slave_SQL_Running']
delay = status['Seconds_Behind_Master']

# 异常判定
if io_run != 'Yes' or sql_run != 'Yes':
return f"线程异常: IO={io_run}, SQL={sql_run}"
if delay is not None and delay > ALERT_THRESHOLD:
return f"延迟超标: {delay}s (阈值: {ALERT_THRESHOLD}s)"

return None # 无异常

except Exception as e:
return f"数据库错误: {str(e)}"
finally:
if 'cursor' in locals(): cursor.close()
if 'conn' in locals(): conn.close()

⚠️ 关键细节

  • Seconds_Behind_MasterNULL 时(线程停止),需单独处理(代码中已覆盖)
  • 使用 DictCursor 确保字段名可读性

4. 邮件告警(异常时触发)

def send_alert(issue):
"""发送邮件告警"""
try:
msg = MIMEText(
f"【MySQL同步异常】\n时间: {datetime.now()}\n异常详情: {issue}",
'plain', 'utf-8'
)
msg['Subject'] = Header(f"{MAIL_CONFIG['subject_prefix']} {socket.gethostname()}", 'utf-8')
msg['From'] = MAIL_CONFIG['sender']
msg['To'] = MAIL_CONFIG['receiver']

with smtplib.SMTP_SSL(MAIL_CONFIG['smtp_server'], MAIL_CONFIG['smtp_port'], timeout=10) as server:
server.login(MAIL_CONFIG['sender'], MAIL_CONFIG['password'])
server.sendmail(MAIL_CONFIG['sender'], [MAIL_CONFIG['receiver']], msg.as_string())
logger.info("告警邮件发送成功")
except Exception as e:
logger.error(f"邮件发送失败: {str(e)}")

五、脚本部署

  1. 保存脚本/usr/local/bin/mysql_slave_monitor.py):

    sudo nano /usr/local/bin/mysql_slave_monitor.py

    💡 用 chmod +x 确保可执行:sudo chmod +x /usr/local/bin/mysql_slave_monitor.py

  2. 创建日志目录

    sudo mkdir -p /var/log
    sudo chown $USER:$USER /var/log # 确保当前用户有写权限

六、定时任务配置(Cron 3小时一次)

# 编辑 cron 任务
crontab -e

# 添加以下行(每3小时执行一次,避开业务高峰)
0 */3 * * * /usr/bin/python3 /usr/local/bin/mysql_slave_monitor.py >> /var/log/mysql_slave_monitor.log 2>&1

验证 Cron

# 检查 cron 是否生效
ps aux | grep cron

# 测试执行(手动触发)
/usr/bin/python3 /usr/local/bin/mysql_slave_monitor.py

七、测试验证

测试 1:正常状态(无告警)

# 模拟正常状态(实际运行中无需操作)
$ /usr/bin/python3 /usr/local/bin/mysql_slave_monitor.py
[2026-02-12 17:00:00] INFO: 同步正常 | IO:Yes | SQL:Yes | 延迟:15s

测试 2:模拟 IO 线程异常(触发告警)

# 在 MySQL 从库执行(模拟故障)
STOP SLAVE IO_THREAD;

# 运行监控脚本
$ /usr/bin/python3 /usr/local/bin/mysql_slave_monitor.py
[2026-02-12 17:05:00] WARNING: IO线程异常: No
[2026-02-12 17:05:00] ERROR: 检测到同步异常:
• Slave_IO_Running 异常: No
[2026-02-12 17:05:00] INFO: 告警邮件发送成功

验证结果

  1. 日志中记录 WARNINGERROR
  2. 邮箱收到告警邮件(检查垃圾箱!)

测试 3:延迟超标(触发告警)

# 在主库执行大事务(制造延迟)
INSERT INTO large_table SELECT * FROM large_table;

# 30 分钟后运行脚本(延迟 > 300s)
$ /usr/bin/python3 /usr/local/bin/mysql_slave_monitor.py
[2026-02-12 17:30:00] WARNING: 同步延迟: 1800s
[2026-02-12 17:30:00] ERROR: 检测到同步异常:
• 同步延迟过高: 1800s (阈值: 300s)
[2026-02-12 17:30:00] INFO: 告警邮件发送成功

常见问题排查

问题现象 解决方案
邮件未收到 1. 检查 126 邮箱是否开启 SMTP 服务2. 确认授权码正确(非登录密码)3. 防火墙放行 465 端口
脚本执行失败(权限错误) sudo chmod +x /usr/local/bin/mysql_slave_monitor.py + 确保日志目录有写权限
Seconds_Behind_Master 为 NULL 检查 Slave_IO_RunningSlave_SQL_Running 是否为 Yes(线程已停止)
Cron 无法执行 在脚本开头添加 #!/usr/bin/env python3 + 使用 绝对路径(如 /usr/bin/python3

示例代码

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MySQL主从同步状态监控脚本
- 每3小时通过cron执行一次
- 监控关键字段:Slave_IO_Running, Slave_SQL_Running, Seconds_Behind_Master
- 异常时发送邮件告警
- 完整日志记录
"""

import pymysql
import smtplib
import logging
import os
import sys
import socket
from datetime import datetime
from email.mime.text import MIMEText
from email.header import Header

# ==================== 配置区(按实际修改)====================
# 数据库配置(监控从库)
DB_CONFIG = {
'host': '127.0.0.1', # 从库IP
'port': 3306,
'user': '', # 建议创建专用监控账号
'password': '',
'database': 'mysql', # 连接必需库
'charset': 'utf8mb4',
'connect_timeout': 10
}

# 邮件配置(使用126邮箱SMTP)
MAIL_CONFIG = {
'smtp_server': '',
'smtp_port': 465,
'sender': '', # 发件人邮箱(需与授权码匹配)
'password': '', # 126邮箱SMTP授权码(非登录密码!)
'receiver': '',
'subject_prefix': '[MySQL同步告警]'
}

# 监控阈值
ALERT_THRESHOLD = 1800 # Seconds_Behind_Master 超过300秒告警
LOG_FILE = '/var/log/mysql_slave_monitor.log' # 建议使用有写入权限的路径

# ==================== 日志配置 ====================
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
handlers=[
logging.FileHandler(LOG_FILE, encoding='utf-8'),
logging.StreamHandler(sys.stdout) # cron执行时可选保留,方便调试
]
)
logger = logging.getLogger(__name__)


# ==================== 邮件发送函数 ====================
def send_alert_email(issue_details: str, slave_status: dict):
"""发送告警邮件"""
try:
# 构建邮件内容
hostname = socket.gethostname()
ip_addr = socket.gethostbyname(hostname)
body = f"""
【MySQL主从同步异常告警】
检测时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
服务器:{hostname} ({ip_addr})
从库地址:{DB_CONFIG['host']}:{DB_CONFIG['port']}

异常详情:
{issue_details}

请及时处理!
"""

msg = MIMEText(body, 'plain', 'utf-8')
msg['From'] = Header(MAIL_CONFIG['sender'])
msg['To'] = Header(MAIL_CONFIG['receiver'])
msg['Subject'] = Header(f"{MAIL_CONFIG['subject_prefix']} 主从同步异常 - {hostname}", 'utf-8')

# 发送邮件(SSL)
with smtplib.SMTP_SSL(MAIL_CONFIG['smtp_server'], MAIL_CONFIG['smtp_port'], timeout=15) as smtp:
smtp.login(MAIL_CONFIG['sender'], MAIL_CONFIG['password'])
smtp.sendmail(MAIL_CONFIG['sender'], [MAIL_CONFIG['receiver']], msg.as_string())

logger.info("告警邮件发送成功")
return True
except Exception as e:
logger.error(f"邮件发送失败: {str(e)}", exc_info=True)
return False


# ==================== 主监控逻辑 ====================
def check_replication():
"""检查主从同步状态"""
conn = None
cursor = None
issues = []
slave_status = {}

try:
# 连接数据库
conn = pymysql.connect(**DB_CONFIG)
cursor = conn.cursor(pymysql.cursors.DictCursor)
cursor.execute("SHOW SLAVE STATUS")
result = cursor.fetchone()

if not result:
err_msg = "未检测到从库配置(SHOW SLAVE STATUS 无结果)"
logger.error(err_msg)
issues.append(err_msg)
else:
slave_status = result
io_running = result.get('Slave_IO_Running', '').strip()
sql_running = result.get('Slave_SQL_Running', '').strip()
seconds_behind = result.get('Seconds_Behind_Master')

# 检查IO线程
if io_running != 'Yes':
issues.append(f"Slave_IO_Running 异常: {io_running}")
logger.warning(f"IO线程异常: {io_running}")

# 检查SQL线程
if sql_running != 'Yes':
issues.append(f"Slave_SQL_Running 异常: {sql_running}")
logger.warning(f"SQL线程异常: {sql_running}")

# 检查延迟(None视为异常)
if seconds_behind is None:
issues.append("Seconds_Behind_Master 为 NULL(可能线程已停止)")
logger.warning("同步延迟为NULL")
elif seconds_behind > ALERT_THRESHOLD:
issues.append(f"同步延迟过高: {seconds_behind}s (阈值: {ALERT_THRESHOLD}s)")
logger.warning(f"同步延迟: {seconds_behind}s")

# 记录正常状态(便于日志追踪)
if not issues:
logger.info(f"同步正常 | IO:{io_running} | SQL:{sql_running} | 延迟:{seconds_behind}s")
return True

# 存在异常且需告警
if issues:
issue_summary = "\n".join([f"• {i}" for i in issues])
logger.error(f"检测到同步异常:\n{issue_summary}")
if send_alert_email(issue_summary, slave_status):
return False
else:
logger.critical("告警邮件发送失败,需人工介入!")
return False

return True

except pymysql.err.OperationalError as e:
logger.error(f"数据库连接失败: {e}", exc_info=True)
send_alert_email(f"数据库连接失败: {str(e)}", {})
return False
except Exception as e:
logger.error(f"监控过程发生未知错误: {str(e)}", exc_info=True)
send_alert_email(f"脚本执行异常: {str(e)}", {})
return False
finally:
if cursor:
cursor.close()
if conn:
conn.close()


# ==================== 程序入口 ====================
if __name__ == '__main__':
logger.info("=" * 50)
logger.info("开始执行MySQL主从同步状态检查")
success = check_replication()
status = "✓ 检查完成(状态正常)" if success else "✗ 检查完成(存在异常)"
logger.info(status)
sys.exit(0 if success else 1)