OJ平台远端判题子系统开发(一):需求拆解与Docker核心知识入门

作为DoReMiFaSo团队中负责判题子系统的开发者,本周的主要工作是明确判题子系统的需求边界、调研代码隔离方案,并学习Docker容器化的核心操作。


一、判题子系统需求拆解

1.1 系统的定位

判题子系统不是OJ后端内部的判题模块,而是一个独立部署的服务,包含HTTP API、异步任务队列、判题Worker和Docker沙箱执行引擎四个组成部分。与OJ后端之间通过HTTP API解耦。

1.2 核心需求

经过与团队讨论,梳理出以下核心需求:

  1. 强隔离性:不同用户提交的代码必须相互隔离,防止恶意代码影响主系统或其他用户的代码执行。
  2. 资源可管控:精确限制代码运行时的CPU、内存和执行时间,防止单份代码耗尽服务器资源。
  3. 异步可调用:OJ后端通过HTTP API提交判题请求后可以立即返回,判题在后台异步完成。判题为典型的重操作,编译加运行通常需要数秒至十几秒,同步等待会大量占用连接资源。
  4. 并行可处理:支持多个判题请求同时执行,通过控制机制限制并发数量。
  5. 结果精准化:支持多类标准化判题结果,包括Accepted、Wrong Answer、Compile Error、Runtime Error、Time Limit Exceeded、Memory Limit Exceeded、Output Limit Exceeded、System Error等,并提供资源使用详情。
  6. 安全可加固:支持seccomp安全策略、cap-drop ALL、网络禁用等多层安全防护。

二、代码隔离方案选型

2.1 调研过程

决定用什么技术来执行用户代码,是本周花费时间最多的调研。考察了三种方案:

隔离方案 隔离性 启动速度 资源占用 开发成本
传统虚拟机 慢(分钟级) 高(GB级)
Linux namespace + cgroup 中等 快(毫秒级) 极低 极高
Docker容器化 快(秒级) 低(MB级)

最初考虑直接使用namespace + cgroup,因为Go可以通过syscall直接操作,与判题逻辑深度集成。但在调研过程中发现手动实现的工作量远超预期:需要管理PID namespace、mount namespace、网络namespace,编写cgroup资源限制逻辑,进程清理和回收也需要自行处理。调研了一些开源OJ项目(如Judge0)的实现方式,发现它们在底层均使用Docker作为沙箱。

学习容器底层原理时参考了Google Cloud的容器技术概述:Containers at Google

2.2 最终选择Docker的原因

  • Docker基于namespace + cgroup实现,封装完成度高
  • Go通过os/exec调用Docker CLI即可操作容器,集成简单
  • 镜像体系天然支持多语言环境,每种语言一个镜像
  • Docker CLI方案在本地运行和在容器化部署(Compose)场景下均能正常工作

2.3 Docker驱动方式的决策

在Docker操作方式上,对两种方案进行了评估:

  • Docker CLI:通过os/exec调用docker命令
  • Go Docker SDK:通过官方SDK调用Docker API

SDK方案提供了类型安全和API文档完善的优势,但在实践中发现其引入的间接依赖过多,且在容器化部署场景下(Judger自身作为容器通过docker socket操作宿主机Docker)SDK客户端连接配置复杂。CLI方案虽然通过字符串拼接命令,但稳定且无额外依赖。最终统一采用CLI驱动方案。


三、Docker核心知识学习

3.1 基本概念与命令

通过实际操作学习了Docker的核心概念:

  • 镜像(Image):只读模板,包含程序运行所需的所有依赖。项目使用Alpine基础镜像构建判题镜像。
  • 容器(Container):镜像的运行实例,隔离环境。每次判题创建一个新容器,判完即销毁。
  • docker exec模式:先通过docker run -d(配合sleep)创建后台容器,后续编译和运行分别通过docker exec进入容器执行。这种分步控制的方式比一次性docker run --rm更灵活。

image

Docker官方基础文档:https://docs.docker.com/get-started/overview/

3.2 资源限制参数

