从零开始的代码评测系统设计与实践(五) —— 新的开始

Posted by LanceLRQ on Sunday, November 15, 2020

0x00 前提

自从2015年WeJudge(亦简称为OJ)第一个版本面世以来,经历过两次比较大规模的重写。为什么是重写不是重构?因为代码写的很烂。目前的3.x版本是最好的一个版本了,但无论是从技术层面来看,还是从产品层面来看,它都还存在很多很多的问题。基于这些问题,加上我对个人成长的期望,我希望能从现有的基础出发,开发一套代码判题服务实现的解决方案。这个项目会以BSD协议开源,同时,判题机程序deer-executor会以GPL协议开源。开源是希望能够和大家分享交流,希望知识和成果要属于大家。

0x01 反思

先从技术层面上谈谈现在的WeJudge。技术选型方面,3.x版本在服务端采用Python3.6 + Django 2.x 的方案;前端则采用React 16.x + Ant design 4.x的方案。同时,我还编写了相关的CI/CD脚本,实现开发环境和生产环境的一键部署,这个要感谢老东家ones.ai提供的实习机会让我能接触devops相关的知识。下边以我自己的角度来评价一下存在的问题。

产品:

  1. **题库:**OJ首个版本的设计思路一开始是参考杭电OJ的设计思路,题目列表、评测队列放全局。后续为适应实际需要,2.x、3.x开始对题目进行分类,通过题库来管理题目。现在主要的问题是题库太多了也不好管理,权限设计上存在问题。目前正在解决
  2. 课程:从设计之初就考虑自研一套方案去解决课程排课问题。3.x定下的方案是学生选课-老师排课-发布作业-按排课设定访问权限。权限方面暂时正常,但学生选课依然是一个大问题。无论如何,北师的老师希望直接从教务系统导入,但3.x已经摆脱了当时学号-账号的模式,改为通用的账号模式。如何更高效的实现学生-账号的绑定呢?并不想依赖教务系统,这种第三方的系统存在风险,你无法预支他什么时候会突然挂掉,或者把你的爬虫封掉。
  3. 评测:现有评测功能是自研的方案,但又多少有杭电的影子,我习惯性称为while(scanf() != EOF),即一个文件内多组数据,需要循环输入来获取。这个方法好处是减少IO,一次性把n组数据丢到程序里跑完即可。然而毛病实在是太多了。WeJudge设计的初衷是服务教学的,面对的群体是广大学生而不是广大ICPCer,你不能用大神的眼光去看待初学者,认为他们啥都能自己解决。他们不能解决的问题,自然需要老师的帮助。但是面对特别大的数据量,这种方式几乎无法定位问题。这方面我又非常赞同以codeforces为代表的那种一数据一文件的方式。服务器资源是拿来用的,哪怕评测一次要不停地打开成百上千的文件,IO效率再低,只要能让用户的使用体验更舒适,那就是值得的。

服务端:

  1. Python并不是一个很适合用来编写复杂系统的语言。自由的数据类型,通过缩进来表示代码块等,给开发者带来一些额外的顾虑。
  2. 没有好的Log习惯,虽然我补充了Sentry的异常跟踪,在500的时候能记录异常点,但是要通过log来排查一些偶发的异常还是比较麻烦
  3. 性能问题,Python确实跑啥都慢
  4. 依赖ORM,导致性能不佳,好在有实现缓存,主要功能的接口不置于慢的难受
  5. 没有单元测试,接口行为基本靠人工调试

前端:

  1. 部分功能的交互行为确实不够清晰,缺乏用户文档,在用户体验上有所不足
  2. 同样是没有单元测试,编程习惯规范的也不是很好,代码存在坏味道
  3. 整个应用功能复杂,加载速度较慢。
  4. 没有类似dva的状态管理方案,状态逻辑部分(Redux+Saga)其实维护起来很复杂。这个我一直在想自己整一个方案来处理,但还没有找到很好的方案。轮子好用,但是为时已晚,不可能把现有代码推翻重来。

**其他:**从3.x开始,我有幸找到两位大佬学弟帮忙开发这个系统。事实上,因为各种原因,从提出方案到开发完成耗费了1年的时间,期间我负责架构和关键部分额开发,他们负责业务逻辑的开发。整个过程相对还是顺利的,但由于比较仓促,没有留下太多的文档,导致后要找人维护起来较为困难。他们快毕业了,我也担心后继无人。

