C++ 从零实现负载均衡式在线OJ判题系统

发布时间:2026/7/18 2:54:39
C++ 从零实现负载均衡式在线OJ判题系统 一、项目前言平时我们在 LeetCode、牛客、洛谷刷题时最核心的体验就是在线写代码、一键提交、即时判题。很多同学只知其用不知其底层原理服务器如何安全编译用户代码如何防止恶意代码死循环、爆内存高并发刷题场景下如何扛住压力为此我基于C Linux 系统编程 httplib ctemplate jsoncpp从零手写实现了一套分布式负载均衡在线OJ判题系统。区别于传统单机OJ本项目实现了服务解耦、进程沙箱隔离、资源限制、分布式负载均衡、故障自动容灾等企业级核心能力完整复刻了主流刷题平台的底层判题逻辑。本文将从项目架构、核心模块、业务流程、技术难点、项目优化五个维度完整拆解这套OJ系统的设计与实现。二、项目整体介绍2.1 核心功能题库页面渲染动态展示全部题目列表、单题详情题号、标题、难度、题目描述、代码模板在线代码判题支持用户提交C代码自动编译、运行、结果校验安全沙箱隔离进程级资源限制杜绝死循环、内存溢出、恶意系统调用分布式负载均衡多台编译服务集群部署自动选择空闲节点分发判题任务故障自动容灾编译节点故障自动下线服务重启一键恢复所有节点全流程日志监控分级日志记录系统运行状态快速定位编译、运行、网络异常2.2 技术栈核心语言C11网络服务cpp-httplib轻量级HTTP服务快速搭建接口页面渲染ctemplate谷歌模板引擎服务端动态渲染HTML数据交互jsoncpp前后端JSON序列化与解析系统编程Linux fork/exec、dup2重定向、setrlimit资源限制、信号处理工具依赖Boost字符串分割、原子变量、文件IO、多线程互斥锁架构设计MVC分层架构 分布式微服务集群2.3 整体架构整套系统分为两大核心服务、多层业务模块完全解耦支持横向扩展OJ主服务Web服务负责页面展示、路由分发、业务调度、负载均衡对接前端与编译集群编译运行子服务集群节点独立部署多实例专注代码编译、沙箱运行、结果返回分层结构公共工具层 → MVC业务层 → 负载均衡层 → 编译沙箱层三、核心模块详细拆解3.1 公共工具层底层支撑所有业务模块的基础依赖封装通用能力避免代码冗余。3.1.1 日志模块log实现分级日志体系INFO、DEBUG、WARNING、ERROR、FATAL通过宏定义简化调用自动携带文件名、行号、时间戳精准定位问题。3.1.2 通用工具模块util时间工具获取秒/毫秒时间戳用于生成唯一文件名路径工具统一管理临时文件路径源码、可执行文件、错误日志、标准输入输出文件文件工具文件读写、文件存在校验、原子唯一文件名生成解决多线程文件冲突字符串工具基于Boost实现字符串分割解析配置文件与题库数据3.2 MVC核心架构OJ主服务主服务严格遵循MVC分层思想Model管数据、View管页面、Control管业务职责完全单一扩展性极强。3.2.1 Model数据层oj_model负责题库数据的加载与管理是系统的数据源。数据载体Question结构体封装题号、标题、难度、CPU/内存限制、题目描述、代码模板、测试用例代码核心逻辑服务启动自动加载questions.list配置文件批量读取每道题的描述、模板代码、测试代码存入哈希表实现O(1)题号查询对外接口获取全部题目、根据题号获取单题信息为上层业务提供纯净数据支撑3.2.2 View视图层oj_view基于谷歌ctemplate模板引擎实现数据与页面分离无需手动拼接HTML。模板文件提前编写好题库列表、单题详情两个HTML模板页面渲染接收Model层的题目数据动态填充模板占位符自动生成完整HTML页面核心方法AllExpandHtml渲染题库列表、OneExpandHtml渲染单题详情3.2.3 Control控制层oj_control系统的业务中枢串联数据层、视图层、负载均衡模块处理所有核心业务。包含两大核心能力页面渲染调度、代码判题业务、分布式负载均衡。1. 页面业务AllQuestions获取全量题目、排序、调用视图层渲染题库页面Question根据题号查询单题渲染题目编辑详情页2. 负载均衡模块核心亮点自定义实现分布式负载均衡器管理所有编译运行节点节点配置读取配置文件自动加载所有编译服务器IP、端口智能选机实时统计各节点负载优先选择负载最低的空闲节点分发任务故障容灾节点请求失败自动标记离线避免重复请求故障机器恢复机制注册SIGQUIT信号服务接收信号后一键恢复所有离线节点3. 判题核心逻辑接收前端代码提交请求完整处理判题全流程解析前端JSON获取用户代码、自定义输入用例读取题目配置拼接用户代码 后台测试用例代码携带CPU/内存限制参数组装判题请求通过负载均衡选择最优编译节点HTTP转发任务接收编译节点返回的判题结果封装JSON返回前端实时更新节点负载任务完成自动减负3.3 编译运行集群服务沙箱核心独立于主服务的微服务节点每台节点负责安全编译、沙箱运行用户代码是整个系统的安全核心。3.3.1 Compiler编译模块基于Linux子进程调用g完成代码编译fork创建子进程隔离编译任务不影响主服务重定向标准错误流捕获编译报错信息execlp调用系统g编译器生成可执行文件校验可执行文件是否存在判断编译成功/失败3.3.2 Runner运行模块安全核心解决用户代码不可信的核心难点实现进程沙箱隔离资源限制通过setrlimit系统调用严格限制子进程CPU运行时长、最大内存使用量杜绝死循环、内存轰炸IO重定向将进程标准输入、输出、错误重定向到临时文件隔离用户代码IO避免污染服务器终端信号捕获通过子进程退出信号识别CPU超时、内存溢出、浮点数异常等运行错误3.3.3 CompileAndRun总控模块整合编译、运行、文件管理、结果封装提供一站式判题能力生成全局唯一临时文件避免多用户并发冲突代码写入、编译、运行全流程自动化执行根据进程状态码精准识别各类错误并生成中文提示自动读取程序运行输出、运行错误自动清理临时文件任务结束后清空所有缓存文件防止磁盘溢出四、核心业务全流程复盘4.1 用户访问题库页面流程浏览器发起GET请求/all_questionshttplib路由拦截调用Control层页面接口Control调用Model层读取全量题库数据并排序View层通过ctemplate动态渲染HTML页面主服务返回完整HTML浏览器渲染展示题库4.2 用户提交代码判题全流程核心前端编辑代码发起POST请求/judge/题号携带代码与输入用例JSON主服务Control层接收请求读取对应题目资源限制与测试代码拼接用户代码与测试代码组装标准化判题参数负载均衡器筛选当前最空闲的编译节点主服务通过HTTP客户端转发任务到编译节点编译节点执行写临时文件→编译代码→沙箱受限运行捕获运行结果、错误信息封装JSON返回主服务主服务更新节点负载将判题结果返回前端展示系统自动清理所有临时文件完成一次判题闭环五、核心源码逐行深度讲解面试重点5.1 Model层题库加载核心代码文件配置 → 内存哈希Model层是整个系统的数据源核心设计启动一次性加载所有题目存入哈希表查询 O(1)。bool LoadQuestionList(const string question_list) { ifstream in(question_list); if (!in.is_open()) { LOG(FATAL) 加载题库失败 \n; return false; } string line; while (getline(in, line)) { vectorstring tokens; StringUtil::SplitString(line, tokens, ); if (tokens.size() ! 5) continue; Question q; q.number tokens[0]; q.title tokens[1]; q.star tokens[2]; q.cpu_limit atoi(tokens[3].c_str()); q.mem_limit atoi(tokens[4].c_str()); // 拼接路径读取题目详情、代码模板、测试用例 string path questins_path q.number /; FileUtil::ReadFile(path desc.txt, (q.desc), true); FileUtil::ReadFile(path header.cpp, (q.header), true); FileUtil::ReadFile(path tail.cpp, (q.tail), true); questions.insert({ q.number, q }); } in.close(); return true; }代码核心思路讲解通过配置文件questions.list统一管理所有题目方便批量增删题目每道题独立文件夹描述、模板代码、测试代码分离结构清晰使用unordered_map存储题号映射题目查询效率极高启动一次性加载避免每次用户请求都读磁盘大幅提升并发性能5.2 View层ctemplate动态渲染核心代码传统OJ用字符串拼接HTML代码巨烂、维护困难。本项目使用模板引擎彻底解耦数据和页面。void AllExpandHtml(const vectorstruct Question questions, std::string *html) { std::string src_html template_path all_questions.html; ctemplate::TemplateDictionary root(all_questions); // 循环填充模板列表 for (const auto q : questions) { ctemplate::TemplateDictionary *sub root.AddSectionDictionary(question_list); sub-SetValue(number, q.number); sub-SetValue(title, q.title); sub-SetValue(star, q.star); } ctemplate::Template *tpl ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP); tpl-Expand(html, root); }核心亮点前端页面只写模板占位符后端只填数据前后端逻辑彻底分离支持列表循环渲染无需手动拼字符串杜绝内存泄漏、拼接错误修改页面样式完全不用改C代码开发效率极高5.3 负载均衡核心代码面试必考这是本项目最大亮点自研最小负载负载均衡 故障自动摘除bool SmartChoice(int *id, Machine **m) { mtx.lock(); int online_num online.size(); if (online_num 0) { mtx.unlock(); LOG(FATAL) 所有编译主机已离线 \n; return false; } // 默认选中第一台 *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; }逻辑拆解全局互斥锁保证多线程并发选机安全遍历所有在线节点选择当前任务数最少的节点分发判题机器任务完成自动减负完美实现负载均衡搭配OfflineMachine实现故障自动下线服务高可用5.4 安全沙箱资源限制核心代码防死循环/爆内存OJ系统最核心、最关键的安全代码没有这段代码服务器会被恶意代码打崩。static void SetProcLimit(int _cpu_limit, int _mem_limit) { // 限制CPU运行时长 struct rlimit cpu_rlimit; cpu_rlimit.rlim_max RLIM_INFINITY; cpu_rlimit.rlim_cur _cpu_limit; setrlimit(RLIMIT_CPU, cpu_rlimit); // 限制内存使用 struct rlimit mem_rlimit; mem_rlimit.rlim_max RLIM_INFINITY; mem_rlimit.rlim_cur _mem_limit * 1024; setrlimit(RLIMIT_AS, mem_rlimit); }原理讲解RLIMIT_CPU限制程序最大运行秒数超过直接系统杀死进程防死循环RLIMIT_AS限制进程虚拟内存最大值防恶意代码无限new内存用户代码全部跑在子进程主进程完全安全5.5 子进程编译 IO重定向核心代码pid_t pid fork(); if (pid 0) { umask(0); int _stderr open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644); dup2(_stderr, 2); // 子进程执行g编译 execlp(g, g, -o, PathUtil::Exe(file_name).c_str(), PathUtil::Src(file_name).c_str(), -stdc11, nullptr); exit(1); }关键技术点fork创建子进程编译失败、崩溃不会影响主服务dup2重定向stderr把编译错误保存到文件前端可以展示报错信息execlp程序替换调用系统编译器完成编译极其稳定5.6 主服务路由 信号容灾代码// 信号恢复所有机器 void Recovery(int signo) { ctrl_ptr-RecoveryMachine(); } int main() { signal(SIGQUIT, Recovery); Server svr; Control ctrl; ctrl_ptr ctrl; svr.Get(/all_questions, [ctrl](const Request req, Response resp) { std::string html; ctrl.AllQuestions(html); resp.set_content(html, text/html; charsetutf-8); }); 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;charsetutf-8); }); svr.listen(0.0.0.0, 8080); return 0; }架构亮点信号处理服务异常后可一键恢复所有离线编译节点容灾能力强RESTful风格接口页面访问、代码提交完全分离路由正则匹配精准匹配题号接口设计规范5.7 判题完整流水线代码解析Judge函数是整个项目最核心的业务代码串联「接收前端数据→拼接代码→负载分发→获取结果」全流程。void Judge(const std::string number, const std::string in_json, std::string *out_json) { // 1. 获取题目信息资源限制、测试代码 struct Question q; model_.GetOneQuestion(number, q); // 2. 解析前端提交的用户代码 Json::Reader reader; Json::Value in_value; reader.parse(in_json, in_value); std::string code in_value[code].asString(); // 3. 拼接 用户代码 后台测试用例代码 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); // 4. 循环选择可用编译节点 while(true) { int id 0; Machine *m nullptr; if(!load_blance_.SmartChoice(id, m)) break; // 5. HTTP请求编译服务 Client cli(m-ip, m-port); m-IncLoad(); if(auto res cli.Post(/compile_and_run, compile_string, application/json;charsetutf-8)) { if(res-status 200) { *out_json res-body; m-DecLoad(); break; } m-DecLoad(); } else { // 节点请求失败自动下线 load_blance_.OfflineMachine(id); } } }核心设计思想代码拼接机制用户只写解题代码后台自动拼接测试用例实现自动判题负载动态增减请求来临负载1结束负载-1保证负载统计精准故障自动剔除节点挂了立刻下线不会影响整体服务JSON标准化交互前后端、微服务之间数据统一扩展性极强5.8 工具模块全套源码讲解文件/路径/时间/字符串工具工具模块是整个项目的底层基石所有文件读写、路径拼接、唯一文件名生成、字符串解析全部依赖该模块彻底复用代码、消除冗余。// 时间工具生成毫秒时间戳 static uint64_t GetTimeStamp() { struct timeval tv; gettimeofday(tv, nullptr); return tv.tv_sec * 1000 tv.tv_usec / 1000; } // 路径工具统一管理所有临时文件路径 namespace PathUtil { static std::string Src(const std::string name) { return tmp_path name .cpp; } static std::string Exe(const std::string name) { return tmp_path name .exe; } static std::string Stdin(const std::string name) { return tmp_path name .stdin; } static std::string Stdout(const std::string name) { return tmp_path name .stdout; } static std::string CompilerError(const std::string name) { return tmp_path name .compile_error; } } // 文件工具读写文件、清空文件 class FileUtil { public: static bool ReadFile(const std::string file_name, std::string *content, bool keep false) { ifstream in(file_name); if(!in.is_open()) return false; std::string line; while(getline(in, line)) { *content line \n; } in.close(); return true; } static bool WriteFile(const std::string file_name, const std::string content) { ofstream out(file_name); if(!out.is_open()) return false; out.write(content.c_str(), content.size()); out.close(); return true; } }; // 字符串工具分割配置文件字符串 class StringUtil { public: static void SplitString(const std::string str, std::vectorstd::string *res, const std::string sep) { boost::split(*res, str, boost::is_any_of(sep), boost::token_compress_on); } };代码逐段解析时间戳工具基于gettimeofday生成毫秒级唯一ID用于多用户并发场景下的临时文件名杜绝文件覆盖冲突路径工具统一封装将源码、可执行文件、输入输出、编译错误文件路径统一管理后续改路径只需改一处维护性拉满文件读写封装适配题库读取、用户代码写入、结果读取全局统一文件IO规范Boost字符串分割压缩连续空格完美解析questions.list配置文件容错性更高5.9 日志模块源码讲解分级日志核心日志模块不使用printf自定义分级日志精准定位开发、测试、线上问题。#define LOG_LEVEL 4 #define LOG_BASE std::cout [ __FILE__ : __LINE__ ] #if LOG_LEVEL 1 #define LOG_INFO LOG_BASE [INFO] #else #define LOG_INFO #endif #if LOG_LEVEL 2 #define LOG_DEBUG LOG_BASE [DEBUG] #else #define LOG_DEBUG #endif #if LOG_LEVEL 3 #define LOG_WARN LOG_BASE [WARNING] #else #define LOG_WARN #endif #if LOG_LEVEL 4 #define LOG_ERROR LOG_BASE [ERROR] #define LOG_FATAL LOG_BASE [FATAL] #else #define LOG_ERROR #define LOG_FATAL #endif核心亮点通过宏定义实现分级日志开关上线可关闭低级别日志提升性能__FILE__、__LINE__自动打印报错文件与行号排错效率极高区分INFO/DEBUG/WARN/ERROR/FATAL适配不同场景日志输出5.10 Machine负载管理类源码讲解线程安全负载统计负载均衡的数据支撑类实现线程安全的负载增减、负载查询、机器重置。class Machine { public: std::string ip; int port; // 原子变量保证多线程负载增减安全 std::atomicint load; Machine() : port(0), load(0) {} ~Machine() default; // 任务增加负载1 void IncLoad() { load; } // 任务结束负载-1 void DecLoad() { load--; } // 获取当前负载 int Load() { return load; } // 重置负载 void ResetLoad() { load 0; } };关键技术点使用std::atomic 原子变量存储负载无锁实现多线程安全比互斥锁性能更高每次分发任务负载自增、任务结束自减实时精准反映节点压力支持故障恢复后重置负载保证集群状态正常5.11 状态码转中文错误信息源码讲解CodeToDesc负责将系统进程退出信号、错误码转为用户可看懂的中文提示是前端展示报错的核心。std::string CodeToDesc(int code) { std::string desc; switch (code) { case 0: desc 通过; break; case 1: desc 编译失败; break; case SIGSEGV: desc 段错误内存越界/空指针; break; case SIGFPE: desc 浮点数异常除零错误; break; case SIGTIM: case SIGXCPU: desc 运行超时; break; case SIGXFSZ: desc 内存超限; break; default: desc 未知错误; break; } return desc; }核心逻辑精准匹配Linux系统进程信号覆盖超时、超内存、段错误、除零错误、编译失败所有OJ常见问题将底层晦涩的系统信号转化为用户易懂的中文提示体验极佳统一错误输出规范前端无需二次处理5.12 编译子服务 main 函数源码讲解集群节点入口每个编译节点独立运行该服务专门接收主服务的HTTP判题请求执行编译与沙箱运行。#include compile_run.hpp int main() { Server svr; // 唯一接口接收判题任务 svr.Post(/compile_and_run, [](const Request req, Response resp){ std::string result; CompileAndRun(req.body, result); resp.set_content(result, application/json;charsetutf-8); }); // 监听端口等待主服务调度 svr.listen(0.0.0.0, 9090); return 0; }架构设计亮点子服务极度轻量化只保留核心判题接口专注做编译运行职责单一多节点可部署多端口9090/9091/9092轻松实现集群扩容完全解耦主服务子服务挂掉不影响主Web服务运行高可用架构六、项目核心技术亮点6.1 极致安全Linux进程沙箱隔离这是OJ系统的核心难点用户提交的代码是完全不可信的随意执行会导致服务器宕机、资源耗尽。本项目通过fork子进程独立执行 setrlimit资源硬限制 IO重定向隔离彻底杜绝恶意代码风险保证服务器安全稳定运行。6.2 分布式负载均衡支持高并发扩容摒弃单机判题瓶颈采用集群化部署。多台编译节点分担判题压力支持横向扩容自动负载筛选、故障节点自动下线、信号一键恢复集群具备工业级容灾能力。6.3 分层解耦高可维护架构MVC分层架构让代码职责清晰改页面只动View、改题库只动Model、改业务逻辑只动Control、改判题规则只动沙箱模块模块间低耦合极易扩展和迭代。6.4 全流程自动化运维设计自动加载题库、自动管理临时文件、自动统计节点负载、自动故障隔离、自动日志记录无需人工干预实现服务自动化运行。5.2 分布式负载均衡支持高并发扩容摒弃单机判题瓶颈采用集群化部署。多台编译节点分担判题压力支持横向扩容自动负载筛选、故障节点自动下线、信号一键恢复集群具备工业级容灾能力。5.3 分层解耦高可维护架构MVC分层架构让代码职责清晰改页面只动View、改题库只动Model、改业务逻辑只动Control、改判题规则只动沙箱模块模块间低耦合极易扩展和迭代。5.4 全流程自动化运维设计自动加载题库、自动管理临时文件、自动统计节点负载、自动故障隔离、自动日志记录无需人工干预实现服务自动化运行。七、项目不足与优化方向7.1 现有短板题库基于本地文件存储无数据库持久化不支持后台管理增删题目无用户系统不支持登录、提交记录保存、刷题统计负载均衡策略简单无节点心跳检测无法主动探测节点健康状态仅支持C代码判题不兼容Java、Python等主流语言7.2 后续优化方案接入MySQL数据库持久化题目、用户、提交记录数据引入Redis缓存热门题目减少文件IO提升响应速度优化负载均衡算法增加节点心跳上报、健康检测机制扩展沙箱模块支持多语言编译运行前端重构为Vue动态页面增加代码高亮、提交历史、判题对比功能增加定时清理脚本兜底清理残留临时文件6.1 现有短板题库基于本地文件存储无数据库持久化不支持后台管理增删题目无用户系统不支持登录、提交记录保存、刷题统计负载均衡策略简单无节点心跳检测无法主动探测节点健康状态仅支持C代码判题不兼容Java、Python等主流语言6.2 后续优化方案接入MySQL数据库持久化题目、用户、提交记录数据引入Redis缓存热门题目减少文件IO提升响应速度优化负载均衡算法增加节点心跳上报、健康检测机制扩展沙箱模块支持多语言编译运行前端重构为Vue动态页面增加代码高亮、提交历史、判题对比功能增加定时清理脚本兜底清理残留临时文件八、项目整体总结这套负载均衡式在线OJ系统是对C后端开发、Linux系统编程、网络编程、分布式架构设计的全方位实战。相比于普通CRUD项目本项目更贴近后端底层核心原理我们不再只是调用接口而是亲手实现了进程隔离、资源管控、负载分发、服务容灾、动态页面渲染等底层核心能力彻底搞懂了在线判题平台的底层运行逻辑。项目整体架构清晰、代码规范、亮点突出无论是作为课程设计、毕设项目还是面试实战项目都具备极高的含金量能够充分体现C后端与Linux底层开发能力。