OJ平台远端判题子系统开发(八):Compose部署、端到端验证与项目总结

本次的核心工作是将判题子系统从"开发环境可运行"升级为"一键部署可验证"的完整系统。具体包括Docker Compose四服务部署编排、Compose环境下的Bug修复以及项目的全面总结。


一、Docker Compose四服务部署

1.1 部署架构

flowchart LR A["mysql:3306"] --- B["server:8080"] C["rabbitmq:5672"] --- B B -->|"gRPC:9090"| D["judger:9090"] D -->|"docker socket"| E["Host Docker Engine"] E --> F["Sandbox Containers"]

四个服务角色:

服务 角色 关键配置
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/

三个关键配置决策:

  1. docker socket挂载而非特权模式:judger容器通过挂载docker socket获得创建沙箱容器的能力,而非使用privileged模式。安全性更好。
  2. Copy模式而非Bind模式:Compose下judger自身是容器,无法直接挂载宿主机目录到沙箱容器。Boot模式要求宿主路径在两个独立的容器之间共享,而Copy模式不依赖此前提。
  3. 健康检查依赖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


![compose_status](https://img2024.cnblogs.com/blog/3349117/202606/3349117-20260609230500339-1737764230.png)

停止服务:

```bash
docker compose down

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

四种场景全部验证通过。

smoke_cpp_ac

smoke_cpp_wa

smoke_cpp_ce

smoke_python_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上的任何二进制文件都无法执行,即使权限正确。

修复方案(两个改动):

  1. tmpfs挂载参数中显式添加 ,exec
--tmpfs "/workspace:size=64m,uid=1000,gid=1000,mode=0775,exec"
  1. 使用 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),一目了然。

参考资料

  1. Docker Compose Specification:https://docs.docker.com/compose/compose-file/
  2. Docker Compose Startup Order:https://docs.docker.com/compose/how-tos/startup-order/
  3. Alpine Linux:https://alpinelinux.org/
  4. RabbitMQ amqp091-go:https://github.com/rabbitmq/amqp091-go
  5. MySQL Go Driver:https://github.com/go-sql-driver/mysql
  6. gRPC-Go Health Probe:https://github.com/grpc-ecosystem/grpc-health-probe
  7. Go chi Router:https://github.com/go-chi/chi
posted @ 2026-06-09 23:07  宋佳奇  阅读(11)  评论(0)    收藏  举报