ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

go-judge判题机从部署到多语言评测:沙箱与API配置实战指南

go-judge判题机从部署到多语言评测:沙箱与API配置实战指南 简介围绕 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-addr0.0.0.0:5051 # 后台运行并写日志 nohup ./go-judge -http-addr0.0.0.0:5051 go-judge-log.log 21 这里解释几个关键点。-http-addr0.0.0.0:5051表示服务监听所有网卡配合云服务器安全组放行对应端口就可以通过公网 IP 访问了。nohup和的组合让进程脱离控制台运行21把标准错误合并到标准输出统一写进日志文件——这在排查问题时很重要因为 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_namespaces10000 /etc/sysctl.d/98-userns.conf sysctl -p reboot开启后重启机器然后拉镜像创建容器# 拉取官方镜像 docker pull criyle/go-judge # 创建容器 docker run -it --rm --privileged --shm-size256m \ -p 5050:5050 --namego-judge criyle/go-judge注意这里-it是交互式终端--rm是退出即删除容器。用宝塔面板这类 Docker 管理工具创建时要把这两个参数去掉否则容器关了就没了还得重新建。--privileged是必需的沙箱需要完整的权限来创建命名空间和 cgroup。--shm-size256m设置共享内存大小多线程程序编译时如果太小会报错。-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-size256m \ -e ES_AUTH_TOKENJgeJeldGeDcjJHg \ -e ES_HTTP_ADDR0.0.0.0:5050 \ -p 5050:5050 --namego-judge criyle/go-judge加上ES_AUTH_TOKEN之后请求接口必须先带 Bearer Token否则返回 401。这个鉴权方式就是 JWTgo-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 是 5050gRPC 是 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_TOKEN空Bearer 鉴权令牌-parallelismES_PARALLELISMCPU 核数并发评测数量-pre-forkES_PRE_FORK1预启动 worker 数-cpusetES_CPUSET空限制容器进程使用指定 CPU 核-tmp-fs-paramES_TMP_FS_PARAMsize128m,nr_inodes4ktmpfs 挂载参数-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 的完整 JSON4.1 /run 接口请求体结构拆解go-judge 最核心的接口是 POST /run请求体是一个 JSON 对象包含 cmd 数组和可选参数。每个 cmd 描述一条命令数组里多条命令按顺序执行。核心字段如下字段类型说明argsstring[]命令及参数如 [/usr/bin/g, a.cc, -o, a]envstring[]环境变量推荐显式指定 PATHfilesobject[]标准输入、标准输出、标准错误的定义cpuLimitintCPU 时间限制纳秒建议至少 1e91 秒memoryLimitint内存限制字节注意是虚拟内存procLimitint进程/线程数限制copyInobject传入文件内容是源码或二进制copyOutstring[]需要返回的文件stdout/stderr 直接返回内容copyOutCachedstring[]需要缓存的文件编译产物用这个后续命令可通过缓存路径引用files 数组里通常放三个对象第一个是标准输入内容content 为空表示空输入第二个和第三个分别定义 stdout 和 stderr 的读取方式name 固定为 stdout/stderrmax 表示最多读取多少字节。超过 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 倍否则明明题目只要 64MBJava 跑起来直接 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 会是 137SIGKILL而不是像正常退出那样返回程序自己的退出码。这时候要结合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.com或mirrors.tuna.tsinghua.edu.cn。改完后apt-get update再 install。5.2 CentOS 7 上 Docker 沙箱无法启动现象CentOS 7 系统的 /proc/sys/user/max_user_namespaces 输出为 0go-judge 容器内跑一个简单命令就报权限错误。原因CentOS 7 的 Red Hat 内核默认禁用了 user namespacesDocker 沙箱创建隔离环境时依赖这个内核特性。解决按前面第 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 TZAsia/Shanghai ENV DEBIAN_FRONTENDnoninteractive 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 --fromgo-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-size256m \ -p 5050:5050 --namego-judge go_judge_base_image--fromgo-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-size512m \ -e ES_PARALLELISM6 \ -e ES_PRE_FORK2 \ -e ES_HTTP_ADDR0.0.0.0:5050 \ -p 5050:5050 --namego-judge go_judge_base_imageES_PARALLELISM6表示同时最多跑 6 个评测任务ES_PRE_FORK2预启动 2 个沙箱进程。--shm-size512m增大共享内存避免多线程程序编译时报内存不足。如果机器是 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, ab.py], env: [PATH/usr/bin:/bin], files: [{content: }, {name: stdout, max: 10240}, {name: stderr, max: 10240}], cpuLimit: 1000000000, memoryLimit: 104857600, procLimit: 50, copyIn: { ab.py: { content: a, b map(int, input().split())\nprint(a b) } }, copyOut: [stdout, stderr] } ] }响应里status: 0且stdout有内容说明整套链路通了。如果返回 401优先检查鉴权 Token 是否一致如果 status 是 137优先看内存限制是否太小。从那以后我每次搭新的判题机都强制走一遍这个流程先裸跑验证参数再 Docker 化最后带鉴权打一次接口全部通过才接 OJ。这套资源里的部署细节和踩坑记录能帮你省掉这最折腾的一周希望帮到你。本文还有配套的精品资源点击获取
返回列表