行业资讯
📅 2026/9/9 13:23:35
基于DOMjudge自建高校OJ判题平台:从源码编译到并发调优全记录
前阵子帮实验室搭了一套DOMjudge从裸机到能跑一场模拟赛前前后后折腾了两个周末。这套系统在ICPC这类算法竞赛生态里地位非常稳定很多高校的内训赛、校赛包括一部分省赛和邀请赛后台用的就是它。当时我的需求很明确给训练队提供一个能出题、能提交、能实时排名的内部评测平台同时支持多语言判题赛时上百人同时提交不能卡死。这篇不是官方文档的翻译是我自己在Ubuntu上从源码编译部署的真实记录。DOMjudge的官方文档其实写得已经算清楚但真正落地的坑全藏在文档缝隙里比如chroot构建、cgroup权限、judgehost账号体系、重启后的服务依赖顺序这些不亲自踩一遍很难体会。如果你正打算给社团、实验室或者学校搭一套OJ或者只是好奇这套比赛系统内部是怎么工作的这篇可以帮你省掉至少一个周末的试错时间。1. 项目背景与整体方案选型1.1 为什么选择自建判题服务训练队之前一直用公共OJ做题日常练习倒是无所谓但一到内部模拟赛就比较麻烦。首先是题目数据问题自己出的题直接放公共平台总不太放心数据泄露出去后面正式比赛就没法用了。其次是规则定制问题训练赛有时候想开IOI赛制有时候想开ICPC赛制甚至只是想限制某个时间段内可提交公共OJ很难按需调整。第三是稳定性公共平台在高峰期经常队列堵住训练赛一旦中断节奏就全乱了。所以自建一套判题服务就成了刚需。自建之后出题人可以用Polygon之类的工具生成数据直接导入教练可以控制所有权限和数据可见性队员在校园网内提交延迟低还能看到完整判题日志。DOMjudge在竞赛圈里口碑一直不错从90年代发展到现在经历过ICPC World Finals级别的压力验证用来做校赛和训练赛心里比较有底。1.2 DOMjudge核心架构与运行链路DOMjudge整体分两大部分domserver和judgehost。domserver管的是Web后台、数据库、题目管理、用户管理、提交队列和排行榜它本质上是一个PHP应用加MySQL/MariaDB的组合对外提供页面和API。judgehost才是真正干活的一方它在独立的机器或者同一台机器的独立进程里运行通过HTTP方式向domserver轮询获取任务拿到提交后执行编译、运行、比对再把结果回传。一次完整提交的路径大致是这样的选手在网页提交代码domserver把代码落库并扔进提交队列judgehost每隔几秒向API发起一次请求拉到新的提交然后judgehost在chroot沙箱里执行编译和运行用cgroup限制内存和CPU时间比对输出结果后把verdict回传给domserverdomserver更新数据库并刷新排行榜。整个过程没有用到消息队列中间件而是非常朴素的“轮询状态机”好处是组件少、好排查坏处是高并发下数据库压力会大一些所以官方也建议生产环境把domserver和judgehost分开部署。2. 搭建前的准备与基础环境初始化2.1 服务器硬件与系统版本怎么选先说硬件。我这次用的是实验室一台4核8线程、16G内存、500G SSD的机器装完系统后感觉这个配置对流偏上。CPU核心数直接决定并发判题能力一个judgehost实例同时判的题数是可以配的核心多就多开几个实例模拟赛80人以内基本不会堵队列。内存方面最吃紧的环节其实是chroot构建构建一个完整的Debian判题环境几分钟内可能吃掉1G到2G内存但构建完成之后单个评测任务的内存占用通常只有几百M实际比赛时16G很宽裕。系统版本强烈建议Ubuntu Server 22.04 LTS或者Debian最新稳定版原因是DOMjudge编译安装需要一堆构建工具和PHP扩展apt源里都有省去大量手动编译依赖的时间。我自己用的Ubuntu 22.04PHP版本是8.1JDK、GCC、Python这些编译器都是apt直接装的整个过程下来没有遇到系统版本层面的硬伤。2.2 编译安装阶段的依赖准备拿到源码之前先把依赖装齐。我是按官方文档的apt依赖列表装的先update再装基础包sudo apt update sudo apt upgrade -y sudo apt install -y build-essential autoconf automake flex bison libtool pkg-config \ php-cli php-gd php-xml php-curl php-intl php-mbstring php-mysql php-zip php-fpm \ mariadb-server mariadb-client nginx \ gcc g openjdk-17-jdk-headless python3 python3-pip gobjc \ debootstrap libcgroup-dev libcurl4-openssl-dev \ libjsoncpp-dev libmagic-dev libzip-dev这里我踩的第一个坑是PHP扩展不完整。configure阶段如果提示找不到某个函数或扩展先别怀疑源码绝大多数情况是apt装少了。比如缺少php-mbstringWeb后台有些菜单会白屏或报错缺少php-curljudgehost和domserver之间的API通信就会异常。装完依赖之后最好执行php -m看一眼扩展列表对着官方文档的requirements核对一遍这一步比后面调bug省时间多了。另外别忽略时间同步。domserver和judgehost时间相差大会出现提交记录时间错乱、判题队列顺序问题。Ubuntu默认有systemd-timesyncd装完系统看下timedatectl确保System clock synchronized: yes这是个小细节但对整个判题链路的一致性很重要。3. 核心安装与配置实战3.1 domserver编译安装与Web入口配置我以DOMjudge 8.2版本为例从GitHub拉源码编译安装domservercd /opt git clone -b main https://github.com/DOMjudge/domjudge.git cd domjudge ./configure --prefix/opt/domjudge/domserver --with-baseurlhttp://oj.example.edu.cn/ make domserver sudo make install-domserver--with-baseurl要填你对外访问的地址如果后面用域名或者换了端口记得同步修改配置文件否则生成的一些链接URL会不对。安装完成后目录在/opt/domjudge/domserver其中bin存管理脚本etc存配置www是Web根目录。接下来配nginx和php-fpm。我是先启了php8.1-fpm然后写一个最基础的nginx站点配置server { listen 80; server_name oj.example.edu.cn; root /opt/domjudge/domserver/www; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.1-fpm.sock; } }配置好后执行sudo nginx -t检查语法然后启动nginx。这一步常出现Web目录权限问题DOMjudge编译安装完目录属主还是root而nginx worker是www-data需要把缓存和日志相关目录交给www-data否则后台打开会报写权限错误。我当时执行了sudo chown -R www-data:www-data /opt/domjudge/domserver/webapp/var/cache sudo chown -R www-data:www-data /opt/domjudge/domserver/webapp/var/log3.2 数据库初始化与账号体系domserver装完之后要初始化数据库。执行sudo /opt/domjudge/domserver/bin/dj-setup-database -u root -p 你的数据库密码 -r install这步会创建名为domjudge的数据库同时生成两个关键账号一个是后台管理员admin另一个是judgehost专用账号。管理员初始密码存在/etc/domjudge/initial_admin_password.secret里安装完成后浏览器打开http://服务器IP/就能看到登录页第一次进去先改密码。另一个关键文件是/etc/domjudge/restapi.secret里面记录了judgehost使用的用户和token这个文件在后面配置judgehost时要原样复制过去。还有一件容易被忽略的事后台的时区设置。DOMjudge默认按UTC展示时间如果比赛是北京时间需要在后台的Config Check或者Settings里调整时区否则选手提交的时间和榜单显示时间会对不上。数据库端如果本身是UTC也没关系应用层统一处理就行主要别让web服务和数据库各用各的时区不然排查问题时会很困惑。3.3 judgehost安装与sudo权限配置judgehost是判题的核心组件可以和domserver装在同一台机器也可以单独装在别的机器。为了测试方便我第一次是装在同一个机器上后面拆出去也就是多复制一个restapi.secret的事。编译安装cd /opt/domjudge ./configure --prefix/opt/domjudge/judgehost make judgehost sudo make install-judgehost然后创建一个专用于判题的系统用户sudo useradd -d /nonexistent -U judge为什么不能直接用root跑judgehost因为判题进程要运行选手提交的不可信代码必须放在沙箱和降权环境里。judge进程本身用judge用户运行只有在需要执行runguard和create_cgroups这类特权操作时才通过sudo临时提升权限最小化攻击面。sudoers配置在/etc/sudoers.d/domjudge里内容类似judge ALL(ALL:ALL) NOPASSWD: /opt/domjudge/judgehost/bin/create_cgroups judge ALL(ALL:ALL) NOPASSWD: /opt/domjudge/judgehost/bin/runguard judge ALL(ALL:ALL) NOPASSWD: /bin/mount judge ALL(ALL:ALL) NOPASSWD: /bin/umount改完用sudo visudo -c校验语法。这里要特别说明sudo配置不当是新手最常碰到的问题常见错误是路径写错或者漏了NOPASSWD导致judgehost启动时提示无法执行runguard。排查方法很简单用sudo -u judge sudo -l看judge用户实际能执行哪些命令一行一行对着检查。接下来写/opt/domjudge/judgehost/etc/judgehost-config.php核心是告诉judgehost去哪找domserver以及用什么身份认证define(DOMJUDGE_HOST, http://127.0.0.1); define(RESTAPI_SECRET_FILE, /opt/domjudge/judgehost/etc/restapi.secret);把domserver安装时生成的restapi.secret复制到对应路径然后启动sudo /opt/domjudge/judgehost/bin/judgedaemon -b 2-b参数是同时判题的数量我机器4核8线程先跑2后面压测没有瓶颈再加。正常启动后domserver后台的Judgehosts页面就能看到这台判题机在线。3.4 chroot沙箱环境构建domserver和judgehost装好离能跑通还差最后一步大工程构建chroot环境。DOMjudge判题不是在宿主机直接跑的而是在一个预先做好的Debian容器里编译和运行选手代码这样即使提交的是恶意代码也无法访问宿主机其他目录和网络。官方脚本是sudo /opt/domjudge/judgehost/bin/dj_make_chroot这个命令会调用debootstrap下载并构建一个最小的Debian系统放在/opt/domjudge/judgehost/chroot下。整个过程取决于网络速度快则十来分钟慢则半小时以上我中途失败过一次原因是/tmp空间不够debootstrap释放临时包时直接写满磁盘。建议提前确认/tmp和/var/lib所在分区至少有5G可用空间。chroot里默认装好了GCC、G、Python、OpenJDK这些常用编译器。如果你需要某个语言比如Rust或者Go有两种做法一种是构建之前修改脚本把对应的包名加进去另一种是构建完成之后进入chroot手动安装命令大概是sudo chroot /opt/domjudge/judgehost/chroot /bin/bash里面就是一个小型Debian系统。我建议第一次搭建先用默认环境跑通之后再按需扩展这样出问题时能缩小排查范围。4. 判题链路联通与并发调优4.1 从AB开始验证完整判题链路整个平台搭建完成之后千万不要直接开正式比赛先做一个最小验证。我的做法是后台创建一个团队、一个用户、一道最简单的AB题目然后自己提交一段代码看整个流程走不走得通。题目管理里需要上传测试数据DOMjudge的测试数据是一个zip压缩包里面有data/secret/和data/sample/两个目录每个测试点两个文件比如01.in和01.out。压缩包结构不复杂但目录名不能写错我当时写成了testdata而不是data上传后一直提示测试数据为空。提交代码后可以在后台Submission页面看状态流转。正常的链路是queued-compiling-running-judging-finished。如果卡在queued不动说明judgehost没拿到任务如果卡在compiling多半是chroot或权限问题。日志排查主要看两个位置domserver侧看/opt/domjudge/domserver/webapp/var/log/下的日志judgehost侧看/opt/domjudge/judgehost/log/judgehost.log。判题机日志写得很详细每一条任务的处理流程、编译错误、运行时间都记录在案绝大多数问题在这两个日志里都能找到答案。4.2 并发判题与资源隔离参数调整第一次压测就发现单judgehost实例有点吃紧因为-b 2意味着同一时间只能判两道题一旦有选手交了耗时的暴力代码后面会排一小段队。解决办法有两个方向一是把-b调大比如4核机器跑-b 4二是开多个judgehost进程每个进程绑定不同实例编号。用systemd管理更省心。官方自带judgehost.service模板我把它复制到/etc/systemd/system/并修改了用户和启动参数然后sudo systemctl daemon-reload sudo systemctl enable judgehost sudo systemctl start judgehost多机扩展时再单独起一个judgehost实例只需要改judgehost-config.php里的并行数和restapi配置即可。domserver后台的Judgehosts页面会实时显示每台判题机的负载方便看出哪台机器空闲。资源隔离这块是DOMjudge的强项。运行选手程序时judgehost会调用runguard配合cgroup限制CPU时间、内存上限、进程数。比如题目设置内存限制512M选手程序试图申请1G内存直接返回MLE而不会拖垮整个宿主机。而且cgroup的限制是按任务隔离的多个判题任务同时进行互相之间不会影响。5. 常见问题与故障排查实录5.1 判题一直Pending五个方向逐个排查这个问题我前后遇到不止一次每次原因还不一样。整理成速查表供参考。现象可能原因排查方式提交后一直queuedjudgehost没启动或崩了登录judgehost机器执行systemctl status judgehost看日志提交后一直queuedrestapi.secret不匹配对比domserver和judgehost机器上的restapi.secret内容提交后一直queueddomserver和judgehost时间差太大两边执行date对比启用chrony或systemd-timesyncd提交后一直queued防火墙挡了API端口用curl从judgehost机器请求domserver的API接口测试连通性提交后一直compilingchroot环境损坏或编译器缺失手动进入chroot试编译确认GCC/G是否存在restapi.secret不匹配是新手最容易犯的错因为domserver和judgehost的secret文件必须同一个但安装路径不同很有可能会复制错。我建议在domserver机器上执行cat /etc/domjudge/restapi.secret然后把输出原封不动写到judgehost的对应文件里不要手动改任何字符。5.2 chroot构建失败与cgroup权限问题chroot构建失败的原因通常有三种。第一种是debootstrap没装或版本太旧Ubuntu 22.04用apt install debootstrap装新版即可。第二种是构建源网络超时dj_make_chroot默认走Debian官方源国内服务器可能很慢甚至失败可以改脚本里MIRROR变量换成国内镜像源构建速度会快很多。第三种是磁盘空间不足构建过程会在/var/lib和/tmp附近产生临时数据提前用df -h看好容量。cgroup权限问题表现比较隐蔽judgehost日志里会出现类似cant create cgroup的报错但服务进程还在正常跑。原因是执行create_cgroups需要root权限而judge用户只能通过sudo执行一定范围的命令如果sudoers文件里漏了这条命令runguard自然无法创建隔离环境。解决方案就是回到3.3那一步用sudo -u judge sudo -l检查权限配置把缺失的命令补全。5.3 重启后的服务恢复与备份习惯整个环境装完系统一重启所有服务全部得手动拉起来这是我最初没考虑到的。后来我把关键服务全部设成开机自启并注意了启动顺序MariaDB先起来php-fpm和nginx随后最后才是judgehost。用systemd的话可以在judgehost.service里加Aftermariadb.service nginx.service php8.1-fpm.service避免启动顺序不对导致judgehost连不上数据库。还有一个教训是及时备份。DOMjudge的数据都在MySQL里题目、用户、提交记录、排行榜都是重要比赛前一定先备份数据库命令很简单sudo mysqldump -u root -p domjudge /backup/domjudge_$(date %Y%m%d).sql测试数据文件、restapi.secret和judgehost配置也建议一并打包存放真的出问题时能快速恢复到比赛前状态。6. 个人经验与后续扩展6.1 让日常维护更省心的几个习惯用了一段时间之后我总结出几个值得坚持的习惯。首先是出题时统一命名规范测试点文件用一个格式比如01.in、01.out不要手滑写一个中文名进去否则数据加载和问题排查时都很痛苦。其次是给每个题目设置合理的内存和时间限制不要直接套默认值尤其是Python的题内存限制给得太小容易出现不明所以的MLE。第三是每周检查一次磁盘空间和judgehost运行日志判题日志增长很快累积起来可以占不少磁盘。另外后台开一个“安全”但好用的选项允许选手在提交后自己能跑自测样例。DOMjudge有allow submit client和自测能力配置好之后选手提交前可以先自测能减少很多无效提交对判题队列造成的压力对正式比赛体验提升很大。6.2 还能往哪些方向继续折腾DOMjudge本身是高度可扩展的我目前只用了基础能力后面还有几个方向值得尝试。一是special judge自定义比较器适合答案不唯一的题目比如输出浮点数、构造题可以在题目里挂自定义的compare脚本用spj验证选手输出二是对接PolygonPolygon是出题神器DOMjudge支持Polygon包导入出题效率会高很多三是把domserver和judgehost彻底拆分到不同机器用内网API通信这样判题机的故障不会影响Web端访问也更接近比赛现场的部署形态。如果对前端有要求DOMjudge的UI确实比较朴素但胜在稳定后台还提供API可以自己做一套面向选手的网页或小程序通过API访问提交状态和排行榜这个自由度就很大了。这次搭建下来我最大的体会是DOMjudge难点确实不在“装”而在把整个运行机制理解透。第一次搭你会觉得每一步都踩坑但当你把judgehost、chroot、cgroup、restapi这套东西串起来想明白之后第二套环境搭建会快到你不敢相信。最后再给一个建议别急着上生产先开一场内部测试赛让几个队员真实地提交、查看榜单、反馈问题把全流程跑顺了正式比赛时才不会手忙脚乱。