实现简易负载式均衡在线判题系统

Posted 4nc414g0n

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现简易负载式均衡在线判题系统相关的知识,希望对你有一定的参考价值。

构建HTTP Server实现简易负载均衡在线判题系统

前言

这个项目的大概:
分为两部分:

  • 主server(负载均衡的选择多个从server来处理业务逻辑)
  • 多个从server

说明:

  • 这个项目主要是想把之前学的知识贯通起来,主业务是oj判题
  • 本人水平有限,本来是想自己实现个httpserver,用在上面说的两部分客户端上,但httpserver的Reactor模型构建部分一直出毛病(我只有将构建部分的写事件就绪改为手动调用event的写回调(半个reactor,只监听读就绪)),并且在高并发情况少数情况会出现段错误,再加上这里实现的httpserver只有cgi来处理非网页请求, 使用cgi来进行负载均衡不方便(restful+rpc等方式来实现较为复杂,也还暂时不熟悉这些技术),所以第一部分的主server使用了cpphttplib库(restful回调机制方便,所有请求共享一个Controller)选择的多个编译运行server使用的是自己写的httpserver
  • 这个项目把前端到后端打通了,相对以前有了一个比较清晰的概览,但也发现没有学的东西又变多了很多

还没有学习muduo库,这个项目就先这样,有点拉,后面学完后再重新构建一遍httpserver,这篇博客作为记录,方便之后重新构建

1)HTTP Server

单例TcpServer

Tcpserver.hpp

  • 固定化写法,只是改为单例
  • 端口,监听套接字
  • 初始化->创建套接字,绑定监听
  • 复习一遍

提供的函数接口:

#include <cstring> //htos htol...
#include <unistd.h>

#include <sys/socket.h>
#include <sys/types.h> //socket

#include <arpa/inet.h>
#include <netinet/in.h> //addr family

#include <pthread.h>
#include <fcntl.h>//non-block

void Socket();//获取listen sock
void Bind();//绑定sockaddr_in
void Listen();//监听
int Sock();//返回sock
void SetNonBlock(ssize_t sock);//设置sock为非阻塞

