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 错误。这通常指示程序试图释放已经释放的内存块,或者内存出现了破坏。这种错误可能由以下原因引起:

  1. MySQL 自身的 bug:可能是 MySQL 的代码中存在缺陷。
  2. 硬件故障:不良的内存模块或其他硬件问题。
  3. 软件冲突:其他软件可能干扰了 MySQL 的正常运行。

处理措施

1. 尝试提交 Bug 报告

我们尝试向 MySQL 官方报告了此问题,并寻求帮助。此步骤的目的是确认是否存在已知的 bug 并寻求修复方案。

2. 升级 MySQL 版本

升级 MySQL 版本可能解决此问题。然而,由于项目处于上线阶段,不适合进行大规模的版本升级,因此我们考虑了其他解决方案。

3. 使用 Docker 容器进行升级

我们尝试使用 Docker 容器来升级 MySQL,以便进行版本测试。我们使用了 mysql:lts 镜像,但遇到了以下问题:

  • 官方文档声称支持 x86-64x86-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

实施步骤

  1. 创建脚本:将上述脚本保存到 /home/scripts/mysql/kill_mysqld.sh 文件中,并确保脚本具有执行权限:

    chmod +x /home/scripts/mysql/kill_mysqld.sh
    
  2. 设置 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