柚子快報(bào)激活碼778899分享:運(yùn)維 實(shí)戰(zhàn)項(xiàng)目: 負(fù)載均衡
柚子快報(bào)激活碼778899分享:運(yùn)維 實(shí)戰(zhàn)項(xiàng)目: 負(fù)載均衡
0. 前言
這個(gè)項(xiàng)目使用了前后端,實(shí)現(xiàn)一個(gè)丐版的LeetCode刷題網(wǎng)站,并根據(jù)每臺(tái)主機(jī)的實(shí)際情況,選擇對(duì)應(yīng)的主機(jī),負(fù)載均衡的調(diào)度?
0.1?所用技術(shù)與開發(fā)環(huán)境
所用技術(shù):??
C++ STL
標(biāo)準(zhǔn)庫(kù)
Boost 準(zhǔn)標(biāo)準(zhǔn)庫(kù)
(
字符串切割
)
cpp-
httplib
第三方開源網(wǎng)絡(luò)庫(kù)
ctemplate 第三方開源前端網(wǎng)頁(yè)渲染庫(kù)
jsoncpp 第三方開源序列化、反序列化庫(kù)
負(fù)載均衡設(shè)計(jì)
多進(jìn)程、多線程
MySQL C connect
Ace前端在線編輯器
(
部分
)
html/css/js/jquery/ajax (部分
)
開發(fā)環(huán)境:?
Centos 7
云服務(wù)器,
vscode,?
Mysql Workbench
0.2 建立目錄及文件
0.3 項(xiàng)目宏觀結(jié)構(gòu)
具體的功能類似 leetcode 的題目列表+在線編程功能?
1.?compile?服務(wù)設(shè)計(jì)
?由于compiler這個(gè)模塊管理的是編譯與運(yùn)行,則可以先直接就創(chuàng)建所需要的文件
1.0 書寫makefile文件?
??隨著后續(xù)代碼的跟進(jìn),并不斷引入第三方庫(kù),這里還會(huì)新增編譯選項(xiàng)
1.1 compiler_server
1.1.0?編譯功能(compiler.hpp)
?在編譯的時(shí)候,無(wú)非存在2種情況,a)要么通過(guò),b)要么不通過(guò)要確定編譯通過(guò): 只需要確定是否生成對(duì)應(yīng)的.exe文件要當(dāng)編譯出錯(cuò)的時(shí)候(stderr): 需要將出錯(cuò)信息,重定向到一個(gè)臨時(shí)文件中,保存編譯出錯(cuò)的結(jié)果 還需要調(diào)用fork();子進(jìn)程完成編譯工作 父進(jìn)程繼續(xù)執(zhí)行
?由于需要頻繁的文件名轉(zhuǎn)換,所以在comm模塊中,新建util.hpp文件并將文件名轉(zhuǎn)換的函數(shù)放在一起
?
還有后面判斷編譯成功生成的可執(zhí)行程序,雖然可以直接暴力的打開文件判斷是否存在,但這里使用stat函數(shù)會(huì)好一些stat結(jié)構(gòu)體會(huì)記錄文件的各種信息
?注意: 程序替換是不會(huì)影響進(jìn)程的文件符描述符表的
1.1.1 日志模塊(log.hpp)
由于一般日志都會(huì)帶上時(shí)間,?這里還需要實(shí)現(xiàn)一個(gè)得到當(dāng)前時(shí)間的函數(shù),則我又在util.hpp把得到時(shí)間函數(shù)的類封裝成了一個(gè)類
?
由于會(huì)頻繁的調(diào)用日志進(jìn)行打印信息,也為了更簡(jiǎn)便的調(diào)用,我進(jìn)行了以下處理
?
如果在宏定義中使用#,那么這個(gè)宏就被稱為帶有字符串化操作的宏。這種宏可以將其參數(shù)轉(zhuǎn)換成字符串常量,并在預(yù)處理階段進(jìn)行替換。?
由于引入了日志,則就可以把之前所有的輸出信息,換成日志輸出?
1.1.2 測(cè)試編譯模塊?
?Compile的參數(shù)是文件名,它內(nèi)部會(huì)自動(dòng)拼接我們還需要再./temp中創(chuàng)建一個(gè)code.cpp文件
?上面我的代碼有一個(gè)錯(cuò)誤,在編譯成功的時(shí)候,并沒(méi)有return,導(dǎo)致LOG日志打印有問(wèn)題
?要是我們的源文件有問(wèn)題,錯(cuò)誤信息就會(huì)重定向到 文件.compile_error中在測(cè)試的時(shí)候,還需要把 文件.exe 文件.compile_error文件刪除,就是上次生成的文件
1.1.3?運(yùn)行功能(runner.hpp)?
程序運(yùn)行: 1)代碼跑完,結(jié)果正確 2)代碼跑完,結(jié)果不正確, 3)代碼沒(méi)跑完,異常了
程序結(jié)果是否正確,是由oj_server中的測(cè)試用例決定的,則run模塊只考慮是否正確運(yùn)行完畢
#include
#include
#include
#include
#include
#include
#include "../comm/log.hpp"
#include "../comm/util.hpp"
namespace ns_runner
{
using namespace ns_util;
using namespace ns_log;
class Runner
{
public:
Runner(){}
~Runner(){}
static int Run(const std::string &file_name){
std::string _execute = PathUtil::Exe(file_name);// 可執(zhí)行
std::string _stdin = PathUtil::Stdin(file_name);// 輸入
std::string _stdout = PathUtil::Stdout(file_name);// 輸出
std::string _stderr = PathUtil::Stderr(file_name);// 錯(cuò)誤
umask(0);
int _stdin_fd = open(_stdin.c_str(),O_CREAT | O_RDONLY,0644);
int _stdout_fd = open(_stdout.c_str(),O_CREAT | O_WRONLY,0644);
int _stderr_fd = open(_stderr.c_str(),O_CREAT | O_WRONLY,0644);
if(_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0){
LOG(ERROR) << "運(yùn)行時(shí)打開標(biāo)準(zhǔn)文件失敗" << "\n";
return -1;// 代表打開文件失敗
}
pid_t pid = fork();
if(pid < 0){
LOG(ERROR) << "運(yùn)行創(chuàng)建子進(jìn)程失敗" << "\n";
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
return -2;// 代表創(chuàng)建自己失敗
}
else if(pid == 0){
// 子進(jìn)程
dup2(_stdin_fd,0);
dup2(_stdout_fd,1);
dup2(_stderr_fd,2);
LOG(INFO) << "123";// 是不是有問(wèn)題啊
// 這個(gè)程序替換等價(jià)于 ./tmp/code.exe ./tmp/code.exe
execl(_execute.c_str(),_execute.c_str(),nullptr);
exit(1);
}
else{
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
int status = 0;// 表示輸出型參數(shù)
waitpid(pid,&status,0);// 阻塞式等待
// 程序運(yùn)行異常,一定是因?yàn)槭盏叫盘?hào)
LOG(INFO) << "運(yùn)行完畢,infor: " << (status & 0x7f) << "\n";
return status & 0x7f;
}
}
};
}
返回值 > 0: 程序異常了,退出時(shí)收到了信號(hào),返回值就是對(duì)應(yīng)的信號(hào)編號(hào)返回值 == 0: 正常運(yùn)行完畢的,結(jié)果保存到了對(duì)應(yīng)的臨時(shí)文件中返回值 < 0: 內(nèi)部錯(cuò)誤 run.hpp也是一樣的,把自己的各種輸出信息,輸出到一個(gè)臨時(shí)文件中要判斷一個(gè)程序是否異常,只需要看它是否收到了異常信號(hào)
解釋waitpid第2個(gè)輸出型參數(shù)?
?
?status并不是按照整數(shù)來(lái)整體使用的,而是按照比特位的方式,將32個(gè)比特位進(jìn)行劃分,只需要學(xué)習(xí)低16位這也是上面為什么會(huì)寫成status & 0x7F的原因
那6個(gè)程序替換的系統(tǒng)接口,具體使用那個(gè)看實(shí)際情況?
沒(méi)有p就需要帶路徑有l(wèi),就是列表式傳命令有v就是數(shù)組式傳命令有e就需要傳自己設(shè)置的環(huán)境變量
1.1.4 測(cè)試運(yùn)行模塊
雖然運(yùn)行模塊已經(jīng)能正常運(yùn)行了,但是萬(wàn)一code.cpp是惡意程序了,比如死循環(huán),不停消耗CPU資源 , 所以還需要進(jìn)一步的資源約束
1.1.5 添加資源限制(setrlimit)
資源不足,導(dǎo)致OS終止進(jìn)程,是通過(guò)信號(hào)終止的 ?
?
?為了方便上層調(diào)用,我直接在Run函數(shù)中增加了cpu_limit和mem_limit形參
?這個(gè)項(xiàng)目走到這里就需要編寫compile_run.hpp,將編譯和運(yùn)行的邏輯連接在一起,且code.cpp需要被處理的源文件,不應(yīng)該是我們自己添加的,而是需要再客戶端中導(dǎo)入的
1.1.6 編譯 && 運(yùn)行功能 (compile_run.hpp)
這個(gè)模塊要做的是:
a)適配用戶請(qǐng)求,引入json定制通信協(xié)議字段
b)形成唯一文件名
c)正確調(diào)用compile and run
在centos中安裝: sudo yum install json-c-devel
頭文件 #include
?注意: 在編譯引入了json的文件,需要加上-ljsoncpp
?
雖然這個(gè)code就是文件名了,但client可能會(huì)提交大量的代碼,所以內(nèi)部就會(huì)需要形成唯一的文件名(待完善)還有很多個(gè)出錯(cuò)問(wèn)題怎么解決(待完善)
?complie_run.hpp
#pragma once
#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include
namespace ns_compile_and_run
{
using namespace ns_log;
using namespace ns_util;
using namespace ns_compiler;
using namespace ns_runner;
// in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
// out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
static void Start(const std::string &in_json,std::string *out_json){
// step1: 反序列化過(guò)程
Json::Value in_value;
Json::Reader reader;
// 把in_json中的數(shù)據(jù)寫到in_value中
reader.parse(in_json,in_value);// 最后再處理差錯(cuò)問(wèn)題
std::string code = in_value["code"].asString();
std::string input = in_value["input"].asString();
int cpu_limit = in_value["cpu_limit"].asInt();
int mem_limit = in_value["mem_limit"].asInt();
Json::Value out_value;
int status_code = 0;
int run_result = 0;
std::string file_name;// 唯一文件名
if(code.size() == 0){
status_code = -1;// 代碼為空
goto END;
}
// 形成的文件名只居有唯一性,沒(méi)有目錄沒(méi)有后綴
// 使用: 毫秒級(jí)時(shí)間戳 + 原子性遞增唯一值 : 來(lái)保證唯一性
file_name = FileUtil::UniqFileName();
// 形成臨時(shí)的src文件
if(!FileUtil::WriteFile(PathUtil::Src(file_name),code)){
status_code = -2;// 未知錯(cuò)誤
goto END;
}
if(!Compiler::Compile(file_name)){
status_code = -3;// 編譯錯(cuò)誤
goto END;
}
run_result = Runner::Run(file_name,cpu_limit,mem_limit);
if(run_result < 0){
// runnem模塊內(nèi)部錯(cuò)誤
status_code = -2;// 未知錯(cuò)誤
}
else if(run_result > 0){
// 程序運(yùn)行崩潰
status_code = run_result;// 這里的run_result是信號(hào)
}
else{
// 運(yùn)行成功
status_code = 0;
}
END:
out_value["status"] = status_code;
out_value["reason"] = CodeToDest(status_code,file_name);// 得到錯(cuò)誤信息字符串
if(status_code == 0){
// 整個(gè)過(guò)程全部成功
std::string _stdout;
FileUtil::ReadFile(PathUtil::Stdout(file_name),&_stdout,true);
out_value["stdout"] = _stdout;
std::string _stderr;
FileUtil::ReadFile(PathUtil::Stdout(file_name),&_stderr,true);
out_value["stdout"] = _stdout;
}
// step2: 序列化
Json::StyledWriter writer;
*out_json = writer.write(out_value);
}
}
?1.1.7 基于compile_run.hpp對(duì)util.hpp的補(bǔ)充
?
注意引入流時(shí)需要引入頭文件: #include
1.1.8 測(cè)試編譯運(yùn)行模塊?
#include "compile_run.hpp"
using namespace ns_compile_and_run;
int main()
{
std::string in_json;
Json::Value in_value;
// R"()", raw string
in_value["code"] = R"(#include
int main(){
std::cout << "你可以看見我了" << std::endl;
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;
// 這個(gè)是將來(lái)給客戶端返回的json串
std::string out_json;
CompileAndRun::Start(in_json, &out_json);
std::cout << out_json << std::endl;
return 0;
}
?實(shí)際上這里的代碼應(yīng)該是client自動(dòng)提交給我們的,我們直接使用第三方庫(kù)就行了待優(yōu)化: 可以把臨時(shí)生成的這些文件都清理掉,
1.1.9 清理臨時(shí)文件
?這個(gè)函數(shù)直接放在compile_server.cc中的start函數(shù)的最后,清理臨時(shí)文件
1.1.10 引入cpp-httplib 網(wǎng)絡(luò)庫(kù)
?下載地址:?cpp-httplib: C++ http 網(wǎng)絡(luò)庫(kù) - Gitee.com
?這個(gè)就是別人寫好的網(wǎng)絡(luò)庫(kù),我們直接使用就行了
1.1.11 更新gcc
安裝scl :?sudo yum install centos-release-scl scl-utils-build
安裝新版本gcc:?
sudo yum install
-
y devtoolset
-
9
-
gcc devtoolset
-
9
-
gcc
-
c
++
把?scl enable devtoolset-9 bash 放在?~/.bash_profile中想每次登陸的時(shí)候,都是較新的gcc
?如果不更新在使用cpp-httplib時(shí)可能會(huì)報(bào)錯(cuò),?用老的編譯器,要么編譯不通過(guò),要么直接運(yùn)行報(bào)錯(cuò)
1.1.12 測(cè)試cpp-httplib網(wǎng)絡(luò)庫(kù)?
可能會(huì)出現(xiàn)服務(wù)器的公網(wǎng)ip無(wú)法訪問(wèn)的問(wèn)題,可以試試把防火墻關(guān)閉,并打開端口
1.1.13 將compiler_server打包成網(wǎng)絡(luò)服務(wù)
compiler_server.cc
#include "compile_run.hpp"
#include "../comm/httplib.h"http:// 引入
using namespace ns_compile_and_run;
using namespace httplib;// 引入
void Usage(std::string proc){
std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}
//./copile_server port
int main(int argc,char *argv[])
{
if(argc != 2){
Usage(argv[0]);
return 1;
}
Server svr;
svr.Post("/compile_and_run",[](const Request&req,Response & resp){
// 用戶請(qǐng)求的服務(wù)正文是我們想要的json string
std::string in_json = req.body;
std::string out_json;
if(!in_json.empty()){
CompileAndRun::Start(in_json,&out_json);
resp.set_content(out_json,"application/json;charset=uft-8");
}
});
svr.listen("0.0.0.0",atoi(argv[1]));
return 0;
}
由于我這里沒(méi)有寫客戶端代碼,則這里暫時(shí)不好測(cè)試,不過(guò)可以借助第三方工具進(jìn)行測(cè)試
?2.?oj_server服務(wù)設(shè)計(jì)
本質(zhì):
建立一個(gè)小型網(wǎng)站
1. 獲取首頁(yè),用題目列表充當(dāng)
2. 編輯區(qū)域頁(yè)面
3. 提交判題功能(編譯并運(yùn)行)
2.1 書寫makefile文件
?隨著后續(xù)代碼的跟進(jìn),并不斷引入第三方庫(kù),這里還會(huì)新增編譯選項(xiàng)
2.2?服務(wù)路由功能(oj_server.cc)?
為用戶實(shí)現(xiàn)的路由功能就3個(gè) a. 獲取所有的題目列表 b.根據(jù)題目編號(hào),獲取題目?jī)?nèi)容 c.判斷用戶提交的代碼??
2.3 MVC 結(jié)構(gòu)的oj 服務(wù)設(shè)計(jì)(M)
Model
,
通常是和數(shù)據(jù)交互的模塊
,比如,對(duì)題庫(kù)進(jìn)行增刪改查(文件版,
MySQL
)
2.3.1?安裝boost庫(kù) && 字符切分功能
sudo yum install -y boost-devel //是boost 開發(fā)庫(kù)
?第一個(gè)參數(shù)為緩沖區(qū),第二個(gè)參數(shù)為被分割的字符串第三個(gè)參數(shù)為分割符,第四個(gè)參數(shù)為是否壓縮
要壓縮: 當(dāng)sep = "空格"時(shí),sepsepsep?-> 空格不壓縮: 當(dāng)sep = "空格"時(shí),sepsepsep -> 空格空格空格
?2.3.2 數(shù)據(jù)結(jié)構(gòu)
header.cpp
#include
#include
#include
#include
#include
using namespace std;
class Solution{
public:
bool isPalindrome(int x)
{
//將你的代碼寫在下面
return true;
}
};
tail.cpp
#ifndef COMPILER_ONLINE
#include "header.cpp"
#endif
// 這里先把測(cè)試用例 暴露出來(lái)
void Test1()
{
// 通過(guò)定義臨時(shí)對(duì)象,來(lái)完成方法的調(diào)用
bool ret = Solution().isPalindrome(121);
if(ret){
std::cout << "通過(guò)用例1, 測(cè)試121通過(guò) ... OK!" << std::endl;
}
else{
std::cout << "沒(méi)有通過(guò)用例1, 測(cè)試的值是: 121" << std::endl;
}
}
void Test2()
{
// 通過(guò)定義臨時(shí)對(duì)象,來(lái)完成方法的調(diào)用
bool ret = Solution().isPalindrome(-10);
if(!ret){
std::cout << "通過(guò)用例2, 測(cè)試-10通過(guò) ... OK!" << std::endl;
}
else{
std::cout << "沒(méi)有通過(guò)用例2, 測(cè)試的值是: -10" << std::endl;
}
}
int main()
{
Test1();
Test2();
return 0;
}
des.txt表示題目信息?header.cpp表示預(yù)設(shè)代碼tail.cpp表示測(cè)試用例
?真正代碼 = 用戶在head.cpp中的代碼 + header.cpp + tail.cpp 并去到COMPILER_ONLINE?這個(gè)條件編譯只是為了編寫tail.cpp時(shí)不報(bào)錯(cuò)
2.3.3?model功能(oj_model.cpp)
數(shù)據(jù)交互 && 提供接口
?
#pragma once
// 文件版本
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include
#include
#include
namespace ns_model
{
using namespace std;
using namespace ns_log;
using namespace ns_util;
struct Question{
string number;// 題目編號(hào),唯一
string tile;// 題目標(biāo)題
string star;// 難度: 簡(jiǎn)單 中等 困難
int cpu_limit;// 題目的時(shí)間復(fù)雜度(S)
int mem_limit;// 題目的空間復(fù)雜度(KB)
string desc;// 題目描述
string header; // 題目預(yù)設(shè)給用戶在線編輯器的代碼
string tail;// 題目測(cè)試用例,需要和header拼接
};
const string questions_list = "./question/quetions.list";
const string questions_path = "./question";
class Model
{
public:
Model(){
// 加載所有題目:底層是用hash表映射的
assert(LoadQuestionList(questions_list));
}
~Model(){
;
}
// 獲取所有題目,這里的out是輸出型參數(shù)
bool GetAllQuestions(vector
if(questions.size() == 0){
LOG(ERROR) << "用戶獲取題庫(kù)失敗" << "\n";
return false;
}
for(const auto&q: questions){
out->push_back(q.second);
}
return true;
}
// 獲取指定題目,這里的q是輸出型參數(shù)
bool GetOneQuestion(const string& number,Question* q){
const auto& iter = questions.find(number);
if(iter == questions.end()){
LOG(ERROR) << "用戶獲取題目失敗,題目編號(hào): " << number << "\n";
return false;
}
(*q) = iter->second;
return true;
}
// 加載配置文件: questions/questions.list + 題目編號(hào)文件
bool LoadQuestionList(const string&question_list){
// 加載配置文件: questions/questions.list +題目編號(hào)文件
ifstream in(question_list);
if(!in.is_open()){
LOG(FATAL) << "加載題庫(kù)失敗,請(qǐng)檢查是否存在題庫(kù)文件" << "\n";
return false;
}
string line;
while(getline(in,line)){
vector
StringUtil::SplitString(line,&tokens," ");// 被分割的字符串 緩沖區(qū) 分割符
// eg: 1 判斷回文數(shù) 簡(jiǎn)單 1 30000
if(tokens.size()!=5){
LOG(WARNING) << "加載部分題目失敗,請(qǐng)檢查文件格式" << "\n";
continue;
}
Question q;
q.number = tokens[0];
q.tile = tokens[1];
q.star = tokens[2];
q.cpu_limit = atoi(tokens[3].c_str());
q.mem_limit = atoi(tokens[4].c_str());
string path = questions_list;
path += q.number;
path += "/";
// 第三個(gè)參數(shù)代表 是否加上 \n
FileUtil::ReadFile(path+"desc.txt",&(q.desc),true);
FileUtil::ReadFile(path+"header.cpp",&(q.header),true);
FileUtil::ReadFile(path+"tail.txt",&(q.tail),true);
questions.insert({q.number,q});// 錄題成功
}
LOG(INFO) << "加載題庫(kù)...成功" << "\n";
in.close();
}
private:
// 題號(hào) : 題目細(xì)節(jié)
unordered_map
};
}
2.4 MVC 結(jié)構(gòu)的oj 服務(wù)設(shè)計(jì)(C)?
2.4.1 負(fù)載均衡模塊
namespace ns_control
{
using namespace std;
using namespace ns_log;
using namespace ns_util;
using namespace ns_model;
using namespace ns_view;
using namespace httplib;
// 提供服務(wù)的主機(jī)
class Machine
{
public:
std::string ip; //編譯服務(wù)的ip
int port; //編譯服務(wù)的port
uint64_t load; //編譯服務(wù)的負(fù)載
std::mutex *mtx; // mutex禁止拷貝的,使用指針
public:
Machine() : ip(""), port(0), load(0), mtx(nullptr)
{
}
~Machine()
{
}
public:
// 提升主機(jī)負(fù)載
void IncLoad()
{
if (mtx) mtx->lock();
++load;
if (mtx) mtx->unlock();
}
// 減少主機(jī)負(fù)載
void DecLoad()
{
if (mtx) mtx->lock();
--load;
if (mtx) mtx->unlock();
}
void ResetLoad()
{
if(mtx) mtx->lock();
load = 0;
if(mtx) mtx->unlock();
}
// 獲取主機(jī)負(fù)載,沒(méi)有太大的意義,只是為了統(tǒng)一接口
uint64_t Load()
{
uint64_t _load = 0;
if (mtx) mtx->lock();
_load = load;
if (mtx) mtx->unlock();
return _load;
}
};
const std::string service_machine = "./conf/service_machine.conf";
class LoadBlance
{
private:
// 可以給我們提供編譯服務(wù)的所有的主機(jī)
// 每一臺(tái)主機(jī)都有自己的下標(biāo),充當(dāng)當(dāng)前主機(jī)的id
std::vector
// 所有在線的主機(jī)id
std::vector
// 所有離線的主機(jī)id
std::vector
// 保證LoadBlance它的數(shù)據(jù)安全
std::mutex mtx;
public:
LoadBlance()
{
assert(LoadConf(service_machine));
LOG(INFO) << "加載 " << service_machine << " 成功"
<< "\n";
}
~LoadBlance()
{
}
public:
bool LoadConf(const std::string &machine_conf)
{
std::ifstream in(machine_conf);
if (!in.is_open())
{
LOG(FATAL) << " 加載: " << machine_conf << " 失敗"
<< "\n";
return false;
}
std::string line;
while (std::getline(in, line))
{
std::vector
StringUtil::SplitString(line, &tokens, ":");
if (tokens.size() != 2)
{
LOG(WARNING) << " 切分 " << line << " 失敗"
<< "\n";
continue;
}
Machine m;
m.ip = tokens[0];
m.port = atoi(tokens[1].c_str());
m.load = 0;
m.mtx = new std::mutex();
online.push_back(machines.size());
machines.push_back(m);
}
in.close();
return true;
}
// id: 輸出型參數(shù)
// m : 輸出型參數(shù)
bool SmartChoice(int *id, Machine **m)
{
// 1. 使用選擇好的主機(jī)(更新該主機(jī)的負(fù)載)
// 2. 我們需要可能離線該主機(jī)
mtx.lock();
// 負(fù)載均衡的算法
// 1. 隨機(jī)數(shù)+hash
// 2. 輪詢+hash
int online_num = online.size();
if (online_num == 0)
{
mtx.unlock();
LOG(FATAL) << " 所有的后端編譯主機(jī)已經(jīng)離線, 請(qǐng)運(yùn)維的同事盡快查看"
<< "\n";
return false;
}
// 通過(guò)遍歷的方式,找到所有負(fù)載最小的機(jī)器
*id = online[0];
*m = &machines[online[0]];
uint64_t min_load = machines[online[0]].Load();
for (int i = 1; i < online_num; i++)
{
uint64_t curr_load = machines[online[i]].Load();
if (min_load > curr_load)
{
min_load = curr_load;
*id = online[i];
*m = &machines[online[i]];
}
}
mtx.unlock();
return true;
}
void OfflineMachine(int which)
{
mtx.lock();
for(auto iter = online.begin(); iter != online.end(); iter++)
{
if(*iter == which)
{
machines[which].ResetLoad();
//要離線的主機(jī)已經(jīng)找到啦
online.erase(iter);
offline.push_back(which);
break; //因?yàn)閎reak的存在,所有我們暫時(shí)不考慮迭代器失效的問(wèn)題
}
}
mtx.unlock();
}
void OnlineMachine()
{
//我們統(tǒng)一上線,后面統(tǒng)一解決
mtx.lock();
online.insert(online.end(), offline.begin(), offline.end());
offline.erase(offline.begin(), offline.end());
mtx.unlock();
LOG(INFO) << "所有的主機(jī)有上線啦!" << "\n";
}
//for test
void ShowMachines()
{
mtx.lock();
std::cout << "當(dāng)前在線主機(jī)列表: ";
for(auto &id : online)
{
std::cout << id << " ";
}
std::cout << std::endl;
std::cout << "當(dāng)前離線主機(jī)列表: ";
for(auto &id : offline)
{
std::cout << id << " ";
}
std::cout << std::endl;
mtx.unlock();
}
};
}
2.4.1?control功能(oj_control.hpp)
邏輯控制模塊
// 這是我們的核心業(yè)務(wù)邏輯的控制器
class Control
{
private:
Model model_; //提供后臺(tái)數(shù)據(jù)
View view_; //提供html渲染功能
LoadBlance load_blance_; //核心負(fù)載均衡器
public:
Control()
{
}
~Control()
{
}
public:
void RecoveryMachine()
{
load_blance_.OnlineMachine();
}
//根據(jù)題目數(shù)據(jù)構(gòu)建網(wǎng)頁(yè)
// html: 輸出型參數(shù)
bool AllQuestions(string *html)
{
bool ret = true;
vector
if (model_.GetAllQuestions(&all))
{
sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2){
return atoi(q1.number.c_str()) < atoi(q2.number.c_str());
});
// 獲取題目信息成功,將所有的題目數(shù)據(jù)構(gòu)建成網(wǎng)頁(yè)
// ...
}
else
{
*html = "獲取題目失敗, 形成題目列表失敗";
ret = false;
}
return ret;
}
bool Question(const string &number, string *html)
{
bool ret = true;
struct Question q;
if (model_.GetOneQuestion(number, &q))
{
// 獲取指定題目信息成功,將所有的題目數(shù)據(jù)構(gòu)建成網(wǎng)頁(yè)
// ....
}
else
{
*html = "指定題目: " + number + " 不存在!";
ret = false;
}
return ret;
}
// code: #include...
// input: ""
void Judge(const std::string &number, const std::string in_json, std::string *out_json)
{
}
};
?control模塊中的判題功能,我打算最后設(shè)計(jì)
2.5??MVC 結(jié)構(gòu)的oj 服務(wù)設(shè)計(jì)(V)
2.4?安裝與測(cè)試 ctemplate(網(wǎng)頁(yè)渲染)
渲染本質(zhì)就是key-value之間的替換
安裝鏡像源: git clone https://gitee.com/mirrors_OlafvdSpek/ctemplate.git?
.
/
autogen
.
sh
.
/
configure
make
//
編譯 如果報(bào)錯(cuò)請(qǐng)
更新gcc
make install
test.cpp?
#include
#include
#include
int main()
{
std::string html = "./test.html";
std::string html_info = "測(cè)試ctemplate渲染";
// 建立ctemplate參數(shù)目錄結(jié)構(gòu)
ctemplate::TemplateDictionary root("test"); // unordered_map
// 向結(jié)構(gòu)中添加你要替換的數(shù)據(jù),kv的
root.SetValue("info", html_info); // test.insert({key, value});
// 獲取被渲染對(duì)象
// DO_NOT_STRIP:保持html網(wǎng)頁(yè)原貌
ctemplate::Template *tpl = ctemplate::Template::GetTemplate(html,ctemplate::DO_NOT_STRIP);
// 開始渲染,返回新的網(wǎng)頁(yè)結(jié)果到out_html
std::string out_html;
tpl->Expand(&out_html, &root);
std::cout << "渲染的帶參html是:" << std::endl;
std::cout << out_html << std::endl;
return 0;
}
?test.html
{{info}}
{{info}}
{{info}}
{{info}}
?錯(cuò)誤原因: error while loading shared libraries: libmpc.so.3: cannot open shared object file?
export LD\_LIBRARY\_PATH=$LD\_LIBRARY\_PATH:/usr/local/lib
?在命令行上輸入 上面這段命令,注:只在當(dāng)前會(huì)話中有效
# ?cat /etc/ld.so.conf include ld.so.conf.d/*.conf # ?echo "/usr/local/lib" >> /etc/ld.so.conf # ?ldconfig
2.5.2 渲染功能(oj_view.hpp)
#pragma once
#include
#include
#include "./oj_model.hpp"
namespace ns_view
{
using namespace ns_model;
const std::string template_path = "./template_html/";
class View
{
public:
View(){}
~View(){}
// 渲染所有題目
void ALLExpandHtml(const vector
// 題目編號(hào) 題目標(biāo)題 題難度
// 推薦表格實(shí)現(xiàn)
// 1.形成路徑
string src_html = template_path + "all_quetions.html";
// 2.形成數(shù)字典
ctemplate::TemplateDictionary root("all_question");
for(const auto& q: question){
ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list");
sub->SetValue("number",q.number);
sub->SetValue("title",q.title);
sub->SetValue("star",q.star);
}
// 3. 獲取被渲染的html
ctemplate::Template*tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
// 4.開始完成渲染功能
tpl->Expand(html,&root);
}
// 渲染一道題目
void OneExpandHtml(const struct Question &q,string *html){
// 1.形成路徑
std::string src_html = template_path + "one_question.html";
// 2. 形成數(shù)字典
ctemplate::TemplateDictionary root("one_question");
root.SetValue("number",q.number);
root.SetValue("title",q.title);
root.SetValue("star",q.star);
root.SetValue("desc",q.desc);
root.SetValue("header",q.header);
// 3.獲取被渲染的html
ctemplate::Template*tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);
// 4.開始完成渲染功能
tpl->Expand(html,&root);
}
};
}
2.6? 聯(lián)動(dòng)MVC模塊并測(cè)試
oj_server.cc?
#include
#include "../comm/httplib.h"http:// 引入
#include "oj_control.hpp"
using namespace httplib;// 引入
using namespace ns_control;
int main()
{
// 用戶請(qǐng)求的服務(wù)器路由功能
Server svr;
Control ctrl;
// 獲取所有的題目列表
svr.Get("/all_questions",[&ctrl](const Request&req,Response &resp){
// 返回一張包含所有題目的html網(wǎng)頁(yè)
std::string html;// 待處理
ctrl.AllQuestions(&html);
resp.set_content(html,"text/html;charset=utf-8");
});
// 根據(jù)題目編號(hào),獲取題目?jī)?nèi)容
// \d+ 是正則表達(dá)式的特殊符合
svr.Get(R"(/question/(\d+))",[&ctrl](const Request&req,Response &resp){
std::string number = req.matches[1];
std::string html;
ctrl.Question(number,&html);
resp.set_content(html,"text/html;charset=utf-8");
});
// 判斷用戶提交的代碼(1.每道題c測(cè)試用例,2.compile_and_run)
svr.Post(R"(/judge/(\d+))",[&ctrl](const Request&req,Response &resp){
std::string number = req.matches[1];
std::string result_json;
ctrl.Judge(number,req.body,&result_json);
resp.set_content(result_json,"application/json;charset=utf-8");
});
svr.set_base_dir("./wwwroot");
svr.listen("0.0.0.0",8080);
return 0;
}
?這里的前端都是提前做好了的,我們可以不關(guān)心前端;control功能還有個(gè)判題功能沒(méi)有實(shí)現(xiàn)
?2.7 完善oj_control.hpp中的判題功能
void Judge(const std::string &number, const std::string in_json, std::string *out_json)
{
// LOG(DEBUG) << in_json << " \nnumber:" << number << "\n";
// 0. 根據(jù)題目編號(hào),直接拿到對(duì)應(yīng)的題目細(xì)節(jié)
struct Question q;
model_.GetOneQuestion(number, &q);
// 1. in_json進(jìn)行反序列化,得到題目的id,得到用戶提交源代碼,input
Json::Reader reader;
Json::Value in_value;
reader.parse(in_json, in_value);
std::string code = in_value["code"].asString();
// 2. 重新拼接用戶代碼+測(cè)試用例代碼,形成新的代碼
Json::Value compile_value;
compile_value["input"] = in_value["input"].asString();
compile_value["code"] = code + "\n" + q.tail;
compile_value["cpu_limit"] = q.cpu_limit;
compile_value["mem_limit"] = q.mem_limit;
Json::FastWriter writer;
std::string compile_string = writer.write(compile_value);
// 3. 選擇負(fù)載最低的主機(jī)(差錯(cuò)處理)
// 規(guī)則: 一直選擇,直到主機(jī)可用,否則,就是全部掛掉
while(true)
{
int id = 0;
Machine *m = nullptr;
if(!load_blance_.SmartChoice(&id, &m))
{
break;
}
// 4. 然后發(fā)起http請(qǐng)求,得到結(jié)果
Client cli(m->ip, m->port);
m->IncLoad();
LOG(INFO) << " 選擇主機(jī)成功, 主機(jī)id: " << id << " 詳情: " << m->ip << ":" << m->port << " 當(dāng)前主機(jī)的負(fù)載是: " << m->Load() << "\n";
if(auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8"))
{
// 5. 將結(jié)果賦值給out_json
if(res->status == 200)
{
*out_json = res->body;
m->DecLoad();
LOG(INFO) << "請(qǐng)求編譯和運(yùn)行服務(wù)成功..." << "\n";
break;
}
m->DecLoad();
}
else
{
//請(qǐng)求失敗
LOG(ERROR) << " 當(dāng)前請(qǐng)求的主機(jī)id: " << id << " 詳情: " << m->ip << ":" << m->port << " 可能已經(jīng)離線"<< "\n";
load_blance_.OfflineMachine(id);
load_blance_.ShowMachines(); //僅僅是為了用來(lái)調(diào)試
}
}
2.8 測(cè)試oj_server服務(wù)
在編譯時(shí)需要加上-D COMPILER_ONLINE條件編譯,?
?設(shè)計(jì)到前端網(wǎng)頁(yè),下面會(huì)有提及
2.9 一個(gè)BUG?
?把tail.txt改成tail.cpp,不然后面無(wú)法進(jìn)行代碼拼接
?3. 前端頁(yè)面設(shè)計(jì)(了解)
3.1 index.html
?3.2?all_questions.html
OnlineJuge題目列表
編號(hào) | 標(biāo)題 | 難度 |
---|---|---|
{{number}} | {{title}} | {{star}} |
3.3 one_questions.html(ACE插件&&JQuery&&ajax)
ACE插件是一個(gè)
編寫代碼的編譯框
收集當(dāng)前頁(yè)面的有關(guān)數(shù)據(jù)
, a.
題號(hào)
a.
代碼
,
我們采用
JQuery
來(lái)進(jìn)行獲取
html
中的內(nèi)容
構(gòu)建json,并通過(guò)
ajax向后臺(tái)
發(fā)起基于http的json請(qǐng)求
全部代碼
{{number}}.{{title}}_{{star}}
{{desc}}
3.4 相關(guān)測(cè)試
?4. MySQL版題目設(shè)計(jì)
4.1 注冊(cè)用戶 && 賦予權(quán)限
create user 'oj_client'@'localhost' identified by '123456';create database oj;grant select on oj.* to 'oj_client'@'localhost';select user,Host from user;
?4.2 下載第三方工具-workbench
?
?下載下來(lái)之后,就不斷的下一步,下一步就行了
4.3 錄題到mysql中
use oj;
drop table if exists oj_table;
create table if not exists oj_table(
_number varchar(200) comment '題目編號(hào)',
_titie varchar(200) comment '題目標(biāo)題',
_start varchar(200) comment '題目簡(jiǎn)單中等困難',
_desc varchar(2000) comment '題目描述',
_header varchar(2000) comment '題目預(yù)設(shè)',
_tail varchar(2000) comment '題目測(cè)試用例',
_cpu_limit int comment '時(shí)間要求',
_mem_limt int comment '空間要求'
);
insert into oj_table values(
1,
'判斷回文數(shù)',
'簡(jiǎn)單',
'判斷一個(gè)整數(shù)是否是回文數(shù)?;匚臄?shù)是指正序(從左向右)和倒序(從右向左)讀都是一樣的整數(shù)。
示例 1:
輸入: 121
輸出: true
示例 2:
輸入: -121
輸出: false
解釋: 從左向右讀, 為 -121 。 從右向左讀, 為 121- 。因此它不是一個(gè)回文數(shù)。
示例 3:
輸入: 10
輸出: false
解釋: 從右向左讀, 為 01 。因此它不是一個(gè)回文數(shù)。
進(jìn)階:
你能不將整數(shù)轉(zhuǎn)為字符串來(lái)解決這個(gè)問(wèn)題嗎?',
'#include
#include
#include
#include
#include
using namespace std;
class Solution{
public:
bool isPalindrome(int x)
{
//將你的代碼寫在下面
return true;
}
};',
'#ifndef COMPILER_ONLINE
#include "header.cpp"
#endif
void Test1()
{
// 通過(guò)定義臨時(shí)對(duì)象,來(lái)完成方法的調(diào)用
bool ret = Solution().isPalindrome(121);
if(ret){
std::cout << "通過(guò)用例1, 測(cè)試121通過(guò) ... OK!" << std::endl;
}
else{
std::cout << "沒(méi)有通過(guò)用例1, 測(cè)試的值是: 121" << std::endl;
}
}
void Test2()
{
// 通過(guò)定義臨時(shí)對(duì)象,來(lái)完成方法的調(diào)用
bool ret = Solution().isPalindrome(-10);
if(!ret){
std::cout << "通過(guò)用例2, 測(cè)試-10通過(guò) ... OK!" << std::endl;
}
else{
std::cout << "沒(méi)有通過(guò)用例2, 測(cè)試的值是: -10" << std::endl;
}
}
int main()
{
Test1();
Test2();
return 0;
}',
1,
30000
);
select * from oj_table;
?
?這里我只錄入了一道題為了測(cè)試
4.4 下載并引入mysql庫(kù)文件
MySQL :: Download MySQL Community Server
要使用C/C++連接MySQL,需要使用MySQL官網(wǎng)提供的庫(kù)
??
?下載完畢后需要將其上傳到云服務(wù)器,這里將下載的庫(kù)文件存放在下面的目錄:
?
然后使用tar命令將壓縮包解壓到當(dāng)前目錄下:?
xz -d mysql-8.0.37-linux-glibc2.28-i686.tar.xz
tar xvf mysql-8.0.37-linux-glibc2.28-i686.tar
?
進(jìn)入解壓后的目錄當(dāng)中,可以看到有一個(gè)include子目錄和一個(gè)lib子目錄,其中,include目錄下存放的一堆頭文件。而lib64目錄下存放的就是動(dòng)靜態(tài)庫(kù)。?
??
?然后在我們的項(xiàng)目中建立軟連接
?
4.5 一個(gè)BUG
如果你當(dāng)時(shí)下載myql把mysql-devel也下載了,不需要進(jìn)行上面步驟 這種引入第三方庫(kù)的操作,可能會(huì)因?yàn)榘姹静患嫒?而導(dǎo)致出錯(cuò)skipping incompatible ./lib/libmysqlclient.so when searching for -lmysqlclient ?建議直接安裝: yum -y install mysql-devel
?4.5 重新設(shè)計(jì)oj_model
因?yàn)閛j_model模塊是管理數(shù)據(jù),提供接口的模塊,所以要把這個(gè)項(xiàng)目變成mysql就需要重新設(shè)計(jì)
#pragma once
// 文件版本
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include
#include
#include
#include "./include/mysql.h"
namespace ns_model
{
using namespace std;
using namespace ns_log;
using namespace ns_util;
struct Question{
string number;// 題目編號(hào),唯一
string title;// 題目標(biāo)題
string star;// 難度: 簡(jiǎn)單 中等 困難
int cpu_limit;// 題目的時(shí)間復(fù)雜度(S)
int mem_limit;// 題目的空間復(fù)雜度(KB)
string desc;// 題目描述
string header; // 題目預(yù)設(shè)給用戶在線編輯器的代碼
string tail;// 題目測(cè)試用例,需要和header拼接
};
const std::string oj_questions = "oj_table";
const std::string host = "127.0.0.1";
const std::string user = "oj_client";
const std::string passwd = "123456";
const std::string db = "oj";
const int port = 3306;
class Model
{
public:
Model(){
}
~Model(){
;
}
bool QueryMysql(const std::string &sql,vector
// 這里的out是輸出型參數(shù)
// 創(chuàng)建mysql句柄
MYSQL *my = mysql_init(nullptr);
// 連接數(shù)據(jù)庫(kù)
if(nullptr == mysql_real_connect(my,host.c_str(),user.c_str(),passwd.c_str(),db.c_str(),port,nullptr,0)){
LOG(FATAL) << "連接數(shù)據(jù)庫(kù)失敗!" << "\n";
return false;
}
// 一定要設(shè)置該鏈接的編碼格式,要不然會(huì)出現(xiàn)亂碼的問(wèn)題
mysql_set_character_set(my,"utf8");
LOG(INFO) << "連接數(shù)據(jù)庫(kù)成功!" << "\n";
// 執(zhí)行sql語(yǔ)句
if(0 != mysql_query(my,sql.c_str())){
LOG(WARNING) << sql << " execute error! " << "\n";
return false;
}
// 提取結(jié)果
MYSQL_RES *res = mysql_store_result(my);// 本質(zhì)就是一個(gè)2級(jí)指針
// 分析結(jié)果
int rows = mysql_num_rows(res);// 獲取行的數(shù)量
int cols = mysql_num_fields(res);// 獲取列的數(shù)量
Question q;
for(int i = 0;i < rows;i++){
MYSQL_ROW row = mysql_fetch_row(res);
q.number = row[0];
q.title = row[1];
q.star = row[2];
q.desc = row[3];
q.header = row[4];
q.tail = row[5];
q.cpu_limit = atoi(row[6]);
q.mem_limit = atoi(row[7]);
out->push_back(q);
}
// 釋放控件
free(res);
// 關(guān)閉mysql連接
mysql_close(my);
return true;
}
// 獲取所有題目,這里的out是輸出型參數(shù)
bool GetAllQuestions(vector
std::string sql = "select * from ";
sql += oj_questions;
return QueryMysql(sql,out);
}
// 獲取指定題目,這里的q是輸出型參數(shù)
bool GetOneQuestion(const string& number,Question* q){
bool res = false;
std::string sql = "select * from ";
sql += oj_questions;
sql += " where number=";
sql += number;
vector
if(QueryMysql(sql,&result)){
if(result.size() == 1){
*q = result[0];
res = true;
}
}
return res;
}
private:
// 題號(hào) : 題目細(xì)節(jié)
unordered_map
};
}
mysql_init: 創(chuàng)建mysql句柄mysql_real_connect: 創(chuàng)建mysql連接 mysql_query: 發(fā)起mysql請(qǐng)求 mysql_close: 關(guān)閉mysql連接
?4.6 相關(guān)測(cè)試
?
編譯期間告訴編譯器頭文件和庫(kù)文件在哪里?-I指明搜索的頭文件,-L指明搜索的lib并加上-lmysqlclient
??
??
?
5. 擴(kuò)展
功能上更完善一下,判斷一道題目正確之后,自動(dòng)下一道題目
基于注冊(cè)和登陸的錄題功能
.....
6. 完整項(xiàng)目鏈接?
projects/負(fù)載均衡/OnlineJudge at main · 1LYC/projects · GitHub
柚子快報(bào)激活碼778899分享:運(yùn)維 實(shí)戰(zhàn)項(xiàng)目: 負(fù)載均衡
精彩內(nèi)容
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場(chǎng)。
轉(zhuǎn)載請(qǐng)注明,如有侵權(quán),聯(lián)系刪除。