C++实战——构建高性能负载均衡OJ系统

张开发
2026/4/11 17:17:23 15 分钟阅读

分享文章

C++实战——构建高性能负载均衡OJ系统
1. 项目背景与核心需求在线判题系统Online Judge简称OJ是程序员刷题、竞赛和面试准备的必备工具。像LeetCode、牛客网这样的平台每天要处理海量代码提交请求这对系统的并发能力和稳定性提出了极高要求。传统单机架构在面对突发流量时常常出现服务崩溃这正是我们需要引入负载均衡技术的关键原因。我去年参与重构某教育机构的OJ系统时曾亲眼目睹单台服务器在高峰期CPU飙升至100%后整个服务瘫痪的场景。后来我们用C重构的负载均衡方案成功将吞吐量提升了8倍。本文将分享如何从零构建一个高性能的负载均衡OJ系统重点解析三个核心技术点多主机动态调度通过轮询最小负载算法自动分配判题任务资源隔离机制使用rlimit严格限制每个判题进程的CPU和内存占用无状态服务设计通过临时文件清理和唯一ID生成确保每次判题环境纯净2. 系统架构设计2.1 整体架构图[浏览器] ←HTTP→ [OJ_Server] ←HTTP→ [Compile_Server集群] ↑ ↑ [MySQL] [负载均衡器]2.2 核心组件说明前端层采用ACE代码编辑器插件支持语法高亮OJ_Server主控服务处理用户请求和负载调度Compile_Server编译集群节点实际执行代码编译运行MySQL存储题目数据和判题结果2.3 关键数据结构// 主机节点信息 struct Machine { std::string ip; int port; std::atomicuint64_t load; std::mutex* mtx; }; // 题目元信息 struct Question { int number; std::string header_code; // 预设代码模板 std::string test_case; // 测试代码 int cpu_limit; // 秒级时间限制 int mem_limit; // KB级内存限制 };3. 负载均衡实现3.1 主机管理策略在LoadBalance类中维护三个核心列表std::vectorMachine machines; // 所有主机 std::vectorint online; // 在线主机ID std::vectorint offline; // 离线主机ID通过定期心跳检测实现主机状态维护这里给出一个简易实现void HealthCheck() { for (int id : online) { httplib::Client cli(machines[id].ip, machines[id].port); auto res cli.Get(/health); if (!res || res-status ! 200) { OfflineMachine(id); // 移入离线列表 } } }3.2 智能调度算法采用最小负载轮询的混合策略实测比纯轮询减少30%的响应延迟bool SmartChoice(int* id, Machine** m) { std::lock_guardstd::mutex lock(mtx); if (online.empty()) return false; // 第一轮找最小负载 *id online[0]; *m machines[*id]; uint64_t min_load (*m)-load; for (size_t i 1; i online.size(); i) { uint64_t curr_load machines[online[i]].load; if (curr_load min_load) { min_load curr_load; *id online[i]; *m machines[*id]; } } // 第二轮随机选择同等负载主机 std::vectorint candidates; for (size_t i 0; i online.size(); i) { if (machines[online[i]].load min_load) { candidates.push_back(online[i]); } } if (!candidates.empty()) { *id candidates[rand() % candidates.size()]; *m machines[*id]; } return true; }4. 编译执行模块4.1 安全沙箱设计通过Linux的rlimit机制实现资源隔离void SetProcessLimit(int cpu_limit, int mem_limit) { rlimit cpu; cpu.rlim_cur cpu_limit; cpu.rlim_max RLIM_INFINITY; setrlimit(RLIMIT_CPU, cpu); rlimit mem; mem.rlim_cur mem_limit * 1024; // 转换为字节 mem.rlim_max RLIM_INFINITY; setrlimit(RLIMIT_AS, mem); }4.2 编译执行流程临时文件管理为每次提交创建唯一工作目录编译阶段通过forkexec调用g运行阶段重定向标准输入输出到临时文件关键代码示例int CompileAndRun(const std::string code, int cpu_limit, int mem_limit, std::string* output) { // 生成唯一文件名 std::string filename UniqFileName(); // 写入用户代码 if (!WriteFile(PathUtil::Src(filename), code)) { return -1; // 写入失败 } // 编译 pid_t pid fork(); if (pid 0) { execlp(g, g, -o, PathUtil::Exe(filename).c_str(), PathUtil::Src(filename).c_str(), -stdc11, nullptr); exit(1); } waitpid(pid, nullptr, 0); // 运行 int status RunProgram(filename, cpu_limit, mem_limit); // 读取输出 FileUtil::ReadFile(PathUtil::Stdout(filename), output); // 清理临时文件 RemoveTempFiles(filename); return status; }5. 性能优化技巧5.1 连接池管理为每个编译节点维护HTTP连接池避免频繁创建销毁连接class ConnectionPool { public: Client* GetClient(const std::string ip, int port) { std::lock_guardstd::mutex lock(pool_mutex_); std::string key ip : std::to_string(port); if (pool_[key].empty()) { return new Client(ip, port); } Client* cli pool_[key].back(); pool_[key].pop_back(); return cli; } void ReleaseClient(Client* cli) { std::lock_guardstd::mutex lock(pool_mutex_); std::string key cli-host : std::to_string(cli-port); pool_[key].push_back(cli); } private: std::unordered_mapstd::string, std::vectorClient* pool_; std::mutex pool_mutex_; };5.2 日志优化方案采用异步日志写入策略避免阻塞主线程void AsyncLog(const std::string message) { static moodycamel::ConcurrentQueuestd::string queue; queue.enqueue(message); static std::thread worker([]{ std::string msg; while (true) { if (queue.try_dequeue(msg)) { WriteToDisk(msg); // 实际写入操作 } else { std::this_thread::yield(); } } }); worker.detach(); }6. 异常处理机制6.1 编译错误捕获通过重定向stderr到临时文件捕获编译错误int _stderr open(PathUtil::CompilerError(filename).c_str(), O_CREAT | O_WRONLY, 0644); dup2(_stderr, STDERR_FILENO);6.2 信号处理针对常见异常信号设置处理函数void SignalHandler(int sig) { switch(sig) { case SIGSEGV: WriteLog(内存越界访问); break; case SIGXCPU: WriteLog(CPU时间超出限制); break; case SIGALRM: WriteLog(运行超时); break; } _exit(1); } // 注册信号处理 signal(SIGSEGV, SignalHandler); signal(SIGXCPU, SignalHandler);7. 部署实践7.1 多节点启动通过不同端口启动多个编译服务实例# 启动三个编译节点 ./compile_server 8081 ./compile_server 8082 ./compile_server 8083 7.2 负载测试结果使用wrk进行压力测试对比单机与集群性能节点数QPS平均延迟CPU使用率112878ms98%335228ms65%551019ms45%测试命令示例wrk -t4 -c100 -d30s http://localhost:8080/judge/18. 常见问题排查问题1编译服务突然不可用检查日志文件/var/log/compile_server.log确认g编译器已安装验证临时目录写入权限问题2负载不均衡检查service_machine.conf配置格式查看各节点load统计是否正常确认健康检查线程正常运行问题3内存泄漏使用valgrind检测valgrind --leak-checkfull ./compile_server 8080这个项目最让我有成就感的是看到系统在高峰期平稳运行的状态。记得有一次在深夜压测时监控大屏上均匀分布的负载曲线让我真切感受到架构设计的力量。建议大家在实现基础功能后一定要亲自做破坏性测试比如随机kill编译节点进程观察系统的自恢复能力。

更多文章