Ubuntu 22.04 安装 Judge0 1.13.1 指南
Ubuntu 22.04.5 源码编译部署 Judge0 1.13.1 全流程:OpenSSL 1.1.1w + Ruby 2.7.0 编译、PostgreSQL 14.24 与 Redis 6.0.16 账号密码配置、isolate 2.0 与 cgroup keeper 安装、24 条迁移与 89 条语言种子、systemd 托管 server/worker、isolate 2.0 去 --cg-timing 适配补丁;附 cgroup v2 下 isolate 1.8.1 报错、Puma 空配置强转、worker 静默退出、语言运行时缺失等真实回显。
Ubuntu 22.04 安装 Judge0 1.13.1 指南
目标:在 Ubuntu 22.04.5 LTS 上以源码编译方式安装 Judge0 1.13.1(Ruby 2.7.0 + PostgreSQL 14.24 + Redis 6.0.16 + isolate 2.0),完成数据库初始化、systemd 托管、语言种子导入与代码提交实测。文中所有命令与回显均来自 2026-10-27 的真实执行记录,cgroup v2 主机上的 isolate 适配问题是本文的重点。
一、环境信息
1.1 软硬件与系统
| 项 | 实测值 | 获取方式 |
|---|---|---|
| 操作系统 | Ubuntu 22.04.5 LTS | /etc/os-release |
| 内核 | 7.0.0-38-generic | uname -r |
| cgroup 形态 | cgroup2fs(unified,v2) | stat -fc %T /sys/fs/cgroup |
| CPU | 6 vCPU(Intel Core i7-14700KF) | /system_info |
| 内存 | 18 GiB,Swap 8.0 GiB | /system_info |
| 主机名 | judge0-b1 | hostname |
| 安装根目录 | /opt/judge0 | — |
| API 端口 | 8141 | judge0-server.service |
stat -fc %T /sys/fs/cgroup 的输出是本文的第一个关键判定点:返回 cgroup2fs 说明系统只挂载了 cgroup v2 统一层级,isolate 1.8.x 在这类主机上无法创建 memory 控制器子树,会直接报错(见 5.4)。因此本文主线采用 isolate 2.0 + isolate-cg-keeper 的组合。
1.2 组件版本清单
| 组件 | 版本 | 安装方式 |
|---|---|---|
| Judge0 | 1.13.1 | git clone 源码 |
| Ruby | 2.7.0p0(2019-12-25 revision 647ee6f091) | 源码编译 |
| OpenSSL | 1.1.1w(11 Sep 2023) | 源码编译 |
| Rails | 5.2.4.3 | Gemfile 锁定 |
| Puma | 4.3.5 | Gemfile 锁定 |
| Resque | 2.0.0(resque-scheduler 4.4.0) | Gemfile 锁定 |
| pg 驱动 | 1.2.3 | Gemfile 锁定 |
| PostgreSQL | 14.24(14.24-0ubuntu0.22.04.1) | apt |
| Redis | 6.0.16(5:6.0.16-1ubuntu1.2) | apt |
| isolate | 2.0(Git commit v2.0,2026-10-27 构建) | 源码编译 |
依赖检查的实际回显:
$ /usr/local/ruby-2.7.0/bin/ruby -v
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]
$ LD_LIBRARY_PATH=/usr/local/openssl-1.1.1/lib /usr/local/openssl-1.1.1/bin/openssl version
OpenSSL 1.1.1w 11 Sep 2023
$ psql -h 127.0.0.1 -U judge0 -d judge0 -tAc "select version()"
PostgreSQL 14.24 (Ubuntu 14.24-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0, 64-bit
$ redis-server --version
Redis server v=6.0.16 sha=00000000:0 malloc=jemalloc-5.2.1 bits=64 build=b4dcd1f8d6a59d4f
$ isolate --version
The process isolator 2.0
(c) 2012--2024 Martin Mares and Bernard Blackham
Built on 2026-10-27 from Git commit v2.0
1.3 端口与目录规划
| 用途 | 值 | 说明 |
|---|---|---|
| API 监听 | 0.0.0.0:8141 | 由 judge0-server.service 的 -p 8141 指定 |
| PostgreSQL | 127.0.0.1:5432 | 仅本机访问 |
| Redis | 127.0.0.1:6379 | 仅本机访问,需密码 |
| 应用目录 | /opt/judge0 | 源码与配置文件 |
| 隔离工作目录 | /var/local/lib/isolate/<box> | isolate 自动创建与回收 |
| 系统单元 | /etc/systemd/system/judge0-{server,worker}.service | 自建 |
二、前置条件
2.1 系统准备
apt-get update
apt-get install -y build-essential git curl wget pkg-config \
zlib1g-dev libyaml-dev libffi-dev libreadline-dev libssl-dev libpq-dev \
postgresql postgresql-contrib redis-server asciidoc-base libsystemd-dev
asciidoc-base 与 libsystemd-dev 不是可选依赖:前者提供 a2x,后者提供 systemd/sd-daemon.h,缺任一个 isolate 2.0 都编译不过(见 8.2)。
安装后的关键包版本:
$ dpkg -l | grep -E "^ii (postgresql-14|redis-server|libsystemd-dev|asciidoc-base)"
postgresql-14 14.24-0ubuntu0.22.04.1
postgresql-contrib 14+238
redis-server 5:6.0.16-1ubuntu1.2
asciidoc-base 10.1.2-1
libsystemd-dev:amd64 249.11-0ubuntu3.23
2.2 内核与权限确认
Judge0 的每一次提交都要调用 isolate 创建独立工作目录,isolate 对 /var/local/lib/isolate 与 /run/isolate 有独占要求,因此安装前需确认两件事:
$ stat -fc %T /sys/fs/cgroup
cgroup2fs
$ id -u
0
以 root 运行 isolate 是最简单的做法(isolate 会自动降权到 boxN 用户执行程序);若以普通用户运行,需把该用户加入 isolate 组。
2.3 网络与软件源
从 cache.ruby-lang.org 与 rubygems.org 下载在本环境中速度很低(实测约 30 KB/s),因此所有构建步骤都在 shell 中显式设置了出口代理;若你的主机可直连,可省略 https_proxy / HTTPS_PROXY 两行。
export https_proxy=http://192.168.1.62:7897
export HTTPS_PROXY=http://192.168.1.62:7897
三、版本选型说明
3.1 为什么必须锁定 Ruby 2.7.0
Gemfile 首行即 ruby '2.7.0',而 Rails ~> 5.0 与 Puma 4.3.5 也不兼容 Ruby 3.x 的部分关键字参数语义。Ubuntu 22.04 的软件源只有 Ruby 3.0.2:
$ ruby -v
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux-gnu]
直接用系统 Ruby 会在 bundle install 阶段或运行期报错,因此 22.04 上的正确路线是源码编译 2.7.0 到独立前缀 /usr/local/ruby-2.7.0,不动系统 Ruby。
3.2 Ruby 2.7.0 需要 OpenSSL 1.1.1
Ruby 2.7 的 openssl 扩展按 OpenSSL 1.1 的 API 编写,链接系统 libssl-dev(22.04 上是 OpenSSL 3.0.2)会在编译或 bundle install 时报 SSL_connect ... certificate verify failed (unable to get local issuer certificate)。所以要先编一份 OpenSSL 1.1.1w 到 /usr/local/openssl-1.1.1,再用 --with-openssl-dir 指给 Ruby。
3.3 isolate 版本:1.8.1 还是 2.0
Judge0 1.13.1 的 app/jobs/isolate_job.rb 是按 isolate 1.8.x 的命令行写的,默认带 --cg、--cg-timing、--cg-mem= 三个开关。两者的现实差异:
| 项 | isolate 1.8.1 | isolate 2.0 |
|---|---|---|
| cgroup v1 主机 | 支持(--cg 创建 memory/box-N) | 支持(需 isolate-cg-keeper) |
| cgroup v2 主机 | 不支持,--cg 直接失败 | 支持(需 isolate-cg-keeper + systemd slice) |
--cg-timing | 存在 | 已移除 |
--no-cg-timing | 存在 | 已移除 |
| Judge0 1.13.1 原样调用 | 适配 | 需去掉 --cg-timing(见 4.10) |
结论:Ubuntu 22.04 默认是 cgroup v2,主线只能走 isolate 2.0;若坚持 isolate 1.8.1,则必须把内核切回 cgroup v1,见第五章。
四、方案 A:源码编译全栈安装(本文主线)
4.1 方案说明
全部组件都编译或从发行版软件源安装到本机,进程由 systemd 直接托管,不引入任何编排层。适用于对进程模型、资源隔离和审计有明确要求的生产主机。
4.2 编译 OpenSSL 1.1.1w
cd /usr/local/src
curl -LO https://www.openssl.org/source/openssl-1.1.1w.tar.gz
tar xf openssl-1.1.1w.tar.gz && cd openssl-1.1.1w
./config --prefix=/usr/local/openssl-1.1.1 --openssldir=/usr/local/openssl-1.1.1 shared
make -j"$(nproc)"
make install_sw
install_sw 只装库与头文件,跳过文档,节省时间。验证时注意:该前缀未写入系统库搜索路径,直接执行二进制会报找不到 libssl.so.1.1。
$ /usr/local/openssl-1.1.1/bin/openssl version
/usr/local/openssl-1.1.1/bin/openssl: error while loading shared libraries: libssl.so.1.1: cannot open shared object file: No such file or directory
$ LD_LIBRARY_PATH=/usr/local/openssl-1.1.1/lib /usr/local/openssl-1.1.1/bin/openssl version
OpenSSL 1.1.1w 11 Sep 2023
这不是安装失败,而是预期行为:Ruby 编译期通过 --with-openssl-dir 找头文件与静态库,不需要运行时 LD_LIBRARY_PATH。
4.3 编译 Ruby 2.7.0
cd /usr/local/src
curl -LO https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.0.tar.gz
tar xf ruby-2.7.0.tar.gz && cd ruby-2.7.0
./configure --prefix=/usr/local/ruby-2.7.0 \
--with-openssl-dir=/usr/local/openssl-1.1.1 \
--disable-install-doc
make -j"$(nproc)"
make install
验证 Ruby 自身记录了正确的 OpenSSL:
$ /usr/local/ruby-2.7.0/bin/ruby -v
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-linux]
$ /usr/local/ruby-2.7.0/bin/ruby -ropenssl -e "puts OpenSSL::OPENSSL_VERSION"
OpenSSL 1.1.1w 11 Sep 2023
$ /usr/local/ruby-2.7.0/bin/ruby -e "puts RbConfig::CONFIG['configure_args']"
'--prefix=/usr/local/ruby-2.7.0' '--with-openssl-dir=/usr/local/openssl-1.1.1' '--disable-install-doc'
接着安装与 Gemfile.lock 匹配的 bundler 版本:
export PATH=/usr/local/ruby-2.7.0/bin:$PATH
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
gem install bundler -v 2.1.4
SSL_CERT_FILE 是必要的一步:自编译的 OpenSSL 没有内置 CA 路径,不指定时 gem install 会报证书校验失败。安装结果:
$ gem list bundler
bundler (2.1.4, default: 2.1.2)
4.4 安装并加固 PostgreSQL 14 与 Redis 6
两个服务都由 apt 安装,重点在账号与密码。PostgreSQL 侧创建独立账号与库:
su - postgres -c "psql -c \"CREATE ROLE judge0 LOGIN PASSWORD 'judge0_pg_pwd_2026' CREATEDB;\""
su - postgres -c "psql -c \"CREATE DATABASE judge0 OWNER judge0;\""
CREATEDB 权限是 rails db:create 阶段需要的(虽然本例已手工建库);Judge0 通过 TCP 连接 127.0.0.1:5432,Ubuntu 默认 pg_hba.conf 对 127.0.0.1/32 已是 scram-sha-256,无需改动:
$ grep -vE "^#|^$" /etc/postgresql/14/main/pg_hba.conf
local all postgres peer
local all all peer
host all all 127.0.0.1/32 scram-sha-256
host all all ::1/128 scram-sha-256
Redis 侧必须设置密码,因为 Judge0 的 config/initializers/resque.rb 会把 REDIS_PASSWORD 直接传给 Redis.new:
sed -i 's/^# requirepass foobared/requirepass judge0_redis_pwd_2026/' /etc/redis/redis.conf
systemctl restart redis-server
验证(默认 bind 127.0.0.1 ::1 保持不变):
$ redis-cli ping
NOAUTH Authentication required.
$ redis-cli -a judge0_redis_pwd_2026 ping 2>/dev/null
PONG
4.5 编译 isolate 2.0 与 cgroup keeper
git clone https://github.com/ioi/isolate.git /usr/local/src/isolate
cd /usr/local/src/isolate
make
make install
make install 会把二进制放到 /usr/local/bin/isolate,把 keeper 放到 /usr/local/sbin/isolate-cg-keeper,并安装 systemd 单元。cgroup v2 主机上 必须启用 keeper,否则任何带 --cg 的调用都会失败:
$ sudo isolate --cg --box-id=0 --init
Cannot open /run/isolate/cgroup: No such file or directory
启用官方提供的两个单元:
cp /usr/local/src/isolate/systemd/isolate.slice \
/usr/local/src/isolate/systemd/isolate.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now isolate.service
$ systemctl is-active isolate.service
active
$ cat /etc/systemd/system/isolate.service
[Unit]
Description=A trivial daemon to keep Isolate's control group hierarchy
[Service]
Type=notify
ExecStart=/usr/local/sbin/isolate-cg-keeper
Slice=isolate.slice
Delegate=true
[Install]
WantedBy=multi-user.target
启用后再验证一次隔离执行本身:
$ isolate --cg --box-id=0 --init
/var/local/lib/isolate/0
$ isolate --cg --box-id=0 --run -- /bin/echo hello_from_isolate
hello_from_isolate
OK (0.001 sec real, 0.010 sec wall)
$ isolate --cg --box-id=0 --cleanup
三条命令分别返回工作目录、程序输出 + 资源统计、无输出,说明 cgroup v2 下的隔离执行链路已经打通。
4.6 获取源码并安装 Gem 依赖
git clone --depth 1 https://github.com/judge0/judge0.git /opt/judge0
cd /opt/judge0
export PATH=/usr/local/ruby-2.7.0/bin:$PATH
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
bundle install
Judge0 的 Gem 数量不多,安装完成后可直接校验:
$ bundle check
The Gemfile's dependencies are satisfied
$ gem list puma resque pg
puma (4.3.5)
resque (2.0.0)
resque-scheduler (4.4.0)
pg (1.2.3)
4.7 最小化 judge0.conf
Judge0 读取配置的方式是「环境变量」,仓库中的 judge0.conf 是一份「可被 shell source 的键值清单」。官方打包脚本 scripts/load-config 会在 source 之后补一批默认值,裸机部署不走该脚本,因此 凡是会被直接 Integer() / Float() 强转的项必须显式写值,否则空字符串会导致启动失败。
本文实际修改的项:
| 配置项 | 值 | 不改的后果 |
|---|---|---|
REDIS_HOST | localhost | 默认示例值指向编排网络内的主机名 |
REDIS_PORT | 6379 | 留空 → Redis::CannotConnectError ... invalid value for Float(): "" |
REDIS_PASSWORD | judge0_redis_pwd_2026 | 留空连不上已加密的 Redis |
POSTGRES_HOST | localhost | 同上 |
POSTGRES_PORT | 5432 | 留空会在个别驱动路径上触发空值转换异常 |
POSTGRES_DB / POSTGRES_USER | judge0 / judge0 | 默认是 postgres,会连错库 |
POSTGRES_PASSWORD | judge0_pg_pwd_2026 | 无默认值,必填 |
RAILS_ENV | production | 生产模式读取 database.yml 的 production 段 |
RAILS_MAX_THREADS | 6 | 留空 → Puma invalid value for Integer(): "" |
RAILS_SERVER_PROCESSES | 2 | 同上 |
INTERVAL | 0.1 | 留空 → worker 静默退出(见 8.5) |
最小可用的取值片段(其余项保持仓库默认):
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=judge0_redis_pwd_2026
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=judge0
POSTGRES_USER=judge0
POSTGRES_PASSWORD=judge0_pg_pwd_2026
RAILS_ENV=production
RAILS_MAX_THREADS=6
RAILS_SERVER_PROCESSES=2
INTERVAL=0.1
其余参数的含义与默认值见同专栏《Judge0 1.13.1 配置参数详解》(本文只覆盖会阻断启动的项)。
4.8 数据库迁移与语言种子
scripts/server 内部会执行 rails db:create db:migrate db:seed,但它同时会 export | sudo tee /api/environment,这是打包发布布局专用的路径;裸机部署手工执行这三条更清晰:
cd /opt/judge0
set -a; source judge0.conf; set +a
export PATH=/usr/local/ruby-2.7.0/bin:$PATH
export RUBYOPT=-W:no-deprecated
export SECRET_KEY_BASE=$(head -c 96 /dev/urandom | base64 | tr -d '\n')
rails db:migrate
rails db:seed
迁移的实际规模与结果:
$ export PGPASSWORD=judge0_pg_pwd_2026
$ psql -h 127.0.0.1 -U judge0 -d judge0 -tAc "select count(*) from schema_migrations"
24
$ psql -h 127.0.0.1 -U judge0 -d judge0 -tAc "select count(*) from languages"
89
db/seeds.rb 只做一件事:清空并重建 languages 表,写入 89 条语言定义(含归档语言)。对外 /languages 接口只返回未归档的 47 条,两个数字不同属正常。
4.9 systemd 托管
SECRET_KEY_BASE 单独放在 /etc/judge0/secret.env,避免每次重启随机生成:
mkdir -p /etc/judge0
echo "SECRET_KEY_BASE=$(head -c 96 /dev/urandom | base64 | tr -d '\n')" > /etc/judge0/secret.env
chmod 600 /etc/judge0/secret.env
API 服务单元:
# /etc/systemd/system/judge0-server.service
[Unit]
Description=Judge0 1.13.1 API server
After=network.target postgresql.service redis-server.service
Wants=postgresql.service redis-server.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/judge0
EnvironmentFile=/opt/judge0/judge0.conf
EnvironmentFile=/etc/judge0/secret.env
Environment=PATH=/usr/local/ruby-2.7.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=JUDGE0_VERSION=1.13.1
Environment=PORT=8141
Environment=RAILS_LOG_TO_STDOUT=true
Environment=RUBYOPT=-W:no-deprecated
Environment=DISABLE_DATABASE_ENVIRONMENT_CHECK=1
ExecStart=/usr/local/ruby-2.7.0/bin/rails s -b 0.0.0.0 -p 8141
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
队列消费单元:
# /etc/systemd/system/judge0-worker.service
[Unit]
Description=Judge0 1.13.1 Resque worker
After=network.target judge0-server.service postgresql.service redis-server.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/judge0
EnvironmentFile=/opt/judge0/judge0.conf
EnvironmentFile=/etc/judge0/secret.env
Environment=PATH=/usr/local/ruby-2.7.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Environment=JUDGE0_VERSION=1.13.1
Environment=QUEUE=1.13.1
Environment=RAILS_LOG_TO_STDOUT=true
Environment=RUBYOPT=-W:no-deprecated
Environment=DISABLE_DATABASE_ENVIRONMENT_CHECK=1
ExecStart=/usr/local/ruby-2.7.0/bin/rake resque:work QUEUE=1.13.1
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
两个细节与官方 scripts/workers 不同,必须注意:
ExecStart用resque:work而不是resque:workers。resque:workers只是Thread.new { system "rake resque:work" }的简单包装,父进程 fork 完就返回,systemd 会立刻判定主进程退出;要并行多个消费者,正确做法是用模板单元起多个resque:work实例。JUDGE0_VERSION必须显式设置。它同时是 Resque 的队列名(queue_as ENV["JUDGE0_VERSION"].to_sym)和/about的版本字段来源,未设置时任务会入到unknown队列,而/about返回空版本。
启动与检查:
systemctl daemon-reload
systemctl enable --now judge0-server.service judge0-worker.service
$ systemctl is-active judge0-server judge0-worker
active
active
Puma 启动日志:
[43444] Puma starting in cluster mode...
[43444] * Version 4.3.5 (ruby 2.7.0-p0), codename: Mysterious Traveller
[43444] * Min threads: 6, max threads: 6
[43444] * Environment: production
[43444] * Process workers: 2
[43444] * Listening on tcp://0.0.0.0:8141
[43444] - Worker 1 (pid: 43483) booted, phase: 0
[43444] - Worker 0 (pid: 43481) booted, phase: 0
4.10 isolate 2.0 的 CLI 适配(去 --cg-timing)
此时服务能起来、也能查语言,但提交一定失败。原文在 app/jobs/isolate_job.rb 中,--cg-timing 的取值逻辑会在「未开启按进程/线程计时」时(即默认情况)拼上该开关,共 3 处(解压额外文件、编译、运行):
#{submission.enable_per_process_and_thread_time_limit ? (cgroups.present? ? "--no-cg-timing" : "") : "--cg-timing"} \
isolate 2.0 已删除这两个开关,实测报错:
$ curl -s -X POST "http://127.0.0.1:8141/submissions?wait=true" \
-H "Content-Type: application/json" \
-d '{"source_code":"print(\"hello judge0\")","language_id":71}' | python3 -m json.tool
{
"stdout": "Usage: isolate [<options>] <command>\n\nOptions:\n --as-uid=<uid>...",
"time": "0.0",
"memory": 0,
"stderr": "isolate: unrecognized option '--cg-timing'\n",
"token": "afcdfb5c-0a4d-47e2-acdd-2da08891ff18",
"compile_output": null,
"message": null,
"status": {
"id": 3,
"description": "Accepted"
}
}
这里有两个坑叠在一起:isolate 把用法说明打到 stdout,Judge0 原样落库;同时 状态被判定为 Accepted(id=3),因为 isolate 的退出码在 Judge0 的判定链路里没有被转换为失败。监控只看状态码就会漏掉这类全量失败。
适配方式是把这 3 行插值整行删除(保留其余开关),删除后 --cg-mem= 仍可用,内存统计取自 metadata[:cg-mem]:
cd /opt/judge0
cp app/jobs/isolate_job.rb app/jobs/isolate_job.rb.orig
python3 - <<'PY'
p = "/opt/judge0/app/jobs/isolate_job.rb"
s = open(p).read()
old = ''' #{submission.enable_per_process_and_thread_time_limit ? (cgroups.present? ? "--no-cg-timing" : "") : "--cg-timing"} \\\n'''
s = s.replace(old, "")
open(p, "w").write(s)
PY
grep -n "cg-timing\|cg-mem" app/jobs/isolate_job.rb
96: #{submission.enable_per_process_and_thread_memory_limit ? "-m " : "--cg-mem="}#{Config::MAX_MEMORY_LIMIT} \
141: #{submission.enable_per_process_and_thread_memory_limit ? "-m " : "--cg-mem="}#{Config::MAX_MEMORY_LIMIT} \
212: #{submission.enable_per_process_and_thread_memory_limit ? "-m " : "--cg-mem="}#{submission.memory_limit} \
245: submission.memory = (cgroups.present? ? metadata[:"cg-mem"] : metadata[:"max-rss"])
改完重启两个单元即可(resque:work 与 Puma 都是长驻进程,必需重启才生效):
systemctl restart judge0-server judge0-worker
💡 技术拓展:删掉
--cg-timing的代价是计时口径退化为「进程级 rusage」。isolate 1.8.x 的--cg-timing让 CPU 时间限制以整个 cgroup 为单位统计,能覆盖 fork 出的子进程;去掉后CPU_TIME_LIMIT只约束被判定的主进程,恶意程序可以通过多进程分摊 CPU 来绕过时间限制。若审计要求严格的总量计时,应回到 cgroup v1 主机使用 isolate 1.8.1(第五章),或等 Judge0 上游适配 isolate 2.0。
4.11 验证回显
版本与语言:
$ curl -s http://127.0.0.1:8141/about
{"version":"1.13.1","homepage":null,"source_code":null,"maintainer":null}
$ curl -s http://127.0.0.1:8141/languages | python3 -c "import sys,json;d=json.load(sys.stdin);print(len(d))"
47
提交一次(同步等待结果):
$ curl -s -X POST "http://127.0.0.1:8141/submissions?base64_encoded=false&wait=true" \
-H "Content-Type: application/json" \
-d '{"source_code":"echo hello judge0; echo $((6*7))","language_id":43}' | python3 -m json.tool
{
"stdout": "hello judge0\n42\n",
"time": "0.003",
"memory": 1004,
"token": "b4b2bccf-374d-4a9c-a16c-417a48fdd929",
"compile_output": null,
"stderr": null,
"message": null,
"status": {
"id": 3,
"description": "Accepted"
}
}
语言 id 43 是 Plain Text,run_cmd 为 /bin/cat text.txt,在只装了基础系统的裸机上也能直接跑通,适合作为安装后的第一条冒烟用例。memory: 1004 来自 isolate 的 cgroup 内存统计。
同一时刻数据库侧的状态:
$ PGPASSWORD=judge0_pg_pwd_2026 psql -h 127.0.0.1 -U judge0 -d judge0 -tAc \
"select token,status_id,time,memory from submissions order by id desc limit 1"
b4b2bccf-374d-4a9c-a16c-417a48fdd929|3|0.003|1004
五、方案 B:isolate 1.8.1 + cgroup v1 内核参数
5.1 方案说明
Judge0 1.13.1 的官方打包版本与文档长期基于 isolate 1.8.1,--cg-timing 等开关未经改造即可工作,计时口径也是总量口径。代价是必须让主机回到 cgroup v1 混合层级。
5.2 切换到 cgroup v1
在 GRUB 启动参数中追加 systemd.unified_cgroup_hierarchy=0:
sed -i 's/^GRUB_CMDLINE_LINUX="/GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=0 /' /etc/default/grub
update-grub
reboot
重启后确认:
$ stat -fc %T /sys/fs/cgroup
tmpfs
$ ls -d /sys/fs/cgroup/memory
/sys/fs/cgroup/memory
出现 tmpfs 与 memory 子目录,说明已回到 v1 层级,isolate 1.8.1 的 --cg 才有落点。
5.3 isolate 1.8.1 编译安装
cd /usr/local/src
git clone --branch v1.8.1 https://github.com/ioi/isolate.git isolate-1.8.1
cd isolate-1.8.1
make
make install
安装后不需要 isolate-cg-keeper,也不需要 isolate.slice。
5.4 cgroup v2 下的实测失败回显
本方案未在本文主机上做完整跑通验证,原因就是本机为 cgroup2fs。以下是同一台主机上 isolate 1.8.1 在 cgroup v2 下的原样报错,可作为「为什么必须切 v1」的证据:
$ isolate --cg -b 6 --init
Failed to create control group /sys/fs/cgroup/memory/box-6/: No such file or directory
报错被 Judge0 的反引号调用静默吞掉(@workdir 为空字符串),最终提交侧表现为:
{"message":"No such file or directory @ rb_sysopen - /box/script.py","status":{"id":13}}
状态 13 是 Internal Error,且路径 /box/script.py 少了理应有的 /var/local/lib/isolate/6 前缀——这是「工作目录创建失败」的典型指纹,排障时可直接据此判断 isolate 与 cgroup 的组合不匹配。
六、方案对比与选择建议
| 维度 | 方案 A(isolate 2.0 + cgroup v2) | 方案 B(isolate 1.8.1 + cgroup v1) |
|---|---|---|
| 主机要求 | Ubuntu 22.04 默认状态即可 | 需改 GRUB 并重启 |
| 是否需改 Judge0 源码 | 需删 3 行 --cg-timing | 不需要 |
| 内存限制 | cgroup 生效,--cg-mem= 有效 | cgroup 生效 |
| CPU 计时口径 | 进程级(删 --cg-timing 后) | 总量级 |
MAX_PROCESSES_AND_OR_THREADS | 有效(-p 依赖 --cg) | 有效 |
| 升级维护成本 | 上游尚未官方适配,需长期打补丁 | 与 1.13.1 原生匹配,但停在新旧交界 |
| 本次实测状态 | 全链路跑通(提交 Accepted) | 仅记录失败回显,未在 v1 主机验证 |
选择建议:全新主机优先 A,理由是 Ubuntu 22.04 默认 cgroup v2,退回 v1 会影响同机其他服务(尤其依赖统一层级统计的新版 systemd 组件);如果这台机器只跑 Judge0、且隔离强度要求高,选 B 更省事。
七、配置说明
只列与安装阶段强相关的项,完整清单见配置参数专篇。
| 项 | 本文取值 | 作用域 | 备注 |
|---|---|---|---|
PORT | 8141 | 服务端 | 不在 judge0.conf 中,由 systemd Environment= 或 rails s -p 传入 |
RAILS_MAX_THREADS | 6 | 服务端 | 同时决定 Puma 线程数与 database.yml 连接池 |
RAILS_SERVER_PROCESSES | 2 | 服务端 | Puma cluster worker 数;总并发 = 进程 × 线程 |
COUNT | 未设 | 消费者 | scripts/workers 使用;本文用单实例 resque:work |
JUDGE0_VERSION | 1.13.1 | 服务端 + 消费者 | 队列名与 /about 版本来源,必填 |
SECRET_KEY_BASE | /etc/judge0/secret.env | 服务端 | 随机生成并持久化,避免每次重启变化 |
REDIS_PASSWORD | 自定 | 服务端 + 消费者 | 无默认值,必填 |
八、常见问题
8.1 bundle install 报证书校验失败
SSL_connect returned=1 errno=0 state=error: certificate verify failed (unable to get local issuer certificate)
自编译 OpenSSL 1.1.1 没有默认 CA 路径。导出 SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt 后重试即可;不必重新编译 Ruby。
8.2 isolate 2.0 编译报 a2x 或 sd-daemon.h 缺失
make: a2x: No such file or directory
fatal error: systemd/sd-daemon.h: No such file or directory
补齐 asciidoc-base(提供 a2x)与 libsystemd-dev(提供头文件)后重新 make。若 apt 报 Could not get lock /var/lib/dpkg/lock-frontend,说明有残留的 apt 进程,清理锁或等待其结束后重试。
8.3 启动即报 Cannot open /run/isolate/cgroup
Cannot open /run/isolate/cgroup: No such file or directory
cgroup v2 主机上没有启用 isolate.service,/run/isolate/cgroup 不存在,isolate --cg --init 的返回码为 2。按 4.5 启用 keeper 后再试。
8.4 Puma 报 invalid value for Integer(): ""
config/puma.rb:1:in `Integer': invalid value for Integer(): "" (ArgumentError)
config/puma.rb 对 RAILS_MAX_THREADS 与 RAILS_SERVER_PROCESSES 直接调用 Integer(),空字符串即抛异常。这两个值在打包布局里由 scripts/load-config 补默认值,裸机部署必须在 judge0.conf 或单元里显式写。
8.5 worker 单元启动 1 秒即退出,退出码 0
$ systemctl is-active judge0-worker
inactive
systemctl status 显示 Result=success、ExecMainStatus=0,日志里没有任何报错。根因是 INTERVAL 为空:Resque::Worker#work 第一行执行 Float(interval) 抛 ArgumentError,异常被 rescue Exception 捕获后只写了一行日志便正常返回,于是进程以 0 退出。把 INTERVAL 设为 0.1 即恢复。
8.6 提交返回 Runtime Error (NZEC),stderr 指向 /usr/local/python-3.8.1/bin/python3
{
"stdout": null,
"time": "0.002",
"memory": 1004,
"stderr": "run: line 1: /usr/local/python-3.8.1/bin/python3: No such file or directory\n",
"message": "Exited with error status 127",
"status": {"id": 11, "description": "Runtime Error (NZEC)"}
}
db/languages/*.rb 里的 run_cmd 与 compile_cmd 是按打包发行版内的绝对路径写死的(如 /usr/local/python-3.8.1/bin/python3、/usr/local/gcc-9.2.0/bin/gcc)。裸机部署必须自行安装对应运行时并放到同路径,或改写 languages 表中的命令。修改语言命令属于数据变更,建议以 SQL 或 Rails console 维护,不要直接改 seed 文件(db:seed 会清空重建表)。
$ PGPASSWORD=judge0_pg_pwd_2026 psql -h 127.0.0.1 -U judge0 -d judge0 -c \
"update languages set run_cmd='/usr/bin/python3 script.py' where id=71"
8.7 提交结果被误记为 Accepted
--cg-timing 引发的失败在 4.10 中表现为 status.id=3,同时 stdout 里塞满了 isolate 的用法说明。判断依据不要只看状态码,还应检查 stdout 是否包含 Usage: isolate、stderr 是否包含 unrecognized option。
九、卸载清理
systemctl disable --now judge0-server judge0-worker
rm -f /etc/systemd/system/judge0-{server,worker}.service
systemctl daemon-reload
su - postgres -c "psql -c 'DROP DATABASE judge0;'"
su - postgres -c "psql -c 'DROP ROLE judge0;'"
isolate --cg --box-id=0 --cleanup 2>/dev/null || true
systemctl disable --now isolate.service
rm -rf /var/local/lib/isolate /run/isolate /opt/judge0 /etc/judge0
apt-get purge -y postgresql-14 postgresql-contrib redis-server
rm -rf /usr/local/ruby-2.7.0 /usr/local/openssl-1.1.1
rm -f /usr/local/bin/isolate /usr/local/sbin/isolate-cg-keeper
若曾按方案 B 修改 GRUB,需删除 systemd.unified_cgroup_hierarchy=0 后 update-grub 并重启,恢复 cgroup v2。
十、参考链接
- Judge0 源码仓库:https://github.com/judge0/judge0
- Judge0 1.13.1 Release:https://github.com/judge0/judge0/releases/tag/v1.13.1
- isolate 源码仓库:https://github.com/ioi/isolate
- isolate 2.0 cgroup keeper 说明:https://github.com/ioi/isolate/blob/master/systemd/isolate.service
- Ruby 2.7 构建文档:https://docs.ruby-lang.org/en/2.7.0/
- 站内:《Judge0 1.13.1 介绍与选型指南》