ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

MySQL 5.7单实例开机自启:systemd服务配置与排错实战

MySQL 5.7单实例开机自启:systemd服务配置与排错实战

周一早上到工位,发现办公室那台测试服务器登录上去很卡,查了一圈才发现是周六凌晨机房有过一次闪断,机器是自动重启了,但跑在上面的 MySQL 5.7 单实例没跟着起来。数据库不启动,一堆定时任务、报表程序全在报错,更要命的是当时没人知道这事,数据积压了大半天。那次之后我给自己定了一条规矩:凡是直接跑在物理机或云服务器上的 MySQL 5.7 单实例,必须配好自启动服务,并且要模拟重启验证过才算完。

这篇文章把我实际配置 MySQL 5.7 单实例开机自启的完整过程写出来,包括 systemd 服务文件怎么写、为什么这么写、验证要验证到什么程度、以及启动失败时怎么一步步排查。适合刚接触 Linux 运维、或者一直靠mysqld_safe手工拉库、被重启折腾过的同学参考。

1. 单实例MySQL为什么非要搞自启动

1.1 一次断电重启教会我的事

MySQL 默认不会自己注册成开机服务。你如果在服务器上手工解压了二进制包,执行mysqld_safe &把库跑起来,那这台机器一旦重启,mysqld 进程就没了,系统不会自动把它拉起来。

有人会说,那我写个/etc/rc.local不就行了?能跑,但你得自己处理启动顺序、用户权限、失败重试、日志记录这些事。更麻烦的是,rc.local 里的命令如果启动时报错,系统不会告诉你,你只能等业务报警才发现数据库没起来。

单实例听起来简单,一台机器一个 mysqld 进程,比多实例少了很多复杂度。但正因为只有一个库,它挂了往往意味着整个环境不可用,反而更应该把自启动这件事做得正规。

1.2 systemd、mysqld_safe和传统init脚本,到底在争什么

先理清几个容易混淆的概念。

mysqld_safe是 MySQL 自带的一个守护脚本。它做的事情是:以正确用户启动 mysqld、设置 umask、把错误日志重定向、在 mysqld 异常退出后尝试重新拉起。它本身不是系统服务管理器,它只负责守护 mysqld 这个进程。

传统的 SysV init 脚本是放在/etc/init.d/下的 shell 脚本,通过service mysql start来调用。很多老教程里都在用这个方案,但它在 7.x 之后的 systemd 系统里已经不算主流了。

systemd 是现在绝大多数 Linux 发行版默认的初始化系统。它用.service文件来描述一个服务,能管理依赖顺序、进程类型、自动重启、资源限制,还统一了systemctl start/stop/status这套命令。

这三者不是替代关系。mysqld_safe可以在 systemd 体系里继续作为启动命令使用,负责守护 mysqld;systemd 负责在开机时拉起 mysqld_safe、在服务崩溃后按策略重启、统一管理日志。分工很清晰。

1.3 单实例自启动的正确适用边界

不是所有 MySQL 都适合用 systemd 管。如果你用的是 Docker 容器,容器本身有 restart 策略,那边归编排系统管,不要在同一环境里同时用 systemd 管理宿主机进程和容器内进程,容易搞乱。

如果是多实例 MySQL,每台机器跑了好几个端口、好几套数据目录,那就需要在/etc/systemd/system/下分别写多个 service 文件,或者用 mysqld_multi。而单实例是最简单的场景,一个 service 文件管一个实例,逻辑最清爽。

这篇文章讲的就是单实例,一个数据目录、一个 mysqld 进程、一套监听端口,用 systemd 统一管理,简单、可靠、易排查。

2. 动手之前的部署姿势:目录、用户和5.7初始化

2.1 二进制包安装与依赖补齐

先把 MySQL 5.7 二进制包准备好。我一般下载通用的 linux-glibc 版本,解压后放到/usr/local/mysql这个路径。给个软链,方便以后升级目录版本时切换。

安装之前先确认依赖:libaio是必须的,少了它 mysqld 启动时会报错。用系统的包管理器装一下即可。另外libnuma在部分版本里也需要,建议一起装上,省得启动时被动态库加载问题折腾。

创建运行用户用系统用户就够了:

useradd -r -s /sbin/nologin mysql

