Judge0 在线判题系统完整实践指南

发表信息: by

Judge0 在线判题系统完整实践指南

本文深入解析如何使用 Judge0 构建生产级在线判题系统,涵盖核心技术原理、完整部署方案、性能优化策略和常见问题解决方案。

目录

  1. 概述
  2. 核心技术原理
  3. Judge0 快速部署
  4. API 使用详解
  5. 编译型语言处理
  6. 沙箱机制深入解析
  7. 构建完整的 OJ 平台
  8. 性能优化方案
  9. 常见问题与解决方案
  10. 生产环境最佳实践

1. 概述

1.1 什么是在线判题系统(OJ)

在线判题系统(Online Judge)是一个自动化评测编程题目的平台,广泛应用于:

  • 编程学习网站(如 LeetCode、牛客网)
  • 编程竞赛(ACM、ICPC、IOI)
  • 技术面试平台
  • 在线教育平台

核心功能:

  • 接收用户提交的代码
  • 编译/解释代码
  • 在安全沙箱中运行
  • 使用测试用例验证
  • 返回执行结果(通过/失败、运行时间、内存占用)

1.2 Judge0 简介

Judge0 是一个开源的、生产级的在线判题系统,具有以下特点:

开箱即用 - Docker Compose 一键部署
多语言支持 - 支持 60+ 种编程语言
RESTful API - 易于集成
安全隔离 - 基于 Isolate 沙箱
生产可用 - 众多商业平台在使用
开源免费 - MIT 协议

项目地址: https://github.com/judge0/judge0

1.3 技术栈概览

┌─────────────────────────────────────┐
│      前端/应用层                     │
│  (你的 Web 应用、移动 App 等)        │
└──────────────┬──────────────────────┘
               │ REST API
┌──────────────▼──────────────────────┐
│         Judge0 API Server            │
│    (Rails, 处理请求和结果)           │
└──────────────┬──────────────────────┘
               │ Redis 队列
┌──────────────▼──────────────────────┐
│        Judge0 Workers                │
│   (并行处理代码执行任务)             │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│      Isolate 沙箱系统                │
│  (轻量级 Linux 沙箱,执行代码)       │
└──────────────┬──────────────────────┘
               │
┌──────────────▼──────────────────────┐
│        Linux 内核特性                │
│  Namespaces, Cgroups, Seccomp       │
└─────────────────────────────────────┘

2. 核心技术原理

2.1 判题系统的核心挑战

安全性挑战:

# 用户可能提交恶意代码
import os
os.system("rm -rf /")  # 删除系统文件

while True:
    os.fork()  # Fork 炸弹

import socket
socket.socket().connect(("hacker.com", 80))  # 网络攻击

资源控制挑战:

# 无限循环
while True:
    pass

# 内存炸弹
data = []
while True:
    data.append("x" * 10000000)

多语言支持挑战:

  • 不同语言有不同的编译/运行方式
  • 资源需求差异大
  • 安全限制方式不同

2.2 Isolate 沙箱技术

Isolate 是专为编程竞赛设计的轻量级沙箱系统,使用 Linux 内核的隔离特性。

核心技术

技术 作用 示例
Namespaces 进程隔离 沙箱看不到宿主机进程
Cgroups 资源限制 限制 CPU、内存使用
Seccomp 系统调用过滤 禁止网络、文件操作
Mount Namespace 文件系统隔离 只能访问指定目录

Isolate vs Docker

特性 Isolate Docker
启动时间 ~10ms ~1s
内存占用 ~1MB ~50MB
适用场景 短时代码执行 长期运行服务
隔离强度 很强
资源统计精度 很高 中等

为什么选择 Isolate?

# 性能对比
Isolate: 启动 10ms + 执行 50ms = 60ms
Docker:  启动 1000ms + 执行 50ms = 1050ms

# 对于每秒数百次的代码执行,Isolate 快 17 倍!

2.3 Judge0 架构设计

不是每次都创建容器! Judge0 使用的是 容器池 + Isolate 沙箱复用 机制:

用户提交代码
    ↓
Judge0 API Server (接收请求)
    ↓
