Skip to content
个人学习网站
4 分钟阅读

容器随时会死,但两小时的进度不该跟着死

Agent 沙盒系列第四篇。把进度从容器的内存里搬到一块共享硬盘上,然后当场删掉正在干活的容器——换一个容器接着从第 17 页往下跑。也照实说清楚:这一步的"自动恢复"其实还得人工点一下。

前三篇一路走到这里:任务之间隔离开了,等待也从 45 秒降到了零点几秒。但有个东西从第一篇起就没变过——进度还是记在容器自己的内存里

这在过去只是“崩了就丢”。现在更糟:温池里的容器是随时会被换掉的。系统升级会换、机器故障会换、资源紧张会被回收。它们本来就是消耗品。

于是问题变成:凭什么让消耗品替你保管两小时的成果?

换个地方记:工地黑板,不是工人的脑子

想象一个工地:进度记在工人脑子里,换个人接班就全断了;记在工地墙上的黑板上,谁来接班都能看懂上一班干到哪儿。

代码要做的就是这件事——把黑板挂到墙上

# 申请一块 1GB 的共享硬盘
kind: PersistentVolumeClaim
spec:
  resources:
    requests:
      storage: 1Gi
---
# 让池子里的每个容器都挂上这块盘
volumeMounts:
  - name: workspace-volume
    mountPath: /workspace     # 容器里看到的就是这个目录

共享硬盘在 K8s 里叫持久卷。关键词是“持久”:容器没了它还在,下一个容器可以接着挂上来。

然后干活的逻辑改两处。每干完一页,就在黑板上写一笔

for i in range(start_page, total_pages):
    await asyncio.sleep(1.0)                                  # 抓一页
    with open(f"/workspace/{task_id}/checkpoint.json", "w") as f:
        json.dump({"pages_crawled": i + 1, "total": total_pages}, f)   # 立刻写盘

开工之前,先看一眼黑板上有没有上一班留下的记录

start_page = 0
if os.path.exists(ckpt_file):
    start_page = json.load(open(ckpt_file)).get("pages_crawled", 0)
    print(f"发现断点,从第 {start_page} 页继续")

就这么多。没有分布式事务,没有消息补偿,一个 JSON 文件而已。

当场把干活的容器删掉

这一步的价值必须靠“故意搞破坏”来验证,光看代码看不出来。

任务跑到一半,直接删掉正在干活的那个容器:

kubectl delete pod <正在干活的那>

再把同一个任务重新派一次,看新容器的日志:

发现断点,从第 17 页继续
[worker-b] task-a3f9 progress: 18/30
[worker-b] task-a3f9 progress: 19/30

前 17 页没有重跑。容器换人了,活还在原处接着。

顺带还有一个变化容易被忽略:查询进度的接口,现在可以问任意一个活着的容器——因为它们挂的是同一块盘。状态已经不属于某个容器了。

这就是那句常被引用的话的实感:容器是牲口,工作区是宠物。 牲口可以随时替换,宠物有名字、要养着。

这一版还差什么(说实话的部分)

一、所谓“自动恢复”,其实要人工点一下。 代码里写得很清楚:

# 如果用户没传 task_id,说明是新任务;传了说明是续传

也就是说,没有任何东西在盯着“这个任务断了”。得有人发现它停了,再带着原来的任务号重新发一次请求。这不是自动故障转移,是“支持断点续传”——两者差着一整个监控与重试机制。这个缺口,是后面两篇要处理的。

二、那块盘在真集群里挂不上三个容器。 申请的是 ReadWriteOnce,意思是同一时刻只能挂在一台机器上。本地实验只有一台机器,所以三个容器共用它毫无问题;换成真正的多机集群,第二台机器上的容器就会卡在挂载失败。真要共享,得换成支持多机读写的存储(NFS、云厂商的文件存储那一类)。

三、每页写一次盘是个需要权衡的选择。 写得越勤,崩溃时丢得越少,但小文件写入本身有代价。真实系统里这个频率该由“重跑一页多贵”来定,而不是拍脑袋。

这一步定下来的事

进度离开了容器,容器就真的成了可替换的执行体。但你也会注意到:现在系统里没人知道一个任务是不是停了——分配器派完就不管了。

而池子满的时候,它依然只会说一句“没有空闲资源”。

这两件事其实是同一个缺口:缺一个记着“还有哪些活没干完”的地方。下一篇把它补上——顺便,我在那一步发现自己并没有真的做成想做的东西。

一句话版本:让状态活得比容器久,容器才敢随便死。

代码:agent-sandbox-oss/lab4