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 静默退出、语言运行时缺失等真实回显。

Judge0Judge0代码评测RubyRailsPostgreSQL2026/10/27

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-genericuname -r
cgroup 形态cgroup2fs(unified,v2)stat -fc %T /sys/fs/cgroup
CPU6 vCPU(Intel Core i7-14700KF)/system_info
内存18 GiB,Swap 8.0 GiB/system_info
主机名judge0-b1hostname
安装根目录/opt/judge0—
API 端口8141judge0-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 组件版本清单

组件版本安装方式
Judge01.13.1git clone 源码
Ruby2.7.0p0(2019-12-25 revision 647ee6f091)源码编译
OpenSSL1.1.1w(11 Sep 2023)源码编译
Rails5.2.4.3Gemfile 锁定
Puma4.3.5Gemfile 锁定
Resque2.0.0(resque-scheduler 4.4.0)Gemfile 锁定
pg 驱动1.2.3Gemfile 锁定
PostgreSQL14.24(14.24-0ubuntu0.22.04.1)apt
Redis6.0.16(5:6.0.16-1ubuntu1.2)apt
isolate2.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 指定
PostgreSQL127.0.0.1:5432仅本机访问
Redis127.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.1isolate 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_HOSTlocalhost默认示例值指向编排网络内的主机名
REDIS_PORT6379留空 → Redis::CannotConnectError ... invalid value for Float(): ""
REDIS_PASSWORDjudge0_redis_pwd_2026留空连不上已加密的 Redis
POSTGRES_HOSTlocalhost同上
POSTGRES_PORT5432留空会在个别驱动路径上触发空值转换异常
POSTGRES_DB / POSTGRES_USERjudge0 / judge0默认是 postgres,会连错库
POSTGRES_PASSWORDjudge0_pg_pwd_2026无默认值,必填
RAILS_ENVproduction生产模式读取 database.yml 的 production 段
RAILS_MAX_THREADS6留空 → Puma invalid value for Integer(): ""
RAILS_SERVER_PROCESSES2同上
INTERVAL0.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 不同,必须注意:

  1. ExecStart 用 resque:work 而不是 resque:workers。resque:workers 只是 Thread.new { system "rake resque:work" } 的简单包装,父进程 fork 完就返回,systemd 会立刻判定主进程退出;要并行多个消费者,正确做法是用模板单元起多个 resque:work 实例。
  2. 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 更省事。

七、配置说明

只列与安装阶段强相关的项,完整清单见配置参数专篇。

项本文取值作用域备注
PORT8141服务端不在 judge0.conf 中,由 systemd Environment= 或 rails s -p 传入
RAILS_MAX_THREADS6服务端同时决定 Puma 线程数与 database.yml 连接池
RAILS_SERVER_PROCESSES2服务端Puma cluster worker 数;总并发 = 进程 × 线程
COUNT未设消费者scripts/workers 使用;本文用单实例 resque:work
JUDGE0_VERSION1.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。

十、参考链接