青岛OJ部署实战:从零搭建在线判题系统的完整踩坑记录
做OJ这个念头在我脑子里转了大半年。最初只是被一些开源OJ项目的界面吸引后来工作在算法题、ACM训练上用得越来越多就越发觉得公共OJ平台在题目定制、比赛复现和数据权限上总差一口气。真正让我下定决心动手的是青岛OJ开源平台。它是目前个人开发者搭建在线判题系统比较省心的一条路线代码开源、依赖明确、前端后台和判题机一起交付不需要从零写沙箱和评测逻辑。这篇文章不是什么官方文档的复读而是一次完整踩坑记录。我会把选型思路、环境准备、判题机编译、参数调优、常见报错这些环节全部展开尽量让一个从没部署过OJ的人也能照着走通。适合谁看想给实验室搭内部刷题平台的同学想做编程培训要一个私有题库的团队以及纯粹想把“在线判题”搞明白的折腾型选手都值得读一遍。1. 为什么是青岛OJ选型思路与整体规划1.1 从零搭一个OJ的几条路线自建在线判题系统市面上能选的路子大致有三条。第一条是全手写从判题沙箱做起。自己用C或者Go实现进程隔离、资源限制、测试数据比对、题目管理后台。优点是自由度最大你想要什么判题结果类型、什么计分规则都能塞进去。缺点也很明显沙箱隔离是高风险模块内核层面的资源限制、权限降级、防逃逸策略任何一个环节有疏漏都有可能让用户通过恶意代码搞崩服务器。以个人开发者的精力把这条链路打磨到能稳定上线少说半年多说无下限。第二条是用一些比较重的开源教学平台改造比如在在线课程系统上挂OJ插件。这类方案功能大而全自带用户管理、课程编排、作业布置但判题引擎往往比较薄支持的比赛模式有限数据格式也常是私有的。如果只是练习用凑合能跑可一旦要模拟ACM赛制、做滚榜封榜就会明显力不从心。第三条就是青岛OJ这条路线判题核心、Web后台、前端界面一体打包系统依赖基本集中在LAMP/LNMP环境里。部署者要做的事情主要是搭好环境、初始化数据库、编译判题机、配置评测语言然后往里灌题就行。我实际走下来青岛OJ对个人开发者最大的价值是“专业部分不重复造轮子”判题核心已经考虑了系统资源限制、进程隔离、输出比对这些硬骨头前台后台和比赛流程也都现成。你要做的是把这套系统安置到自己的服务器上再调校到能稳定对外服务。1.2 青岛OJ的核心功能盘点选型不能只看热闹关键要看它有什么功能。按我实际使用体验整理一下在线提交与实时判题支持标准输入输出比对判题结果覆盖AC、WA、TLE、MLE、RE、CE、PE等常见状态。多语言评测C、C、Java、Python等主流语言都能配置判题机会为每种语言准备对应的编译和执行参数。题库管理题目可以单题添加也支持按格式批量导入测试数据以文件方式挂在服务器目录下。用户与权限体系管理员、出题人、普通用户分角色管理比赛和用户权限可配置。比赛功能支持ACM赛制能设置开始时间、封榜和榜单输出个人训练或学院内的小型比赛都够用。前后台分离管理后台负责数据管理和评测配置前台面向普通用户界面简洁二次开发也不难。1.3 部署架构与运行流程部署之前先把架构想清楚后面就不容易懵。青岛OJ的经典部署形态是一台Linux服务器同时承载Web服务、数据库和判题服务。用户通过浏览器访问Web页面提交代码Web后端把提交记录写入MySQL判题机一个独立守护进程轮询数据库中的待判任务拿到源码后调用对应语言的编译器在受控环境下运行测试数据最后把判题结果写回数据库前端再把结果展示给用户。这里面最核心的其实是判题机。它负责沙箱隔离、资源限制、测试数据比对。这也是青岛OJ这类成熟项目的价值所在你不需要自己用ptrace写一个进程监控器只需要配置好参数让它跑起来。2. 动手前的准备工作服务器、系统和基础依赖2.1 服务器配置应该怎么选选服务器第一个原则不要贪便宜但也不用一步到位。OJ的负载主要来自编译和运行判题CPU越强编译越快高峰期的评测队列越不容易积压。内存方面如果同时判几道多语言题目尤其是Java256MB的分配很容易被吃满所以单机部署建议至少2GB内存起步。磁盘要留足空间一个题目根据测试数据大小可能占几MB到几十MB攒上百道题再加上系统、日志、数据库几十GB的余地比较稳妥。我自己的配置是2核4G内存、40GB SSD日常带十几个并发提交毫无压力。如果你打算组织上百人同时在线的比赛建议4核8G以上带宽也要留意——虽然提交的数据量不大但页面静态资源和频繁的AJAX请求也吃连接数。2.2 Linux系统选择与初始化青岛OJ的判题机依赖Linux的系统调用做资源限制所以部署环境就是Linux。Ubuntu Server和Debian系列的软件源里依赖包齐全对新手非常友好。我选了Ubuntu 20.04 LTS稳定性和包版本都合适。系统装好后第一件事是更新软件源并完成基础设置。这里我用root身份或者一个有sudo权限的用户操作整个过程建议全程用固定IP操作。一个容易踩的坑是云服务器的安全组策略很多人装完环境后浏览器访问不了十有八九是云控制台的安全组没放行80端口而不是软件装错了。另一个容易忽略的是时区。OJ的比赛时间和题目开放时间全都依赖服务器时间如果时区不对整个比赛计划都是乱的。部署时尽快把时区改成Asia/Shanghai并启用NTP同步。2.3 数据库和PHP环境的版本搭配山东青岛这套开源系统是PHP MySQL的经典组合。PHP版本上不要太老也不要太激进。我实测在PHP 7.4下跑得非常稳更高的版本没有明显收益而老版本则容易碰到函数兼容问题。MySQL用的是5.7这也是项目文档里比较推荐的版本。数据库安装完成后需要手动创建一个独立库和对应账号密码强度不用太猛但也不要留空。系统初始化用的SQL文件里会建好表结构你只要导入即可。这个过程中最容易出的问题是MySQL的认证插件和PHP连接方式不匹配导致Web端报“数据库连接失败”后面我会单独讲排查思路。3. 核心部署全流程从源码到能判题3.1 源码获取与目录结构解读青岛OJ的源码通过Git仓库发布部署时直接clone到Web目录下。源码解压后主要目录结构是这样的Web前端和后台存放PHP页面、静态资源、题目展示配置。判题机源码独立的C目录需要手动编译生成可执行文件。数据库脚本建表语句和初始数据安装时导入一次。配置文件数据库连接、判题机参数、语言配置都集中在这里。拿到源码后先别急着运行我建议先整体浏览一遍目录找到配置文件确认库名、路径和监听参数。很多人部署失败就是因为路径写死的位置和实际不一致判题机找不到测试数据目录或者Web端连不上数据库。3.2 数据库初始化数据库这一步是纯机械操作但必须细心。先创建数据库再把源码里自带的SQL文件导进去。导入完成后建议到数据库里手动查一下表数量确认建表有没有遗漏。需要注意的是账号权限。如果Web应用和判题机共用同一个库账号那么这个账号需要有SELECT、INSERT、UPDATE、DELETE权限。实际部署时我会单独建一个只拥有该库全部权限的账号给应用层用避免误删其他库。这也是部署规范里不容含糊的地方。3.3 判题机编译与权限配置这是整个部署过程最有含金量的一步。判题机的核心是一个C程序需要装好编译工具链然后进入判题机源码目录执行编译。编译过程一般不会有致命错误但如果缺少某个依赖库会报头文件或链接错误只要根据提示apt安装对应开发包即可。编译完成后最重要的是权限设置。判题机的设计里运行用户提交的程序必须在最低权限下执行。常规做法是创建一个专用账号比如judge用户让判题机以这个账号运行。这样一来即使提交的代码是恶意的它也只能在隔离的用户态环境里折腾。青岛OJ源码里通常有示例启动脚本部署时把这些脚本里的用户和路径改成自己服务器的实际配置即可。注意千万不要图省事在root下启动判题机。OJ训练题里混入一个带恶意代码的提交不是开玩笑的事root权限下运行用户代码等于把服务器钥匙交出去。3.4 编译器与解释器环境搭建判题要能支持多种语言服务器就得装好这些语言的编译器和解释器。C和C需要gcc、g和标准库Java需要JDKPython需要带好judge用的解释器版本。装好不等于能判题你还得在判题机配置里声明每种语言的调用方式。青岛OJ的做法是给每种语言配置一组编译命令和执行命令比如C语言用gcc编译、生成的可执行文件由判题机运行Java则先javac编译出class文件再通过java命令指定内存参数运行Python不需要编译直接运行源码文件。这里的调校要结合实测。Java在OJ环境下需要显式限制最大堆内存否则可能会出现堆外内存超限或者频繁GC拖慢评测的情况。Python的解释器启动较慢对短时限题目不太友好这个不是青岛OJ的毛病所有OJ都这样。语言配置完成后可以先提交一道测试题验证各语言的编译和判题状态是否正常。这一步建议用最经典的AB如果连这题都编译不过说明编译器路径或权限配置有问题。4. 判题机背后的原理与参数调优经验4.1 判题是怎么跑起来的沙箱和资源限制很多人不明白“判题机”到底做了什么。简单说用户提交的是一段代码判题机拿到后要保证两点一是让代码在受限环境里运行不能影响系统二是记录它花了多少时间、多少内存再和题目限制比较。青岛OJ判题机实现资源限制的手段是Linux系统调用那套能力用setrlimit限制CPU时间和内存大小以专用账号执行来降权再通过管道或文件重定向捕获输出。最终效果是一个死循环提交最多消耗几个毫秒的CPU时间就被杀掉一个试图分配1GB内存的程序会在分配阶段就报错退出。理解这套机制对调优很重要。比如你发现某个题目在本地运行只要0.5秒但OJ上一直TLE很可能不是因为判题慢而是堡垒机资源占用高测试机CPU频率降频又或者Java/Python这类解释型语言的启动开销被计算在运行时间内。这时候光调题目的时间限制是不够的还要看服务器负载和语言启动开销。4.2 关键参数怎么看、怎么调青岛OJ有几个参数部署时一定要摸清楚含义。编译时间限制和运行时间限制要用不同参数控制。如果编译时间给太短机器稍微卡顿就会大面积CE如果运行时间卡太严题目时限又不合理提交体验就会很差。内存限制要分语言区别对待。C/C程序内存管理贴近操作系统静态分配和栈空间直观Java因为有自己的堆内存和JVM开销往往需要给更大的空间Python内存消耗本身也不小。稳妥的做法是先跑一个基准测试摸清不同语言在同一道题上的内存底数再设置题目限制。CPU核数也会影响判题并发数。青岛OJ判题机使用多进程模型还是线程模型具体看版本实现。并发数设太高单个提交的时间会明显变长设太低高峰期的评测队列会积压。我的经验是逐步升压测试先放10个并发提交观察平均响应时间再慢慢往上加找到拐点作为实际并发数。4.3 测试数据的目录组织与批量导入题目测试数据管理是OJ日常维护中最频繁的操作。青岛OJ的测试数据目录按题号组织每个题目目录下放置输入和输出文件。命名规范建议沿用项目默认例如统一使用数字编号对应测试点顺序。做题目数据时边界数据比随机数据更重要。一道题如果只测常规用例很多隐藏问题不容易暴露典型的秒杀型错误全是边界条件没处理。我会在导入前专门写一个造数脚本批量生成极端值、数据上下限和空输入场景保证覆盖度再导入。批量导入时需要注意权限。如果Web端用户和判题机用户不同导入的测试数据文件要确保判题机用户有读权限。很多莫名其妙的WA或者RE最后查出来是测试数据文件权限没放开。5. 上线后的日常运营与安全加固5.1 题目的添加、编辑与格式规范题目上线不是把题目文字和测试数据放上去就完事。健壮的OJ题目至少要有清晰的问题描述、严格的输入输出格式说明、明确的样例数据和覆盖全面的测试点。题目描述里最容易出问题的是格式歧义。比如“输出答案对1000000007取模”如果样例没有体现模数用法很多提交者会直接输出原数判题结果就是WA但用户会觉得题目有问题。所以我建题时坚持一个原则样例必须展示边界用法描述必须写明数据范围。后台添加题目时先去填元信息、写描述、设置时间内存限制再上传测试数据并生成缓存或索引。最后一定要用非管理员的普通账号自测提交一遍避免出现管理员权限掩盖下的权限问题。5.2 用户权限、比赛管理与防作弊设置青岛OJ的角色权限分得比较清晰。普通用户只能提交和查看自己的记录出题人可以管理自己的题目管理员拥有全部权限。个人使用的话多账号注册要管控好否则公网部署容易被刷垃圾账号。建议开启注册审核或者邮箱验证。比赛管理要特别关注封榜和榜单设置的细节。ACM赛制里的封榜是指比赛最后一段时间内实时榜单不再更新给选手制造悬念赛后统一放出最终榜单。青岛OJ支持这种模式但在创建比赛前要确认当前站的时区是否准确否则“比赛开始一小时了但站内显示还没到点”的尴尬场景就会出现。在线评测系统的防作弊本来就难。青岛OJ能做的是记录提交IP、比对代码相似度但如果你要组织严肃比赛最好还是限制参赛白名单和禁用外部账号。对个人刷题场景这些限制不需要太严格否则会误伤正常使用。5.3 备份、日志与基础安全MySQL备份不能只做一次就扔那儿数据库里的用户提交记录、题目数据和比赛配置都是长期成果。我习惯每天凌晨用计划任务自动导出SQL文件再同步到对象存储或者另一台机器。恢复操作我也实际演练过因为备份文件如果从没恢复过到你真要用那天就晚了。日志是排查问题的第一手资料。Web服务的访问日志能告诉你谁在什么时候提交了什么PHP日志能暴露代码层面的错误判题机日志则是判题异常的现场。配置好日志轮转避免日志把磁盘灌满。安全方面公网部署的话尽量做到几点关闭不需要的端口、修改默认SSH端口、禁用root密码登录、MySQL账号不要用通用弱密码。另外要留意常见Web漏洞比如SQL注入和文件上传漏洞。青岛OJ这套老系统本身有一定防护能力但布到公网就不要完全依赖它反代一层Nginx加一些基础规则会更稳。6. 常见故障与排查实录6.1 部署期的高频报错和解决办法我把部署期遇到的高频问题整理成一张速查表都是实际踩过的坑。现象典型原因解决办法浏览器访问不了页面服务器安全组未放行80端口Nginx/Apache未启动检查云厂商安全组规则确认Web进程监听状态页面报数据库连接失败PHP配置的库地址、库名、账号密码和MySQL实际不匹配逐项核对配置重点检查MySQL认证插件判题机无响应提交一直Pending判题守护进程没启动或崩溃判题机用户权限不足查看判题机日志确认专用账号可读测试数据目录一道题所有提交都TLE编译运行环境没配好或者题目的时间限制设置错误用本地命令行方式手动验证测试用例耗时再对题目的限制重新估算Java提交一直MLE堆内存限制未设置到位在judge配置中给Java单独设置足够的-Xmx参数部署期的排查思路要记住一条任何异常都先看日志。数据库问题看库日志Web问题看Web日志判题问题看判题日志。盲改配置十次有九次不如看一眼日志来得准。6.2 上线后最容易遭遇的运行时问题OJ稳定跑起来后真正让人头疼的问题和部署期不一样。判题队列积压是最常见的。某次数据量大的比赛开始了所有人的提交同时涌进来判题机忙不过来页面上出现大量Pending。这个问题的根治方案是提升并发数和服务器性能但临时应对可以先放宽一道题目的评测并发或者暂时关掉部分语言的支持先把高峰扛过去。磁盘空间被打满也容易出现。题库测试数据、系统日志、用户提交的源码、MySQL数据库几样东西加起来很快能吃满磁盘。我会为日志和数据单独挂载分区并加磁盘告警脚本。否则一旦磁盘满了MySQL可能直接拒绝写入整个站就瘫了。6.3 我总结的几条避坑心得部署青岛OJ的过程让我对这套系统有了不少自己的理解。第一服务器上装的东西越少越好。很多人习惯用宝塔面板类工具一键装环境确实快但面板自身会占用端口和资源有时生成的Nginx配置和项目默认配置冲突排查起来反而费劲。我后来都是纯净环境手动装依赖问题反而少。第二权限治理要从第一天认真做。判题机必须以非root用户运行测试数据目录要设置好属主和权限Web目录不能让用户随便写文件。这套系统的安全模型大量依赖操作系统用户权限你偷的懒迟早会变成事故。第三改配置文件前先备份。尤其判题机的参数配置一个数值影响所有题的评测结果。我吃过一次亏把内存限制单位看错导致整批题目MLE折腾了一晚上才想起来是配错了参数。这之后我养成了一个习惯任何文件改动前先复制一份带日期的备份。7. 写在最后的体会我实际用青岛OJ这套系统跑了几个月之后最大的感受是自建OJ不是一件“装个软件就完事”的事它更像养一台需要持续照看的服务器。判题机、数据库、Web服务、题库数据每一环都可能出问题但也正是这种复杂让你在折腾中真正理解了一个在线评测系统是怎么运转的。如果你正准备动手搭我给的建议是先把官方文档通读一遍再按我上面的流程部署一次碰到问题别慌先看日志。这套系统的生态虽然不如商业产品完善但对于个人学习、团队训练和小型比赛来说已经绰绰有余。后续如果你有兴趣还可以从下载源码开始尝试改一改判题机的判题逻辑或者写一个批量管理测试数据的小工具那会是另一段有趣的折腾。