云服务器更换到阿里云:当前站点迁移、新机部署、数据迁移与应用切换

原创 临窗旋墨 2026-11-06 阅读:45 分类: Linux运维 专题: Linux小主机实践 标签: linux,笔记,网络

云服务器更换到阿里云:当前站点迁移、新机部署、数据迁移与应用切换

2026 年 10 月 30 日,我购买了一台阿里云服务器,配置为 2 核、2 GiB 内存、3 Mbps 带宽,系统使用 Ubuntu 24.04。旧云服务器还有一个月到期,这次迁移要把网站、中间件、附件和相关任务完整搬过去,同时保留家中小主机上的 MySQL 从库和附件备份。

我的个人网站可以接受短暂停写,因此没有设计双写或自动故障转移。迁移顺序定为:旧站继续运行时准备新环境并预同步文件;切换当天暂停写入,迁移 MySQL 和最后一批文件;应用验证通过后切换 DNS。

目前业务已经切到新云。MySQL 主从同步、旧附件访问、搜索和主要站点已经验证;新附件上传、日志实际轮转和整机重启恢复仍列在收尾清单中。

目录

  1. 迁移前后是什么样子
  2. 先确定迁移边界和顺序
  3. 准备新服务器
  4. 安装 Docker 和中间件
  5. 安装 Nginx、证书和日志策略
  6. 在停写之前预同步文件
  7. 切换 MySQL 主库
  8. 最终同步文件并发布应用
  9. 调整备份来源并切换 DNS
  10. 验证结果与收尾
  11. 下次迁移如何复用

1. 迁移前后是什么样子

1.1 迁移前的环境

旧云运行 Ubuntu 20.04,配置同样是 2 核、2 GiB 内存。博客和附件服务由家中小主机的 Jenkins 构建镜像,再通过 SCP 传到云服务器,最后用 docker run 发布。

旧环境的中间件比较分散:MySQL、Redis 和 Meilisearch 直接安装在宿主机,Nginx 也在宿主机;家中小主机是 MySQL 从库,每天还会拉取云端附件。Java 应用在 Docker 中访问宿主机中间件,使用过 host.docker.internal,Meilisearch 甚至经过 Nginx 的 84 端口代理。

这套环境一直能用,但下次迁移仍要重新安装、重新找配置。于是这次顺手做了三项整理:

  1. MySQL、Redis、Meilisearch 改为 Docker Compose 管理,配置和数据按应用保存。
  2. Java 应用保留现有 Jenkinsfile 和 docker run,不在迁移期间同时改发布方式。
  3. 新机清单、脚本、Compose、Nginx 配置和迁移记录统一放进 mine-linux 仓库。

1.2 迁移后的连接关系

迁移后的部署与连接关系

图中主从同步、附件备份的箭头表示数据流向,连接由小主机主动发起。

2. 先确定迁移边界和顺序

2.1 哪些数据要搬

  • MySQL:迁移 lcxm_backend、lcxm_attachment 两个业务库,并把家中从库接到新主库。
  • 附件和静态文件:先预同步,停止写入后再做一次最终同步。
  • Redis:不保留旧数据,新环境重新开始。
  • Meilisearch:不迁移索引,由博客应用重新建立。
  • Java 应用:重新构建镜像并发布,不复制旧容器运行目录。
  • Nginx:使用整理后的配置,不直接照搬旧云全部历史文件。
  • 证书:通过 acme.sh 重新签发,不复制旧证书状态。

2.2 准备阶段和切换阶段分开

准备阶段不影响旧站:

  1. 初始化新服务器,安装 Docker。
  2. 部署并验证 MySQL、Redis、Meilisearch。
  3. 配置 WireGuard、安全组、UFW、Nginx 和证书。
  4. 从旧云预同步附件和静态文件。
  5. 准备 Jenkins 到新云的 SSH 登录和发布地址。

切换阶段才暂停写入:

  1. 停止旧云和小主机中可能写业务库的应用或任务。
  2. 等家中 MySQL 从库追平旧主库。
  3. 导出数据库,导入新云,并把家中从库接到新主库。
  4. 最终同步附件和静态文件。
  5. 发布博客、附件应用,重建搜索索引。
  6. 验证新云后切换 DNS,再恢复写入和备份任务。

数据库安装和数据迁移特意分开。MySQL 容器可以提前启动和验证;真正的数据导入留在停写窗口中完成。

3. 准备新服务器

以下命令以 Ubuntu 24.04、root 账号为例。项目保存在 /xqd/infra/mine-linux/,不采用这个路径时,相应修改命令中的项目位置。

