一、使用 Jmeter GUI 进行测试配置
我们在使用Jmeter做性能测试时,通常需要使用 GUI 来配置脚本,生成 jmx 文件,然后使用命令来执行。脚本的配置通常需要有以下几个步骤:
测试计划 → 线程组 → 循环控制器 → Java请求 → 结果统计
通过以上配置生成 jmx 文件后,再使用命令行执行,生成结果文件,如:jmeter -n -t testscript est.jmx -l testresult 1-reslut.jtl
二、运行机制 && 源码分析
- NewDriver 是 Jmeter 程序的入口类,通过反射调用 JMeter 类的 start() 方法;
- JMeter 类的 start() 方法根据不同的命令来执行不同的启动方法,startGui() 和 startNonGui();
- startNonGui() 方法又调用了 runNonGui() 方法来执行脚本;
- HashTree 是 JMeter 执行测试依赖的数据结构,在执行测试之前进行配置测试数据,HashTree将数据组织到一个递归树结构中,并提供了操作该结构的方法;
- StandardJMeterEngine 执行JMeter 测试 ,直接用于本地 GUI 和非 GUI 调用,或者在服务器模式下运行时由 RemoteJMeterEngineImpl 启动;
- JMeterEngine 接口被运行 JMeter的测试类实现,此接口共8个方法,JMeterEngine本质就是一个线程;
源码分析
/** * The main program which actually runs JMeter. * mian方法 * @param args * the command line arguments */ public static void main(String[] args) { if(!EXCEPTIONS_IN_INIT.isEmpty()) { System.err.println("Configuration error during init, see exceptions:"+exceptionsToString(EXCEPTIONS_IN_INIT)); } else { Thread.currentThread().setContextClassLoader(loader); setLoggingProperties(args); try { // 加载JMeter类 Class<?> initialClass = loader.loadClass("org.apache.jmeter.JMeter"); // 获取JMeter类实例 Object instance = initialClass.getDeclaredConstructor().newInstance(); // 获取start方法类型实例 Method startup = initialClass.getMethod("start", new Class[] { new String[0].getClass() }); // 反射调用JMeter类的start方法 startup.invoke(instance, new Object[] { args }); } catch(Throwable e){ // NOSONAR We want to log home directory in case of exception e.printStackTrace(); // NOSONAR No logger at this step System.err.println("JMeter home directory was detected as: "+JMETER_INSTALLATION_DIRECTORY); } } }
很明显,这里是通过反射来调用了 JMeter 类的 start() 方法。我们再来看看 JMeter 类的 start() 方法:
/** * Takes the command line arguments and uses them to determine how to * startup JMeter. * 根据命令行执行不同的操作 * 主要功能为:1)startgui 2)startnogui * * Called reflectively by {@link NewDriver#main(String[])} * @param args The arguments for JMeter */ public void start(String[] args) { // 解析命令号参数的类 CLArgsParser parser = new CLArgsParser(args, options); // 错误信息 String error = parser.getErrorString(); if (error == null){// Check option combinations 检查选项组合 boolean gui = parser.getArgumentById(NONGUI_OPT)==null; boolean nonGuiOnly = parser.getArgumentById(REMOTE_OPT)!=null || parser.getArgumentById(REMOTE_OPT_PARAM)!=null || parser.getArgumentById(REMOTE_STOP)!=null; if (gui && nonGuiOnly) { error = "-r and -R and -X are only valid in non-GUI mode"; } } // 输出错误信息 if (null != error) { System.err.println("Error: " + error);//NOSONAR System.out.println("Usage");//NOSONAR System.out.println(CLUtil.describeOptions(options).toString()); // repeat the error so no need to scroll back past the usage to see it System.out.println("Error: " + error);//NOSONAR return; } try { // 初始化配置,同时初始化JMeter日志 initializeProperties(parser); // Also initialises JMeter logging Thread.setDefaultUncaughtExceptionHandler( (Thread t, Throwable e) -> { if (!(e instanceof ThreadDeath)) { log.error("Uncaught exception: ", e); System.err.println("Uncaught Exception " + e + ". See log file for details."); } }); if (log.isInfoEnabled()) { log.info(JMeterUtils.getJMeterCopyright()); log.info("Version {}", JMeterUtils.getJMeterVersion()); log.info("java.version={}", System.getProperty("java.version"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("java.vm.name={}", System.getProperty("java.vm.name"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("os.name={}", System.getProperty("os.name"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("os.arch={}", System.getProperty("os.arch"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("os.version={}", System.getProperty("os.version"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("file.encoding={}", System.getProperty("file.encoding"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("Max memory ={}", Runtime.getRuntime().maxMemory()); log.info("Available Processors ={}", Runtime.getRuntime().availableProcessors()); log.info("Default Locale={}", Locale.getDefault().getDisplayName()); log.info("JMeter Locale={}", JMeterUtils.getLocale().getDisplayName()); log.info("JMeterHome={}", JMeterUtils.getJMeterHome()); log.info("user.dir ={}", System.getProperty("user.dir"));//$NON-NLS-1$ //$NON-NLS-2$ log.info("PWD ={}", new File(".").getCanonicalPath());//$NON-NLS-1$ log.info("IP: {} Name: {} FullName: {}", JMeterUtils.getLocalHostIP(), JMeterUtils.getLocalHostName(), JMeterUtils.getLocalHostFullName()); } setProxy(parser); updateClassLoader(); if (log.isDebugEnabled()) { String jcp=System.getProperty("java.class.path");// $NON-NLS-1$ String[] bits = jcp.split(File.pathSeparator); log.debug("ClassPath"); for(String bit : bits){ log.debug(bit); } } // Set some (hopefully!) useful properties 设置属性 long now=System.currentTimeMillis(); JMeterUtils.setProperty("START.MS",Long.toString(now));// $NON-NLS-1$ Date today=new Date(now); // so it agrees with above JMeterUtils.setProperty("START.YMD",new SimpleDateFormat("yyyyMMdd").format(today));// $NON-NLS-1$ $NON-NLS-2$ JMeterUtils.setProperty("START.HMS",new SimpleDateFormat("HHmmss").format(today));// $NON-NLS-1$ $NON-NLS-2$ // 判断 if (parser.getArgumentById(VERSION_OPT) != null) { displayAsciiArt(); } else if (parser.getArgumentById(HELP_OPT) != null) { displayAsciiArt(); System.out.println(JMeterUtils.getResourceFileAsText("org/apache/jmeter/help.txt"));//NOSONAR $NON-NLS-1$ } else if (parser.getArgumentById(OPTIONS_OPT) != null) { displayAsciiArt(); System.out.println(CLUtil.describeOptions(options).toString());//NOSONAR } else if (parser.getArgumentById(SERVER_OPT) != null) { // Start the server 启动服务 try { RemoteJMeterEngineImpl.startServer(RmiUtils.getRmiRegistryPort()); // $NON-NLS-1$ startOptionalServers(); } catch (Exception ex) { System.err.println("Server failed to start: "+ex);//NOSONAR log.error("Giving up, as server failed with:", ex); throw ex; } } else { String testFile=null; CLOption testFileOpt = parser.getArgumentById(TESTFILE_OPT); if (testFileOpt != null){ testFile = testFileOpt.getArgument(); if (USE_LAST_JMX.equals(testFile)) { testFile = LoadRecentProject.getRecentFile(0);// most recent } } CLOption testReportOpt = parser.getArgumentById(REPORT_GENERATING_OPT); if (testReportOpt != null) { // generate report from existing file 从现有文件生成报告 String reportFile = testReportOpt.getArgument(); extractAndSetReportOutputFolder(parser, false); ReportGenerator generator = new ReportGenerator(reportFile, null); generator.generate(); } else if (parser.getArgumentById(NONGUI_OPT) == null) { // not non-GUI => GUI // 在GUI模式执行 startGui(testFile); startOptionalServers(); } else { // NON-GUI must be true 必须为无GUI模式 extractAndSetReportOutputFolder(parser, deleteResultFile); CLOption rem = parser.getArgumentById(REMOTE_OPT_PARAM); if (rem == null) { rem = parser.getArgumentById(REMOTE_OPT); } CLOption jtl = parser.getArgumentById(LOGFILE_OPT); String jtlFile = null; if (jtl != null) { jtlFile = processLAST(jtl.getArgument(), ".jtl"); // $NON-NLS-1$ } CLOption reportAtEndOpt = parser.getArgumentById(REPORT_AT_END_OPT); if(reportAtEndOpt != null && jtlFile == null) { throw new IllegalUserActionException( "Option -"+ ((char)REPORT_AT_END_OPT)+" requires -"+((char)LOGFILE_OPT )+ " option"); } // 非GUI模式执行 startNonGui(testFile, jtlFile, rem, reportAtEndOpt != null); startOptionalServers(); } } } catch (IllegalUserActionException e) {// NOSONAR System.out.println("Incorrect Usage:"+e.getMessage());//NOSONAR System.out.println(CLUtil.describeOptions(options).toString());//NOSONAR } catch (Throwable e) { // NOSONAR log.error("An error occurred: ", e); System.out.println("An error occurred: " + e.getMessage());//NOSONAR // FIXME Should we exit here ? If we are called by Maven or Jenkins System.exit(1); } }
JMeter类的 start() 方法根据不同的命令来执行启动方法,分别为startGui() 和 startNonGui(),我们来看一下非 GUI 启动方法:
private void startNonGui(String testFile, String logFile, CLOption remoteStart, boolean generateReportDashboard) throws IllegalUserActionException, ConfigurationException { // add a system property so samplers can check to see if JMeter // is running in NonGui mode System.setProperty(JMETER_NON_GUI, "true");// $NON-NLS-1$ JMeter driver = new JMeter();// TODO - why does it create a new instance? driver.remoteProps = this.remoteProps; driver.remoteStop = this.remoteStop; driver.deleteResultFile = this.deleteResultFile; PluginManager.install(this, false); String remoteHostsString = null; if (remoteStart != null) { remoteHostsString = remoteStart.getArgument(); if (remoteHostsString == null) { remoteHostsString = JMeterUtils.getPropDefault( "remote_hosts", //$NON-NLS-1$ "127.0.0.1");//NOSONAR $NON-NLS-1$ } } if (testFile == null) { throw new IllegalUserActionException("Non-GUI runs require a test plan"); } // 运行场景 driver.runNonGui(testFile, logFile, remoteStart != null, remoteHostsString, generateReportDashboard); }
startNonGui() 方法种通过调用 runNonGui() 方法来执行脚本,我们来看一下 runNonGui() 方法:
// run test in batch mode 批处理运行测试 private void runNonGui(String testFile, String logFile, boolean remoteStart, String remoteHostsString, boolean generateReportDashboard) { try { // 获取脚本文件 File f = new File(testFile); if (!f.exists() || !f.isFile()) { println("Could not open " + testFile); return; } // 设置脚本文件 FileServer.getFileServer().setBaseForScript(f); HashTree tree = SaveService.loadTree(f); @SuppressWarnings("deprecation") // Deliberate use of deprecated ctor JMeterTreeModel treeModel = new JMeterTreeModel(new Object());// NOSONAR Create non-GUI version to avoid headless problems JMeterTreeNode root = (JMeterTreeNode) treeModel.getRoot(); treeModel.addSubTree(tree, root); // Hack to resolve ModuleControllers in non GUI mode SearchByClass<ReplaceableController> replaceableControllers = new SearchByClass<>(ReplaceableController.class); tree.traverse(replaceableControllers); Collection<ReplaceableController> replaceableControllersRes = replaceableControllers.getSearchResults(); for (ReplaceableController replaceableController : replaceableControllersRes) { replaceableController.resolveReplacementSubTree(root); } // Ensure tree is interpreted (ReplaceableControllers are replaced) // For GUI runs this is done in Start.java // 将测试文件(.jmx文件)解析成HashTree HashTree clonedTree = convertSubTree(tree, true); Summariser summariser = null; String summariserName = JMeterUtils.getPropDefault("summariser.name", "");//$NON-NLS-1$ if (summariserName.length() > 0) { log.info("Creating summariser <{}>", summariserName); println("Creating summariser <" + summariserName + ">"); summariser = new Summariser(summariserName); } ResultCollector resultCollector = null; if (logFile != null) { resultCollector = new ResultCollector(summariser); resultCollector.setFilename(logFile); clonedTree.add(clonedTree.getArray()[0], resultCollector); } else { // only add Summariser if it can not be shared with the ResultCollector if (summariser != null) { clonedTree.add(clonedTree.getArray()[0], summariser); } } if (deleteResultFile) { SearchByClass<ResultCollector> resultListeners = new SearchByClass<>(ResultCollector.class); clonedTree.traverse(resultListeners); Iterator<ResultCollector> irc = resultListeners.getSearchResults().iterator(); while (irc.hasNext()) { ResultCollector rc = irc.next(); File resultFile = new File(rc.getFilename()); if (resultFile.exists() && !resultFile.delete()) { throw new IllegalStateException("Could not delete results file " + resultFile.getAbsolutePath() + "(canRead:"+resultFile.canRead()+", canWrite:"+resultFile.canWrite()+")"); } } } ReportGenerator reportGenerator = null; if (logFile != null && generateReportDashboard) { reportGenerator = new ReportGenerator(logFile, resultCollector); } // Used for remote notification of threads start/stop,see BUG 54152 // Summariser uses this feature to compute correctly number of threads // when NON GUI mode is used clonedTree.add(clonedTree.getArray()[0], new RemoteThreadsListenerTestElement()); List<JMeterEngine> engines = new LinkedList<>(); clonedTree.add(clonedTree.getArray()[0], new ListenToTest(remoteStart && remoteStop ? engines : null, reportGenerator)); println("Created the tree successfully using "+testFile); if (!remoteStart) { // 实例化一个JMeterEngine来对付脚本,JMeterEngine本质就是一个线程 JMeterEngine engine = new StandardJMeterEngine(); engine.configure(clonedTree); long now=System.currentTimeMillis(); println("Starting the test @ "+new Date(now)+" ("+now+")"); // 调用runTest方法 engine.runTest(); engines.add(engine); } else { java.util.StringTokenizer st = new java.util.StringTokenizer(remoteHostsString, ",");//$NON-NLS-1$ List<String> hosts = new LinkedList<>(); while (st.hasMoreElements()) { hosts.add((String) st.nextElement()); } DistributedRunner distributedRunner=new DistributedRunner(this.remoteProps); distributedRunner.setStdout(System.out); // NOSONAR distributedRunner.setStdErr(System.err); // NOSONAR distributedRunner.init(hosts, clonedTree); engines.addAll(distributedRunner.getEngines()); distributedRunner.start(); } startUdpDdaemon(engines); } catch (Exception e) { System.out.println("Error in NonGUIDriver " + e.toString());//NOSONAR log.error("Error in NonGUIDriver", e); } }
runNonGui() 方法是用来执行脚本,它的主要逻辑包括:
- 获取脚本文件;
- 配置脚本文件;
- 将脚本文件(jmx文件)解析成 HashTree;
- 实例化一个JMeterEngine来配置转化后的 HashTree;
- 调用 runTest() 方法最终运行测试;
简单来说,Jmeter的执行步骤包括以下几步:
- 解析命令行参数,加载配置文件;
- 将 jmx 文件解析为 HashTree;
- 实例化一个 StandardJMeterEngine对象 JMeterEngine,并把测试的工作交给 JMeterEngine;
- JMeterEngine 调用 runTest() 方法来执行测试;
当然,JMeter类还有其他重要的职责,比如监听所有的 JMeterEngine ,当接收到 GUI 的 StopTestNow / Shutdown 等命令时候来调用JMeterEngine接口相应的方法。接下来看下这个接口提供了哪些方法:
/** * This interface is implemented by classes that can run JMeter tests. */ public interface JMeterEngine { /** * Configure engine * 配置引擎 * @param testPlan the test plan */ void configure(HashTree testPlan); /** * Runs the test * 执行测试 * @throws JMeterEngineException if an error occurs */ void runTest() throws JMeterEngineException; /** * Stop test immediately interrupting current samplers * 停止测试,立即打断当前samplers */ default void stopTest() { stopTest(true); } /** * 停止测试,根据参数是否立即打断当前samplers * @param now boolean that tell wether stop is immediate (interrupt) or not (wait for current sample end) */ void stopTest(boolean now); /** * Stop test if running * 停止测试运行 */ void reset(); /** * set Properties on engine * 设置引擎属性 * @param p the properties to set */ void setProperties(Properties p); /** * Exit engine * 退出引擎 */ void exit(); /** * 引擎是否活跃 * @return boolean Flag to show whether engine is active (true when test is running). Set to false at end of test */ boolean isActive(); }
三、Jmeter扩展实现
从上面的分析我们发现:JMeterEngine 依赖于 HashTree,而 HashTree 是由 jmx 文件解析而来,每一个 JMeter 测试计划都会对应一个 jmx 文件。所以我们只要生成合理的 jmx 文件,就可以通过 JMeterEngine 压测引擎去执行测试任务。但是仔细一想发现,我们通过 Jmeter GUI生成一个 jmx 文件,然后 Jmeter 内部又把 jmx 文件解析成了一个 HashTree,再通过 JMeterEngine 来执行,那我们应该能够直接来创建这个 HashTree,并把这个 HashTree需要的内容添加进去,最后就能通过 JMeterEngine 来执行,这样我们就不必去生成 jmx 文件了,可以全部用代码来实现了,如下:
public class JmxCodeDemo { /** * 使用GUI的方式,做完各种配置之后, 会生成一个jmx文件 * * 实际上这个Jmx文件在给到jmeter进程去执行时,JMeter类去负责将jmx文件解析成hashTree * * 这个hashTree的内容就跟我们在GUI中配置的关系是一致的, 各种层次的包裹关系 * * 最终去调StandardJMeterEngine,将配置关系设置进去(jmeterEngine.configure(tree)) * * 最后启动(执行) */ public static void main(String[] args) { // 1、启动初始化配置 URL resourceUrl = getResource("jmeter.properties"); JMeterUtils.loadJMeterProperties(resourceUrl.getPath()); JMeterUtils.setLocale(Locale.ENGLISH); // 2、创建一个测试计划 TestPlan testPlan = new TestPlan(); testPlan.setName("local perf test plan"); // 3、创建一个javaSampler JavaSampler javaSampler = new JavaSampler(); javaSampler.setName("local perf java sampler"); javaSampler.setClassname(CaseDemo.class.getName()); // 4、创建一个循环控制器 LoopController controller = new LoopController(); controller.setName("local perf loop controller"); controller.setLoops(5); controller.addTestElement(javaSampler); controller.initialize(); // 5、创建threadGroup ThreadGroup threadGroup = new ThreadGroup(); threadGroup.setName("local perf thread group"); threadGroup.setNumThreads(3); threadGroup.setDelay(100); threadGroup.setSamplerController(controller); threadGroup.initialize(); // 6、创建结果收集器 ResultCollector resultCollector = new ResultCollector(); resultCollector.setName("local perf result collector"); // 7、构建同级tree HashTree subTree = new HashTree(); subTree.add(javaSampler); subTree.add(controller); subTree.add(threadGroup); subTree.add(resultCollector); // 8、构建层次tree HashTree tree = new HashTree(); tree.add(testPlan,subTree); // 9、创建Jmeter引擎并将配置的tree赋值进去 StandardJMeterEngine jmeterEngine = new StandardJMeterEngine(); jmeterEngine.configure(tree); // 10、执行引擎 try { jmeterEngine.runTest(); } catch (JMeterEngineException e) { e.printStackTrace(); } } }
值得注意的是 javaSample 中需要传入我们编写的case类,一般我们编写的case中应该包括:构造数据,发送请求,判断返回结果等操作,例如:
public class CaseDemo extends AbstractJavaSamplerClient { /** * order_id, * * insert order_id=123 5*50*600 = x, timestamp - 10000 0000001 - 10000 100000 * * select order_id=123 */ @Override public SampleResult runTest(JavaSamplerContext javaSamplerContext) { SampleResult sampleResult = new SampleResult(); sampleResult.setContentType("hahaha"); boolean resultFlag = false; try { sampleResult.sampleStart(); // send [http|dubbo|thrift] response, Http code 200, errorCode=1000,errorMsg=success // response = sendHttpRequest // parse response, 拿到errorCode resultFlag = true; System.out.println("CaseDemo.runTest=" + Thread.currentThread().getId()); } catch (Exception e) { // ignore resultFlag = false; } finally { sampleResult.setSuccessful(resultFlag); sampleResult.sampleEnd(); } return sampleResult; } }
它需要继承 AbstractJavaSamplerClient 类,重写 runTest() 方法,然后我们需要逻辑都写在这个 runTest() 方法中。
后面我们介绍性能测试平台时将会使用这种方式来实现。