注意

  • 获取listen sock时要setsockopt(_listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // socket地址复用(server挂了立即重启)
  • 单例模式的懒汉模式,在上一个tcmalloc learn项目中也用的比较多,这里不再赘述

Log简易日志

未实现异步日志 (后续重新构建再实现),这里先简单实现打印日志
日志等级:

enum LOGLEVEL

	LOG_LEVEL_ERROR=1,     // error
	LOG_LEVEL_WARNING,   // warning
	LOG_LEVEL_FATAL,     // fatal
	LOG_LEVEL_INFO,      // info	
;

有两种方式

  • LOG(LEVEL, 字符串message);
#define LOG(level, message) Log(#level, message, __FILE__, __LINE__);
void Log(std::string level, std::string message, std::string file_name, int line)

	std::cout <<GetColor(level)<< "[" << level << "] " 
			  <<LIGHT_CYAN<< " ["<<TimeStampToTime()<<"] " 
		   	  <<GetColor(level)<< " [" << message << "] " 
	          <<LIGHT_CYAN<< " [In file: " << file_name << "] " 
	          << " [Line: " << line << "]" <<NONE<< std::endl;

  • LOG(LEVEL)<<字符串message;(返回ostream类型的cout,像cout一样使用)
#define LOG(level) Log(#level, __FILE__, __LINE__)
inline std::ostream &Log(const std::string &level, const std::string &file_name, int line)

   //cout内部包含缓冲区
   std::cerr<<GetColor(level)<<"["<<level<<"]"
            <<LIGHT_CYAN<<" ["<<TimeStampToTime()<<"]"                \\
            <<" [In file: "<<file_name<<"]"
            <<" [Line: "<<line<<"]"
            <<GetColor(level)<<" Message: "
            <<NONE;//注意不要endl刷新
   return std::cerr;//这里不是cout原因:上层httpserver的cgi部分dup了标准输出

支持不同LEVEL等级不同颜色高亮 和 时间戳转北京时间:

协议定制(Protocol概览)

只实现了HTTP1.0,GET/POST请求,可拓展其他不同的请求方式,未实现Cookie Session,基于短连接

Request Response

协议定制部分
HttpRequest请求类的成员设计

//拆分body
       std::string requestLine;//请求行
       std::vector<std::string> requestHeader;//请求头
       std::string blank;//空行
       std::string requestBody;//请求正文
//解析后结果
       //请求行
       //解析完毕之后的结果
       std::string method;//请求方法
       std::string uri; //请求资源(可能包括路径,参数)
       std::string version;//协议即版本
       //请求头
       std::unordered_map<std::string, std::string> headerMap;
       //请求正文
       std::string body;
//继续细化拆分        
       int ContentLength;
       std::string path;//uri中的路径
       std::string suffix;//获得的后缀
       std::string query_string;//uri中的参数(?后面的就是参数语句)

       bool cgi;//是否进行cgi处理
       int size;

HttpResponed响应类成员设计

//构建需要的部分
       std::string status_line;//状态行
       std::vector<std::string> response_header;//响应头
       std::string blank;//空行
       std::string response_body;//响应正文
//细化部分
       int statusCode;
       int fd;//fd用于读取文件,EndPoint的_sock用于接收文件
       int size;//request目标文件的大小,也是返回文件大小

通过文件后缀判定Content-Type(可拓展):

static std::string Suffix2Desc(const std::string &suffix)

   static std::unordered_map<std::string, std::string> suffix2desc = 
       ".html", "text/html",
       ".css", "text/css",
       ".js", "application/javascript",
       ".jpg", "application/x-jpg",
       ".xml", "application/xml",
       ".json", "application/json"
       //...可拓展
   ;
   auto iter = suffix2desc.find(suffix);
   if(iter != suffix2desc.end())
       return iter->second;
   return "text/html";

通过返回码返回描述(可拓展):

static std::string Code2Desc(int code)

   std::string desc;
   switch(code)
       case 200:
           desc = "OK";
           break;
       case 404:
           desc = "Not Found";
           break;
     //case 500:...       
       default:
           break;
   
   return desc;

主部分对端EndPoint

对端EndPoint(提供接口构建请求,解析请求 构建响应 返回响应)
成员设计:

private:
	int sock;
	HttpRequest http_request;
	HttpResponse http_response;
	bool stop;//标记读取(写入)错误
	ns_reactor::Event *event;//对应的事件,见后面reactor部分

接口设计(暴露给外部四个接口:Recv Parse Build Send):

private:
	bool RecvHttpRequestLine();//接收请求行
	bool RecvHttpRequestHeader();//接收头部
	void ParseHttpRequestLine();//解析请求行
	void ParseHttpRequestHeader();//解析头部
	bool HasBody();//判断是否有body(GET无,POST有)
	bool RecvHttpRequestBody()//接收body
	int ProcessCgi();//进行CGI处理
	int ProcessNonCgi();//进行非CGI处理
	void BuildHttpResponseHelper();//构建response的帮助函数
	void ErrorHandler(std::string page);//根据page构建response自定义不同的返回页面
	void OkHandler();//进行正常返回构建
public:
	void RecvHttpRequest();//调用RecvHttpRequestLine(),RecvHttpRequestHeader()
	void ParseHttpRequest();//调用ParseHttpRequestLine(),ParseHttpRequestHeader() RecvHttpRequestBody()
	void BuildHttpResponse();//各种处理逻辑的汇接点,构建出响应HttpResponse
	void SendHttpResponse();//从私有的http_response读取构建好的数据进行开始send给browser

Entrance入口函数

调用对端逻辑

EndPoint *ep = new EndPoint(sock, ev);
ep->RecvHttpRequest();
if(!ep->IsStop()) //如果stop为true了后面的逻辑就没必要执行了
   LOG(LOG_LEVEL_INFO, "Start Building&&Sending");
   ep->BuildHttpResponse();
   ep->SendHttpResponse();

协议定制(Protocol细节)

Util工具类

工具函数:按行读取,字符串切割

static int ReadLine(ns_reactor::Event* event, std::string &out)//按行读取
static bool CutString(const std::string &target, std::string &sub1_out, 
						std::string &sub2_out, std::string sep)//按sep将target字符串切割为两部分

Recv,Parse

HTTP报文结构:

  • 请求(每一行都以一个换行符结束:(\\r\\n或\\r或\\n)):
  • 响应

Recv:

  • RecvHttpRequestLine(),按行读取,Readline返回小于0 读取出错设置标志位stop=true
  • RecvHttpRequestHeader(),按行读取,Readline返回小于0 读取出错设置标志位stop=true,同时将空行读取
  • RecvHttpRequestBody(),HasBody()判断是否有body(POST), 使用recv系统函数接收,出错设置标志位stop=true

Parse:

  • ParseHttpRequestLine(),使用stringstream按空格读取解析前字符串,写入目标,注意:method统一使用transform转大写
  • ParseHttpRequestHeader(),使用Util类CutString,按": "分割,将所有请求头以key-value存储到unordered_map中

先了解CGI

见CGI实现部分:CGI机制及实现

Build Send

Build:

  • BuildHttpResponse(), 主build逻辑:用程序框图表示:

Send:

  • 发送响应行
  • 发送响应头
  • 对于非CGI:获得请求的文件对应的fd,使用sendfile零拷贝发送:关于0拷贝,参考:sendfile—Linux中的"零拷贝"
  • 对于CGI,从http_response读取出body,再send

线程池(阻塞队列)

基于阻塞队列的线程池


设计回调:

  • 任务处理(将Entrance类改为Callback):
class Task
private:
   int sock;
   ns_reactor::Event* event;//见Reactor部分
   CallBack handler; //设置回调
public:
   Task()
   
   Task(int _sock,ns_reactor::Event* ev):sock(_sock),event(ev)
   
   //处理任务
   void ProcessOn()
   
       handler(sock,event);
   
   ~Task()
   
;

在线程池逻辑中,ThreadRoutine在PopTask后会调用ProcessOn(),进行任务处理,Callback类重载了operator(), 调用handler回调,进行处理

主函数Init,Loop

HttpServer

  • InitHttpserver
    注意忽略写入时错误(管道单向写 对端关闭)signal(SIGPIPE, SIG_IGN);
    参考:signal(SIGPIPE, SIG_IGN)
  • 进入Loop
std::shared_ptr<HttpServer> http_server(new HttpServer(port));
http_server->InitServer();
http_server->Loop();

Reactor多线程

之前写过简单Reactor单线程tcp通信server,逻辑参考:Linux----Reactor
这里基本上是复用了上面说的Reactor,加了个基于阻塞队列的线程池想实现Reactor多线程(阻塞队列:容量确定,便于定制策略)
将Loop主函数改为Reactor逻辑:

  • 创建Reactor
  • 创建监听Sock
  • 创建监听事件
  • 将Acceptor回调注册进监听事件
  • 监听事件注册到Reactor对象中
  • 创建一个struct epoll_event类型的数组
  • 进入事件派发逻辑循环, 服务器启动(可以设置epollwait的timeout)

Dispatcher派发逻辑:

  • epoll_wait(R->epfd_, epevs, NUM, -1);阻塞等待就绪事件
    循环就绪事件个数次,进行事件的读/写就绪回调(是监听事件读就绪就进行Accepor逻辑)

Httpserver总体流程图


bug

  • 猜测这里有多线程问题(打印日志出现乱序)
  • 也可能有读写缓冲区问题打印乱码?
  • 段错误:SigmentFault(core dump)

读完muduo库再改

其他拓展方向

  • 有限自动机http协议解析
  • 异步日志
  • 主从reactor
  • 内存池
  • 其他线程池
  • 定时器

2)Load Balance OJ