-r表示系统用户,不分配家目录;-s /sbin/nologin禁止登录,这是安全习惯,避免数据库账户变成可登录的系统账户。

2.2 /etc/my.cnf的配置逻辑

5.7 对配置文件的读取顺序比较敏感,常见路径依次是/etc/my.cnf、/etc/mysql/my.cnf、SYSCONFDIR/my.cnf等。为了避免歧义,我习惯把所有配置集中写在/etc/my.cnf,并在服务文件里显式指定。

这里给一份单实例基础配置:

[mysqld] user=mysql basedir=/usr/local/mysql datadir=/data/mysql socket=/data/mysql/mysql.sock pid-file=/data/mysql/mysqld.pid log-error=/data/mysql/logs/error.log slow-query-log=1 slow-query-log-file=/data/mysql/logs/slow.log long_query_time=2 character-set-server=utf8mb4 collation-server=utf8mb4_general_ci max_connections=1000 innodb_buffer_pool_size=1G

几个容易被忽略的细节:

socket文件我放在数据目录下,而不是默认的/tmp/mysql.sock。因为/tmp下文件可能被系统临时清理策略删掉,导致客户端连接时报找不到 socket。放在自己掌控的目录里更稳。

pid-file必须和后面 systemd 服务文件里的PIDFile保持一致,systemd 要靠这个文件判断主进程是否存活,路径不一致会出现服务状态判断错乱。

log-error写到单独的 logs 子目录,方便日志轮转,也不会把错误日志和数据文件混在一起。

2.3 5.7必须手动初始化数据目录

5.7 和 5.6 有个很大的区别:5.6 时代可以用mysql_install_db初始化,或者某些情况下直接跑 mysqld 让它自己建数据目录;5.7 必须手动执行初始化,否则启动时直接报错,说 datadir 不存在或者没有系统表。

初始化命令长这样:

mkdir -p /data/mysql/logs chown -R mysql:mysql /data/mysql /usr/local/mysql/bin/mysqld --initialize-insecure --user=mysql --basedir=/usr/local/mysql --datadir=/data/mysql

这里我用了--initialize-insecure,意思是 root 用户初始密码为空,方便第一次登录后马上设置新密码。如果追求更严格的初始状态,可以用--initialize,它会生成一个临时随机密码,打印在错误日志里,第一次登录必须用那个随机密码。两种都可以,关键是要意识到:5.7 不初始化数据目录是起不来的。

初始化完成后,进/data/mysql看看,应该能看到ibdata1、ib_logfile0、ib_logfile1、mysql/、performance_schema/、sys/这些目录和文件。看到这些,说明数据目录已经准备好了。

2.4 初始化完成后先手动验证一次

写完配置、做完初始化,别急着配 systemd。先用 mysqld_safe 手动启动一次,确认基础环境没问题:

/usr/local/mysql/bin/mysqld_safe --defaults-file=/etc/my.cnf &

然后看错误日志有没有报错,能正常起来的话用mysql -uroot -S /data/mysql/mysql.sock连一下。这一步能帮你把"数据库本身的问题"和"systemd 服务配置的问题"切开,后面排查时会轻松很多。

手动验证没问题后,记得把进程停掉,再进入下一步:

/usr/local/mysql/bin/mysqladmin -uroot -S /data/mysql/mysql.sock shutdown

3. 写一个干净的mysqld.service:参数逐个拆

3.1 完整服务文件示例

在/etc/systemd/system/mysqld.service下新建服务文件,内容如下:

[Unit] Description=MySQL 5.7 Community Server After=network.target [Service] Type=forking User=mysql Group=mysql PIDFile=/data/mysql/mysqld.pid ExecStart=/usr/local/mysql/bin/mysqld_safe --defaults-file=/etc/my.cnf ExecStop=/bin/kill -s TERM $MAINPID TimeoutStartSec=600 TimeoutStopSec=600 LimitNOFILE=65535 PrivateTmp=true [Install] WantedBy=multi-user.target

这段配置是我在生产环境反复调过之后的版本,逐个参数解释清楚,你就知道为什么这么写。

3.2 启动方式选型:mysqld_safe还是原生mysqld

