MySQL进程崩溃,重启就卡死,设置自动终止脚本的解决方法
问题描述
在生产环境中,我们遇到了 MySQL 进程崩溃的问题,错误信息如下:
double free or corruption (!prev)
2024-09-15T02:17:33Z UTC - mysqld got signal 6 ;
Most likely, you have hit a bug, but this error can also be caused by malfunctioning hardware.
BuildID[sha1]=0b168807aaca44fa650560191d175e4fd878ff78
Thread pointer: 0x7f4bb0c58ec0
Attempting backtrace. You can use the following information to find out
where mysqld died. If you see no messages after this, something went
terribly wrong...
stack_bottom = 7f5000ffac10 thread_stack 0x100000
问题暂时解决方法
因为数据库服务mysqld
是由 systemd
托管的。尝试用 systemd
进行重启,但是这时的 mysqld
已经无法响应任何命令了。通过客户端发送sql 无响应,通过用 systemd stop
也不管用,但是需要等很长时间才行,我等了大概10分钟,但是现场正在使用比较着急, 就使用 kill
命令 这里不是强制用加-9
来杀死进程。 还是无法让mysqld
进程退出。最后通过 kill -9
强制杀死进程才让 mysqld
重启。
systemd
以优雅的方式重启服务。systemd
会先向服务发送停止信号(SIGTERM),然后在规定的超时时间后,发送强制停止信号(SIGKILL),以确保服务完全停止。
kill
命令,可以通过不同的信号来控制进程的终止方式
kill <pid>
发送 SIGTERM(信号 15):优雅地请求进程终止。进程可以进行清理操作并安全地终止
kill -9 <pid>
发送 SIGKILL(信号 9):强制立即终止进程。此信号无法被捕获或忽略,通常用于进程没有响应正常终止请求时
问题分析
该错误信息表明 MySQL 进程出现了严重的内存管理问题,具体是 double free or corruption
错误。这通常指示程序试图释放已经释放的内存块,或者内存出现了破坏。这种错误可能由以下原因引起:
- MySQL 自身的 bug:可能是 MySQL 的代码中存在缺陷。
- 硬件故障:不良的内存模块或其他硬件问题。
- 软件冲突:其他软件可能干扰了 MySQL 的正常运行。
处理措施
1. 尝试提交 Bug 报告
我们尝试向 MySQL 官方报告了此问题,并寻求帮助。此步骤的目的是确认是否存在已知的 bug 并寻求修复方案。
2. 升级 MySQL 版本
升级 MySQL 版本可能解决此问题。然而,由于项目处于上线阶段,不适合进行大规模的版本升级,因此我们考虑了其他解决方案。
3. 使用 Docker 容器进行升级
我们尝试使用 Docker 容器来升级 MySQL,以便进行版本测试。我们使用了 mysql:lts
镜像,但遇到了以下问题:
- 官方文档声称支持
x86-64
和x86-64-v2
架构。 - 实际运行中发现只支持
x86-64-v2
架构,这与官方文档不符。 - GitHub 上有相关议题讨论此问题,但截至 2024 年 9 月,相关议题仍未解决(GitHub 议题链接)。
由于这些问题,我们无法在 Docker 容器中解决升级问题,只能搁置升级计划。
4. 定时杀死 MySQL 进程
在无法进行版本升级和其他修复的情况下,我们选择了定时杀死 MySQL 进程的方式来应对问题。虽然这不是一个理想的解决方案,但可以暂时缓解问题,确保系统的正常运行。
实施解决方案
Shell 脚本
为了自动化这一过程,我们编写了以下 Shell 脚本来定期检查并终止 MySQL 进程,并将操作记录到日志中:
#!/bin/bash
# 设置日志文件路径
LOG_FILE="/home/scripts/mysql/logs/kill_mysqld.log"
# 获取当前时间
CURRENT_TIME=$(date "+%Y-%m-%d %H:%M:%S")
# 写入日志文件的函数
log_message() {
echo "$CURRENT_TIME - $1" >> "$LOG_FILE"
}
# 获取 mysqld 的进程号
pid=$(pgrep mysqld)
# 如果找到了进程号,则杀死这些进程
if [ -n "$pid" ]; then
log_message "Killing mysqld process(es) with PID(s): $pid"
kill -9 $pid
else
log_message "mysqld process not found"
fi
实施步骤
-
创建脚本:将上述脚本保存到
/home/scripts/mysql/kill_mysqld.sh
文件中,并确保脚本具有执行权限:chmod +x /home/scripts/mysql/kill_mysqld.sh
-
设置
cron
任务:为了每天凌晨定时运行该脚本,我们需要在crontab
中设置一个定时任务。使用以下命令编辑crontab
文件:crontab -e
然后添加以下行,设置每天凌晨 1 点执行脚本:
0 1 * * * /home/scripts/mysql/kill_mysqld.sh
保存并退出
crontab
编辑器。
解决方案的测试
1. 手动测试脚本
在设置 cron
任务之前,我们可以手动运行脚本来确保其正常工作:
/home/scripts/mysql/kill_mysqld.sh
检查日志文件 /home/scripts/mysql/logs/kill_mysqld.log
确认脚本的执行情况。你应该看到类似以下内容的记录:
2024-09-15 01:01:00 - Killing mysqld process(es) with PID(s): 1234
如果没有找到 MySQL 进程,你会看到:
2024-09-15 01:01:00 - mysqld process not found
2. 测试 cron
任务
将 cron
任务的时间设置为即将到来的分钟,以便快速测试。例如,将 cron
任务设置为每分钟运行一次,然后检查日志文件是否每分钟更新一次。修改 crontab
文件如下:
* * * * * /home/scripts/mysql/kill_mysqld.sh
在一两分钟内检查日志文件以确认任务是否按预期执行。完成测试后,将 cron
时间恢复到原来的配置(每天凌晨 1 点)。
3. 验证 cron
任务
确保 cron
服务正在运行,并查看 /var/log/syslog
或 /var/log/cron
中的 cron
相关日志,以确认 cron
任务是否按预期执行:
grep CRON /var/log/syslog
或
grep cron /var/log/cron
总结
通过编写和测试上述脚本,我们能够实现每天自动终止 MySQL 进程的需求,并记录操作日志。尽管我们面临了 MySQL 升级和 Docker 容器兼容性的问题,这个临时解决方案能够有效缓解生产环境中的问题,确保系统的稳定性。在未来,计划继续关注 MySQL 和 Docker 的更新,以便在可能的情况下实施更长远的解决方案。
后续完善定时任务脚本,增加获取当前数据库是有事务或者sql执行。如果状态为空就杀死进程重启。
#!/bin/bash
# 数据库连接信息
DB_USER=""
DB_PASSWORD=""
DB_NAME=""
DB_HOST="127.0.0.1"
# 查询语句 测试用sql "SELECT IF(SLEEP(10),COUNT(1),COUNT(1)) FROM information_schema.innodb_trx;"
QUERY="SELECT COUNT(1) FROM information_schema.innodb_trx;"
# 超时时间(秒)60 * 10 = 600 = 10分钟
TIMEOUT=$((60 * 10))
# 查询超时时间(秒)60秒 1分钟
QUERY_TIMEOUT=60
# 每次查询之间的轮询间隔(秒)
POLL_INTERVAL=2
# 日志文件路径
LOG_FILE="/home/scripts/mysql/logs/get_mysql_state_to_kill_mysqld.log"
# 是否将日志输出到控制台(1 表示输出,0 表示不输出)
LOG_TO_CONSOLE=1
# 写入日志文件的函数
log_message() {
local level=$1
local message=$2
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
# 格式化日志内容
local log_entry="$timestamp [$level] $message"
# 输出到控制台(如果需要)
# 输出到终端(通过标准错误输出,防止影响函数返回值)
if [ "$LOG_TO_CONSOLE" -eq 1 ]; then
echo "$log_entry" >&2
fi
# 写入日志文件
echo "$log_entry" >> "$LOG_FILE"
}
print_config() {
log_message "BEGIN" "=============================== 开始执行 ======================================"
log_message "INFO" "CONFIG_PRINT (QUERY:查询语句) : $QUERY"
log_message "INFO" "CONFIG_PRINT (TIMEOUT:脚本运行超时时间_秒) : $TIMEOUT"
log_message "INFO" "CONFIG_PRINT (QUERY_TIMEOUT:查询语句执行超时时间_秒) : $QUERY_TIMEOUT"
log_message "INFO" "CONFIG_PRINT (POLL_INTERVAL:每次查询之间的轮询间隔_秒) : $POLL_INTERVAL"
log_message "INFO" "CONFIG_PRINT (LOG_FILE:日志文件路径) : $LOG_FILE"
log_message "INFO" "CONFIG_PRINT (LOG_TO_CONSOLE:是否将日志输出到控制台(1 表示输出,0 表示不输出)) : $LOG_TO_CONSOLE"
}
# 执行 SQL 查询的函数,使用 timeout 控制执行时间
execute_query() {
local query=$1
log_message "INFO" "Executing query: $query"
# 使用 mysql 命令执行查询,-s 选项表示静默模式(不显示表头),-N 选项表示不显示列名
# mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASSWORD" "$DB_NAME" -e "$query" -s -N
# 使用 timeout 控制 SQL 执行时间,最大执行时间为 QUERY_TIMEOUT 秒 执行查询并返回结果,使用 'tr -d' 删除多余空格和换行符
result=$(timeout $QUERY_TIMEOUT mysql -e "$query" -s -N | tr -d '[:space:]')
if [ -z "$result" ]; then
#"值为空,返回错误信息"
log_message "ERROR" "Query execution timed out."
# 返回 124 代表查询超时
echo "124"
else
#"值不为空,返回查询结果"
echo "$result"
fi
}
# 检查查询结果是否符合预期的函数
check_result() {
local result=$1
if [[ "$result" =~ ^[0-9]+$ ]] && [ "$result" -eq 0 ]; then
return 0 # 符合预期,返回 0
else
return 1 # 不符合预期,返回 1
fi
}
# 主逻辑
start_time=$(date +%s)
print_config
while true; do
# 执行查询并获取结果
result=$(execute_query "$QUERY")
if [ "$result" == "124" ]; then
# 如果查询出现超时,查询的超时时间是1分钟,这肯定意味着数据库已经崩溃了,需要重新启动。
timeout 10 bash /home/scripts/mysql/kill_mysqld.sh
log_message " END " "=============================== 结束执行 ======================================"
exit 1
fi
log_message "INFO" "Query result: $result"
# 检查查询结果是否符合预期
if check_result "$result"; then
log_message "INFO" "Query result is as expected (0). Proceeding with further logic..."
# 执行符合预期的逻辑
timeout 10 bash /home/scripts/mysql/kill_mysqld.sh
log_message " END " "=============================== 结束执行 ======================================"
exit 0
else
log_message "WARNING" "Query result is not as expected. Retrying in $POLL_INTERVAL seconds..."
fi
# 检查是否超时
current_time=$(date +%s)
elapsed_time=$((current_time - start_time))
if [ "$elapsed_time" -ge "$TIMEOUT" ]; then
log_message "ERROR" "Script execution timed out after $TIMEOUT seconds."
log_message " END " "=============================== 结束执行 ======================================"
# 执行超时后的回调逻辑
exit 1
fi
# 等待一段时间后重试
sleep "$POLL_INTERVAL"
done