3.1 目录和全局变量

服务器上的资源统一放在 /xqd:

text 复制代码
/xqd/
├── infra/mine-linux/       脚本、配置样例、文档和迁移记录
├── docker/
│   ├── mysql/              compose.yaml、config/、data/
│   ├── redis/              compose.yaml、config/、data/
│   └── meilisearch/        compose.yaml、config/、data/
├── attachments/            附件文件
└── server/                 Java 日志与静态站点

仓库 server/docker/ 下的 mysql、redis、meilisearch 目录复制到 /xqd/docker/,保留各自的 compose.yaml 和 config/。data/ 是容器启动后产生的运行数据,不随项目复制,也不提交 Git。

仓库中的全局变量文件是无凭证样例。复制到 /etc/profile.d 后,填写服务器自己的密码和阿里云 DNS 凭证:

bash 复制代码
cd /xqd/infra/mine-linux/server

cp config/xqd-global-env.sh /etc/profile.d/xqd-global-env.sh
chmod 600 /etc/profile.d/xqd-global-env.sh
vi /etc/profile.d/xqd-global-env.sh
source /etc/profile.d/xqd-global-env.sh

文件中主要有这些变量:

  • NEW_HOSTNAME:Linux 系统主机名。
  • NEW_SERVER_HOST:SSH/SCP 使用的地址,这次是 infra.xuqiudong.cn。
  • DOMAIN:申请主域名和泛域名证书时使用。
  • MYSQL_*、REDIS_*、MEILI_*:Compose 使用的镜像、端口、密码和密钥。
  • Ali_Key、Ali_Secret:acme.sh 的阿里云 DNS 插件使用。

文件权限为 600,供后续 root 登录 shell 加载。当前会话要手动 source;cron、systemd 和已经运行的容器不会自动继承这些变量。Compose 只会把 environment 中明确声明的值传入容器。

查看时可以只列变量名,避免把凭证复制到终端记录中:

bash 复制代码
printenv | cut -d= -f1 |
  grep -E '^(NEW_HOSTNAME|NEW_SERVER_HOST|DOMAIN|MYSQL_|REDIS_|MEILI_|Ali_)'

3.2 系统基础设置

时区、字符集和主机名先处理:

bash 复制代码
timedatectl set-timezone Asia/Shanghai
timedatectl

locale charmap

hostnamectl set-hostname "$NEW_HOSTNAME"
hostnamectl --static

locale charmap 返回 UTF-8 就不再调整语言界面。时钟同步、Swap 和 journald 使用仓库中的小脚本:

bash 复制代码
cd /xqd/infra/mine-linux/server

bash scripts/02-ntp.sh
timedatectl show -p NTPSynchronized --value

bash scripts/06-swap.sh
swapon --show
grep -F '/swapfile-xqd' /etc/fstab

bash scripts/07-journald.sh
systemd-analyze cat-config systemd/journald.conf
journalctl --disk-usage

这台机器只有 2 GiB 内存,因此把全机 Swap 补到 2 GiB。journald 脚本的目标是持久化日志、最多 200 MiB、最长 14 天,并为磁盘预留 1 GiB 空间;实际生效情况还要用上面的命令核对。

3.3 SSH 地址、密钥和主机信任

基础设施域名 infra.xuqiudong.cn 先解析到新云,Jenkins 和小主机都使用这个地址。博客、附件等业务域名继续指向旧云,等迁移完成再切换。

小主机需要把公钥安装到新云:

bash 复制代码
ssh-copy-id root@infra.xuqiudong.cn
ssh root@infra.xuqiudong.cn true

Jenkins 要在它的实际运行账号下单独验证。私钥始终留在发起连接的一方,目标服务器的 authorized_keys 保存公钥。

首次 SSH 连接还会核对服务器身份,并把结果写进发起方账号的 known_hosts。这和公钥免密是两个环节,后面附件备份遇到的 Host key verification failed 就与此有关。

3.4 UFW、云安全组和 WireGuard

UFW 保留最基本的公网入口,SSH 22 端口始终开放:

bash 复制代码
ufw insert 1 allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
ufw status numbered

阿里云安全组开放 22/TCP、80/TCP、443/TCP 和 WireGuard 的 51820/UDP。MySQL 3306、Redis 6379、Meilisearch 7700,以及 Java 后端端口 10086、10088 都不向公网开放。

