]> git.decadent.org.uk Git - nfs-utils.git/blob - tools/nfs-iostat/nfs-iostat.py
Merge branch 'upstream'
[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         if sends != 0:
370             backlog = (float(self.__rpc_data['backlogutil']) / sends) / sample_time
371         else:
372             backlog = 0.0
373
374         print
375         print '%s mounted on %s:' % \
376             (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
377         print
378
379         print '   op/s\t\trpc bklog'
380         print '%7.2f' % (sends / sample_time), 
381         print '\t%7.2f' % backlog
382
383         if which == 0:
384             self.__print_rpc_op_stats('READ', sample_time)
385             self.__print_rpc_op_stats('WRITE', sample_time)
386         elif which == 1:
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)
390         elif which == 2:
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)
396         elif which == 3:
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)
400
401 #
402 # Functions
403 #
404
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.
410     """
411     ms_dict = dict()
412     key = ''
413
414     f = file(filename)
415     for line in f.readlines():
416         words = line.split()
417         if len(words) == 0:
418             continue
419         if words[0] == 'device':
420             key = words[4]
421             new = [ line.strip() ]
422         else:
423             new += [ line.strip() ]
424         ms_dict[key] = new
425     f.close
426
427     return ms_dict
428
429 def print_iostat_summary(old, new, devices, time, options):
430     stats = {}
431     diff_stats = {}
432
433     if old:
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)
437     else:
438         devicelist = devices
439
440     for device in devicelist:
441         stats[device] = DeviceData()
442         stats[device].parse_stats(new[device])
443         if old:
444             old_stats = DeviceData()
445             old_stats.parse_stats(old[device])
446             diff_stats[device] = stats[device].compare_iostats(old_stats)
447
448     if options.sort:
449         if old:
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)
453         else:
454             # First iteration, just sort by newly parsed ops/s
455             devicelist.sort(key=lambda x: stats[x].ops(time), reverse=True)
456
457     count = 1
458     for device in devicelist:
459         if old:
460             diff_stats[device].display_iostats(time, options.which)
461         else:
462             stats[device].display_iostats(time, options.which)
463
464         count += 1
465         if (count > options.list):
466             return
467
468
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
473     """
474     list = []
475     if len(givenlist) > 0:
476         for device in givenlist:
477             stats = DeviceData()
478             stats.parse_stats(mountstats[device])
479             if stats.is_nfs_mountpoint():
480                 list += [device]
481     else:
482         for device, descr in mountstats.iteritems():
483             stats = DeviceData()
484             stats.parse_stats(descr)
485             if stats.is_nfs_mountpoint():
486                 list += [device]
487     return list
488
489 def iostat_command(name):
490     """iostat-like command for NFS mount points
491     """
492     mountstats = parse_stats_file('/proc/self/mountstats')
493     devices = []
494     origdevices = []
495     interval_seen = False
496     count_seen = False
497
498     mydescription= """
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
509 client are listed.
510 """
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)
516
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",
521                             dest="which",
522                             const=1,
523                             help='displays statistics related to the attribute cache')
524     statgroup.add_option('-d', '--dir',
525                             action="store_const",
526                             dest="which",
527                             const=2,
528                             help='displays statistics related to directory operations')
529     statgroup.add_option('-p', '--page',
530                             action="store_const",
531                             dest="which",
532                             const=3,
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',
538                             action="store_true",
539                             dest="sort",
540                             help="Sort NFS mount points by ops/second")
541     displaygroup.add_option('-l','--list',
542                             action="store",
543                             type="int",
544                             dest="list",
545                             help="only print stats for first LIST mount points")
546     parser.add_option_group(displaygroup)
547
548     (options, args) = parser.parse_args(sys.argv)
549
550     for arg in args:
551
552         if arg == sys.argv[0]:
553             continue
554
555         if arg in mountstats:
556             origdevices += [arg]
557         elif not interval_seen:
558             try:
559                 interval = int(arg)
560             except:
561                 print 'Illegal <interval> value %s' % arg
562                 return
563             if interval > 0:
564                 interval_seen = True
565             else:
566                 print 'Illegal <interval> value %s' % arg
567                 return
568         elif not count_seen:
569             try:
570                 count = int(arg)
571             except:
572                 print 'Ilegal <count> value %s' % arg
573                 return
574             if count > 0:
575                 count_seen = True
576             else:
577                 print 'Illegal <count> value %s' % arg
578                 return
579
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'
584         return
585
586
587     old_mountstats = None
588     sample_time = 0.0
589
590     if not interval_seen:
591         print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
592         return
593
594     if count_seen:
595         while count != 0:
596             print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
597             old_mountstats = mountstats
598             time.sleep(interval)
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'
606                 return
607             count -= 1
608     else: 
609         while True:
610             print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options)
611             old_mountstats = mountstats
612             time.sleep(interval)
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'
620                 return
621
622 #
623 # Main
624 #
625 prog = os.path.basename(sys.argv[0])
626
627 try:
628     iostat_command(prog)
629 except KeyboardInterrupt:
630     print 'Caught ^C... exiting'
631     sys.exit(1)
632
633 sys.exit(0)