一、前言
不知道大家有没有这种痛点,我们公司的服务都是云化的(阿里云、华为云);所以在有些业务场景必须应用到压测技术,所以按照公司惯用流程需要申请虚拟机作为压测机使用,而且还不是一台,可能5台、10台;甚至以上。用完了在让运维回收,领导曰:节约公司成本,虚拟机贵,what fuck?
如果是这种情况的话,大家可能考虑到一个问题,那就是有压测需求要申请机器,申请完后需要手动去搭建机器,然后用完释放。所以再这样的闭环条件下其实大家可以看到里面的很多工作都是可以用自动化去完成的。在这里大家可能又会有另一种想法那就是写一个脚本自动去做这件事情,但是又有另一种问题;脚本还是存在一种局限性,如何让它更自动化?或者说更加白痴化;通过一个小白就知道怎么去做这件事情,而不依赖于操作人需要任何技术,那么自动化平台整合分布式压测机那就是很好的一个产物,将自动化思想打造成一个平台化的应用让用户去操作,好了话不多说开始介绍核心思路
二、操作流程
- 首先用户登录到平台,进行参数配置
录制参数解析:
1.压测名称,根据自己这次压测的业务而定,没有太多含义
2.类型:支持分布式压测、单机压测
3.masterIp:若为分布式压测masterIp作为操控机,若为单机压测masterIp作为负载机
4.slaveIp:若为分布式压测slaveIp作为负载机,若为单机压测此项会前端自动隐藏,无需填
- 录制成功后可以进行修改、删除操作,这个比较简单,大家看下图就行
- 上传操作需要将录制好的jmeter脚本上传,jmx格式文件
- 录制完成之后,点击执行操作,后面自动化帮你部署搭建分布式压测机,并执行压测生成报告
- 压测报告实时查看
这样操作系数就达到白痴化水准,用户无需技术可言即可完成自动化的分布式压测机的搭建以及执行操作,并能够实时获取压测报告
三、设计技术详解
- 首先该平台是一个前后端分离的项目,前端vue、后端springboot
1.前端技术点:vue、axios、elementUI、echarts、mockjs、router、webpack
详情参考github开源地址:https://github.com/henryxiaolovejava/autoplatform-web
2.后端技术点:springweb、aspectj、redis、jwt、mybatisplus、springmvc、log4j2、fastjson、freemarker、javamail、beanmapper
详情参考github开源地址:https://github.com/henryxiaolovejava/autoplatform-backend
- 技术图解
- 代码详解
- 首先点击执行按钮调用后端接口开始执行,先进入后端controller层
@PostMapping("/press/excutePressScript")
public ResponseResult excutePressScript(PressConfigInfoAO pressConfigInfoAO) {
//调用service层处理逻辑
Boolean isSuccess = pressConfigBaseService.executePressScript(pressConfigInfoAO);
if (isSuccess) {
return new ResponseResult(ResponseCodeEnums.SUCCESS);
}
return new ResponseResult(ResponseCodeEnums.FAIL);
}
- 调用service层
@Resource
private PressConfigInfoMapper pressConfigInfoMapper;
@Resource
private PressExecuteRecordMapper pressExecuteRecordMapper;
@Resource
private AsyncExecuteShellOperation shellOperation;
public Boolean executePressScript(PressConfigInfoAO pressConfigInfoAO) {
QueryWrapper<PressConfigInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("id", pressConfigInfoAO.getId());
//从mysql找到执行该条数据的配置信息
PressConfigInfo configInfo = pressConfigInfoMapper.selectOne(queryWrapper);
//若该配置属于脚本jmx未上传状态,则抛出异常无法继续往下执行
if (configInfo.getScriptStatus() == BaseInfoConstant.NUMBER_ZERO) {
throw new BizException(ResponseCodeEnums.FAIL);
}
//校验压测记录表是否有未跑完的case
QueryWrapper<PressExecuteRecord> recordWrapper = new QueryWrapper<>();
recordWrapper.eq("press_id",pressConfigInfoAO.getId());
recordWrapper.orderByDesc("ctime").last("limit 1");
PressExecuteRecord record1 = pressExecuteRecordMapper.selectOne(recordWrapper);
if (!StringUtils.isEmpty(record1)) {
//若存在最近未跑完的case则抛出异常,无法继续往下执行
if (record1.getStatus() == BaseInfoConstant.NUMBER_ZERO){
throw new BizException(ResponseCodeEnums.EXECUTE_ERROR);
}
}
//生成一条压测记录
PressExecuteRecord record = new PressExecuteRecord();
record.setPressId(pressConfigInfoAO.getId());
record.setPressName(pressConfigInfoAO.getPressName());
record.setMasterIp(pressConfigInfoAO.getMasterIp());
//若为单机压测slaveIp无需设值,反之需要设值
if (pressConfigInfoAO.getType() == BaseInfoConstant.NUMBER_ONE) {
record.setSlaveIp(pressConfigInfoAO.getSlaveIp());
}
record.setType(pressConfigInfoAO.getType());
record.setScriptName(pressConfigInfoAO.getScriptName().trim());
record.setExecutor(pressConfigInfoAO.getExecutor());
boolean isSuccess = pressExecuteRecordMapper.insert(record) > 0 ? true : false;
//异步操作执行shell脚本
shellOperation.executeShellScript(pressConfigInfoAO, record.getId());
if (isSuccess) {
return true;
}
return false;
}
- 异步操作的方法解析
@Slf4j
public class AsyncExecuteShellOperation {
@Value("${execute.shell.script}")
private String shellPath;
@Resource
private PressExecuteRecordMapper pressExecuteRecordMapper;
@Async
public void executeShellScript(PressConfigInfoAO pressConfigInfoAO, Long id) {
List<String> scripts = new ArrayList<>();
StringBuffer hostBuffer = new StringBuffer();
scripts.add(BaseInfoConstant.BASH);
scripts.add(shellPath);
//参数masterIp
String masterIp = pressConfigInfoAO.getMasterIp().trim();
//参数scriptName
String scriptName = pressConfigInfoAO.getScriptName().trim();
int status = 0;
if (pressConfigInfoAO.getType() == BaseInfoConstant.NUMBER_ZERO){
//单机压测
try {
//参数masterIp
scripts.add(masterIp);
//参数slaveIp
scripts.add("null");
scripts.add(scriptName);
String[] executor = scripts.toArray(new String[0]);
//执行shell脚本
Process exec = Runtime.getRuntime().exec(executor);
status = exec.waitFor();
if (status != BaseInfoConstant.NUMBER_ZERO) {
log.error("脚本执行失败");
throw new BizException(ResponseCodeEnums.SCRIPT_ERROR);
}else {
log.info("脚本执行成功");
PressExecuteRecord record = new PressExecuteRecord();
record.setId(id);
record.setStatus(BaseInfoConstant.NUMBER_ONE);
pressExecuteRecordMapper.updateById(record);
}
} catch (Exception e) {
e.printStackTrace();
}
}else{
//分布式压测
try {
//参数hosts
String slaveIps = pressConfigInfoAO.getSlaveIp().trim();
String[] hosts = slaveIps.split(",");
for (String host: hosts){
hostBuffer.append(host).append(":1099").append(",");
}
String newHosts = hostBuffer.substring(0, hostBuffer.length() - 1);
scripts.add(masterIp);
scripts.add(slaveIps);
scripts.add(scriptName);
scripts.add(newHosts);
String[] executor = scripts.toArray(new String[0]);
System.out.println("success");
//执行shell脚本
Process exec = Runtime.getRuntime().exec(executor);
status = exec.waitFor();
if (status != BaseInfoConstant.NUMBER_ZERO) {
log.error("脚本执行失败");
throw new BizException(ResponseCodeEnums.SCRIPT_ERROR);
}else {
log.info("脚本运行成功");
PressExecuteRecord record = new PressExecuteRecord();
record.setId(id);
record.setStatus(BaseInfoConstant.NUMBER_ONE);
pressExecuteRecordMapper.updateById(record);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
在调用后端执行接口的整体逻辑大概是这样,有疑惑的可以参考github源码
- shell脚本执行内容解析
后端执行脚本若为单机压测传入三个参数:masterIp、slaveIp、scriptName(slaveIp默认传null)
若为分布式压测传入是个参数:masterIp、slaveIp、scriptName、hosts
#!/bin/bash
#
masterIp=$1
slaveIp=$2
scriptName=$3
hosts=$4
echo $hosts
#定义基础配置路径
scriptPath=/data/workspace/backend/scriptFiles
packagePath=/data/workspace/backend/package
targetPath=/root/jmeter5/bin
reportPath=/data/workspace/tomcat/tomcat8/webapps/press
reportName=`echo $scriptName | awk -F"." '{print $1}'`
echo $reportName
#单机压测
if [ $slaveIp == "null" ];
then
#单机压测方式
nohup ssh $masterIp "rm -rf /root/jmeter5"
nohup scp -r $packagePath/jmeter5 $masterIp:/root
nohup scp -r $scriptPath/$scriptName $masterIp:$targetPath
nohup ssh $masterIp "cd $targetPath;bash jmeter.sh -n -t $scriptName -l ./result.jtl -e -o ./$reportName"
nohup scp -r $masterIp:$targetPath/$reportName $reportPath
else
#分布式压测
#masterIp部署
#前置处理;删除之前可能存在的配置
nohup ssh $masterIp "rm -rf /root/jmeter5"
#将jmeter文件传到操控机
nohup scp -r $packagePath/jmeter5 $masterIp:/root
#将之前录制的jmx文件传入到操控机
nohup scp -r $scriptPath/$scriptName $masterIp:$targetPath
#初始化操控机的配置,用于操控负载机
nohup ssh $masterIp "cd $targetPath;sed -i 's/remote_hosts=127.0.0.1/remote_hosts=$hosts/g' jmeter.properties"
#slaveIp部署
#获取到slaveIp中的每个IP并对其遍历操作
array=(${slaveIp//\,/ })
for i in "${!array[@]}"; do
slaveIp=`echo ${array[i]}`
echo $slaveIp
#将jmeter文件传到每个负载机
nohup scp -r $packagePath/jmeter5 $slaveIp:/root
ssh $slaveIp "cd $targetPath;nohup bash jmeter-server -Djava.rmi.server.hostname=$slaveIp > /dev/null 2>&1 &"
done
#执行压测
sleep 10
nohup ssh $masterIp "cd $targetPath;bash jmeter.sh -n -t $scriptName -l ./result.jtl -r -e -o ./$reportName"
#报告部署到Tomcat
nohup scp -r $masterIp:$targetPath/$reportName $reportPath
fi
四、优势
- 如果你的公司性质跟我差不多,为了节约资源需要每次申请机器、用完回收,那么这套平台是非常符合你的口味
- 该平台避免了认为频繁操作的苦恼,并且认为操作更加容易出错
- 同样我个人实际比对过,这套平台比人为手动部署执行效率在时间上缩短了至少10倍以上
如果你有同样的痛点,希望我的这篇文章能够帮助到你,如有问题可以私信联系本人,不胜解答!