负载均衡式OJ系统

文章目录

    • 负载均衡式OJ系统
      • 1. 演示项目
      • 2. 所用技术及开发环境
          • 所用技术:
          • 开发环境
      • 3. 项目宏观设计
        • I. leetcode结构
        • II.宏观结构
        • III.编写思路
      • 4. compiler服务
            • 1. 编译功能:
            • 2. 运行功能
            • 3. 编译并运行功能
            • 4. 打包成一个网络服务
      • 5. 基于MVC结构的oj服务设计
            • 1. 用户请求的服务路由功能-oj_server.cc
            • 2.Oj-Model
            • 3. Oj_Control
            • 4. Oj_view
            • 5. 判题功能
            • 6. 编辑负载均衡选择功能
      • 6. 设计题库
        • ->文件版题目设计
        • Mysql版本
      • 7. 增加整体Makefile
      • 8. view渲染和前端页面设计(略)
      • 9.总结

负载均衡式OJ系统

1. 演示项目

2. 所用技术及开发环境

所用技术:
  • C++STL标准库
  • Boost准标准库(实现字符串切割)
  • cpp-httplib第三方开源网络库
  • 多进程,多线程
  • ctemplate第三方开源网页渲染库
  • jsoncpp第三方开源序列化,反序列化
  • 负载均衡设计
  • MySQL C connect
  • Ace编辑器和html/css/js/jquery/ajax(了解)
开发环境
  • Centos 7云服务器
  • vscode
  • MySQL Wokbench

3. 项目宏观设计

核心是三个板块

  1. comm:装载着公共的功能板块
  2. compile_server:编译和运行板块
  3. oj_server:获取题目列表,查看文件编写界面,负载均衡。
I. leetcode结构
  • 只实现LeetCode题目列表+在线编程功能
II.宏观结构

负载均衡式OJ系统_第1张图片

III.编写思路
  1. compile_server服务
  2. oj_server服务
  3. version1文件版在线OJ
  4. 前端页面设计
  5. version2基于MySQL版在线OJ

4. compiler服务

提供的服务:编译并运行代码,得到格式化的相关的结果。

1. 编译功能:

compiler.hpp:只负责实现代码的编译功能.

负载均衡式OJ系统_第2张图片

  1. 输入需要编译的临时文件名到Compile方法中,只需要文件名,文件路径和后缀在comm/util.hpp中的PathUtil自动完成拼接。AddSuffix()

    util.hpp:装填着公共工具方法。

  2. 创建子进程来进行文件的编译:子进程采用程序替换execlp(),程序替换成程序名为g++实现了将file_name的源文件编译形成的同名可执行文件.exe,若编译时报错形成.stderr同名文件。负载均衡式OJ系统_第3张图片

  3. 父进程需要等待子进程编译结果waitpid();负载均衡式OJ系统_第4张图片

    检验编译是否成功就是查看是否形成了同名可执行程序,所以需要引进判断文件是否存在的功能函数:temp/util.hpp/FileUtil::IsFileExits(); 使用系统调用接口stat,检测特定路径下文件的相关属性放到struct stat结构体中,获取文件属性成功即文件存在返回0.
    负载均衡式OJ系统_第5张图片

    stat获取到的文件信息各自填充到结构体的字段中:
    负载均衡式OJ系统_第6张图片

  4. g++时产生的错误信息需要放到文件中,便于后续重定向。所以要在编译之前打开错误文件使用系统个调用接口open();
    负载均衡式OJ系统_第7张图片

  5. newfd作为old的一份拷贝,将原来的显示器文件重定向到标准错误文件中。因为如果g++编译失败会打印到显示屏上,为了不让他打印到显示器所以重定向到文件中,便于后续返回到前端。

负载均衡式OJ系统_第8张图片

  1. 在过程中的错误信息只是返回了,而要打印的话不选择cout而是引进了日志功能。在comm中添加Log.hpp。当中添加了日志时间戳,所以引入comm/util.hpp/TimeUtil()::GetTimeStamp();

负载均衡式OJ系统_第9张图片

负载均衡式OJ系统_第10张图片

2. 运行功能

