go-judge 判题沙箱 Docker 部署与多语言评测实战
简介这份资源面向需要自建在线判题系统OJ的开发者与运维人员围绕 go-judge 判题机提供一套可落地的本地与云服务器部署方案。针对官方资料稀缺、仅给出 C 调用样例、缺少鉴权说明等痛点内容系统梳理了服务器直装与 Docker 两种部署路径、启动参数配置、请求接口调用及常见问题排查并补充了中文文档说明。资源为 1 个 docx 文档压缩包约 1.9MB以图文步骤形式组织便于按章节查阅。目前已有 881 人学习。读者可据此快速搭建支持 C、C、Java、Python3、Python2 多语言判题的完整系统掌握 Run 接口的请求参数样例与多语言环境配置方法同时获得镜像源更换、依赖安装报错、Docker 沙箱命名空间、源码包下载异常等排错思路并参考 HOJ、QDUOJ 等开源 OJ 的配置经验适合具备一定 Linux 与容器基础的 OJ 搭建者使用。1. 从一台判题机说起go-judge 到底解决了 OJ 搭建里的哪个死结很多人第一次搭 OJ卡住的地方不是前端页面也不是题库导入而是「提交代码之后谁来跑、怎么跑、跑完怎么把结果安全地拿回来」。自己写一个subprocess调g看似三行代码真上线就会发现用户提交一个死循环把 CPU 吃满、提交一个fork炸弹把机器打挂、提交一段读文件的代码把/etc/passwd读出来。go-judge 就是为这个死结生的——它是一个用 Go 写的沙箱化判题执行引擎负责在受限环境里编译并运行选手代码返回时间、内存、退出状态和输出。它不负责题库、不负责排名只做「安全地跑一段不可信代码」这一件事所以既能塞进自建 OJ也能单独当评测后端。这篇面向的是想在自己云服务器或本地把 go-judge 跑起来、并接上多语言判题的人从 Docker 部署讲到参数边界和踩坑。2. go-judge 的沙箱模型与部署选型为什么不是随便找个容器跑代码2.1 判题沙箱要控制的四类资源理解 go-judge 之前先想清楚「跑一段不可信代码」到底要防什么。第一是 CPU 时间死循环必须能被掐断第二是内存无限申请要触发上限而不是拖垮宿主机第三是系统调用代码不该能随意open、socket、ptrace第四是进程数防止 fork 炸弹。go-judge 的做法是把每次运行放进一个受限的执行环境通过 Linux 的 cgroup 和 namespace 能力限制资源再配合 seccomp 过滤危险系统调用。它对外暴露的是一组运行参数cpuLimit、memoryLimit、procLimit、stackLimit等判题请求里带上这些值引擎按值执行。这里有个容易混淆的点go-judge 本身是「执行器」不是「完整判题系统」。它接收的是一份 JSON 形式的请求里面描述要跑哪些命令、每个命令的资源限制、输入输出怎么传。真正的判题逻辑比对输出、算分、多测试点调度通常由上层服务完成。所以部署 go-judge 时你其实是在部署一个 HTTP 服务然后让 OJ 后端去调它。2.2 本地裸机、Docker、云服务器三种部署方式怎么选常见做法有三种。第一种是直接在 Linux 裸机上编译运行 go-judge 二进制性能最好但依赖宿主机内核版本和 cgroup 配置换台机器容易翻车。第二种是用 Docker 跑官方镜像隔离干净、迁移方便也是目前最主流的方式代价是要处理好容器内的权限和 cgroup 挂载。第三种是云服务器上部署本质还是前两种的组合只是多考虑公网访问、端口和安全组。对绝大多数自建 OJ 的场景我一般推荐 Docker 方式原因是 go-judge 需要的一些内核能力比如 cgroup 控制在容器里配置更可控而且官方镜像已经把依赖打包好了。下面这张表是三种方式的对比方便你按自己的环境选。部署方式适用场景优点主要代价裸机二进制单机高性能评测无容器开销延迟低内核/cgroup 依赖强迁移麻烦Docker 容器自建 OJ、快速验证隔离好镜像即环境需要配置权限与 cgroup云服务器 Docker对外提供判题服务可远程调用易扩容端口、安全组、并发要规划选型时还要注意一点go-judge 的沙箱能力依赖 Linux 内核特性Windows 和 macOS 上通过 Docker Desktop 跑可以用于本地开发调试但不适合作为生产判题机。生产环境请用 Linux 云服务器内核建议 4.x 以上cgroup v2 更佳。2.3 用 Docker 拉起 go-judge 的最小命令先确认 Docker 可用然后拉镜像、起容器。下面是最小可运行的一套命令注意端口映射和权限参数。# 拉取 go-judge 官方镜像镜像名以实际仓库为准这里用通用写法 docker pull gojudge/go-judge:latest # 启动容器映射 5050 端口挂载 cgroup 以便沙箱控制资源 docker run -d \ --name go-judge \ -p 5050:5050 \ --privileged \ --cgroupnshost \ -v /sys/fs/cgroup:/sys/fs/cgroup:ro \ gojudge/go-judge:latest逻辑说明-p 5050:5050把容器内服务端口映射到宿主机go-judge 默认监听 5050--privileged和--cgroupnshost是为了让容器内能操作 cgroup这是沙箱限制内存和 CPU 的前提挂载/sys/fs/cgroup只读让引擎读取 cgroup 层级。参数上如果你用的是 cgroup v1 的老内核挂载路径和 namespace 参数可能要调整具体看docker info里 Cgroup 版本那一行。启动后用curl探一下服务是否活着curl -X POST http://127.0.0.1:5050/run \ -H Content-Type: application/json \ -d { cmd: [{args: [/bin/echo, hello go-judge]}], cpuLimit: 1000000000, memoryLimit: 67108864 }这段请求让沙箱执行echocpuLimit单位是纳秒这里 1 秒memoryLimit单位是字节这里 64MB。如果返回里能看到hello go-judge和状态码 0说明执行链路通了。这一步很关键很多人后面判题失败其实是服务根本没起来或者端口没通。3. 多语言判题怎么配编译命令、运行命令与资源参数3.1 判题请求的 JSON 结构拆解go-judge 的/run接口接收一个 JSON核心字段是cmd数组每个元素代表一条要执行的命令可以有多条按顺序执行。每条命令包含args命令和参数、env环境变量、files输入输出文件映射、cpuLimit、memoryLimit、procLimit等。判题时典型流程是第一条命令编译第二条命令运行编译产物。以 C 为例编译命令是g main.cpp -o main运行命令是./main。编译阶段通常给较大的内存和时间运行阶段给严格的限制。下面是一个完整的 C 判题请求示例。{ cmd: [ { args: [/usr/bin/g, main.cpp, -O2, -o, main], env: [PATH/usr/bin:/bin], files: [{content: }, {name: stdout, max: 10240}, {name: stderr, max: 10240}], cpuLimit: 10000000000, memoryLimit: 536870912, procLimit: 30 }, { args: [./main], env: [PATH/usr/bin:/bin], files: [{content: 1 2\n}, {name: stdout, max: 10240}, {name: stderr, max: 10240}], cpuLimit: 1000000000, memoryLimit: 268435456, procLimit: 1 } ] }逻辑说明files数组按位置对应标准输入、标准输出、标准错误。第一个元素{content: }是给编译命令的 stdin空第二个是 stdout 捕获max限制最大字节数防止输出爆炸。运行命令的 stdin 里放了测试输入1 2\n。procLimit在编译阶段给 30 允许编译器起子进程运行阶段给 1 防止 fork 炸弹。参数上cpuLimit和memoryLimit要按题目限制设置编译阶段可以放宽运行阶段必须收紧。3.2 各语言编译与运行命令对照不同语言的编译和运行方式差别很大下面这张表是我常用的配置覆盖 OJ 里最常见的几种语言。注意解释型语言没有编译阶段直接运行即可但要把解释器路径写对。语言编译命令运行命令备注Cg main.cpp -O2 -o main./main编译内存给 512MBCgcc main.c -O2 -o main./main同上Javajavac Main.javajava -Xmx256m Main类名必须 MainPython无python3 main.py注意解释器路径Gogo build -o main main.go./main编译较慢时间放宽Node.js无node main.js内存限制靠参数Java 有个坑javac编译时可能起多个线程procLimit要给够否则编译直接失败。Python 的坑是解释器路径容器里可能是/usr/bin/python3也可能是/usr/local/bin/python3部署后先用which python3确认。Go 编译时间明显比 C 长cpuLimit建议给到 15 秒以上。3.3 用 Python 脚本封装一次判题调用实际 OJ 后端不会手写 JSON而是用代码封装。下面是一个 Python 调用 go-judge 的最小封装把编译和运行两步串起来。import requests GOJUDGE_URL http://127.0.0.1:5050/run def judge_cpp(source_code, test_input, time_limit_ns1_000_000_000, mem_limit256*1024*1024): payload { cmd: [ { args: [/usr/bin/g, main.cpp, -O2, -o, main], files: [{content: source_code}, {name: stdout, max: 10240}, {name: stderr, max: 10240}], cpuLimit: 10_000_000_000, memoryLimit: 512*1024*1024, procLimit: 30 }, { args: [./main], files: [{content: test_input}, {name: stdout, max: 10240}, {name: stderr, max: 10240}], cpuLimit: time_limit_ns, memoryLimit: mem_limit, procLimit: 1 } ] } resp requests.post(GOJUDGE_URL, jsonpayload, timeout30) return resp.json() if __name__ __main__: result judge_cpp(#include iostream\nint main(){int a,b;std::cinab;std::coutab;}, 1 2\n) print(result)逻辑说明files第一个元素把源码作为 stdin 传给编译器编译器从 stdin 读源码需要加-参数或者用文件方式这里为了简化直接传内容实际生产建议用{name: main.cpp, content: source_code}的方式写文件。参数上timeout30是 HTTP 超时要大于判题总耗时否则请求先断了。返回结果里重点看每条命令的status、time、memory和stdout比对输出由上层做。提示go-judge 的files字段支持name和content两种写法name表示在沙箱工作目录里创建文件content表示直接提供内容。写文件用name传 stdin 用content别搞混。4. 部署与联调避坑那些让判题机「看起来正常却跑不对」的问题4.1 容器起来了但判题报权限错误现象docker run成功curl探活也返回但一提交判题就报 cgroup 相关错误或者permission denied。原因通常是容器没有拿到操作 cgroup 的权限或者 cgroup 版本和挂载方式不匹配。解决确认启动时带了--privileged和--cgroupnshost并检查docker info里的 Cgroup Version。如果是 cgroup v1挂载路径可能是/sys/fs/cgroup下的多个子系统需要分别挂载cgroup v2 则是统一层级。另一个常见原因是宿主机本身没开 cgroup 限制云服务器一般默认有但某些精简系统需要手动确认。4.2 编译通过但运行阶段内存限制不生效现象选手代码申请超大内存判题没有返回内存超限反而把容器拖慢甚至 OOM。原因多半是memoryLimit设了但 cgroup 没真正生效或者运行命令的memoryLimit被编译阶段的值覆盖。解决先确认容器内 cgroup 可写再检查运行命令的memoryLimit是否单独设置。go-judge 的每个cmd元素有独立的限制不要只设第一条。另外注意memoryLimit单位是字节写成256会被当成 256 字节直接编译失败这个单位坑我见过不止一次。4.3 多语言环境下解释器路径找不到现象C 判题正常Python 或 Node.js 提交后报exec: python3: executable file not found。原因是容器镜像里没装对应解释器或者路径不在PATH里。解决先docker exec -it go-judge which python3确认解释器是否存在不存在就换一个带多语言运行时的镜像或者自己基于官方镜像加装。env字段里的PATH要包含解释器所在目录别只写/usr/bin。Java 还要注意JAVA_HOME和javac是否都在。4.4 输出比对总是失败但程序本地跑对现象本地跑程序输出完全正确判题却一直 WA。原因通常是输出末尾的换行、空格差异或者stdout的max太小被截断。解决比对前对输出做规范化去尾部空白并检查max是否够大。另一个隐蔽原因是files里 stdin 的content没有正确传入程序读到空输入。调试时把返回的stdout和stderr打出来看比猜快得多。4.5 并发提交时判题变慢或超时现象单次判题很快多个用户同时提交就变慢甚至 HTTP 超时。原因是 go-judge 单实例的并发能力有限或者容器资源没限制导致互相抢占。解决给容器设置 CPU 和内存上限避免判题进程拖垮宿主机上层加队列控制并发数必要时横向扩多个 go-judge 实例由 OJ 后端做负载分发。HTTP 超时时间也要相应调大别用默认的几秒。5. 把 go-judge 接进 OJ 的进阶做法与验证习惯走到这一步go-judge 已经能跑单次判题了但离「一个能用的 OJ 评测后端」还差一层你得把判题结果结构化并做多测试点调度。我的习惯是在 go-judge 之上写一个薄封装服务负责接收 OJ 的判题任务、拆分测试点、逐个调用 go-judge、汇总结果。这样 go-judge 保持无状态扩容和替换都方便。验证判题机是否可靠我一般用三个层次的测试。第一层是功能测试准备几份已知正确和错误的代码覆盖 AC、WA、TLE、MLE、RE 五种结果确认每种都能正确返回。第二层是边界测试提交空代码、超长输出、死循环、fork 炸弹确认沙箱能拦住且不影响宿主机。第三层是压力测试用脚本并发提交几十个任务观察响应时间和资源占用。下面这个脚本可以快速跑一轮多语言冒烟测试。import requests, time URL http://127.0.0.1:5050/run cases { cpp_ac: { args: [/usr/bin/g, main.cpp, -O2, -o, main], files: [{name: main.cpp, content: #include iostream\nint main(){int a,b;std::cinab;std::coutab;}}], cpuLimit: 10_000_000_000, memoryLimit: 512*1024*1024, procLimit: 30 }, py_ac: { args: [/usr/bin/python3, main.py], files: [{name: main.py, content: a,bmap(int,input().split())\nprint(ab)}, {content: 1 2\n}], cpuLimit: 1_000_000_000, memoryLimit: 256*1024*1024, procLimit: 1 } } for name, cmd in cases.items(): payload {cmd: [cmd]} start time.time() r requests.post(URL, jsonpayload, timeout30) print(name, 耗时, round(time.time()-start, 3), s, r.json())逻辑说明每个用例单独构造cmdfiles里用name写源码文件用content传 stdin。跑完看返回的status和stdout是否符合预期。参数上cpuLimit和memoryLimit按语言调整Python 启动慢时间别给太紧。一个具体技巧把 go-judge 的返回结果里time和memory字段记录下来长期观察同一份代码的耗时波动。如果某天突然变慢往往是宿主机负载或者 cgroup 配置被改动了这比等用户投诉再查要主动得多。我自己的习惯是每次改完部署配置先跑一遍上面这个冒烟脚本确认五种结果都能复现再放流量进来。判题机这种东西出问题往往不是「跑不了」而是「跑得不对还看不出来」所以验证脚本比部署命令更值得花时间。希望帮到你。本文还有配套的精品资源点击获取