OJ平台远端判题子系统开发(八):Compose部署、端到端验证与项目总结
本次的核心工作是将判题子系统从"开发环境可运行"升级为"一键部署可验证"的完整系统。具体包括Docker Compose四服务部署编排、Compose环境下的Bug修复以及项目的全面总结。
一、Docker Compose四服务部署
1.1 部署架构
四个服务角色:
| 服务 | 角色 | 关键配置 |
|---|---|---|
| mysql | 持久化仓储 | 端口3306,健康检查 mysqladmin ping |
| rabbitmq | 异步消息队列 | 端口5672,管理界面15672 |
| judger | 独立判题引擎 | gRPC监听9090,挂载 /var/run/docker.sock |
| server | HTTP API + Worker | 端口8080,Remote模式连接judger |
1.2 配置要点
services:
judger:
build:
context: .
dockerfile: docker/Dockerfile.judger
ports:
- "9090:9090"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- REMOTE_JUDGE_SANDBOX=docker
- REMOTE_JUDGE_DOCKER_TRANSFER=copy
- REMOTE_JUDGE_SECCOMP_PROFILE=
depends_on:
mysql:
condition: service_healthy
rabbitmq:
condition: service_healthy
healthcheck:
test: ["CMD", "grpc-health-probe", "-addr=:9090"]
server:
build:
context: .
dockerfile: docker/Dockerfile.server
ports:
- "8080:8080"
environment:
- REMOTE_JUDGE_JUDGER_MODE=remote
- REMOTE_JUDGE_JUDGER_ADDR=judger:9090
- REMOTE_JUDGE_REPOSITORY=mysql
- REMOTE_JUDGE_QUEUE=rabbitmq
- REMOTE_JUDGE_MYSQL_DSN=root:root@tcp(mysql:3306)/remote_judge?parseTime=true
- REMOTE_JUDGE_RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672/
三个关键配置决策:
- docker socket挂载而非特权模式:judger容器通过挂载docker socket获得创建沙箱容器的能力,而非使用privileged模式。安全性更好。
- Copy模式而非Bind模式:Compose下judger自身是容器,无法直接挂载宿主机目录到沙箱容器。Boot模式要求宿主路径在两个独立的容器之间共享,而Copy模式不依赖此前提。
- 健康检查依赖:
depends_on配合condition: service_healthy确保mysql和rabbitmq就绪后server和judger才启动。
Docker Compose启动顺序控制文档:https://docs.docker.com/compose/how-tos/startup-order/
1.3 部署与停止
# 构建并启动全部服务
docker compose up -d
# 查看服务状态
docker compose ps
# NAME STATUS
# remote_judge-mysql-1 healthy
# remote_judge-rabbitmq-1 healthy
# remote_judge-judger-1 healthy
# remote_judge-server-1 healthy

