2008-06-09 16:47:54

by Chuck Lever III

[permalink] [raw]
Subject: [PATCH 1/4] Import mountstats utility

The "mountstats" utility is a Python program that extracts and displays NFS
client performance information from /proc/self/mountstats.

Note that if mountstats is named 'ms-nfsstat' or 'ms-iostat' it offers
slightly different functionality. It needs two man pages and the install
script should provide both commands by installing the script and providing the
other command via a symlink.

Signed-off-by: Chuck Lever <[email protected]>
---

tools/mountstats/mountstats.py | 584 ++++++++++++++++++++++++++++++++++++++++
1 files changed, 584 insertions(+), 0 deletions(-)
create mode 100755 tools/mountstats/mountstats.py


diff --git a/tools/mountstats/mountstats.py b/tools/mountstats/mountstats.py
new file mode 100755
index 0000000..5f20db6
--- /dev/null
+++ b/tools/mountstats/mountstats.py
@@ -0,0 +1,584 @@
+#!/usr/bin/env python
+# -*- python-mode -*-
+"""Parse /proc/self/mountstats and display it in human readable form
+"""
+
+__copyright__ = """
+Copyright (C) 2005, Chuck Lever <[email protected]>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License version 2 as
+published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+
+import sys, os, time
+
+Mountstats_version = '0.2'
+
+def difference(x, y):
+ """Used for a map() function
+ """
+ return x - y
+
+class DeviceData:
+ """DeviceData objects provide methods for parsing and displaying
+ data for a single mount grabbed from /proc/self/mountstats
+ """
+ def __init__(self):
+ self.__nfs_data = dict()
+ self.__rpc_data = dict()
+ self.__rpc_data['ops'] = []
+
+ def __parse_nfs_line(self, words):
+ if words[0] == 'device':
+ self.__nfs_data['export'] = words[1]
+ self.__nfs_data['mountpoint'] = words[4]
+ self.__nfs_data['fstype'] = words[7]
+ if words[7].find('nfs') != -1:
+ self.__nfs_data['statvers'] = words[8]
+ elif words[0] == 'age:':
+ self.__nfs_data['age'] = long(words[1])
+ elif words[0] == 'opts:':
+ self.__nfs_data['mountoptions'] = ''.join(words[1:]).split(',')
+ elif words[0] == 'caps:':
+ self.__nfs_data['servercapabilities'] = ''.join(words[1:]).split(',')
+ elif words[0] == 'nfsv4:':
+ self.__nfs_data['nfsv4flags'] = ''.join(words[1:]).split(',')
+ elif words[0] == 'sec:':
+ keys = ''.join(words[1:]).split(',')
+ self.__nfs_data['flavor'] = int(keys[0].split('=')[1])
+ self.__nfs_data['pseudoflavor'] = 0
+ if self.__nfs_data['flavor'] == 6:
+ self.__nfs_data['pseudoflavor'] = int(keys[1].split('=')[1])
+ elif words[0] == 'events:':
+ self.__nfs_data['inoderevalidates'] = int(words[1])
+ self.__nfs_data['dentryrevalidates'] = int(words[2])
+ self.__nfs_data['datainvalidates'] = int(words[3])
+ self.__nfs_data['attrinvalidates'] = int(words[4])
+ self.__nfs_data['syncinodes'] = int(words[5])
+ self.__nfs_data['vfsopen'] = int(words[6])
+ self.__nfs_data['vfslookup'] = int(words[7])
+ self.__nfs_data['vfspermission'] = int(words[8])
+ self.__nfs_data['vfsreadpage'] = int(words[9])
+ self.__nfs_data['vfsreadpages'] = int(words[10])
+ self.__nfs_data['vfswritepage'] = int(words[11])
+ self.__nfs_data['vfswritepages'] = int(words[12])
+ self.__nfs_data['vfsreaddir'] = int(words[13])
+ self.__nfs_data['vfsflush'] = int(words[14])
+ self.__nfs_data['vfsfsync'] = int(words[15])
+ self.__nfs_data['vfslock'] = int(words[16])
+ self.__nfs_data['vfsrelease'] = int(words[17])
+ self.__nfs_data['setattrtrunc'] = int(words[18])
+ self.__nfs_data['extendwrite'] = int(words[19])
+ self.__nfs_data['sillyrenames'] = int(words[20])
+ self.__nfs_data['shortreads'] = int(words[21])
+ self.__nfs_data['shortwrites'] = int(words[22])
+ self.__nfs_data['delay'] = int(words[23])
+ elif words[0] == 'bytes:':
+ self.__nfs_data['normalreadbytes'] = long(words[1])
+ self.__nfs_data['normalwritebytes'] = long(words[2])
+ self.__nfs_data['directreadbytes'] = long(words[3])
+ self.__nfs_data['directwritebytes'] = long(words[4])
+ self.__nfs_data['serverreadbytes'] = long(words[5])
+ self.__nfs_data['serverwritebytes'] = long(words[6])
+
+ def __parse_rpc_line(self, words):
+ if words[0] == 'RPC':
+ self.__rpc_data['statsvers'] = float(words[3])
+ self.__rpc_data['programversion'] = words[5]
+ elif words[0] == 'xprt:':
+ self.__rpc_data['protocol'] = words[1]
+ if words[1] == 'udp':
+ self.__rpc_data['port'] = int(words[2])
+ self.__rpc_data['bind_count'] = int(words[3])
+ self.__rpc_data['rpcsends'] = int(words[4])
+ self.__rpc_data['rpcreceives'] = int(words[5])
+ self.__rpc_data['badxids'] = int(words[6])
+ self.__rpc_data['inflightsends'] = long(words[7])
+ self.__rpc_data['backlogutil'] = long(words[8])
+ elif words[1] == 'tcp':
+ self.__rpc_data['port'] = words[2]
+ self.__rpc_data['bind_count'] = int(words[3])
+ self.__rpc_data['connect_count'] = int(words[4])
+ self.__rpc_data['connect_time'] = int(words[5])
+ self.__rpc_data['idle_time'] = int(words[6])
+ self.__rpc_data['rpcsends'] = int(words[7])
+ self.__rpc_data['rpcreceives'] = int(words[8])
+ self.__rpc_data['badxids'] = int(words[9])
+ self.__rpc_data['inflightsends'] = long(words[10])
+ self.__rpc_data['backlogutil'] = int(words[11])
+ elif words[0] == 'per-op':
+ self.__rpc_data['per-op'] = words
+ else:
+ op = words[0][:-1]
+ self.__rpc_data['ops'] += [op]
+ self.__rpc_data[op] = [long(word) for word in words[1:]]
+
+ def parse_stats(self, lines):
+ """Turn a list of lines from a mount stat file into a
+ dictionary full of stats, keyed by name
+ """
+ found = False
+ for line in lines:
+ words = line.split()
+ if len(words) == 0:
+ continue
+ if (not found and words[0] != 'RPC'):
+ self.__parse_nfs_line(words)
+ continue
+
+ found = True
+ self.__parse_rpc_line(words)
+
+ def is_nfs_mountpoint(self):
+ """Return True if this is an NFS or NFSv4 mountpoint,
+ otherwise return False
+ """
+ if self.__nfs_data['fstype'] == 'nfs':
+ return True
+ elif self.__nfs_data['fstype'] == 'nfs4':
+ return True
+ return False
+
+ def display_nfs_options(self):
+ """Pretty-print the NFS options
+ """
+ print 'Stats for %s mounted on %s:' % \
+ (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
+
+ print ' NFS mount options: %s' % ','.join(self.__nfs_data['mountoptions'])
+ print ' NFS server capabilities: %s' % ','.join(self.__nfs_data['servercapabilities'])
+ if self.__nfs_data.has_key('nfsv4flags'):
+ print ' NFSv4 capability flags: %s' % ','.join(self.__nfs_data['nfsv4flags'])
+ if self.__nfs_data.has_key('pseudoflavor'):
+ print ' NFS security flavor: %d pseudoflavor: %d' % \
+ (self.__nfs_data['flavor'], self.__nfs_data['pseudoflavor'])
+ else:
+ print ' NFS security flavor: %d' % self.__nfs_data['flavor']
+
+ def display_nfs_events(self):
+ """Pretty-print the NFS event counters
+ """
+ print
+ print 'Cache events:'
+ print ' data cache invalidated %d times' % self.__nfs_data['datainvalidates']
+ print ' attribute cache invalidated %d times' % self.__nfs_data['attrinvalidates']
+ print ' inodes synced %d times' % self.__nfs_data['syncinodes']
+ print
+ print 'VFS calls:'
+ print ' VFS requested %d inode revalidations' % self.__nfs_data['inoderevalidates']
+ print ' VFS requested %d dentry revalidations' % self.__nfs_data['dentryrevalidates']
+ print
+ print ' VFS called nfs_readdir() %d times' % self.__nfs_data['vfsreaddir']
+ print ' VFS called nfs_lookup() %d times' % self.__nfs_data['vfslookup']
+ print ' VFS called nfs_permission() %d times' % self.__nfs_data['vfspermission']
+ print ' VFS called nfs_file_open() %d times' % self.__nfs_data['vfsopen']
+ print ' VFS called nfs_file_flush() %d times' % self.__nfs_data['vfsflush']
+ print ' VFS called nfs_lock() %d times' % self.__nfs_data['vfslock']
+ print ' VFS called nfs_fsync() %d times' % self.__nfs_data['vfsfsync']
+ print ' VFS called nfs_file_release() %d times' % self.__nfs_data['vfsrelease']
+ print
+ print 'VM calls:'
+ print ' VFS called nfs_readpage() %d times' % self.__nfs_data['vfsreadpage']
+ print ' VFS called nfs_readpages() %d times' % self.__nfs_data['vfsreadpages']
+ print ' VFS called nfs_writepage() %d times' % self.__nfs_data['vfswritepage']
+ print ' VFS called nfs_writepages() %d times' % self.__nfs_data['vfswritepages']
+ print
+ print 'Generic NFS counters:'
+ print ' File size changing operations:'
+ print ' truncating SETATTRs: %d extending WRITEs: %d' % \
+ (self.__nfs_data['setattrtrunc'], self.__nfs_data['extendwrite'])
+ print ' %d silly renames' % self.__nfs_data['sillyrenames']
+ print ' short reads: %d short writes: %d' % \
+ (self.__nfs_data['shortreads'], self.__nfs_data['shortwrites'])
+ print ' NFSERR_DELAYs from server: %d' % self.__nfs_data['delay']
+
+ def display_nfs_bytes(self):
+ """Pretty-print the NFS event counters
+ """
+ print
+ print 'NFS byte counts:'
+ print ' applications read %d bytes via read(2)' % self.__nfs_data['normalreadbytes']
+ print ' applications wrote %d bytes via write(2)' % self.__nfs_data['normalwritebytes']
+ print ' applications read %d bytes via O_DIRECT read(2)' % self.__nfs_data['directreadbytes']
+ print ' applications wrote %d bytes via O_DIRECT write(2)' % self.__nfs_data['directwritebytes']
+ print ' client read %d bytes via NFS READ' % self.__nfs_data['serverreadbytes']
+ print ' client wrote %d bytes via NFS WRITE' % self.__nfs_data['serverwritebytes']
+
+ def display_rpc_generic_stats(self):
+ """Pretty-print the generic RPC stats
+ """
+ sends = self.__rpc_data['rpcsends']
+
+ print
+ print 'RPC statistics:'
+
+ print ' %d RPC requests sent, %d RPC replies received (%d XIDs not found)' % \
+ (sends, self.__rpc_data['rpcreceives'], self.__rpc_data['badxids'])
+ if sends != 0:
+ print ' average backlog queue length: %d' % \
+ (float(self.__rpc_data['backlogutil']) / sends)
+
+ def display_rpc_op_stats(self):
+ """Pretty-print the per-op stats
+ """
+ sends = self.__rpc_data['rpcsends']
+
+ # XXX: these should be sorted by 'count'
+ print
+ for op in self.__rpc_data['ops']:
+ stats = self.__rpc_data[op]
+ count = stats[0]
+ retrans = stats[1] - count
+ if count != 0:
+ print '%s:' % op
+ print '\t%d ops (%d%%)' % \
+ (count, ((count * 100) / sends)),
+ print '\t%d retrans (%d%%)' % (retrans, ((retrans * 100) / count)),
+ print '\t%d major timeouts' % stats[2]
+ print '\tavg bytes sent per op: %d\tavg bytes received per op: %d' % \
+ (stats[3] / count, stats[4] / count)
+ print '\tbacklog wait: %f' % (float(stats[5]) / count),
+ print '\tRTT: %f' % (float(stats[6]) / count),
+ print '\ttotal execute time: %f (milliseconds)' % \
+ (float(stats[7]) / count)
+
+ def compare_iostats(self, old_stats):
+ """Return the difference between two sets of stats
+ """
+ result = DeviceData()
+
+ # copy self into result
+ for key, value in self.__nfs_data.iteritems():
+ result.__nfs_data[key] = value
+ for key, value in self.__rpc_data.iteritems():
+ result.__rpc_data[key] = value
+
+ # compute the difference of each item in the list
+ # note the copy loop above does not copy the lists, just
+ # the reference to them. so we build new lists here
+ # for the result object.
+ for op in result.__rpc_data['ops']:
+ result.__rpc_data[op] = map(difference, self.__rpc_data[op], old_stats.__rpc_data[op])
+
+ # update the remaining keys we care about
+ result.__rpc_data['rpcsends'] -= old_stats.__rpc_data['rpcsends']
+ result.__rpc_data['backlogutil'] -= old_stats.__rpc_data['backlogutil']
+ result.__nfs_data['serverreadbytes'] -= old_stats.__nfs_data['serverreadbytes']
+ result.__nfs_data['serverwritebytes'] -= old_stats.__nfs_data['serverwritebytes']
+
+ return result
+
+ def display_iostats(self, sample_time):
+ """Display NFS and RPC stats in an iostat-like way
+ """
+ sends = float(self.__rpc_data['rpcsends'])
+ if sample_time == 0:
+ sample_time = float(self.__nfs_data['age'])
+
+ print
+ print '%s mounted on %s:' % \
+ (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
+
+ print '\top/s\trpc bklog'
+ print '\t%.2f' % (sends / sample_time),
+ if sends != 0:
+ print '\t%.2f' % \
+ ((float(self.__rpc_data['backlogutil']) / sends) / sample_time)
+ else:
+ print '\t0.00'
+
+ # reads: ops/s, Kb/s, avg rtt, and avg exe
+ # XXX: include avg xfer size and retransmits?
+ read_rpc_stats = self.__rpc_data['READ']
+ ops = float(read_rpc_stats[0])
+ kilobytes = float(self.__nfs_data['serverreadbytes']) / 1024
+ rtt = float(read_rpc_stats[6])
+ exe = float(read_rpc_stats[7])
+
+ print '\treads:\tops/s\t\tKb/s\t\tavg RTT (ms)\tavg exe (ms)'
+ print '\t\t%.2f' % (ops / sample_time),
+ print '\t\t%.2f' % (kilobytes / sample_time),
+ if ops != 0:
+ print '\t\t%.2f' % (rtt / ops),
+ print '\t\t%.2f' % (exe / ops)
+ else:
+ print '\t\t0.00',
+ print '\t\t0.00'
+
+ # writes: ops/s, Kb/s, avg rtt, and avg exe
+ # XXX: include avg xfer size and retransmits?
+ write_rpc_stats = self.__rpc_data['WRITE']
+ ops = float(write_rpc_stats[0])
+ kilobytes = float(self.__nfs_data['serverwritebytes']) / 1024
+ rtt = float(write_rpc_stats[6])
+ exe = float(write_rpc_stats[7])
+
+ print '\twrites:\tops/s\t\tKb/s\t\tavg RTT (ms)\tavg exe (ms)'
+ print '\t\t%.2f' % (ops / sample_time),
+ print '\t\t%.2f' % (kilobytes / sample_time),
+ if ops != 0:
+ print '\t\t%.2f' % (rtt / ops),
+ print '\t\t%.2f' % (exe / ops)
+ else:
+ print '\t\t0.00',
+ print '\t\t0.00'
+
+def parse_stats_file(filename):
+ """pop the contents of a mountstats file into a dictionary,
+ keyed by mount point. each value object is a list of the
+ lines in the mountstats file corresponding to the mount
+ point named in the key.
+ """
+ ms_dict = dict()
+ key = ''
+
+ f = file(filename)
+ for line in f.readlines():
+ words = line.split()
+ if len(words) == 0:
+ continue
+ if words[0] == 'device':
+ key = words[4]
+ new = [ line.strip() ]
+ else:
+ new += [ line.strip() ]
+ ms_dict[key] = new
+ f.close
+
+ return ms_dict
+
+def print_mountstats_help(name):
+ print 'usage: %s [ options ] <mount point>' % name
+ print
+ print ' Version %s' % Mountstats_version
+ print
+ print ' Display NFS client per-mount statistics.'
+ print
+ print ' --version display the version of this command'
+ print ' --nfs display only the NFS statistics'
+ print ' --rpc display only the RPC statistics'
+ print ' --start sample and save statistics'
+ print ' --end resample statistics and compare them with saved'
+ print
+
+def mountstats_command():
+ """Mountstats command
+ """
+ mountpoints = []
+ nfs_only = False
+ rpc_only = False
+
+ for arg in sys.argv:
+ if arg in ['-h', '--help', 'help', 'usage']:
+ print_mountstats_help(prog)
+ return
+
+ if arg in ['-v', '--version', 'version']:
+ print '%s version %s' % (sys.argv[0], Mountstats_version)
+ sys.exit(0)
+
+ if arg in ['-n', '--nfs']:
+ nfs_only = True
+ continue
+
+ if arg in ['-r', '--rpc']:
+ rpc_only = True
+ continue
+
+ if arg in ['-s', '--start']:
+ raise Exception, 'Sampling is not yet implemented'
+
+ if arg in ['-e', '--end']:
+ raise Exception, 'Sampling is not yet implemented'
+
+ if arg == sys.argv[0]:
+ continue
+
+ mountpoints += [arg]
+
+ if mountpoints == []:
+ print_mountstats_help(prog)
+ return
+
+ if rpc_only == True and nfs_only == True:
+ print_mountstats_help(prog)
+ return
+
+ mountstats = parse_stats_file('/proc/self/mountstats')
+
+ for mp in mountpoints:
+ if mp not in mountstats:
+ print 'Statistics for mount point %s not found' % mp
+ continue
+
+ stats = DeviceData()
+ stats.parse_stats(mountstats[mp])
+
+ if not stats.is_nfs_mountpoint():
+ print 'Mount point %s exists but is not an NFS mount' % mp
+ continue
+
+ if nfs_only:
+ stats.display_nfs_options()
+ stats.display_nfs_events()
+ stats.display_nfs_bytes()
+ elif rpc_only:
+ stats.display_rpc_generic_stats()
+ stats.display_rpc_op_stats()
+ else:
+ stats.display_nfs_options()
+ stats.display_nfs_bytes()
+ stats.display_rpc_generic_stats()
+ stats.display_rpc_op_stats()
+
+def print_nfsstat_help(name):
+ print 'usage: %s [ options ]' % name
+ print
+ print ' Version %s' % Mountstats_version
+ print
+ print ' nfsstat-like program that uses NFS client per-mount statistics.'
+ print
+
+def nfsstat_command():
+ print_nfsstat_help(prog)
+
+def print_iostat_help(name):
+ print 'usage: %s [ <interval> [ <count> ] ] [ <mount point> ] ' % name
+ print
+ print ' Version %s' % Mountstats_version
+ print
+ print ' iostat-like program to display NFS client per-mount statistics.'
+ print
+ print ' The <interval> parameter specifies the amount of time in seconds between'
+ print ' each report. The first report contains statistics for the time since each'
+ print ' file system was mounted. Each subsequent report contains statistics'
+ print ' collected during the interval since the previous report.'
+ print
+ print ' If the <count> parameter is specified, the value of <count> determines the'
+ print ' number of reports generated at <interval> seconds apart. If the interval'
+ print ' parameter is specified without the <count> parameter, the command generates'
+ print ' reports continuously.'
+ print
+ print ' If one or more <mount point> names are specified, statistics for only these'
+ print ' mount points will be displayed. Otherwise, all NFS mount points on the'
+ print ' client are listed.'
+ print
+
+def print_iostat_summary(old, new, devices, time):
+ for device in devices:
+ stats = DeviceData()
+ stats.parse_stats(new[device])
+ if not old:
+ stats.display_iostats(time)
+ else:
+ old_stats = DeviceData()
+ old_stats.parse_stats(old[device])
+ diff_stats = stats.compare_iostats(old_stats)
+ diff_stats.display_iostats(time)
+
+def iostat_command():
+ """iostat-like command for NFS mount points
+ """
+ mountstats = parse_stats_file('/proc/self/mountstats')
+ devices = []
+ interval_seen = False
+ count_seen = False
+
+ for arg in sys.argv:
+ if arg in ['-h', '--help', 'help', 'usage']:
+ print_iostat_help(prog)
+ return
+
+ if arg in ['-v', '--version', 'version']:
+ print '%s version %s' % (sys.argv[0], Mountstats_version)
+ return
+
+ if arg == sys.argv[0]:
+ continue
+
+ if arg in mountstats:
+ devices += [arg]
+ elif not interval_seen:
+ interval = int(arg)
+ if interval > 0:
+ interval_seen = True
+ else:
+ print 'Illegal <interval> value'
+ return
+ elif not count_seen:
+ count = int(arg)
+ if count > 0:
+ count_seen = True
+ else:
+ print 'Illegal <count> value'
+ return
+
+ # make certain devices contains only NFS mount points
+ if len(devices) > 0:
+ check = []
+ for device in devices:
+ stats = DeviceData()
+ stats.parse_stats(mountstats[device])
+ if stats.is_nfs_mountpoint():
+ check += [device]
+ devices = check
+ else:
+ for device, descr in mountstats.iteritems():
+ stats = DeviceData()
+ stats.parse_stats(descr)
+ if stats.is_nfs_mountpoint():
+ devices += [device]
+ if len(devices) == 0:
+ print 'No NFS mount points were found'
+ return
+
+ old_mountstats = None
+ sample_time = 0
+
+ if not interval_seen:
+ print_iostat_summary(old_mountstats, mountstats, devices, sample_time)
+ return
+
+ if count_seen:
+ while count != 0:
+ print_iostat_summary(old_mountstats, mountstats, devices, sample_time)
+ old_mountstats = mountstats
+ time.sleep(interval)
+ sample_time = interval
+ mountstats = parse_stats_file('/proc/self/mountstats')
+ count -= 1
+ else:
+ while True:
+ print_iostat_summary(old_mountstats, mountstats, devices, sample_time)
+ old_mountstats = mountstats
+ time.sleep(interval)
+ sample_time = interval
+ mountstats = parse_stats_file('/proc/self/mountstats')
+
+#
+# Main
+#
+prog = os.path.basename(sys.argv[0])
+
+try:
+ if prog == 'mountstats':
+ mountstats_command()
+ elif prog == 'ms-nfsstat':
+ nfsstat_command()
+ elif prog == 'ms-iostat':
+ iostat_command()
+except KeyboardInterrupt:
+ print 'Caught ^C... exiting'
+ sys.exit(1)
+
+sys.exit(0)