Appearance
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 被攻破后可能影响宿主机上的其他服务。
生产部署必须选择以下一种边界:
- 在独立 judge 主机上运行 Docker daemon;或
- 在应用主机上运行只服务于 judge 的 rootless Docker daemon,并使用独立 Unix socket。
rootless Docker 安装
以下步骤在宿主机上创建仅供 Judge 使用的 rootless Docker daemon。
安装依赖与 rootless 组件(需要已配置 Docker 官方 apt 源):
bashsudo apt-get update sudo apt-get install -y uidmap docker-ce-rootless-extras以准备运行 rootless daemon 的普通用户执行安装:
bashdockerd-rootless-setuptool.sh install执行成功后会在该用户下创建
docker-rootless.service,默认 socket 为:text/run/user/<uid>/docker.sock其中
<uid>是当前用户 ID。创建 NOJ 专用 socket 路径,并让指定组可以访问:
bashsudo 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。验证能否通过该 socket 访问 rootless daemon:
bashDOCKER_HOST=unix:///run/noj-judge/docker.sock docker info能看到 daemon 信息且输出中带有 rootless/userns 相关标记即为正常。
在 NOJ 部署配置中填写:
bashJUDGE_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=trueJUDGE_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 与调度阶段完成。
评测流程
每次提交评测按以下流程执行:
- 从 Redis 队列拉取 JudgeTask。
- 获取支持包(缓存优先 → 按
noj-download://host 分派下载 → SHA-256 校验)。 - 为本次评测即时创建 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。
- LLM 调用题会按
- 注入用户代码与支持包,启动双容器 NDJSON 编排。
- 评测完成后按 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+ serviceredis→noj-prod-redis-1)。用docker ps确认实际名称,或改用noj-cli status查看 compose 状态。
密码从 /opt/neuro-oj/.env.prod 的 REDIS_PASSWORD 读取。
如果队列持续堆积:
- 确认 Judge Worker 在线且连接了同一个 Redis(
noj-cli status)。 - 查看 judge 日志是否有拉取/容器错误。
- 检查 Docker daemon 是否可用、评测镜像是否已从 ghcr.io 拉取。
- 如负载确实超过单实例能力,按下一节水平扩展。
水平扩展
启动多个 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:见上文「队列监控」。