go-judge判题机从部署到多语言评测:沙箱与API配置实战指南
2026/9/23 16:32:20 网站建设 项目流程

简介:围绕 GoJudge 判题机部署与调用的中文实践指南,面向需要使用云服务器搭建 OJ 在线评测系统、但对官方文档深感资料不足的开发者与运维人员。原文结合作者实际搭建经验,整理出直接服务器部署与 Docker 部署两条路线,并补充 go-judge 官方项目说明、启动参数设置、REST 接口请求样例,覆盖 C、C++、Java、Python3、Python2 等常见语言调用方式。资源为 1 个 docx 文档,压缩包大小 1.9MB,内容约含官网地址、部署流程、Docker 镜像构建、接口调用参数示例、常见问题排查及 HOJ language.yml 配置参考等模块,可对照步骤快速完成判题环境搭建。已有 880 人浏览学习,适合正在搭建或维护 OJ 平台的后端开发者作为直接可查的操作手册。

1. 从零搭一个能跑多语言的 go-judge 判题机:先看清它到底解决了什么

如果你维护过一个 OJ 或者刷题平台,最头疼的往往不是题目本身,而是判题这层沙箱:怎么安全地编译用户提交的代码、限制 CPU 和内存、拿到 stdout 和 stderr,还不会被恶意代码把宿主机搞崩。go-judge 就是这一层的答案。它用 REST / gRPC API 对外提供服务,底层基于 go-sandbox 做隔离,把“跑一段代码”这件事封装成了黑匣子——你给一段 JSON,它给你编译结果、运行结果和状态码。这套资源把官网语焉不详的部分补全了,尤其是多语言调用参数、鉴权设置和 Docker 部署的坑。适合正在搭 OJ、想自建在线评测服务或者做代码沙箱选型的开发者,省去自己翻源码试错的一周时间。

下面按部署、参数、接口、常见问题、进阶验证的顺序拆,每一步都给了可以直接复制的命令和参数说明。

2. 部署选型:服务器裸跑和 Docker 的边界在哪

2.1 两种部署方式的差异与选型理由

go-judge 官方给了两种部署路径:直接拿可执行文件在服务器上跑,或者用 Docker 容器跑。这两种方式都不需要你额外安装 Go 环境,因为可执行文件本身已经把运行时打包好了。区别在于判题环境——也就是 g++、python、java 这类编译器和解释器——需要你自己装。

我给你的建议是:本地测试可以裸跑二进制文件,方便看日志、调参数;生产环境尽量用 Docker。原因有三点。第一,Docker 可以通过 --memory、--cpus 等参数限制容器资源,沙箱本身再做一层 cgroup 资源隔离,双层保险。第二,一个判题服务的并发能力有限,但 Docker 可以开多个实例,比如一台 16 核 32G 的机器,拆成 4 个小资源实例,前面挂一个调度服务按策略分发请求,吞吐量能上来。第三,容器挂掉后重建成本极低,不用在宿主机上留一堆残留进程。

2.2 服务器部署:下载、解压、启动与后台运行

先从 GitHub 下载对应服务器架构的压缩包,一般选 linux_amd64 版本。放到一个固定目录,解压后目录下会有两个文件:go-judge 是可执行文件,mount.yaml 是挂载配置文件,默认不用动。

# 解压后进入目录 tar -zxvf go-judge_1.8.2_linux_amd64.tar.gz # 前台启动,默认监听 localhost:5050 ./go-judge # 开放外网访问,监听所有网卡的 5051 端口 ./go-judge -http-addr=0.0.0.0:5051 # 后台运行并写日志 nohup ./go-judge -http-addr=0.0.0.0:5051 >> go-judge-log.log 2>&1 &

这里解释几个关键点。-http-addr=0.0.0.0:5051表示服务监听所有网卡,配合云服务器安全组放行对应端口,就可以通过公网 IP 访问了。nohup&的组合让进程脱离控制台运行,2>&1把标准错误合并到标准输出,统一写进日志文件——这在排查问题时很重要,因为 go-judge 的报错信息默认走 stderr。生产环境建议只监听内网地址,不要暴露公网。

2.3 Docker 部署:拉镜像、创建容器的完整步骤

Docker 方式更简单,但有个前置条件需要注意:CentOS 7 系统默认禁用 user namespaces,需要先开启。

# 检查 user namespaces 上限,0 表示禁用 cat /proc/sys/user/max_user_namespaces # 开启,设为 10000 echo user.max_user_namespaces=10000 >> /etc/sysctl.d/98-userns.conf sysctl -p reboot

