评测架构设计:队列 + Worker Pool + Docker 沙箱
在开发智评 Code+ 的过程中,实现一个稳定、高效、可扩展的评测系统是核心挑战之一。这一节我想记录一下「评测机调度系统」的整体架构设计与实现思路。
问题背景:评测任务不可阻塞主线程
用户提交代码后,我们需要:
-
运行用户代码,对每组测试数据进行评测;
-
返回准确的运行结果、错误信息、输出对比等;
-
保证整个过程不影响主流程,支持高并发提交。
因此,最自然的架构选择就是将评测任务异步处理,即:
-
主线程负责接收提交、写入数据库;
-
后台 Worker 拉取任务并独立完成评测。
技术架构概览
我采用的结构如下:
用户提交代码 --> HTTP API
|
V
Redis 消息队列
|
V
Worker Pool (多线程评测机)
|
V
使用 Docker 安全评测代码
|
V
回写数据库 + 存储每组测试结果
Step 1: 提交代码后入队
用户提交代码后,我们调用 EnqueueSubmission,将提交记录 JSON 化后写入 Redis 队列中:
func EnqueueSubmission(sub models.Submission) {
data, _ := json.Marshal(sub)
utils.RDB.LPush(ctx, "judge_queue", data)
}
这一步非常快,基本没有阻塞,因此可以立即返回前端一个“提交成功”的响应。
Step 2: 启动 Worker Pool 出队并评测
我设计了一个 StartJudgeWorkerPool(n int) 方法启动多个 Worker,每个 Worker 会不断地:
-
从 Redis 中阻塞式读取评测任务;
-
执行代码评测;
-
更新数据库结果。
res, err := utils.RDB.BRPop(ctx, 0*time.Second, redisQueueKey).Result()
EvaluateCode(sub.Code, sub.Language, sub.ProblemID)
这使得评测系统具备横向扩展能力:只需要增加 Worker 数量即可提升吞吐量。
Step 3: 代码运行环境封装在 Docker 中
用户代码的执行是潜在的高风险行为,因此我使用 Docker 隔离用户环境。每个评测任务会动态执行如下命令:
docker run --rm -v code_path:/app/code deepjudge-runner cpp /app/code "1 2 3"
这样不仅保证了安全性,还可以将运行环境标准化(如依赖版本、超时限制等)。
Step 4: 回写评测结果 + 测试点记录
评测完成后,会:
-
将总评测结果(Accepted/Wrong Answer等)写入 submissions 表;
-
为每个测试点写入对应的 testcase_results 记录;
-
包含运行结果、预期结果、状态、耗时等内容。
utils.DB.Model(&models.Submission{}).
Where("id = ?", sub.ID).
Updates(map[string]interface{}{...})
utils.DB.Create(&models.TestcaseResult{...})
这样用户不仅可以看到整体状态,还能逐个查看测试点通过情况。
总结:为什么使用这个架构?
技术组件 | 作用 |
---|---|
Redis | 解耦提交逻辑和评测逻辑,实现异步消息队列 |
Worker Pool | 支持并发评测,提高评测吞吐量 |
Docker | 隔离用户代码执行环境,保障系统安全 |
数据库记录 | 实现完整的评测结果追踪和用户查询接口 |
这套系统目前在我的开发机器上已经可以稳定运行,后续如果部署到线上,我会考虑添加 超时任务回收机制、失败重试机制、任务状态监控面板 等功能。