← 返回首页

把这个 VPS 系统搬到新服务器:依赖清单、六步落地,和三个真踩过的坑

上一篇记的是把这套 PVE + RouterOS 的 VPS 自助开通系统搭起来的过程。系统能跑了,但"能不能搬到别的机器上"是另一个问题——一个只有原作者会部署的系统,不算真正交付。这篇把迁移需要的依赖、顺序、验收标准整理清楚,同时把我在"搬"这件事上真踩过的三个坑写下来。其中第一个,把宿主机上的生产数据库整个清空了。

*

一、先回答最常被问的那个问题:要不要装"面板"

不用。这套系统的"面板"就是自己写的那份代码——/opt/vps 一个目录,加一个 systemd 服务,监听 8080。它直接指挥虚拟化层干活,中间没有中间商,所以不需要 SolusVM、Virtualizor、WHMCS 那一类东西。

但有一个硬约束必须先说清楚:

底座必须是 Proxmox VE,不能是裸 KVM / libvirt。

原因是 vpsctl.py 里 44 处直接调 qm(PVE 的命令行),另外还有 pvesm、qemu-img。这些是 PVE 自己的封装,裸 KVM 上没有。真要上裸 KVM,得把这一层全部重写成 virsh / qemu 直调,顺带还要自己做「模板克隆、cloud-init 注入、磁盘扩容、快照」这些 PVE 已经替你做完的事,成本不划算。

