韦俊宇 王宇英
基于Docker的Online Judge容器设计与实现
韦俊宇 王宇英
(桂林电子科技大学计算机与信息安全学院,广西 桂林 541004)
在LeetCode、ACWing等各大Online Judge网站中,每时每刻都有无数的代码在编译运行。但这些网站上提交的代码都是为了解决该网站题库中给定的某一道算法题,不仅规定了程序的标准输入输出,还限制了运行时长和内存占用,有一定局限性。在新冠疫情期间,开设计算机课程的大学也急需搭建符合学校教学需求的Online Judge以正常开展线上课程。针对上述两个问题,文章设计并实现了基于Docker技术的Online Judge容器。其能编译程序源代码和运行程序二进制文件,期间能动态地更改程序的标准输入、运行时长与内存占用,并对容器作了一定的防护,具有一定的安全性,还易于分布式拓展。
在线判题容器;Online Judge;Docker
线上教育是将教育与多媒体充分地结合起来,一同打造的新式教学法,由于其便利、快捷等特点,目前在各大高校兴起。大多数高校计算机专业的教学不同于高中传统理论课程,其实践课程多数为上机操作,但因为师生不是面对面交流,使教师难以了解学生对知识的掌握情况以及时更改自己的教学计划,影响教学质量。网络上针对教学实施的Online Judge[1]少之又少,拥有教学功能的Online Judge提供的功能难以根据计算机教学需求设计问题,当下急需易搭建、易拓展具有一定安全性的Online Judge容器以供给学校根据教学需求[2]自主开发Online Judge。
Docker采用镜像包的形式,将不同的代码和运行环境固定成了一个整体,能以镜像包为基础启动容器,标准化运行程序,稳定且可靠。Linux 内核提供了getrusage和setrlimit系统调用,能在运行时访问系统内核内部数据结构、改变系统内核设置的机制,以达到程序运行时限制资源的效果。本文基于Docker和Linux系统调用,以C++作为目标语言举例,设计并实现了Online Judge容器,其消除了不同运行环境的差异,且极易搭建部署在任意一台拥有Docker运行环境Linux服务器上。该Online Judge容器模块功能颗粒度小,方便二次开发进行拓展,供给开设计算机专业的高校或创业人员,针对新冠疫情带来的问题,搭建自主的Online Judge。
Online Judge中文名是在线判题,用户在网页上编写程序源代码,提交到服务器上运行并给出一定的结果,这是Online Judge一个基本的流程。
使源代码在服务器上进行编译运行操作的,被称之为判题机。判题机不仅需要对用户的源代码进行编译运行,还需要对编译运行期间可能出现的安全问题进行处理,以防止部分用户故意提交具有安全漏洞的代码影响服务器运作。大多数服务器与判题机是1∶n的比例,必须将判题机作为分布式集群,否则难以应对判题需求激增的情况以致服务器无法及时正常响应请求。
目前主流Online Judge网站的判题机能对可能在编译或运行时出现的错误归类成以下四类[1]:
(1)编译失败:编译时出错。
(2)运行时错误:程序异常终止,可能的原因是:段错误,被零除或用非0的代码退出程序。
(3)运行超时:程序使用的CPU时间已超出限制。
(4)内存超限:程序实际使用的内存已超出限制。
本文实现的Online Judge容器可作为判题机独立处理单个编译运行任务。它对上述可能出现的四类错误进行处理,但不涉及对输入输出的判定。Online Judge容器只对程序的标准输入输出作重定向,目的是实现更小颗粒化的容器,方便拓展与二次开发。
为了解决软件开发中环境配置繁琐的问题,本文使用Docker技术,将Online Judge核心的编译模块和运行模块分别独立设计成两个Docker容器。Docker将会提供一个程序运行的独立环境,使用namespace、cgroup等安全隔离机制措施,让Docker容器内与宿主机完全隔离[3]。若将程序必需的环境和文件整合在轻量级的Docker容器中,就可以快速地部署在任意一台拥有Docker运行环境Linux主机上。
不同于传统的在线判题系统设计复杂度高、实现困难,本文设计的在线判题容器因采用了Docker技术,拥有轻量、易用、安全的特点。
在世界上最大Docker容器镜像分享库Docker Hub中,提供了主流程序设计语言的完整编译环境的镜像。例如GCC(GNU Compiler Collection,GNU编译集)的Docker镜像可以在Docker Hub中找到,它可以用来编译C、C++等程序设计语言。在拥有Docker运行环境Linux主机上,使用docker pull gcc命令即可将该镜像拉取到本地。
编译容器的作用是对每一个程序的源文件进行编译并产生对应的可执行文件。虽然Docker启动一个容器的开销极小,但是先将需要编译的源文件与编译环境打包成一个新镜像,再以该镜像启动容器编译出可执行文件,对拥有源源不断提交编译运行请求的服务器而言,这样将会反复地创建新镜像,造成极大的系统资源的浪费。
为了避免这种问题,使用sleep infinity命令启动编译容器后,便会使该容器进程永久进入休眠状态。同时将宿主机某个文件目录挂载到编译容器内。在有新的编译请求到来后,使用docker exec <容器id> <编译命令>,依附于休眠状态的编译容器,在编译容器内部以新进程的形式执行新的编译任务。该编译进程产生的程序可执行文件需输出到宿主机挂载到编译容器内的目录下,宿主机上的Online Judge服务访问挂载的文件目录,即可获取该程序可执行文件。以这种方式编译源程序,不需要反复地打包镜像、创建容器,就可以简单、快速地编译源文件。
以编译C++程序为例,编译容器整体结构如图1所示。
图1 编译容器整体结构
除了某些如JavaScript、TypeScript、Python等动态脚本语言需要解释器外,多数静态语言如C、C++、Go在编译产生程序可执行文件后,即可在编译时指定的目标系统下运行。以已经获得了C++可执行文件main. exe,编译时指定的目标系统为Linux为前提,开始整个运行容器的设计。
运行容器将会不断地创建并周期性地删除,以保证服务器在不断地运行请求压力下,将内存和磁盘占用维持一个良好的水平。如果使用GCC的Docker镜像,虽然它也能执行C++可执行文件,但它拥有近1 GB的磁盘占用,开销极大。所以一个精简的Docker镜像非常有必要,对于运行容器,最后选择使用了非常轻量的ubuntu的Docker镜像。
往往一个程序的运行时间都以毫秒为单位。Docker虽然提供了docker status命令监控每个容器的资源占用,但是一旦容器停止了便无法再获取。而且docker status命令是Docker使用客户端-服务器(C/S)架构模式来通信的[3],中间具有一定的时延。在运行的过程中往往运行容器已经停止了,而docker status命令还没获取到容器信息。此外,docker status命令获取的是一整个容器内的资源占用,不是某一个进程的资源占用。显然它不能满足精确监控某个进程占用系统资源的需求。
Linux系统的getrusage系统调用,可以得到相关进程的资源使用信息,如进程占用内存、进程在用户态下的执行时间、进程在系统态下执行的时间。类似的,setrlimit系统调用可以对某个进程进行资源限制。使用getrusage和setrlimit这两个系统调用,运行容器可以精准地监控和限制进程的资源。
为此,可以编写一个资源监控程序,它将在运行容器内部,以子进程的形式运行目标程序可执行文件,并重定向输入输出流,期间通过getrusage和setrlimit系统调用不断地对其进行监控和限制。若在过程中子进程使用资源超出了给定的限制,例如内存超限、运行超时,则直接停止子进程的运行,并将结果记录下来。若子进程平稳地运行结果,则在子进程结束时返回相关资源使用信息。该监控程序也将被整合在Docker镜像内。
运行容器整体结构如图2所示。
图2 运行容器整体结构
就像两台通信设备需要基站作为中转一样,为了聚合编译容器与运行容器,必须拥有一个Server容器。该Server容器提供友好交互的前端界面与对外访问的Web服务,将用户的源代码和输入流递交到Docker容器中,并将输出流发还给用户。从在用户的角度看,Server容器就是一个普通的服务器应用,如图3所示。
图3 用户视角的服务器
编译容器和运行容器将宿主机上的某一目录共同作为工作区,分别挂载到各自的容器内部,就达到了编译容器和运行容器同时访问宿主机工作区内文件的效果。服务器向该工作区目录写入程序源代码,编译容器根据程序源代码生成程序可执行文件,运行容器读取程序可执行文件并给出标准输入后输出的结果。上述容器独立的任务和之间相互通信都在该目录下进行。
在用户提交程序源代码后,通过docker exec <容器id> <编译命令> <资源限制参数> 启动编译容器执行编译命令,届时可以向编译容器内的监控程序传递资源限制的参数。编译结束后会在工作区目录下产生对应程序的可执行文件。通过docker run <容器id> <运行参数> <资源限制参数> 启动运行容器执行运行命令。
聚合架构如图4所示。
图4 聚合架构
以C++为例,代码如下。在程序源代码的中引入一个永远也读不完的文件,会使编译容器永久停留在编译阶段。
Docker编译容器在运行时可以传递资源限制参数,限制编译时长,就可以避免这种情况。
对于服务器,增加一点磁盘空间或内存空间开销代价都是昂贵的,所以在线测评网站上提交的代码允许磁盘空间占用不会太大。但也不怀有人恶意编译巨大文件[4],导致服务器的磁盘占用迅速上升造成宕机的严重后果,必须要在编译容器执行编译任务时,通过资源限制参数限制编译文件的大小。
System("shutdown now")会使系统直接关机,fork()可以让程序递归地创建自身进程副本,还有很多此类直接影响整个服务器的操作,可以在编译时简单地使用字符串匹配去检测这些危险的命令,检测到这些命令后直接停止整个编译容器的运行并返回错误结果[4]。但是如果有人恶意宏定义,就绕过了字符串匹配。所以这种方式是不可行的。
但本文设计的编译容器和运行容器都是运行在Docker里的,Docker容器内部和宿主机已经完全隔离开,通过Docker限制整个容器的资源,即可解决这个问题。
可能会有那么一种情况,在源代码里写sleep(1000)(该代码在C++中会使程序休眠1000秒),每次程序必定超时,后被强制退出。这非常像类似于DDoS的攻击方式。解决方案也和DDoS类似,要么提高服务器的配置,要么限制这个这类恶意提交运行超时代码用户的正常使用[5]。前者费用昂贵,后者经济实惠。可以在Server容器内规定,如果某个用户提交超时的代码连续超过N次,则在后续一定时间内,该用户的代码不给予编译运行。
Online Judge网站有一特点,通常情况下比如夜晚或节假日,基本上不会有大量的编译运行请求。但在某些特殊的时间点上,比如承办一个民间或官方在线比赛,或组织一场校内或校外的编程考试时,服务器编译运行请求会迅速增加。如果此时服务器在高负载下宕机了,将会给网站带来难以估量的损失。传统单体B/S架构的服务器难以动态地增加配置,且当用户请求量到达一定程度时,客户端和服务器之间的网络资源分配也成了一大问题,再怎么提高服务器的性能都无济于事。所以不得不寻求其他的方案,解决单台服务器性能上限和网络资源分配的问题。
为了解决上述问题,将Online Judge容器部署在多台服务器上,不仅可以应对用户量激增带来的巨额并发访问量,而且在容灾方面,当其中一台判题服务器遭遇意外问题宕机时,整个系统也能正常地运行下去。
在2004年,Google公司提出了一种用于大规模数据的编程模型MapReduce[6]。参考其编程模型的核心思想“分而治之”,可以将每个用户的判题任务分发到不同的服务器上,再统一收集结果返回给用户。这一过程,将在一台服务器上的并行任务转换成在多台服务器上的并行任务。转化前后并行执行效果如图5、图6所示。
可以非常直观地看到,通过多台服务器同时开展并行任务,单台服务器需要承载的压力会被均分到其他服务器上。同时可根据用户在线访问量的波动,动态地增加或减少服务器集群内的主机的数量,减少高昂的服务器资源费用的支出。
图5 原始并行图
图6 分布并行图
参考MapReduce,它会先将用户的原始数据进行切割,然后分发给不同的Map任务处理[6]。用户提交到服务器的数据只有源代码和输入文本,对这些数据进行切割并没有什么意义。Online Judge需要切割的是存储在服务器上的隐藏测试样例,这些输入输出样例的数量,根据题目的不同,有的几十,有的甚至上千。这些不同的输入输出样例的空间占用大小,有的只有几kB,有的可达1 MB。
在分布式集群中,每一台机器的性能可能都是不一样的。对此,可以选择一种较为简单、静态的负载均衡算法——加权轮询调度算法[7]。它用不同的权值记录了每一台服务器的处理能力,在请求到来后,权值高的服务器将会处理更多的请求。分发任务后,权值会根据资源占用和请求处理的情况进行改变。
服务器选择任务分发时,这些占用空间大,运行时间较长的输入输出样例,将会被分发到性能较高的服务器上。而那些占用空间小,运行时间短的样例将会被分发到性能较差的服务器上。因为每个服务器的权值都会被加权轮询调度算法动态的更改,所以一批开始于相同时间的任务,在经过集群分发处理后,等待结束同步的时间不会相差太大。如图7所示。
图7 任务分发
任务归并的过程只需要将之前分发的任务取回,取回过程的顺序不需要根据任务分发的顺序或是分发时间顺序来定。在这一过程中,专门用于任务分发的线程会通过远程过程调用[8]调取集群内Online Judge容器暴露出来的功能函数。该线程在等待Online Judge容器编译运行的过程中,会进入等待状态,直至所有任务的运行结果都获取。
远程过程调用[8]是一个计算机通信协议,可基于HTTP或TCP协议实现,越接近底层的协议数据传输更快。使用远程过程调用,会在服务器集群内部会产生一定内部网络资源消耗。因为集群外的网络资源比集群内的网络资源要更加昂贵,所以在整个集群内部进行的远程过程调用,对整个系统来说是可以接受的。
对Online Judge容器尝试分布式拓展后,将高并发下单台服务器性能上限问题转换成了多台服务器组成集群的性能上线问题;将高并发下客户端与服务器之间网络资源分配问题,转换成了客户端与多台服务器之间的网络资源分配问题。这些问题在分布式领域仍面对着许多的问题与挑战,等待着更好的解决方案的提出。
目前,线上编程教学的需求变得愈发强烈,互联网企业在选人面试招聘也逐渐从线下转变为线上,因此急需免配置系统环境就可运行的编程环境。本文设计的基于Docker的Online Judge容器,功能精巧、易部署、具有一定的安全性,能较好的应对此类急需制作针对特定功能而使用的Online Judge网站。同时基于此类在线测评的特点,基于MapReduce和远程过程调用协议提供了一种分布式拓展的方案,以应对高并发高负载下的场景。Docker属于较新的技术之一,具有天然适应云计算的优点。未来随着云计算、分布式技术的发展,读者还可以根据该基于Docker设计的Online Judge容器,进一步丰富其定制化功能,实现远程线上实验教学、互联网企业远程编程开发等,以响应工业和信息化部关于推动5G加快发展的号召。
[1] 蔡崇超. 基于Web的在线判题系统设计与实现[J]. 软件导刊,2016,15(3): 107-109.
[2] 欧阳佳,肖茵茵,刘少鹏,等. 基于在线判题系统的程序设计课程群教学研究[J]. 信息与电脑(理论版),2021,33(12): 228-231.
[3] 邱建新. 基于Docker容器技术的Linux在线实验环境设计[J]. 信息技术,2022(2): 48-52,58.
[4] 李定才,瞿绍军,胡争,等. 基于Windows的在线判题系统的安全性研究[J]. 计算机技术与发展,2011,21(9): 204-207.
[5] 张彩珍,常元,康斌龙,等. 一种抵御流量型DDoS攻击的告警阈值系统设计[J]. 电子设计工程,2021,29(22): 24-27,32.
[6] Dean J, Ghemawat S. MapReduce: Simplified data processing on large clusters[C]. Proc of OSDI 2004, USENIX Association, 2004.
[7] Reynolds D A, Quatieri T F, Dunn R B. Speaker verification using adapted Gaussian mixture models[J]. Digital Signal Processing, 2000, 10(1): 19-41.
[8] Gutiérrez G J J, González H M. Prioritizing remote procedure calls in Ada distributed systems[J]. ACM SIGAda Ada Letters, 1999, 19(2): 67-72.
Design and Implementation of Online Judge Container Based on Docker
In major Online Judge websites such as LeetCode and ACWing, countless codes are compiled and run all the time. However, the code submitted on these websites is to solve an algorithm problem given in the website's question bank. It not only stipulates the standard input and output of the program, but also limits the running time and memory usage, which has certain limitations. During the COVID-19, universities offering computer courses also urgently need to build Online Judge that meets our teaching needs in order to run online courses normally. Aiming at the above two problems, this paper designs and implements an Online Judge container based on Docker technology. It can compile the source code of the program and run the binary file of the program, dynamically change the standard input, running time and memory occupation of the program, and protect the container to a certain extent. It has certain security, and is easy to expand distributed.
Online Judge container; Online Judge; Docker
TP311
A
1008-1151(2022)09-0014-04
2022-06-28
广西区大学生创新创业训练计划立项项目(202010595168、202110595158)。
韦俊宇(2001-),男,桂林电子科技大学计算机与信息安全学院学生。
王宇英,女,桂林电子科技大学计算机与信息安全学院正高级实验师,硕士,研究方向为高等教育管理。