停止服务:
```bash
docker compose down

二、Compose模式的端到端验证
部署完成后,使用smoke工具提交真实代码验证全链路:
go run ./cmd/smoke -addr http://127.0.0.1:8080 -lang cpp17 -mode ac
go run ./cmd/smoke -addr http://127.0.0.1:8080 -lang cpp17 -mode wa
go run ./cmd/smoke -addr http://127.0.0.1:8080 -lang cpp17 -mode ce
go run ./cmd/smoke -addr http://127.0.0.1:8080 -lang python3.11 -mode ac
四种场景全部验证通过。




三、Compose模式发现的Bug修复
Compose部署验证过程中发现并修复了两个关键Bug。
3.1 Bug 1:Copy模式下编译产物丢失
问题现象:编译阶段成功(exitCode=0),但运行阶段报 exec: "./main": no such file or directory。
根因分析:Copy模式下,编译和运行各创建独立的沙箱容器(tmpfs)。编译产物 main 留在编译容器的tmpfs中,容器销毁后文件丢失:
编译容器 (tmpfs) 运行容器 (tmpfs) Judger工作区
/workspace/main.cpp /workspace/main.cpp main.cpp(仅源码)
/workspace/main ─── ✗ ─── (不存在) (不存在)
Bind模式下不存在此问题——编译和运行容器共用宿主机挂载的工作目录,编译产物直接出现在共享文件系统中。
修复方案:增加 collectOutput 步骤。编译成功后,通过 docker exec cat 将沙箱容器内的产物文件回传到Judger工作区:
if result.ExitCode == 0 {
d.collectOutput(timeoutCtx, containerID, req.WorkDir)
}
Run步骤的 prepareWorkspace 随后将编译产物一并复制到运行容器。
3.2 Bug 2:WSL2 tmpfs默认noexec
问题现象:二进制文件已正确拷贝到运行容器,权限为755,执行时仍报 exec: "./main": permission denied。
根因分析:Docker Desktop for Windows使用WSL2后端,其tmpfs默认带有 noexec 挂载标志:
$ docker run --rm -it --tmpfs /workspace alpine:3.19 sh
/workspace $ mount | grep workspace
tmpfs on /workspace type tmpfs (rw,noexec,relatime,size=65536k)
noexec 标志导致tmpfs上的任何二进制文件都无法执行,即使权限正确。
修复方案(两个改动):
- tmpfs挂载参数中显式添加
,exec:
--tmpfs "/workspace:size=64m,uid=1000,gid=1000,mode=0775,exec"
- 使用
install -m 755替代cat >+chmod创建需要执行权限的文件。--cap-drop ALL移除了CAP_FOWNER,容器内无法使用chmod修改文件权限,因此通过install命令直接创建带执行权限的文件:
cmd := exec.CommandContext(ctx, "docker", "exec", "-i", containerID,
"sh", "-c",
fmt.Sprintf("install -m 755 /dev/null '%s' && cat > '%s'", targetPath, targetPath),
)
该问题仅影响Windows Docker Desktop(WSL2)环境,Linux生产环境的tmpfs默认可执行,不受影响。
四、项目总结
4.1 项目规模
| 维度 | 数据 |
|---|---|
| Go代码行数 | 约3000行 |
| 包数量 | 12个internal包 + 4个cmd入口 |
| 测试覆盖 | 14个测试文件,80个测试函数(Mock 71 + Docker 9),10个包全部通过 |
| Docker镜像 | 3个判题镜像(cpp17/go1.22/python3.11)+ 2个服务镜像(server/judger) |
| Docker集成测试 | 8个场景覆盖全部判题状态 |
| 外部系统对接 | RabbitMQ、MySQL、Docker三套 |
4.2 系统架构总览
| 维度 | 实现方案 |
|---|---|
| 对外接口 | HTTP REST API(7个接口) |
| 内部通信 | gRPC + 自定义JSON Codec |
| 调用方式 | 异步队列驱动 |
| 并发控制 | Worker令牌池(Buffered Channel) |
| Docker操作 | CLI驱动(os/exec调用docker命令) |
| 语言支持 | C++17、Go 1.22、Python 3.11 |
| 判题状态 | 4中间态 + 7终态 + System Error |
| 部署方式 | Docker Compose四服务,docker socket挂载 |
| 安全策略 | cap-drop ALL + Seccomp黑名单 + 禁网 + 非root用户 + pids-limit |
4.3 核心能力清单
- HTTP API提交与查询(7个接口)
- 异步任务队列 + Worker受控并发(默认4并发)
- Docker CLI沙箱(Bind/Copy双文件传输模式)
- 熔断降级(3次失败→Open,30s→HalfOpen,1次成功→Closed)
- Seccomp安全策略(黑名单模式,嵌入二进制)
- 三语言判题(C++17/Go 1.22/Python 3.11)
- 9种标准化判题结果
- 内存 + MySQL双仓储
- 内存 + RabbitMQ双队列
- Embedded/Remote双Judger模式
- 临时目录对象池
- 镜像预拉取
- gRPC压测工具 + HTTP压测工具
- 结构化日志 + TraceId全链路追踪
- Docker Compose四服务一键部署
4.4 技术栈
| 技术 | 用途 |
|---|---|
| Go 1.25 | 开发语言 |
| Docker CLI | 容器化沙箱 |
| gRPC + JSON Codec | 内部RPC通信 |
| RabbitMQ (amqp091-go) | 异步消息队列 |
| MySQL (go-sql-driver/mysql) | 持久化仓储 |
| Alpine Linux | 基础镜像 |
| Seccomp | 系统调用安全 |
| chi Router | HTTP路由 |
4.5 从ACM参赛者视角的回顾
作为ACM参赛者,这套判题子系统的设计中有几个与"用户视角"直接相关的点:
- 判题状态的准确性:区分了AC/WA/CE/RE/TLE/MLE/OLE/SE,每种状态的判定逻辑有明确的优先级顺序。在比赛中,一个准确的错误类型反馈能节省大量调试时间。
- 异步队列的必要性:比赛开场时的集中提交正好对应异步队列的削峰填谷能力。嵌入式判题的场景中,提交入队后立即返回,参赛者通过轮询获取结果——这与主流OJ平台的体验完全一致。
- 每种判题结果附带资源使用详情(Runtime、Memory):这对参赛者在调试代码性能时有直接帮助——是算法逻辑错误(WA)还是时间空间复杂度过高(TLE/MLE),一目了然。
参考资料:
- Docker Compose Specification:https://docs.docker.com/compose/compose-file/
- Docker Compose Startup Order:https://docs.docker.com/compose/how-tos/startup-order/
- Alpine Linux:https://alpinelinux.org/
- RabbitMQ amqp091-go:https://github.com/rabbitmq/amqp091-go
- MySQL Go Driver:https://github.com/go-sql-driver/mysql
- gRPC-Go Health Probe:https://github.com/grpc-ecosystem/grpc-health-probe
- Go chi Router:https://github.com/go-chi/chi

浙公网安备 33010602011771号