面向2万用户/1000并发的类LeetCode可扩展代码执行服务设计
背景介绍
当前正在构建类似LeetCode的在线代码执行平台,采用Fastify后端接收用户请求并转发至部署在专属VM的独立Judge服务,Judge服务为每个执行请求启动Docker容器运行代码后移除。高负载下出现容器清理失效、执行失败率上升等问题。现有两种执行路径:
- Run Code API:直接调用Judge服务(无队列,追求即时响应)
- Submit Code API:通过Redis队列+BullMQ异步执行测试用例
需将系统扩展至2万总用户、1000并发用户,满足Run Code低延迟、成本优化、沙箱可靠管理等目标,针对以下问题给出解决方案:
1. 大型平台(如LeetCode、HackerRank、Codeforces)在大规模代码执行场景下通常采用何种架构?
这类平台普遍采用分层解耦的分布式架构,核心分为四层:
- 前端接入层:负载均衡+API网关,负责请求路由、限流、鉴权
- 业务逻辑层:处理用户交互、题目管理、结果存储,与执行层完全解耦
- 执行调度层:核心是沙池集群+智能调度器,不会为每个请求单独启动容器,而是维护一批预热好的沙箱实例(轻量级容器、微型VM或专用沙箱进程),调度器根据请求类型、资源占用、并发量分配可用沙箱,执行完成后重置沙箱状态而非销毁重建
- 异步处理层:针对Submit类批量测试请求,用消息队列做任务缓冲,后台集群异步执行,执行结果通过Webhook或轮询返回用户
同时配套监控告警系统,实时追踪沙箱状态、执行成功率、延迟指标,以及日志系统用于问题排查。
2. 在此规模下,每个请求启动一个Docker容器是否仍为可行方案?
不可行。1000并发意味着同时要启动1000个Docker容器,容器启动/销毁的开销(几百毫秒到几秒)会导致Run Code的延迟无法达标,且容器清理失效的概率会随并发量指数上升,还会带来极高的资源消耗(每个容器至少占用几十MB内存+进程开销)。
如果坚持用容器,建议改为容器池化复用:提前启动固定数量的Docker容器,执行完请求后重置容器内的文件系统、环境变量,而非销毁容器。但Docker本身的隔离性和资源开销还是不如更轻量的沙箱方案。
3. Run Code API是否应引入队列,或有更优的低延迟执行模式?
Run Code追求低延迟,不适合引入队列(队列会增加至少几十毫秒的调度延迟),更优模式是预热沙箱池+实时调度:
- 提前启动一批轻量级沙箱实例(如gVisor、Firecracker、nsjail这类轻量隔离方案),保持就绪状态
- 当Run Code请求到达时,调度器直接分配可用沙箱,执行代码后立即重置沙箱(清空临时文件、恢复初始环境),放回沙箱池
- 针对突发流量,配置自动扩缩容规则,比如当沙箱使用率超过80%时自动启动新的沙箱实例,空闲时销毁多余实例
如果并发峰值超过沙箱池容量,可临时用队列做缓冲,但要设置超时阈值(比如500ms),超时则返回"当前繁忙,请稍后重试",避免用户等待过久。
4. Kubernetes是否为推荐方案,或有更合适的替代选择?
Kubernetes适合管理大规模的容器集群,但对于低延迟的代码执行场景,它的调度开销(几秒到几十秒)会成为瓶颈,且运维复杂度较高。
更合适的选择:
- 轻量容器编排工具:如Nomad,调度延迟比K8s低,运维更简单,适合批量管理沙箱实例
- 专用沙箱调度框架:如OpenFaaS、Knative,但需做定制化改造,聚焦沙池的快速调度与复用
- 自研调度器:如果团队有足够能力,基于Go或Rust开发轻量调度器,直接管理宿主机上的沙箱进程,跳过容器编排的额外开销
如果已经熟悉K8s,也可以用,但要做优化:比如用StatefulSet维护固定数量的沙箱Pod,开启Pod预调度,减少调度延迟,同时用K8s的探针监控沙箱状态,自动清理异常实例。
5. 如何处理沙箱生命周期管理与清理,避免出现孤儿容器?
核心思路是全链路的生命周期追踪+强制回收机制:
- 请求ID绑定:每个执行请求分配唯一ID,沙箱实例与请求ID绑定,调度器全程追踪沙箱的创建、执行、销毁/重置状态
- 超时强制回收:为每个沙箱设置最大执行时间(如10秒),超时后不管执行状态如何,立即终止沙箱进程并重置/销毁
- 进程监控:在宿主机上用进程管理工具(如systemd、supervisor)监控沙箱进程,一旦发现进程异常退出或变为孤儿进程,立即清理相关资源(如网络端口、文件系统挂载)
- 定时巡检:后台定时任务扫描宿主机上的沙箱实例,对比调度器的任务列表,清理未绑定有效请求ID的实例
- 容器层面防护:如果用Docker,配置
--rm参数确保容器退出时自动销毁,同时用Docker API定时清理Exited状态的容器,配合cron任务执行docker container prune -f
6. 何种成本优化架构可支撑约1000并发执行?
要平衡性能与成本,推荐混合架构:
- 轻量沙箱替代Docker:用nsjail(进程级隔离)、gVisor(轻量容器 runtime)或Firecracker(微型VM)替代Docker,资源占用仅为Docker的1/5~1/10,相同硬件可支撑更多并发
- 按需扩缩容:用云服务商的自动扩缩容功能,根据并发量自动增加/减少宿主机数量,低峰期缩小集群规模降低成本
- 沙箱池动态调整:根据Run Code和Submit Code的请求比例,动态分配沙箱池容量,比如白天Run请求多,增加就绪沙箱数量;夜间Submit请求多,减少就绪沙箱,把资源让给异步执行集群
- 资源复用:对于相同编程语言的请求,复用沙箱的运行时环境(如提前加载Python解释器、JVM),避免每次执行都重新初始化运行时
- Spot实例复用:在云平台使用Spot实例承载异步Submit请求,成本比按需实例低60%~80%,因为Submit请求不要求即时响应,即使实例被回收,重新入队即可
7. 有哪些值得研究的开源判题系统或执行架构?
- Judge0:轻量级开源判题系统,支持多种编程语言,内置沙箱管理,可配置容器池化,适合快速搭建原型
- QOJ:开源在线判题平台,采用分布式架构,支持多节点调度,适合大规模部署
- nsjail:Google开源的轻量沙箱,基于Linux Namespace和Cgroup,适合做进程级隔离,性能优异
- Firecracker:AWS开源的微型VM,启动时间仅几十毫秒,隔离性接近物理机,适合高并发场景
- Sandboxie:经典的进程级沙箱工具,可用于隔离代码执行环境,资源开销极低
内容的提问来源于stack exchange,提问作者Jnanesh