ExecStart里我用了mysqld_safe,而不是直接执行mysqld。原因很简单:mysqld_safe 会额外做几件对生产环境重要的事,比如捕捉 mysqld 异常退出并尝试重新拉起、设置必要的环境变量、把错误日志输出到指定位置。

与之搭配的Type=forking表示这个启动命令会 fork,父进程(mysqld_safe)会在启动后转入后台,实际提供服务的是一个子进程。systemd 通过PIDFile指定的文件来确定真正的 mysqld 主进程 PID。

这里有个经验:不要同时在 systemd 里配Restart=always又用 mysqld_safe 守护。mysqld_safe 本身就会在 mysqld 崩溃后尝试拉起,systemd 的 Restart 机制再插一脚,可能造成进程被双重拉起,日志里出现莫名其妙的启动错乱。我现在的做法是:mysqld_safe 负责守护,systemd 的 Restart 保持默认,靠一套机制兜底就够。

如果你更习惯不用 mysqld_safe,直接用/usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf启动,那服务文件要改成Type=simple,取消PIDFile,再用Restart=on-failure让 systemd 做守护。这条路也通,但对 mysqld_safe 的自动重启机制不熟悉的人,我建议还是按上面的 forking 方案来,更稳。

3.3 关键参数与常见坑位对照

参数作用容易踩的坑
After=network.target等网络服务就绪后再启动如果机器有远程客户端连接,别省;如果只本机访问也可以省,但写上无成本
User=mysql指定进程运行用户忘了写或和数据目录属主不一致,启动秒失败
PIDFilesystemd 判断主进程 PID路径必须和 my.cnf 的 pid-file 一致,否则 status 显示异常
ExecStart启动命令显式--defaults-file最可控,避免读到其他位置的配置
ExecStop停止命令直接kill -s TERM $MAINPID,mysqld 会走正常关闭流程
LimitNOFILE文件描述符上限不显式设置可能被 systemd 默认值限制,连接数大了报 too many open files
PrivateTmp=true隔离 /tmp如果 socket 放在 /tmp 下,这个参数会导致客户端连不上;socket 放数据目录就没问题
WantedBy=multi-user.target开机进入多用户模式时启动忘了写这一行,enable 时会报错

关于ExecStop,很多人习惯写成mysqladmin -uroot shutdown。这要求本机 root 登录不需要密码,或者你愿意把密码暴露在服务文件里,都不太干净。实际验证下来,向 mysqld 主进程发送SIGTERM是 MySQL 支持的安全关闭方式,InnoDB 会完成 flush 和 purge,binlog 也会正常落盘,效果等价于命令行执行 shutdown,所以用kill -s TERM $MAINPID是完全可靠的。

4. 从systemctl到开机自启:验证闭环要跑完整

4.1 启动三步:daemon-reload、enable、start

写完 service 文件后,顺序是这样的:

systemctl daemon-reload systemctl enable mysqld systemctl start mysqld

daemon-reload告诉 systemd 重新读取服务定义文件,这个必须做,否则它还用内存里的旧定义,你改了文件也白改。

enable做的事情是在/etc/systemd/system/multi-user.target.wants/下创建符号链接,表示开机时要启动这个服务。它只负责注册开机项,不会立刻启动进程。

start才是立刻拉起服务。也可以先start再enable,效果一样。我习惯先 enable 再 start,这样即使 start 失败,开机注册这件事也已经完成了,不会出现忘了 enable 的情况。

4.2 用端口/socket/进程三件套验证

启动之后不要只看一眼systemctl status mysqld是 active 就收工。完整的验证应该覆盖三个层面:

systemctl status mysqld ss -lntp | grep 3306 mysql -uroot -S /data/mysql/mysql.sock

第一条看 systemd 视角的服务状态。第二条确认端口在监听,防止出现"进程活着但没监听"的诡异情况。第三条从客户端角度实测能不能连进去,这是最接近业务视角的验证。

systemctl status输出里要留意两个信息:一是Loaded: loaded下面的enabled,确认开机自启被注册了;二是Active: active (running)后面有没有带(running),如果显示activating (start)卡住不动,多半是启动命令没返回,要看日志。

4.3 模拟重启验证不是可选项

很多人配完自启动,systemctl start成功了就觉得完事。但"开机自启"这件事,只有真的重启一次机器才能证明它好使。

