尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MySQL数据目录迁移实战:从系统盘到数据盘,彻底解决磁盘空间不足

MySQL数据目录迁移实战:从系统盘到数据盘,彻底解决磁盘空间不足 MySQL 数据文件把系统盘塞满几乎是每个做开发或者运维的人都会撞上的事。我最近就处理了一台服务器的 MySQL 存储路径迁移默认数据目录落在根分区业务跑了几个月binlog 加 ibdata1 加几张核心业务大表直接把根分区干到 95%。查了一圈监控确认数据盘还有大把空间但 MySQL 就是不会自己把数据放到数据盘上。于是花了半天时间把数据目录从/var/lib/mysql整体迁到了独立数据盘顺手把整个流程和中间踩过的坑整理了一遍。这篇文章不是那种只贴两条命令就完事的教程。我会从为什么默认路径不靠谱讲起然后把停服务、拷贝数据、改配置、启动验证的完整链路拆开每一步都说明原理最后再把权限、SELinux、AppArmor、日志路径这些高频翻车点单独拎出来讲。无论你是第一次改路径的新手还是已经改过但遇到启动失败的老手这文章都能直接照着操作。1. 为什么放着默认路径不用非要去改它1.1 默认路径的尴尬处境不管你用 apt 还是 yum 安装 MySQL数据目录默认都会落在/var/lib/mysql。这个位置本身没毛病真正的问题在于它通常跟根分区绑在一起。装系统的时候根分区一般规划 50G 到 100G想着够用了结果数据库一跑起来空间消耗速度远超预期。MySQL 的数据文件消耗有几个特点首先是 InnoDB 系统表空间ibdata1默认情况下会持续增长而且你删了表它也不一定收缩其次是 binlog只要开启了 binlog 并且没做定期清理它能把磁盘写满再加上业务表产生的.ibd文件、undo 日志、redo 日志根分区半年之内告警是常态。你可能会说清理一下不就完了删 binlog、清慢日志、purge 一下大表的碎片能撑一阵子。但这是治标不治本。数据量只要还在涨过两个月又会报警。最稳妥的办法就是让 MySQL 把数据写到真正有空间的位置也就是独立数据盘。1.2 修改路径的本质只有一个 datadir 参数MySQL 的数据存储路径不是分散管理的它由datadir这个参数统一控制。数据目录下面装的东西包括系统表空间ibdata1、redo 日志、undo 日志、数据字典、每个数据库各自的目录、表结构.frm文件或者 8.0 的数据字典、auto.cnf服务器 UUID、SSL 证书.pem文件等等。也就是说所谓“修改数据存储路径”本质上就是把这个datadir参数指向一个新目录然后把旧目录里的所有数据文件原封不动地搬过去。听起来简单但实际操作中有很多细节坑拷贝方式不对会丢权限配置文件改错会被其他配置覆盖SELinux 或 AppArmor 还会在底层拦截 MySQL 访问新路径。搞清楚这个本质之后你就不会被各种教程里的花式操作绕晕了。不管网上教程用了什么命令核心链条就三条数据完整拷贝过去、配置文件指到新路径、MySQL 有权限读写新路径。1.3 同机换目录 vs 跨机搬数据改存储路径有两种常见场景。第一种是同一台机器上从系统盘迁到数据盘这是本文的主角。第二种是换服务器把数据从旧机器搬到新机器。两者在思路上有交集但操作方式不一样。跨机迁移可以使用逻辑备份mysqldump或者物理备份通常还涉及 MySQL 版本升级、字符集调整、账号权限迁移复杂度明显更高。同机换目录则简单得多MySQL 版本不变、系统不变、数据文件格式不变只要目录搬过去、配置改过来就行。如果你只是想让 MySQL 使用新挂载的数据盘请记住不要用 mysqldump 去导数据。几百 GB 的库用逻辑备份导出再导入耗时是物理拷贝的几十倍而且还有可能遇到字符集、SQL 模式兼容性问题。直接停服务把整个数据目录物理拷贝过去是最快也最稳的方式。2. 动手前的清点工作备份、配置文件与数据目录盘点2.1 先找到真正生效的 my.cnf很多人改路径失败第一步就栽在配置文件上——MySQL 读取配置文件是有顺序的前面的文件里如果存在datadir后面的配置就不生效了。Linux 下 MySQL 配置文件的常见读取位置有四个顺序文件路径1/etc/my.cnf2/etc/mysql/my.cnf3/usr/local/mysql/etc/my.cnf4~/.my.cnf不同发行版还会引入/etc/mysql/mysql.conf.d/和/etc/mysql/conf.d/之类的子目录配置文件里有!includedir指令去加载它们。怎么确认当前生效的是哪个文件执行mysql --help --verbose | grep -A1 Default options输出会直接告诉你 MySQL 按什么顺序读取配置文件。再查看当前生效的 datadirmysql -uroot -p -e SELECT datadir;把这两条命令的输出记下来然后去对应的 my.cnf 里确认datadir参数的位置。我的经验是很多云服务器镜像会把配置写在/etc/mysql/mysql.conf.d/mysqld.cnf里而不是直接写在/etc/my.cnf里你只改后者的话根本没生效。2.2 把数据目录里的文件认全改路径之前先去看看/var/lib/mysql里面到底放了什么。ls -la /var/lib/mysql不同 MySQL 版本的目录结构差异很大一定要先认清楚MySQL 5.7 的典型文件包括ibdata1、ib_logfile0、ib_logfile1、undo_001、undo_002以及每个库对应的目录和mysql、performance_schema、sys三个系统库。MySQL 8.0 的差异更明显——redo 日志不再是根目录下的ib_logfile0/1而是放进了#innodb_redo目录系统表空间也有了变化数据字典全部集中在mysql.ibd里。关于 MySQL 8.0 和 5.7 文件结构的差异下面单独整理一个表格文件/目录MySQL 5.7MySQL 8.0redo 日志ib_logfile0、ib_logfile1#innodb_redo/目录数据字典各库目录下.frm文件mysql.ibd集中管理undo 日志undo_001、undo_002undo_001、undo_002可自行配置数量系统表空间ibdata1ibdata1、mysql.ibd这些差异对迁移的影响是你不需要理解每个文件的具体作用但你必须把整个目录完整复制过去尤其是#innodb_redo和mysql.ibd这种看起来不熟的文件。很多人在拷贝时只复制了业务库目录忽略了系统文件结果 MySQL 启动后直接报数据字典损坏。另外auto.cnf这个文件非常重要它记录了服务器的 UUID主从复制场景下不能删除否则重新生成 UUID 会导致从库报错。拷贝时务必带上隐藏文件。2.3 备份策略与磁盘检查改路径是高风险操作任何情况下都不要跳过备份。同机迁移虽然理论上只需冷拷贝但如果中途断电、磁盘损坏、误操作覆盖没有备份就真的凉了。备份策略我建议分两层先在迁移前做一次逻辑备份兜底再用冷拷贝的方式做物理迁移。逻辑备份用 mysqldump 只备份数据不备份存储过程和事件的话命令如下mysqldump -uroot -p --single-transaction --routines --events --triggers --all-databases /tmp/all_databases.sql--single-transaction对 InnoDB 表可以拿到一致性快照不会阻塞业务写入但如果你有大量 MyISAM 表这个参数对 MyISAM 不生效需要在备份时用--lock-tables或者在低峰期操作。逻辑备份文件别放在/tmp下面因为/tmp在系统盘空间可能不够。放到数据盘或者其他机器上。磁盘检查也很关键。执行df -h确认目标数据盘的挂载点执行du -sh /var/lib/mysql确认数据目录当前大小确保目标路径剩余空间大于数据目录大小最好预留 20% 以上的余量。如果数据目录有 500GB而目标盘只剩 520GB这种 margin 太薄不建议开工。3. 迁移主链路从停服务到启动的全流程3.1 停服务与进程确认迁移的第一步是停掉 MySQL 服务。不同系统的命令不一样CentOS 7/8 用 systemdsystemctl stop mysqldUbuntu 上服务名可能是mysqlsystemctl stop mysql停完服务之后不要急着拷贝。先确认 mysqld 进程是真的退出而不是还在做缓冲池刷新之类的收尾工作ps aux | grep mysqld如果还有残留进程等几秒再查一次。有些场景下 systemd 停服务是优雅关闭InnoDB 需要把 buffer pool 里的脏页刷到磁盘这个时间可能长达几十秒。强制 kill 进程是同步危操作如果 MySQL 正在写入可能导致数据文件损坏。确认进程退出后看一下数据目录大小du -sh /var/lib/mysql这个数字决定了后面拷贝需要的时间也帮你判断目标盘空间是否够用。3.2 拷贝数据目录的正确姿势拷贝数据目录我强烈推荐rsync而不是cp。理由很直接rsync -av可以保留文件属主、权限、软链接、时间戳而且可以断点续传。虽然此时服务已经停了不需要增量同步但 rsync 在传输大目录时比 cp 更稳。假设目标路径是/data/mysql执行mkdir -p /data/mysql rsync -av /var/lib/mysql/ /data/mysql/这里有两个细节必须注意。第一源路径结尾的斜杠/var/lib/mysql/表示只复制该目录下的内容不包含mysql这个目录本身如果去掉斜杠会复制出/data/mysql/mysql/这样的嵌套结构。第二拷贝完成后要检查 rsync 的退出码输出末尾出现sent X bytes received Y bytes并且没有报错才代表成功。拷贝完成后最关键的一步是修改属主。MySQL 进程通常以mysql用户运行新目录必须赋权给这个用户否则启动必失败chown -R mysql:mysql /data/mysql有些教程会说只需要chown -R mysql:mysql /data/mysql这一条但我建议你在赋权之后再检查一遍是否所有文件都正确find /data/mysql -maxdepth 1 -exec ls -ld {} \;主要确认drwxr-xr-x这类的权限位没问题。如果你的机器上 mysql 用户 uid 和 gid 与源目录不同rsync 的-a参数会保留原始 uid/gid可能和新系统不匹配此时chown -R是必须的。3.3 修改配置文件的参数细节找到真正生效的 my.cnf 之后在[mysqld]段落里修改datadir。同时检查有没有innodb_data_home_dir、innodb_log_group_home_dir这两个参数如果有也必须同步改到新路径否则 InnoDB 会去旧位置找数据文件。[mysqld] datadir/data/mysql # 一般不需要单独设置但如果你配置过记得一并修改 # innodb_data_home_dir/data/mysql # innodb_log_group_home_dir/data/mysql这里有个细节路径末尾不要带斜杠。datadir/data/mysql/这种写法在某些版本下会出现路径拼接错误报错信息还很隐蔽只说找不到路径。然后处理 socket 和 pid 文件。如果原来的 socket 路径也依赖数据目录比如配置了socket/var/lib/mysql/mysql.sock可能不需要改因为/var/lib/mysql目录本身还保留着。如果你希望把所有运行时文件都挪走那就要同步修改socket/run/mysqld/mysqld.sock pid-file/run/mysqld/mysqld.pid改 socket 路径时要注意目录权限。/run/mysqld这个目录必须存在且属于 mysql 用户否则客户端连接时会报Cant connect to local MySQL server through socket。3.4 启动 MySQL 并确认基础状态改完配置后先启动服务systemctl start mysqld启动成功的瞬间不要急着庆祝立刻查看错误日志tail -100 /var/log/mysqld.log日志位置因系统而异Ubuntu 上可能是/var/log/mysql/error.log也可以通过systemctl status mysqld查看输出。确认日志尾部没有 ERROR 级别的内容后登录 MySQL 验证 datadirmysql -uroot -p -e SELECT datadir;如果输出/data/mysql/说明配置已经生效。再创建一个临时表并插入数据验证写入路径正常CREATE DATABASE test_move; USE test_move; CREATE TABLE t1 (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50)); INSERT INTO t1 (name) VALUES (hello); SELECT * FROM t1;数据能写能读说明迁移主链路已经打通。但先别急着把旧目录删掉后面还有验证和回滚空间要留。4. 启动失败的高频翻车现场权限、SELinux 与 AppArmor4.1 权限错误最典型的报错与处理迁移后最常见的启动失败原因是新目录权限不对。错误日志里通常会看到类似[ERROR] Could not open file /data/mysql/mysql.ibd for error: 13 (Permission denied)或者更直接的[ERROR] InnoDB: Operating system error number 13 in a file operation.error 13 就是 EACCES权限不足。解决办法就是前面强调过的chown -R mysql:mysql /data/mysql。但如果做完仍然报权限错误要往两个方向排查一是目录路径上的每一级目录权限比如/data本身如果是 700 权限且属于 root那么 mysql 用户无法穿越这个目录需要chmod 755 /data二是如果用了rsync -a某些文件的 ACL 或特殊权限位可能异常这时可以放宽一点直接对数据目录递归赋权chmod -R 750 /data/mysql chown -R mysql:mysql /data/mysql750 这个权限位足够 mysql 用户读写又不会让其他用户随便访问生产数据。4.2 SELinux 强制模式的隐藏拦截CentOS 和 RHEL 系列还有个常见坑就是 SELinux。即使权限全对、属主完全正确MySQL 依然可能启动失败因为 SELinux 在底层把关默认只允许 mysqld 访问/var/lib/mysql这个特定路径。你可以快速确认 SELinux 状态getenforce如果输出Enforcing那就要处理了。先看日志grep mysql /var/log/audit/audit.log | tail -20你会看到大量avc: denied { read write }之类的记录。处理方式有两种一是把新目录的 SELinux 类型设置成mysqld_db_t这是 MySQL 数据目录的正确类型semanage fcontext -a -t mysqld_db_t /data/mysql(/.*)? restorecon -Rv /data/mysql如果系统没装 semanage先装 policycoreutils-python-utils。第二种方式是直接放行 mysqld 域但这会降低整体安全性不推荐。我见过有人图省事直接setenforce 0重启后 SELinux 又自动打开MySQL 又挂了这种临时方案在生产环境是不可取的。4.3 Ubuntu 下 AppArmor 带来的特殊问题Ubuntu 和 Debian 系统的坑在 AppArmor。即便你改了 my.cnf权限和属主也没问题MySQL 启动时依然可能报找不到文件或者无法访问目录但错误日志里不会直接告诉你 AppArmor 拦截了。AppArmor 对 mysqld 的限制配置文件在/etc/apparmor.d/usr.sbin.mysqld里面定义了 mysqld 可以访问的路径。默认只有/var/lib/mysql/你新换的/data/mysql/不在白名单里。处理方式是编辑这个文件在对应的# Site-specific additions and overrides区域加入/data/mysql/ r, /data/mysql/** rwk,然后重载 AppArmorsystemctl reload apparmor这里有个细节rwk三个权限位分别是读、写、锁。MySQL 的数据文件需要锁操作只给rw有时候还不够启动时会报 file lock 相关错误。所以rwk是标准写法。如果你发现重启服务后仍然被拦截可以临时停掉 mysqld 的 AppArmor profile 做交叉验证aa-complain /usr/sbin/mysqld这会让 AppArmor 对 mysqld 进入日志记录模式而不是强制拦截配合dmesg | grep apparmor能看到明确的拦截记录。排查完记得恢复到 enforce 模式。4.4 日志文件路径引发的“假成功”有些时候 MySQL 能启动但表现不正常比如 binlog 写不进去、慢日志没生成、错误日志不更新。这种“假成功”比直接启动失败更难排查因为服务状态是 running没有任何明显的崩溃迹象。根源往往是配置文件里除了datadir还有一堆其他路径引用着旧的数据目录。常见的有log-bin/var/lib/mysql/mysql-bin slow_query_log_file/var/lib/mysql/mysql-slow.log log-error/var/lib/mysql/mysql-error.log这些路径如果指向旧目录而旧目录的文件已经被搬走MySQL 会在启动时尝试创建新文件如果旧目录的磁盘空间不够或者权限不对就会报各种奇怪错误。我的建议是改datadir的同时把 log-bin、slow_query_log_file、log-error 这些路径一并改成新目录或者干脆不指定路径让 MySQL 默认把日志文件放到 datadir 下面。这样整个数据目录自包含后续再迁移或者扩容时所有需要关心的文件都在一个地方。5. 验证迁移结果能连上只是第一步5.1 datadir、socket、错误日志三位一体确认很多教程到“能连上 MySQL”这一步就结束了但我会习惯再做更完整的确认。能连上说明服务起来了但服务起来不代表数据完整也不代表所有路径都迁移成功。进入 MySQL 后执行下面这组查询SHOW VARIABLES LIKE datadir; SHOW VARIABLES LIKE socket; SHOW VARIABLES LIKE pid_file; SHOW VARIABLES LIKE log_error; SHOW VARIABLES LIKE log_bin; SHOW VARIABLES LIKE slow_query_log_file;重点确认datadir以及各类日志文件路径都已经指向新目录。log_error如果还是旧路径后续排障会非常痛苦——服务在跑但日志写到别处你 tail 一个空文件什么都看不到还以为系统很安静。5.2 数据完整性抽查与 InnoDB 健康检查文件路径确认完毕后再做数据完整性检查。先看数据库列表和表数量SHOW DATABASES; SELECT table_schema, COUNT(*) FROM information_schema.tables GROUP BY table_schema;对照迁移前的数据库清单确认每个库的表数量一致。然后抽查几张核心表的数据量SELECT COUNT(*) FROM 业务库.核心表;这里有个朴素的道理表的数量对了不代表文件没有损坏所以最好再跑一次 InnoDB 崩溃恢复状态检查SHOW ENGINE INNODB STATUS\G重点看LOG段落中的Log sequence number以及MEMORY段落中是否大量报错。另外在操作系统层面看错误日志grep -i error\|warning /var/log/mysqld.log | tail -20这一组检查做完基本能确认 InnoDB 引擎状态健康。5.3 先别急着删旧目录留一个回滚窗口迁移并验证成功后最忌讳的事情就是立刻删除旧数据目录。我的习惯是保留旧目录至少一周。具体做法是把旧目录改名而不是直接rm -rfmv /var/lib/mysql /var/lib/mysql.bak.20250120这样有几个好处如果一周内发现新路径有问题可以秒级回滚——停服务、改回 datadir、启动如果一切正常一周后再删除旧目录空间才真正释放。而且把旧目录改名后系统里不会出现两个数据目录同时存在导致的混淆。如果你在迁移后发现磁盘空间不够必须立刻删旧目录那也要确保新目录已经稳定运行至少 24 小时并且逻辑备份文件是完好可用的。6. Windows 与 Docker 容器里的迁移姿势6.1 Windows 下修改数据目录的方法虽然大部分生产环境是 Linux但开发机上用 Windows 装 MySQL 的人也不少。Windows 版本的 MySQL 配置文件是my.ini一般位于 MySQL 安装目录下比如C:\ProgramData\MySQL\MySQL Server 8.0\my.ini。Windows 下修改路径的注意点在于路径分隔符。my.ini里datadir的路径写法有两种datadirC:/MySQLData/Data # 或者 datadirC:\\MySQLData\\Data第一种用正斜杠第二种用双反斜杠转义。只写单个反斜杠C:\MySQLData\Data会导致配置解析错误MySQL 服务根本起不来。操作步骤跟 Linux 类似停掉 Windows 服务服务名叫 MySQL80 或者 MySQL 之类的复制数据目录到新位置修改my.ini重启服务。Windows 下没有 SELinux 和 AppArmor 的困扰但要注意新目录的 NTFS 权限给 MySQL 服务账号通常是NETWORK SERVICE或自定义账号赋完全控制权限。6.2 Docker 容器改存储路径的本质Docker 里跑 MySQL很多人会直接改容器内部路径或者进容器改 my.cnf但我建议先想清楚一个概念容器里的 MySQL 数据目录本身是定死的在/var/lib/mysql真正能帮你“修改存储路径”的是宿主机上的磁盘挂载。Docker 下正确的迁移方式是把宿主机的某个目录挂载到容器的数据目录。默认的挂载点就是/var/lib/mysqldocker run -d \ --name mysql8 \ -v /data/mysql:/var/lib/mysql \ -v /data/mysql-config:/etc/mysql/conf.d \ -e MYSQL_ROOT_PASSWORDyourpassword \ mysql:8.0如果你想把现有容器迁移到新路径先停掉容器确认容器对应的宿主机数据目录位置docker inspect mysql8 | grep -A5 Mounts然后在宿主机上把旧数据目录拷贝到新位置rsync -av /var/lib/docker/volumes/xxx/_data/ /data/mysql/最后删掉旧容器用新的-v /data/mysql:/var/lib/mysql参数重新创建容器。整个过程中容器内的 MySQL 版本、配置都没变变的只是宿主机上挂载的路径。这就是容器化环境下“修改存储路径”的正确姿势。有一点要注意重新创建容器时MYSQL_ROOT_PASSWORD这类环境变量不会覆盖已有的数据目录内容。如果新挂载的目录里已经有历史数据MySQL 会直接使用这些数据而不是重新初始化。6.3 MySQL 版本差异带来的迁移坑最后补充一个版本层面的提醒。如果你正在用的是 MySQL 8.0.30 以上版本文件结构相比 5.7 变化很大迁移时要注意 redo 日志目录#innodb_redo和系统表空间mysql.ibd必须完整拷贝不要只拷贝业务库目录。另外8.0 支持把 redo 日志放在独立路径通过innodb_log_group_home_dir参数控制。有些 DBA 会把 redo 放到单独的 SSD 上提升性能这种场景下改数据路径时要注意 redo 日志和表数据是分离的需要分别迁移不要混在一起。如果你启用了 InnoDB 表空间加密还要关注 keyring 文件的位置。keyring 文件通常通过early-plugin-load和keyring_file_data参数配置默认路径也在数据目录下。迁移后 keyring 文件如果找不到MySQL 启动直接失败而且不会提示你具体原因只会报Cant open keyring file之类的错误。这类冷门问题平时遇不到遇到就会卡很久提一嘴帮你省点时间。回到我开头说的那台服务器迁移完成之后根分区使用率从 95% 直接降到 30%数据盘被充分利用起来。后面几天我每天都会看一眼错误日志和SHOW ENGINE INNODB STATUS的输出确认没有出现异常。如果你也要做类似操作最后再分享一个我个人的习惯把迁移过程中每一步执行的命令和输出都记录到文件里。比如rsync的日志、chown之后目录的属主信息、修改后的 my.cnf 内容、启动后的错误日志片段。这些记录在迁移出问题时能帮你快速定位在后续复盘时更是宝贵的参考资料。我自己就靠这份记录在后来一次磁盘故障中几分钟内确认了数据目录的完整结构而不是对着屏幕瞎猜。迁移操作本身不难难的是把每一步都想清楚、留好退路。按照这篇文章的流程走你会少踩很多我踩过的坑。
返回列表