Redis 队列 (异步处理)
    ↓
Judge0 Worker (Docker 容器,持续运行)
    ↓
Isolate 沙箱池 (快速复用,10-50ms)
    ↓
返回结果

关键设计:

  • Worker 是 Docker 容器(4-8 个常驻容器)
  • 每个 Worker 内有 10+ 个 Isolate 沙箱(可复用)
  • 单机可同时处理 40-80 个代码执行

3. Judge0 快速部署

3.1 系统要求

最低配置:

  • CPU: 2 核
  • 内存: 4GB
  • 磁盘: 20GB
  • 系统: Linux (Ubuntu 20.04+, Debian 11+)

推荐配置:

  • CPU: 4 核+
  • 内存: 8GB+
  • 磁盘: 50GB SSD
  • 系统: Ubuntu 22.04 LTS

必需软件:

# Docker
sudo apt-get update
sudo apt-get install docker.io docker-compose

# 验证安装
docker --version
docker-compose --version

3.2 一键部署(推荐)

# 1. 下载官方配置
wget https://github.com/judge0/judge0/releases/download/v1.13.0/judge0-v1.13.0.zip
unzip judge0-v1.13.0.zip
cd judge0-v1.13.0

# 2. 修改配置(可选)
vim judge0.conf

# 关键配置项:
# REDIS_PASSWORD=your_redis_password
# POSTGRES_PASSWORD=your_postgres_password
# AUTHENTICATION_TOKEN=your_api_token  # API 认证
# WORKERS=4  # Worker 数量

# 3. 启动服务
docker-compose up -d

# 4. 查看状态
docker-compose ps

# 5. 查看日志
docker-compose logs -f judge0-server
docker-compose logs -f judge0-workers

首次启动说明:

  • 首次启动需要拉取镜像(约 2-3GB),需要 10-20 分钟
  • 数据库需要初始化,等待所有服务 healthy

3.3 验证安装

# 检查服务状态
curl http://localhost:2358/languages

# 应该返回 60+ 种语言的列表

4. API 使用详解

4.1 核心 API 端点

基础 URL: http://localhost:2358

端点 方法 说明
/submissions POST 提交代码执行
/submissions/:token GET 获取执行结果
/submissions/batch POST 批量提交
/languages GET 获取支持的语言列表
/statuses GET 获取所有状态码

4.2 提交代码执行

curl -X POST "http://localhost:2358/submissions" \
  -H "Content-Type: application/json" \
  -d '{
    "source_code": "print(\"Hello, World!\")",
    "language_id": 71,
    "stdin": "",
    "expected_output": "Hello, World!\n",
    "cpu_time_limit": 2,
    "memory_limit": 128000
  }'

# 返回
{
  "token": "d85cd024-1548-4165-96c7-7bc88673f194"
}

参数说明:

参数 类型 必需 说明
source_code string 源代码(Base64 编码可选)
language_id integer 语言 ID
stdin string 标准输入
expected_output string 期望输出(自动判题)
cpu_time_limit float CPU 时间限制(秒),默认 5
memory_limit integer 内存限制(KB),默认 128000
wall_time_limit float 墙钟时间限制(秒)

4.3 获取执行结果

# 方式一:等待结果(推荐)
curl "http://localhost:2358/submissions/d85cd024-1548-4165-96c7-7bc88673f194?base64_encoded=false&wait=true"

# 方式二:轮询
while true; do
  result=$(curl "http://localhost:2358/submissions/TOKEN")
  status=$(echo $result | jq -r '.status.id')
  if [ $status -gt 2 ]; then
    echo $result
    break
  fi
  sleep 0.5
done

返回结果:

{
  "stdout": "Hello, World!\n",
  "stderr": null,
  "compile_output": null,
  "message": null,
  "exit_code": 0,
  "exit_signal": null,
  "status": {
    "id": 3,
    "description": "Accepted"
  },
  "created_at": "2025-10-21T10:30:00.000Z",
  "finished_at": "2025-10-21T10:30:00.123Z",
  "token": "d85cd024-1548-4165-96c7-7bc88673f194",
  "time": "0.001",
  "wall_time": "0.012",
  "memory": 3428.0,
  "language_id": 71
}

