ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

MySQL 5.7单实例部署:从初始化到systemd自启动全流程

MySQL 5.7单实例部署:从初始化到systemd自启动全流程

MySQL 5.7单实例这个需求,看起来就是装个数据库、配个开机自启,但真正动手做的时候,细节点非常多。尤其在生产环境里摸过几轮之后就会发现,从选择安装包、划分数据目录,到用systemd把mysqld变成托管服务,每一步都踩过不少坑。这篇就基于实际运维过程中最常用的一套做法,把mysql5.7单实例从零部署到自启动服务的完整流程拆开讲透,适合刚接手数据库维护的运维同学,也适合需要自己搭建内部业务数据库的后端开发者参考。

1. 先在方案层面把安装方式定下来

很多教程上来就是一条yum命令装完拉倒,但在生产环境里,安装方式、依赖处理、版本锁定这些前期决策直接影响后面所有步骤。MySQL 5.7虽然已经进入生命周期尾声,但存量业务系统用得非常多,单实例部署依旧是最常见的形态。这部分先解决“怎么装”和“装什么”两个问题。

1.1 在线仓库安装还是离线rpm包

在线安装最大的好处是省心,yum会自动拉取依赖,命令也少。但劣势同样明显:如果服务器网络环境受限,或者你配置的镜像源里MySQL版本已经更新到8.0,yum install出来的可能根本不是5.7。对依赖MySQL 5.7特性的老业务来说,装错版本是非常麻烦的事情。

我一般倾向于离线rpm包方式。先从官方渠道下载对应系统版本的mysql-community-server-5.7.44-1.el7.x86_64.rpm这类安装包,上传到服务器后用rpm直接安装。这样做的核心价值是版本可控、可复现——同一套安装包在测试环境验证过,就可以原封不动搬到生产,不存在源漂移的问题。

离线安装要处理依赖关系,常见的依赖有libaio和ncursos-compat-libs。libaio是InnoDB的异步IO依赖,缺少它mysqld根本起不来;ncursos-compat-libs主要是为了兼容一些老版本客户端工具的终端渲染库,没有装在某些最小化系统上也会报错。

yum install -y libaio numactl-libs rpm -ivh mysql-community-server-5.7.44-1.el7.x86_64.rpm

如果安装过程中提示缺其他依赖,用yum install把对应的库补上就行。这一点比纯rpm -ivh硬装要省事,既能控制MySQL本体版本,又让依赖交给yum去解决。

1.2 安装前的环境检查

安装前花几分钟检查服务器环境,可以避免后面一堆莫名其妙的问题。

内存方面,MySQL 5.7单实例最低最好有2G物理内存。我见过不少1G内存的机器硬跑5.7,innodb_buffer_pool_size只敢配128M,跑起来捉襟见肘,查询稍微复杂一点就频繁刷盘。如果你的业务有基本的写入压力,内存建议按4G起步来规划。

磁盘方面,数据目录所在的磁盘IO决定了数据库的整体性能天花板。机械盘和SSD跑MySQL的差距是量级的,尤其是redo log刷盘和binlog写入这两个高频操作。条件允许的话,数据盘用SSD,操作系统盘单独一块,避免日志和系统日志抢IO。

透明大页也值得提一句。Linux默认的THP(Transparent Huge Pages)在某些场景下会导致InnoDB出现性能抖动,尤其是内存碎片化严重时。很多公司会在数据库服务器上关闭THP,方法是在/etc/rc.local里追加echo never > /sys/kernel/mm/transparent_hugepage/enabled。如果你刚接触这个操作,先确认系统是否真的开启了THP再动手,有些云主机默认是关闭状态。

安装完成后先验证一下版本,确保装的是5.7而不是别的版本:

mysqld --version mysql --version

mysqld和mysql两个命令分别对应服务端和客户端,版本不一致时客户端可能无法连接服务端,虽然5.7客户端连5.7服务端基本没事,但保险起见两个都看一眼。

1.3 安装完成后的初始文件布局

rpm包装完后,/etc/my.cnf会自动生成,/usr/lib/systemd/system/mysqld.service这个systemd单元文件也被装好了,mysqld命令会出现在/usr/sbin/下。这些细节很重要,意味着官方rpm包本身已经自带开机自启的能力。