使用到的第三方库:

准备:工具类Util

解释:CommonUtil:都可以使用,CompileUtil:为编译服务,RunUtil:为运行服务,CompileRunUtil:编译运行
都可以从名字知道函数用途

class CommonUtil
	static void SuffixAdd(std::string& fileName, const std::string& suffix);
	static void StrCut(std::string &line, std::vector<std::string> *target, const std::string sep);
	static bool FileExists(const std::string &file);
	static bool Write2File(const std::string &in, const std::string &Src);
	static bool ReadFromFile(std::string *out, const std::string &file, bool lineBreak = false);

class CompileUtil
	static std::string File2Src(const std::string& fileName);
	static std::string File2Exe(const std::string & fileName);
	static std::string File2Error(const std::string & fileName);

class RunUtil
	static std::string File2Stdin(const std::string & fileName);
	static std::string File2Stdout(const std::string & fileName)
	static std::string File2Stderr(const std::string & fileName);
	static void SetProcLimit(int cpuLimit, int memLimit);

class CompileRunUtil
	static std::string UniqueFileName();
	static void RemoveTmpFile(const std::string &fileName);
	static std::string Status2Desc(int status, const std::string &fileName);

编译服务

总体流程图:

编译

编译部分主要在tmp目录下形成三个文件

  • 源文件source: ./tmp/fileName.cc
  • g++编译出错error : ./tmp/fileName.stderr
  • 可执行文件exe : ./tmp/fileName.exe

编译部分只有一个函数:static bool Compile(std::string &fileName)

  • fileName:由毫秒级时间戳+atomic原子自增结合形成的唯一文件名(在Util头文件中统一实现)