在允许重启的测试环境,执行:

systemctl reboot

等机器回来后,依次确认:

systemctl is-enabled mysqld systemctl is-active mysqld

is-enabled返回enabled说明开机注册没丢;is-active返回active说明它确实跟着系统起来了。这一步排掉了很多只在 reboot 场景才会出现的问题,比如网络依赖、时区初始化、其他服务抢占资源等。

我遇到过一种情况:平时手工启动好好的,重启后就是起不来。查了半天发现是另一个服务也在监听 3306,把端口占了,systemd 按依赖顺序启动时两个服务撞车。这类问题不模拟重启根本发现不了。

5. 自启动服务失败的完整排查链路

5.1 先从status和journalctl拿第一手信息

服务起不来的时候,最忌讳直接去翻数据目录、改配置,乱猜一气。正确的第一动作永远是看 systemd 自己记录的启动结果:

systemctl status mysqld -l journalctl -u mysqld --no-pager -n 50

status -l能看到当前状态、主进程 PID、最近的日志片段。journalctl能看到 systemd 捕获的标准输出和错误输出,很多时候 mysqld 启动失败的真正原因就在最初几行。

如果这两条命令输出为空,再去看 MySQL 自己的错误日志/data/mysql/logs/error.log。mysqld_safe 启动时会把 mysqld 的 stderr 重定向到错误日志,所以那条日志里通常有最详细的报错,比如[ERROR]级别的那几行才是根因。

5.2 数据目录权限与路径引发的连环故障

自启动失败里,权限问题占了相当大的比例。

最典型的报错是:

[ERROR] Can't open the mysql.plugin table. Please run mysql_upgrade to create it.

这个报错看着像要跑升级,实际上常见原因是数据目录属主不对,mysqld 进程没有写权限,初始化或启动时无法访问系统表。检查方法:

ls -ld /data/mysql ls -ld /data/mysql/logs

属主必须是mysql:mysql。注意不只是数据目录本身,上级目录也要检查。比如/data如果权限是 755、属主 root,那没问题;但如果/data被改成 700 且属主是其他用户,mysql 用户连路径都进不去。

还有一种隐蔽情况:pid-file指向的/data/mysql/mysqld.pid所在目录没写权限,mysqld 启动时能起来但马上退出,因为 PID 文件写不进去。systemd 里PIDFile指定的路径如果和实际不符,systemd 会认为主进程丢失,反复重启。

my.cnf里的basedir、datadir路径写错也会引发连环故障。用strings或者find先确认路径真实存在,再对齐配置文件,这一步能省很多时间。

5.3 SELinux和防火墙:Linux安全机制对自启动的影响

很多人在自己电脑上配 MySQL 自启没问题,一到公司服务器就各种起不来,差得最多的就是 SELinux。

如果你查看journalctl -u mysqld发现 mysqld 根本没执行到写数据目录那一步,同时dmesg里出现avc denied,大概率就是 SELinux 拦截了。

早期我犯过一个错:图省事直接setenforce 0把 SELinux 关掉,问题确实解决了,但这是治标不治本,生产环境不能为了跑一个数据库把整个系统安全策略降级。

正确的做法是给数据目录打上 MySQL 需要的上下文标签:

semanage fcontext -a -t mysqld_db_t "/data/mysql(/.*)?" restorecon -Rv /data/mysql

如果 mysqld 还需要监听非默认端口,比如 3307,还要放行对应端口。semanage、restorecon 这类工具如果没有,先装 policycoreutils-python-utils 之类的包。

防火墙也要看一眼,尤其是从远程客户端验证连接时。但注意,本机自启动和防火墙没有直接关系,firewalld 默认不会拦本机访问 socket 或回环地址,这个问题通常发生在远程连接验证那一步。

5.4 一张排查表收藏备用

症状可能原因排查命令处理方向
服务一直activatingmysqld_safe 没成功 fork 或 PIDFile 不匹配journalctl -u mysqld -f对齐 PID 文件路径
启动后瞬间退出数据目录权限/初始化不完整ls -ld /data/mysqlchown -R mysql:mysql
报 mysql.plugin table 错误系统表损坏或权限不足看 error.log修复属主或重建数据目录
日志里 avc deniedSELinux 拦截dmesg | grep avc打 mysqld_db_t 标签
端口被占用其他实例残留或端口冲突ss -lntp | grep 3306停掉残留进程或改端口
找不到 /var/lib/mysqlmy.cnf datadir 没被读到`mysqld --verbose --helpgrep datadir`

