Skip to content

Judge Worker 运维 ​

本文覆盖 noj-judge 的职责、运行时镜像、评测流程、队列监控、水平扩展与升级。

一句话导览:Judge 是无状态 Worker,从 Redis 拉任务、为每次评测即时创建 双容器、把结果写回 Redis;生产环境必须让它连接独立的 rootless Docker socket(见Docker daemon 权限边界)。

Worker 职责 ​

noj-judge 从 Redis 队列拉取评测任务,下载纯净评测包,为每次评测即时创建 Evaluator + Solution 双容器(用后即毁),并把结果写回 Redis。

支持多个 Judge Worker 实例水平扩展:所有实例消费同一个 Redis 队列,互不冲突。

独立节点部署 ​

如果评测节点不运行 noj-core、noj-ui 或完整源码仓库,可用 noj-cli 的 judge 子命令在独立目录初始化 Worker(不再需要下载任何安装脚本):

bash
# 首次:先准备专用 rootless Docker socket,并检查依赖
noj-cli judge install-env

# 配置并启动(必需参数必须显式给出;本命令不做交互式询问)
noj-cli judge install --dir /srv/noj-judge \
  --version v0.9.5 \
  --redis-url 'redis://:密码@127.0.0.1:6379/0' \
  --socket-path /run/noj-judge/docker.sock \
  --socket-gid "$(stat -c '%g' /run/noj-judge/docker.sock)"

首次配置必填项(缺失会报"首装必须提供 …",退出码 2):

  • --version → NOJ_VERSION:不可变 Release 版本,例如 v0.9.5; 不接受 main/latest。
  • --redis-url → REDIS_URL:与 noj-core 相同的 Redis 地址、数据库和认证信息。
  • --socket-path → JUDGE_DOCKER_SOCKET:只服务于 Judge 的 rootless Docker daemon 的 Unix socket 路径。
  • --socket-gid → JUDGE_DOCKER_SOCKET_GID:该 socket 的组 ID,必须与 stat -c '%g' <socket> 一致,否则启动前的 socket 校验会失败。

其余键(JUDGE_QUEUE / RESULT_QUEUE / 并发数等)使用内置默认值,需要改动时直接 编辑安装目录下的 .env.judge(600)。

--redis-mode local 可省去自建 Redis

独立 Judge 节点若没有现成 Redis,可加 --redis-mode local,让 CLI 在本机创建 一个仅绑定回环地址(127.0.0.1)的 Redis 容器(默认端口 16379, 容器名 noj-judge-redis),并自动生成随机口令写入 600 权限的 redis.conf。 容器只带 com.neuro-oj.component 标签、只管理自己创建的同名容器。

既有配置优先:.env.judge 已存在时 judge install 只更新 --version, 其余旗标不会生效,并会打印"以下旗标未生效(既有配置优先)"提示。 要改 Redis / socket,请直接编辑 .env.judge 或先移走该文件。

管理独立 Worker:

bash
noj-cli judge status  --dir /srv/noj-judge                         # 状态 + 脱敏配置摘要
noj-cli judge logs    --dir /srv/noj-judge [--follow]              # 日志
noj-cli judge check   --dir /srv/noj-judge                         # 配置 / Redis / 专用 socket / 镜像架构
noj-cli judge start   --dir /srv/noj-judge                         # 启动(保留现有容器)
noj-cli judge stop    --dir /srv/noj-judge                         # 停止(保留配置与 Redis 任务)
noj-cli judge upgrade --dir /srv/noj-judge                         # 升级镜像

Judge 的部署与主站部署相互独立;noj-cli 不会安装、替换或配置宿主 Docker daemon,宝塔类面板也只做探测与提示(不调用其 API)。

当前生产 Release 镜像由发布流水线提供 linux/amd64。ARM64 主机必须先确认所选 版本发布了对应 manifest;否则部署会在启动前提示架构不匹配,不能通过回退到宿主机 Docker socket 绕过该限制。

评测并发上限 ​

