CentOS 9 Stream 安装 MySQL 8.0 实战:避开仓库与初始化陷阱
2026/9/16 8:01:49 网站建设 项目流程

1. 先说清楚:CentOS 9 Stream 上装 MySQL 8.0,为什么和以前不太一样

如果你刚从 CentOS 7 迁移到 CentOS 9 Stream,第一次执行yum install mysql-server的时候多半会愣一下:仓库里搜出来的包名不对,版本也怪怪的。CentOS 7 时代大家都习惯了从 Oracle 官方下载 rpm 包,或者用第三方源一把梭。到了 9 Stream,dnf 取代了 yum 的交互习惯,AppStream 仓库里还预置了一个 mysql 模块,这就把很多人绕晕了。

这篇文章我把整个流程重新捋一遍,从仓库选择、模块冲突处理、数据目录初始化,到安全加固和远程连接,全部基于我实际装过的步骤来写。适合刚上手 CentOS 9 Stream 的运维、后端开发,以及那些被测试环境折腾到想摔键盘的人。

先给结论:在 CentOS 9 Stream 上装 MySQL 8.0,最稳妥的路线不是用系统自带的模块,而是用 Oracle 官方提供的 MySQL 社区版仓库。原因后面展开说,但你先记住这条路是通的,而且踩坑最少。

有个细节要先明白:CentOS 9 Stream 默认的 AppStream 仓库确实带 MySQL 相关模块,但它的版本通常跟不上官方最新补丁,而且模块流的切换逻辑对新手很不友好。很多人装完发现mysql --version输出版本低于预期,或者启动时报了一堆依赖错误,基本都是模块流没切对。

还有个很容易忽略的前提:装机时别选带图形界面的版本,尽量用 Minimal 安装,同时确认系统时间、主机名、SELinux 状态。MySQL 对主机名解析很敏感,如果/etc/hosts里没有本机主机名映射,初始化阶段可能报错,这个我在后面排错部分会专门提。

2. 系统自带的 MySQL 模块到底能不能用:能,但坑比想象中多

2.1 AppStream 模块机制到底是啥

RHEL 系从 8 开始引入模块化仓库,CentOS 9 Stream 延续了这套机制。简单来说,dnf module允许同一个软件在仓库里存在多个版本流,你通过dnf module enable或者dnf module switch来选择用哪个流。

MySQL 在 AppStream 里的模块流大概长这样:

dnf module list mysql

输出会列出多个流,比如 mysql-8.0 和 mysql-8.4 之类的。默认情况下,系统可能没有启用任何一个流,你直接dnf install mysql-server的时候,dnf 会按默认流解析,但这里的默认流不一定是你想要的最新版。

2.2 用系统模块装 MySQL 的体验

我最早为了图省事,直接在 9 Stream 上用系统模块装过一次。命令很简单:

dnf module enable mysql:8.0 -y dnf install mysql-server -y

装完确实能跑,但问题出现在几个地方:

  • 版本号偏旧。当时仓库里的 8.0.x 比 Oracle 官方源低了好几个小版本,一些在 8.0.3x 才合入的 bugfix 和性能优化根本用不上。
  • 后续升级路径不清晰。Stream 本身是滚动更新的,模块里的 MySQL 版本什么时候刷新完全取决于发行版维护节奏,这对于数据库这种需要稳定性的服务来说,不确定性太高。
  • 出问题想换官方源时,模块元数据会残留,导致和官方 rpm 包冲突,得手动 reset 模块。

如果你只是在本机跑个轻量应用,不想折腾,那系统模块够用。但如果你希望版本可控、补丁及时,强烈建议走官方仓库,这也是大多数生产环境的常规做法。

3. 走官方仓库的正确姿势:repo 安装、模块禁用和依赖处理

3.1 下载并安装 MySQL 官方 repo 包

Oracle 官方给 RHEL 系提供了专门的 repo rpm 包,注意要选 el9 版本,别拿 el7 或 el8 的硬装。之前有人拿 el8 的包在 9 Stream 上装,结果 GLIBC 版本不满足,报错报得莫名其妙。

rpm -Uvh https://dev.mysql.com/get/mysql80-community-release-el9-5.noarch.rpm

装完之后检查一下仓库是否生效:

dnf repolist enabled | grep mysql

正常情况下你会看到 mysql-community-server、mysql-community-client 等相关仓库。这里有个小技巧:安装 repo 包后先不要急着装 MySQL,先确认仓库里的版本号:

dnf info mysql-community-server --enablerepo=mysql80-community

3.2 强制禁用 AppStream 自带模块

这一步是整个安装过程中最容易翻车的环节。官方仓库里的包名和 AppStream 模块里的包名存在重叠,dnf 在解析依赖时可能会被模块元数据干扰,报类似The operation would result in swapping of module streams的错误。

