用自己写的 ops-mcp 实现一次服务迁移
这篇想记录的其实不只是一次 VPS 迁移,而是这次迁移里我几乎没有手动 SSH 登录过任何一台机器——整个过程是让 AI Agent 调用我自己写的 ops-mcp 完成的。
先交代背景。我的服务架构一直有点尴尬:公网入口在美西(us-yuyoo),应用跑在另一台美西(us-cd),两台机器各自跑 OpenResty,通过 WireGuard 内网互联。隔离入口和业务的思路没错,但两跳延迟对国内访问很不友好。手头刚好多了一台香港高性能线路的机器(hk-yunyoo),干脆把这两个角色合并进去。
集群一共 5 个节点,异构、分布在不同机房,用传统方式迁移意味着开 5 个终端窗口、来回 SSH、记不同的端口和密钥。
ops-mcp 是什么
ops-mcp 是我自己维护的一个 MCP(Model Context Protocol)服务器,把整个集群的运维操作封装成了一套结构化工具。AI Agent 不再是给我贴命令让我复制粘贴,而是直接调用这些工具去读状态、改配置、跑校验。
它覆盖的能力大致分几类:
- 主机层:
server_health、server_exposure_audit、server_ports、server_firewall、server_security_audit这类只读探针,一次调用就能拿到某台机器的资源、暴露面、防火墙状态。 - 容器层:
docker_ps、docker_inspect、docker_logs、compose_ps、compose_config、compose_up等,直接管 Docker 和 Compose 栈。 - 面板层:
onepanel_apps_list、onepanel_websites_list,读 1Panel 里的应用和站点状态。 - 专项:mihomo 代理、服务单元(systemd)管理各一套。
- 兜底:
ops_ssh_exec,没有专用工具覆盖的操作走这个原始通道,但每次调用都会写审计日志。
关键设计是 hostId:每台机器在配置里注册一个 id(us-yuyoo、us-cd、hk-yunyoo……),调用任何工具只需要指定 hostId,不用关心它的 IP、端口、密钥在哪。这个抽象让「对 5 台异构机器做同一件事」变成了循环调用同一个工具换 hostId,而不是 5 段各不相同的手工操作。
详细参见 yuuops-mcp
下面按迁移的实际顺序,讲每个阶段是怎么靠它推进的。


