数秒内扩展至百万级并发沙箱
Scaling to 1 million concurrent sandboxes in seconds
我们构建了一个可在数秒内将单个工作空间的沙箱(sandbox)并发数扩展至100万个的调度系统。系统采用去中心化调度、分层资源池与预分配-即时激活设计,由调度协调器、节点代理和全局状态存储(etcd/Consul)组成。关键实现包括批量预留与惰性分配,将调度延迟从O(n)降至O(1);基于令牌桶的速率控制防止调度风暴;故障恢复可在数秒内完成重调度。测试环境(100节点,每节点256GB内存、64核CPU)下,100万沙箱调度延迟小于2秒,单个沙箱激活延迟小于50毫秒,系统吞吐量达每秒50万个调度请求。
我们如何(以及为何)构建一个可在数秒内扩展至(每个工作空间)100万个并发沙箱的调度系统
背景与动机
在构建大规模基础设施时,调度系统往往是瓶颈所在。我们面临的核心挑战是:如何让一个工作空间内的沙箱(sandbox)数量在几秒内从零扩展到100万个并发实例,同时保持资源利用率和响应速度。
传统的调度方案——无论是基于队列的批处理系统,还是依赖中心化调度器的架构——都无法满足这种弹性需求。它们要么在节点数量激增时产生不可接受的调度延迟,要么在资源碎片化问题上陷入困境。
设计原则
我们围绕三个核心原则构建了这个系统:
- 去中心化调度:没有单一调度器成为瓶颈。每个节点自主决策,通过分布式共识协调全局状态。
- 分层资源池:将资源按层级组织(工作空间 → 集群 → 节点 → 容器),每一层只维护必要的元数据,避免全局锁竞争。
- 预分配与即时激活:沙箱的创建分为两个阶段——资源预留(reservation)和实际激活(activation)。预留阶段在后台异步完成,激活阶段只需毫秒级操作。
架构概览
系统由三个主要组件构成:
- 调度协调器(Scheduler Coordinator):负责接收创建沙箱的请求,将其拆分为可并行执行的子任务,并分发到各节点。
- 节点代理(Node Agent):运行在每个计算节点上,管理本地容器生命周期,并定期向协调器报告资源状态。
- 全局状态存储(Global State Store):使用分布式键值存储(如 etcd 或 Consul)保存集群拓扑、资源配额和预留信息,但避免将其用于实时调度决策。
关键实现细节
1. 批量预留与惰性分配
当收到创建100万个沙箱的请求时,协调器不会立即为每个沙箱分配具体资源。相反,它先向集群广播一个“预留请求”,每个节点根据自身空闲资源量响应一个承诺(promise)。协调器收集所有承诺后,在全局状态中记录预留总量,然后立即返回成功——此时沙箱尚未实际创建。
实际容器创建发生在沙箱被首次访问时(惰性分配)。节点代理在收到访问请求后,从本地预分配的 IP 池和存储卷中快速启动容器。这种设计将调度延迟从 O(n) 降低到 O(1)。
2. 基于令牌桶的速率控制
为了防止调度风暴,每个工作空间和每个节点都维护一个令牌桶(token bucket)。协调器在分发子任务前必须获取工作空间级别的令牌,节点在创建容器前必须获取节点级别的令牌。令牌的补充速率根据历史负载动态调整。
3. 故障恢复与重调度
节点故障时,其上的预留信息会超时失效。协调器定期扫描全局状态,发现失效预留后立即触发重新调度。由于预留阶段不占用实际资源,重调度可以在几秒内完成。
性能基准
在测试环境中(100个节点,每个节点 256GB 内存、64核 CPU),我们实现了以下指标:
- 100万个沙箱的调度延迟:< 2秒(从请求到达协调器到所有预留确认)
- 单个沙箱的激活延迟:< 50毫秒(从首次访问到容器就绪)
- 系统吞吐量:每秒处理 50万个调度请求(线性扩展)
为何选择这种设计
我们选择去中心化+惰性分配方案,而不是更常见的中心化调度器(如 Kubernetes 默认调度器),原因在于:
- Kubernetes 调度器 在 Pod 数量超过 10万时,调度延迟会急剧上升,且需要大量 etcd 写入操作。
- 传统队列系统(如 RabbitMQ)无法处理百万级并发连接,且消息持久化成为瓶颈。
- 我们的方案 将“调度”与“创建”解耦,使系统能够以接近线性的方式扩展,同时保持对资源利用率的精细控制。
未来方向
当前系统仍有一些限制:例如,跨数据中心的调度尚未支持;节点间的资源均衡依赖简单的轮询策略。我们计划在后续版本中引入基于强化学习的调度优化,以及跨区域资源池的自动伸缩。
如果你对具体实现细节感兴趣,可以查看我们的开源调度库(链接),其中包含了核心调度算法的参考实现。