云资源成本控制:从创建到释放的完整清单,和 5 个"假失败"的删除
云资源成本控制:从创建到释放的完整清单,和 5 个"假失败"的删除
十月底那轮清理,KooCLI 日志里连打了 6 条 ERROR,红色一片。把日志还原成台账之后,真实错误只有 1 条,另外 5 条是"资源早就不在了"和"它本来就绑着"。按退出码统计,这次清理是 5 次失败;按资源状态统计,它是干净的。
这个差别不是文字游戏。账单只认资源是否存在,不认日志里的 ERROR 两个字。
一、三种"看起来停了,其实还在计费"的状态
第一种是没绑定的弹性公网 IP。官方计费说明写得很清楚:按需计费的 EIP 未绑定任何实例时,仍收取保有费;按秒计费、每小时整点结算,计费起点是创建成功时刻,终点是释放时刻。关机、解绑、闲置都不构成终点。
第二种是按需 ECS 的"关机"。官方 FAQ 的计费策略表分得很细:普通实例关机后 vCPU、内存、镜像、GPU 不再计费,但系统盘、数据盘、EIP、带宽照常计费;含本地盘、FPGA 卡或裸金属的特殊实例,关机后连计算资源都继续计费。所以"关机省钱"只对普通实例成立,而且只省掉一部分。
第三种是删除动作的两个默认参数。这一条最容易被忽略,因为它藏在命令默认值里。
二、DeleteServers 的两个 false
本机 KooCLI 元数据里,DeleteServers 的 delete_publicip 和 delete_volume 都是可选参数,默认值 false,含义分别是"仅解绑,保留 EIP"和"仅卸载,保留数据盘"。于是一条看起来很干脆的释放命令,默认会给你留下一个继续收保有费的 IP 和一块继续计费的盘:
# 默认写法:实例没了,IP 和盘还在计费
hcloud ECS DeleteServers --cli-region=cn-north-4 \
--servers.1.id=<server-id>
# 临时验证机:连 IP 带盘一起删
hcloud ECS DeleteServers --cli-region=cn-north-4 \
--servers.1.id=<server-id> \
--delete_publicip=true --delete_volume=true
需要强调的是,--delete_publicip=true 只处理随实例创建的那一个公网 IP。事后单独申请再绑上去的 EIP,仍要走 EIP 自己的释放路径。
三、删除顺序:先解绑,再删
DeletePublicip 的接口描述只有一句话:"删除弹性公网IP,绑定状态eip不允许直接删除。"这类前置条件不会写在参数里,只会在删除时以一个 400 系列返回。顺序本身是一张依赖图:
# EIP:解绑 → 释放
hcloud EIP DisassociatePublicips --cli-region=cn-north-4 \
--publicip_id=<eip-id>
hcloud EIP DeletePublicip --cli-region=cn-north-4 \
--publicip_id=<eip-id>
# VPC:规则 → 安全组 → 子网 → VPC
hcloud VPC DeleteSecurityGroupRule --cli-region=cn-north-4 \
--security_group_rule_id=<rule-id>
hcloud VPC DeleteSecurityGroup --cli-region=cn-north-4 \
--security_group_id=<sg-id>
顺序错一步,得到的往往不是报错,而是"删了一半":安全组删不掉,规则还在,VPC 里留着子网。这些残留大多不计费,却会让下一次盘点无法收敛,也让"到底清干净没有"这个问题永远没有答案。
四、6 条失败,5 条是"目标状态已达成"
10 月 23 日 00:13 到 00:26 的日志,是一台临时机的收尾过程。把调用还原成台账:
| 时间 | 状态码 | 错误码 | 判定 |
|---|---|---|---|
| 15:08:35 | 404 | STS5.1106 | 真错:委托不存在 |
| 00:13:17 | 409 | EIP.7922 | 已绑定,目标状态已达成 |
| 00:13:24 | 409 | EIP.7922 | 6.6 秒后重试,结论一致 |
| 00:26:09 | 404 | EIP.7904 | 已删除,目标状态已达成 |
| 00:26:45 | 404 | VPC.0602 | 规则不存在,目标状态已达成 |
| 00:26:46 | 404 | VPC.0602 | 1.6 秒后第二条规则同样不存在 |
从"还在绑"到"查不到",中间隔了 765.667 秒,12.8 分钟。按 EIP 按秒计费的规则,这 12.8 分钟是实实在在计费的,尽管它远不足一个结算小时。
写这个台账脚本,我改了三版才跑对,三版都错在同一个判断上——把结构化日志当纯文本处理:
- 第一版:逐行
startswith('{')后直接json.loads。日志里的响应体是多行 JSON,四行里三行解析失败,6 条记录全部没有错误码,脚本判定"6 条真实错误"。 - 第二版:改成累计花括号配对。仍然 0/6。因为响应体首行是
2026-10-23 00:13:17.654 ERROR [EIP][AssociatePublicips/v3] - {——日志头和左花括号在同一行,按行切分时这一行被当成新的调用,缓冲区被立刻清空。 - 第三版:把首行连同日志头一起喂进缓冲区,解析成功了,但错误码挂在了那条"没有状态码的伪调用"上,台账里还是 6 条
None。第四版把code写回上一条带状态码的记录,才得到 5/6。
三次失败都不是语法问题,而是对"日志是半结构化文本"这件事的判断错误。任何按 ERROR 关键字 grep 出来的统计,都会在这类日志上给出偏高的失败数。
五、判据换成资源状态,而不是退出码
BENIGN = {"EIP.7904", "EIP.7922", "VPC.0602"}
def classify(exit_code, body):
if exit_code == 0:
return "done"
code = body.get("error_code") or body.get("code") # EIP 用 error_code,VPC 用 code
if code in BENIGN:
return "already" # 目标状态已达成,不算失败
return "failed"
注意那个 or:EIP 的错误体字段是 error_code,VPC v2 的错误体只有 code。只读其中一个字段的脚本,会把一类 404 记成失败,把另一类记成未知。
真正的"已释放"判据只有两个:Show 类接口返回 404,或者列表接口的 count 归零。删除命令返回 0 不算证据——删除是异步的,接口先返回,资源后消失,中间那段时间还在计费。
六、把清单前移到创建时刻
清理永远是补救。能用的护栏只有三样:
- 标签。创建时打上
expire-at和owner,盘点时按标签筛,而不是按资源名字猜。名字是给人看的,标签是给脚本看的。 - 盘点脚本。本机 CLI 元数据里共存 790 个操作,名字带 Delete 或 Remove 的有 107 个,分布在 5 个服务里,其中 VPC 一个服务就占 45 个。这意味着"删干净"不是一个动作,而是一张按依赖排序的表。
- 账单侧交叉验证。bss 服务有 92 个操作,
ListResourceUsage、ListCustomerOnDemandResources、ListCosts都能取到按需资源明细。资源列表和账单列表对不上的部分,就是漏网的那部分。
结论
成本控制真正起作用的位置,是创建资源时的那一个默认参数和一次打标签;释放阶段的清单只是把已经发生的浪费截断。少写一个 --delete_publicip=true,付出的不是运维时间,而是一张一直开到下个月的账单。
- 点赞
- 收藏
- 关注作者
评论(0)