3 """Emulate iostat for NFS mount points using /proc/self/mountstats
7 Copyright (C) 2005, Chuck Lever <cel@netapp.com>
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License version 2 as
11 published by the Free Software Foundation.
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 from optparse import OptionParser, OptionGroup
26 Iostats_version = '0.2'
29 """Used for a map() function
73 """DeviceData objects provide methods for parsing and displaying
74 data for a single mount grabbed from /proc/self/mountstats
77 self.__nfs_data = dict()
78 self.__rpc_data = dict()
79 self.__rpc_data['ops'] = []
81 def __parse_nfs_line(self, words):
82 if words[0] == 'device':
83 self.__nfs_data['export'] = words[1]
84 self.__nfs_data['mountpoint'] = words[4]
85 self.__nfs_data['fstype'] = words[7]
87 self.__nfs_data['statvers'] = words[8]
88 elif words[0] == 'age:':
89 self.__nfs_data['age'] = long(words[1])
90 elif words[0] == 'opts:':
91 self.__nfs_data['mountoptions'] = ''.join(words[1:]).split(',')
92 elif words[0] == 'caps:':
93 self.__nfs_data['servercapabilities'] = ''.join(words[1:]).split(',')
94 elif words[0] == 'nfsv4:':
95 self.__nfs_data['nfsv4flags'] = ''.join(words[1:]).split(',')
96 elif words[0] == 'sec:':
97 keys = ''.join(words[1:]).split(',')
98 self.__nfs_data['flavor'] = int(keys[0].split('=')[1])
99 self.__nfs_data['pseudoflavor'] = 0
100 if self.__nfs_data['flavor'] == 6:
101 self.__nfs_data['pseudoflavor'] = int(keys[1].split('=')[1])
102 elif words[0] == 'events:':
104 for key in NfsEventCounters:
105 self.__nfs_data[key] = int(words[i])
107 elif words[0] == 'bytes:':
109 for key in NfsByteCounters:
110 self.__nfs_data[key] = long(words[i])
113 def __parse_rpc_line(self, words):
114 if words[0] == 'RPC':
115 self.__rpc_data['statsvers'] = float(words[3])
116 self.__rpc_data['programversion'] = words[5]
117 elif words[0] == 'xprt:':
118 self.__rpc_data['protocol'] = words[1]
119 if words[1] == 'udp':
120 self.__rpc_data['port'] = int(words[2])
121 self.__rpc_data['bind_count'] = int(words[3])
122 self.__rpc_data['rpcsends'] = int(words[4])
123 self.__rpc_data['rpcreceives'] = int(words[5])
124 self.__rpc_data['badxids'] = int(words[6])
125 self.__rpc_data['inflightsends'] = long(words[7])
126 self.__rpc_data['backlogutil'] = long(words[8])
127 elif words[1] == 'tcp':
128 self.__rpc_data['port'] = words[2]
129 self.__rpc_data['bind_count'] = int(words[3])
130 self.__rpc_data['connect_count'] = int(words[4])
131 self.__rpc_data['connect_time'] = int(words[5])
132 self.__rpc_data['idle_time'] = int(words[6])
133 self.__rpc_data['rpcsends'] = int(words[7])
134 self.__rpc_data['rpcreceives'] = int(words[8])
135 self.__rpc_data['badxids'] = int(words[9])
136 self.__rpc_data['inflightsends'] = long(words[10])
137 self.__rpc_data['backlogutil'] = long(words[11])
138 elif words[1] == 'rdma':
139 self.__rpc_data['port'] = words[2]
140 self.__rpc_data['bind_count'] = int(words[3])
141 self.__rpc_data['connect_count'] = int(words[4])
142 self.__rpc_data['connect_time'] = int(words[5])
143 self.__rpc_data['idle_time'] = int(words[6])
144 self.__rpc_data['rpcsends'] = int(words[7])
145 self.__rpc_data['rpcreceives'] = int(words[8])
146 self.__rpc_data['badxids'] = int(words[9])
147 self.__rpc_data['backlogutil'] = int(words[10])
148 self.__rpc_data['read_chunks'] = int(words[11])
149 self.__rpc_data['write_chunks'] = int(words[12])
150 self.__rpc_data['reply_chunks'] = int(words[13])
151 self.__rpc_data['total_rdma_req'] = int(words[14])
152 self.__rpc_data['total_rdma_rep'] = int(words[15])
153 self.__rpc_data['pullup'] = int(words[16])
154 self.__rpc_data['fixup'] = int(words[17])
155 self.__rpc_data['hardway'] = int(words[18])
156 self.__rpc_data['failed_marshal'] = int(words[19])
157 self.__rpc_data['bad_reply'] = int(words[20])
158 elif words[0] == 'per-op':
159 self.__rpc_data['per-op'] = words
162 self.__rpc_data['ops'] += [op]
163 self.__rpc_data[op] = [long(word) for word in words[1:]]
165 def parse_stats(self, lines):
166 """Turn a list of lines from a mount stat file into a
167 dictionary full of stats, keyed by name
174 if (not found and words[0] != 'RPC'):
175 self.__parse_nfs_line(words)
179 self.__parse_rpc_line(words)
181 def is_nfs_mountpoint(self):
182 """Return True if this is an NFS or NFSv4 mountpoint,
183 otherwise return False
185 if self.__nfs_data['fstype'] == 'nfs':
187 elif self.__nfs_data['fstype'] == 'nfs4':
191 def compare_iostats(self, old_stats):
192 """Return the difference between two sets of stats
194 result = DeviceData()
196 # copy self into result
197 for key, value in self.__nfs_data.iteritems():
198 result.__nfs_data[key] = value
199 for key, value in self.__rpc_data.iteritems():
200 result.__rpc_data[key] = value
202 # compute the difference of each item in the list
203 # note the copy loop above does not copy the lists, just
204 # the reference to them. so we build new lists here
205 # for the result object.
206 for op in result.__rpc_data['ops']:
207 result.__rpc_data[op] = map(difference, self.__rpc_data[op], old_stats.__rpc_data[op])
209 # update the remaining keys we care about
210 result.__rpc_data['rpcsends'] -= old_stats.__rpc_data['rpcsends']
211 result.__rpc_data['backlogutil'] -= old_stats.__rpc_data['backlogutil']
213 for key in NfsEventCounters:
214 result.__nfs_data[key] -= old_stats.__nfs_data[key]
215 for key in NfsByteCounters:
216 result.__nfs_data[key] -= old_stats.__nfs_data[key]
220 def __print_data_cache_stats(self):
221 """Print the data cache hit rate
223 nfs_stats = self.__nfs_data
224 app_bytes_read = float(nfs_stats['normalreadbytes'])
225 if app_bytes_read != 0:
226 client_bytes_read = float(nfs_stats['serverreadbytes'] - nfs_stats['directreadbytes'])
227 ratio = ((app_bytes_read - client_bytes_read) * 100) / app_bytes_read
230 print 'app bytes: %f client bytes %f' % (app_bytes_read, client_bytes_read)
231 print 'Data cache hit ratio: %4.2f%%' % ratio
233 def __print_attr_cache_stats(self, sample_time):
234 """Print attribute cache efficiency stats
236 nfs_stats = self.__nfs_data
237 getattr_stats = self.__rpc_data['GETATTR']
239 if nfs_stats['inoderevalidates'] != 0:
240 getattr_ops = float(getattr_stats[1])
241 opens = float(nfs_stats['vfsopen'])
242 revalidates = float(nfs_stats['inoderevalidates']) - opens
244 ratio = ((revalidates - getattr_ops) * 100) / revalidates
248 data_invalidates = float(nfs_stats['datainvalidates'])
249 attr_invalidates = float(nfs_stats['attrinvalidates'])
252 print '%d inode revalidations, hitting in cache %4.2f%% of the time' % \
254 print '%d open operations (mandatory GETATTR requests)' % opens
256 print '%4.2f%% of GETATTRs resulted in data cache invalidations' % \
257 ((data_invalidates * 100) / getattr_ops)
259 def __print_dir_cache_stats(self, sample_time):
260 """Print directory stats
262 nfs_stats = self.__nfs_data
263 lookup_ops = self.__rpc_data['LOOKUP'][0]
264 readdir_ops = self.__rpc_data['READDIR'][0]
265 if self.__rpc_data.has_key('READDIRPLUS'):
266 readdir_ops += self.__rpc_data['READDIRPLUS'][0]
268 dentry_revals = nfs_stats['dentryrevalidates']
269 opens = nfs_stats['vfsopen']
270 lookups = nfs_stats['vfslookup']
271 getdents = nfs_stats['vfsreaddir']
274 print '%d open operations (pathname lookups)' % opens
275 print '%d dentry revalidates and %d vfs lookup requests' % \
276 (dentry_revals, lookups),
277 print 'resulted in %d LOOKUPs on the wire' % lookup_ops
278 print '%d vfs getdents calls resulted in %d READDIRs on the wire' % \
279 (getdents, readdir_ops)
281 def __print_page_stats(self, sample_time):
282 """Print page cache stats
284 nfs_stats = self.__nfs_data
286 vfsreadpage = nfs_stats['vfsreadpage']
287 vfsreadpages = nfs_stats['vfsreadpages']
288 pages_read = nfs_stats['readpages']
289 vfswritepage = nfs_stats['vfswritepage']
290 vfswritepages = nfs_stats['vfswritepages']
291 pages_written = nfs_stats['writepages']
294 print '%d nfs_readpage() calls read %d pages' % \
295 (vfsreadpage, vfsreadpage)
296 print '%d nfs_readpages() calls read %d pages' % \
297 (vfsreadpages, pages_read - vfsreadpage),
298 if vfsreadpages != 0:
299 print '(%.1f pages per call)' % \
300 (float(pages_read - vfsreadpage) / vfsreadpages)
305 print '%d nfs_updatepage() calls' % nfs_stats['vfsupdatepage']
306 print '%d nfs_writepage() calls wrote %d pages' % \
307 (vfswritepage, vfswritepage)
308 print '%d nfs_writepages() calls wrote %d pages' % \
309 (vfswritepages, pages_written - vfswritepage),
310 if (vfswritepages) != 0:
311 print '(%.1f pages per call)' % \
312 (float(pages_written - vfswritepage) / vfswritepages)
316 congestionwaits = nfs_stats['congestionwait']
317 if congestionwaits != 0:
319 print '%d congestion waits' % congestionwaits
321 def __print_rpc_op_stats(self, op, sample_time):
322 """Print generic stats for one RPC op
324 if not self.__rpc_data.has_key(op):
327 rpc_stats = self.__rpc_data[op]
328 ops = float(rpc_stats[0])
329 retrans = float(rpc_stats[1] - rpc_stats[0])
330 kilobytes = float(rpc_stats[3] + rpc_stats[4]) / 1024
331 rtt = float(rpc_stats[6])
332 exe = float(rpc_stats[7])
334 # prevent floating point exceptions
336 kb_per_op = kilobytes / ops
337 retrans_percent = (retrans * 100) / ops
338 rtt_per_op = rtt / ops
339 exe_per_op = exe / ops
342 retrans_percent = 0.0
347 print '%s' % op.lower().ljust(15),
348 print ' ops/s\t\t kB/s\t\t kB/op\t\tretrans\t\tavg RTT (ms)\tavg exe (ms)'
350 print '\t\t%7.3f' % (ops / sample_time),
351 print '\t%7.3f' % (kilobytes / sample_time),
352 print '\t%7.3f' % kb_per_op,
353 print ' %7d (%3.1f%%)' % (retrans, retrans_percent),
354 print '\t%7.3f' % rtt_per_op,
355 print '\t%7.3f' % exe_per_op
357 def ops(self, sample_time):
358 sends = float(self.__rpc_data['rpcsends'])
360 sample_time = float(self.__nfs_data['age'])
361 return (sends / sample_time)
363 def display_iostats(self, sample_time, which):
364 """Display NFS and RPC stats in an iostat-like way
366 sends = float(self.__rpc_data['rpcsends'])
368 sample_time = float(self.__nfs_data['age'])
369 # sample_time could still be zero if the export was just mounted.
370 # Set it to 1 to avoid divide by zero errors in this case since we'll
371 # likely still have relevant mount statistics to show.
376 backlog = (float(self.__rpc_data['backlogutil']) / sends) / sample_time
381 print '%s mounted on %s:' % \
382 (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
385 print ' op/s\t\trpc bklog'
386 print '%7.2f' % (sends / sample_time),
387 print '\t%7.2f' % backlog
390 self.__print_rpc_op_stats('READ', sample_time)
391 self.__print_rpc_op_stats('WRITE', sample_time)
393 self.__print_rpc_op_stats('GETATTR', sample_time)
394 self.__print_rpc_op_stats('ACCESS', sample_time)
395 self.__print_attr_cache_stats(sample_time)
397 self.__print_rpc_op_stats('LOOKUP', sample_time)
398 self.__print_rpc_op_stats('READDIR', sample_time)
399 if self.__rpc_data.has_key('READDIRPLUS'):
400 self.__print_rpc_op_stats('READDIRPLUS', sample_time)
401 self.__print_dir_cache_stats(sample_time)
403 self.__print_rpc_op_stats('READ', sample_time)
404 self.__print_rpc_op_stats('WRITE', sample_time)
405 self.__print_page_stats(sample_time)
411 def parse_stats_file(filename):
412 """pop the contents of a mountstats file into a dictionary,
413 keyed by mount point. each value object is a list of the
414 lines in the mountstats file corresponding to the mount
415 point named in the key.
421 for line in f.readlines():
425 if words[0] == 'device':
427 new = [ line.strip() ]
429 new += [ line.strip() ]
435 def print_iostat_summary(old, new, devices, time, options):
440 # Trim device list to only include intersection of old and new data,
441 # this addresses umounts due to autofs mountpoints
442 devicelist = filter(lambda x:x in devices,old)
446 for device in devicelist:
447 stats[device] = DeviceData()
448 stats[device].parse_stats(new[device])
450 old_stats = DeviceData()
451 old_stats.parse_stats(old[device])
452 diff_stats[device] = stats[device].compare_iostats(old_stats)
456 # We now have compared data and can print a comparison
457 # ordered by mountpoint ops per second
458 devicelist.sort(key=lambda x: diff_stats[x].ops(time), reverse=True)
460 # First iteration, just sort by newly parsed ops/s
461 devicelist.sort(key=lambda x: stats[x].ops(time), reverse=True)
464 for device in devicelist:
466 diff_stats[device].display_iostats(time, options.which)
468 stats[device].display_iostats(time, options.which)
471 if (count > options.list):
475 def list_nfs_mounts(givenlist, mountstats):
476 """return a list of NFS mounts given a list to validate or
477 return a full list if the given list is empty -
478 may return an empty list if none found
481 if len(givenlist) > 0:
482 for device in givenlist:
484 stats.parse_stats(mountstats[device])
485 if stats.is_nfs_mountpoint():
488 for device, descr in mountstats.iteritems():
490 stats.parse_stats(descr)
491 if stats.is_nfs_mountpoint():
495 def iostat_command(name):
496 """iostat-like command for NFS mount points
498 mountstats = parse_stats_file('/proc/self/mountstats')
501 interval_seen = False
505 Sample iostat-like program to display NFS client per-mount'
506 statistics. The <interval> parameter specifies the amount of time in seconds
507 between each report. The first report contains statistics for the time since
508 each file system was mounted. Each subsequent report contains statistics
509 collected during the interval since the previous report. If the <count>
510 parameter is specified, the value of <count> determines the number of reports
511 generated at <interval> seconds apart. If the interval parameter is specified
512 without the <count> parameter, the command generates reports continuously.
513 If one or more <mount point> names are specified, statistics for only these
514 mount points will be displayed. Otherwise, all NFS mount points on the
517 parser = OptionParser(
518 usage="usage: %prog [ <interval> [ <count> ] ] [ <options> ] [ <mount point> ]",
519 description=mydescription,
520 version='version %s' % Iostats_version)
521 parser.set_defaults(which=0, sort=False, list=sys.maxint)
523 statgroup = OptionGroup(parser, "Statistics Options",
524 'File I/O is displayed unless one of the following is specified:')
525 statgroup.add_option('-a', '--attr',
526 action="store_const",
529 help='displays statistics related to the attribute cache')
530 statgroup.add_option('-d', '--dir',
531 action="store_const",
534 help='displays statistics related to directory operations')
535 statgroup.add_option('-p', '--page',
536 action="store_const",
539 help='displays statistics related to the page cache')
540 parser.add_option_group(statgroup)
541 displaygroup = OptionGroup(parser, "Display Options",
542 'Options affecting display format:')
543 displaygroup.add_option('-s', '--sort',
546 help="Sort NFS mount points by ops/second")
547 displaygroup.add_option('-l','--list',
551 help="only print stats for first LIST mount points")
552 parser.add_option_group(displaygroup)
554 (options, args) = parser.parse_args(sys.argv)
558 if arg == sys.argv[0]:
561 if arg in mountstats:
563 elif not interval_seen:
567 print 'Illegal <interval> value %s' % arg
572 print 'Illegal <interval> value %s' % arg
578 print 'Ilegal <count> value %s' % arg
583 print 'Illegal <count> value %s' % arg
586 # make certain devices contains only NFS mount points
587 devices = list_nfs_mounts(origdevices, mountstats)
588 if len(devices) == 0:
589 print 'No NFS mount points were found'
593 old_mountstats = None
596 if not interval_seen:
597 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
602 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
603 old_mountstats = mountstats
605 sample_time = interval
606 mountstats = parse_stats_file('/proc/self/mountstats')
607 # automount mountpoints add and drop, if automount is involved
608 # we need to recheck the devices list when reparsing
609 devices = list_nfs_mounts(origdevices,mountstats)
610 if len(devices) == 0:
611 print 'No NFS mount points were found'
616 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
617 old_mountstats = mountstats
619 sample_time = interval
620 mountstats = parse_stats_file('/proc/self/mountstats')
621 # automount mountpoints add and drop, if automount is involved
622 # we need to recheck the devices list when reparsing
623 devices = list_nfs_mounts(origdevices,mountstats)
624 if len(devices) == 0:
625 print 'No NFS mount points were found'
631 prog = os.path.basename(sys.argv[0])
635 except KeyboardInterrupt:
636 print 'Caught ^C... exiting'