runner.hpp:

  1. 指明文件名即可,不需要带路径和后缀。

  2. 创建子进程去执行新形成的.exe,也就是temp/同名.exe文件。

  3. 运行结果存在

    • 代码跑完,结果正确
    • 代码跑完结果不正确
    • 代码没跑完,异常中止

    Run方法不需要考虑结果是否正确,正确与否是有给定的测试用例决定的!只考虑是否正确运行完毕

  • 必须知道可执行程序是谁?comm/util.hpp/PathUtil(filename);

  • 一个程序在默认启动的时候

    • 标准输入:不处理,由OJ平台处理
    • 标准输出:程序运行完成,输出结果是什么
    • 标准错误:运行时错误信息,运行时错误写到Stderr中

    把这些临时文件东西全都写到同名文件当中,运行函数只是帮你跑完,结果不关心,最关心的是有没有运行时异常中止。所以报错文件有两个:.Compiler_err编译时报错文件,.std_err运行时报错文件。运行时需要的临时文件也需要.stdin,.stdout,.stderr三个临时文件。

    文件保存运行结果,所以对应的文件需要先都打开。

  • 子进程负责将012的内容重定向到三个文件中,然后调用程序替换函数,因为我们有可执行程序的全称,所以使用execl();

  1. 如何知道程序运行完是异常呢?一定是因为收到了信号!好奇是什么原因异常的?退出信号判定

    返回值>0.程序异常,返回值就是对应的信号编号

    返回值==0,正常运行完毕,结果不关心,保存到对应临时文件中。

    返回值<0,内部错误。

  2. 测试并且添加日志

  3. 设置时间限制和内存限制,以及不同的错误映射不同的原因并且返回。

    setrlimit();负载均衡式OJ系统_第11张图片

在这里插入图片描述

负载均衡式OJ系统_第12张图片

终止进程是通过信号中止的。

//测试超出资源限制
#include 
#include 
#include 
#include 
#include 
void handler(int signo)
{
std::cout << "signo : " << signo << std::endl;
exit(1);
}
int main()
{
//资源不足,导致OS终止进程,是通过信号终止的
for(int i =1; i <= 31; i++){
signal(i, handler);
}
// 限制累计运行时长
// struct rlimit r;
// r.rlim_cur = 1;
// r.rlim_max = RLIM_INFINITY;
// setrlimit(RLIMIT_CPU, &r);
//while(1);
struct rlimit r;
r.rlim_cur = 1024 * 1024 * 40; //20M
r.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_AS, &r);
int count = 0;
while(true)
{
int *p = new int[1024*1024];
count++;
std::cout << "size: " << count << std::endl;
sleep(1);
}
return 0;
}
//内存申请失败
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
signo : 6
[whb@bite-alicloud OnlineJudge]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
...
//CPU使用超时
[whb@bite-alicloud OnlineJudge]$ ./a.out
signo : 24
[whb@bite-alicloud OnlineJudge]$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR

3. 编译并运行功能

compile_run.hpp:

temp目录下的用于测试编译运行功能的code.cpp应该是从客户端传过来的。所以,

  • 适配用户的需求,定制通信协议字段
  • 正确调用compile 和 run方法
  • 形成唯一文件名。编译服务随时可能被多个人请求,必须保证传递上来的code形成源文件名称的时候具有本身的唯一性,避免多个用户之间互相影响。
  1. 需要引进网络功能,通过http,client上传一个json cpp。认识jsoncpp
    负载均衡式OJ系统_第13张图片

    所以需要jsoncpp来实现网络的序列化和反序列化。将结构化数据转化为一个字符串,结构化数据就是用户的各种信息,时间,提交的代码,用户名等等,我们需要将他之转化为字符串。

  2. 输入应该有字段:

    • code: 用户提交的代码
    • cpu_limit
    • mem_limit

    输出的字段应该有:

    • status:状态退出码
    • reason:请求结果,解释信息。
    • stdout: 运行结果

    如果有报错:还需要stderr

  3. 解析json串,也就是反序列化的过程,将一大串字符串转化为K-V结构的结构化数据,所以可以根据[]进行获取对应字符串。

  4. 形成唯一文件名放到temp目录下->/comm/util.hpp/UniqFileName();,没有路径和后缀。

    毫秒级时间戳+原子性递增的唯一值:来保证唯一性。中的gettimeofday();

    原子性唯一定增的值,C++11中的定义一个id,为了避免每次调用时重新定义,所以设置为static。

  5. 将code的代码写到文件中,/comm/util.hpp/WriteFile();形成src源文件

    ostream out(target);

  6. 编译并运行文件全路径下的文件 ,

    • 编译可能存在成功或者失败的情况,都需要进行差错处理。

      编译出错,将Compile_err文件中的内容交给reason,作为失败的原因。/comm/util.hpp/ReadFile();

      读取文件,一行一行读取getline(),不保存行分隔符,但是有的时候需要保存分隔符比如代码中的换行是需要的,还有一些特殊格式是需要保存的。所以添加一个标识符keep=false来确定是否需要保存换行符。

      getline()内部重载了强制类型转换,返回之时bool类型

    • 运行结果,<0属于服务器内部错误,==0是正常运行结束,结果在stdout文件中,使用函数读出来就行,>0就是异常中止信号。而每一种报错都对应着一种信息,所以设计一个映射关系的描述函数:CodeToDes();

      特殊几个情况采用switch-case语句进行处理。引入

  7. 输出out_json就是序列化的过程。

  8. 差错处理

    为了统一化管理避免出现代码冗余,在结尾设置END标签,在每一种情况中设置goto END;注意:在goto语句和END标签之间定义变量是不被允许的,所以都要提前定义好。

  9. 测试时的问题;

    include "compile_run.hpp"
    using namespace ns_compile_and_run;
    //编译服务随时可能被多个人请求,必须保证传递上来的code,形成源文件名称的时候,要具有
    //唯一性,要不然多个用户之间会互相影响
    int main()
    {
    //提供的编译服务,打包形成一个网络服务
    //cpp-httplib
    // in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
    // out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
    // 通过http 让client 给我们 上传一个json string
    // 下面的工作,充当客户端请求的json串
    std::string in_json;
    Json::Value in_value;
    //R"()", raw string
    in_value["code"] = R"(#include
    int main(){
    std::cout << "你可以看见我了" << std::endl;
    aaaaaaaa
    return 0;
    })";
    in_value["input"] = "";
    in_value["cpu_limit"] = 1;
    in_value["mem_limit"] = 10240*3;
    Json::FastWriter writer;
    in_json = writer.write(in_value);
    std::cout << in_json << std::endl;
    //这个是将来给客户端返回的json串
    std::string out_json;
    CompileAndRun::Start(in_json, &out_json);
    std::cout << out_json << std::endl;
    return 0;
    }
    
    • R"()",raw string 将括号中的特殊字符按照原生字符打印出来,保持原貌。
    • 但是在测试json_string时赋值给input时,我用\n无法成功,但是在用回车键代替时就可以成功。
    • 预设制的空间限制时太小,以至于程序在加载库的时候都无法完成。
  10. 删除每一次产生的很多临时文件:RemoveTempFile ();

    • 产生的临时文件的个数是不确定的,因为可能在任何时刻报错产生不一致的文件.但是应该有什么我知道,最都有6个。先拼接各种文件,然后再判断temp文件夹中是否有,然后指定删除文件用unlink();重复6次判断就行。

负载均衡式OJ系统_第14张图片

4. 打包成一个网络服务

引入cpp-httplib 0.7.15,

  • 简介:避免对编译器要求太高。只需要将.h文件拷贝到项目中comm中即可使用,或者放到usr/include目录下。需要更新gcc到gcc7~9,如果没有升级就是编译报错。scl gcc devsettool升级gcc,已经完成升级。
    在这里插入图片描述

用法测试:Makefile中还需要引入-lpthread,因为cpp-httplib是阻塞式原生线程库。

网页访问时,不指定资源时,默认是根目录,在访问hello资源时自动返回后面的内容响应,但是会出现乱码。所以后续添加charset=utf-8编码设置。
负载均衡式OJ系统_第15张图片

所以,当我们收到req的json串,在Lambda表达式中进行编译运行程序调用,并返回一个json串作为Rsponse的串就可以实现交互。

响应回去应该是"applaction/json"负载均衡式OJ系统_第16张图片

  • 云服务器版本较低,在用vscode连接时调用高版本gcc太浪费资源导致被干掉了,偶发事件,只需要重启vscode重新连接即可。