0. 服务器选购
这台香港机器的详细评测我单独写了一篇,这里只交代和迁移相关的选型结论。
为什么是香港
本想考虑日本的机器,但逛了一圈,发现要么性能符合线路不符合,要么差那么一点点(比如联通和移动可用,但电信基本全炸)
思来想去,最终选择香港,大陆地区访问延迟极低,加上这家IDC提供机器性能也符合预期
配置与线路
位置 : 香港九龙
CPU : 4核心 Intel Xeon E5v4 E5-2697
内存 : 4GB DDR4
系统盘 : 30GB SSD
带宽 : 30Mbps 不限流量
网络 : CMI + 4837 + CN2 混合优化
系统选择
Rocky Linux 9.8
完整评测见:YunYoo 香港四区(优化线路产品) 测评
1. 查看服务情况
迁移前得先知道现状。这一步几乎全是只读工具,没碰任何配置。
先列出所有节点确认 hostId 和角色:
ops_hosts_list
然后对旧节点逐个体检。server_health 看资源,docker_ps 看容器,server_exposure_audit 看公网暴露面。最后这个直接暴露了旧架构的几个问题:
| 节点 | 问题 |
|---|---|
| us-cd | MySQL 发布到 0.0.0.0:3306,公网直连 |
| us-yuyoo | File Browser 40071 公网直出 |
| us-nfs | NFS 导出走公网 IP,且 no_root_squash |
数据体量也是通过工具查的,ops_ssh_exec 跑 du 拿到实际占用:
| 数据 | 体量 |
|---|---|
| MySQL 数据 | 约 225M |
| Halo 本地数据 | 约 42M |
| Halo NFS 附件 | 约 19M |
结论很快就清晰了:数据量根本不是瓶颈,真正的复杂度在服务边界重组和安全收缩。这个判断能几分钟内下出来,是因为一次会话里就把 4 台机器的状态全查了一遍,不用在终端之间反复横跳。
2. 不合并的业务
有人可能会问:既然合并,为什么不顺手把 us-nfs 也迁进香港?
最大原因:没钱
回归正题,备份节点的核心价值就是业务主机出事时还能找回东西。把备份塞进 hk-yunyoo,一次主机故障同时打掉业务和恢复能力,备份就没意义了。所以 us-nfs 原地不动,hk-yunyoo 通过 WireGuard 走内网挂它,作为冷库,热备还是在本服务器上
目标端口策略也在这时确定:
- OpenResty 公网 80/443
- MySQL 只绑
127.0.0.1:3306 - Halo 只绑
127.0.0.1:8090 - File Browser 只绑
127.0.0.1:40071,通过反代访问 - NFS 走 WireGuard
10.77.0.5 -> 10.77.0.3
3. 新机上线并纳入 ops-mcp
hk-yunyoo 装完系统和基础组件(Docker、1Panel、WireGuard、NFS client)后,第一件事是把它加进 ops-mcp 的主机清单:
注:以下涉及隐私信息均为修改过的,并非实际信息
ops_host_add
id: hk-yunyoo
host: 203.0.113.10
roles: onepanel,docker,compose,systemd
加完立刻用工具确认它可达、角色识别正确:
server_status hk-yunyoo
server_health hk-yunyoo
server_exposure_audit hk-yunyoo
docker_ps hk-yunyoo (all=true)
系统层的基础配置(改 hostname、时区、SSH 密钥、sshd_config)走 ops_ssh_exec,因为这些是一次性的底层动作,没必要做成专用工具:
hostnamectl set-hostname hk-yunyoo
timedatectl set-timezone Asia/Shanghai
sshd_config 检查:
PubkeyAuthentication yes
PasswordAuthentication no
PermitRootLogin prohibit-password
4. WireGuard 和 NFS
这两块是典型的「没有专用工具、靠 ops_ssh_exec 兜底,但用探针工具做校验」的组合。
WireGuard 新机用 10.77.0.5,不复用旧地址,方便回滚期间新旧节点并存。密钥生成和配置通过 ops_ssh_exec 写入,hk-yunyoo 的 wg0.conf 把 us-nfs、Zgo-us 加为 peer,同时在 us-nfs 上把 hk-yunyoo 加为 peer 并放行 10.77.0.5/32。
关键在校验环节,这里 ops-mcp 有现成工具:
server_ping hk-yunyoo --target 10.77.0.3 # 新机 -> us-nfs
server_ping us-nfs --target 10.77.0.5 # us-nfs -> 新机
proxy_connectivity_test hk-yunyoo # DNS/IPv4/IPv6/HTTPS 一次跑完
双向能通之后再做 NFS,us-nfs 上新增导出目录和 exports 条目(root_squash,比旧的 no_root_squash 安全),hk-yunyoo 上挂两个点:
/data/nfs-root -> 10.77.0.3:/srv/nfs
/data/hk-yunyoo -> 10.77.0.3:/srv/nfs/hk-yunyoo
fstab 里带 nofail,_netdev,vers=4.2,挂载和写测试都通过 ops_ssh_exec 验证:
mount -a
df -h /data/hk-yunyoo
touch /data/hk-yunyoo/.write-test && rm -f /data/hk-yunyoo/.write-test
5. 应用部署
应用层是 ops-mcp 的容器工具重要的地方。策略是新机用 1Panel 装同版本应用,再迁数据,不做整机 /opt/1panel 覆盖。
装完 MySQL 8.4.10、Halo 2.25.4、OpenResty、File Browser 后,先用工具校验 compose 配置是否符合预期,尤其是 MySQL 的端口绑定:
compose_projects hk-yunyoo
compose_config hk-yunyoo --project mysql
compose_config 会返回合并后的实际配置,确认端口是 127.0.0.1:3306 而不是 0.0.0.0 再启动。如果 Halo 走 Docker 网络访问 MySQL,宿主机端口可以完全不发布。
Halo 的配置改两处,这是最容易踩的坑:
# 旧
--halo.external-url=http://localhost:8090
# 新
--halo.external-url=https://blog.example.com
external-url 不对,Halo 生成的所有附件链接会指向 localhost。
启动和排障也走工具,不用手动 docker 命令:
compose_up hk-yunyoo --project halo
docker_logs hk-yunyoo --container <halo容器> --tail 200
docker_ps hk-yunyoo
容器起没起来、健康检查过没过,docker_ps 跑一次就知道
6. 数据迁移
数据搬运本身是 rsync + mysqldump 的经典组合,命令通过 ops_ssh_exec 在源机和目标机之间执行。
预同步阶段(不停业务)先把静态数据推过去:
rsync -aHAX --numeric-ids /opt/1panel/apps/halo/halo/data/ \
root@203.0.113.10:/opt/1panel/apps/halo/halo/data/
rsync -aHAX --numeric-ids /data/us-cd/halo/ \
root@203.0.113.10:/data/hk-yunyoo/halo/
最终切换窗口:DNS TTL 提前降到 300,旧入口切维护页停写,停旧 Halo,导出 MySQL:
docker exec <旧MySQL容器名> \
mysqldump -u root -p"${MYSQL_ROOT_PASSWORD}" \
--single-transaction --quick --routines --triggers --events \
--all-databases > /data/us-cd/backups/mysql/final/final.sql
导入新机后做最终差量同步,再启动应用。切 DNS 前用 --resolve 绕过 DNS 直接打新机验证:
curl -I --resolve blog.example.com:443:203.0.113.10 https://blog.example.com/
首页 200、后台能登、附件能传、Halo 链接是正式域名,这几项都过了才修改域名解析
7. 备份
旧节点的备份靠散落的 crontab,没有统一界面。这次全部迁进 1Panel 计划任务,脚本放 /usr/local/sbin/ 并同步到 1Panel 脚本库。
MySQL 备份脚本从 .env 读密码,不明文写在 crontab:
#!/usr/bin/env bash
set -euo pipefail
D="$(date +%Y%m%d)"
BASE="/data/hk-yunyoo/backups/mysql/${D}"
mkdir -p "${BASE}"
set -a; . /opt/1panel/apps/mysql/mysql/.env; set +a
MYSQL_CONTAINER="${MYSQL_CONTAINER:-1Panel-mysql-hk}"
MYSQL_PASSWORD="${PANEL_DB_ROOT_PASSWORD:-${MYSQL_ROOT_PASSWORD:-}}"
docker exec "${MYSQL_CONTAINER}" \
mysqldump -u root -p"${MYSQL_PASSWORD}" \
--all-databases --single-transaction --quick --routines --triggers --events \
> "${BASE}/all-databases.sql"
gzip -f "${BASE}/all-databases.sql"
三个计划任务:02:00 MySQL 备份,02:30 文件备份,04:00 清理 7 天前的旧备份。备份到 NFS,独立于业务节点。
8. 安全检查与最终验收
这是 ops-mcp 价值最直接的一步。安全检查做完之后,验证「某个端口到底还能不能公网访问」不用猜,直接跑审计工具:
server_exposure_audit hk-yunyoo
server_security_audit hk-yunyoo
ssh_security_audit hk-yunyoo
server_exposure_audit 把监听端口、公网 IP、防火墙规则一次性列出来,我能明确看到 3306、8090、40071 都不在公网监听列表里。同样的工具指向旧节点,就能确认 us-yuyoo 的 40071 和 us-cd 的 3306 公网直出已经关掉。
最终状态:
| 服务 | 状态 | 端口策略 |
|---|---|---|
| OpenResty | 运行中 | 公网 80/443 |
| MySQL | 运行中 | 仅 127.0.0.1:3306 |
| Halo | healthy | 仅 127.0.0.1:8090 |
| File Browser | healthy | 仅 127.0.0.1:40071 |
DNS 已生效,blog.example.com 和 files.example.com 都指向 203.0.113.10,HTTPS 200,证书通过 1Panel + Let's Encrypt + Cloudflare DNS 自动续签。
9. 回退方案
DNS 快速回退:新机 HTTPS 异常或线路不达标时,A 记录切回 198.51.100.11(us-yuyoo),确认旧 Halo/MySQL 正常
如果 DNS 切新机后已经有写入,不能直接切回旧库(会丢数据),得先从新机导出增量再导回旧机。旧节点保留计划:24 小时完整保留,72 小时停用不删数据,7 天后确认备份和恢复测试通过再退役。
10. 用下来的一些感受
最直接的感受是效率大大提高,一共花了1h多完成全部迁移工作。
然后就是让工具来让判断。数据检查、安全检查、查容器健康这些,以前是一堆零散命令加肉眼比对,现在是几次结构化调用,返回的数据能直接处理。迁移前那个「数据量不是瓶颈」的判断,就是这么几分钟得出来的。
当然像 WireGuard 配置、NFS exports、sshd_config 这种底层文本编辑,目前还是靠 ops_ssh_exec 兜底,没有专用工具,本质上还是在跑裸命令,只是多了一层审计日志。这部分的体验和纯手工 SSH 差别不大。