不过官方自带的unit文件和默认配置是为通用场景准备的,datadir默认在/var/lib/mysql,socket默认在/var/lib/mysql/mysql.sock。生产环境里我基本都会改成独立目录,改完之后官方unit文件里很多路径就失效了,后面讲systemd配置的时候再细说。这里先提一句是因为很多人忽略这个差异,直接用官方unit文件去配自定义路径,结果服务怎么都起不来。

2. 数据目录、配置文件和服务账号的准备

MySQL跑起来之后,最核心的东西就是数据目录。数据目录规划得好不好,直接影响故障排查的难度和扩容的便利性。

2.1 为什么单独划分数据目录

默认的/var/lib/mysql在系统盘上,系统盘通常还要承载操作系统、日志、临时文件。数据库一旦遇到慢查询日志爆炸、binlog积压、备份文件占满磁盘,很容易把系统盘写满,最终连ssh都登不上,这是非常被动的事。

所以我习惯把数据目录放到独立的数据盘或者至少独立的挂载点上,比如/data/mysql。这样即使数据库把磁盘写满,影响范围被圈定在数据盘内,系统盘还能正常工作,给运维留了足够的抢救时间。

数据目录内部再按职责细分:

  • /data/mysql/data:InnoDB表空间、ibdata、undo等核心数据
  • /data/mysql/binlog:二进制日志单独放,方便按目录粒度做磁盘空间管理
  • /data/mysql/logs:错误日志、慢查询日志
  • /data/mysql/tmp:临时表目录

这样划分之后,清理日志、备份binlog、扩容数据盘这些操作都变得非常清晰,不需要在一大堆混放的文件里找东西。

2.2 my.cnf核心参数逐个说

一份能用的my.cnf模板大概是这样的:

[mysqld] user=mysql basedir=/usr/local/mysql datadir=/data/mysql/data socket=/data/mysql/mysql.sock pid-file=/data/mysql/mysqld.pid log-error=/data/mysql/logs/mysql-error.log character-set-server=utf8mb4 collation-server=utf8mb4_general_ci default-storage-engine=InnoDB server-id=1 log-bin=/data/mysql/binlog/mysql-bin binlog_format=ROW expire_logs_days=7 sync_binlog=1 innodb_buffer_pool_size=2G innodb_log_file_size=256M innodb_flush_log_at_trx_commit=1 max_connections=500

这些参数看着简单,但每个都值得理解背后的逻辑,否则出了问题都不知道该往哪个方向查。

innodb_buffer_pool_size是InnoDB最重要的内存参数,它决定InnoDB在内存里能缓存多少数据和索引。经验值一般是物理内存的50%到70%,配置的时候要留出足够内存给操作系统页缓存、连接线程、排序缓冲区等。比如4G内存的机器,buffer pool配2G比较合理,配到3G就可能出现内存紧张导致的swap抖动。这个参数可以在线改,但改完要重启才能完全生效,所以部署时一次配到位最好。

innodb_log_file_size控制InnoDB redo日志文件的大小,默认还是48M(5.7早期版本),对稍微有点写入量的业务来说太小了,会产生频繁的checkpoint刷盘,性能损耗明显。256M到512M是比较常见的配置区间。redo日志文件不能在线改大小,只能通过先关闭实例、删除旧日志文件、改配置、重启的方式调整,所以首次部署时把这个值定好非常重要。

innodb_flush_log_at_trx_commit=1和sync_binlog=1就是常说的“双1”模式。每个事务提交时都要把redo log刷到磁盘,binlog也同步刷盘,这种模式对数据安全最友好,但写入性能会打折扣。如果是内部工具类系统、允许丢失最后一两秒数据的业务,可以降级成0或2来换性能,但生产核心业务强烈建议保持双1。

character-set-server=utf8mb4是为了彻底绕开中文乱码问题。utf8mb4是真正的四字节UTF-8,能存emoji和生僻字,而传统的utf8是utf8mb3的别名,遇到特殊字符会报错。把服务端字符集固定成utf8mb4之后,就算客户端连接串没设置字符集,默认也不会乱码。

expire_logs_days=7在5.7里控制binlog保留7天。需要注意在生产上,binlog的保留时间还要结合全量备份周期考虑,至少要能覆盖一次完整备份的时间点,否则增量恢复会出现空洞。

