文章目录
- 一、绪论
- 1.项目背景及意义
- 2.在线OJ系统概述
- 二、项目功能
- 1.概述
- 2.详细介绍
- 2.1编译运行服务
- 2.1.1编译服务
- 2.1.2运行服务
- 2.2负载均衡服务及主服务器搭建
- 2.2.1主机对象的设计
- 2.2.2负载均衡模块的设计
- 2.2.3主服务器搭建
- 2.3通用模块
- 2.3.1日志模块
- 2.3.2Session模块
- 2.3.3数据库连接池
- 2.3.4时间工具
- 2.3.5路径生成工具类
- 2.3.6文件工具类
- 2.3.7字符串切割工具类
- 三、具体测试
- 1.功能测试
- 1.1用户管理功能
- 1.2题目管理功能
- 1.3提交与评测功能
- 2.自动化测试
- 2.1自动化测试脚本编写
- 2.1.1注册测试
- 2.1.2登录测试
- 2.1.3题目管理功能测试
- 2.1.4提交与评测功能测试
- 2.2自动化测试结果
- 3.性能测试
- 3.1测试结果总览
- 3.2 注册模块的瓶颈
- 3.3 吞吐量趋势
- 四、测试总结
- 1. 测试结果概述
- 2. 问题与建议
- 问题 1:注册功能响应过慢
- 问题 2:吞吐量波动
- 3. 后续工作计划
一、绪论
1.项目背景及意义
随着在线编程教育的普及和编程竞赛的增多,越来越多的用户需要高效、稳定的在线编程环境来练习和提升自己的编程能力。并且传统的单服务器架构已无法满足大量用户同时在线提交代码并进行评测的需求。
因此,负载均衡技术被引入到在线OJ系统中,以解决高并发访问和服务器资源分配不均等问题。
2.在线OJ系统概述
在线OJ(Online Judge)系统是一种面向程序员的在线练习平台,旨在提供高效、稳定且可靠的在线编程环境。它支持多种编程语言,拥有丰富的编程题库和在线评测机制,使得用户能够进行编程练习、竞赛和交流。这类系统通常应用于编程教育、竞赛培训以及企业招聘中的编程能力测试等环节。
二、项目功能
1.概述
负载均衡式在线OJ系统是一个旨在处理大量编程问题提交,并通过负载均衡机制分配判题任务到多台服务器上的在线OJ系统,系统分为三个模块:编译运行服务模块、负载均衡服务及网站搭建模块、通用工具模块。
在编译运行服务中我采用创建子进程让子进程程序替换编译程序或可执行程序并将标准流重定向到指定本地临时文件,未来从本地临时文件读取编译运行结果。
在负载均衡服务及网站搭建模块中我采用了MVC的设计模式,其中 control
层是核心业务逻辑,model
层负责进行数据交互,view
层负责生成网页,负载均衡策略为轮询检测。
通用工具模块包括日志类、Session管理类、数据库连接池、时间工具类、路径生成工具类、文件工具类和字符串切割工具类等。
2.详细介绍
2.1编译运行服务
编译运行服务从功能上可以分为两部分:编译服务、运行服务。
2.1.1编译服务
编译服务主要工作是将用户代码进行编译得到可执行程序,而编译动作服务器程序无法完成,所以我需要创建子进程并进行程序替换,让编译程序完成编译动作,对于C++代码来说,该编译程序就是g++,编译过程中的标准输出、错误流需要重定向到设定好的文件中,后续我需要根据文件内容返回给用户编译结果。最后检测是否生成了可执行程序,如果成功生成了可执行程序,则编译成功。
2.1.2运行服务
运行服务主要工作是运行可执行程序,同上,我需要创建子进程并进行程序替换,执行可执行程序,执行过程中的标准输出、错误流需要重定向到设定好的文件中,后续我需要根据文件内容返回给用户运行结果。
负载均衡式在线OJ系统允许存在多个编译运行服务,即我可以将编译运行服务部署在多个服务器上,未来仅需在主服务器上进行配置(可采用配置文件方式),主服务器通过配置文件中的IP和端口号即可调用编译运行服务。
2.2负载均衡服务及主服务器搭建
2.2.1主机对象的设计
首先对于负载均衡服务,基于面向对象的编程思想,我首先需要将编译运行服务设计为一个主机对象,那么对于一个编译运行服务来说,我首先需要一个值来表示主机的负载,需要IP和端口号来标识一个主机,由于http-lib
库是基于多线程的,所以我还需要考虑负载值的线程安全问题,至此我明确一个主机对象需要的属性为:string _ip
、int _port
、uint64_t _load
、mutex *_mutex
(考虑传参过程,mutex
禁止拷贝,使用指针)。
对于一个主机对象来说,我需要IncLoad
方法来增加负载值,DecLoad
方法来降低负载值,以及相应的get
、set
方法等。
2.2.2负载均衡模块的设计
负载均衡模块我采用STL库中的vector容器来管理主机对象,并且维护两个vector,用来个记录当前在线和离线的主机,而负载均衡模块无疑需要选择一个负载最低的主机,所以在选择这一过程中,需要考虑线程安全,至此我明确一个负载均衡模块需要的属性为:vector<Machine> machines
、vector<int> online
、vector<int> offline
、mutex mtx
。
对于一个负载均衡模块来说,我需要LoadConf
方法来读取配置文件,以便获取所有提供编译运行服务的主机,SmartChoice
方法来轮询检测获取负载最低的主机,OfflineMachine
方法来将主机对象从在线容器中下线,并加入到离线容器中,OnlineMachine
方法来上线所有主机,ShowMachines
方法来调试查看当前主机列表信息。
2.2.3主服务器搭建
主服务器搭建我采用MVC的设计模式,即M(Model)数据交互、V(View)视图、C(Control)核心业务逻辑。
(1)Model层
首先对于Model层,基于面向对象的编程思想,我要将数据封装成为一个对象,以便后面更好的操作,而这里我需要与数据库交互的信息是一个编程问题,基于此我设计问题对象如下:
与之对应的数据库表设计:
那么对于一个Model
层来讲,为了在高并发场景下更高效的与数据库进行数据交互,这里我实现了一个数据库连接池用来管理数据库连接,未来通过该连接池获取连接进而实现数据交互。
所以Model
层仅需要一个单例连接池对象即可:ConnPool *_connPool
。
对于Model
层来说,功能上需要实现四个接口,与用户管理相关的登陆注册功能的数据交互insert
方法和login
方法,同时我需要对请求中的密码利用hashPassword
方法进行SHA-256加密,与问题列表展示相关的数据交互GetAllQuestions
方法和GetOneQuestion
方法。在Model
的构造函数中创建这个数据库连接池单例对象即可。
(2)View层
其次对于View层,我采用了ctemplate
库渲染html网页,通过数据字典将数据渲染到html网页中。
ctemplate
库渲染html
网页的方式简要介绍如下:
HTML网页中的处理:
{{#question_list}}
和{{/question_list}}
是模板的起始和结束标记,表示这里是一个循环,用于遍历名为question_list
的数据集合。- 在循环内部,对于
question_list
中的每一个元素,即问题对象,都会生成一个<tr>
表格行。 {{number}}
会被替换为当前问题对象的number
属性值,显示问题的编号。{{title}}
会被替换为当前问题对象的title
属性值,显示问题的标题。并且这个标题被包裹在一个<a>
标签中,其href
属性设置为/question/{{number}}
,意味着点击这个链接会跳转到该问题的详情页面。{{star}}
会被替换为当前问题对象的star
属性值,显示与该问题相关的星级或评分。
View层中的处理:
对于questions
列表中的每个问题,我都创建了一个新的TemplateDictionary
对象,并使用SetValue
函数来设置问题的属性。这些属性将在模板文件中被引用,以生成最终的HTML输出。
(3)Control层
最后对于Control
层,Control
层通常是服务器的核心逻辑,而Model
层和View
层中提供的方法基本都是提供给Control
层调用的,从功能角度上分析,服务器的核心业务有渲染问题列表界面、渲染代码编辑界面、用户注册、用户登录等。
其中渲染问题列表界面我通过AllQuestions
方法完成,该方法首先需要验证用户的sesson
信息,即登录状态,校验完成后允许用户继续访问页面,由于需要问题列表信息,所以我需要调用Model
层提供的GetAllQuestions
方法,并将获取到的问题列表信息通过View层的AllExpandHtml
方法渲染出html
界面,之后我刷新session
过期时间。
渲染代码编辑界面我通过OneQuestion
方法完成,该方法同样需要验证用户的sesson
信息,校验完成后允许用户继续访问页面,由于需要具体问题信息,所以我需要调用Model
层提供的GetOneQuestion
方法,并将获取到的问题列表信息通过View
层的OneExpandHtml
方法渲染出html
界面,之后我刷新session
过期时间为长session
,这里由于用户需要进行代码编辑,所以我提供给用户更长的session
生命周期,以便用户顺利完成代码编辑。
代码编辑界面提供了判题功能,这背后需要编译运行服务和负载均衡模块的支持,首先我们同样需要通过Model层提供的GetOneQuestion
方法获取题目关键信息,然后将关键信息形成Json
,利用负载均衡模块选择合适的主机执行编译运行服务,最后将响应数据展示到前端界面。
用户注册功能很简单,就是前端发起请求,后端解析请求获取用户名密码信息,然后调用Model
层的insert
方法将用户插入到数据库中,最后构建响应返回。
用户登录功能同上,但需要进行session
信息的处理,即如果登录成功,需要给用户创建一个session
,并设置session
的过期时间。
2.3通用模块
2.3.1日志模块
日志需要设定等级划分,这里我使用枚举变量设置了五种日志等级INFO
、DEBUG
、WARNNING
、ERROR
、FATAL
。
并且规定日志格式为:[日志等级][文件名称][行号][时间]日志信息。
调用方式为:LOG(日志等级) << "message"<<std::endl;
。
由于LOG
是一个很常用的并且规模较小的方法,所以我设计该函数为内联函数,建议编译器直接在调用位置展开,以降低函数调用的代价。
2.3.2Session模块
会话管理模块,用于管理和维护用户会话(session)的生命周期和状态。该模块提供了一个完整的会话管理系统,包括会话的创建、更新、删除和过期检查。通过维护一个会话哈希表,它允许快速查找和更新会话信息。此外,通过启动一个后台线程定期检查会话的过期状态,确保了会话的有效性和安全性。
它主要由两个类组成:session
类和session_manager
类。
(1)session类
session
类代表一个用户会话,包含以下主要成员变量和成员函数:
- 成员变量:
_ssid
:会话ID,唯一标识一个会话。_uid
:用户ID,表示该会话对应的用户。_statu
:用户状态,可以是未登录(UNLOGIN
)或已登录(LOGIN
)。_expiryTime
:会话过期时间。
- 成员函数:
- 构造函数和析构函数:用于创建和销毁会话对象,并在日志中记录相应信息。
ssid()
:返回会话ID。set_user(uint64_t uid)
和get_user()
:设置和获取用户ID。set_statu(ss_statu statu)
和is_login()
:设置用户状态和检查用户是否已登录。set_time()
和get_time()
:设置和获取会话过期时间。
(2)session_manager类
session_manager
类是会话管理模块的核心,负责创建、更新、删除和检查会话。它包含以下主要成员变量和成员函数:
- 成员变量:
_next_ssid
:下一个可用的会话ID,用于生成新的会话。_mutex
:互斥锁,用于保护会话数据的并发访问。_session
:存储会话的哈希表,键是会话ID,值是session
对象的智能指针。_sessionLifetime
:会话的默认生命周期,默认为短时间(SESSION_SHORT
)。_running
:标志检查线程是否正在运行。_checkThread
:用于检查会话是否过期的线程。
- 成员函数:
- 构造函数和析构函数:用于创建和销毁会话管理器对象,并在日志中记录相应信息。同时,启动一个线程来定期检查会话是否过期。
create_session(uint64_t uid, ss_statu statu)
:创建一个新的会话,设置用户ID、状态和过期时间,并将其添加到会话哈希表中。get_session_by_ssid(uint64_t ssid)
:通过会话ID获取会话对象。如果会话不存在,返回一个空的智能指针。set_session_expire_time(uint64_t ssid, int mesc)
:根据传入的参数更新会话的过期时间。如果mesc
大于0,则设置为短时间(SESSION_SHORT
);否则,设置为长时间(SESSION_LONG
)。get_ssid_by_uid(const uint64_t &uid)
:通过用户ID查找ssid并返回。append_session(const session_ptr &ssp)
:将一个已存在的会话对象添加到会话哈希表中。remove_session(uint64_t ssid)
:通过会话ID删除会话对象。session_is_exists(uint64_t uid)
:检查是否存在具有指定用户ID的会话。checkSessions()
:一个成员函数,作为线程函数运行,定期检查会话是否过期,并删除过期的会话。
2.3.3数据库连接池
首先我需要一个容器用来管理连接,这里我采用STL标准库中的queue队列来管理连接,在多线程场景下,获取连接的操作需要保证线程安全,所以引入互斥锁,还有一些其他需要的属性。
即数据库连接池对象需要的属性有:int _curSize
、int _maxSize
、string _user
、string _password
、string _url
、queue<sql::Connection *> _connQueue
、mutex _mutex
、sql::Driver *_driver
。
注意:该连接池需要设计为单例模式,即系统中该类只有一个实例。
单例模式的设计简要概述为提供一个全局访问点(GetInstance方法),在GetInstance方法中设计一个局部静态对象,该局部静态对象会在第一次调用GetInstance函数时被创建出来,而又因为该对象是静态的,生命周期为全局,所以利用这种方式也就实现了懒汉模式,注意删除拷贝构造和赋值拷贝函数。
其次我需要获取连接,为了防止内存泄露,我将获取到的连接交给智能指针shared_ptr
管理,这样可以确保连接对象不会因为某种原因没有释放导致内存泄漏,而又因为池化技术需要对连接进行重复利用,那么因此shared_ptr
我需要定制删除器,所以我设计出ReleaseConnection方法,该方法可以将数据库连接对象放回连接池,这里注意获取连接和放回连接的操作需要加锁。
其他还有诸如初始化数据库连接池,即创建一批连接放入到容器中等操作。
2.3.4时间工具
一个简单的C++工具类,用于获取当前时间的不同表示形式。它包含了两个静态成员函数,GetTimeStamp
和 GetTimeMs
,分别用于获取当前时间的秒级时间戳和毫秒级时间戳。
(1)GetTimeStamp 函数
- 功能:获取当前时间的秒级时间戳。
- 实现:使用
gettimeofday
函数填充一个timeval
结构体tv
,该结构体包含了自Epoch(即1970年1月1日00:00:00 UTC)以来的秒数(tv_sec
)和微秒数(tv_usec
)。然后,函数将秒数(tv_sec
)转换为字符串并返回。 - 返回值:返回一个
std::string
类型的字符串,表示当前时间的秒级时间戳。
(2)GetTimeMs 函数
- 功能:获取当前时间的毫秒级时间戳。
- 实现:同样使用
gettimeofday
函数填充timeval
结构体tv
。然后,函数计算毫秒级时间戳,通过将秒数(tv_sec
)乘以1000加上微秒数(tv_usec
)除以1000来实现。最后,将计算结果转换为字符串并返回。 - 返回值:返回一个
std::string
类型的字符串,表示当前时间的毫秒级时间戳。
2.3.5路径生成工具类
主要为编译运行服务生成的临时文件生成路径文件名。
2.3.6文件工具类
主要提供了一系列用于执行与文件相关的常见操作的静态方法,如检查文件是否存在、生成唯一文件名、写入文件和读取文件。
2.3.7字符串切割工具类
该类主要提供一个静态成员函数 SplitString
,该函数的目的是将一个字符串 src
根据指定的分隔符 sep
进行切分,并将切分后的结果存储在提供的 std::vector<std::string>
类型的输出参数 target
中。这个函数使用了 Boost 库中的 boost::split
函数来实现字符串的切分。
三、具体测试
1.功能测试
声明:
- 测试环境下短session过期时间为5秒钟,长session过期时间为10秒钟。为消除误差脚本中短session延迟时间为6秒钟,长session延迟时间为11秒。
- 正式环境下短session过期时间为5分钟,长session过期时间为2小时。
1.1用户管理功能
测试用例:
部分测试结果展示:
1.2题目管理功能
测试用例:
部分测试结果展示:
1.3提交与评测功能
测试用例:
部分测试结果展示:
2.自动化测试
2.1自动化测试脚本编写
本自动化测试脚本采用selenium测试工具编写而成,主要对负载均衡式在线OJ系统做功能测试,测试用例为基于需求的设计方法设计而成,该脚本可分为4个阶段。注册测试、登录测试、题目管理功能测试、提交与评测功能测试,测试结果通过截图方式展现。
2.1.1注册测试
由于注册成功和失败是不相同的需求逻辑,所以这里封装两个方法,success_register方法用来测试正确注册逻辑,fail_register用来测试失败注册逻辑。
- success_register方法
- fail_register方法
2.1.2登录测试
同样的由于登录成功和失败是不相同的需求逻辑,所以这里封装两个方法,success_login方法用来测试正确登录逻辑,fail_login用来测试失败登录逻辑。
- success_login方法
- fail_login方法
2.1.3题目管理功能测试
题目管理功能主要是检测登录与非登录状态下请求页面是否可以得到正确的结果,即登录状态下成功加载题目列表、具体题目信息(代码编辑界面),非登录状态下提示登录过期等信息。
首先进行登录状态下的题目列表展示:
其次是登录状态下的具体题目信息(代码编辑界面)的展示:
此时由于该账号已进入过代码编辑界面,所以该账号的session为长session,这里进行强制等待session_long(10+1s)时间,而后刷新界面,检测session过期情况下是否提示重新登陆:
而后重新登陆,检测session过期时题目列表的展示是否提示重新登陆:
2.1.4提交与评测功能测试
提交与评测功能测试主要检测用户提交代码是否获取到正确结果,包括:
- 登录状态下:
- 正确运行的情况:
- 提交正确代码
- 提交结果错误代码
- 异常运行的情况:
- 提交编译错误代码(非法字符)
- 提交运行错误代码(越界访问)
- 提交空代码(编译错误)
- 正确运行的情况:
- 未登录状态下:
- 提示重新登录
为此我封装出一个用来检测提交与评测功能的方法judge_test:
该方法主要是将代码插入到Ace Editor中的代码编辑区,然后点击提交与评测按钮提交代码。
2.2自动化测试结果
本次自动化测试采用selenium测试工具完成,主要检测负载均衡式在线OJ系统的功能正确性。
测试覆盖3个功能:用户管理、题目展示、提交评测,共产出32张截图,其中测试用户管理生成21张截图,测试题目展示生成5张截图,测试提交评测生成6张截图,覆盖5个界面,基本覆盖了负载均衡式在线OJ系统的常规使用情况,经自动化测试查验截图后得出系统基本功能正常,发现0个BUG。
3.性能测试
Jmeter测试报告连接:百度网盘下载 提取码: 582b
本性能测试通过Jmeter工具完成,共测试了5个接口,分别是注册、登录、获取题目列表、获取具体题目、提交与评测。测试策略为并发梯度测试,每隔10s新增10个线程,最终达到100个线程并发访问。
3.1测试结果总览
模块性能:
响应时间:
3.2 注册模块的瓶颈
注册功能的平均响应时间达到了 17502.20 ms,最大响应时间更是高达 31376 ms,明显高于其他模块,成为性能瓶颈。
3.3 吞吐量趋势
通过时间序列图可以观察到系统的吞吐量保持稳定,但在某些高负载时段,吞吐量略有下降。
四、测试总结
1. 测试结果概述
在本次测试中,对系统的关键性能指标进行了全面评估,包括但不限于响应时间、吞吐量和错误率。测试覆盖了以下3个核心功能模块:用户管理、题目展示、提交评测。
- 响应时间
- 总体表现:系统在大多数情况下响应时间较为稳定。用户登录、题目列表加载、具体题目查看、提交评测等操作在高并发场景下表现良好。
- 例外情况:注册功能在高并发场景下出现性能瓶颈,响应时间显著增加,部分请求达到 30,000ms 以上,影响用户体验。
- 吞吐量
系统在高负载条件下表现出良好的吞吐能力,能够有效处理大量并发请求。尽管在某些时段出现小幅波动,但整体保持稳定。 - 错误率
系统的错误率维持在 0%,没有出现服务不可用或关键功能故障的情况。
2. 问题与建议
问题 1:注册功能响应过慢
现象:高并发场景下,注册功能响应时间显著增加,部分请求延迟超过合理范围,影响用户体验。
建议:
- 优化数据库写入:对注册模块的数据库操作进行优化,如引入批量写入或缓存机制,减少单次写入的延迟。
- 负载均衡:部署多个后端服务器,使用代理服务器实现负载均衡,将注册请求分发到不同服务器实例上,分担压力。
- 弹性扩展:结合高并发场景,使用云服务动态增加注册服务实例,根据流量动态调整资源。
问题 2:吞吐量波动
现象:在某些高峰时段,系统吞吐量出现一定波动,虽未影响主要功能,但需进一步优化。
建议:
- 优化队列管理:使用消息队列(如 RabbitMQ、Kafka)对请求进行分流处理,减少系统压力峰值。
- 增加缓存:对频繁访问的静态数据(如题目列表、用户信息)进行缓存,减轻数据库查询负担。
3. 后续工作计划
- 针对注册功能的性能瓶颈问题,进行详细分析并实施优化方案:
- 调整数据库结构,优化写操作。
- 部署负载均衡策略,测试性能提升效果。
- 对系统整体吞吐量进行优化:
- 引入消息队列,优化请求处理流程。
- 对静态数据增加缓存,降低高频查询对数据库的压力。
- 制定新的测试计划:
- 增加压力测试用例,覆盖更复杂的并发场景。
- 针对优化后的系统重新测试,确保改进措施有效。