昨天接入第二个 PVE 节点后,订单一开就炸:HTTP 595,响应体是空的 {"raw":""}。排查了一整天,模板下载了、参数逐个测了、存储桥接都查了——全部正常。最后发现是一个藏在默认参数里的低级错误,而它能潜伏这么久,恰恰是因为「单节点时永远不会触发」。
今天连发四个小版本(v7.1.0 ~ v7.1.3),把这件事彻底修掉。这篇把全过程记下来,因为它暴露的问题比 bug 本身更值得说。
症状:595,没有任何信息
创建容器的日志长这样:
[02:45:54] 部署节点:node2
[02:45:54] 顺序分配 vmid = 202
[02:45:54] 请求 PVE 创建容器(vmid=202,模板=debian-12-standard_12.12-1_amd64.tar.zst)
[02:45:55] ❌ 失败:创建容器失败(595):{"raw":""}
595 不是标准 HTTP 状态码,是 PVE 内部异常时返回的。关键是:响应体是空的,没有 message,没有 errors 字段。而 PVE 节点上连对应的任务记录都没有——说明请求在到达任务调度层之前就被拒了。
排查:先把能想到的都排除
按错误提示的常见原因挨个查:
| 怀疑对象 | 验证方法 | 结果 |
|---|---|---|
| 模板不存在 | 直接查 storage content | ✅ debian-12 模板在(118MB) |
| 模板架构不对 | 列出全部模板 | ✅ 全是 amd64(arm64 的已替换) |
| 参数有问题 | 手工逐组参数 POST 创建 | ✅ 最小参数、加 swap、加 ip6、加 firewall——全部 HTTP 200 |
| 权限不足 | 用同一个 token 手工创建 | ✅ 成功 |
| vmid 冲突 | 查 202 是否存在 | ✅ 不存在 |
全部排除之后,只剩一个可能:请求根本没打到正确的路径上。
根因:一个 id,两种身份
看 createLxc 的代码:
async createLxc({ ..., node = null }) {
const n = node || 'pve' // ← 默认值
const r = await request(`/nodes/${n}/lxc`, 'POST', null, body)
}
而现实是:
我们的 nodeId: node2 ← 控制台里给节点起的名字
PVE 的节点名: pve02 ← PVE 安装时的 hostname
请求打到了 /nodes/pve/lxc
但 node2 上根本没有叫 "pve" 的节点
→ PVE 返回 595(路径不存在走的是内部异常分支,不是 404)
那为什么主节点一直没事?因为主节点的 PVE 节点名恰好就叫 pve,默认值碰巧是对的:
主节点:nodeId='main' PVE 节点名='pve' ← 默认值碰巧命中
node2: nodeId='node2' PVE 节点名='pve02' ← 默认值错了
单节点部署时这个 bug 永远不会触发。加第二个节点的第一天就炸了。
修复:自动探测 + 缓存(v7.1.3)
在 PVE 客户端内部加节点名解析:
let _cachedNodeName = ''
const resolveNodeName = async () => {
if (_cachedNodeName) return _cachedNodeName
const nl = await api('/nodes')
_cachedNodeName = (nl && nl[0] && nl[0].node) || 'pve'
return _cachedNodeName
}
然后把所有用到节点名的方法全部改掉——一共 9 个:
| 方法 | 改前 | 改后 | ||
|---|---|---|---|---|
createLxc |
`node \ | \ | ‘pve’` | 自动探测 |
destroyLxc |
node = 'pve' |
自动探测 | ||
waitForIp |
内部写死 | 自动探测 | ||
power |
写死 | 自动探测 | ||
guestConfig / guestStatus / guestInterfaces |
写死 | 自动探测 | ||
storages / storageStatus / templates / guestRrd |
写死 | 自动探测 |
修完后立刻验证:node2 上两台容器(#202、#205)创建成功,订单状态 active。
顺手修的三件事
① 595 错误信息增强(v7.1.1):创建失败时主动拉最近的 vzcreate 任务日志,从里面提取 error 行拼进错误消息。以后再遇到 595,不用再上 PVE 翻日志。
② 模板校验加固(v7.1.1):原来的模板存在性校验有个吞异常的 catch——templates() 抛错就静默跳过。现在改成双路兜底,遍历存储失败就直接查 local 存储的 content。
③ 节点删除保护(v7.1.2):这是今天另一场小事故。配置里的 pve.nodes 被误清空后,系统不报错,而是静默从老字段 pve.host 兜底生成一个 main 节点——界面上看起来”正常”,实际上 node2 已经消失了。现在:删除节点时强制校验(不能删到没有剩余节点、不能没有主节点)、不能把唯一的主节点降级、启动和热加载时检测配置异常并告警。
学到的两件事
一、「碰巧正确的默认值」是最危险的 bug 温床。 它让所有单节点测试都绿着通过,直到第二个节点上线。凡是「我们的 id」和「外部系统的 id」不是同一个东西的地方,每次调用都要显式转换,不要用默认值兜底。
二、兜底逻辑必须 Fail Soft + Fail Loud。 从老配置字段兜底生成节点列表,保证了老用户升级不炸——这是 Soft;但没有任何日志说「我在兜底」——这不够 Loud。正确的做法是:服务继续跑,但日志里必须留下痕迹,否则排查时方向完全是错的。
今天的四个版本已经全部推到线上(当前运行 v7.1.3),node2 节点正常接单中。接下来排期里的是监控告警和异地备份——毕竟今天这场「配置静默消失」已经证明:不出事不代表没问题,只是还没发现。