韋俊宇 王宇英
基于Docker的Online Judge容器設計與實現
韋俊宇 王宇英
(桂林電子科技大學計算機與信息安全學院,廣西 桂林 541004)
在LeetCode、ACWing等各大Online Judge網站中,每時每刻都有無數的代碼在編譯運行。但這些網站上提交的代碼都是為了解決該網站題庫中給定的某一道算法題,不僅規(guī)定了程序的標準輸入輸出,還限制了運行時長和內存占用,有一定局限性。在新冠疫情期間,開設計算機課程的大學也急需搭建符合學校教學需求的Online Judge以正常開展線上課程。針對上述兩個問題,文章設計并實現了基于Docker技術的Online Judge容器。其能編譯程序源代碼和運行程序二進制文件,期間能動態(tài)地更改程序的標準輸入、運行時長與內存占用,并對容器作了一定的防護,具有一定的安全性,還易于分布式拓展。
在線判題容器;Online Judge;Docker
線上教育是將教育與多媒體充分地結合起來,一同打造的新式教學法,由于其便利、快捷等特點,目前在各大高校興起。大多數高校計算機專業(yè)的教學不同于高中傳統理論課程,其實踐課程多數為上機操作,但因為師生不是面對面交流,使教師難以了解學生對知識的掌握情況以及時更改自己的教學計劃,影響教學質量。網絡上針對教學實施的Online Judge[1]少之又少,擁有教學功能的Online Judge提供的功能難以根據計算機教學需求設計問題,當下急需易搭建、易拓展具有一定安全性的Online Judge容器以供給學校根據教學需求[2]自主開發(fā)Online Judge。
Docker采用鏡像包的形式,將不同的代碼和運行環(huán)境固定成了一個整體,能以鏡像包為基礎啟動容器,標準化運行程序,穩(wěn)定且可靠。Linux 內核提供了getrusage和setrlimit系統調用,能在運行時訪問系統內核內部數據結構、改變系統內核設置的機制,以達到程序運行時限制資源的效果。本文基于Docker和Linux系統調用,以C++作為目標語言舉例,設計并實現了Online Judge容器,其消除了不同運行環(huán)境的差異,且極易搭建部署在任意一臺擁有Docker運行環(huán)境Linux服務器上。該Online Judge容器模塊功能顆粒度小,方便二次開發(fā)進行拓展,供給開設計算機專業(yè)的高?;騽?chuàng)業(yè)人員,針對新冠疫情帶來的問題,搭建自主的Online Judge。
Online Judge中文名是在線判題,用戶在網頁上編寫程序源代碼,提交到服務器上運行并給出一定的結果,這是Online Judge一個基本的流程。
使源代碼在服務器上進行編譯運行操作的,被稱之為判題機。判題機不僅需要對用戶的源代碼進行編譯運行,還需要對編譯運行期間可能出現的安全問題進行處理,以防止部分用戶故意提交具有安全漏洞的代碼影響服務器運作。大多數服務器與判題機是1∶n的比例,必須將判題機作為分布式集群,否則難以應對判題需求激增的情況以致服務器無法及時正常響應請求。
目前主流Online Judge網站的判題機能對可能在編譯或運行時出現的錯誤歸類成以下四類[1]:
(1)編譯失?。壕幾g時出錯。
(2)運行時錯誤:程序異常終止,可能的原因是:段錯誤,被零除或用非0的代碼退出程序。
(3)運行超時:程序使用的CPU時間已超出限制。
(4)內存超限:程序實際使用的內存已超出限制。
本文實現的Online Judge容器可作為判題機獨立處理單個編譯運行任務。它對上述可能出現的四類錯誤進行處理,但不涉及對輸入輸出的判定。Online Judge容器只對程序的標準輸入輸出作重定向,目的是實現更小顆?;娜萜?,方便拓展與二次開發(fā)。
為了解決軟件開發(fā)中環(huán)境配置繁瑣的問題,本文使用Docker技術,將Online Judge核心的編譯模塊和運行模塊分別獨立設計成兩個Docker容器。Docker將會提供一個程序運行的獨立環(huán)境,使用namespace、cgroup等安全隔離機制措施,讓Docker容器內與宿主機完全隔離[3]。若將程序必需的環(huán)境和文件整合在輕量級的Docker容器中,就可以快速地部署在任意一臺擁有Docker運行環(huán)境Linux主機上。
不同于傳統的在線判題系統設計復雜度高、實現困難,本文設計的在線判題容器因采用了Docker技術,擁有輕量、易用、安全的特點。
在世界上最大Docker容器鏡像分享庫Docker Hub中,提供了主流程序設計語言的完整編譯環(huán)境的鏡像。例如GCC(GNU Compiler Collection,GNU編譯集)的Docker鏡像可以在Docker Hub中找到,它可以用來編譯C、C++等程序設計語言。在擁有Docker運行環(huán)境Linux主機上,使用docker pull gcc命令即可將該鏡像拉取到本地。
編譯容器的作用是對每一個程序的源文件進行編譯并產生對應的可執(zhí)行文件。雖然Docker啟動一個容器的開銷極小,但是先將需要編譯的源文件與編譯環(huán)境打包成一個新鏡像,再以該鏡像啟動容器編譯出可執(zhí)行文件,對擁有源源不斷提交編譯運行請求的服務器而言,這樣將會反復地創(chuàng)建新鏡像,造成極大的系統資源的浪費。
為了避免這種問題,使用sleep infinity命令啟動編譯容器后,便會使該容器進程永久進入休眠狀態(tài)。同時將宿主機某個文件目錄掛載到編譯容器內。在有新的編譯請求到來后,使用docker exec <容器id> <編譯命令>,依附于休眠狀態(tài)的編譯容器,在編譯容器內部以新進程的形式執(zhí)行新的編譯任務。該編譯進程產生的程序可執(zhí)行文件需輸出到宿主機掛載到編譯容器內的目錄下,宿主機上的Online Judge服務訪問掛載的文件目錄,即可獲取該程序可執(zhí)行文件。以這種方式編譯源程序,不需要反復地打包鏡像、創(chuàng)建容器,就可以簡單、快速地編譯源文件。
以編譯C++程序為例,編譯容器整體結構如圖1所示。
圖1 編譯容器整體結構
除了某些如JavaScript、TypeScript、Python等動態(tài)腳本語言需要解釋器外,多數靜態(tài)語言如C、C++、Go在編譯產生程序可執(zhí)行文件后,即可在編譯時指定的目標系統下運行。以已經獲得了C++可執(zhí)行文件main. exe,編譯時指定的目標系統為Linux為前提,開始整個運行容器的設計。
運行容器將會不斷地創(chuàng)建并周期性地刪除,以保證服務器在不斷地運行請求壓力下,將內存和磁盤占用維持一個良好的水平。如果使用GCC的Docker鏡像,雖然它也能執(zhí)行C++可執(zhí)行文件,但它擁有近1 GB的磁盤占用,開銷極大。所以一個精簡的Docker鏡像非常有必要,對于運行容器,最后選擇使用了非常輕量的ubuntu的Docker鏡像。
往往一個程序的運行時間都以毫秒為單位。Docker雖然提供了docker status命令監(jiān)控每個容器的資源占用,但是一旦容器停止了便無法再獲取。而且docker status命令是Docker使用客戶端-服務器(C/S)架構模式來通信的[3],中間具有一定的時延。在運行的過程中往往運行容器已經停止了,而docker status命令還沒獲取到容器信息。此外,docker status命令獲取的是一整個容器內的資源占用,不是某一個進程的資源占用。顯然它不能滿足精確監(jiān)控某個進程占用系統資源的需求。
Linux系統的getrusage系統調用,可以得到相關進程的資源使用信息,如進程占用內存、進程在用戶態(tài)下的執(zhí)行時間、進程在系統態(tài)下執(zhí)行的時間。類似的,setrlimit系統調用可以對某個進程進行資源限制。使用getrusage和setrlimit這兩個系統調用,運行容器可以精準地監(jiān)控和限制進程的資源。
為此,可以編寫一個資源監(jiān)控程序,它將在運行容器內部,以子進程的形式運行目標程序可執(zhí)行文件,并重定向輸入輸出流,期間通過getrusage和setrlimit系統調用不斷地對其進行監(jiān)控和限制。若在過程中子進程使用資源超出了給定的限制,例如內存超限、運行超時,則直接停止子進程的運行,并將結果記錄下來。若子進程平穩(wěn)地運行結果,則在子進程結束時返回相關資源使用信息。該監(jiān)控程序也將被整合在Docker鏡像內。
運行容器整體結構如圖2所示。
圖2 運行容器整體結構
就像兩臺通信設備需要基站作為中轉一樣,為了聚合編譯容器與運行容器,必須擁有一個Server容器。該Server容器提供友好交互的前端界面與對外訪問的Web服務,將用戶的源代碼和輸入流遞交到Docker容器中,并將輸出流發(fā)還給用戶。從在用戶的角度看,Server容器就是一個普通的服務器應用,如圖3所示。
圖3 用戶視角的服務器
編譯容器和運行容器將宿主機上的某一目錄共同作為工作區(qū),分別掛載到各自的容器內部,就達到了編譯容器和運行容器同時訪問宿主機工作區(qū)內文件的效果。服務器向該工作區(qū)目錄寫入程序源代碼,編譯容器根據程序源代碼生成程序可執(zhí)行文件,運行容器讀取程序可執(zhí)行文件并給出標準輸入后輸出的結果。上述容器獨立的任務和之間相互通信都在該目錄下進行。
在用戶提交程序源代碼后,通過docker exec <容器id> <編譯命令> <資源限制參數> 啟動編譯容器執(zhí)行編譯命令,屆時可以向編譯容器內的監(jiān)控程序傳遞資源限制的參數。編譯結束后會在工作區(qū)目錄下產生對應程序的可執(zhí)行文件。通過docker run <容器id> <運行參數> <資源限制參數> 啟動運行容器執(zhí)行運行命令。
聚合架構如圖4所示。
圖4 聚合架構
以C++為例,代碼如下。在程序源代碼的中引入一個永遠也讀不完的文件,會使編譯容器永久停留在編譯階段。
Docker編譯容器在運行時可以傳遞資源限制參數,限制編譯時長,就可以避免這種情況。
對于服務器,增加一點磁盤空間或內存空間開銷代價都是昂貴的,所以在線測評網站上提交的代碼允許磁盤空間占用不會太大。但也不懷有人惡意編譯巨大文件[4],導致服務器的磁盤占用迅速上升造成宕機的嚴重后果,必須要在編譯容器執(zhí)行編譯任務時,通過資源限制參數限制編譯文件的大小。
System("shutdown now")會使系統直接關機,fork()可以讓程序遞歸地創(chuàng)建自身進程副本,還有很多此類直接影響整個服務器的操作,可以在編譯時簡單地使用字符串匹配去檢測這些危險的命令,檢測到這些命令后直接停止整個編譯容器的運行并返回錯誤結果[4]。但是如果有人惡意宏定義,就繞過了字符串匹配。所以這種方式是不可行的。
但本文設計的編譯容器和運行容器都是運行在Docker里的,Docker容器內部和宿主機已經完全隔離開,通過Docker限制整個容器的資源,即可解決這個問題。
可能會有那么一種情況,在源代碼里寫sleep(1000)(該代碼在C++中會使程序休眠1000秒),每次程序必定超時,后被強制退出。這非常像類似于DDoS的攻擊方式。解決方案也和DDoS類似,要么提高服務器的配置,要么限制這個這類惡意提交運行超時代碼用戶的正常使用[5]。前者費用昂貴,后者經濟實惠。可以在Server容器內規(guī)定,如果某個用戶提交超時的代碼連續(xù)超過N次,則在后續(xù)一定時間內,該用戶的代碼不給予編譯運行。
Online Judge網站有一特點,通常情況下比如夜晚或節(jié)假日,基本上不會有大量的編譯運行請求。但在某些特殊的時間點上,比如承辦一個民間或官方在線比賽,或組織一場校內或校外的編程考試時,服務器編譯運行請求會迅速增加。如果此時服務器在高負載下宕機了,將會給網站帶來難以估量的損失。傳統單體B/S架構的服務器難以動態(tài)地增加配置,且當用戶請求量到達一定程度時,客戶端和服務器之間的網絡資源分配也成了一大問題,再怎么提高服務器的性能都無濟于事。所以不得不尋求其他的方案,解決單臺服務器性能上限和網絡資源分配的問題。
為了解決上述問題,將Online Judge容器部署在多臺服務器上,不僅可以應對用戶量激增帶來的巨額并發(fā)訪問量,而且在容災方面,當其中一臺判題服務器遭遇意外問題宕機時,整個系統也能正常地運行下去。
在2004年,Google公司提出了一種用于大規(guī)模數據的編程模型MapReduce[6]。參考其編程模型的核心思想“分而治之”,可以將每個用戶的判題任務分發(fā)到不同的服務器上,再統一收集結果返回給用戶。這一過程,將在一臺服務器上的并行任務轉換成在多臺服務器上的并行任務。轉化前后并行執(zhí)行效果如圖5、圖6所示。
可以非常直觀地看到,通過多臺服務器同時開展并行任務,單臺服務器需要承載的壓力會被均分到其他服務器上。同時可根據用戶在線訪問量的波動,動態(tài)地增加或減少服務器集群內的主機的數量,減少高昂的服務器資源費用的支出。
圖5 原始并行圖
圖6 分布并行圖
參考MapReduce,它會先將用戶的原始數據進行切割,然后分發(fā)給不同的Map任務處理[6]。用戶提交到服務器的數據只有源代碼和輸入文本,對這些數據進行切割并沒有什么意義。Online Judge需要切割的是存儲在服務器上的隱藏測試樣例,這些輸入輸出樣例的數量,根據題目的不同,有的幾十,有的甚至上千。這些不同的輸入輸出樣例的空間占用大小,有的只有幾kB,有的可達1 MB。
在分布式集群中,每一臺機器的性能可能都是不一樣的。對此,可以選擇一種較為簡單、靜態(tài)的負載均衡算法——加權輪詢調度算法[7]。它用不同的權值記錄了每一臺服務器的處理能力,在請求到來后,權值高的服務器將會處理更多的請求。分發(fā)任務后,權值會根據資源占用和請求處理的情況進行改變。
服務器選擇任務分發(fā)時,這些占用空間大,運行時間較長的輸入輸出樣例,將會被分發(fā)到性能較高的服務器上。而那些占用空間小,運行時間短的樣例將會被分發(fā)到性能較差的服務器上。因為每個服務器的權值都會被加權輪詢調度算法動態(tài)的更改,所以一批開始于相同時間的任務,在經過集群分發(fā)處理后,等待結束同步的時間不會相差太大。如圖7所示。
圖7 任務分發(fā)
任務歸并的過程只需要將之前分發(fā)的任務取回,取回過程的順序不需要根據任務分發(fā)的順序或是分發(fā)時間順序來定。在這一過程中,專門用于任務分發(fā)的線程會通過遠程過程調用[8]調取集群內Online Judge容器暴露出來的功能函數。該線程在等待Online Judge容器編譯運行的過程中,會進入等待狀態(tài),直至所有任務的運行結果都獲取。
遠程過程調用[8]是一個計算機通信協議,可基于HTTP或TCP協議實現,越接近底層的協議數據傳輸更快。使用遠程過程調用,會在服務器集群內部會產生一定內部網絡資源消耗。因為集群外的網絡資源比集群內的網絡資源要更加昂貴,所以在整個集群內部進行的遠程過程調用,對整個系統來說是可以接受的。
對Online Judge容器嘗試分布式拓展后,將高并發(fā)下單臺服務器性能上限問題轉換成了多臺服務器組成集群的性能上線問題;將高并發(fā)下客戶端與服務器之間網絡資源分配問題,轉換成了客戶端與多臺服務器之間的網絡資源分配問題。這些問題在分布式領域仍面對著許多的問題與挑戰(zhàn),等待著更好的解決方案的提出。
目前,線上編程教學的需求變得愈發(fā)強烈,互聯網企業(yè)在選人面試招聘也逐漸從線下轉變?yōu)榫€上,因此急需免配置系統環(huán)境就可運行的編程環(huán)境。本文設計的基于Docker的Online Judge容器,功能精巧、易部署、具有一定的安全性,能較好的應對此類急需制作針對特定功能而使用的Online Judge網站。同時基于此類在線測評的特點,基于MapReduce和遠程過程調用協議提供了一種分布式拓展的方案,以應對高并發(fā)高負載下的場景。Docker屬于較新的技術之一,具有天然適應云計算的優(yōu)點。未來隨著云計算、分布式技術的發(fā)展,讀者還可以根據該基于Docker設計的Online Judge容器,進一步豐富其定制化功能,實現遠程線上實驗教學、互聯網企業(yè)遠程編程開發(fā)等,以響應工業(yè)和信息化部關于推動5G加快發(fā)展的號召。
[1] 蔡崇超. 基于Web的在線判題系統設計與實現[J]. 軟件導刊,2016,15(3): 107-109.
[2] 歐陽佳,肖茵茵,劉少鵬,等. 基于在線判題系統的程序設計課程群教學研究[J]. 信息與電腦(理論版),2021,33(12): 228-231.
[3] 邱建新. 基于Docker容器技術的Linux在線實驗環(huán)境設計[J]. 信息技術,2022(2): 48-52,58.
[4] 李定才,瞿紹軍,胡爭,等. 基于Windows的在線判題系統的安全性研究[J]. 計算機技術與發(fā)展,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
廣西區(qū)大學生創(chuàng)新創(chuàng)業(yè)訓練計劃立項項目(202010595168、202110595158)。
韋俊宇(2001-),男,桂林電子科技大學計算機與信息安全學院學生。
王宇英,女,桂林電子科技大學計算機與信息安全學院正高級實驗師,碩士,研究方向為高等教育管理。