主逻辑:

  • 创建子进程:打开./tmp/fileName.stderr,将stderr也就是2 重定向到open的fd(errorFd),execlp程序替换调用g++,进行对源文件的编译生成./tmp/fileName.exe,编译失败形成./tmp/fileName.stderr文件,里面存储编译失败信息
  • fork出错:return false
  • 父进程:FileExists判定 ./tmp/fileName.exe文件是否存在,返回false/true

运行

运行结果是否正确是由测试用例决定的,这里不考虑,这里仅考虑

  • 运行成功
  • 程序崩溃

运行部分主要在tmp目录下形成三个文件 :

  • 用于存储标准输入: ./tmp/fileName.stdin
  • 用于存储标准输出: ./tmp/fileName.stdout
  • 用于存储标准错误: ./tmp/fileName.stderr

Runner内部也只有一个函数:static int Run(std::string& fileName, int cpuLimit, int memLimit)

  • fileName:由毫秒级时间戳+atomic原子自增结合形成的唯一文件名(在Util头文件中统一实现)
  • cpuLimit:控制程序运行的时间复杂度(系统函数setrlimit在Util头文件的SetProcLimit工具函数中调用)
  • memLimit:控制程序运行的空间复杂度

返回值:

  • 返回值>0: 对应错误码(程序崩溃)
  • 返回值=0: 正常退出
  • 返回值<0: 内部错误(open出错,fork出错…)

主逻辑:

  • 打开.stdin .stdout .stderr文件,得到对应文件描述符
  • fork创建子进程:dup2分别替换对应的文件描述符(.stdin->0 .stdout->1 .stderr->2),
    设置程序运行限制,开始程序替换,运行 ./tmp/fileName.exe,
    程序运行得到的结果会放到.stdout文件中,得到的error信息会存放到.stderr文件中
  • 失败:
  • 父进程:返回status & 0x7F

编译&&运行

也只有一个函数:static void Start(const std::string& inJson, std::string *outJson)

  • inJson:获取的Json串,反序列化获取内容
  • outJson:输入输出型参数,返回序列化好的内容

设计逻辑:

  • int status 统一获取所有逻辑的返回值,通过Util头文件中的Status2Desc获得描述message
    注意:所有非信号终止错误设为负值,方便Run返回值的信号值判定
  • 错误:用户代码为空,写入文件错误,读取文件错误,编译错误,运行错误(注意返回的三种情况)
    这里的写入读取使用了C++更方便的ofstream,ifstream
  • 统一进行序列化形成outJson,
    编译&&运行出错:outJson中只需要两个键值:”status“ ”message“
    编译&&运行成功:outJson中需要四个键值:”status“ ”message“ ”stdout“ “stderr”
  • 文件统一删除(./tmp文件夹下的文件全部删除)

测试:
inJson:


   "code": "#include<iostream>\\nint main()std::cout<<\\"test\\"<<std::endl;", 
   "input": "",
   "cpuLimit": 1, 
   "memLimit": 102400

outJson:


  "message" : "Compile And Run Success...",
  "status" : 0,
  "stderr" : "",
  "stdout" : "test\\n"

结合httpserver形成服务,postman json测试

注意踩过的坑

  • 设置setrlimit虚拟内存过小,进行程序替换会导致替换的程序被11号信号终止,段错误
  • HTTPserver的cgi部分已经重定向了cout cin,进程替换Compile_Run的Log信息只能用cerr打印

httpserver的cgi中新增一个compile_run,形成可执行文件compile_run.json
使用postman进行post请求测试:

负载均衡OJ服务

采用MVC(Model, View, Controller)

Model

文件题库

设计题库:

  • 编号文件->question.list(问题描述) header.cpp(基础代码) test.cpp(测试用例代码) ;设计条件编译g++ -D HEADER(ifndef仅仅为了让编译器不要报错)

    questions.list存储题目标题等
    每个题目文件夹中有描述,precode,测试用例

Model获取数据

题目结构:

struct Question

//5个变量描述题目
   std::string num;//题号
   std::string title;//题目标题
   std::string difficulty;//题目难度
   std::string desc;//题目描述
   int time;//事件复杂度要求(s)
   int space;//空间复杂度要求(kb)
//拼接下面两部分
   std::string header;//题目自带代码部分
   std::string test;//题目测试用例部分
;

接口:

利用nginx和docker实现一个简易的负载均衡

nginx 下轮询简易负载均衡

Spring Cloud Eureka 分布式开发之服务注册中心负载均衡声明式服务调用实现

consul+upsync+nginx实现动态负载均衡

负载均衡LVS概述以及DR模式简易部署

项目基于负载均衡的在线OJ项目