开启后重启机器,然后拉镜像创建容器:

# 拉取官方镜像 docker pull criyle/go-judge # 创建容器 docker run -it --rm --privileged --shm-size=256m \ -p 5050:5050 --name=go-judge criyle/go-judge

注意这里-it是交互式终端,--rm是退出即删除容器。用宝塔面板这类 Docker 管理工具创建时,要把这两个参数去掉,否则容器关了就没了,还得重新建。--privileged是必需的,沙箱需要完整的权限来创建命名空间和 cgroup。--shm-size=256m设置共享内存大小,多线程程序编译时如果太小会报错。-p 5050:5050做端口映射,公网访问时还要在云安全组放行 5050。

2.4 Docker 部署时如何传递启动参数

裸跑时用命令行参数,Docker 里则通过环境变量传递。go-judge 的环境变量规则很简单:所有命令行参数都有对应的ES_前缀的大写环境变量。比如-http-addr对应ES_HTTP_ADDR-auth-token对应ES_AUTH_TOKEN

docker run --privileged --shm-size=256m \ -e ES_AUTH_TOKEN=JgeJeldGeDcjJHg \ -e ES_HTTP_ADDR=0.0.0.0:5050 \ -p 5050:5050 --name=go-judge criyle/go-judge

加上ES_AUTH_TOKEN之后,请求接口必须先带 Bearer Token,否则返回 401。这个鉴权方式就是 JWT,go-judge 收到的 Token 会通过密钥校验。生产环境如果纯内网部署,可以不开鉴权,因为校验本身有性能损耗;但如果服务要暴露到非完全可信的网络,就必须开。

3. 启动参数与鉴权机制:--help 里每个参数的真实用途

3.1 常用启动参数逐个拆解

进到容器里执行./go-judge --help,能看到全部参数。这里挑生产环境最需要关注的几个说透。

-parallelism控制并发执行的命令数量,默认等于 CPU 核数。这个参数决定判题机同时能跑几个评测任务,不是说并发越高越好——每个评测任务会吃满 CPU 和内存,并行度过高会导致单个任务变慢,我一般会设成 CPU 核数的一半到三分之二。

-pre-fork控制预启动的 worker 数量。预 fork 的意思是提前创建好沙箱进程池,请求到了直接分配,省去冷启动时间。默认值是 1,评测量大的场景建议调高到 2-4,内存充足的情况下能明显降低首字节响应时间。

-http-addr-grpc-addr分别指定 HTTP 和 gRPC 监听地址。注意这两个默认地址都是 localhost 加不同端口,HTTP 是 5050,gRPC 是 5051。需要同时开启 gRPC 时,加-enable-grpc参数。

-src-prefix值得单独说。它指定源码类型 copyIn 的目录前缀,比如-src-prefix=/home,/usr,允许请求中的文件路径以这些前缀开头。这个参数在特殊场景下会用,比如评测某个需要读取本地文件的题目时。

-output-limit限制每个命令的 POSIX rlimit 输出量,默认 256MiB。这个参数防的是程序疯狂打印导致磁盘写满,OJ 里通常会根据题目要求调小,比如 64MiB。

3.2 环境变量映射关系速查

命令行参数环境变量默认值说明
-http-addrES_HTTP_ADDRlocalhost:5050HTTP 监听地址
-grpc-addrES_GRPC_ADDRlocalhost:5051gRPC 监听地址
-auth-tokenES_AUTH_TOKENBearer 鉴权令牌
-parallelismES_PARALLELISMCPU 核数并发评测数量
-pre-forkES_PRE_FORK1预启动 worker 数
-cpusetES_CPUSET限制容器进程使用指定 CPU 核
-tmp-fs-paramES_TMP_FS_PARAMsize=128m,nr_inodes=4ktmpfs 挂载参数

-cpuset在混合部署时很有用。如果有其他服务在同一台机器上跑,可以用它把判题进程固定到某几个 CPU 核上,避免 CPU 资源抢占导致评测时间波动。-tmp-fs-param控制 /tmp 的大小和 inode 数量,程序运行时的临时文件都往这里写,太小会导致「No space left on device」。

3.3 Bearer Token 鉴权的调用方式

开启鉴权后,请求需要带请求头。用 curl 验证最直接:

curl -X POST http://127.0.0.1:5050/run \ -H "Content-Type: application/json" \ -H "Authorization: Bearer JgeJeldGeDcjJHg" \ -d @payload.json