单个 Worker 同时执行的评测任务数由 JUDGE_MAX_CONCURRENT_JUDGES 控制:

项值
默认值2
有效范围1 – 1024(正整数)
超范围/未设置回退默认值 2

需要提高吞吐时,应结合 Docker、CPU、内存和数据库连接池容量调整该值。 跨 Worker 还会限制「同一用户同时最多 1 个评测」(按 Redis claim 协调)。

评测容器资源 ​

每个 Worker 创建的 Evaluator 和 Solution 容器默认限制为 1 个 CPU 核。可通过 JUDGE_CPU_LIMIT_MILLICORES 调整该 Worker 的统一上限:

项值
单位millicores(1000m = 1 核)
默认值1000m
有效范围100m – 16000m

未设置或超出范围时回退到 1000m,不会因为配置为 0 而变成不限制 CPU。

Docker daemon 权限边界 ​

noj-judge 需要调用 Docker API 创建评测容器。生产环境不得把应用宿主机的 /var/run/docker.sock 直接挂载给 Worker;该 socket 等价于授予 Docker daemon 控制权限,Worker 被攻破后可能影响宿主机上的其他服务。

生产部署必须选择以下一种边界:

  1. 在独立 judge 主机上运行 Docker daemon;或
  2. 在应用主机上运行只服务于 judge 的 rootless Docker daemon,并使用独立 Unix socket。

rootless Docker 安装 ​

以下步骤在宿主机上创建仅供 Judge 使用的 rootless Docker daemon。

  1. 安装依赖与 rootless 组件(需要已配置 Docker 官方 apt 源):

    bash
    sudo apt-get update
    sudo apt-get install -y uidmap docker-ce-rootless-extras
  2. 以准备运行 rootless daemon 的普通用户执行安装:

    bash
    dockerd-rootless-setuptool.sh install

    执行成功后会在该用户下创建 docker-rootless.service,默认 socket 为:

    text
    /run/user/<uid>/docker.sock

    其中 <uid> 是当前用户 ID。

  3. 创建 NOJ 专用 socket 路径,并让指定组可以访问:

    bash
    sudo mkdir -p /run/noj-judge
    sudo chown root:<judge-docker-group> /run/noj-judge
    sudo chmod 0750 /run/noj-judge
    sudo ln -sf /run/user/<uid>/docker.sock /run/noj-judge/docker.sock

    <judge-docker-group> 通常是运行 rootless Docker 的用户主组(例如 1000); 记下它的 GID,稍后写入 JUDGE_DOCKER_SOCKET_GID。

  4. 验证能否通过该 socket 访问 rootless daemon:

    bash
    DOCKER_HOST=unix:///run/noj-judge/docker.sock docker info

    能看到 daemon 信息且输出中带有 rootless/userns 相关标记即为正常。

  5. 在 NOJ 部署配置中填写:

    bash
    JUDGE_DOCKER_SOCKET=/run/noj-judge/docker.sock
    JUDGE_DOCKER_SOCKET_GID=<judge-docker-group>
    JUDGE_DOCKER_HOST=unix:///run/noj-judge/docker.sock
    JUDGE_REQUIRE_ISOLATED_DOCKER=true

不同发行版的 rootless Docker 安装方式略有差异。Fedora/RHEL 可参考 Rootless mode 官方文档。

生产 Compose 使用以下配置连接该 socket:

bash
JUDGE_DOCKER_SOCKET=/run/noj-judge/docker.sock
JUDGE_DOCKER_SOCKET_GID=10001
JUDGE_DOCKER_HOST=unix:///run/noj-judge/docker.sock
JUDGE_REQUIRE_ISOLATED_DOCKER=true

JUDGE_DOCKER_SOCKET 是宿主机上独立 daemon 的 socket 路径,不能填写应用宿主机 的 /var/run/docker.sock。JUDGE_DOCKER_SOCKET_GID 必须匹配该 socket 的组权限, Compose 会以非 root 用户运行 Worker,并只挂载该 socket 和评测缓存。

