3 """Emulate iostat for NFS mount points using /proc/self/mountstats
6 from __future__ import print_function
9 Copyright (C) 2005, Chuck Lever <cel@netapp.com>
11 This program is free software; you can redistribute it and/or modify
12 it under the terms of the GNU General Public License version 2 as
13 published by the Free Software Foundation.
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
27 from optparse import OptionParser, OptionGroup
29 Iostats_version = '0.2'
32 """Used for a map() function
76 """DeviceData objects provide methods for parsing and displaying
77 data for a single mount grabbed from /proc/self/mountstats
80 self.__nfs_data = dict()
81 self.__rpc_data = dict()
82 self.__rpc_data['ops'] = []
84 def __parse_nfs_line(self, words):
85 if words[0] == 'device':
86 self.__nfs_data['export'] = words[1]
87 self.__nfs_data['mountpoint'] = words[4]
88 self.__nfs_data['fstype'] = words[7]
90 self.__nfs_data['statvers'] = words[8]
91 elif 'nfs' in words or 'nfs4' in words:
92 self.__nfs_data['export'] = words[0]
93 self.__nfs_data['mountpoint'] = words[3]
94 self.__nfs_data['fstype'] = words[6]
96 self.__nfs_data['statvers'] = words[7]
97 elif words[0] == 'age:':
98 self.__nfs_data['age'] = long(words[1])
99 elif words[0] == 'opts:':
100 self.__nfs_data['mountoptions'] = ''.join(words[1:]).split(',')
101 elif words[0] == 'caps:':
102 self.__nfs_data['servercapabilities'] = ''.join(words[1:]).split(',')
103 elif words[0] == 'nfsv4:':
104 self.__nfs_data['nfsv4flags'] = ''.join(words[1:]).split(',')
105 elif words[0] == 'sec:':
106 keys = ''.join(words[1:]).split(',')
107 self.__nfs_data['flavor'] = int(keys[0].split('=')[1])
108 self.__nfs_data['pseudoflavor'] = 0
109 if self.__nfs_data['flavor'] == 6:
110 self.__nfs_data['pseudoflavor'] = int(keys[1].split('=')[1])
111 elif words[0] == 'events:':
113 for key in NfsEventCounters:
114 self.__nfs_data[key] = int(words[i])
116 elif words[0] == 'bytes:':
118 for key in NfsByteCounters:
119 self.__nfs_data[key] = long(words[i])
122 def __parse_rpc_line(self, words):
123 if words[0] == 'RPC':
124 self.__rpc_data['statsvers'] = float(words[3])
125 self.__rpc_data['programversion'] = words[5]
126 elif words[0] == 'xprt:':
127 self.__rpc_data['protocol'] = words[1]
128 if words[1] == 'udp':
129 self.__rpc_data['port'] = int(words[2])
130 self.__rpc_data['bind_count'] = int(words[3])
131 self.__rpc_data['rpcsends'] = int(words[4])
132 self.__rpc_data['rpcreceives'] = int(words[5])
133 self.__rpc_data['badxids'] = int(words[6])
134 self.__rpc_data['inflightsends'] = long(words[7])
135 self.__rpc_data['backlogutil'] = long(words[8])
136 elif words[1] == 'tcp':
137 self.__rpc_data['port'] = words[2]
138 self.__rpc_data['bind_count'] = int(words[3])
139 self.__rpc_data['connect_count'] = int(words[4])
140 self.__rpc_data['connect_time'] = int(words[5])
141 self.__rpc_data['idle_time'] = int(words[6])
142 self.__rpc_data['rpcsends'] = int(words[7])
143 self.__rpc_data['rpcreceives'] = int(words[8])
144 self.__rpc_data['badxids'] = int(words[9])
145 self.__rpc_data['inflightsends'] = long(words[10])
146 self.__rpc_data['backlogutil'] = long(words[11])
147 elif words[1] == 'rdma':
148 self.__rpc_data['port'] = words[2]
149 self.__rpc_data['bind_count'] = int(words[3])
150 self.__rpc_data['connect_count'] = int(words[4])
151 self.__rpc_data['connect_time'] = int(words[5])
152 self.__rpc_data['idle_time'] = int(words[6])
153 self.__rpc_data['rpcsends'] = int(words[7])
154 self.__rpc_data['rpcreceives'] = int(words[8])
155 self.__rpc_data['badxids'] = int(words[9])
156 self.__rpc_data['backlogutil'] = int(words[10])
157 self.__rpc_data['read_chunks'] = int(words[11])
158 self.__rpc_data['write_chunks'] = int(words[12])
159 self.__rpc_data['reply_chunks'] = int(words[13])
160 self.__rpc_data['total_rdma_req'] = int(words[14])
161 self.__rpc_data['total_rdma_rep'] = int(words[15])
162 self.__rpc_data['pullup'] = int(words[16])
163 self.__rpc_data['fixup'] = int(words[17])
164 self.__rpc_data['hardway'] = int(words[18])
165 self.__rpc_data['failed_marshal'] = int(words[19])
166 self.__rpc_data['bad_reply'] = int(words[20])
167 elif words[0] == 'per-op':
168 self.__rpc_data['per-op'] = words
171 self.__rpc_data['ops'] += [op]
172 self.__rpc_data[op] = [long(word) for word in words[1:]]
174 def parse_stats(self, lines):
175 """Turn a list of lines from a mount stat file into a
176 dictionary full of stats, keyed by name
183 if (not found and words[0] != 'RPC'):
184 self.__parse_nfs_line(words)
188 self.__parse_rpc_line(words)
190 def is_nfs_mountpoint(self):
191 """Return True if this is an NFS or NFSv4 mountpoint,
192 otherwise return False
194 if self.__nfs_data['fstype'] == 'nfs':
196 elif self.__nfs_data['fstype'] == 'nfs4':
200 def compare_iostats(self, old_stats):
201 """Return the difference between two sets of stats
203 result = DeviceData()
205 # copy self into result
206 for key, value in self.__nfs_data.items():
207 result.__nfs_data[key] = value
208 for key, value in self.__rpc_data.items():
209 result.__rpc_data[key] = value
211 # compute the difference of each item in the list
212 # note the copy loop above does not copy the lists, just
213 # the reference to them. so we build new lists here
214 # for the result object.
215 for op in result.__rpc_data['ops']:
216 result.__rpc_data[op] = map(difference, self.__rpc_data[op], old_stats.__rpc_data[op])
218 # update the remaining keys we care about
219 result.__rpc_data['rpcsends'] -= old_stats.__rpc_data['rpcsends']
220 result.__rpc_data['backlogutil'] -= old_stats.__rpc_data['backlogutil']
222 for key in NfsEventCounters:
223 result.__nfs_data[key] -= old_stats.__nfs_data[key]
224 for key in NfsByteCounters:
225 result.__nfs_data[key] -= old_stats.__nfs_data[key]
229 def __print_data_cache_stats(self):
230 """Print the data cache hit rate
232 nfs_stats = self.__nfs_data
233 app_bytes_read = float(nfs_stats['normalreadbytes'])
234 if app_bytes_read != 0:
235 client_bytes_read = float(nfs_stats['serverreadbytes'] - nfs_stats['directreadbytes'])
236 ratio = ((app_bytes_read - client_bytes_read) * 100) / app_bytes_read
239 print('app bytes: %f client bytes %f' % (app_bytes_read, client_bytes_read))
240 print('Data cache hit ratio: %4.2f%%' % ratio)
242 def __print_attr_cache_stats(self, sample_time):
243 """Print attribute cache efficiency stats
245 nfs_stats = self.__nfs_data
246 getattr_stats = self.__rpc_data['GETATTR']
248 if nfs_stats['inoderevalidates'] != 0:
249 getattr_ops = float(getattr_stats[1])
250 opens = float(nfs_stats['vfsopen'])
251 revalidates = float(nfs_stats['inoderevalidates']) - opens
253 ratio = ((revalidates - getattr_ops) * 100) / revalidates
257 data_invalidates = float(nfs_stats['datainvalidates'])
258 attr_invalidates = float(nfs_stats['attrinvalidates'])
261 print('%d inode revalidations, hitting in cache %4.2f%% of the time' % \
262 (revalidates, ratio))
263 print('%d open operations (mandatory GETATTR requests)' % opens)
265 print('%4.2f%% of GETATTRs resulted in data cache invalidations' % \
266 ((data_invalidates * 100) / getattr_ops))
268 def __print_dir_cache_stats(self, sample_time):
269 """Print directory stats
271 nfs_stats = self.__nfs_data
272 lookup_ops = self.__rpc_data['LOOKUP'][0]
273 readdir_ops = self.__rpc_data['READDIR'][0]
274 if self.__rpc_data.has_key('READDIRPLUS'):
275 readdir_ops += self.__rpc_data['READDIRPLUS'][0]
277 dentry_revals = nfs_stats['dentryrevalidates']
278 opens = nfs_stats['vfsopen']
279 lookups = nfs_stats['vfslookup']
280 getdents = nfs_stats['vfsreaddir']
283 print('%d open operations (pathname lookups)' % opens)
284 print('%d dentry revalidates and %d vfs lookup requests' % \
285 (dentry_revals, lookups))
286 print('resulted in %d LOOKUPs on the wire' % lookup_ops)
287 print('%d vfs getdents calls resulted in %d READDIRs on the wire' % \
288 (getdents, readdir_ops))
290 def __print_page_stats(self, sample_time):
291 """Print page cache stats
293 nfs_stats = self.__nfs_data
295 vfsreadpage = nfs_stats['vfsreadpage']
296 vfsreadpages = nfs_stats['vfsreadpages']
297 pages_read = nfs_stats['readpages']
298 vfswritepage = nfs_stats['vfswritepage']
299 vfswritepages = nfs_stats['vfswritepages']
300 pages_written = nfs_stats['writepages']
303 print('%d nfs_readpage() calls read %d pages' % \
304 (vfsreadpage, vfsreadpage))
305 print('%d nfs_readpages() calls read %d pages' % \
306 (vfsreadpages, pages_read - vfsreadpage))
307 if vfsreadpages != 0:
308 print('(%.1f pages per call)' % \
309 (float(pages_read - vfsreadpage) / vfsreadpages))
314 print('%d nfs_updatepage() calls' % nfs_stats['vfsupdatepage'])
315 print('%d nfs_writepage() calls wrote %d pages' % \
316 (vfswritepage, vfswritepage))
317 print('%d nfs_writepages() calls wrote %d pages' % \
318 (vfswritepages, pages_written - vfswritepage))
319 if (vfswritepages) != 0:
320 print('(%.1f pages per call)' % \
321 (float(pages_written - vfswritepage) / vfswritepages))
325 congestionwaits = nfs_stats['congestionwait']
326 if congestionwaits != 0:
328 print('%d congestion waits' % congestionwaits)
330 def __print_rpc_op_stats(self, op, sample_time):
331 """Print generic stats for one RPC op
333 if not self.__rpc_data.has_key(op):
336 rpc_stats = self.__rpc_data[op]
337 ops = float(rpc_stats[0])
338 retrans = float(rpc_stats[1] - rpc_stats[0])
339 kilobytes = float(rpc_stats[3] + rpc_stats[4]) / 1024
340 rtt = float(rpc_stats[6])
341 exe = float(rpc_stats[7])
343 # prevent floating point exceptions
345 kb_per_op = kilobytes / ops
346 retrans_percent = (retrans * 100) / ops
347 rtt_per_op = rtt / ops
348 exe_per_op = exe / ops
351 retrans_percent = 0.0
356 print('%s' % op.lower().ljust(15))
357 print(' ops/s\t\t kB/s\t\t kB/op\t\tretrans\t\tavg RTT (ms)\tavg exe (ms)')
359 print('\t\t%7.3f' % (ops / sample_time))
360 print('\t%7.3f' % (kilobytes / sample_time))
361 print('\t%7.3f' % kb_per_op)
362 print(' %7d (%3.1f%%)' % (retrans, retrans_percent))
363 print('\t%7.3f' % rtt_per_op)
364 print('\t%7.3f' % exe_per_op)
366 def ops(self, sample_time):
367 sends = float(self.__rpc_data['rpcsends'])
369 sample_time = float(self.__nfs_data['age'])
370 return (sends / sample_time)
372 def display_iostats(self, sample_time, which):
373 """Display NFS and RPC stats in an iostat-like way
375 sends = float(self.__rpc_data['rpcsends'])
377 sample_time = float(self.__nfs_data['age'])
378 # sample_time could still be zero if the export was just mounted.
379 # Set it to 1 to avoid divide by zero errors in this case since we'll
380 # likely still have relevant mount statistics to show.
385 backlog = (float(self.__rpc_data['backlogutil']) / sends) / sample_time
390 print('%s mounted on %s:' % \
391 (self.__nfs_data['export'], self.__nfs_data['mountpoint']))
394 print(' op/s\t\trpc bklog')
395 print('%7.2f' % (sends / sample_time))
396 print('\t%7.2f' % backlog)
399 self.__print_rpc_op_stats('READ', sample_time)
400 self.__print_rpc_op_stats('WRITE', sample_time)
402 self.__print_rpc_op_stats('GETATTR', sample_time)
403 self.__print_rpc_op_stats('ACCESS', sample_time)
404 self.__print_attr_cache_stats(sample_time)
406 self.__print_rpc_op_stats('LOOKUP', sample_time)
407 self.__print_rpc_op_stats('READDIR', sample_time)
408 if self.__rpc_data.has_key('READDIRPLUS'):
409 self.__print_rpc_op_stats('READDIRPLUS', sample_time)
410 self.__print_dir_cache_stats(sample_time)
412 self.__print_rpc_op_stats('READ', sample_time)
413 self.__print_rpc_op_stats('WRITE', sample_time)
414 self.__print_page_stats(sample_time)
420 def parse_stats_file(filename):
421 """pop the contents of a mountstats file into a dictionary,
422 keyed by mount point. each value object is a list of the
423 lines in the mountstats file corresponding to the mount
424 point named in the key.
430 for line in f.readlines():
434 if words[0] == 'device':
436 new = [ line.strip() ]
437 elif 'nfs' in words or 'nfs4' in words:
439 new = [ line.strip() ]
441 new += [ line.strip() ]
447 def print_iostat_summary(old, new, devices, time, options):
451 # Trim device list to only include intersection of old and new data,
452 # this addresses umounts due to autofs mountpoints
453 devicelist = filter(lambda x:x in devices,old)
457 for device in devicelist:
458 stats[device] = DeviceData()
459 stats[device].parse_stats(new[device])
461 old_stats = DeviceData()
462 old_stats.parse_stats(old[device])
463 diff_stats[device] = stats[device].compare_iostats(old_stats)
467 # We now have compared data and can print a comparison
468 # ordered by mountpoint ops per second
469 devicelist.sort(key=lambda x: diff_stats[x].ops(time), reverse=True)
471 # First iteration, just sort by newly parsed ops/s
472 devicelist.sort(key=lambda x: stats[x].ops(time), reverse=True)
475 for device in devicelist:
477 diff_stats[device].display_iostats(time, options.which)
479 stats[device].display_iostats(time, options.which)
482 if (count > options.list):
486 def list_nfs_mounts(givenlist, mountstats):
487 """return a list of NFS mounts given a list to validate or
488 return a full list if the given list is empty -
489 may return an empty list if none found
492 if len(givenlist) > 0:
493 for device in givenlist:
495 stats.parse_stats(mountstats[device])
496 if stats.is_nfs_mountpoint():
499 for device, descr in mountstats.items():
501 stats.parse_stats(descr)
502 if stats.is_nfs_mountpoint():
506 def iostat_command(name):
507 """iostat-like command for NFS mount points
509 mountstats = parse_stats_file('/proc/self/mountstats')
512 interval_seen = False
516 Sample iostat-like program to display NFS client per-mount'
517 statistics. The <interval> parameter specifies the amount of time in seconds
518 between each report. The first report contains statistics for the time since
519 each file system was mounted. Each subsequent report contains statistics
520 collected during the interval since the previous report. If the <count>
521 parameter is specified, the value of <count> determines the number of reports
522 generated at <interval> seconds apart. If the interval parameter is specified
523 without the <count> parameter, the command generates reports continuously.
524 If one or more <mount point> names are specified, statistics for only these
525 mount points will be displayed. Otherwise, all NFS mount points on the
528 parser = OptionParser(
529 usage="usage: %prog [ <interval> [ <count> ] ] [ <options> ] [ <mount point> ]",
530 description=mydescription,
531 version='version %s' % Iostats_version)
532 parser.set_defaults(which=0, sort=False, list=sys.maxsize)
534 statgroup = OptionGroup(parser, "Statistics Options",
535 'File I/O is displayed unless one of the following is specified:')
536 statgroup.add_option('-a', '--attr',
537 action="store_const",
540 help='displays statistics related to the attribute cache')
541 statgroup.add_option('-d', '--dir',
542 action="store_const",
545 help='displays statistics related to directory operations')
546 statgroup.add_option('-p', '--page',
547 action="store_const",
550 help='displays statistics related to the page cache')
551 parser.add_option_group(statgroup)
552 displaygroup = OptionGroup(parser, "Display Options",
553 'Options affecting display format:')
554 displaygroup.add_option('-s', '--sort',
557 help="Sort NFS mount points by ops/second")
558 displaygroup.add_option('-l','--list',
562 help="only print stats for first LIST mount points")
563 parser.add_option_group(displaygroup)
565 (options, args) = parser.parse_args(sys.argv)
568 if arg == sys.argv[0]:
571 if arg in mountstats:
573 elif not interval_seen:
577 print('Illegal <interval> value %s' % arg)
582 print('Illegal <interval> value %s' % arg)
588 print('Ilegal <count> value %s' % arg)
593 print('Illegal <count> value %s' % arg)
596 # make certain devices contains only NFS mount points
597 devices = list_nfs_mounts(origdevices, mountstats)
598 if len(devices) == 0:
599 print('No NFS mount points were found')
603 old_mountstats = None
606 if not interval_seen:
607 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
612 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
613 old_mountstats = mountstats
615 sample_time = interval
616 mountstats = parse_stats_file('/proc/self/mountstats')
617 # automount mountpoints add and drop, if automount is involved
618 # we need to recheck the devices list when reparsing
619 devices = list_nfs_mounts(origdevices,mountstats)
620 if len(devices) == 0:
621 print('No NFS mount points were found')
626 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
627 old_mountstats = mountstats
629 sample_time = interval
630 mountstats = parse_stats_file('/proc/self/mountstats')
631 # automount mountpoints add and drop, if automount is involved
632 # we need to recheck the devices list when reparsing
633 devices = list_nfs_mounts(origdevices,mountstats)
634 if len(devices) == 0:
635 print('No NFS mount points were found')
641 prog = os.path.basename(sys.argv[0])
645 except KeyboardInterrupt:
646 print('Caught ^C... exiting')