这样,响应构建完成因为没有客户端,所以直接建立请求和响应。为了方便测试可以使用postman 进行发送请求个访问。
负载均衡式OJ系统_第17张图片

5. 基于MVC结构的oj服务设计

本质:建立一个小型网站

  1. 获取首页,用题目列表充当

  2. 编辑区域页面

  3. 提交编译判断题目功能(编译并运行)

  4. Modle:数据交互模块,对题库的增删查的功能(文件版,数据库版)oj_model.hpp

    v:view,通常是拿到数据之后,要进行构建网页,渲染网页内容(浏览器)oj_view.hpp

    c:control,控制器,核心业务逻辑。oj_control.hpp

    • MVC格式,control根据model提供的数据,以view为基准将model数据信息转化成网页。
1. 用户请求的服务路由功能-oj_server.cc

获取所有题目列表

用户根据编号获取题目内容

提交代码使用判题功能(测试用例&编译运行功能)

2.Oj-Model

根据题目list文件,加载所有的题目信息到内存中。,主要用来和数据进行交互,对外提供访问数据的接口。

  1. 根据题目属性,构建Question结构体。

  2. 建立题号-题目细节的映射关系unordered_map.

    • 加载配置文件:先获取到questions/questionlist+题目编号文件 。读取文件列表的同时,填充结构体字段属性。

      • 打开文件 ifstream in(question_list);

      • 按行读取getline();对一行字符串进行指定分隔符的切分,将切分出来的子字符串放到vector中作为输出型参数,comm/util.hpp/StringUtil::SplitString();

        使用boost库中的split方法。安装boost库

        #include

        将vector中的元素一次对应填充给Question.注意转型。

      • 根据题目编号,补充某一个题目的路径字符串,然后根据路径,找到对应文件夹下的三个文件。然后再对文件描述属性,文件预设代码和测试用例属性进行填充。FileUtil::Readfile();

      • 填充好题目属性之后,将题目添加到unordered_map中。

    • 获取所有题目的函数GetAllQuestion();

      将unordered_map中的题目都放到vector数组当中输出。

    • 获取单个题号对应的题目GetOneQuestion();

3. Oj_Control

逻辑控制模块,完成对于请求处理的路由功能。

当用户发起请求之后,我们要返回的是一张填充信息的网页。调用control的方法,获取所有数据然后填充网页。AllQuestions();

ojserver负责获取用户请求,通过control控制器完成服务的路由功能,其中组合了model和view,将数据构建成网页。引入ctemplate网页渲染的库,goole,C/C++的渲染程序。实际上就是将头文件和库文件分别安装到了usr路径下。渲染原理:就是K-V的方式指定。
负载均衡式OJ系统_第18张图片

渲染现象展示:
负载均衡式OJ系统_第19张图片

4. Oj_view

获取数据成功之后,返回一个渲染网页。View _view;Expandhtml();需要一个被渲染的网页。所以提供两个网页,template目录下allquestions_html 和onequestion_html供view代码进行渲染。

  • 渲染allquestion:oj_view.cc中指定渲染模板路径,对每一个题目形成数字典。根字典,子字典,开始完成渲染功能。

  • 加载到题目列表之后,根据题号完成对某一个题目的路由。

  • 渲染onequestion:onequestion的网页中,应该具备代码编辑区,题目描述,题目预设代码等。

    形成路径,形成数据字典,获取被渲染的html,开始渲染。在title里添加a标签,使得他具备跳转功能。

  • 如果后续引入了ctemplate,一旦对网页结构进行修改,尽量的每次想看到结果,将server重启一下。ctemplate有 自己的优化加速策略,可能在内存中存在缓存网页数据(old) 。

5. 判题功能

control调用后端服务,提供判题功能。用户提交上来的json 串应该包含code:input: id:

  • 根据题号得到题目信息,GetOneQuestion();

  • 反序列化in_json得到题目信息。

  • 重新拼接用户代码和测试用例代码。客户传进来的连main函数都没有,所以需要和我提供的tailer.hpp进行拼接。

  • 选择负载最低的主机;一直选择,直到有主机可用,都这就是全部挂掉。

  • 发起http请求。->cpp-httplib,定义client对象cli就可以向对端发起请求。

    class httplib::Result res->cli.post(); out_json=res->body;

    result类型就是一个智能指针指向response,是否存在response决定是否请求成功,请求失败是没有response的。
    负载均衡式OJ系统_第20张图片

    如果状态码status是200,才是成功。如果不是200,即使访问成功也不是我们想要额,就需要重新选择主机。

  • 离线offline();就是将Online下标放到offline数组中,可萌同时有人选择主机,所以要加锁保证安全性。

  • 将结果赋值给out_json返回给用户。