这里Authorization: Bearer <token>是标准做法。go-judge 拿到 Token 后会验证其有效性,验证通过才继续处理请求。Token 本身在 go-judge 内部是字符串比对,开启后所有接口都需要带。注意如果上层 OJ 已经内网访问,开了鉴权会多一次解析开销,但安全性提升明显——尤其当你的判题机和其他服务共用一台机器时,我强烈建议开。

4. 请求接口与多语言参数样例:从 C++ 到 Java、Python 的完整 JSON

4.1 /run 接口请求体结构拆解

go-judge 最核心的接口是 POST /run,请求体是一个 JSON 对象,包含 cmd 数组和可选参数。每个 cmd 描述一条命令,数组里多条命令按顺序执行。核心字段如下:

字段类型说明
argsstring[]命令及参数,如 ["/usr/bin/g++", "a.cc", "-o", "a"]
envstring[]环境变量,推荐显式指定 PATH
filesobject[]标准输入、标准输出、标准错误的定义
cpuLimitintCPU 时间限制,纳秒,建议至少 1e9(1 秒)
memoryLimitint内存限制,字节,注意是虚拟内存
procLimitint进程/线程数限制
copyInobject传入文件,内容是源码或二进制
copyOutstring[]需要返回的文件,stdout/stderr 直接返回内容
copyOutCachedstring[]需要缓存的文件,编译产物用这个,后续命令可通过缓存路径引用

files 数组里通常放三个对象:第一个是标准输入内容,content 为空表示空输入;第二个和第三个分别定义 stdout 和 stderr 的读取方式,name 固定为 "stdout"/"stderr",max 表示最多读取多少字节。超过 max 多的部分会被丢弃。

4.2 C 和 C++:编译与运行两阶段模型

C++ 的评测请求要让 go-judge 跑两个命令:先编译,再运行编译产物。编译阶段的 copyIn 是源码,copyOutCached 是编译产物 a;运行阶段的 copyIn 直接引用编译产物的缓存路径。

{ "cmd": [ { "args": ["/usr/bin/g++", "a.cc", "-o", "a"], "env": ["PATH=/usr/bin:/bin"], "files": [ {"content": ""}, {"name": "stdout", "max": 10240}, {"name": "stderr", "max": 10240} ], "cpuLimit": 10000000000, "memoryLimit": 104857600, "procLimit": 50, "copyIn": { "a.cc": { "content": "#include <iostream>\nusing namespace std;\nint main() {\nint a, b;\ncin >> a >> b;\ncout << a + b << endl;\n}" } }, "copyOut": ["stdout", "stderr"], "copyOutCached": ["a"] }, { "args": ["a"], "env": ["PATH=/usr/bin:/bin"], "files": [ {"content": ""}, {"name": "stdout", "max": 10240}, {"name": "stderr", "max": 10240} ], "cpuLimit": 10000000000, "memoryLimit": 104857600, "procLimit": 50, "copyIn": { "a": {"cached": "a"} }, "copyOut": ["stdout", "stderr"] } ] }

第一个 cmd 的copyOutCached把编译产物 a 存到缓存,第二个 cmd 里用"cached": "a"引用。status 字段是返回码,0 表示成功,非 0 需要结合 stderr 判断。编译阶段的 stderr 就是编译报错信息,运行阶段的 stderr 是程序自身的错误输出。C 语言把 args 改成 ["/usr/bin/gcc", "a.c", "-o", "a"],copyIn 的文件名和内容换成 .c 文件即可。

4.3 Java:内存参数和编译路径的坑

Java 的评测套路跟 C/C++ 不一样,main 方法所在的类名必须是 Main,编译产物是 .class 文件。JVM 自身会占不少内存,所以 memoryLimit 一般要比 C++ 调大 1.5 到 2 倍,否则明明题目只要 64MB,Java 跑起来直接 MLE。

{ "cmd": [ { "args": ["/usr/bin/javac", "Main.java"], "env": ["PATH=/usr/bin:/bin", "JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64"], "files": [ {"content": ""}, {"name": "stdout", "max": 10240}, {"name": "stderr", "max": 10240} ], "cpuLimit": 10000000000, "memoryLimit": 209715200, "procLimit": 50, "copyIn": { "Main.java": { "content": "import java.util.Scanner;\npublic class Main {\n public static void main(String[] args) {\n Scanner sc = new Scanner(System.in);\n int a = sc.nextInt(), b = sc.nextInt();\n System.out.println(a + b);\n }\n}" } }, "copyOut": ["stdout", "stderr"], "copyOutCached": ["Main.class"] }, { "args": ["/usr/bin/java", "Main"], "env": ["PATH=/usr/bin:/bin", "JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64"], "files": [ {"content": ""}, {"name": "stdout", "max": 10240}, {"name": "stderr", "max": 10240} ], "cpuLimit": 10000000000, "memoryLimit": 209715200, "procLimit": 50, "copyIn": { "Main.class": {"cached": "Main.class"} }, "copyOut": ["stdout", "stderr"] } ] }