状态码说明:

ID 描述 说明
1 In Queue 在队列中等待
2 Processing 正在处理
3 Accepted 通过
4 Wrong Answer 答案错误
5 Time Limit Exceeded 超时
6 Compilation Error 编译错误
7-12 Runtime Error 各种运行时错误
13 Internal Error 内部错误

4.4 常用语言 ID

LANGUAGE_IDS = {
    # 解释型语言
    'python': 71,       # Python 3.8+
    'javascript': 63,   # Node.js 12.14
    'ruby': 72,         # Ruby 2.7
    'php': 68,          # PHP 7.4
    
    # 编译型语言
    'java': 62,         # Java (OpenJDK 13)
    'cpp': 54,          # C++ (GCC 9.2)
    'c': 50,            # C (GCC 9.2)
    'csharp': 51,       # C# (Mono 6.6)
    'go': 60,           # Go 1.13
    'rust': 73,         # Rust 1.40
    
    # 其他
    'typescript': 74,   # TypeScript 3.7
    'swift': 83,        # Swift 5.2
    'kotlin': 78,       # Kotlin 1.3
}

4.5 Python 客户端封装

import requests
import time
from typing import Optional, Dict

class Judge0Client:
    def __init__(self, base_url="http://localhost:2358", api_token=None):
        self.base_url = base_url
        self.headers = {
            'Content-Type': 'application/json'
        }
        if api_token:
            self.headers['Authorization'] = f'Bearer {api_token}'
    
    def submit(
        self, 
        source_code: str,
        language_id: int,
        stdin: str = "",
        expected_output: Optional[str] = None,
        cpu_time_limit: float = 5.0,
        memory_limit: int = 128000
    ) -> str:
        """提交代码执行"""
        url = f"{self.base_url}/submissions"
        payload = {
            "source_code": source_code,
            "language_id": language_id,
            "stdin": stdin,
            "cpu_time_limit": cpu_time_limit,
            "memory_limit": memory_limit
        }
        
        if expected_output is not None:
            payload["expected_output"] = expected_output
        
        response = requests.post(url, json=payload, headers=self.headers)
        response.raise_for_status()
        return response.json()["token"]
    
    def get_result(self, token: str, wait: bool = True) -> Dict:
        """获取执行结果"""
        url = f"{self.base_url}/submissions/{token}"
        params = {"base64_encoded": "false"}
        
        if wait:
            params["wait"] = "true"
            response = requests.get(url, params=params, headers=self.headers)
            response.raise_for_status()
            return response.json()
        
        # 轮询模式
        max_attempts = 20
        for _ in range(max_attempts):
            response = requests.get(url, params=params, headers=self.headers)
            response.raise_for_status()
            result = response.json()
            
            if result["status"]["id"] > 2:
                return result
            
            time.sleep(0.5)
        
        raise TimeoutError("获取结果超时")
    
    def execute(
        self,
        source_code: str,
        language_id: int,
        stdin: str = "",
        expected_output: Optional[str] = None
    ) -> Dict:
        """提交并等待结果(便捷方法)"""
        token = self.submit(source_code, language_id, stdin, expected_output)
        return self.get_result(token, wait=True)

# 使用示例
if __name__ == '__main__':
    client = Judge0Client()
    
    python_code = '''
n = int(input())
print(n * 2)
'''
    
    result = client.execute(
        source_code=python_code,
        language_id=71,
        stdin="21"
    )
    
    print(f"状态: {result['status']['description']}")
    print(f"输出: {result['stdout']}")
    print(f"时间: {result['time']}s")
    print(f"内存: {result['memory']}KB")

5. 编译型语言处理

5.1 为什么编译型语言需要特殊处理?

编译型语言(Java, C++, Go 等)需要两个阶段

  1. 编译阶段 - 将源代码编译成可执行文件/字节码
  2. 运行阶段 - 执行编译后的文件

挑战:

  • 编译器本身需要资源(内存、时间)
  • 编译可能失败(语法错误)
  • 编译和运行的资源限制不同