开启 JUDGE_REQUIRE_ISOLATED_DOCKER=true 后,Worker 会在启动阶段拒绝 /var/run/docker.sock 与 /run/docker.sock,也会拒绝 tcp://、http:// 等 未实现安全认证的 endpoint;校验失败时不会开始消费评测队列。开发环境可以省略 这两个变量,继续使用默认本地 daemon,但不应将该配置用于生产。

部署前检查:

bash
test "$JUDGE_DOCKER_HOST" = "unix:///run/noj-judge/docker.sock"
test "$JUDGE_REQUIRE_ISOLATED_DOCKER" = "true"

# 只应看到独立 daemon socket 和评测缓存,不得出现应用宿主机 socket、
# /var/lib/docker、/etc 或其他宿主路径。
docker compose --env-file /opt/neuro-oj/.env.prod -f /opt/neuro-oj/docker-compose.prod.yml config
docker inspect "$(docker compose --env-file /opt/neuro-oj/.env.prod -f /opt/neuro-oj/docker-compose.prod.yml ps -q judge)" \
  --format '{{json .Mounts}}'

首次发布时先启动一个 Worker,观察日志中的 Docker PING 成功信息,再执行一次 无害的样例评测;确认结果正常后再扩容其他 Worker。升级时先停止 Worker,替换 镜像并重复上述检查。若需回滚,恢复上一版本镜像和同一组 endpoint 配置,启动后 确认带有本实例标签的孤儿容器已被清理;不要通过回滚重新挂载应用宿主机 socket。

双容器运行时 ​

默认 Python 题目使用三个镜像(生产环境从 ghcr.io 拉取):

  • ghcr.io/neuro-oj/noj-evaluator-python:运行出题人的 evaluate.py。
  • ghcr.io/neuro-oj/noj-solution-python:运行用户提交的代码(硬编码 main.py)和 Solution Host。
  • ghcr.io/neuro-oj/noj-solution-ai:运行需要 CPU PyTorch、CV/ML 依赖的产物提交题和 Solution Host。

Evaluator 容器可以通过 Neuro OJ Evaluator SDK 调用 Solution 容器中的用户函数。

构建/发布评测镜像 ​

评测镜像由 GitHub Actions 在 Release 时自动构建并推送到 ghcr.io,无需在服务器上构建。

本地开发/调试时仍可使用 noj-judge/scripts/build-sdk-images.sh:

bash
cd noj-judge
./scripts/build-sdk-images.sh               # 构建三个镜像,默认 tag :latest
./scripts/build-sdk-images.sh --tag v0.1.0  # 自定义 tag

生产部署时,init system 会根据 JUDGE_IMAGE_BASE(默认 ghcr.io/neuro-oj/)写入 ghcr 全限定镜像名;若需要手工确认,见 生产部署的配置说明。

noj-evaluator-python 与 noj-solution-python 基于 python:3.12-slim,不预装题目专用依赖,题目依赖由出题人在 evaluator 中自行管理;noj-solution-ai 额外内置 CPU 版 PyTorch、torchvision 与常用 CV/ML 依赖。

镜像白名单 ​

noj-core 维护评测镜像白名单(judgeImages),并在题目 CRUD / 调度阶段完成校验。Judge Worker 侧还会按 JUDGE_IMAGE_PREFIX / JUDGE_COMMAND_WHITELIST 对 MQ 消息做一次纵深复验,不再通过 Redis RPC 拉取白名单。

镜像规则包含:

  • image:镜像名(ghcr 全限定名或裸名)。
  • kind:evaluator 或 solution。
  • mode:版本匹配模式,exact 或 all_versions。

新增或修改镜像后,需在 noj-core 的管理端「评测镜像」白名单中登记;校验在 core 侧的题目 CRUD 与调度阶段完成。

评测流程 ​