Java 的JAVA_HOME必须显式设置,否则java命令找不到。JDK 版本建议固定用 8,我在 HOJ 里遇到过一次很诡异的坑:JDK 11 编译时 HOJ 调用报 java.security 相关错误,换成 openjdk-8-jdk 后问题消失。具体原因没深究,但如果你也踩到,先检查 JDK 版本。

4.4 Python2 和 Python3:直跑解释器的参数差异

Python 没有编译阶段,直接调用解释器运行源码。Python2 和 Python3 的命令路径不同,前者是/usr/bin/python2.7,后者是/usr/bin/python3

{ "cmd": [ { "args": ["/usr/bin/python3", "main.py"], "env": ["PATH=/usr/bin:/bin"], "files": [ {"content": ""}, {"name": "stdout", "max": 10240}, {"name": "stderr", "max": 10240} ], "cpuLimit": 10000000000, "memoryLimit": 209715200, "procLimit": 50, "copyIn": { "main.py": { "content": "a, b = map(int, input().split())\nprint(a + b)" } }, "copyOut": ["stdout", "stderr"] } ] }

Python 的 memoryLimit 建议也给大一些,解释器启动本身要占几十 MB。input()在 EOF 时会抛异常,题目数据如果有空行,就读不到,这一点在出题时要特别小心——Python 的input().split()碰到空行会直接 ValueError,不是空数组。

4.5 从返回的 status 和 files 判断编译错误与运行时错误

返回结果里 status 字段有几种值需要记住:0 表示成功,非 0 表示命令退出码非零,比如编译错误一般是 1。真正容易混淆的是 time limit exceeded 和 memory limit exceeded,这两种情况 go-judge 会直接杀掉进程,status 会是 137(SIGKILL),而不是像正常退出那样返回程序自己的退出码。这时候要结合files里的 stderr 内容判断:如果 stderr 是空的但程序被杀,优先怀疑超时或内存超限;如果有 Error 输出,说明是程序自身的问题。

5. 避坑 / 常见问题排查:部署 go-judge 时最容易踩的五个坑

5.1 apt-get install g++ 报 Unable to locate package

现象:在基于 Ubuntu 的 Docker 容器里执行apt-get install g++,提示Unable to locate package g++

原因:容器内的 apt 源索引还没更新,刚拉取的镜像本地源列表是空的;或者源本身配置有问题,连不到软件仓库。

解决:先执行apt-get update再安装。如果 update 也很慢或者失败,就把源换成国内镜像源。常见做法是修改/etc/apt/sources.list,把archive.ubuntu.com换成mirrors.aliyun.commirrors.tuna.tsinghua.edu.cn。改完后apt-get update再 install。

5.2 CentOS 7 上 Docker 沙箱无法启动

现象:CentOS 7 系统的 /proc/sys/user/max_user_namespaces 输出为 0,go-judge 容器内跑一个简单命令就报权限错误。

原因:CentOS 7 的 Red Hat 内核默认禁用了 user namespaces,Docker 沙箱创建隔离环境时依赖这个内核特性。

解决:按前面第 2.3 节的方式,写入 sysctl 配置开启 user namespaces,然后 reboot。开启后再次确认 /proc/sys/user/max_user_namespaces 为 10000。

5.3 官网源码压缩包下载后提示有病毒或无法解压

现象:从 GitHub 下载 go-judge 源码压缩包,Chrome 提示有病毒,或者解压时报文件损坏。

原因:压缩包里含有 go-sandbox 用到的 seccomp 编译产物和可执行文件,某些杀毒软件对这类文件比较敏感,会误报;下载过程中断也会导致压缩包损坏。

解决:用wget在服务器上重新下载,下载完后先校验文件大小是否和 GitHub 页面显示的一致。解压报错就用unzip -t检查压缩包完整性。Git 克隆报错时,把https://开头的地址换成git://协议再试一次,或者直接下载 release 页面的 tar.gz 包。

5.4 Java 编译报 java.security 错误

现象:HOJ 调用 go-judge 编译 Java 代码时报 java.security 相关异常,但同样的代码在本地编译没问题。

原因:基础镜像里装的 JDK 版本过高(比如 JDK 11),与 HOJ 的调用方式存在兼容性问题。我在 openjdk-11-jdk 上稳定复现,换成 openjdk-8-jdk 后不再出现。

