一次搭建内部 Online Judge 及调优的笔记

嗯,就是搭 OJ 这破事把我拽进了 Python 和 docker 的大坑中…… 为了给本校 OI 学员的交流和练(gao)习(shi)提供条件,便有了搭建 OJ 的这么一个想法。以前一直以为这种事情就和装个 WordPress 什么的差不多嘛,真正跳进去才发现,woc 这水居然这么深。 关于 OJ 的选择,这里我用的是青岛大学 (QingdaoU) 的开源项目 qduoj

之所以选 qduoj 呢是因为好看这个 OJ 的架构比较让人满意,网页端采用了 Python Django,判题端的沙箱做得还可以,然后整个项目跑在 docker 中,可以最大限度地减少对宿主机留下的后遗症,以及在判题方面有更高的安全保障。

我是在一台 Xeon E3 的 ThinkServer 上部署的 Online Judge,操作系统是 Arch Linux, 运行截至写本文之日最新版的 docker. 这个环境可以供各位参考一下,毕竟 Arch Linux 大法好.

Installation

部署之前,先按照项目 README 中的一步一步走就是了。项目需要 docker 和 docker-compose,由于我用的是 Arch,所以通过下面的命令安装依赖:

# pacman -S git python-pip docker docker-compose

安装完之后我们启动 docker 服务:

# systemctl start docker

然后就是把项目克隆到本地,这一步就不多说了,然后接下来是关键的一步,同时也是这个项目的第一个坑:

首先我们来看看原文 (README) 中是怎么说的: > 启动服务:运行 docker-compose up -d ,不需要其他的步骤,大约一分钟之后 web 界面就可以访问了,默认开放80和443端口。其中443端口是自签名证书。

这就结束了:超级管理员用户名是root,默认密码是password@root,请及时修改。登录/admin,添加一个判题服务器,地址为judger,端口为8080,密码是上面自定义的rpc_token。修改custom_settings.py可以自定义站点信息。

天真的我照做了,然后一切正常,打开浏览器访问 http://localhost/ ,然后喜闻乐见地 connection reset 了。对于一个对 docker 和 python 一窍不通的萌新来说,没有什么比用当场懵逼形容更合适不过的了……

于是翻 issues, 查找 docker 的玩法,调出了 oj_web_server 这个容器的 log,从 log 中发现找不到 WEBSITE_INFO 的 attribute,WEBSITE_INFO 在 custom_settings.py 当中有定义,初步推断应该是容器内的 django 读不到放在容器外的 custom_settings.py.

然后打开 docker-compose.yml 一看,似乎没什么不对啊。于是乎在群内求助,经由田师傅的指导,将 docker-compose.yml 中的 $PWD 变量统统改成 ./ 之后,重新 docker-compose up -d,终于在浏览器中看到了这个 OJ 的真容。大概可能也许是原项目对 $PWD 的环境变量使用有问题吧 OvO……或者是不同发行版不兼容……或者是我少配置了什么……谁知道呢。

一些优化

搭好了之后,就是魔改它的时间了……想了想,这是个基于 docker 的项目诶,想要魔改似乎很麻烦的说。

鉴于每次在容器中修改之后再 commit 的方法十分麻烦,这里我直接把容器里的代码部分复制出来,然后再用 docker 的数据卷机制从宿主机映射回去,这样就可以很方便地修改了,同时容器中也会实时更新:

# docker cp oj_web_server:/code /home/username/code

然后再修改 docker-compose.yml,按照其中的其它映射数据卷的配置的格式抄一遍,把复制出来的目录映射回去镜像里就可以了。

填坑

生产过程中还是在不停地踩坑……大部分是在判题的部分踩的。

添加题目的时候标题等字段必须有英文,且不能出现公式字符

这个只能说似乎防 XSS 的机制做得好像太严格了,稍微修改一下问题的表达,问题不大。

编译失败,Compile Error,后面是一大串 JSON

大概像这样的:

Compile error: Compile error, info: {'cpu_time': 1203, 'exit_status': 0, 'signal': 0, 'flag': 3, 'memory': 164458496, 'real_time': 1263}

当我看到标程 CE 的时候,又是一脸懵逼状。

参考这个 issue: https://github.com/QingdaoU/OnlineJudge/issues/40 ,原来是评测机里限制了编译时候的资源,包括 CPU 时间和内存什么的。但是可能是评测机的性能太差了,即使原作者放宽了限制依旧存在这个问题。那么只能再人为地放宽这个限制了。

用你喜欢的办法修改 judger 容器中的 language.py,把其中的 compile_max_cpu_time 和 compile_max_memory 稍微调大一点就好了。

Compiler Bomb

如果你闲着无聊的话,试试下面这件事:在一个 .c 文件中写入这段代码: main[-1u]={1};,然后用 gcc 编译。仅对 C 语言有效,C++ 无效。

这可是你自己要作死的。

编译这段代码之后,会生成16GB的文件。

关于编译器炸弹的原理,请看这里:http://link.zhihu.com/?target=https%3A//wikicoding.org/wiki/c/Compiler_Bomb/%3Futm_content%3Dbuffer7c944%26utm_medium%3Dsocial%26utm_source%3Dfacebook.com%26utm_campaign%3Dbuffer

网上有一篇文章说限制编译出的可执行文件的大小,然而我并没有在 gcc 的编译选项中看到任何可以限制大小的参数……

不过既然这个东西会生成 16GB 的文件,那么写入的话肯定需要时间对吧,既然如此我们还是可以通过限制编译所用的最大时间来防范这种攻击,超时的话直接干掉编译器返回 CE. 鉴于我并不知道判题端容器中的 gcc 似乎没有受到上文说的 compile_max_cpu_time 的限制的原因,我们直接从编译命令上下手,修改 language.py 中的 compile_command:

"compile_command": "/usr/bin/gcc -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c99 {src_path} -lm -o {exe_path}/main"

改成:

"compile_command": "timeout 3s /usr/bin/gcc -DONLINE_JUDGE -O2 -w -fmax-errors=3 -std=c99 {src_path} -lm -o {exe_path}/main"

这样子给编译设置一个最长 3s 的限制(当然,根据机器性能的不同,你可以设置短一点或者长一点),超过了就直接 kill 掉 gcc.

(大概还有很多坑没有踩,接下来再慢慢补充吧……)