WireGuard 地址为:云主机 10.10.0.1、家中小主机 10.10.0.2、Windows 10.10.0.3。云主机允许 wg0 上来自 10.10.0.0/24 的入站,并允许 Peer 之间经云主机转发;家中小主机允许同一网段从 wg0 入站。

WireGuard 安装并配置好后,云主机的 UFW 规则为:

bash 复制代码
ufw allow 51820/udp
# 访问云主机本身的服务。
ufw allow in on wg0 from 10.10.0.0/24
# Windows 与小主机等节点之间经云主机转发。
ufw route allow in on wg0 out on wg0 \
  from 10.10.0.0/24 to 10.10.0.0/24

Docker 发布端口的流量路径与普通宿主机服务不同,UFW 不能单独覆盖所有 Docker 转发流量。云端用安全组挡住公网入口,家中小主机使用 DOCKER-USER 规则。详细配置见这三篇记录:

  1. WireGuard 打通云端与家庭网络
  2. 理解 Docker、iptables 与 UFW 的关系
  3. Docker 发布端口的访问控制与持久化

4. 安装 Docker 和中间件

4.1 Docker 安装与统一设置

安装脚本采用阿里云 Docker CE 源,依据阿里云安装手册整理,适合国内访问 Docker 官方源不稳定的环境:

bash 复制代码
cd /xqd/infra/mine-linux/server
bash scripts/10-docker.sh

安装脚本会继续调用 10-docker-config.sh,完成三项设置:Docker Hub 加速地址使用 https://docker.m.daocloud.io;默认日志驱动为 json-file,单文件 10 MiB,保留 3 份;创建外部 bridge 网络 xqd-net。

新服务器没有额外挂载磁盘,因此 Docker 继续使用默认的 /var/lib/docker,没有为了目录形式单独迁移存储位置。

bash 复制代码
docker compose version
docker info --format '{{.DockerRootDir}}'
docker network inspect xqd-net
cat /etc/docker/daemon.json
docker run --rm hello-world

Docker 日志默认值只应用于新建容器。修改后想让旧容器采用新值,需要重建容器。

软件安装源和镜像加速地址是两回事:前者用于安装 Docker,后者用于拉取 Docker Hub 镜像。加速地址以后不可用时,修改 10-docker-config.sh 开头的配置即可。

4.2 MySQL

MySQL 使用 8.0.46,先加载全局变量,再启动 Compose:

bash 复制代码
source /etc/profile.d/xqd-global-env.sh
cd /xqd/docker/mysql

docker compose config --quiet
docker compose up -d
docker compose exec mysql mysql -uroot -p

新主库针对 2 GiB 机器做了适度收缩:

  • server_id=101,与旧主库和家中从库不同。
  • 开启 binlog,格式为 ROW,只记录两个业务库,保留 30 天。
  • innodb_buffer_pool_size=128M,max_connections=50。
  • 关闭 performance_schema 和 MySQL X Protocol,降低常驻内存。
  • innodb_flush_log_at_trx_commit=1、sync_binlog=1,不放宽事务持久化。
  • 数据挂载在 /xqd/docker/mysql/data,配置文件只读挂载进容器。
sql 复制代码
SELECT VERSION();
SHOW VARIABLES WHERE Variable_name IN
('innodb_buffer_pool_size','max_connections','performance_schema',
 'server_id','log_bin','binlog_format','gtid_mode');

本次目标值为 MySQL 8.0.46、128 MiB、50、OFF、server_id=101、binlog ON、ROW、GTID OFF。root 没有设置为远程账号;业务用户和复制用户在数据导入后创建。

配置见 MySQL Compose 和 my.cnf。注意:

  • MYSQL_ROOT_PASSWORD 只在空数据目录首次初始化时设置密码;已有数据时,修改环境变量不会修改数据库密码。
  • docker compose config --quiet 检查配置和变量是否有效,登录查询才是服务验证。
  • binlog 只包含两个业务库;以后新增数据库时,需要同步调整过滤配置。
  • 新云宿主机发布 3306,应用容器始终连接 mysql:3306。

4.3 Redis

bash 复制代码
source /etc/profile.d/xqd-global-env.sh
cd /xqd/docker/redis

docker compose config --quiet
docker compose up -d
docker compose exec redis sh -c \
  'REDISCLI_AUTH="$REDIS_PASSWORD" redis-cli ping'

返回 PONG 表示服务和认证正常。Redis 使用 7.4,开启 AOF,每秒刷盘,不另外做定时 RDB 快照;旧 Redis 数据不迁移。

部署文件为 Redis Compose 和 redis.conf。