6. 编辑负载均衡选择功能

可以提供服务的主机列表conf文件中,保存机器的部署信息。(一台主机提供不同接口)。

class LoadBalance{};class Machine{};提供服务的主机machine

  • 编辑负载的时候需要引入锁的概念。
  • C++中的锁是禁止拷贝,所以用拷贝锁指针的方式调用这把锁
  • 将各种机器放到vector中,实现负载均衡方法选择,用下标充当主机编号。
  • 在线主机数组和离线主机数组,用id填充数组标识是否在线。
  • 在加载时将所有主机上线:拼接主机路径,在LoadBalance()构造函数中加载。
  • 智能选择 bool SmartChoice(int *id, Machine **m) //解引用是Machine* ,让外面通过地址得到主机
  1. 加载配置文件:打开machine_conf文件,行读取配置机器内容。还需要将ip和端口号进行分割用/comm/util.hpp/stringutil::StringSplit();

    将配置信息填入Machine对象中,然后将所有主机放到online数组中。

  2. 智能选择:更新主机负载进行选择,后续可能离线该主机。

    多执行流可能会同时访问,需要保证数据安全性,上锁。

    • 负载均衡算法:轮询遍历找到最小值,随机数法。
    • 更新负载的方式:递增负载,递减负载。都需要上锁。

6. 设计题库

->文件版题目设计

需要两个文件:

  1. 第一个:题目标号,题目标题,题目难度,时间要求,空间要求
  2. 第二个:题目描述,题目的预设值代码(header.cpp),测试用例代码(tail.cpp)

在题目列表同级文件夹下,创建以题目编号命名的文件夹,文件夹中放着同名题目描述文件,预设代码文件,以及测试用例代码文件。

用户点击提交之后,是将header.cpp和同名的tail.cpp一起提交给后端的compile_run进行编译运行。

  • 下面的代码,我们不想让编译器编译的时候,保留它,而是裁剪掉(g++ -D COMPILER_ONLINE)

    仅仅是为了让我们设计测试用例的时候,不要报错.在后端调用g++的时候加两个选项就行。

ifndef COMPILER_ONLINE 
#include "header.cpp" 
#endif

所以最大的成本是设计测试用例。

Mysql版本
  1. 数据库建立用户并赋权oj_yuanwei,设置数据库和表结构questions。

  2. 录题功能使用MySQL Workbench图形化界面设置。

  3. 连接数据库

    使用第三方库引入的方式,不进行安装。C语言连接

负载均衡式OJ系统_第21张图片

  1. oj_server基于MVC模式,升级为MYSQL版本只需要将oj_model模块进行更改就行。

  2. 因为有数据库的存在,所以获取题目列表和单个题目只有SQL语句的区别。

  3. 遇到问题内存不够了,需要增加swap区:https://blog.csdn.net/qq_34980668/article/details/119562307

  4. 解决这个问题,需要链接-ldl, dl库是用来做动态库加载的(dynamic load),将-ldl链接选项改到最后才可以 。

8.负载均衡式OJ系统_第22张图片

  1. 如果页面乱码,需要考虑是否是编码不正确。

7. 增加整体Makefile

/:一共有两个用,一个是防止转义,一个就是续行;

前面加@:可以让他不在命令行上显示。

发布模式:文件夹下只有两个可执行程序和各种配置文件,不然个被人看代码。

只需要将output文件夹发给他就行。

8. view渲染和前端页面设计(略)

9.总结

负载均衡式在线OJ项目中,

  • 负载均衡的依据仅仅是遍历所有机器的使用情况,可以有所提升。
  • 项目所需要安装的插件以及项目过程中遇到的问题以及解决方法都在文章的各个板块中展现。
  • 题库中的题目还有待丰富。

你可能感兴趣的:(负载均衡,运维)