2.3 服务账号与目录权限

MySQL进程在Linux上应当使用专用账号运行,不能用root。原因是mysqld一旦被漏洞利用或者配置失误,如果它以root身份运行,攻击者拿到的是整个系统的权限,这风险没有任何理由去冒。

创建专用账号的做法:

useradd -r -s /sbin/nologin mysql mkdir -p /data/mysql/data /data/mysql/binlog /data/mysql/logs chown -R mysql:mysql /data/mysql

-r表示创建系统账号,-s /sbin/nologin让这个账号无法登录shell,这是最小权限原则的一部分。

这里有个细节:创建完账号后目录权限必须是mysql:mysql,否则后面mysqld初始化时会报Could not open required defaults file或者Permission denied之类的错误。目录属主错误是数据库部署新手最容易踩的第一大坑。

3. 初始化实例:这一步决定后面是否顺利

配置文件和目录都准备好了,接下来就可以初始化数据目录。这一步如果操作不对,后面启动起来全是问题。

3.1 初始化命令与临时密码

MySQL 5.7和5.6的一个重大区别是,5.7初始化不再用mysql_install_db脚本,而是用mysqld --initialize命令。执行初始化时,mysqld会根据my.cnf里的配置创建整个数据目录结构、系统表空间、系统库,并生成一个root临时密码。

初始化命令:

mysqld --defaults-file=/etc/my.cnf --initialize --user=mysql

这里--defaults-file明确指定配置文件,避免读到系统其他地方的默认配置。--user=mysql表示以mysql用户的身份来初始化数据目录,确保后面mysqld进程能用同一个用户读写这些文件。

初始化完成后,root临时密码会写到错误日志里:

grep 'temporary password' /data/mysql/logs/mysql-error.log

你会看到类似A temporary password is generated for root@localhost: xxxxxxxx的输出,这一串就是初始密码。

有一种情况可以跳过复杂临时密码的步骤,用mysqld --initialize-insecure代替--initialize,它生成的root账号密码为空,登录时直接回车就好。但这样做会让root账号处于无密码状态,如果数据目录的机器在不可信网络里,风险很大。我的习惯是老老实实用--initialize,拿到临时密码后立刻改掉,并不麻烦。

3.2 启动验证与基本状态检查

初始化成功之后,用systemd启动服务:

systemctl start mysqld systemctl status mysqld

看到Active: active (running)才说明服务起来了。再用ss -lntp | grep 3306确认端口在监听。

然后用临时密码登录MySQL,立刻修改root密码:

mysql -uroot -p ALTER USER 'root'@'localhost' IDENTIFIED BY 'YourStrongPassword123!';

5.7的默认密码校验插件要求密码至少包含大小写字母、数字、特殊字符,长度至少8位。如果密码强度不达标,ALTER USER会直接报错,告诉你ERROR 1819。如果你所在的内部网络环境觉得这个策略太严格,可以降低校验等级:

SET GLOBAL validate_password_policy=LOW;

但不建议为了省事把密码搞得太简单,数据库是最后一道防线,密码复杂度是最基本的自我保护。

3.3 初始安全设置

如果MySQL是全新安装,库里会存在匿名用户和test库,这些都是安全风险。可以用mysql_secure_installation脚本一键处理,也可以手动执行:

DELETE FROM mysql.user WHERE User=''; DROP DATABASE IF EXISTS test; FLUSH PRIVILEGES;

接着创建业务账号和业务库:

CREATE DATABASE app_db DEFAULT CHARACTER SET utf8mb4; GRANT ALL ON app_db.* TO 'app_user'@'192.168.10.%' IDENTIFIED BY 'AppUserPass123!'; FLUSH PRIVILEGES;

这里要养成一个习惯:授权时尽量限定来源网段,不要用'app_user'@'%'一把梭。%意味着这个账号可以从任何IP登录,密码一旦泄露就是全网裸奔。按网段或者具体IP授权,配合防火墙规则,可以把风险控制到最小范围。

4. 用systemd把实例变成自启动服务

安装MySQL、初始化、启动,这些只是第一步。真正让数据库在服务器重启后自动恢复运行,需要在systemd层面把这个服务管起来。这一步做得好,运维可以省掉大量手工介入。

