Ubuntu 服务器更换数据盘:迁移 WordPress、MySQL 和 Docker 数据的方法

服务器用久之后,最容易变得混乱的地方通常不是系统本身,而是数据放置方式:站点文件在一个目录,数据库在另一个目录,Docker 数据和日志又在系统盘里。等到系统盘快满、云硬盘需要扩容或更换时,才发现迁移边界并不清楚。

这篇文章记录一种比较稳妥的数据盘迁移方法:把站点文件、MySQL 数据、Docker 数据和 Docker 日志统一放到新的数据盘上,系统盘尽量只保留系统、软件包和配置文件。

目标

迁移后的结构大致是这样:

/
├── var
│   ├── lib
│   │   ├── mysql  -> 绑定到数据盘
│   │   └── docker -> 绑定到数据盘
│   └── www
│       └── example.com -> 指向数据盘里的站点目录

/mnt/data
├── www
│   └── example.com
└── system-data
    ├── mysql
    └── docker

这样做有几个好处:

  • 系统盘更干净,重装或扩容时风险更小。
  • MySQL、Docker、站点文件都在数据盘,备份和迁移边界更清晰。
  • Docker 的容器日志也跟随 Docker 数据目录放到数据盘,避免系统盘被日志写满。
  • 以后更换数据盘时,只需要围绕一个挂载点处理。

迁移前先盘点

不要一上来就复制文件。先确认当前磁盘、挂载点、服务和主要数据目录。

lsblk -f
df -hT
findmnt
systemctl --failed

看 MySQL 和 Docker 当前数据目录:

mysql -NBe "SELECT @@datadir;"
docker info --format 'DockerRootDir={{.DockerRootDir}} Driver={{.Driver}}'

看几个关键目录的大小:

du -sh /var/lib/mysql /var/lib/docker /var/www /mnt/data 2>/dev/null

如果 Docker 里有 bind mount,也要提前列出来:

docker ps -q | xargs -r docker inspect \
  --format '{{.Name}} {{range .Mounts}}{{.Source}}=>{{.Destination}} {{end}}'

这一步的目标是搞清楚:哪些是运行时数据,哪些只是旧备份、旧应用或可以删除的历史文件。

准备新数据盘

假设新盘是 /dev/vdb,旧数据盘挂载在 /mnt/data,临时挂载点使用 /mnt/data-new。实际操作时请替换成自己的设备名。

先确认新盘没有挂错:

lsblk -f

然后分区并格式化。下面只是示例,执行前一定要确认设备名:

parted /dev/vdb --script mklabel gpt
parted /dev/vdb --script mkpart primary ext4 0% 100%
mkfs.ext4 -L data-disk /dev/vdb1

创建临时挂载点并挂载:

mkdir -p /mnt/data-new
mount /dev/vdb1 /mnt/data-new

先做在线预同步

真正停服务前,可以先把大部分静态数据同步过去,减少最终停机时间。

rsync -aHAX --numeric-ids --info=stats2 /mnt/data/ /mnt/data-new/

如果 MySQL 和 Docker 还在系统盘,可以先在新数据盘上准备目录:

mkdir -p /mnt/data-new/system-data/mysql
mkdir -p /mnt/data-new/system-data/docker

在线预同步一次:

rsync -aHAX --numeric-ids --delete /var/lib/mysql/ /mnt/data-new/system-data/mysql/
rsync -aHAX --numeric-ids --delete /var/lib/docker/ /mnt/data-new/system-data/docker/

这里的预同步只是为了节省时间,不能当作最终一致的数据。MySQL 和 Docker 运行时仍然会写文件,所以最后还需要停服务再同步一次。

进入维护窗口

最终切换时,需要短暂停掉会写入数据的服务。常见包括:

systemctl stop cron nginx php8.2-fpm docker mysql

如果机器上服务名不同,例如 PHP 版本不是 8.2,按实际情况替换。

停服务后做最终同步

先同步旧数据盘内容到新数据盘:

rsync -aHAX --numeric-ids --delete /mnt/data/ /mnt/data-new/

再同步 MySQL 和 Docker 数据:

rsync -aHAX --numeric-ids --delete /var/lib/mysql/ /mnt/data-new/system-data/mysql/
rsync -aHAX --numeric-ids --delete /var/lib/docker/ /mnt/data-new/system-data/docker/

