72 个沙箱每天花掉近五百元,对账接口却只能看到 50 个

Flashduty AI SRE 技术揭秘第 16 篇:从 72 个孤儿沙箱的清理经历,解释远端反向对账、完整分页、部署归属标签与 15 分钟宽限期如何减少遗漏和误删。

作者 快猫星云

接口返回了一张列表,不代表已经列完所有资源。

Flashduty AI SRE 是一个专注 SRE 领域、帮你做故障根因分析、日常运维工作的 AI Agent 产品,注册即可免费试用。这是系列文章《Flashduty AI SRE 技术揭秘》第 16 篇,本系列会持续分享我们在 agent 工程上的真实设计、踩过的坑和做过的取舍。

核心要点

  • 2026 年 7 月 13 日的清理涉及 72 个无有效会话认领的沙箱,原稿记录其在当时阿里云 FC 运行方式下每天产生约 492 元费用;旧版接口起初只返回 50 个。
  • 数据库回收看不到未入库资源,需要从厂商侧反向对账;分页未遍历完整、达到页数上限或状态查询失败,都不能报告为全量盘点完成。
  • 自动回收同时要求无有效数据库记录、带本部署 owner 标签、创建超过 15 分钟;身份或时间不明则保留,后续操作回到实例的原厂商。

2026 年 7 月 13 日,我们人工清理出一批发布测试遗留的云沙箱,共 72 个。它们在阿里云 FC 上每天产生约 492 元费用,却没有任何有效会话能够认领。更麻烦的是,清理时调用列表接口,起初只能看到其中 50 个。

剩下的 22 个并没有被暂停计费,也没有被另一个系统接管。我们使用的旧版列表接口,在结果超过 50 条时会静默截断。请求成功,返回值也是正常数组,但它没有把全部实例交出来。

这篇讲两件相连的事:没有数据库记录的沙箱,为什么能一直存在;以及加上对账之后,为什么还必须确认自己拿到的是完整清单。

沙箱还在,平台的账上却没有它

agent 要执行命令、处理文件,需要一个运行环境。云沙箱就是按需创建的执行环境,用完后可以暂停或销毁。正常情况下,平台数据库会记下沙箱属于哪个账号、哪个会话、由哪家厂商承接,以及当前状态。

所谓孤儿沙箱,就是厂商侧仍有实例,平台却没有有效记录与之对应。它可能仍占着资源、继续产生费用,但按会话查找时找不到它;从数据库挑选待回收对象时,它也不会出现。

这次事故之前,回收程序主要沿着数据库记录工作:找出闲置实例,再调用厂商接口处理。对于有记录的沙箱,这条路径能运行;对于从没入过库的实例,无论扫描多少次,都不会看见。

事故中的一批实例来自版本发布时的压测工具。工具直接调用云厂商 API 创建沙箱,绕过了平台正常的创建和登记流程,结束后又没清干净。平台的数据库可以完全自洽,厂商那里却仍有一批付费实例。这也是后来增加反向对账的原因:从厂商列出实际存在的资源,再与平台记录比较。

正常创建流程,也有可能留下孤儿

即使所有工具都遵守流程,也不能只靠数据库回收。

我们的正常创建顺序是先向厂商申请沙箱,拿到远端 ID,再把记录写进数据库。这样不会预先留下一个始终等不到远端实例的“创建中”记录,但两个系统之间仍有一个窗口:沙箱已经创建成功,数据库还没写入。

如果进程恰好在这个窗口退出,远端实例就没有对应记录。如果写数据库返回失败,代码会尽力删除刚创建的远端实例;删除也可能遇到网络或厂商错误,不能保证每次补偿都成功。

云厂商的创建请求和本地数据库写入,并不在同一个事务里。我们保留失败时的即时清理,同时用定期对账寻找漏下的实例。即时清理处理已知失败,对账再从远端事实里补找遗漏,两条路径各自承担一部分责任。