解决方法是先把自带的 mysql 模块彻底 reset 掉:

dnf module reset mysql -y dnf module disable mysql -y

disable 之后,AppStream 里的 mysql 模块就不会再参与依赖解析了。然后再装官方包:

dnf install mysql-community-server -y

装完用mysql --version验证,通常你会看到类似mysql Ver 8.0.4x for Linux on x86_64的输出,这个版本号后续可以通过官方源持续更新。

3.3 依赖问题:Perl、libaio 这些基础库别忽略

MySQL 8.0 在 RHEL 系上依赖libaioperl相关模块。Minimal 安装的系统里这些可能没装全,但 dnf 会自动处理依赖,只要网络通畅一般没问题。不过有个容易忽略的是ncurses-compat-libs,在文本模式安装或某些脚本初始化场景下可能需要,建议提前装上:

dnf install -y libaio ncurses-compat-libs perl

这一步不是必须的,但可以避免后续mysqld --initialize的时候因为缺库文件报一些看不懂的错误。

4. 初始化数据目录和 systemd 启动:这一步决定你后面省不省心

4.1 数据目录初始化:mysqld --initialize 的两种模式

你会发现用 dnf 装完 MySQL 后,/var/lib/mysql目录是空的,MySQL 不会像 MariaDB 那样自动初始化。必须手动执行:

mysqld --initialize --user=mysql

执行后会自动生成数据目录、系统表、临时 root 密码,并把初始密码写到错误日志里。如果没有报错,继续查看日志:

grep 'temporary password' /var/log/mysqld.log

另一种方式是--initialize-insecure,它会生成一个无密码的 root 账户。我强烈建议不要在生产环境用这个,除非你确定自己会在三秒内设置密码。

还有个细节:如果目录权限不对,初始化会直接失败。正常情况下mysql用户对/var/lib/mysql有写权限,不需要额外 chmod。如果你把数据目录迁移到独立数据盘,要记得chown -R mysql:mysql /data/mysql,否则启动时一堆权限报错。

4.2 通过 systemd 管理 MySQL 服务

初始化完成后,启动服务前先执行一次 daemon-reload:

systemctl daemon-reload systemctl start mysqld systemctl enable mysqld

enable是为了设置开机自启,很多人装了 MySQL 忘记这一步,服务器一重启数据库起不来,业务方半夜打电话找你,那就难受了。

启动后立刻检查状态:

systemctl status mysqld ss -lntp | grep 3306

ss查看 3306 端口监听情况,比netstat更直观。如果你发现端口没监听,多半是初始化出了问题,先把日志翻出来看:

tail -100 /var/log/mysqld.log

4.3 区分 mysqld 和 mysql 两个服务的坑

在 RHEL 系上,你有时候会看到mysql这个 systemd 服务名,但 9 Stream 官方仓库装的 MySQL 服务名是mysqld。不要习惯性地敲systemctl status mysql,你会得到一个不存在的服务报错。同理,开机自启要用systemctl enable mysqld

这里建议在登录提示符上加个备忘,或者写进自己的 CheatSheet 里,不然过几个月再回来操作,真的会忘记。

5. 第一轮安全配置:从临时密码到最小权限账户

5.1 用初始密码登录并强制改密

从日志里查到临时密码之后,登录:

mysql -uroot -p

输入临时密码后,你会进入 MySQL 命令行。MySQL 8.0 默认装了 validate_password 组件,所以不允许你设置太弱的密码。执行:

ALTER USER 'root'@'localhost' IDENTIFIED BY '你的强密码';

如果密码不够强,会直接报错,提示密码策略不满足。建议密码包含大小写、数字、特殊字符,长度至少 12 位以上。

5.2 mysql_secure_installation 要不要跑

Oracle 官方提供了一键安全脚本:

mysql_secure_installation

它会引导你设置密码强度、移除匿名用户、禁止 root 远程登录、删除 test 库等。我个人的做法是:在测试环境可以完整跑一遍,但在已经手动配置过的服务器上,只执行其中几条关键项就够了。

有一点要注意:脚本里的 “禁止 root 远程登录” 建议选 Y,因为实际业务账号不应该用 root 去连接。后面创建业务账号时按需授权即可。

5.3 创建业务账号并控制权限范围

假设你有一个业务库叫appdb,要给后端服务创建一个账号,我通常这样写:

CREATE USER 'appuser'@'192.168.1.%' IDENTIFIED BY '复杂密码'; GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'appuser'@'192.168.1.%'; FLUSH PRIVILEGES;