沙箱的核心需求之一是资源管控。Docker提供的资源限制参数:

--network none                  # 禁用网络
--cpus 1                        # 限制1个CPU核心
--memory 256m                   # 内存硬限制(OOM Kill触发退出码137)
--pids-limit 64                 # 最大进程数
--cap-drop ALL                  # 移除所有Linux Capability
--security-opt no-new-privileges # 禁止提权
--tmpfs /workspace:...          # 临时文件系统

资源限制官方文档:https://docs.docker.com/engine/containers/resource_constraints/

3.3 镜像构建

三种判题镜像均基于Alpine Linux构建,以保持最小体积:

# C++17
FROM alpine:3.19
RUN apk add --no-cache g++ libstdc++
RUN adduser -D runner
USER runner
WORKDIR /workspace
# Python 3.11  
FROM python:3.11-alpine
RUN adduser -D runner
USER runner
WORKDIR /workspace
# Go 1.22
FROM golang:1.22-alpine
RUN adduser -D runner
USER runner
WORKDIR /workspace

构建命令:

docker build -t remote-judge-cpp17 -f docker/images/cpp17/Dockerfile .
docker build -t remote-judge-go122 -f docker/images/go1.22/Dockerfile .
docker build -t remote-judge-python311 -f docker/images/python3.11/Dockerfile .

构建后的镜像体积:

镜像 体积
remote-judge-cpp17 308 MB
remote-judge-go122 348 MB
remote-judge-python311 83.7 MB

屏幕截图 2026-04-02 173836

3.4 文件传输模式

沙箱需要将用户代码和测试用例传递到容器内部。设计了两种文件传输模式:

  • Bind模式:通过 -v 绑定挂载宿主机目录到容器 /workspace,适用于本地开发调试
  • Copy模式:通过 docker exec + install 命令将文件复制到沙箱容器,适用于容器通过docker socket操作宿主Docker的场景

选择两种模式而非单一方案的原因是:Bind模式性能更好但不适用于Compose场景(Judger自身容器化后无法访问宿主机文件系统),Copy模式通用性更强但实现更复杂。


四、遇到的问题与解决

问题1:容器生命周期管理

问题描述:最初使用 docker run --rm 一次性执行命令,编译和运行合并在同一个容器中。这种方式将编译和运行耦合在一起,无法分步控制。

排查过程:发现如果编译失败需要返回错误信息(stderr),但容器已经销毁,无法获取。同时编译和运行需要不同的资源限制(编译阶段内存需求更大),合并执行无法区分。

解决方案:改为 docker run -d image sleep 3600 先创建后台容器,然后通过 docker exec 分别执行编译命令和运行命令。编译完成后可根据exitCode决定是继续运行还是提前结束,两个阶段的资源参数也可独立配置。

问题2:内存限制配置

问题描述:最初将 --memory 设为256m时,Go编译出现OOM(Out of Memory)错误。

排查过程:通过逐步增加内存限制进行测试,发现Alpine上的Go编译器本身需要一定的内存开销,加上编译过程的临时分配,256m在编译阶段不够用。

解决方案:适当调整内存限制。同时发现 --memory 是硬限制,超出后Docker会直接OOM Kill容器,退出码变为137——这个退出码后续用作Memory Limit Exceeded判定的关键信号。


五、测试验证

本周的验证工作以手动执行为主。构建了三个语言的判题镜像并确认镜像构建成功:

测试项 预期结果 实际结果
C++17镜像构建 构建成功 通过,308MB
Go 1.22镜像构建 构建成功 通过,348MB
Python 3.11镜像构建 构建成功 通过,83.7MB

六、本周总结

完成的内容

  1. 拆解判题子系统6项核心需求
  2. 对比三种代码隔离方案,确定Docker为技术基础
  3. 学习Docker基本操作、资源限制参数和镜像构建
  4. 设计Bind/Copy两种文件传输模式
  5. 统一采用CLI驱动方案,舍弃SDK方案

调研查阅的资料

posted @ 2026-04-07 12:44  宋佳奇  阅读(53)  评论(0)    收藏  举报