排查的原则是:从 systemd 视角到 MySQL 视角,一层层往下找。先确认系统认为服务是什么状态,再看进程有没有起来,最后看 MySQL 自己怎么说。跳过任何一层都容易走弯路。

6. 从"能自启"到"稳运行":几个运维细节别省

6.1 文件描述符与连接上限

MySQL 在连接数高的时候,最怕遇到Too many open files。systemd 默认对服务进程的文件描述符限制是 1024,这个值对 MySQL 来说太小了,所以我在服务文件里加了LimitNOFILE=65535。

同时 my.cnf 里可以设open_files_limit=65535。两边配合,才能让 mysqld 真正突破文件描述符瓶颈。

这个坑很隐蔽:你可能会发现手工跑 mysqld_safe 没问题,一切换到 systemd 管理就报文件描述符不足,因为手工 shell 环境继承的 ulimit 往往比 systemd 默认宽松。加了LimitNOFILE之后就一致了。

6.2 日志轮转和不间断运行的配合

服务跑起来之后,错误日志和慢查询日志会一直增长。我的做法是加一个 logrotate 配置,避免日志文件撑爆磁盘。

/data/mysql/logs/*.log { daily rotate 14 missingok notifempty copytruncate compress dateext }

这里用copytruncate是考虑到 mysqld 进程持有日志文件句柄,直接 rename 旧日志再新建会导致 mysqld 还在往旧句柄里写,磁盘空间不会释放。copytruncate先把文件复制走,再截断原文件,mysqld 不用重启就能继续写。

代价是复制与截断之间可能丢极小一段日志,但对于错误日志和慢查询日志来说可接受。如果你对完整性要求很高,可以考虑让轮转脚本执行mysqladmin flush-logs,但那就涉及登录凭据管理,复杂度上去了。

6.3 维护窗口里systemctl的正确用法

自启动服务配置好之后,日常停启都不要再用mysqladmin shutdown或pkill mysqld了,统一走systemctl stop mysqld。这样 systemd 的状态才和真实进程状态保持一致,也能确保走 ExecStop 里的安全关闭流程。

升级 MySQL 小版本时,我的流程是:

systemctl stop mysqld cp -a /data/mysql /data/mysql_bak_$(date +%Y%m%d) # 替换 /usr/local/mysql 目录下的二进制文件 systemctl start mysqld

如果要改 service 文件,改完一定记得systemctl daemon-reload。曾经有人改了 PIDFile 路径忘了 reload,systemd 拿着旧定义去判断新进程,状态一直显示异常,排查了半天才发现是没刷新。

大事务回滚需要时间,所以TimeoutStopSec=600给了足够宽限期,避免 systemd 等得不耐烦直接把进程 kill 掉,导致回滚没走完。

6.4 自启动状态的自愈与巡检

自启动只负责开机把服务拉起来,运行期间 mysqld 如果因为 OOM 或 bug 挂了,mysqld_safe 会尝试拉起。要不要再加一层巡检,取决于你对可用性的要求。

我自己习惯写一个简单的巡检脚本,用 systemd 的接口判断状态:

#!/bin/bash if ! systemctl is-active --quiet mysqld; then systemctl restart mysqld echo "$(date '+%F %T') mysqld restarted" >> /var/log/mysql_autorestart.log fi

注意systemctl is-active只有active才返回 0,其他状态返回非 0,所以用它做判断是可靠的。但别把is-enabled当成运行状态指标,它只返回是否注册开机启动。

巡检脚本可以挂到计划任务里,频率自己定。这个脚本只做兜底,核心还是要靠 service 文件本身的守护机制。

最后分享一个实际经验:配置完自启动服务,一定要找机会做一次真实的机器重启实验,而不是只点一下systemctl restart。很多环境差异、依赖冲突、资源竞争,只会在冷启动那一刻暴露。把重启验证纳入交付清单,MySQL 5.7 单实例的自启动才算是真正闭环了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进