数据库内核月报

数据库内核月报 - 2022 / 04

MySQL · 源码阅读 · mysqld_safe的代码考古

Author: zhenping

Part 1

mysqld_safe是一个跟随mysql安装包一起发布的bash脚本,源码目录在scripts/mysqld_safe.sh。核心功能就是启动mysqld,在mysqld进程故障(比如crash)之后,自动探测并重启实例。参考官方文档的说明,mysqld_safe是在Linux部署mysql数据库的推荐方法,执行命令大致如下:

mysqld_safe --defaults-file=file_name <options> <mysqld_options>

运行完之后,在bash上执行ps,能看到有一个mysqld_safe进程和一个mysqld进程,mysqld_safe会自动为mysqld准备一系列的参数,包括my.cnf的地址、basedir、错误日志、端口等。这些都可以从ps的命令行输出中查看到。

Part 2

mysqld_safe目前有1000多行,不过核心逻辑就200行,都是围绕mysqld进程和$pid_file展开。$pid_file存放在my.cnf中配置的pid-file路径上,是一个普通的文本文件,里面存放了创建者的pid,也就是对应的mysqld进程。mysqld_safe依赖pid精确判断是否需要重启。
围绕涉及到的各个文件操作,简化版的mysqld_safe逻辑可以描述如下(参考8.0.28):

准备一系列参数和路径,包括最后要拼接在mysqld命令后面的defaults-file、basedir、pid-file、socket等参数
if ($pid_file文件存在) {
  if (pid对应的进程存在 && 该进程名字是mysqld) {
    "A mysqld process already exists"
    报错退出
  }
  删除$pid_file // 说明是老的mysqld生成的
  if ($pid_file文件存在) {
    "Fatal error: Can't remove the pid file: $pid_file. Please remove the file manually and start $0 again; mysqld daemon not started" // 文件删失败了
    报错退出
  }
  同上,尝试删除socket文件 // my.cnf中socket配置的路径
  同上,尝试删除$pid_file.shutdown文件
}
"Starting $MYSQLD daemon with databases from $DATADIR"
while true { // 核心逻辑的主循环
  启动mysqld // 正常启动成功后,mysqld_safe就会等在这里
  if (返回值 == 16) {
    dont_restart_mysqld=false
    "Restarting mysqld..."
  } else {
    dont_restart_mysqld=true
  }

  if (dont_restart_mysqld) {
    if ($pid_file文件不存在) {
      // 说明是normal shutdown,pid文件会在mysqld退出时自动被删掉
      break; // 跳出while循环
    } else {$pid_file读取pid
      if (pid进程存在) {
        "A mysqld process with pid=$PID is already running. Aborting!!"
        报错退出
      }
    }
  }

  if (存在$pid_file.shutdown文件) {
    "$pid_file.shutdown present. The server will not restart."
    break;
  }

  判断$fast_restart变量,做一些限速 // 细节暂时省略

  if (启动mysqld_safe时没配置--skip-kill-mysqld选项) { // 正常都是不配的
    $numofproces = 统计当前使用了$pid_file路径的mysqld进程数
    "Number of processes running now: $numofproces"
    while (循环$numofproces) {
      获取其中一个mysqld进程的pid
      if (kill -9 该进程) { // 发SIGKILL
        "$MYSQLD process hanging, pid $T - killed"
			} else {
        break;
      }
    }
  }
  删除$pid_file、socket文件、$pid_file.shutdown
  "mysqld restarted"
}
删除$pid_file.shutdown文件
"mysqld from pid file $pid_file ended"
删除$safe_pid文件 // 似乎是毫无意义的一段代码

Part 3

整个代码的理解,主要涉及了一些commit历史“考古”的工作和bash脚本的写法。

EXECUTE_PROCESS(COMMAND sh -c "kill -0 $$"
  OUTPUT_QUIET ERROR_QUIET RESULT_VARIABLE result)
IF(result MATCHES 0)
  SET(CHECK_PID "kill -0 $PID > /dev/null 2> /dev/null")
ELSE()
  SET(CHECK_PID "kill -s SIGCONT $PID  > /dev/null 2> /dev/null")
ENDIF()
safe_mysql_unix_port=${mysql_unix_port:-${MYSQL_UNIX_PORT:-@MYSQL_UNIX_ADDR@}}
ps xaww | grep -v "grep" | grep "$ledir/$MYSQLD\>" | grep -c "pid-file=$pid_file"
PROC=`ps xaww | grep "$ledir/$MYSQLD\>" | grep -v "grep" | grep "pid-file=$pid_file" | sed -n '$p'`

for T in $PROC
do
  break
done
if [ ! -h "$pid_file" ]; then
  rm -f "$pid_file"
  if test -f "$pid_file"; then
    log_error "Fatal error: Can't remove the pid file: $pid_file. Please remove the file manually and start $0 again; mysqld daemon not started"
    exit 1
  fi
fi
[mysqld_safe]
malloc-lib=/path/libjemalloc.so

Part 4

之前线上还出现过一个bug,当一台机器的某个mysqld故障之后,会出现同宿主机的其他mysqld被自动重启一遍,非常诡异。最后排查下来就和mysqld_safe有关。云环境mysqld都是混布的,通过K8S这样的技术去做隔离。最开始的时候我们容器技术做的不完善,各个mysqld的文件系统是隔离的,但是进程权限没隔离。导致的情况就是从每个容器里面看,能看到所有的mysqld进程,且–pid-file上都是配置的同一目录。

PROC=`ps xaww | grep "$ledir/$MYSQLD\>" | grep -v "grep" | grep "pid-file=$pid_file" | sed -n '$p'`

结果mysqld_safe通过如上命令找进程的时候,就把别的不属于自己管理的mysqld kill了…之后修复方法就是把进程权限的隔离做上去,这样从一个容器就看不到其他容器里跑的mysqld了。

大概就总结这些