从 Kubernetes 对象洪流中抽身:百万并发 Sandbox 的控制面设计

2026-09-23 33 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:12 分钟

Modal 的工程师重新构建了 Sandbox 基础设施,目标是承载数百万个并发 Sandbox,并把创建吞吐量推到每秒数万个。这个数量级改变的不只是集群规模:当临时执行环境像请求一样高速产生和销毁时,传统的“每个 Sandbox 对应一组 Kubernetes 对象”很容易让控制面先于计算资源触顶。

公开摘要没有披露 Modal 的完整实现细节,因此下面不会猜测其内部组件,而是分析这组规模数字意味着什么,以及类似平台可以怎样实践。

百万并发真正压垮的往往不是 CPU

假设平台每秒创建 20,000 个 Sandbox,平均存活 50 秒,仅按 Little's Law 粗略计算,稳态并发量已经达到:

20,000 sandbox/s × 50 s = 1,000,000 sandboxes

此时系统面对的是持续的生命周期事件流:

  • 创建身份、网络和资源租约;
  • 选择宿主机并完成资源预留;
  • 准备镜像、文件系统或快照;
  • 启动隔离环境并报告就绪;
  • 收集退出状态、日志和计量数据;
  • 超时回收,处理宿主机失联与重复请求。

即使每个 Sandbox 只产生 5 次持久化状态变更,每秒 20,000 次创建也可能带来每秒 100,000 次写入,还不包括心跳、日志和销毁操作。问题因此从“有没有足够机器”转向“控制面能否避免全局协调,并稳定处理高基数对象”。

百万并发也不能只看平均值。突发流量、长尾启动时间、宿主机故障和集中回收都可能形成队列。如果创建延迟 SLO 是秒级,排队预算通常只占其中一小部分;控制面必须尽早拒绝、降级或分流,而不是接受所有请求后让它们静默超时。

为什么不能简单地继续堆 Kubernetes

Kubernetes 很适合管理长期运行的服务,但将每个超短生命周期 Sandbox 直接建模为 Pod,会把 API Server、调度器、etcd、控制器和网络插件都放进高频创建路径。风险包括:

  • 对象写放大:一个 Pod 的创建、调度、状态更新和删除会形成多次存储与 watch 事件;
  • 全局调度成本:通用调度器需要考虑节点、资源、亲和性和约束,而临时 Sandbox 更需要低延迟的局部决策;
  • 删除风暴:大量任务同时完成时,终结器、垃圾回收和网络清理可能形成反向压力;
  • 故障域过大:单一控制面抖动可能影响所有租户的新建请求;
  • 状态过细:将每一次短暂状态变化都持久化,会让元数据系统承担不必要的写入压力。

这不等于 Kubernetes 无用。更现实的边界是:让 Kubernetes 管理宿主机服务、网关、控制面组件和容量池,而由专用 Sandbox 控制面处理高频分配。换句话说,Kubernetes 管“较少、较重、较长寿”的对象,专用调度器管“海量、轻量、短寿”的执行单元。

面向百万并发的专用路径通常需要以下能力;这些是通用工程推导,并非对 Modal 内部实现的断言:

  1. 分片所有权:按租户、Sandbox ID 或宿主机分片,避免每次调度都扫描全局状态。
  2. 租约而非同步事务:分配结果带过期时间,故障后可以自动回收。
  3. 幂等创建:客户端携带请求 ID,重试不会生成两个 Sandbox。
  4. 预热容量:提前准备宿主机、运行时、镜像层或快照,缩短关键路径。
  5. 分级背压:按租户限制速率,并根据队列深度和可用容量动态拒绝请求。
  6. 异步清理:用户看到 Sandbox 已结束,不代表所有日志、网络和磁盘清理必须同步完成。

先把容量模型算清楚

下面是一个可直接运行的容量估算脚本。它不是 Modal 的工具,也不模拟真实调度器;它用于回答三个基础问题:给定创建速率和平均生命周期,需要多少并发容量、多少预留容量,以及至少多少个分片。

将以下命令复制到终端。运行前可修改 --create-rate、--avg-lifetime、--headroom 和 --shard-capacity:

cat > sandbox_capacity.py <<'PY'
#!/usr/bin/env python3
import argparse
import math

parser = argparse.ArgumentParser(description='Estimate sandbox control-plane capacity')
parser.add_argument('--create-rate', type=float, default=20_000,
                    help='sandbox creations per second')
parser.add_argument('--avg-lifetime', type=float, default=50,
                    help='average sandbox lifetime in seconds')
parser.add_argument('--declared-concurrency', type=int, default=1_000_000,
                    help='minimum concurrency target')