4.1 为什么不用mysqld_safe来守进程

5.7官方rpm包里的旧版服务脚本还有mysqld_safe的痕迹,但如果你自己写unit文件,我强烈建议直接引导mysqld而不是mysqld_safe。

mysqld_safe是MySQL自带的一个守护进程包装器,它的作用是拉起mysqld、监控进程状态、崩溃后自动重启。这套机制在SysVinit时代是标准方案,但现在systemd已经提供了进程监控、崩溃重启的能力。再用mysqld_safe就变成了两层守护:

mysqld_safe守mysqld,systemd守mysqld_safe,职责重复,而且一旦出问题,你要同时排查两个守护层,非常绕。

我采取的方案是:unit文件里直接执行mysqld --daemonize,让mysqld自己fork出主进程,systemd用Type=forking来识别服务启动完成。这个方案简洁、透明,systemd的Restart参数能完全接管崩溃重启的工作。

4.2 编写unit文件

如果你使用的是官方rpm包的unit文件,且数据目录没改过,那直接enable就行。但如果你像我一样把数据目录放在自定义位置,建议自己写一份明确指定路径的unit文件。放在/etc/systemd/system/mysqld.service,优先级会覆盖/usr/lib/systemd/system/下的同名文件。

一份经过实测可用的unit文件示例:

[Unit] Description=MySQL 5.7 Database Server Documentation=man:mysqld(8) After=network.target Wants=network-online.target [Service] Type=forking User=mysql Group=mysql PIDFile=/data/mysql/mysqld.pid ExecStart=/usr/sbin/mysqld --defaults-file=/etc/my.cnf --daemonize --pid-file=/data/mysql/mysqld.pid ExecReload=/bin/kill -HUP $MAINPID LimitNOFILE=65535 Restart=on-failure RestartSec=5s TimeoutSec=300 StandardError=journal [Install] WantedBy=multi-user.target

逐个解释关键参数为什么这么写:

After=network.target的意思是,在系统进入network.target之后再启动mysqld。MySQL需要网络就绪才能监听3306端口,虽然现代系统网络初始化很快,但明确声明依赖顺序可以避免偶发的端口绑定失败。Wants=network-online.target则声明了另一个软依赖,如果系统里这个target存在,会尝试等待网络完全就绪。两者配合,基本杜绝了开机时网络未准备好导致MySQL启动失败的情况。

Type=forking需要重点理解。之前提过ExecStart里用了--daemonize,mysqld会fork出一个后台运行的子进程,然后父进程退出。systemd要等父进程退出后,去查看PIDFile指定的pid文件,确认主进程已经接管服务。如果pid文件路径和mysqld实际写入的路径不一致,systemd会一直等待直到TimeoutSec超时,然后报failed。所以unit文件里PIDFile和ExecStart的--pid-file必须保持一致。

User=mysql和Group=mysql让整个服务以mysql身份运行,即使my.cnf里也写了user=mysql,unit层面再锁一次是双保险。如果unit文件不写,而my.cnf里写了user=mysql,mysqld在读取配置后会setuid切换用户,但systemd管理的主进程监控关系会有点微妙,所以统一在unit里声明最省心。

LimitNOFILE=65535是很多MySQL部署容易漏掉的一项。mysqld默认的文件描述符上限继承自systemd启动环境的1024或65535,但每张InnoDB表都会占用一个文件描述符,连接数一上来、分区表一多,文件描述符很容易耗尽。设成65535之后基本够用,如果业务表数量特别大(比如上万张表),可以再往上调。

Restart=on-failure让mysqld在异常退出时自动拉起,RestartSec=5s避免无限快速重启导致CPU打满。TimeoutSec=300给大库恢复预留足够时间,因为InnoDB在崩溃恢复或redo log回放阶段可能比较久,默认的90秒在某些场景下不够用。

4.3 enable、start、验证自启动

unit文件写好后,执行:

systemctl daemon-reload systemctl enable mysqld systemctl start mysqld systemctl status mysqld systemctl is-enabled mysqld

执行顺序有讲究。daemon-reload是必须的,systemd只有在收到这个指令后才会重新读取unit文件,否则你改了文件它还在用旧配置。enable创建开机启动的软链接,start才是真正启动服务,两个操作不同,很多人只记得start,结果重启后又得手动起服务。