容器内端口一直是 6379。博客和附件使用 redis:6379;小主机需要访问时,使用 WireGuard 地址 10.10.0.1:6379。主机发布端口由 REDIS_HOST_PORT 决定,云安全组不开放这个端口。

4.4 Meilisearch

Meilisearch 固定为 v1.54.3,Java SDK 仍使用 0.20.1。本地演练时已经验证建索引和查询可以正常工作。

bash 复制代码
source /etc/profile.d/xqd-global-env.sh
cd /xqd/docker/meilisearch

docker compose config --quiet
docker compose up -d
curl http://127.0.0.1:7700/health
docker stats --no-stream meilisearch

生产模式设置至少 16 字节的 Master Key。索引任务最多使用 128 MiB、1 个线程;容器上限为 512 MiB 内存、1 个 CPU;数据挂载到 /xqd/docker/meilisearch/data,匿名统计关闭,日志级别为 WARN。重建索引时观察实际内存,容器发生 OOM 再调整限额。

部署文件为 Meilisearch Compose 和 meilisearch.toml。

同机博客容器使用 http://meilisearch:7700;跨主机管理使用 http://10.10.0.1:7700。旧索引不复制,应用发布后重新初始化。

5. 安装 Nginx、证书和日志策略

5.1 为什么使用 Nginx 官方仓库

旧云由 Ubuntu apt 安装 Nginx 1.18。新配置中的家庭服务 upstream 使用 resolve 跟随 DDNS 地址变化,这项写法要求开源 Nginx 版本不低于 1.27.3,因此新云改用 Nginx 官方 stable 仓库。

bash 复制代码
apt-get update
apt-get install -y curl ca-certificates gnupg ubuntu-keyring \
  openssl cron logrotate

curl -fsSL https://nginx.org/keys/nginx_signing.key |
  gpg --dearmor --batch --yes \
  -o /usr/share/keyrings/nginx-archive-keyring.gpg

source /etc/os-release
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] https://nginx.org/packages/ubuntu $VERSION_CODENAME nginx" \
  > /etc/apt/sources.list.d/nginx.list

apt-get update
apt-get install -y nginx
nginx -v

云端博客和附件分别代理到宿主机的 127.0.0.1:10086、127.0.0.1:10088。task、wiki、test、duoduo 等家庭服务通过 xuqiudong.us.to 转发。

Nginx 的 DNS 结果缓存 5 分钟,upstream 的 resolve 负责跟随域名地址变化,不检查家中小主机是否在线。家中关机时,对应请求返回 503 页面;不再用脚本每半小时检查域名并反复重启 Nginx。

5.2 使用 acme.sh 签发泛域名证书

acme.sh 继续安装在云主机,因为云主机是主要的证书使用方。泛域名证书使用阿里云 DNS 验证,不要求家中开放 80 或 443。

bash 复制代码
curl -fsSL https://get.acme.sh | sh

source /etc/profile.d/xqd-global-env.sh
/root/.acme.sh/acme.sh --issue --server letsencrypt --dns dns_ali \
  -d "$DOMAIN" -d "*.$DOMAIN" --keylength 2048

mkdir -p /etc/nginx/pem
/root/.acme.sh/acme.sh --install-cert -d "$DOMAIN" \
  --key-file /etc/nginx/pem/key.pem \
  --fullchain-file /etc/nginx/pem/full.pem \
  --reloadcmd 'nginx -t && systemctl reload nginx'

chmod 600 /etc/nginx/pem/key.pem

--install-cert 很关键。Nginx 使用固定的 /etc/nginx/pem 路径,acme.sh 续期后把新证书复制到这里,再执行 reload hook。Nginx 配置不直接引用 acme.sh 内部目录。下载受限时可参考 acme.sh 国内安装说明。

5.3 安装站点配置

bash 复制代码
cd /xqd/infra/mine-linux/server

mkdir -p /etc/nginx/xqd
cp -a config/nginx/conf.d config/nginx/snippets \
  config/nginx/html /etc/nginx/xqd/
cp config/nginx/nginx.conf /etc/nginx/nginx.conf

nginx -t
systemctl enable --now nginx
systemctl reload nginx

apt 安装后 Nginx 可能已经启动;复制新配置后要执行 reload,enable --now 不会让已运行的进程重新加载配置。

配置安装后的目录为:

text 复制代码
/etc/nginx/
├── nginx.conf             TLS、DNS 解析、日志及公共设置
├── pem/                   full.pem、key.pem
└── xqd/
    ├── conf.d/            各站点与家庭 upstream
    ├── snippets/          公共代理参数、503 错误处理
    └── html/              服务不可用提示页

