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'])
370 backlog = (float(self.__rpc_data['backlogutil']) / sends) / sample_time
375 print '%s mounted on %s:' % \
376 (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
379 print ' op/s\t\trpc bklog'
380 print '%7.2f' % (sends / sample_time),
381 print '\t%7.2f' % backlog
384 self.__print_rpc_op_stats('READ', sample_time)
385 self.__print_rpc_op_stats('WRITE', sample_time)
387 self.__print_rpc_op_stats('GETATTR', sample_time)
388 self.__print_rpc_op_stats('ACCESS', sample_time)
389 self.__print_attr_cache_stats(sample_time)
391 self.__print_rpc_op_stats('LOOKUP', sample_time)
392 self.__print_rpc_op_stats('READDIR', sample_time)
393 if self.__rpc_data.has_key('READDIRPLUS'):
394 self.__print_rpc_op_stats('READDIRPLUS', sample_time)
395 self.__print_dir_cache_stats(sample_time)
397 self.__print_rpc_op_stats('READ', sample_time)
398 self.__print_rpc_op_stats('WRITE', sample_time)
399 self.__print_page_stats(sample_time)
405 def parse_stats_file(filename):
406 """pop the contents of a mountstats file into a dictionary,
407 keyed by mount point. each value object is a list of the
408 lines in the mountstats file corresponding to the mount
409 point named in the key.
415 for line in f.readlines():
419 if words[0] == 'device':
421 new = [ line.strip() ]
423 new += [ line.strip() ]
429 def print_iostat_summary(old, new, devices, time, options):
434 # Trim device list to only include intersection of old and new data,
435 # this addresses umounts due to autofs mountpoints
436 devicelist = filter(lambda x:x in devices,old)
440 for device in devicelist:
441 stats[device] = DeviceData()
442 stats[device].parse_stats(new[device])
444 old_stats = DeviceData()
445 old_stats.parse_stats(old[device])
446 diff_stats[device] = stats[device].compare_iostats(old_stats)
450 # We now have compared data and can print a comparison
451 # ordered by mountpoint ops per second
452 devicelist.sort(key=lambda x: diff_stats[x].ops(time), reverse=True)
454 # First iteration, just sort by newly parsed ops/s
455 devicelist.sort(key=lambda x: stats[x].ops(time), reverse=True)
458 for device in devicelist:
460 diff_stats[device].display_iostats(time, options.which)
462 stats[device].display_iostats(time, options.which)
465 if (count > options.list):
469 def list_nfs_mounts(givenlist, mountstats):
470 """return a list of NFS mounts given a list to validate or
471 return a full list if the given list is empty -
472 may return an empty list if none found
475 if len(givenlist) > 0:
476 for device in givenlist:
478 stats.parse_stats(mountstats[device])
479 if stats.is_nfs_mountpoint():
482 for device, descr in mountstats.iteritems():
484 stats.parse_stats(descr)
485 if stats.is_nfs_mountpoint():
489 def iostat_command(name):
490 """iostat-like command for NFS mount points
492 mountstats = parse_stats_file('/proc/self/mountstats')
495 interval_seen = False
499 Sample iostat-like program to display NFS client per-mount'
500 statistics. The <interval> parameter specifies the amount of time in seconds
501 between each report. The first report contains statistics for the time since
502 each file system was mounted. Each subsequent report contains statistics
503 collected during the interval since the previous report. If the <count>
504 parameter is specified, the value of <count> determines the number of reports
505 generated at <interval> seconds apart. If the interval parameter is specified
506 without the <count> parameter, the command generates reports continuously.
507 If one or more <mount point> names are specified, statistics for only these
508 mount points will be displayed. Otherwise, all NFS mount points on the
511 parser = OptionParser(
512 usage="usage: %prog [ <interval> [ <count> ] ] [ <options> ] [ <mount point> ]",
513 description=mydescription,
514 version='version %s' % Iostats_version)
515 parser.set_defaults(which=0, sort=False, list=sys.maxint)
517 statgroup = OptionGroup(parser, "Statistics Options",
518 'File I/O is displayed unless one of the following is specified:')
519 statgroup.add_option('-a', '--attr',
520 action="store_const",
523 help='displays statistics related to the attribute cache')
524 statgroup.add_option('-d', '--dir',
525 action="store_const",
528 help='displays statistics related to directory operations')
529 statgroup.add_option('-p', '--page',
530 action="store_const",
533 help='displays statistics related to the page cache')
534 parser.add_option_group(statgroup)
535 displaygroup = OptionGroup(parser, "Display Options",
536 'Options affecting display format:')
537 displaygroup.add_option('-s', '--sort',
540 help="Sort NFS mount points by ops/second")
541 displaygroup.add_option('-l','--list',
545 help="only print stats for first LIST mount points")
546 parser.add_option_group(displaygroup)
548 (options, args) = parser.parse_args(sys.argv)
552 if arg == sys.argv[0]:
555 if arg in mountstats:
557 elif not interval_seen:
561 print 'Illegal <interval> value %s' % arg
566 print 'Illegal <interval> value %s' % arg
572 print 'Ilegal <count> value %s' % arg
577 print 'Illegal <count> value %s' % arg
580 # make certain devices contains only NFS mount points
581 devices = list_nfs_mounts(origdevices, mountstats)
582 if len(devices) == 0:
583 print 'No NFS mount points were found'
587 old_mountstats = None
590 if not interval_seen:
591 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
596 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
597 old_mountstats = mountstats
599 sample_time = interval
600 mountstats = parse_stats_file('/proc/self/mountstats')
601 # automount mountpoints add and drop, if automount is involved
602 # we need to recheck the devices list when reparsing
603 devices = list_nfs_mounts(origdevices,mountstats)
604 if len(devices) == 0:
605 print 'No NFS mount points were found'
610 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
611 old_mountstats = mountstats
613 sample_time = interval
614 mountstats = parse_stats_file('/proc/self/mountstats')
615 # automount mountpoints add and drop, if automount is involved
616 # we need to recheck the devices list when reparsing
617 devices = list_nfs_mounts(origdevices,mountstats)
618 if len(devices) == 0:
619 print 'No NFS mount points were found'
625 prog = os.path.basename(sys.argv[0])
629 except KeyboardInterrupt:
630 print 'Caught ^C... exiting'