简介:基于go-judge项目的一站式判题机搭建指南,面向自建OJ平台的开发者、运维人员及计算机相关专业学生。go-judge官方资料仅提供C++调用样例,且未明确鉴权方式与内部原理,这份文档结合真实OJ搭建经验,系统梳理了从部署到调用的完整路径。资源为单个docx文档,约1.9MB,结构清晰,包含官网项目介绍、服务器直接部署与Docker部署两种方式、启动参数设置、请求接口举例等章节。文档以C、C++、Java、Python3、Python2共五种语言给出run接口调用样例,并补充了官方中文文档缺失的细节。同时针对apt源缓慢、缺少语言包、CentOS7需开启User命名空间等常见问题提供了具体排查方法,可帮助读者少走弯路。已有880人学习下载,适合需要快速搭建判题服务并扩展多语言支持的OJ搭建者参考。
1. 从 OJ 判题机到 go-judge:这套工具的定位与适用场景
想做 OJ 的人,大部分都会卡在同一个地方:判题内核敲定了 go-judge,但官网 README 只给了 C++ 的调用样例,Java、Python 怎么传参、鉴权怎么配、镜像里要装哪些环境,全是黑匣子。我是在云服务器上部署 go-judge 判题机时踩完这一圈坑的,前后花了一周,把服务器直接部署、Docker 部署、多语言镜像构建、/run 接口五种语言调用和 HOJ 对接全走通之后,才有了下面这份配置记录。它适合两类人:准备自建 OJ 的开发者,以及想把判题服务和主站拆开的运维。照着做,可以很快用 Docker 跑通一个支持 C、C++、Java、Python3、Python2 的判题服务,再花十分钟验证接口通不通。
2. 服务器部署还是 Docker 部署:两条路线与三个选型依据
2.1 服务器直接部署:解压、启动、开放外网访问
go-judge 提供两种部署方式,官网上写得不算细,我实际跑下来,服务器部署的核心操作只有三步:下载二进制、解压、启动。判题机本质上是一个常驻 HTTP 服务,go-judge 的可执行文件是编译好的,不需要额外装 Go 环境,但判题用的语言运行时(g++、Java、Python)得自己准备。
mkdir -p /root/cicyle/go-judge cd /root/cicyle/go-judge # 按服务器架构下载对应 release,x86_64 通常选 linux_amd64 wget https://github.com/criyle/go-judge/releases/download/v1.8.2/go-judge_1.8.2_linux_amd64.tar.gz tar -zxvf go-judge_1.8.2_linux_amd64.tar.gz ls -l解压之后目录里会出现两个文件:go-judge 是可执行文件,mount.yaml 是默认挂载配置。mount.yaml 在默认情况下不用动,它负责告诉沙箱哪些目录可以挂进容器,改坏了大不了恢复默认。wget 下载如果慢,可以本地下载后上传服务器,效果一样。
直接执行启动命令:
./go-judge默认监听 localhost:5050,也就是只在本机监听,局域网和公网都访问不到。要验证服务起来没有,可以另开一个终端请求一下curl localhost:5050;判题机一般部署在内网,这样默认反而安全。想开放外网访问,用-http-addr指定监听地址:
./go-judge -http-addr=0.0.0.0:50510.0.0.0 表示任何 IP 都能访问这个端口,适合用 APIFOX 或 Postman 做接口调试。这一步之前记得先去云服务器控制台放行 5051 端口,否则外部请求根本到不了进程。测试环境用 5051 而不是默认的 5050,是为了避免和默认值混淆,看到 401/200 时能确定流量确实走到了 go-judge。
还有一个细节:直接启动的 go-judge 会跟着控制台退出而死。要让它常驻后台,我一般用 nohup:
nohup ./go-judge -http-addr=0.0.0.0:5051 >> go-judge-log.log 2>&1 && 让命令在后台执行,nohup 保证终端退出后进程不挂,2>&1 把标准错误重定向到标准输出,统一写进 go-judge-log.log。后面排查问题,直接tail -f go-judge-log.log看输出。到这里服务器部署就完成了,判断服务状态是不是正常,就看这个日志文件有没有持续输出异常。
2.2 Docker 部署:CentOS 开用户命名空间、容器命令与鉴权
Docker 部署官网给了一条现成命令,但不能直接照抄:
docker run -it --rm --privileged --shm-size=256m -p 5050:5050 --name=go-judge criyle/go-judge-it是交互式进入容器,--rm是退出后自动删除容器。这两个参数在命令行调试时没问题,但用宝塔面板或 docker-compose 创建常驻判题容器时要去掉,否则容器一退就没了,到时候会怀疑是不是自己误删了容器,实际上只是启动参数的问题。
CentOS 7 上跑这条命令前,有一个前置坑必须处理:内核默认关掉了 user namespaces,不开的话容器内的沙箱直接报错。
cat /proc/sys/user/max_user_namespaces # 结果是 0 就需要开启,已经改过的会显示 10000 之类的数字 echo user.max_user_namespaces=10000 >> /etc/sysctl.d/98-userns.conf sysctl -p rebootRed Hat 为了稳定性和安全性,默认禁用了 user namespace,Docker 这类软件依赖它做容器内权限隔离,必须手动开。改完记得重启,sysctl -p对这项参数不一定立刻生效,不重启的话后面容器启动还是可能报错,这个顺序别省。
镜像拉取慢或拉取失败,多半是网络问题。我一般多试几次,实在不行就换镜像加速地址,把加速配置写进/etc/docker/daemon.json。拉取成功的判断标准是出现完整的镜像 ID 和 digest,不要中途跳出拉取界面。
Docker 部署和服务器部署有一个关键差异:加启动参数不是追加命令行,而是通过环境变量。比如想开鉴权:
docker run --privileged --shm-size=256m -p 5050:5050 --name=go-judge \ -e ES_AUTH_TOKEN=JgeJeldGeDcjJHg \ criyle/go-judge加上之后,再调用接口不带 Token 会得到 401。go-judge 用的是 Bearer 认证,也就是常说的 JWT/Token,调用时在 Header 里带Authorization: Bearer <token>。生产环境如果整个判题网段都在内网,鉴权开不开差别不大,开了反而多一次校验损耗,我自己的做法是内网裸跑、跨网段必须开。
2.3 选型依据:为什么生产环境我倾向 Docker 多实例
两种方式跑通之后,选型逻辑就很清楚了。直接服务器部署的好处是少一层抽象,解压就能跑,适合快速验证功能;但沙箱直接裸跑在宿主上,隔离粒度取决于 go-sandbox 自身,一旦某个评测任务把资源吃满,宿主其它服务会跟着遭殃。
Docker 的优势有三个:第一,可以通过--memory、--cpus这类参数限制容器资源,把评测任务关在笼子里;第二,可以开多个容器实例,比如单机 16 核 32G,拆成四个小资源实例,互不干扰地并行判题;第三,宿主服务器上可以再写一个统一调度服务,按负载把题目分发给各个判题容器节点,相当于做了一个迷你判题集群。
正文里提到生产环境下一般不会服务器直接部署沙箱,尽量在 Docker 中运行,这句话我后来深有体会。Docker 部署的官方镜像基于 debian 构建,是最小化环境,装 g++、Java、Python 这些语言环境很麻烦,所以就有了下一章的构建全语言镜像。
3. 构建全语言镜像:把 C++/Java/Python 环境一次装进容器
3.1 官网基础镜像的边界:为什么只能跑 C
criyle/go-judge 官方镜像基于 debian 构建,是一个刻意做过减法的最小化 Linux 环境。正文原话是官网的 docker 案例并没有安装各类运行环境,无法评测 C 语言之外的语言。实际测下来,g++ 都要自己装,更不用提 Java、Python。
所以部署 go-judge 的正确姿势不是拉官方镜像直接用,而是以官方镜像为底座,自己叠加语言运行时。叠加方式有两种:一种是容器启动后 docker exec 进去装,装完 docker commit 保存成新镜像;另一种是写 Dockerfile 一次性构建。commit 方式我踩过坑,装到一半网络断了,容器状态乱七八糟,commit 出来的镜像也不知道装了什么。所以我后来都用 Dockerfile。
官网其实给过一个参考例子:github.com/criyle/go-judger-demo 里的 Dockerfile.exec,它就是基于 ubuntu 构建 go-judge 镜像的模板。我改造它的时候主要动了三处:Java 版本、语言包、依赖裁剪。
3.2 改造后的 Dockerfile:Java 8、language-pack、去冗余
我自用的 Dockerfile 长这样:
FROM criyle/go-judge:latest AS go-judge FROM ubuntu:latest ENV TZ=America/Vancouver 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"]逐段说明。多阶段构建的第一段FROM criyle/go-judge:latest,作用是把官方镜像里的 go-judge 可执行文件和 mount.yaml 原样拷出来;第二段才是真正的运行时镜像,基于 ubuntu:latest,装语言环境后把 go-judge 复制进去。这样做的好处是语言环境干净,不会和官方镜像里的 debian 运行时打架。
Java 我特意改成了 openjdk-8-jdk。原版 demo 用的是 JDK 11,但在 HOJ 里调用 Java 编译会报一个 java.security 相关错误,换成 JDK 8 就好了,这是集成 HOJ 时实打实踩过的,后文避坑部分还会展开。
language-pack-en-base 也是我加进去的。不加的话,部分 OJ 在调用时会出现 locale 相关报错,具体现象是运行阶段输出 perl warning 之类的信息。vim 是排查用的,容器里没有文本编辑器,改配置很难受。golang、C# 这些不常用的依赖我删掉了,镜像体积能小不少。
TZ=America/Vancouver是原 demo 带的时区,我换成 Asia/Shanghai 更顺手;DEBIAN_FRONTEND=noninteractive是避免 apt 安装时弹出交互式配置界面导致 Docker 构建卡住。
注意:
--no-install-recommends会跳过推荐的依赖包,镜像更小,但也可能缺一些非标准库的运行时文件,如果某个语言环境跑不起来,先去掉这个参数重试。
3.3 构建与启动:build、run、进容器确认
构建命令很简单:
docker build -t go_judge_base_image -f Dockerfile.demo .-t指定镜像名,-f指定 Dockerfile 文件名。命令末尾的.是构建上下文,Dockerfile 里 COPY 用到的文件都必须在这个目录下。
构建成功后创建容器:
docker run --privileged --shm-size=256m -p 5050:5050 --name=go-judge go_judge_base_image这里有个容易翻车的细节:docker run 最后的镜像名必须和 docker build -t 保持一致,写错的话 docker 会提示找不到镜像。另外--privileged不能省,go-judge 的沙箱需要特权去创建隔离环境;--shm-size=256m是 /dev/shm 的大小,一些程序对共享内存有要求,设太小会莫名其妙失败。
启动后进容器确认语言环境:
docker exec -it go-judge bash which g++ java python3 python2.7这三条命令分别确认 C++、Java、Python 的运行时都在。如果 which 输出路径,比如 /usr/bin/g++,说明环境没问题。这一步我建议每次新镜像起来都走一遍,别省,等上线了才发现没有 g++ 就晚了。
3.4 Ubuntu 版本隐患:latest 到底是不是 20.04
最后提一个隐患。demo 用的是 ubuntu:latest,HOJ 官方 Dockerfile 用的是 ubuntu:18.04,这俩差着版本。我构建时实际拿到的镜像是 Ubuntu 20.04.3 LTS,以后 base 镜像更新,latest 指向的版本会漂移,可能出现上次构建好好的,这次构建 g++ 版本变了导致评测结果不稳定的玄学问题。
我的做法是显式指定版本:ubuntu:20.04 或 ubuntu:22.04,构建前先想清楚要哪个 LTS。以后如果 go-judge 或 Ubuntu 出现兼容性问题,回头改这里就行,别的不用动。
4. 启动参数与鉴权:--help 每个参数的真实用途
4.1 全参数解读:默认值背后是一整套资源管控
go-judge 的启动参数,进容器里执行./go-judge --help就能看到全部。我第一次看的时候差点被几十个参数淹没,但结合判题场景梳理一遍,核心就三类:网络与鉴权、资源限制、沙箱行为。
先看网络相关参数,这是部署时最先要动的:
| 参数 | 默认值 | 作用 |
|---|---|---|
| -http-addr | localhost:5050 | HTTP 服务监听地址 |
| -grpc-addr | localhost:5051 | gRPC 服务监听地址 |
| -auth-token | 空 | REST/gRPC 的 Bearer 鉴权 Token |
| -enable-grpc | false | 是否开启 gRPC 接口 |
| -enable-metrics | false | 是否暴露 Prometheus 指标 |
| -monitor-addr | localhost:5052 | metrics 监听地址 |
资源限制相关参数,这是判题稳定性的核心:
| 参数 | 默认值 | 作用 |
|---|---|---|
| -parallelism | CPU 核数 | 并发执行的任务数 |
| -pre-fork | 1 | 预 fork 的 worker 数,减少启动延迟 |
| -cpu-cfs-period | 100ms | CPU 调度周期,配合 cpu rate 控制使用 |
| -enable-cpu-rate | false | 是否开启 CPU 速率控制 |
| -extra-memory-limit | 16 KiB | 内存检测的额外缓冲 |
| -copy-out-limit | 256 MiB | 单文件 copy out 的大小上限 |
| -output-limit | 256 MiB | 命令输出 rlimit |
| -open-file-limit | 256 | 单进程最大打开文件数 |
| -file-timeout | 0s | 文件缓存过期时间 |
沙箱行为参数,日常调整频率不高但出了问题要会看:
| 参数 | 默认值 | 作用 |
|---|---|---|
| -mount-conf | mount.yaml | 挂载配置文件 |
| -seccomp-conf | seccomp.yaml | seccomp 过滤规则 |
| -tmp-fs-param | size=128m,nr_inodes=4k | tmpfs 挂载参数 |
| -container-cred-start | 0 | 容器起始 uid/gid |
| -net-share | false | 是否共享宿主机网络命名空间 |
| -dir | 内存 | 文件上传下载的存储目录 |
| -src-prefix | 空 | 源码型 copyIn 的目录前缀 |
常规场景下需要动的最多就是-http-addr、-parallelism、-dir这三个。parallelism 默认等于 CPU 核数,如果容器只分到 2 核,并发判题就 2 路,想开到 4 路就得显式传-parallelism=4,但别忘了同时给容器足够的资源配额,否则开了也是排队。
4.2 环境变量传参:ES_ 前缀规则
go-judge 设计了一个很实用的规则:所有命令行参数都对应一个环境变量,规则是把参数名转大写、-变成_、再加ES_前缀。比如-http-addr对应ES_HTTP_ADDR,-auth-token对应ES_AUTH_TOKEN。
这个设计对 Docker 部署特别重要,因为 docker run 后面追加长串命令行参数很难维护,环境变量可以写在 compose 文件里,也可以放在 Dockerfile ENV 里统一管理。
docker run --privileged --shm-size=256m -p 5050:5050 --name=go-judge \ -e ES_HTTP_ADDR=0.0.0.0:5051 \ -e ES_PARALLELISM=4 \ -e ES_DIR=/tmp/gojudge \ -e ES_AUTH_TOKEN=my_secret_token \ go_judge_base_image这样启动后,HTTP 监听在 5051、并发 4 路的判题服务就起来了。ES_DIR 设成磁盘目录而不是默认的内存,适合需要缓存大量测试数据的场景,代价是文件读写速度比 tmpfs 慢。如果评测的输入输出文件小,保持默认的内存存储性能更好。
4.3 ES_AUTH_TOKEN 鉴权:Bearer 认证怎么带
go-judge 的鉴权是 Bearer 认证,本质上就是 JWT 风格的 Token。没配置时接口直接裸奔,任何人拿到地址都能提交代码执行,这在公网环境是不可接受的。
用法上面已经写了,调用端要做的只是在 HTTP Header 里带一行:
Authorization: Bearer JgeJeldGeDcjJHg不带会返回 401,带错也会返回 401。用 APIFOX 或 Postman 测试时,在 Authorization 一栏选 Bearer Token,把值填进去即可。我习惯把 Token 放进环境变量文件管理,Docker 容器用--env-file加载,避免 Token 写进 docker run 历史记录。
还有一点经验之谈:go-judge 一般部署成内网访问,内网环境里鉴权开不开影响不大,开了反而每次请求多一次 token 校验延迟。但如果端口暴露到了公网,哪怕只是测试期间,也必须开,否则你就是一台免费的代码执行服务器。另外 gRPC 和 HTTP 的鉴权共用同一个 token,开 gRPC 时记得一起配上。
5. 调用 /run 接口:C/C++/Java/Python3/Python2 请求样例
5.1 /run 请求结构:先看懂命令组
部署好之后,核心接口只有一个:POST /run。它提交的不是一行命令,而是一个 cmd 数组,每个 cmd 是一个命令组。判题流程本质上是编译和运行两个命令组:第一个 cmd 负责 g++ 编译生成可执行文件,第二个 cmd 负责运行它,这样一个任务拆成两段,每一段都可以独立限制 CPU、内存、进程数。
请求体的核心字段:
- args:命令和参数列表,第一个元素是程序路径
- env:环境变量列表,比如 PATH=/usr/bin:/bin
- files:标准输入、标准输出、标准错误的定义,stdin 用 content 给文本,stdout/stderr 用 name 加 max 限制输出上限
- cpuLimit:CPU 时间限制,单位纳秒
- memoryLimit:内存限制,单位字节
- procLimit:进程数限制
- copyIn:预写入沙箱的文件,content 是文件内容
- copyOut:需要回传的内容,比如 stdout、stderr
- copyOutCached:需要缓存在服务端的文件,返回 fileId 供后续命令组引用
这里最容易踩的坑是单位。cpuLimit 是纳秒,1 秒是 1000000000;memoryLimit 是字节,100MB 是 104857600。我见过有人把秒直接填进去,结果题目全部超时。
注意:cpuLimit 单位是纳秒,memoryLimit 单位是字节,写错一个单位,评测结果全盘皆输。
5.2 C++ 样例:最完整的参考模板
官网给出的 C++ 样例是理解所有语言的基础,先完整贴一遍:
POST /run { "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" ] } ] }这个样例做了三件事:把 A+B 的 C++ 源码通过 copyIn 写进沙箱,用 g++ 编译,最后把编译产物 a 通过 copyOutCached 缓存起来。编译阶段的 stdout 和 stderr 都被限制在 10240 字节,超过即报错,防止编译输出刷爆文件。
注意这里的 cpuLimit 是 10 秒,memoryLimit 是 100MB。编译和运行阶段如果共用一份参数,编译吃内存大的时候容易误杀,我一般会给编译阶段更大的 cpuLimit,给运行阶段更小的内存限制,分两个 cmd 写。
接下来是运行阶段,第二个 cmd 引用编译缓存的产物:
{ "cmd": [ { "args": ["a"], "env": ["PATH=/usr/bin:/bin"], "files": [ {"content": "1 2\n"}, {"name": "stdout", "max": 10240}, {"name": "stderr", "max": 10240} ], "cpuLimit": 1000000000, "memoryLimit": 52428800, "procLimit": 50, "copyIn": {}, "copyOut": ["stdout", "stderr"] } ] }运行阶段通过 files[0].content 写入了标准输入 "1 2\n",这就是测试数据。返回结果里 stdout 就是程序输出,OJ 拿它和期望输出对比即可。
5.3 Java 与 Python 的常见写法差异
Java 的逻辑和 C++ 差不多,只是编译命令和入口类名有讲究:
{ "cmd": [ { "args": ["/usr/bin/javac", "Main.java"], "env": ["PATH=/usr/bin:/bin", "LANG=en_US.UTF-8"], "files": [ {"content": ""}, {"name": "stdout", "max": 10240}, {"name": "stderr", "max": 10240} ], "cpuLimit": 10000000000, "memoryLimit": 262144000, "procLimit": 50, "copyIn": { "Main.java": { "content": "import java.util.Scanner;\npublic class Main {\npublic static void main(String[] args) {\nScanner sc = new Scanner(System.in);\nint a = sc.nextInt();\nint b = sc.nextInt();\nSystem.out.println(a + b);\n}\n}" } }, "copyOut": ["stdout", "stderr"], "copyOutCached": ["Main.class"] } ] }Java 有两点必须注意:一是源文件名必须和 public class 名一致,都叫 Main,这几乎是所有 OJ 的约定;二是运行阶段要执行的是 Main 类而不是 Main.class 文件,所以第二个 cmd 的 args 是/usr/bin/java Main,并且环境变量里要有 LANG,否则某些中文字符串输出会乱码。Java 的启动内存开销大,memoryLimit 我一般给到 256MB 起步,否则 JVM 还没跑起来就被沙箱杀了。
Python 就简单得多,不需要编译阶段,直接一个 cmd 运行:
{ "cmd": [ { "args": ["/usr/bin/python3", "a.py"], "env": ["PATH=/usr/bin:/bin", "LANG=en_US.UTF-8"], "files": [ {"content": "1 2\n"}, {"name": "stdout", "max": 10240}, {"name": "stderr", "max": 10240} ], "cpuLimit": 1000000000, "memoryLimit": 262144000, "procLimit": 50, "copyIn": { "a.py": { "content": "a, b = map(int, input().split())\nprint(a + b)" } }, "copyOut": ["stdout", "stderr"] } ] }Python2 同理,把 python3 换成 python2.7,源码内容按 py2 语法写。注意 python2 的 input() 行为跟 python3 不一样,这道坎想必大家都有体会。
正文给出的 C 语言写法把 g++ 换成 gcc,源文件扩展名和编译产物名对应改掉即可,其余结构完全一致。正文只给了 C++ 的完整样例,Java、Python 以上写法是我在 go-judger-demo 基础上按常用 OJ 判题逻辑整理的,可以直接对照着改成其它语言。
5.4 copyIn/copyOut 与文件上限:四个参数坑
最后集中说几个参数坑。
第一个是 copyOutCached 的 fileId 复用。编译产物通过 copyOutCached 缓存后,服务端返回一个 fileId,运行阶段要在 copyIn 里按这个 fileId 引用文件,而不是重新传内容。这个 fileId 有有效期,受-file-timeout控制,默认为 0 表示不过期,但生产环境建议设一个合理值,防止文件堆积占满磁盘。
第二个是 stdout/stderr 的 max。max 是字节数上限,超了沙箱直接判定失败,而不是截断返回。如果某道题的错误输出很长,max 设小会把本来有意义的报错信息也吞掉。我一般编译阶段给 10240,运行阶段按题目可能产生的输出量调整。
第三个是 memoryLimit 对 JVM 的误杀。Java 进程的内存占用包含 JVM 自身,峰值远高于堆设置,所以在 go-judge 层给 Java 的内存限制必须比-Xmx高出一截,否则出现本机跑得好好的,沙箱里就内存超限的灵异现象。
第四个是 procLimit。g++ 编译时会有预处理进程,javac 也会 fork 子进程,procLimit 给 50 通常够了,但某些构建工具可能超过。出现 procLimit 相关报错时,先看是编译阶段还是运行阶段,分别调整对应 cmd 里的值。
6. 避坑排查:部署一周的血泪记录与最终验证
6.1 apt update 慢、Unable to locate package
现象:sudo apt-get update执行超时,或apt install g++直接报 Unable to locate package。
原因:默认源在国外,网络差;另外从 Linux 发行版最小系统开始,源列表里可能没有包含目标包。
解决:先换国内镜像源,把 /etc/apt/sources.list 里的源地址替换成清华或阿里镜像,再apt-get update,最后再 install。顺序不能反,换源后不 update 一样找不到包。
6.2 官网压缩包提示病毒、Git clone 报错
现象:Chrome 下载 go-judge 源码压缩包时提示有病毒,或者下载完无法解压;git clone 时网络中断报错。
原因:go-sandbox 源码里有 ptrace、exec 这类系统调用,杀毒软件特征库容易误报;git clone 大仓库时网络不稳定。
解决:解压失败先换浏览器或关掉实时防护再下一遍;git clone 失败用浅克隆,git clone --depth=1只拉最新 commit,能省大量时间。多试几次也是办法,网络抖动是常态。
6.3 Java 报 security 错误与语言包问题
现象:用 JDK 11 的镜像评测 Java 时,HOJ 报 java.security 相关错误;部分程序运行时输出 locale 警告。
原因:JDK 11 的安全策略和 HOJ 的调用方式有冲突;镜像缺 language-pack-en-base。
解决:Dockerfile 里固定 openjdk-8-jdk,语言环境加上 language-pack-en-base。这两个改动我都写进了第 3 章的 Dockerfile,一次构建终身受用。
6.4 最终验证:curl 跑一遍 A+B
镜像构建和容器都就绪后,我习惯用 curl 做最终验证,不依赖 APIFOX 或 Postman:
curl -X POST http://localhost:5050/run \ -H "Content-Type: application/json" \ -d '{"cmd":[{"args":["/usr/bin/python3","a.py"],"env":["PATH=/usr/bin:/bin"],"files":[{"content":"1 2\n"},{"name":"stdout","max":10240},{"name":"stderr","max":10240}],"cpuLimit":1000000000,"memoryLimit":262144000,"procLimit":50,"copyIn":{"a.py":{"content":"a,b=map(int,input().split());print(a+b)"}},"copyOut":["stdout","stderr"]}]}'如果返回里 stdout 是 3,说明整条链路通了。如果返回 200 但 stdout 为空,先看 stderr 是不是报了语法错误;如果返回 401,检查鉴权 Token 有没有带上;如果连接超时,先确认容器有没有起来、端口映射对不对。
从那以后我每次部署 go-judge 都强制走一遍:--help确认版本参数、进容器which确认语言环境、再 curl 跑一次 A+B,三步下来才敢接 HOJ。希望这些记录能帮到你。
本文还有配套的精品资源,点击获取