之前在老东家做性能测试的时候,一开始是使用 LR 的(服务端测试)。在 LR 里写 vuser scripts 去进行 RPC 协议的接口性能测试。但是后来觉得 LR 实在太重了,一般的机器消受不了。而且无法自动化的驱动测试。所以后来引入了 Jmeter。但是 Jmeter 也有它的缺点, 尤其是最后结果统计中那蛋疼的报表简直 low 到爆。所以临走前搞了一次二次封装。增强 jmeter 的特性,可惜只做了比较少的一部分我就离职了。 这里我写出来就当抛砖引玉了吧。
其实原理十分简单,我们一般就是用 Jmeter 的 GUI 来完成工作么。现在我们不用 GUI 了,我们直接引用 jmeter 的核心 lib,直接调用 jmeter 的 API 来执行我们的操作。你需要引入 maven 的依赖。如下
<dependency>
<groupId>com.lazerycode.jmeter</groupId>
<artifactId>jmeter-maven-plugin</artifactId>
<version>1.10.1</version>
<exclusions>
<exclusion>
<artifactId>ApacheJMeter_config</artifactId>
<groupId>org.apache.jmeter</groupId>
</exclusion>
</exclusions>
</dependency>
然后运行的时候,你还是需要装一个 Jmeter 的,这个我没绕过去。需要在代码中设置 jmter 的 home。以及一些其他的配置信息
JMeterUtils.setJMeterHome(Constant.getJmeterHome());
JMeterUtils.loadJMeterProperties(Constant.getJmeterHome()
+ "/bin/jmeter.properties");
JMeterUtils.setProperty("jmeter.save.saveservice.output_format", "xml");
File log = new File(Constant.logFilePath() + "perfTest.log");
private static String jmeterHome = "E:\\apache-jmeter-2.13";//jmeter的home目录
现在我们来看看核心代码部分吧
Logger.info("测试方法: "+caseInfo.getName());
Logger.info("起始并发数: "+caseInfo.getBeginThread());
Logger.info("递增并发数: "+caseInfo.getAddThread());
Logger.info("结束并发数: "+caseInfo.getEndThread());
Logger.info("每个线程的循环次数: "+caseInfo.getLoop());
//Logger.html_link("1111", "222");
JMeterUtils.setJMeterHome(Constant.getJmeterHome());
JMeterUtils.loadJMeterProperties(Constant.getJmeterHome()
+ "/bin/jmeter.properties");
JMeterUtils.setProperty("jmeter.save.saveservice.output_format", "xml");
File log = new File(Constant.logFilePath() + "perfTest.log");
JMeterUtils.setProperty(LoggingManager.LOG_FILE, log.getAbsolutePath());
JMeterUtils.initLogging();// you can comment this line out to see extra
// log messages of i.e. DEBUG level
JMeterUtils.initLocale();
// Initialize JMeter SaveService
SaveService.loadProperties();
JavaSampler javaSample = new JavaSampler();
javaSample.setClassname(caseInfo.getName());
// Loop Controller
LoopController loopController = new LoopController();
loopController.addTestElement(javaSample);
loopController.setLoops(caseInfo.getLoop());
loopController.setFirst(true);
loopController.initialize();
// Thread Group
List<ThreadGroup> threadGrouplist = this.setupThreadGroup(caseInfo, loopController);
// JMeter Test Plan, basic all u JOrphan HashTree
TestPlan testPlan = new TestPlan("My Test Plan");
testPlan.setSerialized(true);
// JMeter Test Plan, basic all u JOrphan HashTree
HashTree testPlanTree = new HashTree();
// Construct Test Plan from previously initialized elements
testPlanTree.add("TestPlan", testPlan);
testPlanTree.add("LoopController", loopController);
testPlanTree.add("JavaSampler", javaSample);
int count = 1000;
for(ThreadGroup threadGroup:threadGrouplist){
testPlanTree.add("ThreadGroup"+count,threadGroup);
count--;
}
// Store execution results
MyResultCollector requestCollector = new MyResultCollector();
requestCollector.setFilename(Constant.logFilePath()+"result.log");
testPlanTree.add(testPlanTree.getArray()[0], requestCollector);
// Run Test Plan
StandardJMeterEngine jmeterEngine = new StandardJMeterEngine();
jmeterEngine.configure(testPlanTree);
jmeterEngine.run();
Map<Integer,List<SampleResult>> sampleResultsMap = requestCollector.getSampleResults();
for (Object key : sampleResultsMap.keySet()) {
List<SampleResult> list = sampleResultsMap.get(key);
/*for(SampleResult s : list){
System.out.println(s.getTime()+"fffff");
}*/
System.out.println(list.size()+"kkkkkkkkkkk");
}
PerfChartHelper.createChart_Jmeter(1, sampleResultsMap,caseInfo.getName());
可以看到,我们在代码中设置循环,设置线程组,测试计划等等,熟悉 jmeter 的同学一定熟悉这些概念。其中我们测试的 java 的接口么。所以用的是 javasimpler
JavaSampler javaSample = new JavaSampler();
javaSample.setClassname(caseInfo.getName());
其中需要给 sample 传一个测试类(如果是 http 接口,其实就不需要了)。下面我们看看这个测试类是怎么定义的。
public class TestP extends AbstractJavaSamplerClient {
private static AtomicInteger temp = new AtomicInteger();
@Override
public Arguments getDefaultParameters() {
System.out.println("===========init parameters ========");
Arguments arg = new Arguments();
return arg;
}
public SampleResult runTest(JavaSamplerContext paramJavaSamplerContext) {
SampleResult sr = new SampleResult();
sr.sampleStart();
//System.out.println(123);
//此处是调用逻辑
sr.setSuccessful(true);
sr.sampleEnd();
return sr;
}
}
可以看到我们只要继承 jmeter 定义的类 AbstractJavaSamplerClient 就可以了。 jmeter 也是有集合点这个概念的。即便没有,现在你都能调用它的 API 了,你自己写一个实现嵌入进去也行。然后我们看结果收集,这里我是自己进行了计算。
public class MyResultCollector extends ResultCollector {
private static final long serialVersionUID = -8648350950445938218L;
//private List<SampleResult> sampleResults;
private Map<Integer,List<SampleResult>> threadResultsMap;
public MyResultCollector() {
threadResultsMap = new HashMap<Integer,List<SampleResult>>();
}
@Override
public void sampleOccurred(SampleEvent e) {
super.sampleOccurred(e);
SampleResult r = e.getResult();
int threadGroupName = Integer.parseInt(e.getThreadGroup());
if(threadResultsMap.containsKey(threadGroupName)){
List<SampleResult> sampleResults = threadResultsMap.get(threadGroupName);
sampleResults.add(r);
}else{
List<SampleResult> sampleResults = new ArrayList<SampleResult>();
sampleResults.add(r);
threadResultsMap.put(threadGroupName, sampleResults);
}
}
public Map<Integer,List<SampleResult>> getSampleResults() {
return threadResultsMap;
}
}
首先是扩展 Jmeter 自己的结果收集器。
public static void createChart_Jmeter(int type, Map<Integer, List<SampleResult>> sampleResultsMap,String className) throws IOException {
ArrayList<double[]> tpsSeries = new ArrayList<double[]>();
ArrayList<double[]> averageSeries = new ArrayList<double[]>();
ArrayList<double[]> minSeries = new ArrayList<double[]>();
ArrayList<double[]> maxSeries = new ArrayList<double[]>();
ArrayList<double[]> time60 = new ArrayList<double[]>();
ArrayList<double[]> time90 = new ArrayList<double[]>();
ArrayList<double[]> time95 = new ArrayList<double[]>();
//用treeSet排序
TreeSet<Integer> tree = new TreeSet<Integer>();
for(int thread : sampleResultsMap.keySet()) {
tree.add(thread);
}
// 遍历每一个threadgroup的结果集,算出tps,平均相应时间并画图
for(int thread : tree) {
AggregatedParser aggregated = new AggregatedParser(sampleResultsMap.get(thread));
double tps = aggregated.getTps();
double averageTime = aggregated.getAverageTime();
long maxTime = aggregated.getMaxTime();
long minTime = aggregated.getMinTime();
long per60Time = aggregated.getPercentTime(0.6);
long per90Time = aggregated.getPercentTime(0.9);
long per95Time = aggregated.getPercentTime(0.95);
double errorRate = aggregated.getErrorRate();
tpsSeries.add(new double[]{thread, tps});
averageSeries.add(new double[]{thread, averageTime});
maxSeries.add(new double[]{thread, maxTime});
minSeries.add(new double[]{thread, minTime});
time60.add(new double[]{thread, per60Time});
time90.add(new double[]{thread, per90Time});
time95.add(new double[]{thread, per95Time});
Logger.info("在"+thread+"并发下错误率:"+errorRate);
}
Map<String, ArrayList<double[]>> timeMap = new HashMap<String, ArrayList<double[]>>();
timeMap.put("average time", averageSeries);
timeMap.put("min time", minSeries);
timeMap.put("max time", maxSeries);
timeMap.put("60% time", time60);
timeMap.put("90% time", time90);
timeMap.put("95% time", time95);
JfreeChart tpsChart = new JfreeChart("thread_tps", "thread", "tps");
tpsChart.addXYSeries("thread_tps_report", tpsSeries);
File folder = new File(Constant.imagePath()+className);
folder.mkdirs();
File imgTps = new File(Constant.imagePath()+className, "_tps.png");
tpsChart.createJfreeChartImage(imgTps, 800, 600);
//在html report中增加图片链接
//Logger.html_img(Constant.imagePath()+className+"/_tps.png");
//echarts_Qps(tpsSeries);
JfreeChart avgTimeChart = new JfreeChart("avgTime", "thread", "avg_time");
avgTimeChart.addXYSeries("thread_avgTime", averageSeries);
File imgAvgTime = new File(Constant.imagePath()+className, "_avg.png");
avgTimeChart.createJfreeChartImage(imgAvgTime, 800, 600);
//在html report中增加图片链接
//Logger.html_img(Constant.imagePath()+className+"/_avg.png");
//使用echart在html页面中花出tps和平均响应时间的图
Integer id = tempCount.getAndAdd(1);
Logger.html("<div id=\"chart_qps"+id+"\" style=\"width:800px;height:600px;\"></div>");
Logger.html("<div id=\"chart_time"+id+"\" style=\"width:800px;height:600px;\"></div>");
Logger.html("<script>");
echarts_Qps(tpsSeries,id);
echarts_Time(timeMap,id);
Logger.html("</script>");
然后增加结果收集算法。并画出图来。我分别用 jfreechart 和 echart 画图。
然后我们一下 case 控制
<suite name="rock" >
<paras>
<para host_ip="10.9.20.171"></para>
</paras>
<cases>
<case name="performance.TestP" run="false">
<perf begin_thread="100" end_thread="200" add_thread="20" loop="10"/>
<para desc="根据订单ID查询订单"></para>
</case>
<case name="performance.TestP" run="false">
<perf begin_thread="100" end_thread="200" add_thread="20" loop="10"/>
<para desc="根据订单ID查询订单"></para>
</case>
</cases>
</suite>
case 里有我们想要控制的信息,例如起始的并发数,每次递增的并发数。结束的并发数等等。
其中 name 就是我们的测试类的全路径(我这里都是我自己写的 demo 类)。
现在我们来看看效果图吧,就是我们最后的结果
以及
我之前想实现更多的功能,例如可以写个连接池让 Jmeter 也可以像 LR 一样对数据库做参数化,加入时间控制等等。当初也就是按着 LR 的功能去设想的。只可惜才开始做就离职了。新公司是 To B 的业务,性能压力根本不在高并发上,就用不上这一套了。不知道老东家的小伙伴们有没有把这个想法发扬光大。当初希望的是把性能测试也加到 CI 中去,可惜我是没这个机会了。