张天昀的个人博客

问题求解助教工作技术总结

2021年08月12日

程序评测

问题求解的程序评测系统原先用的是HustOJ,然而学校服务器上原有的HustOJ实在太拉:

  1. 无法使用Markdown,需要用HTML写题面,甚至不支持LaTeX
  2. 题目编辑器有不保存数据的bug,编辑完了点击提交数据消失
  3. OJ不支持Python,输出超过100行就会出错
  4. OJ的Special Judge要自己在Linux上编译,还要编译32位程序
  5. 你甚至可以通过改前端HTML来绕过某些验证

所以自己做了一个OJ:

  • 前后端分离,后端和判题程序用ASP.NET(C#),前端用Angular(后悔了)
  • 判题机核心程序直接用Isolate,Special Judge内置一个testlib,查重内置一个jplag,通过自定义判题机的行为来增加对于课程开设lab的支持
  • 数据持久化的数据库用MySQL(MariaDB),杂项用文件系统存储
  • 用Docker部署,只要操作系统上装了Docker就能运行,不需要复杂配置

开发OJ的过程中有一些可以总结的经验:

  1. 开发前要选好编程语言和框架。ASP.NET固然好用,但是总是能在各种想象不到的地方给你挖坑(比如说微软官方的ORM EntityFramework竟然只支持MSSQL,ASP.NET文档推荐的OAuth客户端说停止维护就停止维护……);另一方面Angular固然好用,但是写起来太烦了(烦的堪比JavaScript界的Java)。前端另外还有一个Vue,被Vue 3响应式数据管理、组合式API、各种组件不兼容的操作坑过之后以后只打算用Spring Boot + React的组合。
  2. 不要做过早的优化(premature optimization),应该先考虑跑起来,效率成为瓶颈的时候再去考虑优化问题。开发OJ的时候一直在想类关系、数据读取效率之类的问题,浪费了很多时间。刚开始开发的时候应该秉持一个“又不是不能用”的想法,只要这个程序能跑,管他什么算法什么效率都可以;等到开发完了或者某些函数效率拖后腿了再去考虑优化。
  3. 要坚持使用较好的编程风格和范式,通过继承等方法快速扩展系统的功能。如对于题目评测方式、编程语言的支持,可以通过抽象方法快速扩展功能。
  4. 学习使用开发工具(比如Git/SVN、CI/CD),通过预先准备好工具、写好脚本的方式减少以后开发部署的负担,强烈推荐GitLab和Jenkins。

除此以外还有一些服务器部署方面的工作经验:

  1. 院系提供的是虚拟机,虚拟机有校园网内网IP,并开放了外网的端口8085,映射到虚拟机的80
  2. 虚拟机内部使用HTTP服务器,监听80端口的HTTPS请求,反向代理到虚拟机内的8000端口
  3. Docker运行OJ的服务器程序,容器内部默认监听80端口,通过Docker将虚拟机的8000端口映射到容器内部的80端口
  4. 服务器程序将有路由的地址保留,其他地址全部重定向到静态的Angular程序入口实现OJ访问

在这个实现的过程中有一些比较麻烦的地方:

  1. 首先是HTTP服务器需要监听80端口,而使用Caddy不允许在80端口上监听HTTPS,所以只能变花样绕过这个限制:首先将Caddy的HTTP和HTTPS端口修改成其他数值,然后在80端口上启用tls加密。
{
  http_port 8080
  https_port 8443
}

:80 {
  tls xxx.crt xxx.key
  reverse_proxy http://localhost:8000
}
  1. 另外是OJ上静态文件上传的问题,由于OJ实现过程中并没有做文件上传,所以需要手动SCP文件到服务器的指定位置,通过Docker的卷挂载到容器内部的public文件夹实现访问。(这种反正不影响使用的事情就理解一下好了)
  2. OJ服务器的存储容量并不大,只有20+20=40G的空间(虚拟磁盘上有两个逻辑卷)。本身OJ的容器就占据了大约2.5G空间(需要安装很多的依赖);在同学们大二的时候需要提交lab作业,提交文件非常多,磁盘空间非常捉急,考验空间管理大师的水平。可以先创建一个大的空文件(dd if=/dev/zero of=...),然后当磁盘空间不足的时候第一时间进行删除,给后续的清理磁盘空间留出时间。
  3. 编写代码的注释水平和日志水平有待提高。(企业级理解:不出错就不需要日志了)

题解网站

题解网站是使用Hugo编写的,具备有隐藏部分内容(高斯模糊)、展示代码、观看视频、定期更新等功能。定期更新是依赖于GitLab的CI实现的。

题解网站部署在腾讯云的存储桶上,具体来说是通过以下方式操作的:

  1. 购买域名并添加一个腾讯云的对象存储(COS)存储桶
  2. 将渲染出来的文件上传到存储桶中
  3. 开通存储桶的静态页面访问功能
    • 设置存储桶的错误页面为404.html(或其他文件)
    • 关闭存储桶的强制HTTPS,这部分由CDN负责
    • 如果是React等JavaScript应用程序,需要将404重定向到index.html,实现路由访问
  4. 给存储桶添加内容分发网络(CDN)
    • CDN类型为静态加速,在DNS中添加一个二级域名,CNAME指向CDN的地址
    • 给域名申请一张证书,添加到CDN服务中,开启CDN的HTTPS服务
    • 可以配置CDN回源的授权,这样存储桶就可以关闭公有读的权限了
  5. 配置自动部署和刷新CDN的任务
    • 由于CDN缓存数据的特性,部署到存储桶后不一定会更新CDN中的内容
    • 在腾讯云的云函数中添加由COS触发的函数,当COS的文件发生变化时请求CDN刷新缓存(下面代码使用时需要配置一下CDN的密钥和域名)
'use strict';
console.log('Loading cdn refresh function');
var fs = require('fs');
var path = require('path');
const CDN = require('./cdn_nodejs_sdk');
CDN.config({
    secretId: '',
    secretKey: ''
})
exports.main_handler = (event, context, callback) => {
    console.log('Received event:', JSON.stringify(event, null, 2));
    const Bucket = event.Records[0].cos.cosBucket.name;
    const Key = decodeURIComponent(event.Records[0].cos.cosObject.key
        .replace(/\/\w+\/\w+\//,"")
        .replace(/index.html/, ""));
    console.log("cos file: ", Key)
    console.log("refresh cdn url: ", Key)

    CDN.request('RefreshCdnUrl', {
        'urls.0': `https://your.domain.com/${Key}`
    }, (res) => {
        if (res.code != 0) {
            console.log(res);
            console.log(res.message);
            callback(res.message);
        } else {
            console.log(res);
            callback(null, res);
        }
    })
};

题解网站的费用:

  1. 存储桶:
    • 购买了存储浏览的资源包(10G,每年10元)
    • 购买了CDN回流流量的资源包(50G,每年60元)
    • 以上两个项目的费用被我的博客所包含
  2. CDN:每月赠送10G流量包,根本用不完
  3. 云函数:每月计算资源免费额度40GB秒,根本用不完

综上所述,在我同时在腾讯云挂博客的条件下,开题解网站的费用为0。需要注意的是由于存储桶有公有读取权限,因此会产生外网下行流量,这部分流量我没有买流量包,所以每个月还是会产生大约0.10元左右的额外开销。

另外有一些写题解网站的经验总结:

  1. 一定要挂在国内公网上,避免放在GitHub Pages等同学们打不开的地方。
  2. 一定要做好自动部署,避免自己每次都要手动部署。
  3. 同学们学习的时候并不像我们复习那样简单,少让看的人猜谜。