parser.add_argument('--headroom', type=float, default=0.25,
                    help='fractional spare capacity, for example 0.25')
parser.add_argument('--shard-capacity', type=int, default=25_000,
                    help='maximum active sandboxes managed by one shard')
args = parser.parse_args()

implied_concurrency = args.create_rate * args.avg_lifetime
required_concurrency = max(args.declared_concurrency, implied_concurrency)
provisioned = math.ceil(required_concurrency * (1 + args.headroom))
shards = math.ceil(provisioned / args.shard_capacity)
creates_per_minute = args.create_rate * 60

print(f'Implied steady-state concurrency : {implied_concurrency:,.0f}')
print(f'Required active capacity        : {required_concurrency:,.0f}')
print(f'Capacity with headroom          : {provisioned:,.0f}')
print(f'Minimum control-plane shards    : {shards:,}')
print(f'Create operations per minute    : {creates_per_minute:,.0f}')

if implied_concurrency > args.declared_concurrency:
    print('WARNING: arrival rate and lifetime exceed the declared concurrency target.')
PY

python3 sandbox_capacity.py

默认参数会得到约 100 万稳态并发、125 万预留容量和至少 50 个容量为 25,000 的分片。这里的“50 个”只是数学下限,不是部署建议:生产环境还要考虑可用区、租户热点、故障转移容量和分片维护时的冗余。

平均生命周期也会掩盖长尾。更稳妥的压测应混合不同任务:例如 80% 的任务运行 10 秒,15% 运行 1 分钟,5% 运行 10 分钟,并在压测中注入宿主机失联、镜像缓存未命中和控制面重启。

如果仍以 Kubernetes 承载外围控制面,可以把容量参数放进普通 ConfigMap。下面的 YAML 可以直接应用,但假设你自己的分配服务会读取这些键;它不是 Modal API:

kubectl apply -f - <<'YAML'
apiVersion: v1
kind: ConfigMap
metadata:
  name: sandbox-admission-policy
  namespace: default
data:
  max_create_rate_per_second: '20000'
  target_concurrency: '1000000'
  capacity_headroom_ratio: '0.25'
  max_queue_delay_ms: '250'
  shard_capacity: '25000'
  overload_action: 'reject_with_retry_after'
YAML

kubectl get configmap sandbox-admission-policy -o yaml

实际系统不要只靠 ConfigMap 实现实时限流;更合适的做法是让网关或 admission 服务在内存中执行令牌桶,将策略配置作为版本化输入,并为不同租户设置独立配额。

观测重点要从节点转向创建路径

只监控 CPU、内存和节点数量不足以解释 Sandbox 为什么启动慢。应把创建请求拆成可观测阶段:

request accepted
  → admission passed
  → shard selected
  → host reserved
  → runtime prepared
  → sandbox started
  → ready reported

每个阶段至少记录吞吐量、p50/p95/p99 延迟、错误率和队列深度。还应关注:

  • 每个分片的活跃 Sandbox 数与分配速率;
  • 预热池命中率,以及镜像或快照缓存未命中率;
  • 幂等重试次数和重复创建拦截次数;
  • 宿主机失联后未回收的租约数量;
  • 创建速率与销毁速率的差值;
  • 已结束但尚未完成清理的 Sandbox 数量;
  • 按租户拆分的拒绝率和 Retry-After 时长。

尤其要避免把所有高基数标签直接写入时序数据库。Sandbox ID 更适合进入日志或追踪系统,指标标签则应限制在分片、区域、结果和租户等级等可控维度。

采用这类架构前的检查清单

从 Kubernetes 原生对象转向专用 Sandbox 控制面,会换来更低的创建延迟和更高的生命周期吞吐量,但也意味着团队需要自己承担调度、公平性、租约恢复、资源计量和故障排查。

可以按以下顺序推进:

  • 先测量 API Server、调度、运行时准备和网络设置分别占用多少时间;
  • 只有在通用控制面确实成为瓶颈时,才拆出专用快速路径;
  • 保留 Kubernetes 管理稳定服务和容量池,不必一次性替换整个平台;
  • 在设计阶段就定义幂等键、租约过期语义和过载响应;
  • 用突发流量、删除风暴和宿主机故障验证系统,而不只跑均匀压测;
  • 将公平性和租户隔离视为核心调度约束,而不是上线后的补丁;
  • 确保任何一个分片失效都不会阻塞全局创建路径。

百万并发并不是把现有集群放大一千倍。真正的转折点,是把 Sandbox 当成高速数据面资源:控制面只保存必要状态,让分配局部化、重试幂等化、清理异步化,并在容量耗尽前明确施加背压。


相关推荐