5.2 Java 完整执行流程

# 流程图
用户提交 Main.java
    ↓
写入沙箱: /var/local/lib/isolate/0/box/Main.java
    ↓
编译阶段: javac Main.java (宽松限制)
    ├─ 成功 → 生成 Main.class
    └─ 失败 → 返回 Compilation Error
    ↓
运行阶段: java Main (严格限制)
    ├─ 正常 → 返回输出
    ├─ 超时 → Time Limit Exceeded
    ├─ 崩溃 → Runtime Error
    └─ 内存超限 → Memory Limit Exceeded
    ↓
清理沙箱

5.3 实际示例:执行 Java 代码

BOX_ID=0

# 1. 初始化沙箱
isolate --init --box-id=$BOX_ID
BOX_DIR=$(isolate --init --box-id=$BOX_ID)

# 2. 写入 Java 源码
cat > $BOX_DIR/Main.java << 'EOF'
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt();
        int b = sc.nextInt();
        System.out.println(a + b);
    }
}
EOF

# 3. 编译(宽松限制)
isolate --run \
  --box-id=$BOX_ID \
  --processes=128 \
  --mem=524288 \
  --time=30 \
  --wall-time=60 \
  --cg \
  --meta=$BOX_DIR/compile_meta.txt \
  --stderr=$BOX_DIR/compile_error.txt \
  --dir=/etc:noexec \
  --dir=/usr:noexec \
  --env=HOME=/tmp \
  -- /usr/bin/javac Main.java

# 检查编译结果
if [ $? -ne 0 ]; then
    echo "编译错误"
    cat $BOX_DIR/compile_error.txt
    exit 1
fi

# 4. 运行(严格限制)
echo -e "5\n10" > $BOX_DIR/input.txt

isolate --run \
  --box-id=$BOX_ID \
  --processes=60 \
  --mem=262144 \
  --time=5 \
  --wall-time=10 \
  --cg --cg-mem=262144 \
  --meta=$BOX_DIR/run_meta.txt \
  --stdin=$BOX_DIR/input.txt \
  --stdout=$BOX_DIR/output.txt \
  --stderr=$BOX_DIR/error.txt \
  --dir=/etc:noexec \
  --dir=/usr:noexec \
  --env=HOME=/tmp \
  -- /usr/bin/java -Xmx256m -Xss64m Main

# 5. 读取结果
cat $BOX_DIR/output.txt

# 6. 清理
isolate --cleanup --box-id=$BOX_ID

5.4 不同语言的配置差异

LANGUAGE_CONFIGS = {
    'python': {
        'needs_compile': False,
        'run_processes': 1,
        'run_memory': 256000,
        'run_time': 5,
    },
    'java': {
        'needs_compile': True,
        'compile_processes': 128,
        'compile_memory': 512000,
        'compile_time': 30,
        'run_processes': 60,
        'run_memory': 256000,
        'run_time': 5,
    },
    'cpp': {
        'needs_compile': True,
        'compile_processes': 10,
        'compile_memory': 512000,
        'compile_time': 10,
        'run_processes': 1,
        'run_memory': 256000,
        'run_time': 5,
    },
    'rust': {
        'needs_compile': True,
        'compile_processes': 20,
        'compile_memory': 1048576,  # 1GB
        'compile_time': 60,
        'run_processes': 1,
        'run_memory': 256000,
        'run_time': 5,
    },
}

6. 沙箱机制深入解析

6.1 Isolate 文件存储位置

核心原理:文件存在宿主机上,通过 Namespace 实现隔离感。

# 宿主机的真实目录结构
/var/local/lib/isolate/
├── 0/
│   ├── box/                    # ← 文件实际存储在这里
│   │   ├── Main.java
│   │   ├── Main.class
│   │   └── output.txt
├── 1/
│   └── box/
└── 2/
    └── box/

# 沙箱进程看到的目录(通过 Mount Namespace)
/box/                           # ← 映射到真实位置
├── Main.java
├── Main.class
└── output.txt

验证实验:

# 终端 1:在沙箱中创建文件
isolate --init --box-id=0
echo "Hello from sandbox" > /var/local/lib/isolate/0/box/test.txt
isolate --run --box-id=0 -- cat test.txt

# 终端 2:在宿主机上直接查看
cat /var/local/lib/isolate/0/box/test.txt
# 输出:Hello from sandbox
# ✅ 证明文件在宿主机上!

6.2 隔离技术详解

Mount Namespace

Isolate 通过 mount namespace 让沙箱进程看到"独立"的文件系统:

# Isolate 内部执行的操作(简化)
# 1. 创建新的 mount namespace
unshare(CLONE_NEWNS);

# 2. 挂载工作目录
mount("/var/local/lib/isolate/0/box", "/box", NULL, MS_BIND, NULL);

# 3. 挂载系统目录(只读)
mount("/etc", "/etc", NULL, MS_BIND | MS_RDONLY, NULL);
mount("/usr", "/usr", NULL, MS_BIND | MS_RDONLY, NULL);

# 4. 执行用户代码
execve("/usr/bin/python3", ["python3", "solution.py"], env);

Cgroups 资源限制

# 创建 cgroup
mkdir /sys/fs/cgroup/memory/isolate-box-0

# 设置内存限制(256MB)
echo 268435456 > /sys/fs/cgroup/memory/isolate-box-0/memory.limit_in_bytes

# 将进程加入 cgroup
echo $PID > /sys/fs/cgroup/memory/isolate-box-0/cgroup.procs

Seccomp 系统调用过滤

// Isolate 阻止危险的系统调用
struct sock_filter filter[] = {
    // 阻止网络相关
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_socket, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    
    // 阻止文件挂载
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mount, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    
    // 默认允许
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};

6.3 沙箱池复用机制

Judge0 Worker 的内部结构:

class Judge0Worker
  def initialize
    # 预分配 10 个 Isolate box
    @box_pool = Queue.new
    (0..9).each { |i| @box_pool.push(i) }
  end
  
  def process_submission(submission)
    # 1. 从池中获取一个可用的 box
    box_id = @box_pool.pop
    
    begin
      # 2. 清理并初始化
      run_cmd("isolate --cleanup --box-id=#{box_id}")
      run_cmd("isolate --init --box-id=#{box_id}")
      
      # 3. 执行代码
      result = execute_in_sandbox(box_id, submission)
      
      return result
    ensure
      # 4. 放回池中复用
      @box_pool.push(box_id)
    end
  end
end

为什么快?

# 创建新容器(Docker)
启动容器: 1000ms
初始化环境: 200ms
执行代码: 50ms
总计: 1250ms

# 复用沙箱(Isolate)
获取 box: 1ms
清理 box: 5ms
执行代码: 50ms
总计: 56ms

# 快 22 倍!

6.4 安全性测试

测试各种恶意代码:

# 1. Fork 炸弹
test_code = '''
import os
while True:
    os.fork()
'''
# Isolate 限制:--processes=1,无法 fork

# 2. 内存炸弹
test_code = '''
data = []
while True:
    data.append('x' * 10000000)
'''
# Isolate 限制:--cg-mem=262144,超过后被杀死

# 3. 无限循环
test_code = '''
while True:
    pass
'''
# Isolate 限制:--time=5,5 秒后 SIGKILL

# 4. 文件系统攻击
test_code = '''
open('/etc/passwd', 'r').read()
'''
# Mount Namespace 隔离:看不到真实的 /etc/passwd

# 5. 网络攻击
test_code = '''
import socket
socket.socket().connect(('google.com', 80))
'''
# Network Namespace 隔离:无网络访问

7. 构建完整的 OJ 平台

7.1 系统架构设计

┌─────────────────────────────────────────┐
│          前端 Web 应用                   │
│   (React/Vue, 题目列表, 代码编辑器)      │
└────────────────┬────────────────────────┘
                 │ HTTP/WebSocket
