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., 51 Franklin Street, Fifth Floor, Boston,
25 from optparse import OptionParser, OptionGroup
27 Iostats_version = '0.2'
30 """Used for a map() function
74 """DeviceData objects provide methods for parsing and displaying
75 data for a single mount grabbed from /proc/self/mountstats
78 self.__nfs_data = dict()
79 self.__rpc_data = dict()
80 self.__rpc_data['ops'] = []
82 def __parse_nfs_line(self, words):
83 if words[0] == 'device':
84 self.__nfs_data['export'] = words[1]
85 self.__nfs_data['mountpoint'] = words[4]
86 self.__nfs_data['fstype'] = words[7]
88 self.__nfs_data['statvers'] = words[8]
89 elif words[0] == 'age:':
90 self.__nfs_data['age'] = long(words[1])
91 elif words[0] == 'opts:':
92 self.__nfs_data['mountoptions'] = ''.join(words[1:]).split(',')
93 elif words[0] == 'caps:':
94 self.__nfs_data['servercapabilities'] = ''.join(words[1:]).split(',')
95 elif words[0] == 'nfsv4:':
96 self.__nfs_data['nfsv4flags'] = ''.join(words[1:]).split(',')
97 elif words[0] == 'sec:':
98 keys = ''.join(words[1:]).split(',')
99 self.__nfs_data['flavor'] = int(keys[0].split('=')[1])
100 self.__nfs_data['pseudoflavor'] = 0
101 if self.__nfs_data['flavor'] == 6:
102 self.__nfs_data['pseudoflavor'] = int(keys[1].split('=')[1])
103 elif words[0] == 'events:':
105 for key in NfsEventCounters:
106 self.__nfs_data[key] = int(words[i])
108 elif words[0] == 'bytes:':
110 for key in NfsByteCounters:
111 self.__nfs_data[key] = long(words[i])
114 def __parse_rpc_line(self, words):
115 if words[0] == 'RPC':
116 self.__rpc_data['statsvers'] = float(words[3])
117 self.__rpc_data['programversion'] = words[5]
118 elif words[0] == 'xprt:':
119 self.__rpc_data['protocol'] = words[1]
120 if words[1] == 'udp':
121 self.__rpc_data['port'] = int(words[2])
122 self.__rpc_data['bind_count'] = int(words[3])
123 self.__rpc_data['rpcsends'] = int(words[4])
124 self.__rpc_data['rpcreceives'] = int(words[5])
125 self.__rpc_data['badxids'] = int(words[6])
126 self.__rpc_data['inflightsends'] = long(words[7])
127 self.__rpc_data['backlogutil'] = long(words[8])
128 elif words[1] == 'tcp':
129 self.__rpc_data['port'] = words[2]
130 self.__rpc_data['bind_count'] = int(words[3])
131 self.__rpc_data['connect_count'] = int(words[4])
132 self.__rpc_data['connect_time'] = int(words[5])
133 self.__rpc_data['idle_time'] = int(words[6])
134 self.__rpc_data['rpcsends'] = int(words[7])
135 self.__rpc_data['rpcreceives'] = int(words[8])
136 self.__rpc_data['badxids'] = int(words[9])
137 self.__rpc_data['inflightsends'] = long(words[10])
138 self.__rpc_data['backlogutil'] = long(words[11])
139 elif words[1] == 'rdma':
140 self.__rpc_data['port'] = words[2]
141 self.__rpc_data['bind_count'] = int(words[3])
142 self.__rpc_data['connect_count'] = int(words[4])
143 self.__rpc_data['connect_time'] = int(words[5])
144 self.__rpc_data['idle_time'] = int(words[6])
145 self.__rpc_data['rpcsends'] = int(words[7])
146 self.__rpc_data['rpcreceives'] = int(words[8])
147 self.__rpc_data['badxids'] = int(words[9])
148 self.__rpc_data['backlogutil'] = int(words[10])
149 self.__rpc_data['read_chunks'] = int(words[11])
150 self.__rpc_data['write_chunks'] = int(words[12])
151 self.__rpc_data['reply_chunks'] = int(words[13])
152 self.__rpc_data['total_rdma_req'] = int(words[14])
153 self.__rpc_data['total_rdma_rep'] = int(words[15])
154 self.__rpc_data['pullup'] = int(words[16])
155 self.__rpc_data['fixup'] = int(words[17])
156 self.__rpc_data['hardway'] = int(words[18])
157 self.__rpc_data['failed_marshal'] = int(words[19])
158 self.__rpc_data['bad_reply'] = int(words[20])
159 elif words[0] == 'per-op':
160 self.__rpc_data['per-op'] = words
163 self.__rpc_data['ops'] += [op]
164 self.__rpc_data[op] = [long(word) for word in words[1:]]
166 def parse_stats(self, lines):
167 """Turn a list of lines from a mount stat file into a
168 dictionary full of stats, keyed by name
175 if (not found and words[0] != 'RPC'):
176 self.__parse_nfs_line(words)
180 self.__parse_rpc_line(words)
182 def is_nfs_mountpoint(self):
183 """Return True if this is an NFS or NFSv4 mountpoint,
184 otherwise return False
186 if self.__nfs_data['fstype'] == 'nfs':
188 elif self.__nfs_data['fstype'] == 'nfs4':
192 def compare_iostats(self, old_stats):
193 """Return the difference between two sets of stats
195 result = DeviceData()
197 # copy self into result
198 for key, value in self.__nfs_data.iteritems():
199 result.__nfs_data[key] = value
200 for key, value in self.__rpc_data.iteritems():
201 result.__rpc_data[key] = value
203 # compute the difference of each item in the list
204 # note the copy loop above does not copy the lists, just
205 # the reference to them. so we build new lists here
206 # for the result object.
207 for op in result.__rpc_data['ops']:
208 result.__rpc_data[op] = map(difference, self.__rpc_data[op], old_stats.__rpc_data[op])
210 # update the remaining keys we care about
211 result.__rpc_data['rpcsends'] -= old_stats.__rpc_data['rpcsends']
212 result.__rpc_data['backlogutil'] -= old_stats.__rpc_data['backlogutil']
214 for key in NfsEventCounters:
215 result.__nfs_data[key] -= old_stats.__nfs_data[key]
216 for key in NfsByteCounters:
217 result.__nfs_data[key] -= old_stats.__nfs_data[key]
221 def __print_data_cache_stats(self):
222 """Print the data cache hit rate
224 nfs_stats = self.__nfs_data
225 app_bytes_read = float(nfs_stats['normalreadbytes'])
226 if app_bytes_read != 0:
227 client_bytes_read = float(nfs_stats['serverreadbytes'] - nfs_stats['directreadbytes'])
228 ratio = ((app_bytes_read - client_bytes_read) * 100) / app_bytes_read
231 print 'app bytes: %f client bytes %f' % (app_bytes_read, client_bytes_read)
232 print 'Data cache hit ratio: %4.2f%%' % ratio
234 def __print_attr_cache_stats(self, sample_time):
235 """Print attribute cache efficiency stats
237 nfs_stats = self.__nfs_data
238 getattr_stats = self.__rpc_data['GETATTR']
240 if nfs_stats['inoderevalidates'] != 0:
241 getattr_ops = float(getattr_stats[1])
242 opens = float(nfs_stats['vfsopen'])
243 revalidates = float(nfs_stats['inoderevalidates']) - opens
245 ratio = ((revalidates - getattr_ops) * 100) / revalidates
249 data_invalidates = float(nfs_stats['datainvalidates'])
250 attr_invalidates = float(nfs_stats['attrinvalidates'])
253 print '%d inode revalidations, hitting in cache %4.2f%% of the time' % \
255 print '%d open operations (mandatory GETATTR requests)' % opens
257 print '%4.2f%% of GETATTRs resulted in data cache invalidations' % \
258 ((data_invalidates * 100) / getattr_ops)
260 def __print_dir_cache_stats(self, sample_time):
261 """Print directory stats
263 nfs_stats = self.__nfs_data
264 lookup_ops = self.__rpc_data['LOOKUP'][0]
265 readdir_ops = self.__rpc_data['READDIR'][0]
266 if self.__rpc_data.has_key('READDIRPLUS'):
267 readdir_ops += self.__rpc_data['READDIRPLUS'][0]
269 dentry_revals = nfs_stats['dentryrevalidates']
270 opens = nfs_stats['vfsopen']
271 lookups = nfs_stats['vfslookup']
272 getdents = nfs_stats['vfsreaddir']
275 print '%d open operations (pathname lookups)' % opens
276 print '%d dentry revalidates and %d vfs lookup requests' % \
277 (dentry_revals, lookups),
278 print 'resulted in %d LOOKUPs on the wire' % lookup_ops
279 print '%d vfs getdents calls resulted in %d READDIRs on the wire' % \
280 (getdents, readdir_ops)
282 def __print_page_stats(self, sample_time):
283 """Print page cache stats
285 nfs_stats = self.__nfs_data
287 vfsreadpage = nfs_stats['vfsreadpage']
288 vfsreadpages = nfs_stats['vfsreadpages']
289 pages_read = nfs_stats['readpages']
290 vfswritepage = nfs_stats['vfswritepage']
291 vfswritepages = nfs_stats['vfswritepages']
292 pages_written = nfs_stats['writepages']
295 print '%d nfs_readpage() calls read %d pages' % \
296 (vfsreadpage, vfsreadpage)
297 print '%d nfs_readpages() calls read %d pages' % \
298 (vfsreadpages, pages_read - vfsreadpage),
299 if vfsreadpages != 0:
300 print '(%.1f pages per call)' % \
301 (float(pages_read - vfsreadpage) / vfsreadpages)
306 print '%d nfs_updatepage() calls' % nfs_stats['vfsupdatepage']
307 print '%d nfs_writepage() calls wrote %d pages' % \
308 (vfswritepage, vfswritepage)
309 print '%d nfs_writepages() calls wrote %d pages' % \
310 (vfswritepages, pages_written - vfswritepage),
311 if (vfswritepages) != 0:
312 print '(%.1f pages per call)' % \
313 (float(pages_written - vfswritepage) / vfswritepages)
317 congestionwaits = nfs_stats['congestionwait']
318 if congestionwaits != 0:
320 print '%d congestion waits' % congestionwaits
322 def __print_rpc_op_stats(self, op, sample_time):
323 """Print generic stats for one RPC op
325 if not self.__rpc_data.has_key(op):
328 rpc_stats = self.__rpc_data[op]
329 ops = float(rpc_stats[0])
330 retrans = float(rpc_stats[1] - rpc_stats[0])
331 kilobytes = float(rpc_stats[3] + rpc_stats[4]) / 1024
332 rtt = float(rpc_stats[6])
333 exe = float(rpc_stats[7])
335 # prevent floating point exceptions
337 kb_per_op = kilobytes / ops
338 retrans_percent = (retrans * 100) / ops
339 rtt_per_op = rtt / ops
340 exe_per_op = exe / ops
343 retrans_percent = 0.0
348 print '%s' % op.lower().ljust(15),
349 print ' ops/s\t\t kB/s\t\t kB/op\t\tretrans\t\tavg RTT (ms)\tavg exe (ms)'
351 print '\t\t%7.3f' % (ops / sample_time),
352 print '\t%7.3f' % (kilobytes / sample_time),
353 print '\t%7.3f' % kb_per_op,
354 print ' %7d (%3.1f%%)' % (retrans, retrans_percent),
355 print '\t%7.3f' % rtt_per_op,
356 print '\t%7.3f' % exe_per_op
358 def ops(self, sample_time):
359 sends = float(self.__rpc_data['rpcsends'])
361 sample_time = float(self.__nfs_data['age'])
362 return (sends / sample_time)
364 def display_iostats(self, sample_time, which):
365 """Display NFS and RPC stats in an iostat-like way
367 sends = float(self.__rpc_data['rpcsends'])
369 sample_time = float(self.__nfs_data['age'])
370 # sample_time could still be zero if the export was just mounted.
371 # Set it to 1 to avoid divide by zero errors in this case since we'll
372 # likely still have relevant mount statistics to show.
377 backlog = (float(self.__rpc_data['backlogutil']) / sends) / sample_time
382 print '%s mounted on %s:' % \
383 (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
386 print ' op/s\t\trpc bklog'
387 print '%7.2f' % (sends / sample_time),
388 print '\t%7.2f' % backlog
391 self.__print_rpc_op_stats('READ', sample_time)
392 self.__print_rpc_op_stats('WRITE', sample_time)
394 self.__print_rpc_op_stats('GETATTR', sample_time)
395 self.__print_rpc_op_stats('ACCESS', sample_time)
396 self.__print_attr_cache_stats(sample_time)
398 self.__print_rpc_op_stats('LOOKUP', sample_time)
399 self.__print_rpc_op_stats('READDIR', sample_time)
400 if self.__rpc_data.has_key('READDIRPLUS'):
401 self.__print_rpc_op_stats('READDIRPLUS', sample_time)
402 self.__print_dir_cache_stats(sample_time)
404 self.__print_rpc_op_stats('READ', sample_time)
405 self.__print_rpc_op_stats('WRITE', sample_time)
406 self.__print_page_stats(sample_time)
412 def parse_stats_file(filename):
413 """pop the contents of a mountstats file into a dictionary,
414 keyed by mount point. each value object is a list of the
415 lines in the mountstats file corresponding to the mount
416 point named in the key.
422 for line in f.readlines():
426 if words[0] == 'device':
428 new = [ line.strip() ]
430 new += [ line.strip() ]
436 def print_iostat_summary(old, new, devices, time, options):
441 # Trim device list to only include intersection of old and new data,
442 # this addresses umounts due to autofs mountpoints
443 devicelist = filter(lambda x:x in devices,old)
447 for device in devicelist:
448 stats[device] = DeviceData()
449 stats[device].parse_stats(new[device])
451 old_stats = DeviceData()
452 old_stats.parse_stats(old[device])
453 diff_stats[device] = stats[device].compare_iostats(old_stats)
457 # We now have compared data and can print a comparison
458 # ordered by mountpoint ops per second
459 devicelist.sort(key=lambda x: diff_stats[x].ops(time), reverse=True)
461 # First iteration, just sort by newly parsed ops/s
462 devicelist.sort(key=lambda x: stats[x].ops(time), reverse=True)
465 for device in devicelist:
467 diff_stats[device].display_iostats(time, options.which)
469 stats[device].display_iostats(time, options.which)
472 if (count > options.list):
476 def list_nfs_mounts(givenlist, mountstats):
477 """return a list of NFS mounts given a list to validate or
478 return a full list if the given list is empty -
479 may return an empty list if none found
482 if len(givenlist) > 0:
483 for device in givenlist:
485 stats.parse_stats(mountstats[device])
486 if stats.is_nfs_mountpoint():
489 for device, descr in mountstats.iteritems():
491 stats.parse_stats(descr)
492 if stats.is_nfs_mountpoint():
496 def iostat_command(name):
497 """iostat-like command for NFS mount points
499 mountstats = parse_stats_file('/proc/self/mountstats')
502 interval_seen = False
506 Sample iostat-like program to display NFS client per-mount'
507 statistics. The <interval> parameter specifies the amount of time in seconds
508 between each report. The first report contains statistics for the time since
509 each file system was mounted. Each subsequent report contains statistics
510 collected during the interval since the previous report. If the <count>
511 parameter is specified, the value of <count> determines the number of reports
512 generated at <interval> seconds apart. If the interval parameter is specified
513 without the <count> parameter, the command generates reports continuously.
514 If one or more <mount point> names are specified, statistics for only these
515 mount points will be displayed. Otherwise, all NFS mount points on the
518 parser = OptionParser(
519 usage="usage: %prog [ <interval> [ <count> ] ] [ <options> ] [ <mount point> ]",
520 description=mydescription,
521 version='version %s' % Iostats_version)
522 parser.set_defaults(which=0, sort=False, list=sys.maxint)
524 statgroup = OptionGroup(parser, "Statistics Options",
525 'File I/O is displayed unless one of the following is specified:')
526 statgroup.add_option('-a', '--attr',
527 action="store_const",
530 help='displays statistics related to the attribute cache')
531 statgroup.add_option('-d', '--dir',
532 action="store_const",
535 help='displays statistics related to directory operations')
536 statgroup.add_option('-p', '--page',
537 action="store_const",
540 help='displays statistics related to the page cache')
541 parser.add_option_group(statgroup)
542 displaygroup = OptionGroup(parser, "Display Options",
543 'Options affecting display format:')
544 displaygroup.add_option('-s', '--sort',
547 help="Sort NFS mount points by ops/second")
548 displaygroup.add_option('-l','--list',
552 help="only print stats for first LIST mount points")
553 parser.add_option_group(displaygroup)
555 (options, args) = parser.parse_args(sys.argv)
559 if arg == sys.argv[0]:
562 if arg in mountstats:
564 elif not interval_seen:
568 print 'Illegal <interval> value %s' % arg
573 print 'Illegal <interval> value %s' % arg
579 print 'Ilegal <count> value %s' % arg
584 print 'Illegal <count> value %s' % arg
587 # make certain devices contains only NFS mount points
588 devices = list_nfs_mounts(origdevices, mountstats)
589 if len(devices) == 0:
590 print 'No NFS mount points were found'
594 old_mountstats = None
597 if not interval_seen:
598 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
603 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
604 old_mountstats = mountstats
606 sample_time = interval
607 mountstats = parse_stats_file('/proc/self/mountstats')
608 # automount mountpoints add and drop, if automount is involved
609 # we need to recheck the devices list when reparsing
610 devices = list_nfs_mounts(origdevices,mountstats)
611 if len(devices) == 0:
612 print 'No NFS mount points were found'
617 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
618 old_mountstats = mountstats
620 sample_time = interval
621 mountstats = parse_stats_file('/proc/self/mountstats')
622 # automount mountpoints add and drop, if automount is involved
623 # we need to recheck the devices list when reparsing
624 devices = list_nfs_mounts(origdevices,mountstats)
625 if len(devices) == 0:
626 print 'No NFS mount points were found'
632 prog = os.path.basename(sys.argv[0])
636 except KeyboardInterrupt:
637 print 'Caught ^C... exiting'