systemctl is-enabled mysqld如果输出enabled,说明开机自启的配置已经生效。最好的验证方式是直接重启服务器,看MySQL是否自动恢复,这个测试虽然费一点时间,但比任何静态检查都靠谱。

4.4 开机自启的原理

顺便把原理说清楚,这样以后遇到异常也好排查。systemctl enable mysqld做的事情,本质上是在/etc/systemd/system/multi-user.target.wants/目录下创建一个指向unit文件的软链接。系统启动时,systemd会读取multi-user.target的Wants依赖,发现这个软链接,就会去启动mysqld服务。

你可以直接查看这个目录:

ls -l /etc/systemd/system/multi-user.target.wants/ | grep mysql

会看到类似mysqld.service -> /etc/systemd/system/mysqld.service的输出。

理解这一点后,如果某天遇到服务没自启,第一反应不应该是瞎猜,而是检查这个软链接是否存在。软链接丢了,多半是有人执行过systemctl disable mysqld;存在但没启动,那就要查启动日志了。

5. 常见问题与排错实录

部署和使用过程中,有几个问题出现的频率极高,这里整理成一份排查速查表,都是实际踩过的坑。

5.1 服务启动后立即退出

如果你执行systemctl start mysqld后看到状态是failed,或者进程起来了没几秒又没了,最常见的原因有四个:

  • 数据目录权限不对,mysqld进程没有写权限
  • my.cnf里datadir路径写错,或者目录根本不存在
  • error log文件路径对应的目录不存在
  • InnoDB初始化阶段崩溃,比如磁盘空间不足

首选排查手段是看日志:

journalctl -u mysqld -n 50 --no-pager tail -100 /data/mysql/logs/mysql-error.log

journalctl -u mysqld是systemd视角的日志,能看到unit的执行记录;error log是mysqld自己写的日志,内容更详细。两个配合,基本能快速定位问题。

我遇到过最典型的一个案例是:my.cnf里把datadir写成了/data/mysql/data,但建目录的时候只建了/data/mysql,没有建data子目录。mysqld初始化时直接报Can't create directory,服务反复启动失败。解决办法很简单,补上目录:

mkdir -p /data/mysql/data chown mysql:mysql /data/mysql/data

这类问题很基础,但很容易在自动化脚本或批量部署时出现,因为人手工操作时总会记得,脚本里反而容易漏。

5.2 socket连接失败的排查

mysql -uroot -p报错Can't connect to local MySQL server through socket '/tmp/mysql.sock',这是最常见的坑。原因是客户端默认找/tmp/mysql.sock,而my.cnf里把socket配置到了/data/mysql/mysql.sock。

解决方式有两种。第一种是显式指定socket连接:

mysql -uroot -p -S /data/mysql/mysql.sock

第二种是在my.cnf的[client]段里也配置相同的socket路径:

[client] socket=/data/mysql/mysql.sock

这样mysql命令会自动读取默认配置,不用每次手动加参数。

顺便建议把socket放在数据目录下,而不是默认的/tmp。/tmp目录经常会被清理工具扫描,权限控制也比较宽松,socket文件一旦被删,连接就走TCP,绕过socket会带来额外的网络层排查负担。放数据目录里,权限和生命周期都能和数据库绑定在一起。

5.3 忘记root密码的应急处理

这个场景谁都可能遇到:root密码忘了,或者某次改密码后没记住。应急方法很成熟:

先停掉服务:

systemctl stop mysqld

然后跳过授权表方式启动:

mysqld_safe --skip-grant-tables --skip-networking &

或者直接用:

mysqld --defaults-file=/etc/my.cnf --skip-grant-tables --skip-networking --user=mysql &

这里必须把--skip-networking加上,作用是让mysqld不监听TCP端口。如果漏掉这个参数,实例会以“任何人免密登录”的状态暴露在网络上,这在多租户网络环境里等于裸奔。同类问题在另一套环境里,有个运维图省事只用了--skip-grant-tables,结果一台网内机器用空密码直接登进去了,教训相当惨痛。

启动后连接并修改密码:

mysql -uroot UPDATE mysql.user SET authentication_string=PASSWORD('NewStrongPassword123!') WHERE User='root' AND Host='localhost'; FLUSH PRIVILEGES;