┌────────────────▼────────────────────────┐
│         后端 API 服务                    │
│  (FastAPI/Django/Express)               │
│  - 用户认证                              │
│  - 题目管理                              │
│  - 提交记录                              │
│  - 排行榜统计                            │
└─────┬────────────────────┬──────────────┘
      │                    │
      │ PostgreSQL/MySQL   │ REST API
      ▼                    ▼
┌────────────┐      ┌──────────────────┐
│   数据库    │      │   Judge0 服务    │
│            │      │  (判题引擎)      │
└────────────┘      └──────────────────┘

7.2 数据库设计

-- 用户表
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 题目表
CREATE TABLE problems (
    id SERIAL PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    description TEXT NOT NULL,
    difficulty ENUM('easy', 'medium', 'hard') NOT NULL,
    time_limit INTEGER DEFAULT 5,
    memory_limit INTEGER DEFAULT 256000,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 测试用例表
CREATE TABLE test_cases (
    id SERIAL PRIMARY KEY,
    problem_id INTEGER REFERENCES problems(id),
    input TEXT NOT NULL,
    expected_output TEXT NOT NULL,
    is_sample BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 提交记录表
CREATE TABLE submissions (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    problem_id INTEGER REFERENCES problems(id),
    language_id INTEGER NOT NULL,
    source_code TEXT NOT NULL,
    status VARCHAR(50),
    execution_time FLOAT,
    memory_used INTEGER,
    test_cases_passed INTEGER,
    test_cases_total INTEGER,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 索引
CREATE INDEX idx_submissions_user ON submissions(user_id);
CREATE INDEX idx_submissions_problem ON submissions(problem_id);
CREATE INDEX idx_submissions_status ON submissions(status);

7.3 后端 API 实现(FastAPI)

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import asyncio

app = FastAPI()

class SubmitCodeRequest(BaseModel):
    problem_id: int
    language_id: int
    source_code: str

@app.post("/api/submit")
async def submit_code(request: SubmitCodeRequest, user_id: int):
    """提交代码"""
    
    # 1. 获取题目和测试用例
    problem = await db.get_problem(request.problem_id)
    test_cases = await db.get_test_cases(request.problem_id)
    
    # 2. 创建提交记录
    submission = await db.create_submission(
        user_id=user_id,
        problem_id=request.problem_id,
        language_id=request.language_id,
        source_code=request.source_code,
        status="Judging"
    )
    
    # 3. 异步判题
    asyncio.create_task(
        judge_submission(submission.id, request, problem, test_cases)
    )
    
    return {"submission_id": submission.id, "status": "Judging"}

async def judge_submission(submission_id, request, problem, test_cases):
    """判题逻辑"""
    passed_count = 0
    max_time = 0.0
    max_memory = 0
    
    for test_case in test_cases:
        # 提交到 Judge0
        token = judge0.submit(
            source_code=request.source_code,
            language_id=request.language_id,
            stdin=test_case.input,
            expected_output=test_case.expected_output,
            cpu_time_limit=problem.time_limit,
            memory_limit=problem.memory_limit
        )
        
        # 等待结果
        result = judge0.get_result(token, wait=True)
        
        if result['status']['id'] == 3:  # Accepted
            passed_count += 1
        else:
            break  # 第一个失败就停止
        
        max_time = max(max_time, float(result['time'] or 0))
        max_memory = max(max_memory, int(result['memory'] or 0))
    
    # 更新提交状态
    final_status = "Accepted" if passed_count == len(test_cases) else "Wrong Answer"
    await db.update_submission(
        submission_id=submission_id,
        status=final_status,
        execution_time=max_time,
        memory_used=max_memory,
        test_cases_passed=passed_count
    )

8. 性能优化方案

8.1 Judge0 配置优化

# judge0.conf 生产环境配置

# 并发配置
WORKERS=8                        # Worker 数量(建议 = CPU 核心数)
MAX_QUEUE_SIZE=200              # 最大队列长度
ISOLATE_BOXES_PER_WORKER=10     # 每个 worker 的沙箱数

# 资源限制
MAX_CPU_TIME_LIMIT=15
MAX_MEMORY_LIMIT=512000

# 安全配置
ENABLE_NETWORK=false
ALLOW_ENABLE_NETWORK=false

8.2 使用 tmpfs 加速

# docker-compose.yml
services:
  judge0-workers:
    image: judge0/judge0:1.13.0
    volumes:
      # 使用内存文件系统
      - type: tmpfs
        target: /var/local/lib/isolate
        tmpfs:
          size: 1G

性能提升:

  • 磁盘 I/O:~100 MB/s → 内存:~10 GB/s
  • 延迟:~10ms → ~0.1ms

8.3 编译缓存优化

import hashlib

class CompilationCache:
    def get_code_hash(self, source_code, language_id):
        content = f"{language_id}:{source_code}"
        return hashlib.sha256(content.encode()).hexdigest()
    
    def compile_with_cache(self, source_code, language_id):
        code_hash = self.get_code_hash(source_code, language_id)
        
        # 检查缓存
        cached = self.get_cached_binary(code_hash)
        if cached:
            return {"success": True, "binary": cached, "from_cache": True}
        
        # 编译并缓存
        result = compile_code(source_code, language_id)
        if result["success"]:
            self.cache_binary(code_hash, result["binary"])
        
        return result

8.4 数据库优化

-- 添加索引
CREATE INDEX idx_submissions_user_problem ON submissions(user_id, problem_id);
CREATE INDEX idx_submissions_created_at ON submissions(created_at DESC);

-- 分区表(按时间)
CREATE TABLE submissions_2025_01 PARTITION OF submissions
    FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');

-- 统计缓存表
CREATE TABLE problem_statistics (
    problem_id INTEGER PRIMARY KEY,
    total_submissions INTEGER DEFAULT 0,
    accepted_submissions INTEGER DEFAULT 0,
    acceptance_rate FLOAT,
    last_updated TIMESTAMP
);

9. 常见问题与解决方案

9.1 Judge0 启动失败

# 查看日志
docker-compose logs judge0-workers

# 常见错误:
# 1. Cannot connect to Redis
docker-compose restart judge0-redis

# 2. Permission denied
# 确保 docker-compose.yml 中有 privileged: true

# 3. Database not ready
# 等待数据库初始化完成

9.2 代码执行超时

可能原因:

  1. 用户代码有死循环(正常行为)
  2. 时间限制设置过严
  3. JVM 启动时间过长(Java 需要更长的 wall_time)

解决:

judge0.submit(
    source_code=code,
    language_id=62,  # Java
    cpu_time_limit=10,
    wall_time_limit=20  # 给 JVM 更多时间
)

9.3 编译错误但代码正确

// 问题:文件名与类名不匹配
// 用户提交:
public class Solution {  // 类名是 Solution
    public static void main(String[] args) {
        System.out.println("Hello");
    }
}

// 但保存为 Main.java

// 解决:动态提取类名
import re

def extract_java_class_name(code):
    match = re.search(r'public\s+class\s+(\w+)', code)
    return match.group(1) if match else "Main"

class_name = extract_java_class_name(source_code)
file_name = f"{class_name}.java"

9.4 判题结果不一致

原因:

  1. 浮点数精度问题
  2. 输出格式差异(多余空格/换行)
  3. 随机数未固定种子

解决:

def normalize_output(text):
    return '\n'.join(line.strip() for line in text.strip().split('\n'))

def compare_float_output(expected, actual, epsilon=1e-6):
    try:
        exp_val = float(expected.strip())
        act_val = float(actual.strip())
        return abs(exp_val - act_val) < epsilon
    except:
        return expected.strip() == actual.strip()

9.5 特殊字符处理

# 使用 Base64 编码处理特殊字符
import base64

encoded_code = base64.b64encode(source_code.encode('utf-8')).decode('ascii')

response = requests.post(
    f"{judge0_url}/submissions",
    json={
        "source_code": encoded_code,
        "language_id": 71,
        "base64_encoded": "true"  # 重要!
    }
)

10. 生产环境最佳实践

10.1 安全加固

# 1. API 认证
# judge0.conf
AUTHENTICATION_TOKEN=your-secure-token

# 2. 使用 HTTPS
# nginx 配置
server {
    listen 443 ssl http2;
    server_name judge.example.com;
    
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    
    location / {
        proxy_pass http://judge0:2358;
    }
}

# 3. 限制请求频率
limit_req_zone $binary_remote_addr zone=submissions:10m rate=10r/s;

10.2 监控告警

from prometheus_client import Counter, Histogram

submission_counter = Counter(
    'submissions_total',
    'Total submissions',
    ['language', 'status']
)

execution_time = Histogram(
    'execution_time_seconds',
    'Execution time',
    ['language']
)

# 在判题逻辑中记录指标
submission_counter.labels(language='python', status='Accepted').inc()
execution_time.labels(language='python').observe(0.123)

10.3 备份策略

#!/bin/bash
# 定期备份脚本

BACKUP_DIR="/backup/judge0"
DATE=$(date +%Y%m%d_%H%M%S)

# 备份数据库
docker-compose exec -T judge0-db pg_dump -U judge0 judge0 > \
    "$BACKUP_DIR/db_$DATE.sql"

# 清理旧备份(保留 30 天)
find "$BACKUP_DIR" -name "*.sql" -mtime +30 -delete

# Crontab: 0 2 * * * /path/to/backup.sh

10.4 日志管理

import logging
import json

class JSONFormatter(logging.Formatter):
    def format(self, record):
        return json.dumps({
            "timestamp": datetime.utcnow().isoformat(),
            "level": record.levelname,
            "message": record.getMessage(),
            "submission_id": getattr(record, 'submission_id', None)
        })

logger = logging.getLogger('judge0_app')
handler = logging.FileHandler('/var/log/judge0/app.log')
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)

11. 总结与参考资源

11.1 关键要点回顾

Judge0 核心优势:

  • ✅ 开箱即用,降低开发成本
  • ✅ 基于 Isolate,性能卓越(10-50ms 启动)
  • ✅ 支持 60+ 种语言
  • ✅ 生产级稳定性

系统架构要点:

  • Judge0 使用 Worker + Isolate 沙箱池复用机制
  • 文件存储在宿主机,通过 Namespace 隔离
  • 编译型语言需要两阶段处理
  • 使用 Cgroups、Seccomp、Namespace 多层安全隔离

性能优化策略:

  • 使用 tmpfs 加速 I/O
  • 实现编译缓存
  • 批量提交
  • 负载均衡
  • 数据库优化

11.2 参考资源

官方文档:

  • Judge0 GitHub: https://github.com/judge0/judge0
  • Judge0 API 文档: https://ce.judge0.com/
  • Isolate GitHub: https://github.com/ioi/isolate

相关技术:

  • Linux Namespaces: https://man7.org/linux/man-pages/man7/namespaces.7.html
  • Cgroups: https://www.kernel.org/doc/Documentation/cgroup-v1/
  • Seccomp: https://www.kernel.org/doc/Documentation/prctl/seccomp_filter.txt

开源项目参考:

  • DMOJ: https://github.com/DMOJ/judge-server
  • Codeforces Polygon: https://polygon.codeforces.com/

11.3 快速启动命令

# 一键部署
wget https://github.com/judge0/judge0/releases/download/v1.13.0/judge0-v1.13.0.zip
unzip judge0-v1.13.0.zip
cd judge0-v1.13.0
docker-compose up -d

# 测试
curl http://localhost:2358/languages

# 提交代码
curl -X POST "http://localhost:2358/submissions" \
  -H "Content-Type: application/json" \
  -d '{"source_code":"print(42)","language_id":71}'

结语

通过本指南,你已经掌握了:

  1. ✅ Judge0 的工作原理和部署方法
  2. ✅ Isolate 沙箱的技术细节
  3. ✅ 编译型语言的特殊处理
  4. ✅ 完整 OJ 平台的架构设计
  5. ✅ 性能优化和问题排查
  6. ✅ 生产环境的最佳实践

下一步建议:

  • 动手部署 Judge0,运行测试
  • 实现简单的 Web 界面
  • 添加自己的题目和测试用例
  • 根据需求定制功能
  • 监控性能,持续优化

祝你构建出色的在线判题系统!🎉