项目标题"HUSTOJ(VMware)开发"指向的场景很明确:把 HUSTOJ 这套在线评测系统(在线判题系统)装在 VMware 虚拟机里,一边跑一边改代码。之所以要费劲开一台虚拟机,是因为 OJ 这种东西属于"编译器、数据库、Web 服务、守护进程、沙箱权限"五件套绑死在同一台机器上的组合,任何一个环节配错都会牵连一片,裸机装一次失败就得重装系统。虚拟机给了你一次"试错不用心疼"的机会——快照一打,改崩了三十秒回到上一秒的状态。这篇内容适合三类人:想在自己电脑上搭一套 OJ 用来出题和教学的;接了二次开发需求、需要读懂 HUSTOJ 源码的;以及已经装好了但判题一直卡住、提交后一直 Pending 想找根因的。我会按"虚拟机怎么配、服务栈按什么顺序装、判题机怎么编、二次开发从哪儿下刀、踩过的坑怎么排"这条线走完,中间给的都是可以直接抄的命令和配置,不会只讲概念。
1. 先把边界划清楚:虚拟机里的 OJ 到底要做成什么形态
动手之前得先想明白一件事:你是要一个"能用的 OJ",还是要一个"能改的 OJ"。这两个目标对环境的要求完全不同。前者追求一次装完不再碰,后者追求随时能重启服务、随时能回滚、随时能对照新旧两版代码的行为差异。我自己第一版就是按"能用"搭的,结果改判题逻辑的时候把数据库搞坏,没备份,只能从零重来,那次之后才彻底转成"开发形态"。
1.1 裸机装、容器装、虚拟机装,三种做法的真实代价
先摆一张对比表,这张表是我自己踩完之后总结的,不是抄的:
| 做法 | 装起来快不快 | 改崩了恢复成本 | 判题沙箱的权限问题 | 适合谁 |
|---|---|---|---|---|
| 物理机或本机双系统 | 中等,依赖包冲突多 | 高,可能要重装系统 | 正常 | 长期对外提供服务的场景 |
| 容器里跑全套 | 快,镜像一把梭 | 低,容器删了重来 | 麻烦,容器内 setuid 和 ptrace 行为容易和预期不一致 | 只想跑通看效果 |
| VMware 虚拟机 | 中等,但可复制 | 极低,快照秒回 | 和物理机一致,沙箱行为正常 | 要做二次开发的人 |
关键差异在第三列。HUSTOJ 的判题链路里有一个绕不开的东西:判题客户端需要以较高权限运行,才能对被测程序做资源限制和系统调用约束。在容器里做这套权限模型,经常出现"宿主机上好好的,进容器就报错"的情况,而且报错信息通常非常隐晦——不是告诉你权限不足,而是直接给你一个 Runtime Error,让你以为是用户代码写错了。虚拟机里跑的是完整内核和完整用户态,行为和你最终部署的服务器几乎一致,这一点对排查问题太重要了。
1.2 我为什么最终定成"单网卡 + 快照基线 + 双目录分离"
最终的形态是这样的:VMware 里一台虚拟机,网络用 NAT 模式,固定内网 IP,只给自己宿主机访问;系统装最小化版本,额外挂一块数据盘专门放题目数据和数据库文件;装完之后立刻打一个名为baseline-clean的快照,后续所有实验都在快照之后做。
单网卡 NAT 的理由很实际:你不需要让外网访问它,宿主机用浏览器打开http://虚拟机IP/就够了。桥接模式会让虚拟机暴露在局域网里,多一层不必要的麻烦。数据盘单独挂载的理由更实际:题目数据目录里动辄几百上千个测试点文件,数据库文件又是另一个增长点,这两块和你改的代码是分离的。代码改坏了你可以直接回滚系统盘快照,但题目数据留在数据盘上不受影响——这个细节能帮你省掉很多次"回滚之后题目全没了"的崩溃。
1.3 验收标准:什么样才算这套环境"搭好了"
我给自己的验收标准有四条,缺一条我就认为没搭完,不往下做二次开发:
- 能用一个普通账号登录 Web 端,看到题目列表;
- 提交一段正确代码,状态从"等待"走到"通过";
- 提交一段死循环代码,状态走到"超时",且 Web 端显示的时间接近设定的时限;
- 重启虚拟机之后,不手动敲任何命令,判题队列能自动恢复工作。
第四条最容易被忽略。绝大多数人装完能跑,一重启就废,因为守护进程没配开机自启,或者数据库没设开机启动。开发过程中你会频繁重启虚拟机(改内核参数、清理环境、恢复快照),每次都要手动拉一堆服务,一天下来能烦死人。
2. 虚拟机的规格单:给判题留够余量
很多人给虚拟机配 2 核 2G,装完发现判题慢得像蜗牛,提交一道题要等十几秒才出结果,还以为是软件问题。判题这个场景的资源特征是"短时高占用"——编译阶段吃满一个核,运行阶段要限制内存,同时来几份提交就并行跑几个进程。资源给不够,你调试的时候会分不清是代码问题还是机器跑不动。
2.1 CPU 与内存的分配逻辑
我给开发机的配置是 4 核 CPU、8G 内存、系统盘 40G、数据盘 60G。这个配置的推导过程是这样的:
判题的并发度通常设成 CPU 核心数,因为每个判题进程在同一时刻基本只用一个核。4 核意味着最多 4 份提交并行判,对于开发调试够用了。内存方面,编译一份 C++ 代码峰值能吃到几百兆,并行四份就是 1G 以上,再加上数据库、PHP-FPM 进程池、Web 服务,8G 是一个比较舒服的余量。如果你只想单人调试,2 核 4G 也能跑,但把并发度调到 2,否则多份提交会互相抢资源,测出来的时间完全不可信。
注意:判题机测出来的运行时间是"墙钟时间 + CPU 时间"混合判断的,虚拟机本身有调度开销。如果你要校准题目的时间限制,最好在虚拟机空闲时测,并且把并发度临时降到 1,否则同样一份代码两次测出来的时间能差出一倍。
2.2 磁盘布局:系统盘与数据盘分开
系统盘 40G 装系统和服务栈足够。数据盘 60G 挂到/home/judge这个位置,因为 HUSTOJ 默认把源码、数据、日志全放在这个用户的家目录下。挂载的时候注意两件事:一是格式化时选常规的 ext4 就够了,别折腾花哨的文件系统;二是在/etc/fstab里写挂载项时加上defaults,noatime,减少文件访问时间戳的写入,题目数据目录里的文件会被频繁读取,这个参数能省一点 IO。
先建目录再挂载,顺序别反了:
sudo mkdir -p /home/judge sudo mount /dev/sdb1 /home/judge sudo blkid /dev/sdb1 # 拿到 UUID然后把 UUID 写进/etc/fstab:
UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx /home/judge ext4 defaults,noatime 0 2用 UUID 而不是/dev/sdb1的原因很直白:虚拟机上加了新磁盘或者调整了控制器顺序之后,盘符可能变,用设备名写死的话某天开机就挂不上,服务全挂。
2.3 网络模式选择与固定 IP 的落地
NAT 模式下 VMware 会给你一个虚拟网段,通常是192.168.x.0/24。默认走 DHCP,IP 可能会变,而你在浏览器里存的地址、配置文件里写的回调地址都会因此失效。所以进系统第一件事就是把 IP 固定下来。
用 netplan 的写法(新版 Ubuntu 系):
network: version: 2 ethernets: ens33: dhcp4: false addresses: [192.168.150.128/24] routes: - to: default via: 192.168.150.2 nameservers: addresses: [223.5.5.5]网卡名和网关地址要按你自己虚拟机里的实际情况改,ip a和 VMware 的虚拟网络编辑器里都能查到。改完执行sudo netplan apply。这里有个小坑:如果你在虚拟机里又装了 Docker 之类的会改防火墙规则的东西,重启后 netplan 的规则可能被覆盖,表现出来就是"IP 是对的但网页打不开",遇到这种情况先sudo iptables -L看一眼转发链有没有被改。
3. 系统底座与服务栈的安装顺序(顺序错了会返工)
HUSTOJ 的安装脚本其实做了一件事:把 Web 服务、数据库、PHP 扩展、编译工具链、判题程序按正确顺序串起来。手工装的时候最容易犯的错是"先把源码丢进去再装依赖",结果安装脚本检测不到某些组件就跳过了一步,最后现象是网页能开但提交报错。
3.1 依赖包的分层安装
我把依赖分成三层,按层装,每层装完验证一次:
第一层是基础工具链:build-essential、g++、make、flex、unzip、curl、vim。这一层是给编译判题程序用的。
第二层是 Web 与数据库:nginx、php-fpm、php-mysql、php-gd、php-xml、php-mbstring、php-curl、mysql-server或mariadb-server。注意 PHP 的版本——HUSTOJ 的老代码里有相当多 PHP 5/7 时代的写法,直接上最新的 PHP 8.x 会遇到一堆警告甚至致命错误。我在虚拟机上固定用 PHP 7.4 或 8.1 这两个版本,兼容性最好,装之前先apt-cache policy php-fpm看看仓库里给的是哪个版本。
第三层是判题专用:libmysqlclient-dev(编译判题程序要链接数据库客户端库)、libseccomp-dev(如果版本里带了 seccomp 支持)、time、sudo。
一条命令装完(以 Debian 系为例):
sudo apt update sudo apt install -y build-essential g++ make flex unzip curl vim \ nginx php-fpm php-mysql php-gd php-xml php-mbstring php-curl \ mariadb-server libmariadb-dev libseccomp-dev time sudo装完之后验证三件事:php -v有输出、nginx -v有输出、mysql --version有输出。三个都通再往下走,别嫌麻烦,这一步省下来的时间后面要加倍还。
3.2 数据库初始化与字符集设定
建库这一步,字符集必须一次设对。用默认的 latin1 建库,后面题目描述里的中文全变问号,改起来要导表重建,非常折磨。
CREATE DATABASE jol DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER 'jol'@'localhost' IDENTIFIED BY '换成你自己的强密码'; GRANT ALL PRIVILEGES ON jol.* TO 'jol'@'localhost'; FLUSH PRIVILEGES;然后把 HUSTOJ 源码里自带的建表脚本导进去,脚本一般位于src/install/目录下,文件名类似hustoj.sql。导入之前建议先打开看一眼,确认里面没有CREATE DATABASE语句覆盖你刚建的库。
导入之后验证编码:
USE jol; SHOW VARIABLES LIKE 'character_set%';character_set_server和character_set_database都应该是utf8mb4。如果不是,去/etc/mysql/下的配置文件里补上[mysqld]段的character-set-server=utf8mb4和collation-server=utf8mb4_unicode_ci,然后重启数据库。
提示:数据库账号密码要和后面 PHP 配置文件里的完全一致,包括大小写和特殊字符。密码里有
#、$、@的时候,写进 PHP 文件的单引号字符串里一般没问题,但写进 shell 脚本时记得转义,我在这上面浪费过半小时。
3.3 Web 服务与 PHP 的对接断层
nginx 和 php-fpm 之间靠一个 unix socket 或者 TCP 端口通信,这一层没接上,网页就是 502。先确认 php-fpm 的监听方式:
grep -r "listen" /etc/php/*/fpm/pool.d/www.conf通常会看到listen = /run/php/php7.4-fpm.sock或listen = 127.0.0.1:9000两种。把这个值抄到 nginx 的站点配置里:
server { listen 80; server_name _; root /home/judge/src/web; index index.php index.html; client_max_body_size 64m; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php7.4-fpm.sock; } }client_max_body_size这一行别省。默认 1M,用户提交一份长代码或者上传一个测试数据包,直接 413 报错,而且报错页面很难看懂。同时把 PHP 的upload_max_filesize、post_max_size一起调大,三个地方的值要匹配,只改一个没用。
还有两个和开发体验直接相关的 PHP 配置:date.timezone设成Asia/Shanghai,否则页面上所有时间都差八小时;display_errors在开发阶段设成On,能省掉大量"白屏不知道为什么"的排查时间。生产环境记得关掉。
4. 判题机的编译、配置与第一次真机验证
到这一步 Web 端应该能打开、能登录、能看到题目列表了。但只要你提交一次,就会发现状态永远停在"等待"——因为判题程序还没编译、没启动。这是整个搭建过程中技术含量最高的一段,也是二次开发的主战场。
4.1 judge.conf 里每个字段到底在管什么
配置文件一般在/home/judge/etc/judge.conf。这个文件不长,但每个字段配错都对应一个具体故障,我挑关键的说:
OJ_NAME=MyOJ OJ_HOME=/home/judge OJ_HOST_NAME=127.0.0.1 OJ_DB_NAME=jol OJ_DB_USERNAME=jol OJ_DB_PASSWORD=和数据库一致 OJ_TEMPLATE=bs3 OJ_DATA=/home/judge/data OJ_RUNNING=4 OJ_SLEEP_TIME=2 OJ_CPU_TIME_LIMIT=10 OJ_COMPILE_CPU_TIME_LIMIT=10 OJ_MEM_LIMIT=256OJ_RUNNING是并发判题数,设成 CPU 核心数比较合理。OJ_SLEEP_TIME是守护进程轮询数据库的间隔秒数,设太小数据库压力大,设太大提交后要等很久才有反应,2 到 3 秒是常用值。OJ_CPU_TIME_LIMIT是全局兜底时限,题目的时间限制在这个范围内才生效。OJ_DATA指向题目数据根目录,每个题目一个以题目编号命名的子目录。
OJ_TEMPLATE决定前端用哪套模板,值对应 Web 目录下template/里的子目录名。这个字段在二次开发时会频繁改,因为你可以复制一份模板目录改名,然后改配置切换,出问题随时切回来。
4.2 judged 与 judge_client 的权限模型
这两个程序的关系是:judged是常驻守护进程,负责轮询数据库里待判的提交;judge_client是单次判题的执行体,负责编译、运行、比对结果。为什么拆成两个?因为每次判题面对的是不可信的用户代码,需要一个进程专门承担风险,判完就退,而守护进程要长期稳定。
编译命令在源码的core目录下,通常是一个 shell 脚本包着 g++ 调用:
cd /home/judge/src/core sudo ./make.sh编完会生成judged和judge_client两个可执行文件,安装脚本会把它们复制到/home/judge/下。
这里有个非常关键、非常容易漏掉的步骤:权限位。判题客户端需要对被测程序施加资源限制,这需要额外的权限。手工编译时如果漏了chmod u+s judge_client这一步,现象是每次提交都是 Runtime Error,日志里写着创建沙箱失败之类的信息,但你在 Web 端完全看不出来原因。同样,资源限制的辅助程序(通常叫judex或者版本不同的对应文件)也需要正确的权限位。
sudo chown root:root /home/judge/judge_client /home/judge/judex sudo chmod u+s /home/judge/judge_client sudo chmod 755 /home/judge/judex题目数据目录的属主也要调整,因为判题进程会以较低权限用户去读这些文件:
sudo chown -R judge:judge /home/judge/data sudo chmod -R 755 /home/judge/data4.3 用一道 A+B 跑通全链路并读懂日志
先手动启动守护进程看日志:
sudo /home/judge/judged tail -f /home/judge/log/judge.log另开一个窗口,在 Web 端建一道最简单的题:输入两个整数,输出它们的和。测试数据放在/home/judge/data/1000/下,文件名是1.in、1.out、2.in、2.out这种格式。这里有个细节:.out文件的末尾最好留一个换行,并且判题时通常会忽略行尾空白,但如果你的数据文件是在 Windows 上用记事本编辑的,里面会混入回车符,比对就会失败。我建议在虚拟机里直接用vim或者printf生成测试数据。
建完题提交一份正确代码,然后盯着日志看。正常的日志会依次出现:读取提交、创建临时目录、编译成功、逐个测试点比对、写回结果。哪一步断了,问题就在哪一步。
判题结果的状态码在 Web 端显示为中文,但在数据库里是数字。不同版本的状态码定义不完全一样,最可靠的做法是打开 Web 源码里定义常量的那个文件(通常在include/目录下),把数字和含义对一遍。我调试时习惯直接查库:
SELECT solution_id, problem_id, user_id, result, time, memory FROM solution ORDER BY solution_id DESC LIMIT 10;result字段里,待判状态是一个较小的数字,判完之后会变成各种错误码。看到大量提交卡在待判状态超过一分钟,基本可以断定守护进程没跑或者数据库连不上。
5. 二次开发的切入点:从改模板到写自定义判题
环境跑通之后才进入正题。HUSTOJ 的代码结构是典型的"老派 PHP + C++ 守护进程"组合,不算优雅,但分工清晰,摸清目录之后改起来很顺。
5.1 源码目录地图
先把这个地图记住,能省掉大量grep时间:
| 路径 | 职责 | 你会在什么时候进这里 |
|---|---|---|
src/web/ | PHP 前端,所有页面 | 改界面、加字段、加新页面 |
src/web/include/ | 公共函数、数据库连接配置、常量定义 | 改配置、查状态码、加公共方法 |
src/web/template/<名称>/ | 各套前端模板 | 换皮肤、调布局 |
src/web/admin/ | 后台管理页面 | 加管理功能 |
src/core/ | 判题程序 C++ 源码 | 改判题逻辑、编译方式、资源限制 |
src/install/ | 安装脚本与建表 SQL | 重建环境、查表结构 |
/home/judge/data/ | 题目测试数据 | 加题、改数据、挂特殊判题 |
/home/judge/etc/judge.conf | 判题配置 | 调并发、改路径 |
/home/judge/log/judge.log | 判题日志 | 排查一切判题问题 |
改动最频繁的是src/web/include/db_info.inc.php,数据库地址、账号、模板名、站点标题都在这里。这个文件相当于整个前端的总开关。
5.2 改前端模板的最小改动路径
假设你要改首页的排版。正确做法不是直接改src/web/index.php,而是:
- 把
template/下现有的某套模板目录整个复制一份,改名成mytpl; - 在
db_info.inc.php里把模板变量改成mytpl; - 只改
template/mytpl/里的文件。
这么做的原因是升级源码时,src/web/根目录下的文件会被覆盖,模板目录是可以保留的,你的改动不会丢。我第一版为了省事直接改根目录文件,后来同步上游代码时被全覆盖,白干两天。
模板文件里通常引用了公共的头部和尾部文件,改布局时注意别把<head>里的样式表引用删了,删了之后页面能开但样式全丢,看上去像坏了其实只是丢了一行。
5.3 加一个字段:数据库、表单、展示三处必须联动
这是二次开发里最高频的需求,比如你想给题目加一个"难度星级"字段。改动点有三处,漏一处就会出问题:
第一处,数据库加列:
ALTER TABLE problem ADD COLUMN difficulty TINYINT DEFAULT 3;第二处,后台的题目编辑页面。在src/web/admin/下找到题目编辑相关的页面,里面会有一段拼接 SQL 的代码,把新字段加到 INSERT 和 UPDATE 语句里,同时在表单 HTML 里加一个下拉框或输入框。
第三处,前台的题目展示页面,在模板文件里把新字段读出来渲染。
这里最容易出的事故是 SQL 拼接时类型没处理好。difficulty是数字,表单提交上来的是字符串,直接拼进 SQL 不会报错但类型不对;如果字段是文本类型,而用户输入了单引号,轻则语句报错,重则被注入。老代码里这种拼接口很多,我改的时候会用参数化查询把新加的这部分包起来,改动量不大但能避免很多隐患。
改完记得清缓存——如果你的模板里做了文件缓存或者用了 PHP 的 opcache,改完代码刷新页面没变化,先sudo systemctl restart php*-fpm再看。
5.4 特殊判题的接入方式
标准判题是"逐字符比对输出",但有些题答案不唯一,比如要求输出任意一组可行解。这种题需要特殊判题程序(SPJ)。
接入方式是:在题目数据目录下放一个判题程序的源码或可执行文件,判题客户端检测到它的存在就会优先调用它,把"输入文件、选手输出文件、标准输出文件"三个路径传给它,由它返回判定结果。约定一般是返回 0 表示通过,返回非 0 表示失败,返回特定值表示需要人工确认。
写 SPJ 时有三个坑值得提前说:
第一,SPJ 自己也有时间限制,写一个 O(n²) 的校验逻辑在校验大数据时会超时,表现出来是选手代码明明正确却判失败。校验逻辑一定要写成线性的。
第二,SPJ 崩溃或者返回了预期之外的值,判题系统可能把它当成"失败"而不是"系统错误",于是你会看到一批莫名其妙的错误提交,实际是判题程序自己出问题了。所以调试阶段要单独用命令行手动跑一遍 SPJ,喂三个文件进去看返回值。
第三,SPJ 要用判题机上的编译器重新编译,编译器版本和你本地不同可能导致语法不兼容。我在本地用较新的标准写,扔到虚拟机上编译失败,卡了很久才发现是标准版本问题。
6. 三次重装换来的排错清单
下面这些坑,每一条我都真实踩过,而且都是在"看着应该没问题"的情况下踩的。我把现象、根因、验证方法列出来,你可以当成一个对照表用。
6.1 网页白屏或 502:先分清是哪一层挂了
现象是浏览器打开一片空白或者提示网关错误。排查顺序是自下而上:
先看 PHP 是否活着:sudo systemctl status php*-fpm。如果挂了,看journalctl -u php*-fpm -n 50的日志。常见原因是扩展缺失,比如代码里用了mysql_*系列函数但装的是 PHP 8,这些函数已经被移除了,报的是"调用未定义函数"。
再看 nginx 的错误日志:tail -f /var/log/nginx/error.log。如果里面写着connect() to unix:/run/php/xxx.sock failed,说明 nginx 的fastcgi_pass指向的 socket 路径和 php-fpm 实际监听的路径不一致。这个不一致经常是因为系统里装了两个 PHP 版本,一个 7.4 一个 8.1,各自有自己的 socket 目录。
最后才看应用层。在 Web 根目录临时放一个info.php,内容就是输出 PHP 信息,访问它看能不能出页面。这个文件能出、主页面不能出,问题就在应用配置;这个文件也出不来,问题在服务层。排查完记得删掉,这东西暴露出去不合适。
6.2 提交后一直"等待":判题队列的四种卡死原因
按概率从高到低排:
第一种,守护进程没启动。ps aux | grep judged看一眼就知道。手动启动后记得配开机自启,否则重启虚拟机就复现。
第二种,守护进程起来了但连不上数据库。日志里会出现连接失败的信息。验证方法是在同一台机器上用mysql -u jol -p手动登录一次,能登进去说明账号密码对,登不进去就是配置问题。注意配置文件里的密码有没有被引号包住、有没有多余空格。
第三种,数据目录权限不对。判题进程读不到测试数据,日志里会写找不到文件。ls -l /home/judge/data/1000/看一下属主和权限位。
第四种,上一次判题进程僵死,占着队列。这种情况的表现是日志停在某个提交不动,后面所有提交都不推进。ps aux | grep judge看有没有僵尸进程,有的话杀掉再重启守护进程。根治方法是给判题程序加超时兜底,不让单次判题无限期挂着。
6.3 编译通过却全是错误答案:数据文件的隐形差异
这类问题最气人,因为你的代码逻辑明明是对的。我用一道最简单的求和题查过两个小时,最后发现是数据文件的问题。
第一个隐形差异是换行符。Windows 下生成的.out文件行尾是\r\n,选手程序输出的是\n,逐字节比对直接失败。用file 1.out看是不是有CRLF字样,有的话用sed -i 's/\r$//' 1.out批量清理一遍。
第二个是文件末尾的空行。有的比对逻辑要求输出完全一致,多一个空行就判错,少一个换行也判错。建议统一规范:每个.out文件以一个换行结尾,不多不少。
第三个是编码。如果题目有中文输出,数据文件是 GBK 编码而选手程序输出 UTF-8,比对必然失败。统一用 UTF-8。
排查这类问题最快的办法是绕过判题系统,直接在命令行手动跑一遍:把输入喂给程序,输出重定向到文件,然后diff一下标准输出和自己生成的文件。diff能直接告诉你差在第几行第几个字符。
6.4 中文乱码与时间错乱
乱码问题排查起来有个技巧:先确定是数据库层还是展示层。直接查库SELECT title FROM problem LIMIT 5;,如果终端里显示正常,说明数据存储没问题,问题在 PHP 连接数据库时的字符集设置。在数据库连接代码里执行一次设置连接字符集的语句即可,通常是在建立连接之后紧接着执行SET NAMES utf8mb4。
如果终端里也是问号,那问题在建库或建表阶段,得检查表和列的字符集:
SHOW FULL COLUMNS FROM problem;Collation那一列应该都是utf8mb4开头的。有latin1的就要改,ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4。
时间错乱就一个原因:PHP 的时区没设。改php.ini里的date.timezone = Asia/Shanghai,同时数据库的时区也确认一下,两边不一致会出现"提交时间比现在晚八小时"或者"早八小时"的情况。
7. 让这套环境能长期用下去的几个习惯
搭好只是开始,能不能长期用取决于你维护它的方式。我见过太多人搭完之后不敢动,因为一动就崩,最后这套环境变成摆设。
7.1 快照的命名规则与回滚时机
我的规则是:每个快照名都带日期和状态,比如20240610-baseline、20240610-pre-edit、20240610-db-changed。打快照的时机有三个:装完系统和服务栈之后、每次要动数据库结构之前、每次升级源码之前。
不要指望"出了问题再想办法",判题系统涉及数据库和文件系统两套状态,手工回滚经常只回滚了一半。快照是整机状态,回滚就是彻底回到那个时间点,干净利落。
代价也要清楚:快照会真实占用磁盘空间,你打十个快照,虚拟磁盘能膨胀好几倍。我的习惯是保留三个:干净基线、当前稳定点、待观察的实验点,更早的定期删掉。
7.2 代码与数据分离备份
快照解决的是"整机回滚",但如果你只想保住题目数据和自己的代码改动,快照太重了。我的做法是两条独立的备份线:
代码线:自己改过的文件和模板目录,用 git 管起来,在 Web 目录下建一个仓库,只跟踪你改过的文件,别把整个目录都加进去——里面有一堆上传的附件和缓存文件,全提交会让仓库迅速膨胀。
数据线:题目数据目录和数据库导出。数据库用mysqldump定时导出,题目数据目录用tar打包。这两样加起来的体积比整机快照小得多,可以往宿主机上拷一份。
mysqldump -u jol -p jol > /home/judge/backup/jol_$(date +%Y%m%d).sql tar -czf /home/judge/backup/data_$(date +%Y%m%d).tar.gz /home/judge/data这两个命令我写进了一个脚本,每次大改动之前跑一次。有一次我误删了一个题目目录,就是靠这个包恢复的,前后三分钟。
7.3 多环境并行:一套改,一套验
真正开始做二次开发之后,单台虚拟机会不够用。你改代码的时候总得有个对照,不然改出问题分不清是自己改错了还是本来就这样。我的做法是克隆一台虚拟机,一台叫dev,一台叫verify,两台配置完全一样,dev上随便折腾,verify上只跑稳定版本。
克隆完要改两件事:IP 地址(否则两台冲突,都上不了网),以及主机名(方便在终端里分清自己在哪台机器上)。改完 IP 记得把judge.conf里连数据库的主机地址一起改掉,如果两台共用一个数据库实例就更要注意,别让两台机器的判题进程同时抢同一个队列。
让我最后说一句实在话:这套东西最大的价值不是你搭起来的那一刻,而是你改崩了能迅速恢复的能力。我前两次搭的时候,所有精力都花在"怎么装上去",一次都没想过"装坏了怎么办",结果每次都是从头再来。第三次我花了半天先把快照、备份、克隆这套流程走通,后面所有改动都变得很轻松——改错了回滚,改对了打个快照继续。至于判题程序那些参数,我建议你第一次就手动启动它、盯着日志看一遍完整的判题过程,比起读十篇教程,那几十行日志能让你立刻明白这套系统是怎么运转的。