还有很多可能一时想不到,先暂时写下这么多。

0x02 WeJudge Polygon – 打造一个通用的开源代码评测服务

在设计之初,WeJudge就被定义为是一个面向教学的OJ系统,以实现基本的评测功能为前提,开发出题库、课程、比赛这三个核心模块。服务端采用“接口服务”-“异步队列”-“判题机”的工作模型,异步完成评测流程。

这是一个较为简单的模式,能够快速的完成这套系统的开发。但并不是一个面向未来的设计,说到底,谁都离不开谁,谁离开谁都不能独立工作。系统只会变得越来越臃肿,变得更加难以维护。

在微服务的热度不断提高的今天,我决定将WeJudge自带的评测功能重新开发,令其能够独立地运行评测任务。无论未来系统如何变化,只要需要评测功能,都可以使用这个服务来支持。

这就是为什么我要重写deer-executor和开发一个全新的评测服务:WeJudge Polygon

0x03 Deer-executor的前世今生

2015年5月,我在OJ项目立项之初就在github找到了一位大佬开发的名为Lo-runner的评测核心,这是一个CPython模块,需要在linux下进行编译后使用。1.x和2.x的OJ都是使用这个模块作为核心的,我在这个项目里加入了特殊评测的代码,以及做了一些优化调整,缝缝补补又一年。

后来觉得需要研发自己的判题机,在接触了Go语言后,参考Lo-runner里的判题工作思路,经过1个多月时间的准备,于2018年12月31日发布了deer-executor的第一个版本。此时deer-executor只是一个功能包,它并不能独立工作。3.x版其实有个内部为开源的判题机叫deer-judger,真正的判题调度、队列管理、结果持久化和通知等,是由它完成的。简单的理解就是,v1版的executor只能评测一题里的一个数据,真正能够完整运行一个题目所有测试数据并计算结果的,只有deer-judger

而v2版本,我将deer-judger里关于评测的基本逻辑搬运过来,并增加了CLI功能,让它能够独立编译成一个程序。在Linux/Mac环境下,你只要通过

./deer-executor run <config>

即可在本地完整运行一次评测流程。同时我还增加了一套本地的题目和评测结果持久化方案。通过自研的二进制打包格式,支持用户将出好的题目打包成一个独立的题目文件,也可以将运行结果打包成一个文件分享给别人。

题目打包可以实现在任意地方轻松地运行一次评测,未来也方便出题者直接将题目更新到OJ上,以及从OJ上备份题目;

结果分享则方便老师下载以后,回放评测过程,定位问题。毕竟大量数据如果通过浏览器去比对,性能和效率都太差,你不好我也不好。比对工具暂未实现,在做了在做了(新建github项目

v2版本提供了对cf的出题库testlib.h支持,方便出题人出相关特判的题目,当然出题门槛也稍高,但至少是支持的。

安全性方面暂时还是又docker去实现隔离,能力有限,未来有空再研究沙盒了;由于windows下不能支持评测(同样能力有限),我屏蔽了windows下的评测功能,但testlib.h的出题、题目打包解包那些还是支持的。为未来出题人可以在自己电脑上直接出题做好准备。(当然需要安装编译器的,我可没把gcc打包进去)

总之,deer-executor要成为一个基于命令行的,独立的判题机程序。目前它已经发布了,但我不敢说它是正式发布或者稳定版,因为它只能跑过单元测试,并没有经过时间和用户的检验。先成长吧,勿骄勿躁。

为啥叫deer-executor?有故事,但我没有酒,所以你猜呗。

0x04 所以呢?

从这篇开始,我将以日记的方式,记录WeJudge Polygon的开发历程,真正如标题所说,从零开始去开发一个评测系统,分享我的心得。

咸鱼了,有缘再见。

如果你有想法,欢迎和我讨论。

判题机: https://github.com/LanceLRQ/deer-executor

WeJudge Polygon公共项目:https://github.com/wejudge/wejudge-polygon (目前在我个人Fork出的项目下开发,等准备好了会merge过去)