顺带说一句:PVE 本身就带 Web 管理面板(https://IP:8006),所以从"能管虚拟机"这个角度,你本来就是有一个面板的,我们要加的只是它上面那层业务。

二、依赖清单:三层,只有中间一层要你动手

依赖栈分层:自研面板 / Proxmox VE 底座 / 虚拟机与网络 / 额外 apt 包

第一层,底座。 Debian 12 + Proxmox VE 8.x(当前是 8.4.21),带 qemu-server 8.4.10、pve-qemu-kvm 9.2.0 和 KVM 内核模块。用 PVE 官方 ISO 装最省事,或者在 Debian 12 上按官方文档加 PVE 源。

第二层,宿主机上额外要装的 apt 包——就这几个,没有更多:

  • sshpass:交付判定要用它真的用密码 SSH 登一次,不能用"端口开着"代替
  • xorriso + genisoimage:构建 Windows 母盘时做软盘镜像和辅助光盘
  • wimlib-imagex:备用,当前流程没直接用到
BASH
apt install -y sshpass xorriso genisoimage wimlib-imagex

其余的命令(qm / pvesm / qemu-img / ip / curl / wget / sqlite3)随 PVE 或 Debian 自带,不用管。

第三层,业务运行时(不是 apt 包,是要建的东西):RouterOS CHR 虚拟机、两个金镜像模板、两张网桥。

明确不需要的东西,列出来比列需要的更有说服力:

  • ❌ 任何 Python 第三方包。控制端 100% 标准库(http.server / sqlite3 / hashlib / socket…),没有 Flask、没有 paramiko。我用语法树扫过一遍确认,第三方依赖为空。
  • ❌ Nginx / Apache。面板自带 http.server 直接监听 8080。要上 HTTPS 再另外加反代。
  • ❌ MySQL / PostgreSQL。存储是 SQLite 单文件。
  • ❌ Node.js 或任何前端构建工具。页面是纯静态 HTML。
这套"全标准库"的收益在迁移时体现得最明显:目标机器上只要有 python3,拷过去就能跑,不存在装依赖装到一半版本冲突这种事。

三、包里没带、要另外准备的五个大件

迁移包里装的是代码和脚本,下面这些体积大、有授权或来源要求的东西得自己备:

  • PVE 安装 ISO(约 1.5 GB)——装底座
  • Debian 12 通用云镜像(约 350 MB)——烘焙模板 9000,build_template2.sh 会校验 SHA512
  • Windows Server 2019 官方 ISO(5.2 GB)——烘焙模板 9001,必须用官方盘,别用网上流传的 DD 镜像
  • virtio-win.iso(约 700 MB)——只为从里面取 vioserial 驱动,这是 Windows 客户机唯一必须注入的 virtio 驱动
  • RouterOS CHR 镜像(64 MB)——chr-6.49.23.img,做 VM100
只跑 Debian 客户机的话,Windows 那两件可以先不准备,链路照样通。

四、六步落地

迁移六步:装底座配网络 → 装 apt 包 → 建 RouterOS → 烘焙模板 → 改配置部署面板 → 跑回归

第 1 步:装底座 + 配网络。 Debian 12 + PVE 8,配 br0 桥接和静态公网 IP。这里有个只在这类"无带外通道"的机器上才成立的前提——改网络之前先装好看门狗(我这边用的是自写的 netguard 服务),否则 /etc/network/interfaces 一配错就直接失联,而这台机器没有 IPMI、没有 VNC,救不回来。

第 2 步:装那三个 apt 包。 就上一条命令。

第 3 步:建 RouterOS + 内网桥。 VM100 跑 RouterOS CHR,网卡桥到 br0 拿 WAN,另一条腿桥到内部网桥 vmbr1 做 LAN。端口映射(dstnat)和 DHCP 静态租约都在它身上。vmbr1 特意不挂物理口,只给虚拟机用。

第 4 步:烘焙两个模板。 Debian 走 build_template2.sh,Windows 走 win_build.py。这一步最慢也最容易出岔子,Windows 母盘从零到能用大概要小一个钟头。

第 5 步:改配置 + 部署面板。 所有可调参数都集中在 config.py 一个文件里,换机器只需改它:公网 IP、三个资源池(VMID / 内网 IP / 公网端口)、RouterOS 的地址和账号、模板 VMID。改完推 /opt/vps,装 systemd 单元。别忘了把 admin_key 和 admin_key.pub 一起带过去——它既是免密登录工具,也是灾难恢复时唯一能把客户机密码找回来的手段。

第 6 步:跑回归验收。 三个套件,见下面第六节。

五、三个真踩过的坑

三个坑有个共同点,先说结论:都不是"逻辑写错了",而是"接口约定没对齐"——文件名、枚举值、返回值语义。它们在前端回归里全都测不出来,只有跑一次真实动作才会暴露。

三个真 BUG:现象 / 根因 / 改法 对照

坑一:部署脚本把生产库清空了

现象:面板一刷新,机器列表全空,几百条记录瞬间归零。

排查:好在部署前顺手 cp 了一份数据库备份。一看那个备份文件——4096 字节,而且 sqlite_master 里一张表都没有。也就是说,备份的那个时刻,主库就已经是个空壳了。

顺着往下查,根因是这个:

  • 源码目录里曾经跑过一次自检脚本,SQLite 连接时顺手在源码目录建了一个空的 vps.db(4 KB,没表)
  • 而部署脚本干的事是"整个目录推上去"
  • 于是每次部署,都在把宿主机上那个活库覆盖成空壳
清理逻辑本身没问题,问题是推送范围没有边界——把"运行时会生成的文件"当成了"要部署的代码"。

改法:给部署脚本加了运行时文件排除名单,白名单式的思路反过来用:

PYTHON
SKIP_DIRS  = {"__pycache__", ".git", ".stage", "node_modules", ".workbuddy"}
SKIP_FILES = {"vps.db", "vps.db-wal", "vps.db-shm", "vps.log",
              "admin_key", "admin_key.pub"}
SKIP_SUFFIX = (".pyc", ".pyo")

vps.db 是数据、vps.log 是日志、admin_key 是私钥——这三类都不该被覆盖,也不该被上传。顺手把源码目录里那个空壳也删了,从源头断掉。

恢复:数据没了,但系统还在——这是个关键判断。因为同一份事实在三处留了痕,可以互相印证:

一次部署如何把生产库清空,以及怎么靠两条独立证据把记录重建回来

  1. 每台机器的 PVE 配置里,有开通时写进去的 #VPSID=n USER=n 注释,直接给出了原来的记录主键
  2. RouterOS 的 dst-nat 规则注释里有「VPS n」,给出了公网端口和内网目标地址
  3. 内网 IP、MAC、CPU/内存/磁盘规格,PVE 配置里全都有
写了个恢复脚本,照着这三处把 vps 表重建出来,再用注入到客户机的 admin_key 重新设一个已知密码——两台机器都原地复活,客户机里的数据一点没动。
最值钱的一条经验:数据库不是唯一的真相来源。虚拟化层和网络层各自持有一份"设备事实",只要它们还在,业务记录就能反推。但这要求开通的时候就把关联信息写进那里——#VPSID=n 这种注释平时看着多余,出事的时候就是救命的那行字。

坑二:一个参数名写错,开通 100% 失败

现象:Debian 机器下单后 1 秒内就失败,刚克隆出来的虚机立刻被关机回收。

根因:我把 vga 参数写成了 virtio-gpu。

这个值是 QEMU 的设备名,但 qm set --vga 走的是 PVE 自己的枚举:

TEXT
cirrus | qxl | qxl2-4 | none | serial0-3 | std | virtio | virtio-gl | vmware

里面没有 virtio-gpu。所以 qm set 直接返回 400:

TEXT
400 Parameter verification failed.
vga.type: value 'virtio-gpu' does not have a value in the enumeration ...

要命的是这个错不会让整条流程体面地失败——它埋在 PVE 的参数校验里,日志上只看到"开通失败",一眼看不出是哪个参数。而且前端回归测不到它,因为前端回归不真的开机器。

改法:改成 virtio,并加了一层取值收敛,任何不在枚举里的值都退回 std,绝不把非法值喂给 qm。另外把母盘构建脚本里同名的错误也一并改了——那份如果跑起来,模板会直接烘焙失败。

教训:改开通参数,必须跑一次真实开通。 这个坑我上一轮就埋下了,当时因为只跑了前端回归和页面检查,它安安静静躺了一整轮才发作。凡是"参数透传到外部系统"的地方,本地测试永远测不完整。

坑三:把返回值当成了 ID

现象:机器明明开好了、跑起来了,订单却判失败、钱也退了——等于白送一台机器。

根因:create_vps() 返回的是整条 vps 记录的 dict,我在销售层却当成整数 ID 用:

PYTHON
vid = vpsctl.create_vps(...)     # 返回 dict
vpsdb.update_order(order_id, vps_id=vid)   # dict 一路进了 SQL 参数

SQLite 直接报:

TEXT
Error binding parameter 1: type 'dict' is not supported

异常被外层的 except 接住,走退款 + 标记订单失败。但异常发生时,机器已经开好了。

改法:取 ID 的时候把两种返回都处理掉:

PYTHON
res = vpsctl.create_vps(...)
vid = res["id"] if isinstance(res, dict) else int(res)
这个坑的危险性在于它不会报错——它会安静地"成功失败"。系统认为订单失败了,用户却拿到了一台正在运行的机器。凡是"扣了钱又要退钱"的分支,都值得单独跑一遍验证。

六、迁移后的验收清单

迁移最容易糊弄的地方就是验收——"页面能打开"不等于"系统能用"。这套系统有三层验证,判据全部是真实业务动作:

  • e2e_test.py(22 项) —— 登录 → 开通 → SSH 真实密码登录 → 关机/开机/重启/重装 → 销毁。这一套验的是底层开通引擎。
  • panel_browser_check.py(47 项) —— 用无头浏览器真实点一遍面板:登录、渲染机器卡片、打开 VNC 监控、判定画布上出现非黑像素、拖动窗口、密码显示/复制、各操作的二次确认弹窗。这一套验的是前端交互。
  • e2e_store.py(22 项) —— 建支付方式 → 提交充值申请 → 人工确认入账 → 算价 → 下单扣款 → 自动开通 → 订单回填 → 续费扣款 → 到期顺延 → 删除回收。这一套验的是销售链路。
三套跑完,外加一条平台自查(模板存在 / 存储可用 / RouterOS 连通 / 网桥就绪 / 剩余 IP 池),迁移才算收工。

两条我认为不能省的判据,列出来备查:

  1. SSH 交付判定必须是"用密码真的登进去",不是"22 端口通"。端口通只能说明 NAT 建好了,客户机里的 sshd 可能压根没起来。
  2. VNC 判定必须是"画布上出现非黑像素",不是 noVNC 的 connect 事件。后者只代表 RFB 握手完成,第一帧还没到,此时截图必然是黑底。

七、最后:迁移这件事的三条经验

  1. "能跑"和"能搬"是两件事,后者需要显式的清单。 这套系统我写得很自包含(全标准库、配置集中在一个文件),但如果不是把依赖清单和顺序写下来,半年后我自己搬第二台机器照样会漏东西——事实也确实漏了,漏的那一项把数据库清空了。
  2. 部署脚本要有边界。 把"运行时会生成的文件"和"要部署的代码"混在一个目录里,是这类事故的温床。数据、日志、密钥这三类文件,永远不要参与"整目录同步"。
  3. 每个关键动作都要在别处留下可反推的痕迹。 #VPSID=n 是随手写的注释,RouterOS 规则的「VPS n」也只是为了好认。但它们共同构成了一张跨系统的对账网——当中心化的数据库出问题时,只有这种东西能把你捞回来。
*

相关:上一篇《PVE + RouterOS CHR 自建 VPS 自助开通系统:从 Web 开户到 Debian / Windows 双系统交付全记录》记录了这套系统从零搭起来的过程,包括 cloud-init 不给 root 留门、一次硬关机把客户机 sshd 写死、以及微软官方安装盘 UDF 那堵墙。

本文只讲方法与依赖,不含任何服务器凭据、口令或密钥。

💬 评论 (5)

今日还可评论 3 条
发的口吻还是AI
IP 112.18.177.3 · 📍 四川省 · 资阳市 · 🏠 中国移动 · 🕑 15 天前
发一条就是2条?
IP 112.18.177.3 · 📍 四川省 · 资阳市 · 🏠 中国移动 · 🕑 15 天前
11
IP 183.221.39.86 · 📍 四川省 · 成都市 · 🏠 中国移动 · 🕑 15 天前
111🤪🤫
IP 117.176.0.166 · 📍 四川省 · 资阳市 · 🏠 中国移动 · 🕑 15 天前
你发一条还是两条
IP 39.144.141.200 · 📍 中国 · 🏠 中国移动 · 🕑 15 天前