几个参数含义:

  • -a:归档模式,保留权限、时间等信息。
  • -HAX:尽量保留硬链接、ACL、扩展属性。
  • --numeric-ids:按数字 UID/GID 同步,避免用户名映射差异。
  • --delete:删除目标端多余文件,让目标端和源端一致。

更新挂载关系

先查新盘 UUID:

blkid /dev/vdb1

备份 /etc/fstab

cp /etc/fstab /etc/fstab.bak.$(date +%Y%m%d%H%M%S)

然后把数据盘挂载改成新 UUID。示例:

UUID=<NEW_DATA_DISK_UUID> /mnt/data ext4 defaults 0 1

如果希望 MySQL 和 Docker 继续使用原来的路径,可以使用 bind mount。这样配置文件和服务不需要感知真实数据位置变化:

/mnt/data/system-data/mysql /var/lib/mysql none bind,nofail 0 0
/mnt/data/system-data/docker /var/lib/docker none bind,nofail 0 0

切换挂载点

为了保留回滚机会,可以先把旧目录改名,而不是直接删除:

mv /var/lib/mysql /var/lib/mysql.before-data-disk
mkdir -p /var/lib/mysql
chown mysql:mysql /var/lib/mysql
chmod 750 /var/lib/mysql

mv /var/lib/docker /var/lib/docker.before-data-disk
mkdir -p /var/lib/docker
chmod 711 /var/lib/docker

卸载旧挂载点和临时挂载点,再按新的 fstab 挂载:

umount /mnt/data
umount /mnt/data-new
mount /mnt/data
mount /var/lib/mysql
mount /var/lib/docker

确认挂载结果:

findmnt /mnt/data
findmnt /var/lib/mysql
findmnt /var/lib/docker
df -hT /mnt/data /var/lib/mysql /var/lib/docker

启动服务并验证

服务启动顺序可以按依赖来:

systemctl start mysql docker php8.2-fpm nginx cron

验证服务状态:

systemctl is-active mysql docker php8.2-fpm nginx cron
systemctl --failed

验证 MySQL:

mysqladmin ping

验证 Docker:

docker info --format 'DockerRootDir={{.DockerRootDir}}'
docker ps

如果是 WordPress,可以用 PHP 直接加载一次站点:

php -r "require '/mnt/data/www/example.com/wp-load.php'; echo get_bloginfo('name'), PHP_EOL;"

最后从 HTTP 层验证:

curl -I https://example.com/

控制 Docker 日志大小

迁移 Docker 数据目录后,顺手应该把 Docker 日志大小控制住。否则 json-file 日志可能持续增长。

编辑 /etc/docker/daemon.json

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "20m",
    "max-file": "5"
  },
  "live-restore": true
}

这个配置对新创建的容器生效。已经存在的容器,可以再用 logrotate 兜底:

/var/lib/docker/containers/*/*-json.log {
    rotate 5
    daily
    size 20M
    compress
    missingok
    delaycompress
    copytruncate
    notifempty
}

这样新容器有 Docker 原生日志限制,旧容器也不会无限写大日志文件。

旧盘怎么处理

确认新盘运行正常后,旧盘先不要急着格式化。建议至少保留一段观察期:

  • 服务都能启动。
  • 网站能访问。
  • 数据库能读写。
  • Docker 容器状态正常。
  • 重启后挂载关系仍然正确。

确认没有回滚需求后,再到云厂商控制台分离旧数据盘。注意:系统里的 umount 只是卸载文件系统,云厂商控制台里的“卸载/分离硬盘”是另一步。

回滚思路

如果切换后发现问题,回滚的关键是:旧数据不要立刻删除,旧盘不要立刻释放。

常见回滚方式:

  1. 停止相关服务。
  2. 恢复 /etc/fstab 备份。
  3. 重新挂载旧数据盘。
  4. /var/lib/mysql/var/lib/docker 指回旧数据。
  5. 启动服务并验证。

等新盘稳定运行后,再删除系统盘上的迁移备份目录,释放空间。

总结

更换数据盘最重要的不是某一条命令,而是迁移边界要清楚:哪些数据必须一致,哪些服务会写入,哪些路径需要保持兼容。

我比较推荐的结构是:系统盘只放系统和软件,数据盘承载站点文件、MySQL、Docker 数据和 Docker 日志。通过 fstab 和 bind mount 保持原路径不变,既减少配置改动,也让后续备份、扩容和迁移更容易。

发表评论