每次提交评测按以下流程执行:

  1. 从 Redis 队列拉取 JudgeTask。
  2. 获取支持包(缓存优先 → 按 noj-download:// host 分派下载 → SHA-256 校验)。
  3. 为本次评测即时创建 Evaluator + Solution 两个容器(安全 HostConfig: cap_drop ALL / network_mode none / pids_limit 等)。
    • LLM 调用题会按 JUDGE_ALLOW_EVALUATOR_NETWORK / JUDGE_EVALUATOR_NETWORK 让 Evaluator 加入指定网络(如 noj-net)以访问 noj-llm-gateway; Solution 容器始终 network_mode=none。
  4. 注入用户代码与支持包,启动双容器 NDJSON 编排。
  5. 评测完成后按 RAII 顺序清理容器(先 Solution 后 Evaluator),下次评测重新创建。

健康检查与状态查看 ​

生产环境使用 noj-cli 管理:

bash
# 查看所有服务状态(含 judge 是否在线)
noj-cli status

# 查看 judge 日志
noj-cli logs judge --follow
调高 judge 日志详细度(临时)

judge 的日志级别同时读 RUST_LOG 与 LOG_LEVEL(RUST_LOG 优先)。 用 compose 临时覆盖环境变量即可开启 debug,无需改 .env.prod:

bash
docker compose --env-file /opt/neuro-oj/.env.prod -f /opt/neuro-oj/docker-compose.prod.yml run --rm \
  -e RUST_LOG=noj_judge=debug judge

队列监控 ​

评测任务按优先级在 Redis 三级队列 noj:judge:queue:high / :medium / :low 中排队(前缀由 .env.prod 的 JUDGE_QUEUE 决定,core 与 judge 必须一致), 结果写回 noj:judge:results:

bash
docker exec noj-prod-redis-1 redis-cli -a '<REDIS_PASSWORD>' LLEN noj:judge:queue:high
docker exec noj-prod-redis-1 redis-cli -a '<REDIS_PASSWORD>' LLEN noj:judge:queue:medium
docker exec noj-prod-redis-1 redis-cli -a '<REDIS_PASSWORD>' LLEN noj:judge:queue:low

容器名由 Compose 项目名派生(name: noj-prod + service redis → noj-prod-redis-1)。用 docker ps 确认实际名称,或改用 noj-cli status 查看 compose 状态。

密码从 /opt/neuro-oj/.env.prod 的 REDIS_PASSWORD 读取。

如果队列持续堆积:

  1. 确认 Judge Worker 在线且连接了同一个 Redis(noj-cli status)。
  2. 查看 judge 日志是否有拉取/容器错误。
  3. 检查 Docker daemon 是否可用、评测镜像是否已从 ghcr.io 拉取。
  4. 如负载确实超过单实例能力,按下一节水平扩展。

水平扩展 ​

启动多个 noj-judge 实例即可分担负载:

  • 所有实例消费同一组三级队列,互不冲突。
  • 新实例启动后即可消费任务,无需额外注册。

升级与重启 ​

  • 停止实例会进入优雅关闭流程:排空正在执行的 in-flight 任务后再退出,避免提交丢失。
  • 升级步骤:修改 .env.prod 中的 NOJ_VERSION → noj-cli update。
  • 升级评测镜像后应先在 noj-core 白名单登记,再启动 Worker。

常见排查方向 ​

  • Redis 连接失败:检查 Redis 地址和服务状态。
  • Docker 连接失败:确认 Docker daemon 可用,当前用户有权限访问。
  • 镜像不存在:确认 ghcr.io 镜像已发布,且 judge_images 白名单中的镜像名与发布的 ghcr 全限定名一致。
  • 白名单为空:确认 noj-core 已启动、init system 已执行;白名单校验在 noj-core 侧完成,judge 侧使用镜像前缀白名单复验。
  • error:通常是纯净评测包、运行时配置、镜像、协议或 evaluator 本身异常,需要查看 Judge Worker 日志。
  • 提交长时间 Pending:见上文「队列监控」。

Neuro OJ 是一个独立社区项目,与 CCF、LMCC、IOAI 及 NOAI 无官方关系。