注意MySQL 5.7里mysql.user表里的密码字段是authentication_string,不是老版本的password字段。改完密码后杀掉跳过的实例进程,再正常启动服务。

5.4 systemd环境变量和命令行工具问题

用systemd管理MySQL后,如果手动在shell里执行mysql提示命令找不到,原因是/usr/local/mysql/bin不在用户PATH里。这不是systemd的问题,是shell环境的PATH配置问题。

两种解决方式:

ln -s /usr/local/mysql/bin/mysql /usr/local/bin/mysql ln -s /usr/local/mysql/bin/mysqldump /usr/local/bin/mysqldump

或者编辑/etc/profile,把MySQL的bin目录追加到PATH。前者更直接,后者更通用,推荐第一种。

另一个体验上的问题是,如果你习惯用mysqladmin ping来检查实例是否存活,但机器上没这个命令,也可以换成:

mysqladmin -uroot -p ping

或者用systemctl status mysqld和ss -lntp | grep 3306代替,能看出实例是否真正在监听。

5.5 权限与SELinux的特殊情况

CentOS和RedHat系服务器默认开启SELinux,它对mysqld能访问哪些路径有严格限制。如果你把数据目录从/var/lib/mysql换到了/data/mysql,SELinux会拒绝mysqld访问这个新目录,无论目录权限多正确都没用。

启动失败后,/var/log/audit/audit.log或者journalctl里能看到SELinux is preventing mysqld from read access on the directory之类的报错。处理方式是为自定义目录配置正确的SELinux上下文:

yum install -y policycoreutils-python semanage fcontext -a -t mysqld_db_t "/data/mysql(/.*)?" restorecon -Rv /data/mysql

如果实在不愿折腾SELinux,可以临时用setenforce 0验证是不是SELinux导致的,但不建议长期关闭。生产环境还是建议花几分钟把context配好,既保留SELinux防护,又能用自定义路径。

6. 长期维护单实例的几个习惯

部署完成只是开始,数据库长期稳定运行靠的是日常维护习惯。这里分享我个人比较看重的几个点。

备份是绝对不能省的一环。单实例架构下没有主从节点可以兜底,数据丢了就是真丢了。我的习惯是全量备份加binlog增量备份配合。全量备份用mysqldump或者物理备份工具都可以,频率根据数据量来定——数据量小的一天一次,大的话一周一次全量加每天binlog备份。binlog的保留时间要覆盖到下一次全量备份,否则增量恢复会有断点。

慢查询日志非常值得开。5.7里通过slow_query_log=ON和long_query_time=1可以记录执行时间超过1秒的SQL。很多性能问题在出现明显故障之前,都会在慢查询日志里暴露苗头。比如某条SQL越来越慢,连接数随之堆积,最终把实例拖垮。定期翻慢查询日志,很多隐患能在变成事故之前被处理掉。

磁盘空间监控对数据库来说比CPU内存更重要。数据文件、binlog、redo log、慢日志、错误日志,每一个都可能迅速膨胀。我的习惯是磁盘使用率超过80%就告警,超过90%就必须人为介入。数据库磁盘写满带来的往往是实例直接hang住,而不是优雅拒绝写入,那个恢复过程非常痛苦。

配置管理上,我建议把my.cnf当成一份可以重复使用的模板来维护。新环境从模板复制出来,改掉datadir路径和buffer pool大小就能用。这样既能保证每个实例的配置风格一致,排查问题时也容易对比。

升级是迟早要面对的问题。MySQL 5.7升级8.0不是简单的替换二进制文件,8.0对参数默认值、认证插件、字符集行为都有很大变化。升级前先用官方工具对实例做兼容性检查,确认没有不兼容的对象再动手。单实例升级没有主从可以做滚动切换,只能停服维护,窗口要留足,回滚方案也要提前准备好。

回头看看,从安装包选择到systemd自启配置,每一步都不复杂,但环环相扣。很多人卡在某个环节,往往不是操作难度大,而是对一个参数一个路径背后的原因没想清楚。把这些细节捋顺了,MySQL 5.7单实例的自启动服务就能变成一套可以放心交付的运维资产。

RELATED READING

延伸阅读

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