解决:在 Dockerfile 里固定openjdk-8-jdk,不要用默认的 JDK 11 或更高版本。如果已经构建了镜像,重新构建时注意 apt 安装包名要写对。

5.5 HOJ 调用 go-judge 报 language-pack 错误

现象:HOJ 判题日志里出现语言包相关的错误提示,判题失败。

原因:go-judge 镜像基于精简的 Debian 或 Ubuntu,缺少language-pack-en-base语言包。HOJ 在调用时会检查系统语言环境,缺了就会报错。

解决:Dockerfile 里加上language-pack-en-base,并在 RUN 阶段显式安装。这个包体积不大,但会引入一些额外的依赖,安装时间会多一分钟左右。

6. 进阶:构建一个多语言全家桶镜像,并用端到端请求验证

6.1 完整的 Dockerfile 与构建命令

前面criyle/go-judge官方镜像没有装 g++、python、java 等判题环境,只能评测 C 语言。要把 go-judge 和编译环境打到一个镜像里,可以参考criyle/go-judger-demo项目里的 Dockerfile.exec,我做了一些调整:JDK 固定为 8,去掉了 golang、c# 等 OJ 不常用的依赖,增加了language-pack-en-base

FROM criyle/go-judge:latest AS go-judge FROM ubuntu:20.04 ENV TZ=Asia/Shanghai ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update \ && apt-get install -y --no-install-recommends \ gcc \ g++ \ python2.7 \ python3 \ openjdk-8-jdk \ vim \ language-pack-en-base \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* WORKDIR /opt COPY --from=go-judge /opt/go-judge /opt/mount.yaml /opt/ EXPOSE 5050/tcp 5051/tcp ENTRYPOINT ["./go-judge"]

构建命令如下:

docker build -t go_judge_base_image -f Dockerfile.exec . docker run --privileged --shm-size=256m \ -p 5050:5050 --name=go-judge go_judge_base_image

--from=go-judge表示从第一个阶段拷贝 go-judge 可执行文件和 mount.yaml。ENTRYPOINT ["./go-judge"]让容器启动时直接跑 go-judge,配合在宿主机上用-e传环境变量来控制监听地址和鉴权。这里指定了ubuntu:20.04而不是官方 demo 里的ubuntu:latest,能避免未来 Ubuntu 大版本更新导致依赖行为变化——之前我吃过一次亏,latest 标签跳到 22.04 后有些老版本的动态库行为跟预期不一样。

6.2 并发参数设置经验

构建完成后,用环境变量把并发控制好。我给一个适合 8 核 16G 机器的配置:

docker run --privileged --shm-size=512m \ -e ES_PARALLELISM=6 \ -e ES_PRE_FORK=2 \ -e ES_HTTP_ADDR=0.0.0.0:5050 \ -p 5050:5050 --name=go-judge go_judge_base_image

ES_PARALLELISM=6表示同时最多跑 6 个评测任务,ES_PRE_FORK=2预启动 2 个沙箱进程。--shm-size=512m增大共享内存,避免多线程程序编译时报内存不足。如果机器是 16 核 32G,我建议开两个容器,每个-p 5050:5050-p 5051:5050映射到不同宿主机端口,再写个简单的调度脚本按请求数分发。

6.3 带鉴权和多语言的端到端自检

构建完镜像后,我习惯用一段带鉴权的 POST 请求做端到端验证——确认编译、运行、鉴权三个环节都正常后再接入 OJ。

curl -X POST http://127.0.0.1:5050/run \ -H "Content-Type: application/json" \ -H "Authorization: Bearer testtoken123" \ -d '{ "cmd": [ { "args": ["/usr/bin/python3", "a+b.py"], "env": ["PATH=/usr/bin:/bin"], "files": [{"content": ""}, {"name": "stdout", "max": 10240}, {"name": "stderr", "max": 10240}], "cpuLimit": 1000000000, "memoryLimit": 104857600, "procLimit": 50, "copyIn": { "a+b.py": { "content": "a, b = map(int, input().split())\nprint(a + b)" } }, "copyOut": ["stdout", "stderr"] } ] }'

响应里status: 0stdout有内容,说明整套链路通了。如果返回 401,优先检查鉴权 Token 是否一致;如果 status 是 137,优先看内存限制是否太小。从那以后我每次搭新的判题机,都强制走一遍这个流程:先裸跑验证参数,再 Docker 化,最后带鉴权打一次接口,全部通过才接 OJ。这套资源里的部署细节和踩坑记录能帮你省掉这最折腾的一周,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询