高级
整体架构

整体架构

Seele 评测系统分为两个组成部分:评测服务和安全沙箱。评测服务作为服务进程,接收用户提交的请求并产生安全沙箱子进程来运行评测程序。 评测系统的整体架构如下图所示:

Seele 架构图

其中,评测服务使用 Rust (opens in a new tab) 语言编写,基于流行的高性能异步编程框架 Tokio (opens in a new tab)。 Rust 语言通过其独特的所有权系统和内存安全特性,让人们在使用它编写高性能应用程序时能够更加安全、高效地管理内存,避免应用程序因为内存问题而崩溃。 此外,Rust 也拥有出色的并发性能和可伸缩性,使得它成为编写高吞吐量和高并发性能应用程序的理想选择。 Tokio 框架为 Rust 语言带来了高性能的异步运行时实现,提供了基于事件驱动的非阻塞 I/O 模型,能够让评测服务高效地处理大量 I/O 任务。

对于安全沙箱,它使用 Go (opens in a new tab) 语言编写,基于著名的容器运行时 runc (opens in a new tab)。参见安全沙箱

Exchange

Exchange 接收用户提交的评测任务,将其交给 Composer,然后将 Composer 发送的评测报告反馈给用户。为了提升灵活性,评测系统允许用户配置多个 Exchange 共同工作。 目前,评测服务提供了两种 Exchange 的实现,为用户提供了多种与评测系统进行数据交互的方式。

Composer

Composer 接收来自 Exchange 的评测任务,解析评测任务并生成一棵由步骤构成的多叉树,从沿着这棵树从根部开始向 Worker 发送步骤,追踪步骤的执行。 当无法再沿着多叉树执行更多步骤时,评测任务执行完毕,Composer 此时汇总数据产生评测报告并发送给 Exchange。

Worker

Worker 接收 Composer 发来的执行步骤,并根据其中的配置执行对应的任务,最后将执行报告发送给 Composer。目前,Worker 中提供了两种任务:添加文件和运行容器。 在运行容器任务中,Worker 通过 skopeo (opens in a new tab) 从用户指定的镜像源中拉取镜像到本地,然后通过 umoci (opens in a new tab) 将镜像解压。 Worker 会通过调用安全沙箱程序来启动容器、运行评测程序并收集报告。

线程池

Seele 将 Exchange、Composer 和 Worker 运行在主线程中,确保系统的其它 CPU 核心带来的并发能力能够更多地分配给安全沙箱的运行,提高评测服务并发执行评测任务的能力。同时,为了确保运行 Tokio 框架事件循环的主线程不被持续运行时间较长的同步任务影响导致事件响应时间变高,拖累系统的整体性能,需要将一些阻塞型 I/O 任务和 CPU 密集型任务分发给线程池中的辅助线程运行。在运行每个安全沙箱时,由于需要确保评测程序能够尽可能地独占一个 CPU 核心以确保公平性,我们也需要将运行安全沙箱的任务发给线程池中的辅助线程运行。

此外,由于安全沙箱的运行时间相比其它同步任务要长的多,为了避免 Tokio 线程池被安全沙箱运行任务占满导致其他关键的同步任务无法即时执行,Seele 在 Tokio 线程池的基础上构建了一个仅限 Worker 使用的安全沙箱线程池。此线程池仅包含 Tokio 线程池中的部分线程,避免了安全沙箱运行任务占满 Tokio 线程池,而安全沙箱线程池中的线程仍然能够被其它同步任务使用,提高了线程的利用率。

在评测服务启动时,主线程和 Tokio 线程池中的线程都会被评测服务通过 cgroup 的 cpu controller 绑定到系统不同的 CPU 核心上。这在为安全沙箱的运行提供了公平性(降低了运行时间的波动性)的同时,也在一定程度上通过更好的 CPU 缓存局部性提升了各个线程的运行性能。参见公平性