对账加上之后,列表完整性成了新的前提

补上的孤儿回收逻辑,每五分钟向厂商查询一次实例列表,再与数据库里的存活记录比较。没有有效记录的实例,进入进一步检查。

但最初使用的是已经废弃的 v1 列表接口。在阿里云 FC 上,这个接口只返回前 50 条结果,没有通过错误或分页提示告诉调用者“后面还有”。人工清理先看到 50 个、后来确认总数为 72 个,暴露了这个问题;同一接口也会让自动对账漏掉列表之外的实例。

对账依赖列表完整性:没有出现在结果里,可能意味着实例不存在,也可能只意味着它排在接口截断的位置之后。一次成功的查询拿不到完整集合,对账就无法据此断定没有遗漏。 即使已经处理完返回的 50 个,也仍需确认后面还有没有实例。

我们把列表切到 v2 分页接口。它仍然返回一个数组,下一页的位置却放在响应头 X-Next-Token 中。程序要带着这个游标继续请求,直到响应头不再给下一页。每页取 100 条;修复时对接的两家厂商都不接受超过 100 的值。

分页循环还有上限,防止厂商不断返回游标,让一轮清理永远结束不了。达到上限时,代码记录警告,返回已经拿到的部分列表。这是一种有明确盲区的降级:可以继续处理已经看见的候选对象,但不能把它解读为完成了全量盘点。

配套测试把两个应回收的暂停实例安排在不同页。如果程序只取第一页,第二个实例就不会被删除,测试会直接失败。比起只验证“列表接口能返回一个实例”,这样的用例才覆盖了这次事故暴露的缺口。

事故或实现参数 原稿记录 适用范围
人工清理规模与费用 72 个,约 492 元/天 2026 年 7 月 13 日这批遗留实例及当时的运行配置
旧 v1 列表结果 超过 50 条时静默截断 本次使用的旧接口,不能推广到所有厂商列表 API
v2 分页 每页 100 条,通过响应头 X-Next-Token 继续 修复时对接的接口;达到循环上限仍是部分清单
对账周期与宽限期 每五分钟对账;创建超过 15 分钟才可回收 本文实现参数,仍需同时通过记录与归属检查

数据库里没有,不代表就能删除

远端列表完整之后,也不能对差集直接执行删除。程序还要回答:这是不是本部署创建的?它会不会仍处于创建过程?

我们用三项条件共同决定是否回收:

  1. 没有有效的数据库记录。 数据库中的存活记录是保护名单,能找到对应记录就不按孤儿处理。数据库查询失败时,本轮直接结束,不能把“查账失败”当成“账上没有”。
  2. 远端带有本部署的 owner 标签。 平台创建沙箱时会写入部署归属。多个部署可能共用一套厂商凭据,生产不能把测试环境的实例当成自己的孤儿。归属不匹配,或者缺少标签,自动回收都会放过。
  3. 创建时间已经超过 15 分钟。 创建调用最长可能等待约 210 秒,而数据库记录要在调用返回后才写入。宽限期需要覆盖这段窗口。创建时间缺失或无法确定,也不删除。

三项条件共同满足,才进入自动回收。缺少归属或时间信息时,选择保留。

这也说明了自动清理的范围。平台外的工具如果不打归属标签,即使留下了孤儿,也不会被当前这条安全回收规则自动删除。那批历史遗留实例需要人工核实和清理;要让后续工具创建的资源进入自动治理,工具也必须遵守归属标记约定。

多保留几分钟会增加少量费用,但误删一个仍在创建、即将供用户使用的环境,会直接造成任务失败。这里的宽限期和标签检查,都是为了避免把“暂时对不上账”误判成“可以销毁”。

会翻页之后,超时也得重新算

旧列表只发一次请求,使用 30 秒超时。改成分页之后,一轮查询可能要分别遍历 running 和 paused 两种状态,每种最多 100 页。如果还沿用原来的超时,大规模实例列表就可能在中途被切断。

