尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

GoJudge本地部署实战:判题沙箱原理与Docker云服务器配置

发布时间:2026/9/23 22:06:17

资讯中心
01
ARTICLE

GoJudge本地部署实战:判题沙箱原理与Docker云服务器配置

GoJudge本地部署实战:判题沙箱原理与Docker云服务器配置
简介面向OJ系统搭建者与在线判题平台开发者的GoJudge部署实战指南内容突破官方资料仅提供C样例的限制系统梳理多语言判题支持、鉴权设置与内部调用原理帮助读者快速完成判题机搭建。内含1个docx文档压缩包约1.9MB便于下载后按章节查阅已有880人学习。文档从官网及项目结构讲起分别演示不使用Docker的服务器部署含可执行文件解压、监听端口与外网访问配置和基于官方镜像的Docker容器部署并补充启动参数设置、后台nohup运行方式与常见调用接口针对C、C、Java、Python3、Python2等语言给出完整的Run请求样例读者可直接套用扩展其他语言。在排错部分文档整理了apt更换镜像源、Docker沙箱用户命名空间开启、语言包安装、源码包下载异常等高频问题整体内容既适合初次接触go-judge的开发者快速上手也能为已有OJ系统的二次开发与调优提供参考。1. GoJudge 本地部署前先想清楚判题沙箱到底替你扛了什么自己搭 OJ 的人早晚会撞上同一个问题用户提交的代码到底该在哪里跑直接在主服务器上跑一个while(1)malloc(1024)就能吃光内存一行fork()就能让机器卡死。GoJudge 本地部署解决的就是这件事——它是一个独立的判题沙箱把用户代码关进隔离环境限制 CPU、内存、进程数跑完把结果吐出来。配合云服务器部署它能接在任意 OJ 后端后面充当判题机这也是华为 OJ 这类企业评测、杭电 OJ 这类竞赛平台的通用架构。下面按部署顺序走一遍原理、选型、本地跑通、上云、排错、压测相当于一份能照着敲的运维手册适合正在搭 OJ 的开发者也适合想把判题能力从单体服务里拆出来的团队。2. GoJudge 的运行原理与部署选型沙箱隔离、cgroup 限流和容器化理由go-judge 本质上不是一个「判题业务系统」而是一个「执行引擎」。它不关心你交的是 C 还是 Python也不负责比对答案它只做一件事把你给的命令放进隔离环境里跑按你给的限额掐时间、掐内存、限制进程数然后返回退出码、耗时、内存和标准输出。OJ 后端拿到结果再去比对标准答案算得分。理解这个分工后面所有参数就都好解释了。2.1 判题沙箱的三个核心机制命名空间、cgroup 和 seccompgo-judge 的隔离是三层叠加的缺一层都只能算「半沙箱」。第一层是 Linux 命名空间。它会给每次判题创建一个独立的 PID 命名空间沙箱里看到的进程树是从 1 号进程重新开始的用户代码里的 fork 炸弹只能在自己的命名空间里炸影响不到宿主挂载命名空间让沙箱只能看到临时工作目录和映射进去的文件读不到宿主机上的配置和敏感数据网络和 IPC 命名空间让沙箱默认不共享宿主网络程序没法轻易向外发起连接。第二层是 cgroup 资源控制。go-judge 会在每次 /run 请求时把执行进程放进一个新的 cgroupCPU 时间到了就强杀内存超了就给 OOM进程数超限就拒绝再 fork。这就是判题结果里 Time Limit Exceeded 和 Memory Limit Exceeded 的来源——不是 OJ 后端拿秒表量的是 cgroup 控制器报上来的。第三层是 seccomp 系统调用过滤。沙箱默认只放行程序需要的常规系统调用危险的比如直接 mount、写内核模块这类会被直接拒绝。提示这三层机制决定了 go-judge 通常必须用 root 身份运行因为它需要权限去创建命名空间和 cgroup。这个特性是后面几乎所有部署坑的根源第 5 章会专门展开。2.2 裸机二进制和 Docker 安装怎么选我选容器化的三个理由go-judge 本身是一个 Go 编译出来的单二进制文件不带运行时依赖理论上裸机部署最省事下载二进制写个 systemd 单元启动。但实际做多语言判题机时我几乎总是先用 Docker 把环境固化下来理由有三个。第一环境可复现。判题机要支持多少种语言镜像里就得装多少编译器和解释器gcc、g、python3、openjdk、go。裸机方案里这些靠人肉记住「这台机器装过什么」换台机器就全凭运气容器方案把这些写进 Dockerfile重新 build 一次就是一模一样的环境。第二宿主文件系统更干净。go-judge 每次判题都会在临时目录里写源码、编译产物、运行输出长期跑会产生大量碎片文件。容器里这些文件写进容器层或独立数据卷不会把系统盘 /tmp 塞爆。第三升级和回滚方便。判题机要升级编译器版本或者要加一种新语言裸机是「改系统加祈祷别炸」容器是「改 Dockerfile 出个新镜像 tag」出问题一条命令切回旧镜像。下面这张表是我常用的对比口径对比项裸机二进制Docker 容器部署速度快一个二进制稍慢要先 build 镜像编译器一致性依赖机器维护Dockerfile 可复现沙箱权限直接用 root省心需要 --privileged升级回滚替换二进制风险自担切换镜像 tag适合场景内网固定环境、快速验证生产、多台判题机统一版本单机演示我推荐 Docker够快生产环境如果团队里没人精通容器网络和权限裸机二进制加 systemd 反而更直接go-judge 就一个进程没有魔法。两种我都用过后面第 3 章按 Docker 讲第 4 章按二进制加 systemd 讲正好覆盖两条路。2.3 云服务器规格怎么定CPU、内存与并发判题数的换算很多第一次部署的人上来就问「2 核 4G 够不够」。我的经验是先算并发再定规格因为 go-judge 的 parallelism 参数决定同时能跑几个沙箱而每个沙箱的资源需求是确定的。判题任务基本都是单核 CPU 密集型的所以并行判题数约等于 CPU 核数2 核机器同时跑 2 个任务4 核跑 4 个。内存上一个 C 任务给 128MB 沙箱限额基本够Python 建议 256MBJava 因为 JVM 有堆外开销一个任务至少预留 512MB。算下来 4C8G 的云服务器同时跑 4 个 C 任务、2 个 Python、1 个 Java 比较从容这正好是一个几十人在线的教学 OJ 的日常负荷。服务器规格建议并行数适合规模2C4G2个人练习、小组内部 OJ4C8G4教学班、小型社区 OJ8C16G8几百人同时 oj 刷题的常规场16C32G 起16竞赛集中提交、多站共用判题集群还有两个常被忽略的点。一个是云服务器的 swap 最好关掉或设成很小否则内存超限的程序会先换页而不是被杀掉内存限制形同虚设另一个是判题机的临时目录要独立出来别用系统盘上的 /tmp系统盘 IO 被编译写盘拖垮的时候整个 OJ 都会跟着慢。3. 本地部署 GoJudgeDocker 最小启动命令、多语言配置和一次真实调用第 2 章把原理讲清楚了这一章直接落地。目标只有一个在你自己的机器上跑起一个能用的 go-judge并且亲眼看一次判题结果。按 docker 安装部署的老流程走先拉镜像再验证不要一上来就配一堆参数。3.1 用 Docker 跑起 go-judge最小命令和参数对照go-judge 官方仓库会发布 Docker 镜像registry 路径是 ghcr.io/criyle/go-judgetag 用 latest 或者你 fork 构建时的版本。最小启动命令如下# 拉镜像 docker pull ghcr.io/criyle/go-judge:latest # 启动5050 是 go-judge 默认的 HTTP 端口 docker run -d --name go-judge \ --privileged \ -p 5050:5050 \ --restart unless-stopped \ ghcr.io/criyle/go-judge:latest # 验证进程活着/version 返回一段 JSON curl -s http://127.0.0.1:5050/version这里三个参数分别对应三个坑。--privileged是必须的沙箱要创建命名空间和 cgroup普通容器权限不够不加的话第一次调用 /run 就会返回 System Error-p 5050:5050把容器端口映射到宿主机注意云服务器上这个端口如果暴露公网必须靠安全组限制来源 IP第 4 章细说--restart unless-stopped保证容器退出后自动拉起这是生产环境最基本的自愈。启动之后建议先敲一下 /version确认 HTTP 服务真起来了再往下走。很多部署翻车都是在这一步直接拿 /run 测试结果分不清是容器没起还是沙箱权限出问题。3.2 多运行语言支持怎么配编译器才是语言支持的本体go-judge 自己不编译代码它只负责执行你给的命令。所以「支持多少种语言」这个问题的答案不在 go-judge 里而在镜像里装了哪些编译器。官方镜像默认是一个精简环境直接跑 Java 十有八九报找不到 javac。我的做法是基于官方镜像写一个自己的 Dockerfile把常用编译器一次性装进去FROM ghcr.io/criyle/go-judge:latest RUN apt-get update apt-get install -y --no-install-recommends \ gcc g python3 python3-pip \ openjdk-17-jdk-headless \ golang-go \ rm -rf /var/lib/apt/lists/*装 headless 版 JDK 就够了判题不需要图形界面能省不少镜像体积。装完重新 build 成自己的判题镜像以后多台判题机都用这个镜像语言环境就完全统一了。对应到 OJ 后端每种语言需要维护一份「编译命令 运行命令 资源上限」的配置。下面是我常用的对照表可以直接抄语言编译命令args运行命令args建议内存限额Cgcc main.c -o main -O2 -stdc11 -lm./main128MBCg main.cpp -o main -O2 -stdc17./main128MBPython3无需编译python3 main.py256MBJavajavac -encoding UTF-8 Main.javajava -Xmx256m -XX:UseSerialGC -XX:-UsePerfData -cp . Main512MBGogo build -o main main.go./main256MB注意编译阶段也要设 CPU 时间上限一般给 5 到 10 秒就够了。编译本身也是一个沙箱任务不设上限的话一个故意写得很慢的模板展开就能把判题机拖住。Java 的坑最多运行命令里必须手动限制堆内存-Xmx不要顶满沙箱限额留一半给 JVM 的元空间和 JITUseSerialGC是为了减少 GC 线程数-UsePerfData是关掉性能统计文件这两个参数是老 OJ 社区常用的标准配置。3.3 用 curl 完成一次真实判题请求体字段逐段解读判题机就绪后用 curl 发一个最简单的请求把 C 代码通过 copyIn 写进沙箱一次性完成编译和运行curl -s -X POST http://127.0.0.1:5050/run \ -H Content-Type: application/json \ -d { cmd: [{ args: [/bin/bash, -c, gcc main.c -o main ./main], env: [PATH/usr/bin:/bin], files: [ {content: }, {name: stdout, max: 10240}, {name: stderr, max: 10240} ], cpuTimeLimit: 5000000000, memoryLimit: 268435456, procLimit: 64, copyIn: { main.c: {content: #include stdio.h\nint main(){printf(\hello go-judge\\n\);return 0;}} } }] }返回的 JSON 大概是这样的{ status: Accepted, exitStatus: 0, time: 26874598, memory: 1048576, files: { stdout: aGVsbG8gZ28tanVkZ2UK } }几个字段必须看懂。status是沙箱运行状态Accepted 表示没超时没超内存正常退出TLE、MLE、OLE、RE 对应各种违规Internal Error 说明沙箱自身出问题了多半是权限或 cgroup 配置不对time单位是纳秒上面返回的 26874598 就是约 26.8 毫秒memory单位是字节files.stdout是 base64 编码的标准输出OJ 后端拿到后解码再去和标准答案比对。cmd数组里的字段args[0]必须是绝对路径的可执行文件files数组三个元素依次对应 stdin、stdout、stderrname加max表示把输出存成文件并限制最大字节数超出就报 Output Limit ExceededcopyIn的 key 就是沙箱工作目录下的文件名。还有一点这个示例用一个 bash -c 把编译和运行串起来了方便演示但生产上不推荐。正确做法是分两次请求第一次用 gcc 编译并把产物通过 copyOutCached 缓存返回一个 fileId第二次请求的 copyIn 里用 cached 类型引用这个 fileId 来运行。这样编译和运行能分开设限制也方便排查到底是编译挂了还是运行挂了。提示每次 /run 都是独立无状态的沙箱沙箱里没有任何历史文件。所有输入要么走 copyIn 注入要么走 stdin想当然地用宿主机路径是这里最常见的误用。4. 云服务器部署 GoJudge接通 OJ 后端、systemd 守护和横向扩展本地跑通之后上云部署不是把同一套命令搬到云服务器上就完了。云服务器的核心变化是机器长时间无人值守、可能有公网 IP、会有真实的并发提交。这一章按生产标准拆成三件事怎么和后端通信、挂了怎么拉起来、一台不够怎么横向扩。4.1 判题机与 OJ 后端怎么对接任务队列、回调与安全组设置常见 OJ 的架构是这样的Web 后端负责接收提交、存数据库、管理用户判题队列用 Redis 或 RabbitMQ 承接任务go-judge 判题机作为 worker 从队列里拿任务执行。通信有两种模式一种是后端拿到提交后直接 POST 到判题机的 /run同步等结果另一种是判题机这边挂一个 worker 进程阻塞地从队列拉取任务。同步回调写起来简单但后端和判题机耦合紧判题机一重启后端的请求就超时报错。我一般用「队列 worker」模式判题机独立消费后端只管把任务丢进队列判题机挂了对提交入口无感。worker 的伪代码大致是这样import redis, requests, json, os r redis.Redis(hostos.environ[QUEUE_HOST], port6379, db0) while True: task r.blpop(judge_queue, timeout5) if not task: continue payload json.loads(task[1]) resp requests.post( http://127.0.0.1:5050/run, jsonpayload[body], timeout60 ) r.lpush( judge_result, json.dumps({ requestId: payload[requestId], result: resp.json() }) )blpop是阻塞式弹出队列为空就一直挂着等不空转 CPUrequestId从取任务到写回结果全程携带后面做幂等和重试都靠它判题机自身跑在 worker 进程里go-judge 的 HTTP 服务只监听 127.0.0.1外部网络完全不可达。安全组的设置原则是判题机的 5050 端口永远不对公网开放。后端和判题机在同一个云 VPC 内网里安全组入站规则只放行后端服务器的内网 IP目的端口 5050 一条规则就够。有的团队图省事把 5050 直接暴露公网等于把一台 root 权限的沙箱机器开放给全网被拉去当挖矿节点是迟早的事。如果后端和判题机不在同一内网就用云厂商的对等连接打通原则不变公网永远不可达判题端口。4.2 用 systemd 守护 go-judge 进程unit 文件与开机自启如果你不想用容器go-judge 就是一个 Go 单二进制扔到 /usr/local/bin 下面配一个 systemd 服务这是最稳的守护方式。下面这个 unit 文件我直接贴# /etc/systemd/system/go-judge.service [Unit] DescriptionGoJudge sandbox executor Afternetwork-online.target Wantsnetwork-online.target [Service] Userroot ExecStart/usr/local/bin/go-judge \ --http-addr 127.0.0.1:5050 \ --parallelism 4 \ --work-dir /var/lib/go-judge Restartalways RestartSec5 LimitNOFILE65536 [Install] WantedBymulti-user.target保存后执行systemctl daemon-reload systemctl enable --now go-judge journalctl -u go-judge -f几个参数说明。--http-addr 127.0.0.1:5050是让 go-judge 只监听本机回环配合第 4.1 节的 worker外部无法直接触达如果后端不在本机可以改成内网 IP但千万别配 0.0.0.0 对公网暴露。--parallelism 4是同一时刻最多跑几个沙箱按第 2.3 节的换算4 核机器配 4 正合适。--work-dir指定沙箱临时文件目录我习惯放在 /var/lib/go-judge 而不是 /tmp理由前面说过/tmp 可能被系统定时清理也会拖慢系统盘。Restartalways配合RestartSec5进程崩了 5 秒后自动拉起这是无人值守下的自愈。这里有个容易翻车的地方Restartalways会把任何原因的退出都拉起来如果端口被占用导致进程反复退出就会形成重启循环。所以启动前先确认 5050 端口没被别的进程占用unit 里也不要画蛇添足写 PIDFilego-judge 自己管理端口。4.3 多判题机横向扩展分片队列、幂等回写和失败重试用户量上来之后单台判题机不够横向扩展是必然的。多判题机的架构不复杂但有三件事必须做对。第一队列分片。多台判题机如果都从同一个 judge_queue 消费Redis 的 blpop 本身是原子的不会重复消费所以队列可以共用一个但如果你担心单队列成为瓶颈可以按submission_id % N分片到 judge_queue_0、judge_queue_1 这样 N 个队列每台机器认领一片。分片后某台判题机挂了只影响它负责的那部分提交。第二结果幂等。判题结果写回 judge_result 队列时后端消费端必须用 requestId 做唯一键重复的结果只处理一次。判题机崩溃恢复后可能重发结果后端不幂等的话同一道题会被算两次分。第三失败重试。worker 调 /run 返回超时或 Internal Error 时任务要重新入队而不是丢掉。重试次数上限建议 3 次超过就把任务标记为 system_error 交给人来看别无限重试把队列堵死。还有一个经验多台判题机的 parallelism 不要超配。宁可机器 1 满载、机器 2 空闲也不要每台都超配到 CPU 竞争。判题任务本身是 CPU 密集的超配的后果是每个任务的实际耗时线性上涨整体吞吐反而下降这就是看似「都在跑」、实际都在等 CPU 的假并发。每台新机器接入前先跑一遍第 6 章的冒烟脚本确认语言环境齐全再放量。5. GoJudge 部署避坑指南5 个真实翻车点与排查路径判题机的坑有个共同特点表面现象在 OJ 后端根因在沙箱和系统层排错链路长。这一章挑我实际踩过、也见过别人踩的 5 个问题按「现象 → 原因 → 解决」写可以直接当排查手册用。5.1 现象第一次调用 /run 就返回 Internal Error现象Docker 部署好/version 正常但一发 /run 就返回 Internal Error日志里能看到operation not permitted或mkdir /sys/fs/cgroup失败。原因go-judge 的沙箱要创建命名空间和 cgroup普通容器的默认 seccomp 和 capabilities 不允许这些操作。最常见的就是 docker run 时漏了--privileged或者图省事用--cap-add只加了部分能力结果能力不全。解决容器方案直接加--privileged裸机二进制方案确认是用 root 启动的然后检查内核参数unprivileged_userns_clone是否被禁用。改完容器参数后要重建容器而不是 restart因为运行参数只在创建时生效。5.2 现象Java 提交稳定超时但同样的代码本地秒过现象OJ 上 C/C 都正常只有 Java 提交几乎全部 TLE本地跑明明只要一两百毫秒。原因JVM 启动有固定开销判题的 CPU 时间限制如果按 C 的标准给 1 秒Java 光启动就占掉大半更隐蔽的是 JVM 的堆外内存元空间、JIT 编译产物不计入-Xmx沙箱内存限额给太紧时JVM 还没跑起来就被 OOM 杀掉表现也是超时而不是内存超限。解决Java 的 CPU 时间限制单独配置至少给到 C 语言的 2 到 3 倍运行时加-Xmx256m -XX:UseSerialGC -XX:-UsePerfData沙箱 memoryLimit 给到 512MB 而不是 256MB给 JVM 留出堆外余量。还有个细节javac 编译那一步也要单独设时间上限Java 的编译比 C 慢不少。5.3 现象Python 程序读不到输入文件报 FileNotFoundError现象代码里写了open(input.txt)本地有文件没问题判题时永远 FileNotFoundError改成从 stdin 读就正常。原因这是对沙箱工作机制的误用。go-judge 每次 /run 的工作目录是临时创建的空目录里面除了 copyIn 注入的文件什么都没有。宿主机文件路径、上次请求留下的文件统统不存在。解决OJ 后端把测试数据文件通过 copyIn 注入key 就是工作目录下的文件名代码里用相对路径读。对于输出比对类的题更好的做法是把输入放在 files[0] 即 stdin程序统一读标准输入免去文件注入的路径管理。我在 OJ 里通常约定所有题目输入走 stdin不需要判题机管文件直接少掉一半的诡异问题。5.4 现象内存限制没生效恶意程序把整台云服务器拖慢现象提交一个不断分配列表的 Python 程序按理说应该在几百 MB 处被掐掉结果服务器的负载一路飙升其他服务跟着卡顿。原因两个叠加因素。一是云服务器默认开了 swap内存超限的程序先换页到磁盘cgroup 的 memory.limit 没有直接触发 OOM二是系统是 cgroup v2 但 go-judge 版本较老或者容器没有正确挂载内存限制根本没生效。判断方法是进沙箱查看对应 cgroup 的 memory.events 文件看 oom_kill 计数有没有增长。解决云服务器关掉 swap或者至少把 swappiness 调到接近 0确认内核和 go-judge 用的是同一套 cgroup 版本必要时升级 go-judge最后用真实内存爆炸程序做一次验证确认状态位返回的是 Memory Limit Exceeded而不是把机器跑死。5.5 现象高并发下判题结果串了别人代码出现在自己提交里现象并发一上来偶发出现某次提交的 stdout 是别人代码的输出或者 copyOut 的文件内容张冠李戴。原因最典型的是多实例共用了同一个--work-dir。go-judge 虽然每次 /run 独立沙箱但 work-dir 是共享的临时文件根目录多个实例同时读写相同路径文件就串了。另一种可能在后端回调结果没带 requestId 或没做幂等结果写入时覆盖了之前的结果。解决每台机器、每个 systemd 实例用独立的 --work-dir按实例名区分目录结果回写的 requestId 必须全链路携带消费端做唯一键去重。压测时特意混入不同用户的请求比对返回内容和提交内容是否一致这是这类问题的标准验证姿势。6. 进阶上线前用 wrk 压测 go-judge把并发参数调准配置都正确、功能测试都过了不代表能直接上线。我现在的习惯是任何一台新的判题机在接入正式队列之前先压一轮测用数据校准 parallelism并把三语言冒烟测试写进部署脚本。压测工具用 wrk 就够了配合一个 Lua 脚本构造 /run 请求-- /tmp/judge.lua wrk.method POST wrk.headers[Content-Type] application/json wrk.body [[ {cmd:[{args:[/usr/bin/python3,loop.py], env:[PATH/usr/bin:/bin], files:[{content:},{name:stdout,max:1024},{name:stderr,max:1024}], cpuTimeLimit:1000000000, memoryLimit:134217728, copyIn:{loop.py:{content:for i in range(100000): pass\nprint(1)}}}] ]]执行压测wrk -t4 -c16 -d30s -s /tmp/judge.lua http://127.0.0.1:5050/run看三个指标平均延迟、错误率、有没有 Internal Error。这个负载是一个约 200 毫秒的纯 CPU 任务压测得到的 QPS 乘以单任务耗时就是这台机器的饱和并发数。然后把 systemd 里的--parallelism调到这个数附近再压一次观察延迟是否平稳。如果延迟随并发线性飙高说明超配了降两档再测。压测之后把三语言冒烟测试固化成脚本每次部署完必须跑一遍#!/bin/bash set -e curl -fsS http://127.0.0.1:5050/version /dev/null # 用 curl 分别提交 C、Python、Java 的最小可执行程序 # 检查返回 JSON 里的 status 均为 Accepted echo smoke test passed这套冒烟脚本我会存进部署仓库跟 Dockerfile 放一起。走完一次完整上线的流程后我的固定动作是先压测校准并行数再放真实队列观察半小时日志确认没有 Internal Error 和超时重试才把流量完全切过去。这个顺序帮我挡掉了至少两次线上事故。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。