从nGrinder3.1.3版本开始,就可以添加自定义的监控数据并显示到最终的测试报告中。我们可以在测试对象所在的服务器上,创建一个文件,叫“custom.data”,然后使用任意的程序或脚本,每隔一定时间将监控信息写进这个文件里面,那个在target服务器上运行的monitor就会获取到这些数据,并传送给controller保存,然后在最终的测试报告中,以图表的形式显示出来。
这一特性可以用来给目标服务器上的添加任意的运行数据,并最终显示到测试报告中。例如可以添加系统的I/O,java的VM状态等等。
这个"custom.data"文件保存的位置是:
${ngrinder_agent}/monitor/custom.data
它的内容就是当前的监控数据,多条数据以逗号隔开,并且都在一行,最多只能5条数据,多于的数据不会被保存。例如下面所示:
315630613,1123285602,1106612131
有了这个文件,target服务器上运行的监控程序nGrinder monitor就会读取文件的内容,并在测试运行过程中把他发送给controller保存。然后最终显示到测试报告中就如下图所示:
需要注意的是,自定义的监控数据的图表名字是 “CUSTOM MONITOR DATA 1”, “CUSTOM MONITOR DATA 2” .., 直到“CUSTOM MONITOR DATA 5”。而且最多只能有5条数据,所以也最多有5个自定义监控数据的图。由于这个图的名字很不直观,但是又无法自定义,用户可以把这些字段的意义作为测试的注释(comment)保存在这个测试的属性里面。
接下来,我们就需要利用一下工具来获取并生成监控数据。我们需要定时的获取系统的某一个属性并保存在文件中。
说到这里,可能很多人就会想到用Linux的cron,例如创建一个脚本用来获取监控数据并保存的文件中,然后用cron定时的调用。但是,cron最低只能设置每分钟执行,但是,nGrinder的监控数据基本都是每秒钟获取一次。
所以,在这个例子中,我要使用java来实现。例如,我要做的是获取Tomcat的GC执行情况,用Java JMX来连接Tomcat进程,用获取GC的执行情况,并保存在文件中。
要使用JMX连接本地服务器上的其它进程,一般情况下,需要那个Java进程启动了JMX服务,但是,一般情况下,我们使用Tomcat是不启动这个服务的。那我们要怎么才能使用JMX连接呢?Attach API。在本地服务器上,我们可以使用attach API来绑定到目标Java进程,然后启动目标进程上的“management agent”。这样就可以使用JMX连接到了。
有关使用attach API和JMX连接到其他Java进程的程序,可以参考这个。有一点需要特别说明的是,我们是使用JMX对象名来获取远程进程的属性,所以我们需要知道GC的名称来获取。但是,在不同的Java版本已经不同的GC配置下,GC的名字也是不一样的,所以,在这个例子中,我先获取了一下本地JVM的GC名称,然后通过这个名字来获取目标进程中GC属性。这就要求,我们允许这个代码的Java环境和运行目标java进程的java环境必须一样,然后使用的VM参数也必须一样。
在这个代码中,我整理了sun和bea的JVM的GC名称,以及它们对应的minor GC或者full GC。而且对于其他的JVM例如IBM的就没有,如果你们需要其他的,请参考相关文档自己添加。
下面,我们就仿照这个事例,来编写一个类,来每隔一秒钟,获取一次目标java进程的GC信息,并写到文件中。其代码如下:
001 | import java.io.BufferedWriter; |
003 | import java.io.FileWriter; |
004 | import java.io.IOException; |
005 | import java.lang.management.GarbageCollectorMXBean; |
006 | import java.lang.management.ManagementFactory; |
007 | import java.util.HashSet; |
008 | import java.util.List; |
011 | import javax.management.MBeanServerConnection; |
012 | import javax.management.ObjectName; |
013 | import javax.management.remote.JMXConnector; |
014 | import javax.management.remote.JMXConnectorFactory; |
015 | import javax.management.remote.JMXServiceURL; |
017 | import com.sun.tools.attach.AttachNotSupportedException; |
018 | import com.sun.tools.attach.VirtualMachine; |
025 | public class GCMonitor { |
027 | public static Set<String> youngGCNames = new HashSet<String>(); |
028 | public static Set<String> oldGCNames = new HashSet<String>(); |
032 | youngGCNames.add( "Copy" ); |
033 | youngGCNames.add( "ParNew" ); |
034 | youngGCNames.add( "PS Scavenge" ); |
037 | youngGCNames.add( "Garbage collection optimized for short pausetimes Young Collector" ); |
038 | youngGCNames.add( "Garbage collection optimized for throughput Young Collector" ); |
039 | youngGCNames.add( "Garbage collection optimized for deterministic pausetimes Young Collector" ); |
042 | oldGCNames.add( "MarkSweepCompact" ); |
043 | oldGCNames.add( "PS MarkSweep" ); |
045 | oldGCNames.add( "ConcurrentMarkSweep" ); |
048 | oldGCNames.add( "Garbage collection optimized for short pausetimes Old Collector" ); |
049 | oldGCNames.add( "Garbage collection optimized for throughput Old Collector" ); |
050 | oldGCNames.add( "Garbage collection optimized for deterministic pausetimes Old Collector" ); |
053 | static final String CONNECTOR_ADDRESS = "com.sun.management.jmxremote.localConnectorAddress" ; |
055 | public static void main(String[] args) throws InterruptedException { |
056 | if (args == null || args.length == 0 ) { |
057 | System.err.println( "Please specify the target PID to attach." ); |
064 | vm = VirtualMachine.attach(args[ 0 ]); |
065 | } catch (AttachNotSupportedException e) { |
066 | System.err.println( "Target application doesn't support attach API." ); |
069 | } catch (IOException e) { |
070 | System.err.println( "Error during attaching to target application." ); |
077 | String connectorAddress = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS); |
078 | MBeanServerConnection serverConn; |
080 | if (connectorAddress == null ) { |
081 | String agent = vm.getSystemProperties().getProperty( "java.home" ) + File.separator + "lib" |
082 | + File.separator + "management-agent.jar" ; |
085 | connectorAddress = vm.getAgentProperties().getProperty(CONNECTOR_ADDRESS); |
089 | JMXServiceURL url = new JMXServiceURL(connectorAddress); |
090 | JMXConnector connector = JMXConnectorFactory.connect(url); |
091 | serverConn = connector.getMBeanServerConnection(); |
092 | ObjectName objName = new ObjectName(ManagementFactory.RUNTIME_MXBEAN_NAME); |
095 | String vendor = (String) serverConn.getAttribute(objName, "VmVendor" ); |
096 | System.out.println( "vendor:" + vendor); |
098 | String[] gcNames = getGCNames(); |
100 | long minorGCCount = 0 ; |
101 | long minorGCTime = 0 ; |
102 | long fullGCCount = 0 ; |
105 | for (String currName : gcNames) { |
106 | objName = new ObjectName( "java.lang:type=GarbageCollector,name=" + currName); |
107 | Long collectionCount = (Long) serverConn.getAttribute(objName, "CollectionCount" ); |
108 | Long collectionTime = (Long) serverConn.getAttribute(objName, "CollectionTime" ); |
109 | if (youngGCNames.contains(currName)) { |
110 | minorGCCount = collectionCount; |
111 | minorGCTime = collectionTime; |
112 | } else if (oldGCNames.contains(currName)) { |
113 | fullGCCount = collectionCount; |
114 | fullGCTime = collectionTime; |
116 | StringBuilder sb = new StringBuilder( "[" ); |
117 | sb.append(getGCType(currName)).append( "\t: " ); |
118 | sb.append( "Count=" + collectionCount); |
119 | sb.append( " \tGCTime=" + collectionTime); |
121 | System.out.println(sb.toString()); |
123 | StringBuilder valueStr = new StringBuilder(); |
126 | valueStr.append(minorGCCount); |
127 | valueStr.append( "," ); |
128 | valueStr.append(minorGCTime); |
129 | valueStr.append( "," ); |
130 | valueStr.append(fullGCCount); |
131 | valueStr.append( "," ); |
132 | valueStr.append(fullGCTime); |
133 | writeToFile(valueStr.toString()); |
136 | } catch (Exception e) { |
141 | public static String getGCType(String name) { |
142 | if (youngGCNames.contains(name)) { |
144 | } else if (oldGCNames.contains(name)) { |
151 | public static String[] getGCNames() { |
152 | List<GarbageCollectorMXBean> gcmbeans = ManagementFactory.getGarbageCollectorMXBeans(); |
153 | String[] rtnName = new String[gcmbeans.size()]; |
155 | for (GarbageCollectorMXBean gc : gcmbeans) { |
156 | rtnName[index] = gc.getName(); |
162 | public static void writeToFile(String gcData) { |
163 | String currDir = System.getProperty( "user.dir" ); |
164 | BufferedWriter writer = null ; |
167 | File customFile = new File(currDir + File.separator + "custom.data" ); |
168 | if (!customFile.exists()) { |
169 | customFile.createNewFile(); |
171 | writer = new BufferedWriter( new FileWriter(customFile)); |
172 | writer.write(gcData); |
174 | } catch (IOException e) { |
175 | System.err.println( "Error to read custom monitor data:" + e.getMessage()); |
177 | if (writer != null ) { |
180 | } catch (IOException e) { |
有关这个代码,有几个需要注意的:
a) 我们需要知道目标进程的ID,并把它作为运行参数。
b) 运行这个java程序的环境必须和目标Tomcat服务器的java环境一致,例如"-server"和其他VM的配置必须一样。
c) 自定义的监控数据的格式是“minorGCCount,minorGCTime,fullGCCount,fullGCTime”.
d) 运行这个java程序时,必须在 “${ngrinder_agent}/monitor/” 目录中,因为在代码中,我将在当前目录中创建和更新custom.data文件。
e) 编译这段代码需要JDK的 “tools.jar”。你需要用类似下面的方式来编译和运行:
1 | javac -cp/home/ngrinder/jdk1. 6 .0_38/lib/tools.jar GCMonitor.java |
3 | #get target tomcat process ID, it is 24003 |
4 | java -cp/home/ngrinder/jdk1. 6 .0_38/lib/tools.jar: GCMonitor 24003 |
运行以后,应该在控制台看到类似下面的结果:
1 | current dir:/home/ngrinder/.ngrinder_agent/monitor |
2 | [Minor GC : Count= 3564 GCTime= 27850 ] |
3 | [Full GC : Count= 166 GCTime= 65525 ] |
4 | [Minor GC : Count= 3564 GCTime= 27850 ] |
5 | [Full GC : Count= 166 GCTime= 65525 ] |
然后在当前目录中会生成custom.data文件,其内容是:
然后,创建一个测试,在这个测试的属性中,设置合适的target服务器,然后运行,当运行完成后,就可以在测试报告中的target monitor里面,看到这些监控数据。
(因为这个例子中的GC不是很频繁,所以看到的基本上就是一条直线。)
使用这样的方式,我们就可以在我们的测试结果中添加任意的监控数据,来帮助我们对target服务器上的某些运行状态有一个更好的展示。并保存便于以后查看。