/* * Sun Public License * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is available at http://www.sun.com/ * * The Original Code is the SLAMD Distributed Load Generation Engine. * The Initial Developer of the Original Code is Neil A. Wilson. * Portions created by Neil A. Wilson are Copyright (C) 2004-2010. * Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): Neil A. Wilson */ package com.slamd.resourcemonitor; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.StringTokenizer; import com.slamd.common.Constants; import com.slamd.common.SLAMDException; import com.slamd.stat.FloatValueTracker; import com.slamd.stat.IntegerValueTracker; import com.slamd.stat.StatTracker; /** * This class defines a SLAMD resource monitor that uses command-line utilities * to monitor I/O utilization. Note that this monitor only fully supports * Solaris systems. It provides some support for Linux systems with 4.1.x * versions of iostat, since earlier iostat versions don't provide the ability * to determine the amount of data read/written per second. The iostat commands * on HP-UX and AIX are so crippled as to not provide any ability to get * separate read and write statistics and as such are basically worthless. * * * @author Neil A. Wilson */ public class IOStatResourceMonitor extends ResourceMonitor { /** * The display name of the stat tracker that monitors the amount of data * read in kilobytes. */ public static final String STAT_TRACKER_KB_READ = "Kilobytes Read"; /** * The display name of the stat tracker that monitors the amount of data * written in kilobytes. */ public static final String STAT_TRACKER_KB_WRITTEN = "Kilobytes Written"; /** * The display name of the stat tracker that monitors the device percent busy. */ public static final String STAT_TRACKER_PCT_BUSY = "Percent Busy"; /** * The configuration property that specifies the path to the iostat command * to use. */ public static final String PROPERTY_IOSTAT_COMMAND = "iostat_command"; /** * The default iostat command that will be used if no other value is given. */ public static final String DEFAULT_IOSTAT_COMMAND = "iostat"; /** * The configuration property that specifies which disks to monitor. If * specified, it should be a comma-delimited list of disk names as they are * output by iostat. If not specified, information about all disks will be * captured. It may optionally also include a set of alternate names to use * for those disks by specifying the values in the comma-delimited list in * the form "name=altname" (e.g., "ssd1=Database,ssd2=Logs"). */ public static final String PROPERTY_MONITOR_DISKS = "monitor_disks"; // Flags used to help skip the first set of data since it will be aggregate // information since boot which is completely irrelevant to what we actually // want to see. private boolean firstIterationSkipped; private boolean skipThisIteration; // The hash map that specifies alternate names to use for the disks to // monitor. private HashMap<String,String> altNameMap; // The information to use when collecting statistics. private int collectionInterval; private String clientID; private String threadID; // The hash maps that will be used to hold the data read from iostat. private LinkedHashMap<String,ArrayList<Double>> readData; private LinkedHashMap<String,ArrayList<Double>> writeData; private LinkedHashMap<String,ArrayList<Integer>> busyData; // The iostat command to execute. private String iostatCommand; // The set of disks for which monitoring has been requested. private String[] requestedDisks; /** * Performs any initialization specific to this resource monitor. * * @throws SLAMDException If a problem occurs while performing the * initialization. */ @Override() public void initializeMonitor() throws SLAMDException { firstIterationSkipped = false; skipThisIteration = false; altNameMap = new HashMap<String,String>(); readData = new LinkedHashMap<String,ArrayList<Double>>(); writeData = new LinkedHashMap<String,ArrayList<Double>>(); busyData = new LinkedHashMap<String,ArrayList<Integer>>(); iostatCommand = getProperty(PROPERTY_IOSTAT_COMMAND, DEFAULT_IOSTAT_COMMAND); switch (getClientOS()) { case OS_TYPE_SOLARIS: iostatCommand += " -x -I"; break; case OS_TYPE_LINUX: iostatCommand += " -d -k"; break; } String diskStr = getProperty(PROPERTY_MONITOR_DISKS); if ((diskStr == null) || (diskStr.length() == 0)) { requestedDisks = null; } else { ArrayList<String> diskList = new ArrayList<String>(); StringTokenizer tokenizer = new StringTokenizer(diskStr, ","); while (tokenizer.hasMoreTokens()) { String diskName = null; String token = tokenizer.nextToken().trim(); int equalPos = token.indexOf('='); if (equalPos > 0) { diskName = token.substring(0, equalPos); String altName = token.substring(equalPos+1); diskList.add(diskName); altNameMap.put(diskName, altName); } else { diskName = token; diskList.add(diskName); } readData.put(diskName, new ArrayList<Double>()); writeData.put(diskName, new ArrayList<Double>()); busyData.put(diskName, new ArrayList<Integer>()); } requestedDisks = new String[diskList.size()]; diskList.toArray(requestedDisks); } } /** * Indicates whether the current client system is supported for this resource * monitor. * * @return <CODE>true</CODE> if the current client system is supported for * this resource monitor, or <CODE>false</CODE> if not. */ @Override() public boolean clientSupported() { int osType = getClientOS(); switch (osType) { case OS_TYPE_SOLARIS: return solarisSupported(); case OS_TYPE_LINUX: return linuxSupported(); default: return false; } } /** * Determines whether a Solaris client system should be supported. It does * this by verifying that the iostat command exists and reports data in the * expected format. In addition, if a list of requested disks has been * provided, it will verify that they all exist. If no set of requested * disks has been given, then this will set the requested disks to all disks * in the system. * * @return <CODE>true</CODE> if the Solaris client system is supported, or * <CODE>false</CODE> if it is not. */ private boolean solarisSupported() { Process p = null; BufferedReader reader = null; try { p = Runtime.getRuntime().exec(iostatCommand); reader = new BufferedReader(new InputStreamReader(p.getInputStream())); } catch (Exception e) { writeVerbose("Unable to execute \"" + iostatCommand + "\" -- " + e); return false; } ArrayList<String> availableDiskList = new ArrayList<String>(); try { String line; while ((line = reader.readLine()) != null) { line = line.trim(); if ((line.length() == 0) || line.startsWith("extended") || line.startsWith("device")) { continue; } StringTokenizer tokenizer = new StringTokenizer(line, " \t"); String diskName = tokenizer.nextToken(); availableDiskList.add(diskName); if ((requestedDisks == null) || (requestedDisks.length == 0)) { readData.put(diskName, new ArrayList<Double>()); writeData.put(diskName, new ArrayList<Double>()); busyData.put(diskName, new ArrayList<Integer>()); } } } catch (Exception e) { writeVerbose("Unable to parse output from iostat command -- " + e); return false; } try { reader.close(); } catch (Exception e) {} if ((requestedDisks != null) && (requestedDisks.length > 0)) { for (int i=0; i < requestedDisks.length; i++) { if (! availableDiskList.contains(requestedDisks[i])) { writeVerbose("Requested disk " + requestedDisks[i] + " not available on client system"); return false; } } } if (readData.isEmpty()) { writeVerbose("No disks available to be monitored."); return false; } return true; } /** * Determines whether a Linux client system should be supported. It does * this by verifying that the iostat command exists and reports data in the * expected format. In addition, if a list of requested disks has been * provided, it will verify that they all exist. If no set of requested * disks has been given, then this will set the requested disks to all disks * in the system. * * @return <CODE>true</CODE> if the Linux client system is supported, or * <CODE>false</CODE> if it is not. */ private boolean linuxSupported() { Process p = null; BufferedReader reader = null; try { p = Runtime.getRuntime().exec(iostatCommand); reader = new BufferedReader(new InputStreamReader(p.getInputStream())); } catch (Exception e) { writeVerbose("Unable to execute \"" + iostatCommand + "\" -- " + e); return false; } ArrayList<String> availableDiskList = new ArrayList<String>(); try { String line; while ((line = reader.readLine()) != null) { if ((line.length() == 0) || line.startsWith("Linux ") || line.startsWith("Device:")) { continue; } StringTokenizer tokenizer = new StringTokenizer(line, " \t"); String diskName = tokenizer.nextToken(); availableDiskList.add(diskName); if ((requestedDisks == null) || (requestedDisks.length == 0)) { readData.put(diskName, new ArrayList<Double>()); writeData.put(diskName, new ArrayList<Double>()); } } } catch (Exception e) { writeVerbose("Unable to parse output from iostat command -- " + e); return false; } try { reader.close(); } catch (Exception e) {} if ((requestedDisks != null) && (requestedDisks.length > 0)) { for (int i=0; i < requestedDisks.length; i++) { if (! availableDiskList.contains(requestedDisks[i])) { writeVerbose("Requested disk " + requestedDisks[i] + " not available on client system"); return false; } } } if (readData.isEmpty()) { writeVerbose("No disks available to be monitored."); return false; } return true; } /** * Creates a new instance of this resource monitor thread. Note that the * <CODE>initialize()</CODE> method should have been called on the new * instance before it is returned. * * @return A new instance of this resource monitor thread. * * @throws SLAMDException If a problem occurs while creating or initializing * the resource monitor. */ @Override() public ResourceMonitor newInstance() throws SLAMDException { IOStatResourceMonitor monitor = new IOStatResourceMonitor(); monitor.initialize(getMonitorClient(), getMonitorProperties()); Iterator<String> iterator = readData.keySet().iterator(); while (iterator.hasNext()) { String key = iterator.next(); monitor.readData.put(key, new ArrayList<Double>()); monitor.writeData.put(key, new ArrayList<Double>()); monitor.busyData.put(key, new ArrayList<Integer>()); } return monitor; } /** * Initializes the stat trackers maintained by this resource monitor. * * @param clientID The client ID to use for the stubs. * @param threadID The thread ID to use for the stubs. * @param collectionInterval The collection interval to use for the stubs. */ @Override() public void initializeStatistics(String clientID, String threadID, int collectionInterval) { // Just capture the information we need. We will actually create the // trackers later. this.clientID = clientID; this.threadID = threadID; this.collectionInterval = collectionInterval; } /** * Retrieves the name to use for this resource monitor. * * @return The name to use for this resource monitor. */ @Override() public String getMonitorName() { return "IOStat"; } /** * Retrieves the statistical data collected by this resource monitor. * * @return The statistical data collected by this resource monitor. */ @Override() public StatTracker[] getResourceStatistics() { ArrayList<StatTracker> statList = new ArrayList<StatTracker>(); Iterator<String> diskNames = readData.keySet().iterator(); while (diskNames.hasNext()) { String diskName = diskNames.next(); String altName = altNameMap.get(diskName); if ((altName == null) || (altName.length() == 0)) { altName = diskName; } ArrayList<Double> readList = readData.get(diskName); ArrayList<Double> writeList = writeData.get(diskName); double[] readArray = new double[readList.size()]; double[] writeArray = new double[writeList.size()]; int[] countArray = new int[readList.size()]; for (int i=0; i < readArray.length; i++) { readArray[i] = readList.get(i); writeArray[i] = writeList.get(i); countArray[i] = 1; } FloatValueTracker readTracker = new FloatValueTracker(clientID, threadID, clientID + ' ' + altName + ' ' + STAT_TRACKER_KB_READ, collectionInterval); readTracker.setIntervalData(readArray, countArray); statList.add(readTracker); FloatValueTracker writeTracker = new FloatValueTracker(clientID, threadID, clientID + ' ' + altName + ' ' + STAT_TRACKER_KB_WRITTEN, collectionInterval); writeTracker.setIntervalData(writeArray, countArray); statList.add(writeTracker); ArrayList<Integer> busyList = busyData.get(diskName); if ((busyList != null) && (! busyList.isEmpty())) { int[] busyArray = new int[busyList.size()]; for (int i=0; i < busyArray.length; i++) { busyArray[i] = busyList.get(i); } IntegerValueTracker busyTracker = new IntegerValueTracker(clientID, threadID, clientID + ' ' + altName + ' ' + STAT_TRACKER_PCT_BUSY, collectionInterval); busyTracker.setIntervalData(busyArray, countArray); statList.add(busyTracker); } } StatTracker[] trackers = new StatTracker[statList.size()]; statList.toArray(trackers); return trackers; } /** * Performs the work of actually collecting resource statistics. This method * should periodically call the <CODE>shouldStop()</CODE> method to determine * whether to stop collecting statistics. * * @return A value that indicates the status of the monitor when it * completed. */ @Override() public int runMonitor() { Process p = null; InputStream inputStream = null; BufferedReader reader = null; try { p = Runtime.getRuntime().exec(iostatCommand + ' ' + collectionInterval); inputStream = p.getInputStream(); reader = new BufferedReader(new InputStreamReader(inputStream)); } catch (Exception e) { logMessage("Unable to execute \"" + iostatCommand + ' ' + collectionInterval + "\" -- " + e); return Constants.JOB_STATE_STOPPED_DUE_TO_ERROR; } int clientOS = getClientOS(); while (! shouldStop()) { try { if (inputStream.available() <= 0) { try { Thread.sleep(10); } catch (InterruptedException ie) {} continue; } String line = reader.readLine(); if (line == null) { logMessage("iostat command stopped producing output"); return Constants.JOB_STATE_COMPLETED_WITH_ERRORS; } else if (line.length() == 0) { continue; } switch (clientOS) { case OS_TYPE_SOLARIS: parseSolarisLine(line); break; case OS_TYPE_LINUX: parseLinuxLine(line); break; } } catch (Exception e) { try { reader.close(); inputStream.close(); p.destroy(); } catch (Exception e2) {} logMessage("Error parsing iostat output: " + e); return Constants.JOB_STATE_STOPPED_DUE_TO_ERROR; } } if (p != null) { try { p.destroy(); } catch (Exception e) {} } try { reader.close(); inputStream.close(); } catch (Exception e) {} return Constants.JOB_STATE_COMPLETED_SUCCESSFULLY; } /** * Parses the provided line of iostat output as it would be generated on a * Solaris system. * * @param line The iostat output line to parse. */ private void parseSolarisLine(String line) { line = line.trim(); if (line.startsWith("extended")) { if (! firstIterationSkipped) { // This will be the first line of output. if (skipThisIteration) { firstIterationSkipped = true; skipThisIteration = false; } else { skipThisIteration = true; } } return; } if (skipThisIteration) { // We're in the first round of output, and we want to skip it. return; } if (line.startsWith("device")) { // This is a header line -- skip it. return; } // This should be actual output. Parse and handle it appropriately. StringTokenizer tokenizer = new StringTokenizer(line, " \t"); String diskName = tokenizer.nextToken(); ArrayList<Double> readList = readData.get(diskName); if (readList == null) { // This isn't a disk we're monitoring. return; } ArrayList<Double> writeList = writeData.get(diskName); ArrayList<Integer> busyList = busyData.get(diskName); tokenizer.nextToken(); // Skip the number of reads per interval. tokenizer.nextToken(); // Skip the number of writes per interval. Double kbRead = new Double(tokenizer.nextToken()); Double kbWritten = new Double(tokenizer.nextToken()); tokenizer.nextToken(); // Skip the wait queue length. tokenizer.nextToken(); // Skip the number of active transactions. tokenizer.nextToken(); // Skip the average service response time. tokenizer.nextToken(); // Skip the percent wait time. Integer pctBusy = new Integer(tokenizer.nextToken()); readList.add(kbRead); writeList.add(kbWritten); busyList.add(pctBusy); } /** * Parses the provided line of iostat output as it would be generated on a * Linux system. * * @param line The iostat output line to parse. */ private void parseLinuxLine(String line) { // This will be the first line of output with uname information. if (line.startsWith("Linux ")) { return; } // This indicates that we're starting a new round of output. if (line.startsWith("Device:")) { if (! firstIterationSkipped) { if (skipThisIteration) { firstIterationSkipped = true; skipThisIteration = false; } else { skipThisIteration = true; } } return; } // We're in the first round of output, and we want to skip it. if (skipThisIteration) { return; } // This should be actual output. Parse and handle it appropriately. StringTokenizer tokenizer = new StringTokenizer(line, " \t"); String diskName = tokenizer.nextToken(); ArrayList<Double> readList = readData.get(diskName); if (readList == null) { // This isn't a disk we're monitoring. return; } ArrayList<Double> writeList = writeData.get(diskName); tokenizer.nextToken(); // Skip the number of transactions per second. tokenizer.nextToken(); // Skip the KB read per second tokenizer.nextToken(); // Skip the KB written per second. Double kbRead = new Double(tokenizer.nextToken()); Double kbWritten = new Double(tokenizer.nextToken()); readList.add(kbRead); writeList.add(kbWritten); } }