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 display_iostats(self, sample_time, which):
358 """Display NFS and RPC stats in an iostat-like way
360 sends = float(self.__rpc_data['rpcsends'])
362 sample_time = float(self.__nfs_data['age'])
364 backlog = (float(self.__rpc_data['backlogutil']) / sends) / sample_time
369 print '%s mounted on %s:' % \
370 (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
373 print ' op/s\t\trpc bklog'
374 print '%7.2f' % (sends / sample_time),
375 print '\t%7.2f' % backlog
378 self.__print_rpc_op_stats('READ', sample_time)
379 self.__print_rpc_op_stats('WRITE', sample_time)
381 self.__print_rpc_op_stats('GETATTR', sample_time)
382 self.__print_rpc_op_stats('ACCESS', sample_time)
383 self.__print_attr_cache_stats(sample_time)
385 self.__print_rpc_op_stats('LOOKUP', sample_time)
386 self.__print_rpc_op_stats('READDIR', sample_time)
387 if self.__rpc_data.has_key('READDIRPLUS'):
388 self.__print_rpc_op_stats('READDIRPLUS', sample_time)
389 self.__print_dir_cache_stats(sample_time)
391 self.__print_rpc_op_stats('READ', sample_time)
392 self.__print_rpc_op_stats('WRITE', sample_time)
393 self.__print_page_stats(sample_time)
399 def parse_stats_file(filename):
400 """pop the contents of a mountstats file into a dictionary,
401 keyed by mount point. each value object is a list of the
402 lines in the mountstats file corresponding to the mount
403 point named in the key.
409 for line in f.readlines():
413 if words[0] == 'device':
415 new = [ line.strip() ]
417 new += [ line.strip() ]
423 def print_iostat_summary(old, new, devices, time, ac):
425 # Trim device list to only include intersection of old and new data,
426 # this addresses umounts due to autofs mountpoints
427 devicelist = filter(lambda x:x in devices,old)
431 for device in devicelist:
433 stats.parse_stats(new[device])
435 stats.display_iostats(time, ac)
437 old_stats = DeviceData()
438 old_stats.parse_stats(old[device])
439 diff_stats = stats.compare_iostats(old_stats)
440 diff_stats.display_iostats(time, ac)
442 def list_nfs_mounts(givenlist, mountstats):
443 """return a list of NFS mounts given a list to validate or
444 return a full list if the given list is empty -
445 may return an empty list if none found
448 if len(givenlist) > 0:
449 for device in givenlist:
451 stats.parse_stats(mountstats[device])
452 if stats.is_nfs_mountpoint():
455 for device, descr in mountstats.iteritems():
457 stats.parse_stats(descr)
458 if stats.is_nfs_mountpoint():
462 def iostat_command(name):
463 """iostat-like command for NFS mount points
465 mountstats = parse_stats_file('/proc/self/mountstats')
468 interval_seen = False
472 Sample iostat-like program to display NFS client per-mount'
473 statistics. The <interval> parameter specifies the amount of time in seconds
474 between each report. The first report contains statistics for the time since
475 each file system was mounted. Each subsequent report contains statistics
476 collected during the interval since the previous report. If the <count>
477 parameter is specified, the value of <count> determines the number of reports
478 generated at <interval> seconds apart. If the interval parameter is specified
479 without the <count> parameter, the command generates reports continuously.
480 If one or more <mount point> names are specified, statistics for only these
481 mount points will be displayed. Otherwise, all NFS mount points on the
484 parser = OptionParser(
485 usage="usage: %prog [ <interval> [ <count> ] ] [ <options> ] [ <mount point> ]",
486 description=mydescription,
487 version='version %s' % Iostats_version)
488 parser.set_defaults(which=0)
490 statgroup = OptionGroup(parser, "Statistics Options",
491 'File I/O is displayed unless one of the following is specified:')
492 statgroup.add_option('-a', '--attr',
493 action="store_const",
496 help='displays statistics related to the attribute cache')
497 statgroup.add_option('-d', '--dir',
498 action="store_const",
501 help='displays statistics related to directory operations')
502 statgroup.add_option('-p', '--page',
503 action="store_const",
506 help='displays statistics related to the page cache')
507 parser.add_option_group(statgroup)
509 (options, args) = parser.parse_args(sys.argv)
513 if arg == sys.argv[0]:
516 if arg in mountstats:
518 elif not interval_seen:
522 print 'Illegal <interval> value %s' % arg
527 print 'Illegal <interval> value %s' % arg
533 print 'Ilegal <count> value %s' % arg
538 print 'Illegal <count> value %s' % arg
541 # make certain devices contains only NFS mount points
542 devices = list_nfs_mounts(origdevices, mountstats)
543 if len(devices) == 0:
544 print 'No NFS mount points were found'
548 old_mountstats = None
551 if not interval_seen:
552 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options.which)
557 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options.which)
558 old_mountstats = mountstats
560 sample_time = interval
561 mountstats = parse_stats_file('/proc/self/mountstats')
562 # automount mountpoints add and drop, if automount is involved
563 # we need to recheck the devices list when reparsing
564 devices = list_nfs_mounts(origdevices,mountstats)
565 if len(devices) == 0:
566 print 'No NFS mount points were found'
571 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options.which)
572 old_mountstats = mountstats
574 sample_time = interval
575 mountstats = parse_stats_file('/proc/self/mountstats')
576 # automount mountpoints add and drop, if automount is involved
577 # we need to recheck the devices list when reparsing
578 devices = list_nfs_mounts(origdevices,mountstats)
579 if len(devices) == 0:
580 print 'No NFS mount points were found'
586 prog = os.path.basename(sys.argv[0])
590 except KeyboardInterrupt:
591 print 'Caught ^C... exiting'