Part 1

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


1. 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):


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

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

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

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

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

Part 3

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

  • 如何确定某个pid属于一个running的进程,scripts/CMakeLists.txt下面有CHECK_PID的定义。通过kill -0返回值确定。如果系统不支持signal 0,就发SIGCONT,本身这俩类型的signal发给一个运行的进程是没副作用的。

1. EXECUTE_PROCESS(COMMAND sh -c "kill -0 $$"
2. OUTPUT_QUIET ERROR_QUIET RESULT_VARIABLE result)
3. IF(result MATCHES 0)
4. SET(CHECK_PID "kill -0 $PID > /dev/null 2> /dev/null")
5. ELSE()
6. SET(CHECK_PID "kill -s SIGCONT $PID  > /dev/null 2> /dev/null")
7. ENDIF()

  • socket file的地址是这么算的。其中:-符号是bash中变量默认值的用法,${a:-b}的意思就是如果a存在且不为空,返回a,否则返回b。

1. safe_mysql_unix_port=${mysql_unix_port:-${MYSQL_UNIX_PORT:-@MYSQL_UNIX_ADDR@}}

  • “$numofproces = 统计当前使用了$pid_file路径的mysqld进程数”,这个是判断当前是否存在mysqld_safe负责的mysqld还hang在那里。ps中的ww是为了打印完成的命令,否则默认会限制window size;排除grep自己;过滤mysqld和pid这俩路径;grep中的>是匹配一个空格;grep -c可以返回行数。

1. ps xaww | grep -v "grep" | grep "$ledir/$MYSQLD\>" | grep -c "pid-file=$pid_file"

  • 统计完$numofproces之后,要kill hanging的mysqld,用了如下几行脚本找pid。PROC前半段类似上一条,最后sed -n ‘$p’是取出最后一行。随后的for循环,相当于把PROC当做空格分割的数组,取出第一列,也就是pid。其实这么搞复杂了,还不如直接用awk…

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

3. for T in $PROC
4. do
5. break
6. done

  • 代码逻辑中有几处处理$pid_file.shutdown文件的。据考证是以前mysql.init里面用的历史遗留代码,现在已经废弃了。很早之前官方就都替换成给DB发SIGTERM信号,走normal shutdown了。所以$pid_file.shutdown这些目前其实是废代码。
  • 脚本中涉及到删除文件的地方,比如删除$pid_file、socket file,都加了非symbolic links的限制(! -h "$pid_file")。这个是为了规避symbolic links可能产生的privilege escalation的风险。以前有很多符号链接漏洞攻击的案例,感兴趣细节的可以Google。

1. if [ ! -h "$pid_file" ]; then
2. rm -f "$pid_file"
3. if test -f "$pid_file"; then
4. 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"
5. exit 1
6. fi
7. fi

  • “判断$fast_restart变量,做一些限速”,实际逻辑很简单,统计每次“启动mysqld”这一步的开始时间和结束时间,连续多次(代码里是5次)间隔时间小于1s,就sleep 1s后再跳到下一轮循环。这个优化是为了防止mysqld一直拉不起来,比如有非法参数,导致mysqld_safe一直疯狂尝试重启,占用100%的cpu。
  • 为啥会有“返回值 == 16”条件下dont_restart_mysqld=false的逻辑?因为是支持通过SQL请求发restart命令来重启mysqld的,代码里有#define MYSQLD_RESTART_EXIT 16。只要有父进程管理mysqld(参考sql_restart_server.cc的代码is_mysqld_managed),restart命令会走SIGUSR2的信号处理函数,返回的值是MYSQLD_RESTART_EXIT。
  • mysqld_safe还支持设malloc-lib,在脚本里,转换成LD_PRELOAD设置在mysqld启动的命令中。常见的用法是改成jemalloc,在my.cnf里加上如下配置:

1. [mysqld_safe]
2. malloc-lib=/path/libjemalloc.so

Part 4

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


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

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

大概就总结这些

原文:http://mysql.taobao.org/monthly/2022/04/03/