跳到主要内容

Beam (beta9) — 架构与原理

30 秒导读: Beam 是一个开源的 serverless AI 运行时。你给一个 Python 函数加个装饰器(@endpoint@task_queue),它就被打包成容器、按请求量自动扩缩到很多台机器上跑,空闲时缩到零。它最值得学的地方是怎么把容器冷启动压到一秒以内:懒加载镜像(不等整个镜像下载完就开跑)+ 进程检查点恢复。

1. 这是什么(零基础也能懂)

一句话定义: Beam 把「写一个 Python 函数」和「让它在一堆带 GPU 的远程机器上弹性地跑起来」之间的所有基础设施(打镜像、调度、扩缩容、网络、存储)全部托管掉,你只管写函数。

解决什么问题 / 给谁用:

假设你是一个做 AI 应用的工程师,手上有一个推理函数,想把它变成一个能扛流量的 HTTP 接口:

  • 自己搭 → 要会 Docker、Kubernetes、autoscaler、GPU 调度、镜像仓库……几周起步。
  • 用 Beam → 加一个 @endpoint(gpu="A10G") 装饰器,beam deploy,完事。

再比如你在做一个 coding agent,需要一个隔离沙箱来跑「LLM 刚生成的、不可信的代码」——Beam 的 Sandbox 就是为这个场景做的:起一个隔离容器,往里塞代码执行,看输出。

它能做什么(功能):

  • Endpoint:把函数变成自动扩缩的 HTTP 推理端点。
  • Task Queue:把函数变成可重试的后台任务队列(替代 Celery)。
  • Sandbox:起隔离容器跑不可信代码(agent 场景)。
  • Function / Scheduled job:远程函数、定时任务。
  • Volume / 分布式存储GPU 支持(自家云或自带 GPU)。

用起来什么样: 一个最小的真实例子(来自 README.md)——

from beam import Image, endpoint

@endpoint(image=Image(python_version="python3.11"), gpu="A10G", cpu=2, memory="16Gi")
def handler():
return {"label": "cat", "confidence": 0.97}

beam deploy app.py:handler 之后,你就有一个会自动扩缩、空闲缩到零的 GPU 推理接口。

一句话直觉/类比: 把 Beam 想成「给容器用的 AWS Lambda,但是为 AI / GPU / 长镜像优化过」。Lambda 的痛点是冷启动慢、镜像大、不好放 GPU;Beam 的整套工程几乎都在围着「冷启动」这个核心难题转。

2. 顶层全景(它大概怎么转)

Beam 是一个 Go 写的控制面 + 数据面,外加一个 Python SDK。三个主进程(见 cmd/):

进程干什么入口
Gateway对外的 API/gRPC 入口;托管所有「抽象」(endpoint/taskqueue/sandbox…);内嵌调度器cmd/gateway/main.gopkg/gateway/gateway.go
Worker跑在每台计算机器上,真正把容器拉起来、跑、清理cmd/worker/main.gopkg/worker/worker.go
Agent让用户「自带机器」加入 Beam 的算力池cmd/agent/main.gopkg/agent/

它们之间不直接 RPC 排队,而是通过 Redis 传递容器请求和 worker 状态(这是理解整个系统的关键)。

顶层数据流(一次 endpoint 请求从来到落地):

用户请求 (HTTP)


┌───────────────────────── Gateway ─────────────────────────┐
│ endpointInstance(每个部署一个) │
│ Autoscaler 每秒采样:在途请求数 ÷ tasksPerContainer │
│ → 「该有 N 个容器」 │
│ HandleScalingEvent → startContainers(delta) │
│ → Scheduler.Run(ContainerRequest) ① │
└───────────────────────────┬───────────────────────────────┘
│ 写入 Redis backlog

┌──────────────────────── Scheduler(内嵌在 Gateway)───────┐
│ StartProcessingRequests 循环 │
│ PopN backlog → 给每个请求评分选 worker ② │
│ 选到 → RPush 到该 worker 的 Redis 请求队列 │
│ 没空闲 worker → 预留待启容量 / 调云厂商 API 开新机 ③ │
└───────────────────────────┬───────────────────────────────┘
│ Redis: SchedulerWorkerRequests(workerId)

┌──────────────────────────── Worker ───────────────────────┐
│ Run 循环:gRPC 流式 GetNextContainerRequest ④ │
│ PullLazy(CLIP 懒加载镜像,FUSE 挂载)⑤ │
│ specFromRequest → runc/gVisor 起容器 ⑥ │
│ (可选)CRIU 从检查点恢复,跳过冷启动 ⑦ │
│ 跑用户进程 → 退出 → 清理、归还容量 │
└────────────────────────────────────────────────────────────┘

