nfs-utils: nfs-iostat.py autofs cleanup and option to sort by ops/s
[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 display_iostats(self, sample_time, which):
358         """Display NFS and RPC stats in an iostat-like way
359         """
360         sends = float(self.__rpc_data['rpcsends'])
361         if sample_time == 0:
362             sample_time = float(self.__nfs_data['age'])
363         if sends != 0:
364             backlog = (float(self.__rpc_data['backlogutil']) / sends) / sample_time
365         else:
366             backlog = 0.0
367
368         print
369         print '%s mounted on %s:' % \
370             (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
371         print
372
373         print '   op/s\t\trpc bklog'
374         print '%7.2f' % (sends / sample_time), 
375         print '\t%7.2f' % backlog
376
377         if which == 0:
378             self.__print_rpc_op_stats('READ', sample_time)
379             self.__print_rpc_op_stats('WRITE', sample_time)
380         elif which == 1:
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)
384         elif which == 2:
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)
390         elif which == 3:
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)
394
395 #
396 # Functions
397 #
398
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.
404     """
405     ms_dict = dict()
406     key = ''
407
408     f = file(filename)
409     for line in f.readlines():
410         words = line.split()
411         if len(words) == 0:
412             continue
413         if words[0] == 'device':
414             key = words[4]
415             new = [ line.strip() ]
416         else:
417             new += [ line.strip() ]
418         ms_dict[key] = new
419     f.close
420
421     return ms_dict
422
423 def print_iostat_summary(old, new, devices, time, ac):
424     if old:
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)
428     else:
429         devicelist = devices
430
431     for device in devicelist:
432         stats = DeviceData()
433         stats.parse_stats(new[device])
434         if not old:
435             stats.display_iostats(time, ac)
436         else:
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)
441
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
446     """
447     list = []
448     if len(givenlist) > 0:
449         for device in givenlist:
450             stats = DeviceData()
451             stats.parse_stats(mountstats[device])
452             if stats.is_nfs_mountpoint():
453                 list += [device]
454     else:
455         for device, descr in mountstats.iteritems():
456             stats = DeviceData()
457             stats.parse_stats(descr)
458             if stats.is_nfs_mountpoint():
459                 list += [device]
460     return list
461
462 def iostat_command(name):
463     """iostat-like command for NFS mount points
464     """
465     mountstats = parse_stats_file('/proc/self/mountstats')
466     devices = []
467     origdevices = []
468     interval_seen = False
469     count_seen = False
470
471     mydescription= """
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
482 client are listed.
483 """
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)
489
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",
494                             dest="which",
495                             const=1,
496                             help='displays statistics related to the attribute cache')
497     statgroup.add_option('-d', '--dir',
498                             action="store_const",
499                             dest="which",
500                             const=2,
501                             help='displays statistics related to directory operations')
502     statgroup.add_option('-p', '--page',
503                             action="store_const",
504                             dest="which",
505                             const=3,
506                             help='displays statistics related to the page cache')
507     parser.add_option_group(statgroup)
508
509     (options, args) = parser.parse_args(sys.argv)
510
511     for arg in args:
512
513         if arg == sys.argv[0]:
514             continue
515
516         if arg in mountstats:
517             origdevices += [arg]
518         elif not interval_seen:
519             try:
520                 interval = int(arg)
521             except:
522                 print 'Illegal <interval> value %s' % arg
523                 return
524             if interval > 0:
525                 interval_seen = True
526             else:
527                 print 'Illegal <interval> value %s' % arg
528                 return
529         elif not count_seen:
530             try:
531                 count = int(arg)
532             except:
533                 print 'Ilegal <count> value %s' % arg
534                 return
535             if count > 0:
536                 count_seen = True
537             else:
538                 print 'Illegal <count> value %s' % arg
539                 return
540
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'
545         return
546
547
548     old_mountstats = None
549     sample_time = 0.0
550
551     if not interval_seen:
552         print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options.which)
553         return
554
555     if count_seen:
556         while count != 0:
557             print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options.which)
558             old_mountstats = mountstats
559             time.sleep(interval)
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'
567                 return
568             count -= 1
569     else: 
570         while True:
571             print_iostat_summary(old_mountstats, mountstats, devices, sample_time, options.which)
572             old_mountstats = mountstats
573             time.sleep(interval)
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'
581                 return
582
583 #
584 # Main
585 #
586 prog = os.path.basename(sys.argv[0])
587
588 try:
589     iostat_command(prog)
590 except KeyboardInterrupt:
591     print 'Caught ^C... exiting'
592     sys.exit(1)
593
594 sys.exit(0)