]> git.decadent.org.uk Git - nfs-utils.git/blob - tools/nfs-iostat/nfs-iostat.py
nfs-iostat.py: divide by zero with fresh mount
[nfs-utils.git] / tools / nfs-iostat / nfs-iostat.py
1 #!/usr/bin/python
2 # -*- python-mode -*-
3 """Emulate iostat for NFS mount points using /proc/self/mountstats
4 """
5
6 __copyright__ = """
7 Copyright (C) 2005, Chuck Lever <cel@netapp.com>
8
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.
12
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.
17
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
21 """
22
23 import sys, os, time
24 from optparse import OptionParser, OptionGroup
25
26 Iostats_version = '0.2'
27
28 def difference(x, y):
29     """Used for a map() function
30     """
31     return x - y
32
33 NfsEventCounters = [
34     'inoderevalidates',
35     'dentryrevalidates',
36     'datainvalidates',
37     'attrinvalidates',
38     'vfsopen',
39     'vfslookup',
40     'vfspermission',
41     'vfsupdatepage',
42     'vfsreadpage',
43     'vfsreadpages',
44     'vfswritepage',
45     'vfswritepages',
46     'vfsreaddir',
47     'vfssetattr',
48     'vfsflush',
49     'vfsfsync',
50     'vfslock',
51     'vfsrelease',
52     'congestionwait',
53     'setattrtrunc',
54     'extendwrite',
55     'sillyrenames',
56     'shortreads',
57     'shortwrites',
58     'delay'
59 ]
60
61 NfsByteCounters = [
62     'normalreadbytes',
63     'normalwritebytes',
64     'directreadbytes',
65     'directwritebytes',
66     'serverreadbytes',
67     'serverwritebytes',
68     'readpages',
69     'writepages'
70 ]
71
72 class DeviceData:
73     """DeviceData objects provide methods for parsing and displaying
74     data for a single mount grabbed from /proc/self/mountstats
75     """
76     def __init__(self):
77         self.__nfs_data = dict()
78         self.__rpc_data = dict()
79         self.__rpc_data['ops'] = []
80
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]
86             if words[7] == 'nfs':
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:':
103             i = 1
104             for key in NfsEventCounters:
105                 self.__nfs_data[key] = int(words[i])
106                 i += 1
107         elif words[0] == 'bytes:':
108             i = 1
109             for key in NfsByteCounters:
110                 self.__nfs_data[key] = long(words[i])
111                 i += 1
112
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
160         else:
161             op = words[0][:-1]
162             self.__rpc_data['ops'] += [op]
163             self.__rpc_data[op] = [long(word) for word in words[1:]]
164
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
168         """
169         found = False
170         for line in lines:
171             words = line.split()
172             if len(words) == 0:
173                 continue
174             if (not found and words[0] != 'RPC'):
175                 self.__parse_nfs_line(words)
176                 continue
177
178             found = True
179             self.__parse_rpc_line(words)
180
181     def is_nfs_mountpoint(self):
182         """Return True if this is an NFS or NFSv4 mountpoint,
183         otherwise return False
184         """
185         if self.__nfs_data['fstype'] == 'nfs':
186             return True
187         elif self.__nfs_data['fstype'] == 'nfs4':
188             return True
189         return False
190
191     def compare_iostats(self, old_stats):
192         """Return the difference between two sets of stats
193         """
194         result = DeviceData()
195
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
201
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])
208
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']
212
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]
217
218         return result
219
220     def __print_data_cache_stats(self):
221         """Print the data cache hit rate
222         """
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
228
229             print
230             print 'app bytes: %f  client bytes %f' % (app_bytes_read, client_bytes_read)
231             print 'Data cache hit ratio: %4.2f%%' % ratio
232
233     def __print_attr_cache_stats(self, sample_time):
234         """Print attribute cache efficiency stats
235         """
236         nfs_stats = self.__nfs_data
237         getattr_stats = self.__rpc_data['GETATTR']
238
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
243             if revalidates != 0:
244                 ratio = ((revalidates - getattr_ops) * 100) / revalidates
245             else:
246                 ratio = 0.0
247
248             data_invalidates = float(nfs_stats['datainvalidates'])
249             attr_invalidates = float(nfs_stats['attrinvalidates'])
250
251             print
252             print '%d inode revalidations, hitting in cache %4.2f%% of the time' % \
253                 (revalidates, ratio)
254             print '%d open operations (mandatory GETATTR requests)' % opens
255             if getattr_ops != 0:
256                 print '%4.2f%% of GETATTRs resulted in data cache invalidations' % \
257                    ((data_invalidates * 100) / getattr_ops)
258
259     def __print_dir_cache_stats(self, sample_time):
260         """Print directory stats
261         """
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]
267
268         dentry_revals = nfs_stats['dentryrevalidates']
269         opens = nfs_stats['vfsopen']
270         lookups = nfs_stats['vfslookup']
271         getdents = nfs_stats['vfsreaddir']
272
273         print
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)
280
281     def __print_page_stats(self, sample_time):
282         """Print page cache stats
283         """
284         nfs_stats = self.__nfs_data
285
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']
292
293         print
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)
301         else:
302             print
303
304         print
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)
313         else:
314             print
315
316         congestionwaits = nfs_stats['congestionwait']
317         if congestionwaits != 0:
318             print
319             print '%d congestion waits' % congestionwaits
320
321     def __print_rpc_op_stats(self, op, sample_time):
322         """Print generic stats for one RPC op
323         """
324         if not self.__rpc_data.has_key(op):
325             return
326
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])
333
334         # prevent floating point exceptions
335         if ops != 0:
336             kb_per_op = kilobytes / ops
337             retrans_percent = (retrans * 100) / ops
338             rtt_per_op = rtt / ops
339             exe_per_op = exe / ops
340         else:
341             kb_per_op = 0.0
342             retrans_percent = 0.0
343             rtt_per_op = 0.0
344             exe_per_op = 0.0
345
346         op += ':'
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)'
349
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
356
357     def ops(self, sample_time):
358         sends = float(self.__rpc_data['rpcsends'])
359         if sample_time == 0:
360             sample_time = float(self.__nfs_data['age'])
361         return (sends / sample_time)
362
363     def display_iostats(self, sample_time, which):
364         """Display NFS and RPC stats in an iostat-like way
365         """
366         sends = float(self.__rpc_data['rpcsends'])
367         if sample_time == 0:
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.
372         #
373         if sample_time == 0:
374             sample_time = 1;
375         if sends != 0:
376             backlog = (float(self.__rpc_data['backlogutil']) / sends) / sample_time
377         else:
378             backlog = 0.0
379
380         print
381         print '%s mounted on %s:' % \
382             (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
383         print
384
385         print '   op/s\t\trpc bklog'
386         print '%7.2f' % (sends / sample_time), 
387         print '\t%7.2f' % backlog
388
389         if which == 0:
390             self.__print_rpc_op_stats('READ', sample_time)
391             self.__print_rpc_op_stats('WRITE', sample_time)
392         elif which == 1:
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)
396         elif which == 2:
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)
402         elif which == 3:
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)
406
407 #
408 # Functions
409 #
410
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.
416     """
417     ms_dict = dict()
418     key = ''
419
420     f = file(filename)
421     for line in f.readlines():
422         words = line.split()
423         if len(words) == 0:
424             continue
425         if words[0] == 'device':
426             key = words[4]
427             new = [ line.strip() ]
428         else:
429             new += [ line.strip() ]
430         ms_dict[key] = new
431     f.close
432
433     return ms_dict
434
435 def print_iostat_summary(old, new, devices, time, options):
436     stats = {}
437     diff_stats = {}
438
439     if old:
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)
443     else:
444         devicelist = devices
445
446     for device in devicelist:
447         stats[device] = DeviceData()
448         stats[device].parse_stats(new[device])
449         if old:
450             old_stats = DeviceData()
451             old_stats.parse_stats(old[device])
452             diff_stats[device] = stats[device].compare_iostats(old_stats)
453
454     if options.sort:
455         if old:
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)
459         else:
460             # First iteration, just sort by newly parsed ops/s
461             devicelist.sort(key=lambda x: stats[x].ops(time), reverse=True)
462
463     count = 1
464     for device in devicelist:
465         if old:
466             diff_stats[device].display_iostats(time, options.which)
467         else:
468             stats[device].display_iostats(time, options.which)
469
470         count += 1
471         if (count > options.list):
472             return
473
474
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
479     """
480     list = []
481     if len(givenlist) > 0:
482         for device in givenlist:
483             stats = DeviceData()
484             stats.parse_stats(mountstats[device])
485             if stats.is_nfs_mountpoint():
486                 list += [device]
487     else:
488         for device, descr in mountstats.iteritems():
489             stats = DeviceData()
490             stats.parse_stats(descr)
491             if stats.is_nfs_mountpoint():
492                 list += [device]
493     return list
494
495 def iostat_command(name):
496     """iostat-like command for NFS mount points
497     """
498     mountstats = parse_stats_file('/proc/self/mountstats')
499     devices = []
500     origdevices = []
501     interval_seen = False
502     count_seen = False
503
504     mydescription= """
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
515 client are listed.
516 """
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)
522
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",
527                             dest="which",
528                             const=1,
529                             help='displays statistics related to the attribute cache')
530     statgroup.add_option('-d', '--dir',
531                             action="store_const",
532                             dest="which",
533                             const=2,
534                             help='displays statistics related to directory operations')
535     statgroup.add_option('-p', '--page',
536                             action="store_const",
537                             dest="which",
538                             const=3,
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',
544                             action="store_true",
545                             dest="sort",
546                             help="Sort NFS mount points by ops/second")
547     displaygroup.add_option('-l','--list',
548                             action="store",
549                             type="int",
550                             dest="list",
551                             help="only print stats for first LIST mount points")
552     parser.add_option_group(displaygroup)
553
554     (options, args) = parser.parse_args(sys.argv)
555
556     for arg in args:
557
558         if arg == sys.argv[0]:
559             continue
560
561         if arg in mountstats:
562             origdevices += [arg]
563         elif not interval_seen:
564             try:
565                 interval = int(arg)
566             except:
567                 print 'Illegal <interval> value %s' % arg
568                 return
569             if interval > 0:
570                 interval_seen = True
571             else:
572                 print 'Illegal <interval> value %s' % arg
573                 return
574         elif not count_seen:
575             try:
576                 count = int(arg)
577             except:
578                 print 'Ilegal <count> value %s' % arg
579                 return
580             if count > 0:
581                 count_seen = True
582             else:
583                 print 'Illegal <count> value %s' % arg
584                 return
585
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'
590         return
591
592
593     old_mountstats = None
594     sample_time = 0.0
595
596     if not interval_seen:
597         print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
598         return
599
600     if count_seen:
601         while count != 0:
602             print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
603             old_mountstats = mountstats
604             time.sleep(interval)
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'
612                 return
613             count -= 1
614     else: 
615         while True:
616             print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
617             old_mountstats = mountstats
618             time.sleep(interval)
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'
626                 return
627
628 #
629 # Main
630 #
631 prog = os.path.basename(sys.argv[0])
632
633 try:
634     iostat_command(prog)
635 except KeyboardInterrupt:
636     print 'Caught ^C... exiting'
637     sys.exit(1)
638
639 sys.exit(0)