主配置只加载 /etc/nginx/xqd/conf.d/*.conf。这样不会把旧的默认站点、临时文件或遗留配置一起加载。各站点只保留域名、后端、日志和必要差异,重复的 TLS 与代理设置抽到公共配置中。

此时可以检查证书和 Nginx 配置;博客等应用尚未发布,后端访问验证留到第 9 节。

bash 复制代码
/root/.acme.sh/acme.sh --list
openssl x509 -in /etc/nginx/pem/full.pem -noout \
  -issuer -dates -ext subjectAltName
nginx -t

5.4 三种日志分别限制

系统日志交给 journald;容器标准输出交给 Docker 的日志驱动;Nginx 文件日志单独配置 logrotate。它们不会共用一套保留策略。

bash 复制代码
cd /xqd/infra/mine-linux/server

cp config/nginx/logrotate /etc/logrotate.d/nginx
chmod 644 /etc/logrotate.d/nginx
systemctl enable --now logrotate.timer

# 只检查规则,不执行轮转。
logrotate --debug /etc/logrotate.conf
  • access 日志每天轮转,保留 7 份,超过 50 MiB 时可提前轮转。
  • error 日志每月轮转,保留 3 份,超过 20 MiB 时可提前轮转。
  • 文件大小在 logrotate 被调度时检查,不是写到阈值就立即截断。
  • 轮转后向 Nginx 发送 USR1 重新打开日志文件,不重启服务。

5.5 保留必要的定时任务

确认 cron 已运行,查看 root 当前任务,acme.sh 安装器生成的续期任务不重复添加:

bash 复制代码
systemctl enable --now cron
crontab -l
crontab -e
cron 复制代码
# acme.sh 检查是否需要续期,成功续期后执行已保存的 reload hook。
28 0 * * * /root/.acme.sh/acme.sh --cron --home /root/.acme.sh > /dev/null

# 保留每日平滑重载,先检查配置。
15 1 * * * /usr/sbin/nginx -t && /usr/bin/systemctl reload nginx

acme.sh 的 DNS 插件会保存后续续期所需的凭证,因此不能指望 cron 自动读取 /etc/profile.d。首次签发和续期任务要分别验证。本次两项都已经测试。

旧的 wiki-check.sh 不再迁移。家庭小主机关机时,周期性检查脚本没有必要反复重启 Nginx。

6. 在停写之前预同步文件

6.1 新云登录旧云

新云需要主动从旧云拉取附件和静态文件,因此在新云生成独立密钥,并把公钥安装到旧云:

bash 复制代码
# 已有可用密钥时跳过生成,不覆盖。
ssh-keygen -t ed25519

ssh-copy-id -i /root/.ssh/id_ed25519.pub root@旧云公网IP
ssh -i /root/.ssh/id_ed25519 root@旧云公网IP true

id_ed25519 是私钥,只留在新云;id_ed25519.pub 是安装到旧云的公钥。首次连接核对并接受旧云主机指纹。

这里使用旧云固定 IP,不使用即将切换的业务域名,否则 DNS 改完后可能从新云连接到新云自身。

6.2 第一次同步

bash 复制代码
apt-get install -y rsync

OLD_SERVER_HOST='填写旧云公网IP'
mkdir -p /xqd/attachments \
  /xqd/server/nav \
  /xqd/server/duoduo-school-district

rsync -a "root@$OLD_SERVER_HOST:/xqd/attachments/" \
  /xqd/attachments/
rsync -a "root@$OLD_SERVER_HOST:/xqd/server/nav/" \
  /xqd/server/nav/
rsync -a "root@$OLD_SERVER_HOST:/xqd/server/duoduo-school-district/" \
  /xqd/server/duoduo-school-district/

-a 保留权限、修改时间等元数据;源路径末尾的 / 表示复制目录内容。准备阶段不加 --delete,避免删除新云目标目录中已有的文件。

第一次同步完成后,旧站继续运行。切换时再次执行相同命令,大小和修改时间等信息一致的文件会跳过。

7. 切换 MySQL 主库

7.1 当前条件

迁移前,旧云 MySQL 8.0.35 是主库,家中 MySQL 8.0.46 是从库。两个节点的 gtid_mode 都是 OFF,使用 binlog 文件名和位置复制;家中从库数据完整,并且每天另做 dump。

新云使用 MySQL 8.0.46,已经开启 binlog。迁移方法是让家中从库先追平旧云,从家中导出一致数据;新云导入后记录自己的 binlog 坐标,再让家中 MySQL 改为跟随新云。

7.2 停止业务写入并等待追平

先暂停小主机 02:00 的附件同步任务,再停止旧云和小主机中会写业务库的应用、定时任务。旧云 MySQL 保持运行,以便从库拉取最后的 binlog。

旧云记录最终坐标:

sql 复制代码
-- 记录 File 和 Position,待从库执行到同一位置。
SHOW MASTER STATUS;

家中从库检查复制:

sql 复制代码
SHOW REPLICA STATUS\G

需要同时确认:

  • Replica_IO_Running 和 Replica_SQL_Running 都是 Yes。
  • 没有复制错误。
  • Relay_Source_Log_File、Exec_Source_Log_Pos 已到旧云最终坐标。

Seconds_Behind_Source=0 可以辅助判断,但不单独作为追平依据。确认后在家中停止复制,保留已有数据和连接配置:

sql 复制代码
STOP REPLICA;

MySQL 主从是从库主动连接主库拉取 binlog。新主库有自己的 binlog 文件和位置,仅修改域名解析不会自动完成主库切换。

7.3 从家中从库导出

家中小主机执行:

bash 复制代码
# dump 中包含业务数据,限制新文件权限。
umask 077

mysqldump -uroot -p --single-transaction --routines --events --triggers \
  --hex-blob --set-gtid-purged=OFF --no-tablespaces \
  --databases lcxm_backend lcxm_attachment \
  > /tmp/xqd-final.sql

scp /tmp/xqd-final.sql \
  root@infra.xuqiudong.cn:/tmp/xqd-final.sql

参数含义:

  • --single-transaction:获得 InnoDB 一致快照;导出期间不要改表结构。
  • --databases:导出建库语句、表结构和数据。
  • --routines --events --triggers:保留存储过程、事件和触发器。本次两个库没有事件。
  • --hex-blob:二进制字段使用十六进制表示。
  • --set-gtid-purged=OFF:不写入 GTID_PURGED 设置。
  • --no-tablespaces:不导出表空间定义。

这个 dump 不包含 MySQL 用户和权限,账号要在新云单独创建。

7.4 导入新云并创建账号

新云执行:

bash 复制代码
docker exec -i mysql sh -c \
  'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot' \
  < /tmp/xqd-final.sql

docker exec -it mysql mysql -uroot -p

-i 让容器内 mysql 客户端从宿主机标准输入读取 dump。这里使用容器初始化时的 root 密码;如果之后改过数据库密码,需使用实际密码登录。

原来的 qiudong 拥有过多全局管理权限。这次只给两个业务库授权,并单独建立复制账号,以下密码和复制账号名按实际情况填写:

sql 复制代码
CREATE USER 'qiudong'@'%' IDENTIFIED BY '填写应用密码';
GRANT ALL PRIVILEGES ON lcxm_backend.* TO 'qiudong'@'%';
GRANT ALL PRIVILEGES ON lcxm_attachment.* TO 'qiudong'@'%';
SHOW GRANTS FOR 'qiudong'@'%';

CREATE USER 'xqd_repl'@'%' IDENTIFIED BY '填写复制密码';
GRANT REPLICATION SLAVE ON *.* TO 'xqd_repl'@'%';

-- 导入和账号准备完成后,记录新主库自己的 File、Position。
SHOW MASTER STATUS;

'%' 是 MySQL 账号的来源匹配,不代表公网应开放 3306。家中从库通过 WireGuard 访问,公网仍由阿里云安全组隔离。

从导出开始,到新云导入并接好从库之前,业务继续停写。新云和家中从库应保持相同的业务数据,后续才从新主库坐标开始接收新增变更。

7.5 家中 MySQL 改为跟随新云

先从小主机验证 10.10.0.1:3306 可达,再在家中 MySQL执行:

sql 复制代码
CHANGE REPLICATION SOURCE TO
  SOURCE_HOST='10.10.0.1',
  SOURCE_PORT=3306,
  SOURCE_USER='xqd_repl',
  SOURCE_PASSWORD='填写复制密码',
  SOURCE_AUTO_POSITION=0,
  SOURCE_LOG_FILE='填写新主库binlog文件',
  SOURCE_LOG_POS=填写新主库位置,
  GET_SOURCE_PUBLIC_KEY=1;

START REPLICA;
SHOW REPLICA STATUS\G
  • SOURCE_AUTO_POSITION=0 表示继续使用文件和位置,不启用 GTID 自动定位。
  • SOURCE_LOG_POS 填上一步记录的整数,不加引号。
  • GET_SOURCE_PUBLIC_KEY=1 允许获取密码认证所需的服务器 RSA 公钥;传输路径仍是 WireGuard。
  • 小主机已经拥有导出时的完整数据,不需要重新导入。
  • 不执行 RESET MASTER 或 RESET REPLICA ALL。

确认 IO、SQL 线程都为 Yes,没有错误,再在新主库做一次可核对的修改。本次修改已经成功同步到小主机。

7.6 为什么导入后表空间变小

导入后,DBeaver 显示部分表的占用空间比旧库小。我抽查了几个表,行数一致。

mysqldump 是逻辑迁移。导入时会重建表和索引,原数据文件中的空闲空间不会原样复制;客户端显示的表大小和行数还可能来自统计信息。因此,空间变小本身不能说明数据丢失。

核对时应查询实际行数、关键记录,并验证应用查询。本次确认的是抽查表行数一致,没有做全库逐行校验。

8. 最终同步文件并发布应用

8.1 补齐最后的附件变化

旧附件服务停止写入后,在新云再次运行第 6 节的三条 rsync 命令。然后做一次只读检查:

bash 复制代码
# 如果换过 shell,先重新设置为旧云的固定 IP。
OLD_SERVER_HOST='填写旧云公网IP'

rsync -a --dry-run --itemize-changes \
  "root@$OLD_SERVER_HOST:/xqd/attachments/" \
  /xqd/attachments/

这次命令没有输出,表示按 rsync 默认比较规则没有发现需要同步的变化。它不是逐文件内容校验,也不会报告只存在于目标端的多余文件,因为命令没有使用 --checksum 和 --delete。

8.2 Java 容器改用 Docker 服务名

博客和附件都加入 xqd-net。博客使用 application-docker.properties,附件使用 application-prod.properties,中间件连接改为:

properties 复制代码
ph.mysql.host=mysql
ph.mysql.port=3306
ph.redis.host=redis
ph.redis.port=6379
ph.meilisearch.host=http://meilisearch:7700/

MySQL、Redis 配置用于两个应用,Meilisearch 只用于博客。账号、密码和 API key 仍写入各应用自己的生产配置,不在 docker run 中重复维护;浏览器访问的附件地址保持为 https://attachment.xuqiudong.cn。

服务名只在共同 Docker 网络中可用。小主机上的 console 等跨主机程序需要改为云端 WireGuard 地址和对应宿主机端口。

原来的 --add-host=host.docker.internal:host-gateway 可以保留,它与 xqd-net 不冲突;这三个中间件已经不再通过宿主机地址连接。Meilisearch 也不再经过旧 Nginx 84 端口。

如果旧环境中的容器仍要连接宿主机服务,加入新网桥会改变容器来源网段,必须同时核对宿主机服务监听地址和 UFW 放行规则。不能认为只是增加一个 --network 就完全没有访问影响。

8.3 Jenkins 镜像传输增加 gzip

Java 应用继续沿用 Jenkinsfile 中的 docker run。发布地址统一改为 infra.xuqiudong.cn,容器启动时加入 --network xqd-net。

镜像传输调整为:

text 复制代码
docker save
→ gzip
→ scp 镜像.tar.gz
→ 新云解压并覆盖原 tar
→ docker load
→ 更新容器

我测试的镜像包压缩后约为原来的 50%。原来一次 SCP 要 4 到 5 分钟,因此保留了压缩。远端完成解压和 docker load 后,再更新应用容器;传输或解压失败时不先停止运行容器。

博客和附件两条流水线都已经执行成功。附件发布时选择 DEPLOY_CLOUD=true、DEPLOY_LOCAL=false,避免同时重建家中测试容器。

8.4 重建搜索索引

Meilisearch 不搬旧索引。博客启动后执行应用已有的初始化流程,再验证查询。本次初始化成功,页面可以正常搜索。

9. 调整备份来源并切换 DNS

9.1 小主机附件备份改到新云

小主机每天 02:00 拉取云端附件,目标目录不变,只修改远程来源:

ini 复制代码
root@infra.xuqiudong.cn:/xqd/attachments/prod=/xqd/backup/lcxm-attachment/prod

首次运行时报过:

text 复制代码
Host key verification failed.

公钥已经配置好,问题出在 known_hosts。在实际执行定时任务的账号下,先手动 SSH,核对并接受新云主机指纹:

bash 复制代码
ssh root@infra.xuqiudong.cn true

/xqd/mine-shell/backup/attachment-remote-backup.sh
tail -n 30 \
  /xqd/logs/mine-shell/backup/attachment-remote-backup.log

附件备份脚本使用 rsync --delete,目标是云端附件的镜像。它会先通过 SSH 检查远程目录是否存在;连接失败或目录不存在时不启动 rsync。新云数据确认完整后,才恢复 02:00 的任务。

小主机 01:00 的本地目录备份和每日 MySQL dump 不需要修改。

9.2 先用新云验证站点

业务 DNS 还指向旧云时,可以只让一条测试请求走新 IP:

bash 复制代码
nginx -t
NEW_SERVER_IP='填写新云公网IP'

# 保留域名对应的 Host 和 TLS SNI,只替换本次请求的目标地址。
curl -I --resolve "xuqiudong.cn:443:$NEW_SERVER_IP" \
  https://xuqiudong.cn/
curl -I --resolve "wiki.xuqiudong.cn:443:$NEW_SERVER_IP" \
  https://wiki.xuqiudong.cn/

逐项检查:

  1. 博客页面、登录和主要接口。
  2. 附件地址、已有文件和新上传。
  3. Redis 连接、Meilisearch 搜索。
  4. nav、task、wiki 等站点,特别是家庭服务代理。
  5. 新云写入后,家中从库能否同步。

curl -I 只验证 HTTP 响应,不能代替这些业务检查。写入测试限定在新主库,旧云仍保持停写。

9.3 切换业务 DNS

确认数据库复制、附件最终同步和应用发布完成后,在阿里云 DNS 中把业务域名解析到新服务器,再恢复新云业务写入。旧云应用保持停止,避免 DNS 缓存期间两边同时接收写入。

新主库一旦产生新的业务数据,回退就不能只改 DNS。必须先处理新云产生的增量数据,再决定如何恢复旧主库。

10. 验证结果与收尾

10.1 已确认

  • MySQL 两个业务库已经导入,抽查表行数一致。
  • 新主库的修改可以同步到家中从库。
  • 博客和附件两条 Jenkins 流水线发布成功。
  • 旧附件能够查看,附件最终 rsync 检查无待传变化。
  • Meilisearch 索引初始化成功,可以搜索。
  • nav、task、wiki 等站点访问正常。
  • WireGuard 三个节点可以互通。
  • acme.sh 可以签发证书,自动续期任务已经测试。
  • 小主机附件备份来源已经改为 infra.xuqiudong.cn。
  • 业务 DNS 已经切换到新云。
  • 2026 年 11 月 6 日 15:00 收到管局短信,备案信息变更完成。

10.2 收尾清单

  • 实际上传一个新附件,验证写入、下载和后续备份。
  • 检查日志实际轮转后的文件与权限。
  • 核对 journald 实际配置及日志占用。
  • 择机重启整台新云,检查 WireGuard、Docker 中间件、Java 应用和 Nginx 是否恢复。
  • 清理 /tmp/xqd-final.sql 等临时文件。
  • 观察稳定后确认旧云应用停用并下线旧机。
  • 在迁移记录中补充实际 DNS 切换时间。

三个中间件的 Compose 使用 restart: unless-stopped;Java 应用沿用 Jenkinsfile 的重启策略。容器当前运行正常不能替代整机重启验证,这一项需要实际操作后再勾选。

11. 下次迁移如何复用

这次保留了已经跑通的 Jenkins 发布方式,把中间件安装和持久化配置交给 Compose,Nginx 继续在宿主机运行。下次迁移时,不需要重新找安装命令和猜测配置文件在哪里。

mine-linux 仓库保存以下资源:

  1. server/:0–19 执行清单、系统脚本、Compose、Nginx 和全局变量样例。
  2. home/:小主机备份脚本、来源配置和定时任务说明。
  3. docs/migrations/:迁移前环境、实际结果和收尾记录。
  4. docs/blog/:本次迁移文章和网络防护系列。

脚本、配置和完整执行清单保存在 Gitee 仓库 xuqiudong/mine-linux。仓库只提交样例值;服务器密码、阿里云密钥、证书私钥、数据库文件和附件不入库。

下次仍要根据实际环境填写服务器地址和凭证,核对 MySQL 最终坐标、附件差异和 DNS。安装脚本、目录、服务配置、备份任务与检查顺序则可以沿用,修改后再逐项验证。

评论区

avatar
未登录

请围绕文章内容交流,勿发布广告、违法信息或他人隐私。

暂无评论,来发表第一条评论吧~