怎么读这张图: ①→④ 是「请求怎么找到一台机器」(控制流,第 01 章);⑤→⑦ 是「机器怎么把容器秒拉起来」(冷启动,第 02、03 章);最上面的 Autoscaler 是「该不该多开一个容器」(第 04 章)。

部件一句话职责:

部件干什么在哪
抽象实例 AutoscaledInstance每个部署一个,管自己的扩缩与容器集合pkg/abstractions/common/instance.go
Autoscaler每秒采样 + 算「想要几个容器」pkg/abstractions/common/autoscaler.go
Scheduler把容器请求评分匹配到 worker,或开新机pkg/scheduler/scheduler.go
RequestBacklogRedis 里的待调度请求队列pkg/scheduler/backlog.go
WorkerPoolManager + Controller管理 worker 池、向 K8s/云厂商要新机器pkg/scheduler/pool*.go
Worker在机器上拉容器、跑、清理pkg/worker/worker.golifecycle.go
ImageClientCLIP 懒加载镜像 + FUSE 挂载pkg/worker/image.go
CRIUManager检查点/恢复,实现热启动pkg/worker/criu.go
Runtime (runc/runsc)真正的容器运行时抽象pkg/runtime/runtime.go

3. 主线走一遍(不进代码)

以「一个 endpoint 突然来了流量」为例:

  1. 采样endpointInstance 的 autoscaler 每秒查 Redis「在途任务数」,除以 tasksPerContainer,得出「想要 N 个容器」。
  2. 下扩缩指令HandleScalingEvent 比较「想要 N」和「现有 M」,差值正就 startContainers(N-M)
  3. 生成请求:每个新容器构造一个 ContainerRequest(带 image、cpu、gpu、entrypoint),调 Scheduler.Run
  4. 入 backlog:调度器把请求推进 Redis backlog,自己有个循环不停 PopN 出来处理。
  5. 选 worker:对每个请求过一串过滤器(池选择器、资源够不够、GPU 型号),再打分,挑最高分的 worker;选不到就预留一台待启的机器调云 API 开新机
  6. 派给 worker:把请求 RPush 进那台 worker 在 Redis 里的专属队列。
  7. worker 接活:worker 用一条 gRPC 长流不断 GetNextContainerRequest(底层是 LPop 那个队列),拿到请求。
  8. 冷启动PullLazy 把镜像懒挂载成 FUSE 文件系统(不下整包),生成 runc spec,起容器;如果这个 stub 之前做过检查点,直接 CRIU 恢复,几乎瞬时。
  9. 跑 + 收尾:用户进程跑起来,对外可路由;退出后 worker 清理网络/overlay/GPU,把容量归还给调度器。

4. 阅读地图

建议顺序:

  1. 01-scheduler.md — 调度器。先搞清「请求怎么找到机器」:backlog 批处理、worker 过滤+评分、容量预留、按需 provision。这是 Beam 控制面的心脏。
  2. 02-worker-lifecycle.md — Worker 端。一台 worker 怎么经 Redis 接活、RunContainer 的分阶段启动、容器退出与优雅停机。
  3. 03-cold-start.md精华章。冷启动为什么能快:CLIP 懒加载镜像 + FUSE 挂载 + CRIU 检查点恢复。
  4. 04-abstractions.md — 抽象层。endpoint/taskqueue/sandbox 共用的自动扩缩框架,以及 agent 场景的 Sandbox。

每章末尾都有「代码地图」,按符号名可直接 grep 跳进源码。

5. 边界与局限(先知道)

  • 强依赖 Redis:调度状态、worker 状态、请求队列全在 Redis。Redis 是事实上的单点。
  • CRIU 检查点有前提:需要缓存可用(cacheManager != nil)、worker 池显式开了 CRIUEnabledworker.go:347),GPU 还有特殊限制(见 criu_nvidia.go)。
  • 冷启动「快」是相对的:懒加载让「开始跑」很快,但首次访问到的文件仍要按需从仓库拉(FUSE 触发),重 IO 的首请求会慢。
  • CRIU 是有开关、有依赖的可选能力,不是某个隔离运行时专属的「拿不到」:检查点恢复由运行时的 Capabilities().CheckpointRestore 门控(lifecycle.go:259),但在本仓里 runc 和 gVisor 两个运行时都把它声明为 truerunc.go:61runsc.go:72)。换句话说,能不能用 CRIU 取决于池是否开了 CRIUEnabled、缓存是否可用、以及 GPU/TCP 等状态是否兼容,而不取决于你选 runc 还是 gVisor。