因此,列表阶段单独获得两分钟预算,删除单个实例仍使用 30 秒预算。这不是把所有远程调用一律调长,而是承认两个操作的工作量不同:列清单包含许多连续请求,删除只针对一个对象。

同时,running 和 paused 分开查询。某个状态查询失败,不应丢弃另一种状态已经成功取得的结果;失败会记录日志,缺失的那部分仍然是缺失的。列表超时、分页上限和状态查询错误,都不能被记成“本轮没有孤儿”。

暂停和停止计费,不能只看状态名

这次对账需要包含 paused 状态,有一个当时的具体原因:在未启用快照能力的阿里云 FC 运行方式下,标成暂停的会话不会因此释放底层容器,仍可能持续产生费用,直到显式删除。只查 running,就会漏掉这类遗留实例。

这里要保留时间和配置条件。后续实现已经涉及快照和自动暂停,不能把当时的现象扩写成“所有厂商的 paused 都全天计费”。平台数据库里的 paused,也未必与厂商的暂停机制一一对应。

比如,当前接入代码不会向阿里云 FC 调用远程 /pause 接口,这条路径曾返回 403;它依靠创建时配置的超时和自动暂停机制。腾讯 AGS 则支持显式暂停和恢复。平台可以使用相同的业务状态,底层动作却不同。

参数下限也曾造成故障。我们把闲置参数调到 180 秒后,FC 的创建请求因自动暂停超时必须至少为 600 秒而被拒绝。现在对接层会把发给 FC 的值抬到这个下限。产品配置表达的是希望多久后闲置,厂商参数表达的是它允许怎样运行,中间需要一次明确转换。

找到实例之后,必须回到创建它的厂商

我们接了多家沙箱厂商,创建失败时可以沿备用链尝试另一家。但这种切换只适用于还没有确定实例归属的创建阶段。

实例一旦创建,后续恢复、连接和销毁,都要使用记录中的原厂商。A 家返回的实例 ID,不能因为 A 家暂时失败,就拿到 B 家接口重试。B 家不认识它,即使碰巧有相同 ID,也不代表是同一个资源。

历史记录里没有厂商字段的,按明确约定解释为旧厂商,而不能随着当前主厂商配置变化。这让对账后的删除请求能找到正确对象,也避免一次切换配置就改变已有资源的身份。

那次事故留下的两项修复都需要保留:回收程序要能从厂商侧发现账外实例;查询远端时,要确认翻完了清单。与此同时,自动删除只处理归属和年龄都能确定的对象。清单不完整会漏钱,归属判断太宽会删错环境,两种问题不能靠同一个“请求成功”来排除。

下一篇继续讲回收时机。对于仍有归属的正常沙箱,什么时候可以销毁?我们会从工具调用之间的空闲间隔出发,解释为什么不能只看这一轮对话是否已经结束。


这是「Flashduty AI SRE 技术揭秘」系列的第十六篇。我们在做一个专注 SRE 领域、帮你做故障根因分析的 AI Agent 产品,本系列会持续分享我们在 agent 工程上的真实设计、踩过的坑和做过的取舍。

常见问题

什么是孤儿沙箱?

厂商侧仍有实例,但平台没有有效记录能与之对应。只沿数据库扫描闲置资源,无法发现从未入库的实例。

数据库没有记录,就可以直接删除远端实例吗?

不可以。还必须确认实例属于本部署,且创建超过 15 分钟;数据库查询失败、归属缺失或创建时间无法确定时,都不能据此删除。

paused 状态一定停止计费吗?

不能只凭状态名判断。原稿讨论的是当时未启用快照的阿里云 FC 运行方式,并明确限制了时间和配置条件,不能推广到所有厂商和后续实现。

延伸路径

继续看解决方案和产品对比

如果你正在做监控、可观测性或故障定位相关选型,建议从解决方案和产品对比继续往下看。

快猫星云 联系方式 快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云 联系方式
快猫星云