blob: e46105da57c90dadfd90db39c890197403d4f094 [file] [log] [blame]
#!/usr/bin/env python
# -*- python-mode -*-
"""Parse /proc/self/mountstats and display it in human readable form
"""
from __future__ import print_function
__copyright__ = """
Copyright (C) 2005, Chuck Lever <cel@netapp.com>
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., 51 Franklin Street, Fifth Floor, Boston,
MA 02110-1301 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 'nfs' in words or 'nfs4' in words:
self.__nfs_data['export'] = words[0]
self.__nfs_data['mountpoint'] = words[3]
self.__nfs_data['fstype'] = words[6]
if words[6].find('nfs') != -1:
self.__nfs_data['statvers'] = words[7]
elif words[0] == 'age:':
self.__nfs_data['age'] = int(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'] = int(words[1])
self.__nfs_data['normalwritebytes'] = int(words[2])
self.__nfs_data['directreadbytes'] = int(words[3])
self.__nfs_data['directwritebytes'] = int(words[4])
self.__nfs_data['serverreadbytes'] = int(words[5])
self.__nfs_data['serverwritebytes'] = int(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'] = int(words[7])
self.__rpc_data['backlogutil'] = int(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'] = int(words[10])
self.__rpc_data['backlogutil'] = int(words[11])
elif words[1] == 'rdma':
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['backlogutil'] = int(words[10])
self.__rpc_data['read_chunks'] = int(words[11])
self.__rpc_data['write_chunks'] = int(words[12])
self.__rpc_data['reply_chunks'] = int(words[13])
self.__rpc_data['total_rdma_req'] = int(words[14])
self.__rpc_data['total_rdma_rep'] = int(words[15])
self.__rpc_data['pullup'] = int(words[16])
self.__rpc_data['fixup'] = int(words[17])
self.__rpc_data['hardway'] = int(words[18])
self.__rpc_data['failed_marshal'] = int(words[19])
self.__rpc_data['bad_reply'] = int(words[20])
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] = [int(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 'nfsv4flags' in self.__nfs_data:
print(' NFSv4 capability flags: %s' % ','.join(self.__nfs_data['nfsv4flags']))
if 'pseudoflavor' in self.__nfs_data:
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)), end=' ')
print('\t%d retrans (%d%%)' % (retrans, ((retrans * 100) / count)), end=' ')
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), end=' ')
print('\tRTT: %f' % (float(stats[6]) / count), end=' ')
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.items():
result.__nfs_data[key] = value
for key, value in self.__rpc_data.items():
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] = list(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), end=' ')
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), end=' ')
print('\t\t%.2f' % (kilobytes / sample_time), end=' ')
if ops != 0:
print('\t\t%.2f' % (rtt / ops), end=' ')
print('\t\t%.2f' % (exe / ops))
else:
print('\t\t0.00', end=' ')
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), end=' ')
print('\t\t%.2f' % (kilobytes / sample_time), end=' ')
if ops != 0:
print('\t\t%.2f' % (rtt / ops), end=' ')
print('\t\t%.2f' % (exe / ops))
else:
print('\t\t0.00', end=' ')
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() ]
elif 'nfs' in words or 'nfs4' in words:
key = words[3]
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.items():
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)