阶段 2:生命周期与清理
对应学习路线阶段 2(核心 · 2–3 天)。本阶段把沙箱当作一项有成本、有期限、会失败的远端资源来管理。 源码目录:
$OSB_SRC。 核心产出:一个在成功、异常、取消三条路径都能释放远端资源的封装,以及一次暂停/恢复实验记录。
一、这一阶段到底要解决什么
跳转到“一、这一阶段到底要解决什么”阶段 0 的脚本能创建和销毁一个沙箱,阶段 1 的地图告诉你创建和执行是两条路径。但生产系统还要回答更难的问题:
- 创建请求已经返回,沙箱为什么还可能不可用?
- 进程崩溃、用户取消、模型超时后,谁来删除远端资源?
- 长任务运行到一半,TTL 到期怎么办?
- “暂停”保存的是内存、磁盘,还是只释放 Pod?
- 我手里的
Sandbox对象是远端资源本身,还是只是一个本地句柄?
这些问题的共同主题是:所有权。
只要一个沙箱被创建出来,就必须有一个明确的负责人和退出路径;否则它会继续占用 CPU、内存、磁盘、网络名额和费用。AI 应用尤其容易泄漏资源,因为模型调用、工具调用和用户取消都可能发生在任意时刻。
先把三个层次分开
跳转到“先把三个层次分开”| 层次 | 是什么 | 谁拥有 | 典型动作 |
|---|---|---|---|
| 远端资源 | 容器、Pod、微虚拟机、快照 | 平台 + 你的清理协议 | create、kill、pause、resume |
| 控制面记录 | sandbox ID、状态、过期时间、元数据 | 生命周期服务器 + 运行时 | get_info、renew、list |
| 本地句柄 | HTTP 客户端、连接池、服务适配器 | 你的进程 | close、connect、resume |
把这三层混在一起,就会出现两种典型错误:
- “代码已经结束,沙箱还在跑”:只关闭了本地句柄,没有终止远端。
- “以为关闭了连接,其实删除了远端”:把
close()当成kill()用,任务被误杀。
本阶段的目标,就是让你能准确说出:此刻我操作的是哪一层,失败时哪一层还留着东西。
二、生命周期词汇表
跳转到“二、生命周期词汇表”| 术语 | 准确含义 | 容易误解成 |
|---|---|---|
| 绝对过期时间 | expiresAt 是一个时间点,到了就终止 |
“还剩多少秒” |
| TTL | 创建时指定的存活时长,最终折算成绝对时间 | 空闲超时 |
| Renew | 把过期时间设为“当前时间 + 传入时长” | 在原时间上累加 |
| 手动清理 | timeout=None,没有自动到期删除 |
永不过期、无需清理 |
| Pause | 请求暂停;不同后端保留的内容不同 | 一定保存内存 |
| Resume | 恢复暂停的沙箱,返回新句柄 | 在原对象上继续用 |
| Snapshot | 某个时间点的持久化检查点 | 暂停的同义词 |
| Template | FastSandbox 预构建的不可变工作负载定义 | 普通镜像 |
| 终态 | Terminated / Failed,不会自己回到 Running |
可以重试恢复 |
有一条总原则:生命周期 API 是异步的,任何“请求已接受”都不等于“状态已完成”。 看到 202,下一步永远是查状态,而不是假设成功。
三、状态机:先认识“中间态”
跳转到“三、状态机:先认识“中间态””公开契约里的状态与迁移大致如下:
┌──────────────┐ │ Pending │ └──────┬───────┘ │ 资源就绪 ▼ ┌──────────────► Running ◄──────────────┐ │ │ │ │ │ pause │ │ kill / TTL │ │ ▼ ▼ │ │ Pausing Stopping │ │ │ │ │ │ ▼ ▼ │ │ Paused Terminated │ │ │ │ │ resume ▼ │ └──────────── Resuming ─────────────────┘
任意非终态在关键错误下可进入 Failed| 状态 | 含义 | 你该做什么 |
|---|---|---|
Pending |
已受理,资源尚未就绪 | 等待或轮询;不要发业务请求 |
Running |
可用 | 执行命令、访问文件或服务 |
Pausing |
暂停处理中 | 不要假设已经暂停 |
Paused |
已暂停 | 可恢复;保留范围取决于后端 |
Resuming |
恢复处理中 | 等待回到 Running,不要复用旧端点 |
Stopping |
终止处理中 | 不要再发起新业务操作 |
Terminated |
已正常终止 | 做最终清理和审计 |
Failed |
终态故障 | 读 reason / message,不要盲目重试 |
两个实现细节:
- SDK 常量里没有
Resuming。SandboxState定义了Pending、Running、Pausing、Paused、Stopping、Terminated、Failed、Unknown,但公开契约明确把Resuming列为合法状态。客户端应把状态当作可扩展字符串,遇到未知值记录日志并继续轮询,而不是写死一组if state == ...。 Stopping是真实存在的观察窗口。 在 Kubernetes 上删除BatchSandbox后,对象可能短暂保留,等待任务清理和池释放;这期间查询会看到Stopping,不是“删除失败”。阶段 4 会解释背后的控制器行为。
状态机之外,还有一个状态字段常被忽略:
info.status.reason # 机器可读原因,如 user_delete、ttl_expiry、provision_timeoutinfo.status.message # 人类可读说明info.last_transition_at排错时先读 reason,再读日志;不要只看 state 是 Failed 就开始猜。
四、创建:从“请求成功”到“可以负责”
跳转到“四、创建:从“请求成功”到“可以负责””阶段 1 已经拆过创建链路,这里只补生命周期视角的三个关键点。
1. create() 返回时,你拿到的是“可用句柄”,不是“所有权证明”
跳转到“1. create() 返回时,你拿到的是“可用句柄”,不是“所有权证明””SDK 默认会等到端点发布并完成健康检查才返回。但这只说明“现在能用”,不说明:
- 你已经记录了 sandbox ID;
- 你的进程退出时它会被清理;
- 它的 TTL 足够覆盖任务;
- 后续另一个 worker 不会把它杀掉。
句柄是能力,所有权是协议。生产系统必须显式设计后者。
2. 创建失败时,SDK 会尽力清理“半成品”
跳转到“2. 创建失败时,SDK 会尽力清理“半成品””Sandbox._launch 在端点解析或健康检查失败时,会尝试 kill_sandbox 清理已经创建出来的远端沙箱,然后抛出异常。这能减少孤儿,但不是保证:
- 进程被
SIGKILL、机器断电,清理代码不会执行; - 清理请求本身可能失败;
- 服务端可能在返回 ID 之前就失败,客户端根本不知道远端是否已创建。
所以仍需要 TTL 兜底和孤儿扫描,不能把 SDK 的尽力清理当成事务。
3. 就绪预算和健康检查是两件事
跳转到“3. 就绪预算和健康检查是两件事”ready_timeout(默认 30 秒)是“等端点发布 + 健康检查”的总预算;skip_health_check=True 只是跳过健康探测,仍然会等端点发布。这意味着:
- 跳过健康检查不等于不等待;
- 自定义
health_check可能比/ping更严格,也更适合业务服务; - 预算耗尽应被视为可诊断的失败,而不是“再试一次就好”。
五、TTL:绝对时间,不是倒计时
跳转到“五、TTL:绝对时间,不是倒计时”创建时的 TTL
跳转到“创建时的 TTL”Sandbox.create 的默认 timeout 是 10 分钟,最终由服务端折算成 expiresAt 这个绝对时间点。服务端配置里的 max_sandbox_timeout_seconds 限制的是创建时能申请的最大时长。
timeout=None 表示手动清理模式:没有自动到期删除。它不意味着免费、安全或永久;它只意味着泄漏风险完全转移到你的代码身上。Kubernetes 的某些工作负载提供方甚至可能拒绝“不设过期时间”的请求。
续期的真实语义
跳转到“续期的真实语义”源码写得很直接:
new_expiration = datetime.now(timezone.utc) + timeout所以 renew(timedelta(minutes=5)) 的含义是“从现在起再活 5 分钟”,不是“在原过期时间上加 5 分钟”。它可能延长,也可能缩短剩余寿命。
一个实用预算是:
任务预估时长 + 排队等待 + 工具重试预算 < TTL不要把续期当成无限保活;也不要在每个工具调用前无脑续期,否则一个卡死的 Agent 可以永久占用资源。
为什么绝对时间更可靠
跳转到“为什么绝对时间更可靠”绝对过期时间有一个重要好处:服务重启不会重置寿命。
- Docker 运行时把过期时间写进容器标签;服务端启动时会扫描已有沙箱并恢复定时器。
- Kubernetes 把过期时间写进
BatchSandbox的expireTime,由控制器执行。
这就是“绝对 TTL 而不是空闲超时”的价值:沙箱的寿命可以从创建时间审计,不会因为一次重启、一次空闲、一次重连而悄悄延长。
TTL 到期的表现
跳转到“TTL 到期的表现”TTL 到期不是“优雅地通知你的代码”,而是资源被终止:
- 正在执行的命令会断开;
- 后续请求可能得到
404或连接失败; - 状态最终进入
Terminated,reason可能是ttl_expiry。
应用应把“执行中途断开”与“命令本身失败”分开处理;前者是生命周期事件,后者是业务事件。
六、暂停与恢复:同一个接口,不同的保留范围
跳转到“六、暂停与恢复:同一个接口,不同的保留范围”这是全课程最容易被“接口相似”误导的地方之一。
共同点
跳转到“共同点”sandbox.pause()只负责发起暂停,不等待完成;要轮询到Paused才算完成。- 暂停后旧端点可能失效;SDK 在
pause()时会清空端点缓存。 - 恢复用类方法
Sandbox.resume(id, ...),它会重新解析端点、重建服务对象、做健康检查,返回新的句柄。 - 暂停中的沙箱仍可能按原 TTL 到期;暂停不是“冻结计费”。
不同点:Docker 与 Kubernetes 的“暂停”不是一回事
跳转到“不同点:Docker 与 Kubernetes 的“暂停”不是一回事”| 维度 | Docker 运行时 | Kubernetes(BatchSandbox rootfs 模式) |
|---|---|---|
| 实现方式 | 暂停容器进程(freeze) | 提交根文件系统快照,释放 Pod 与池名额 |
| 运行内存 | 保留在内存中 | 默认不保留 |
| 资源占用 | 容器仍存在,仍占资源 | Pod 被释放,主要留镜像/快照 |
| 恢复速度 | 快,解冻即可 | 需要从快照重建工作负载 |
| 端点 | 通常不变,但不应缓存 | 可能变化,必须重新解析 |
| Credential Vault | sidecar 仍在,秘密可能保留 | sidecar 随运行时释放,秘密丢失,需要重新注入 |
| 适用场景 | 短暂中断、保留进程状态 | 长时间停用、释放集群容量 |
这张表要带进阶段 4:同一个 pause(),不同后端提供的是不同保证。 看到“支持暂停”,必须追问“保留了哪些状态、释放了哪些资源”。
暂停/恢复实验的注意事项
跳转到“暂停/恢复实验的注意事项”- 暂停前先确认状态是
Running;对Paused再暂停通常得到409。 - 恢复后不要复用旧
Sandbox对象;用resume()返回的新句柄。 - 如果任务依赖进程内存(例如一个正在训练的 Python 进程),在 Kubernetes rootfs 模式下恢复后不会从原内存继续。
- 如果启用了 Credential Vault,恢复后要重新注入凭据;不要假设秘密会跟着快照回来。
七、快照:持久化检查点,不是“暂停的别名”
跳转到“七、快照:持久化检查点,不是“暂停的别名””快照和暂停解决不同问题:
- 暂停:让一个沙箱暂时不可用,之后恢复同一个沙箱 ID。
- 快照:把某个时刻的状态持久化,之后可以创建新的沙箱,也可以长期保留。
公开状态
跳转到“公开状态”生命周期 API 暴露的快照状态是:
| 状态 | 含义 |
|---|---|
Creating |
已接受,正在捕获 |
Ready |
可用于恢复 |
Failed |
创建失败,读原因 |
Deleting |
删除处理中 |
创建接口返回时通常还是 Creating,必须轮询到 Ready 才能用于 Sandbox.create(snapshot_id=...)。
在 Kubernetes 内部,控制器还会经历 Pending → Committing → Succeed / Failed 的阶段;这是实现层的阶段,不要和公开状态混用。你通过 API 看到的是公开状态。
边界
跳转到“边界”- 默认保存根文件系统;运行内存默认不保存。可选的 QEMU VMState 能保存虚拟机内存状态,但属于特定后端和特定配置。
- 暂停/恢复和快照通常只支持单副本沙箱(
replicas=1)。 - 删除快照记录不等于删除已经推送的镜像;镜像保留策略是运维问题。
- 快照的 registry、凭据、保留策略由部署配置决定,不是每个请求都能覆盖。
恢复时的参数约束
跳转到“恢复时的参数约束”Sandbox.create 要求 image 和 snapshot_id 恰好提供一个。同时传两个会立刻失败;两个都不传也会失败。恢复出来的沙箱是一个新 ID,不是原沙箱的“复活”。
八、模板:创建前就固定了形状
跳转到“八、模板:创建前就固定了形状”FastSandbox 模板是另一类生命周期对象,容易和快照混淆:
| 维度 | 快照 | 模板 |
|---|---|---|
| 来源 | 某个沙箱的运行结果 | 预构建的黄金镜像 |
| 用途 | 恢复该沙箱的状态 | 快速创建同形状的新沙箱 |
| 状态 | Creating / Ready / Failed / Deleting |
Pending / Building / Succeeded / Failed |
| 创建入口 | Sandbox.create(snapshot_id=...) |
Sandbox.create_from_template(template_id, timeout=...) |
| 可覆盖字段 | 部分创建参数 | 几乎不可覆盖:入口、环境、资源、卷、平台、生命周期都来自模板 |
模板模式有三个硬约束:
timeout是必需参数;- 只有
Succeeded的模板能创建沙箱; - 不能用每请求的
image、env、resource、entrypoint覆盖模板形状。
如果业务需要“每个任务不同镜像、不同资源”,模板不是合适的选择;它适合“形状稳定、启动要快”的场景。
九、四个清理动作:close、kill、destroy、async with
跳转到“九、四个清理动作:close、kill、destroy、async with”先看源码语义,再看使用场景。
| 操作 | 远端沙箱 | 本地连接 | 典型用途 |
|---|---|---|---|
close() |
保留 | 关闭 | 任务结束,但希望稍后 connect() 或交给 TTL |
kill() |
终止 | 保留 | 不再需要远端,但仍要用客户端做收尾 |
destroy() |
终止 | 关闭 | 创建—使用—丢弃的标准退出路径 |
async with sandbox |
不会自动终止 | 退出时 close() |
只保证本地资源释放 |
connect() |
连接已有 | 新建句柄 | 跨进程接手、断线重连 |
resume() |
恢复暂停沙箱 | 新句柄 | 暂停后继续任务 |
几个实现细节值得记住:
destroy()先kill(),再在finally里close();即使终止失败,本地资源也会关闭,然后异常继续抛出。close()只记录错误、不抛出,避免清理路径二次爆炸。pause()和kill()都会清空端点缓存,防止继续使用旧地址。async with的__aexit__只调用close(),所以“上下文退出后沙箱还在”是设计行为,不是 bug。
一个最小但可靠的封装
跳转到“一个最小但可靠的封装”import asynciofrom datetime import timedelta
from opensandbox import Sandboxfrom opensandbox.config import ConnectionConfig
async def run_in_sandbox(work): config = ConnectionConfig(domain="localhost:8080", api_key="dev-secret") sandbox = await Sandbox.create( "python:3.12", connection_config=config, timeout=timedelta(minutes=15), ) try: return await work(sandbox) finally: # 用 shield 防止当前任务被取消时清理也被打断; # 它挡不住第二次取消或进程退出,所以 TTL 仍是兜底。 await asyncio.shield(sandbox.destroy())这段代码解决了“成功”和“异常”两条路径,但还没有解决:
- 清理失败如何告警;
- 清理超时如何处理;
- 取消发生时如何确保清理任务真的跑完;
- 如何记录 sandbox ID 以便事后审计。
生产封装至少应补上结构化日志、销毁失败告警和 sandbox ID 记录。不要在 finally 里静默吞掉销毁异常,否则泄漏会被伪装成成功。
三条路径都要想清楚
跳转到“三条路径都要想清楚”| 路径 | 典型场景 | 你要保证 |
|---|---|---|
| 成功 | 任务正常完成 | 显式 destroy(),不要只依赖 GC |
| 异常 | 命令失败、解析失败、业务抛错 | finally 中仍执行销毁 |
| 取消 | 用户中断、上层超时、进程关闭 | 清理不被同一次取消打断;有 TTL 和孤儿扫描兜底 |
十、实验:看见“句柄”和“远端”的差别
跳转到“十、实验:看见“句柄”和“远端”的差别”实验 1 · 生命周期观察器
跳转到“实验 1 · 生命周期观察器”import asynciofrom datetime import datetime, timezone, timedelta
from opensandbox import Sandboxfrom opensandbox.config import ConnectionConfig
def connection() -> ConnectionConfig: return ConnectionConfig( domain="localhost:8080", api_key="dev-secret", request_timeout=timedelta(seconds=60), )
async def wait_for_state(sandbox, wanted: str, timeout_seconds: float = 120) -> str: loop = asyncio.get_running_loop() deadline = loop.time() + timeout_seconds while True: info = await sandbox.get_info() state = info.status.state print(f"state={state} reason={info.status.reason}") if state == wanted: return state if state in {"Failed", "Terminated"}: raise RuntimeError(f"sandbox reached {state}: {info.status.reason}") if loop.time() >= deadline: raise TimeoutError(f"state did not become {wanted}") await asyncio.sleep(1)
async def main() -> None: sandbox = await Sandbox.create( "python:3.12", connection_config=connection(), timeout=timedelta(minutes=10), resource={"cpu": "500m", "memory": "512Mi"}, metadata={"lesson": "stage-2"}, ) print("sandbox id:", sandbox.id) try: info = await sandbox.get_info() print("expires_at:", info.expires_at) print("now :", datetime.now(timezone.utc))
await wait_for_state(sandbox, "Running")
await sandbox.renew(timedelta(minutes=3)) new_info = await sandbox.get_info() print("after renew:", new_info.expires_at) finally: await sandbox.destroy()
if __name__ == "__main__": asyncio.run(main())观察三件事:
expires_at是绝对时间,和now的差值接近你传入的 TTL;renew(3m)后新的expires_at接近“现在 + 3 分钟”,而不是“原时间 + 3 分钟”;- 脚本结束后远端资源才由
destroy()终止;finally是资源安全的关键。
实验 2 · close() 之后,远端还在吗
跳转到“实验 2 · close() 之后,远端还在吗”把 finally 里的 destroy() 换成 close(),运行后在另一个窗口执行:
docker ps --format "table {{.Names}}\t{{.Status}}"你会看到沙箱容器仍然存在。然后手动终止它:
await sandbox.kill()再用 docker ps 确认消失。这个实验把“本地句柄”和“远端资源”彻底分开。
实验 3 · 暂停与恢复(Docker)
跳转到“实验 3 · 暂停与恢复(Docker)”async def pause_and_resume() -> None: config = connection() sandbox = await Sandbox.create( "python:3.12", connection_config=config, timeout=timedelta(minutes=30), ) sandbox_id = sandbox.id try: await sandbox.pause() await wait_for_state(sandbox, "Paused") print("paused; check `docker ps` for (Paused)")
resumed = await Sandbox.resume( sandbox_id, connection_config=config, resume_timeout=timedelta(seconds=60), ) try: execution = await resumed.commands.run("echo resumed") print(execution.text) finally: await resumed.destroy() finally: try: await sandbox.close() except Exception as exc: print("close failed:", exc)在暂停后、恢复前执行 docker ps,观察容器状态;恢复后再执行一次命令。注意:Docker 的暂停保留进程内存,Kubernetes rootfs 暂停不保留;换后端重做这个实验,结论会不同。
实验 4 · 快照(需要 Kubernetes 后端)
跳转到“实验 4 · 快照(需要 Kubernetes 后端)”如果当前只有 Docker,跳过执行,但仍要读懂代码:
from opensandbox import Sandbox, SandboxManager
async def snapshot_lab(sandbox: Sandbox) -> None: created = await sandbox.create_snapshot(name="stage-2-checkpoint") manager = await SandboxManager.create(connection_config=connection()) try: deadline = asyncio.get_running_loop().time() + 300 while True: current = await manager.get_snapshot(created.id) state = current.status.state print("snapshot state:", state) if state == "Ready": break if state == "Failed": raise RuntimeError(current.status.message or "snapshot failed") if asyncio.get_running_loop().time() >= deadline: raise TimeoutError("snapshot did not become Ready") await asyncio.sleep(2)
restored = await Sandbox.create( snapshot_id=created.id, connection_config=connection(), timeout=timedelta(minutes=10), ) try: print((await restored.get_info()).id) finally: await restored.destroy() finally: await manager.close()要观察:快照 ID 和恢复出的 sandbox ID 不同;create(snapshot_id=...) 不能再传 image。
实验 5 · 取消路径
跳转到“实验 5 · 取消路径”async def cancelled_work() -> None: try: await asyncio.sleep(60) finally: print("work cancelled; cleanup still runs")用一个外层 asyncio.timeout(2) 包住整个 run_in_sandbox(cancelled_work),观察:
CancelledError会传播;finally中的destroy()仍应执行;- 如果清理没有完成,
docker ps会留下证据。
这一步是为阶段 7 的 Agent 预算和阶段 8 的泄漏排查做铺垫。
十一、失败模式表
跳转到“十一、失败模式表”| 现象 | 可能原因 | 先查什么 | 处理 |
|---|---|---|---|
| 创建超时 | 镜像拉取慢、调度失败、execd 未就绪 | 服务端日志、docker ps -a、状态 reason |
区分“慢”和“死”;不要无脑重试 |
| 创建失败后仍残留容器 | 清理请求失败、进程被杀 | sandbox ID、容器标签 | 依赖 TTL + 孤儿扫描,不假设 SDK 清理可靠 |
401 |
API Key 不一致 | 服务端配置、客户端配置 | 修密钥,不要重试 |
暂停返回 409 |
沙箱不在 Running |
当前状态 | 先等到稳定状态 |
| 恢复失败 | 快照未 Ready、后端不支持、资源不足 |
快照状态、事件 | 等 Ready 再恢复;读 reason |
| 恢复后命令连接失败 | 端点已变化、旧句柄被复用 | 是否用 resume() 新句柄 |
重新解析端点,不缓存旧地址 |
| 长任务中途断开 | TTL 到期 | expiresAt、reason |
在预算内续期;把 TTL 事件与业务失败分开 |
快照一直 Creating |
镜像推送慢、registry 不可达 | 快照状态、控制器事件、registry | 设超时并告警,不要无限轮询 |
| 删除后仍能查到 | Kubernetes Stopping 窗口 |
status.reason 是否为 DELETING |
等待对象真正消失 |
| 清理失败但业务成功 | destroy() 抛错被吞 |
日志级别、异常处理 | 记录 ID 并告警,不要伪装成功 |
十二、常见误区
跳转到“十二、常见误区”| 误区 | 更准确的理解 |
|---|---|
“create() 返回了,资源就归我管了” |
返回的是可用句柄;所有权要靠清理协议、TTL 和审计来建立。 |
“renew(30m) 是在原过期时间上加 30 分钟” |
是 now + 30m,可能延长也可能缩短。 |
“timeout=None 就是永久沙箱” |
是没有自动到期删除,泄漏风险更高。 |
“close() 会删除沙箱” |
只关闭本地连接,远端仍在。 |
“async with 会自动销毁” |
只调用 close(),不会终止远端。 |
| “暂停一定保存内存” | Docker freeze 保留内存;Kubernetes rootfs 快照默认不保留。 |
“恢复后还能用旧 Sandbox 对象” |
端点可能变化;用 resume() 的新句柄。 |
| “快照创建返回成功就能恢复” | 要轮询到 Ready;Creating 不是可用。 |
| “模板和快照都是保存状态” | 模板是预构建形状,快照是某个沙箱的检查点。 |
十三、通关自检(含参考答案)
跳转到“十三、通关自检(含参考答案)”1. 为什么说生命周期管理的本质是所有权管理?
因为资源一旦创建就持续占用成本,而“创建成功”只证明它现在存在,不证明有人会在成功、异常、取消三条路径上负责释放它。所有权意味着明确的负责人、期限和退出路径。
2. renew(5m) 到底做了什么?
把过期时间设为“当前 UTC 时间 + 5 分钟”。它不是累加,也不是保活;如果原剩余时间更长,这次调用会缩短寿命。
3. close()、kill()、destroy() 的区别是什么?
close() 只释放本地连接;kill() 终止远端但保留本地客户端;destroy() 先终止远端,再无条件关闭本地资源,是标准的创建—使用—丢弃退出路径。
4. 为什么 async with 不能替代清理?
它的 __aexit__ 只调用 close(),不会调用 kill() 或 destroy()。远端沙箱会继续运行,直到 TTL 到期或被其他机制清理。
5. Kubernetes 上暂停后,进程内存还在吗?
默认不在。rootfs 模式提交文件系统并释放 Pod,恢复时从快照重建;运行内存默认不保留。需要保留内存时要使用特定的快照/VMState 模式,并确认后端支持。
6. 为什么暂停后要重新解析端点?
暂停/恢复可能改变工作负载的地址(尤其在 Kubernetes 上)。SDK 的 pause() 会清空端点缓存,resume() 会重新解析并返回新句柄;业务代码不应缓存旧地址。
7. 快照的公开状态和内部阶段为什么不一样?
公开 API 面向客户端,使用 Creating / Ready / Failed / Deleting;Kubernetes 控制器内部还有 Pending → Committing → Succeed / Failed。前者是契约,后者是实现阶段,客户端只应依赖公开状态。
8. 创建失败后一定没有残留吗?
不一定。SDK 会尽力清理已经拿到 ID 的沙箱,但进程被杀、清理请求失败、服务端提前失败都可能留下资源。TTL 和独立的孤儿扫描仍是必要的兜底。
完成清单
跳转到“完成清单”- 能画出
Pending → Running → Pausing → Paused → Resuming → Running和终止分支。 - 能解释
resume为什么返回新句柄,以及为什么不能缓存旧端点。 - 能说出默认 TTL、
timeout=None和renew的实际语义。 - 写出一个在成功、异常、取消三条路径都能清理的封装。
- 能区分
close、kill、destroy和async with。 - 在 Docker 上完成一次暂停、轮询到
Paused、恢复并执行命令。 - 能解释 Docker 与 Kubernetes 暂停保留内容的差异。
- 能说明快照
Ready和模板Succeeded为什么都必须轮询。
十四、做完之后
跳转到“十四、做完之后”现在你能把一个沙箱从“会跑代码的东西”变成“有所有者、期限和退出路径的资源”。
下一阶段进入数据面:命令、流式输出、文件读写、终端和代码上下文。你会看到 Execution 对象里到底有什么、SSE 事件怎样变成证据,以及为什么“传输成功”和“命令成功”是两件事。
带着这个问题进入下一章:“模型说‘运行这段代码’之后,系统凭什么让应用相信它真的运行了、真的成功了?”
进入阶段 3。
配套学习路线阶段 2 编写 · 2026-10-30 · 基于 release-1.1.1(531700ca)