这里有两个要点。一是主机部分不要随手写成'%',能限定网段就限定网段;二是权限颗粒度按需给,不要一上来就GRANT ALL。如果后续发现业务需要 DDL 权限,再单独补,这样即使账号泄露,损失面也可控。

如果你确实需要通过 Navicat、DBeaver 这类图形客户端从办公网连过去,需要给对应网段授权远程访问,并配合防火墙放行 3306 端口。

5.4 防火墙和 SELinux 对远程连接的影响

CentOS 9 Stream 默认防火墙是 firewalld,3306 端口默认不放行。如果你要远程连接,需要:

firewall-cmd --permanent --add-port=3306/tcp firewall-cmd --reload

但很多时候你做完这一步,远程还是连不上,那就要查 SELinux 了。MySQL 的 SELinux 策略默认允许 mysqld 监听 3306 端口,一般情况下不用调整。但如果你改了端口,比如换成 3307,就必须用 semanage 添加端口上下文:

dnf install -y policycoreutils-python-utils semanage port -a -t mysqld_port_t -p tcp 3307

这个命令是在 SELinux 策略里把 3307 标记为 mysqld 专用端口,不执行的话,mysqld 根本无法绑定该端口。报错信息通常很隐晦,比如Can't start server: Bind on TCP/IP port: Permission denied,很多人以为端口被占用,其实是 SELinux 不放行。

6. 字符集、时区、sql_mode:MySQL 8 上最容易忽略的三件套

6.1 字符集:默认是 utf8mb4,但排序规则值得关注

MySQL 8.0 的默认字符集已经是 utf8mb4,这一点比 5.7 时代进步了很多。但默认排序规则是utf8mb4_0900_ai_ci,它和旧的utf8mb4_general_ci在某些排序和比较行为上有差异。比如某些 emoji 和特殊字符的排序、大小写比较逻辑,在老应用里可能会出现不一致。

如果是从 5.7 迁移过来的老系统,我的建议是在my.cnf里显式固定:

[mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_0900_ai_ci

如果是全新业务,直接用默认值就行。但要注意:官方 rpm 安装的 MySQL 不会自动往my.cnf里写这些参数,你得自己建/etc/my.cnf.d/mysql-default.cnf之类的文件。很多时候你新建了一个数据库,执行SHOW CREATE DATABASE发现字符集是对的,但历史库或手工创建的库字符集不对,就会导致应用写入乱码或报错。

6.2 时区:default-time-zone 和 system_time_zone 的区别

服务器时区是 UTC 的话,MySQL 的NOW()也会返回 UTC 时间,这对于国内业务来说是个隐患。你需要在my.cnf中指定:

[mysqld] default-time-zone = '+08:00'

设置完重启 mysqld。system_time_zone是系统时区,default-time-zone是会话时区,两者分开。官方文档里允许用命名时区,比如Asia/Shanghai,但前提是 MySQL 里已经加载了时区表,否则会报错。所以没加载过时区表的话,直接用+08:00最省事:

SELECT NOW();

6.3 sql_mode:ONLY_FULL_GROUP_BY 引发的血案

MySQL 8.0 默认启用ONLY_FULL_GROUP_BY,这意味着只要 SELECT 列表、HAVING 条件或 ORDER BY 列表里出现了既没有聚合函数包裹、又不在 GROUP BY 中的列,就会直接报错。

很多从 5.7 迁过来的老代码,尤其是那些用GROUP BY但随便 select 非聚合列的历史 SQL,会在 8.0 上翻车。我的处理原则是:尽量改 SQL 去适配新规范,而不是关掉这个模式。关闭它只是治标,长期来看 SQL 写法会越来越糟。

如果确实有历史包袱短期内改不完,可以在my.cnf里临时调整:

[mysqld] sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

但一定要知道这是临时缓解方案,后续该改的 SQL 还是要改。

7. 安装后最容易踩的三个坑:我把完整排查链路写出来

7.1 日志级别与初始化失败的判定逻辑

mysqld --initialize报错时,第一反应应该是看/var/log/mysqld.log,但不是所有日志都会很直白。比如数据目录权限错误时,日志会写[ERROR] failed to initialize,但根本原因是stat目录失败。这个时候要顺着日志往上看,通常在报错前几行会有Permission deniedCan't create/write to file字样。

有一次我在一个自定义数据目录上初始化,日志一直报[ERROR] Aborting,我排查了半天,最后发现是my.cnfdatadir配置写错了路径,MySQL 直接拿着错误路径去操作。建议/etc/my.cnf/etc/my.cnf.d/下所有配置文件的路径都核对一遍,再执行初始化。

7.2 AppStream 模块残留导致的版本回退

这个坑我在第 3 节提过,但值得拿完整链路再讲一遍。具体表现是:你明明装的是官方仓库的 mysql-community-server,执行dnf update之后,版本莫名其妙变成了系统模块里的版本。原因就是dnf module disable mysql没有执行,或者 reset 后没有 disable。

排查方法:

dnf module list mysql dnf history | grep mysql

看到模块状态为[e][d]才是禁用状态。如果状态是默认状态[d],那 dnf 在升级时还是可能把 APStream 的包拉进来。正确操作:

dnf module reset mysql -y dnf module disable mysql -y

然后再确认仓库来源:

dnf list installed | grep mysql

确保所有 mysql 相关包都来自@mysql80-community

7.3 caching_sha2_password 导致的老客户端连接失败

MySQL 8.0 的默认认证插件是caching_sha2_password,比 5.7 时代的mysql_native_password更安全。但它有一个很现实的兼容性门槛:老版本的客户端、旧版 JDBC 驱动、某些低版本的 Navicat/PHP 扩展,根本不认识新插件,连接时会报Authentication plugin 'caching_sha2_password' cannot be loaded

这个问题的解法有两个方向:

第一,把服务端默认认证插件改回mysql_native_password

[mysqld] default-authentication-plugin = mysql_native_password

这种方式最省事,但牺牲了安全性,不推荐长期使用。

第二,只对特定兼容不了的账号降级:

ALTER USER 'appuser'@'192.168.1.%' IDENTIFIED WITH mysql_native_password BY '复杂密码';

这样核心账号还是走新插件,老客户端用的账号单独降级,影响面最小。我个人倾向于第二种,因为真正需要降级的通常只是历史遗留的几个账号。

8. 数据目录迁移、备份策略和 Stream 本身的定位问题

8.1 预先把数据目录放到独立分区/独立盘

默认安装下数据目录在/var/lib/mysql,但生产环境通常会把系统盘和数据盘分开。迁移思路很简单:

systemctl stop mysqld cp -a /var/lib/mysql /data/mysql chown -R mysql:mysql /data/mysql

然后修改/etc/my.cnf

[mysqld] datadir=/data/mysql

重启服务前,注意/etc/my.cnf里如果同时配置了datadirsocket等路径,要一起改,否则日志里会出现Can't create test file之类的问题。另外,SELinux 下如果改了数据目录路径,还要用 semanage 调整mysqld_db_t上下文,否则服务启动时无法读取目录。具体命令:

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

这一步不做,就算权限全部正确,mysqld 也可能因为 SELinux 拒绝读取数据目录而启动失败,而且日志里只写一个[ERROR] Aborting

8.2 备份策略:mysqldump 和 xtrabackup 怎么选

数据库装好不是终点,备份才是日常。小数据量直接 mysqldump:

mysqldump -uroot -p --single-transaction --routines --triggers --events appdb > appdb.sql

大数据量建议用 Percona XtraBackup,支持物理备份和增量备份,比逻辑备份快很多。但要注意 XtraBackup 的版本要和 MySQL 8.0 大版本匹配,8.0.3x 配套的 XtraBackup 版本是 8.0.x,别拿 2.4 时代的版本去备份 MySQL 8.0,会直接报This version of Percona XtraBackup is not compatible with MySQL server

8.3 关于 CentOS 9 Stream 的环境定位

最后说一句可能会得罪人的话:CentOS 9 Stream 是滚动更新发行版,它的定位是 Fedora 和 RHEL 之间的中间地带,更适合开发者、测试环境和想提前体验新特性的场景。对于核心生产数据库,我个人会谨慎一点,要么选用 RHEL 系稳定小版本,要么干脆用容器方式打包好 MySQL,降低宿主系统滚动更新对数据库的冲击。

这不是说 Stream 上跑 MySQL 就跑不稳,而是滚动更新的不确定性会增加运维成本。比如某次 dnf update 把 glibc 或者 OpenSSL 升了,理论上可能影响 MySQL 运行,虽然概率低,但生产环境多一事不如少一事。如果你只能在 Stream 上跑,请务必把自动更新关掉,或者至少把exclude=mysql*写进 dnf 配置里,确保不会半夜自动升级数据库。

9. 最后分享一个我后来一直在用的小习惯

每次装完 MySQL,我做的第一件事不是跑业务,而是写一个环境核对脚本,把版本、端口、字符集、时区、sql_mode、自动更新状态一次性查出来:

mysql --version ss -lntp | grep 3306 mysql -uroot -p -e "SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'time_zone'; SHOW VARIABLES LIKE 'sql_mode';"

这样下次接手或者排查问题时,不用一条条重新回忆配置过什么。数据库这东西,装好只是开始,后面真正花时间的往往是那些不起眼的默认值和环境细节。把基础打牢,后续不管是做读写分离还是换硬件迁移,都能少熬夜。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询