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
25 Iostats_version = '0.2'
28 """Used for a map() function
72 """DeviceData objects provide methods for parsing and displaying
73 data for a single mount grabbed from /proc/self/mountstats
76 self.__nfs_data = dict()
77 self.__rpc_data = dict()
78 self.__rpc_data['ops'] = []
80 def __parse_nfs_line(self, words):
81 if words[0] == 'device':
82 self.__nfs_data['export'] = words[1]
83 self.__nfs_data['mountpoint'] = words[4]
84 self.__nfs_data['fstype'] = words[7]
86 self.__nfs_data['statvers'] = words[8]
87 elif words[0] == 'age:':
88 self.__nfs_data['age'] = long(words[1])
89 elif words[0] == 'opts:':
90 self.__nfs_data['mountoptions'] = ''.join(words[1:]).split(',')
91 elif words[0] == 'caps:':
92 self.__nfs_data['servercapabilities'] = ''.join(words[1:]).split(',')
93 elif words[0] == 'nfsv4:':
94 self.__nfs_data['nfsv4flags'] = ''.join(words[1:]).split(',')
95 elif words[0] == 'sec:':
96 keys = ''.join(words[1:]).split(',')
97 self.__nfs_data['flavor'] = int(keys[0].split('=')[1])
98 self.__nfs_data['pseudoflavor'] = 0
99 if self.__nfs_data['flavor'] == 6:
100 self.__nfs_data['pseudoflavor'] = int(keys[1].split('=')[1])
101 elif words[0] == 'events:':
103 for key in NfsEventCounters:
104 self.__nfs_data[key] = int(words[i])
106 elif words[0] == 'bytes:':
108 for key in NfsByteCounters:
109 self.__nfs_data[key] = long(words[i])
112 def __parse_rpc_line(self, words):
113 if words[0] == 'RPC':
114 self.__rpc_data['statsvers'] = float(words[3])
115 self.__rpc_data['programversion'] = words[5]
116 elif words[0] == 'xprt:':
117 self.__rpc_data['protocol'] = words[1]
118 if words[1] == 'udp':
119 self.__rpc_data['port'] = int(words[2])
120 self.__rpc_data['bind_count'] = int(words[3])
121 self.__rpc_data['rpcsends'] = int(words[4])
122 self.__rpc_data['rpcreceives'] = int(words[5])
123 self.__rpc_data['badxids'] = int(words[6])
124 self.__rpc_data['inflightsends'] = long(words[7])
125 self.__rpc_data['backlogutil'] = long(words[8])
126 elif words[1] == 'tcp':
127 self.__rpc_data['port'] = words[2]
128 self.__rpc_data['bind_count'] = int(words[3])
129 self.__rpc_data['connect_count'] = int(words[4])
130 self.__rpc_data['connect_time'] = int(words[5])
131 self.__rpc_data['idle_time'] = int(words[6])
132 self.__rpc_data['rpcsends'] = int(words[7])
133 self.__rpc_data['rpcreceives'] = int(words[8])
134 self.__rpc_data['badxids'] = int(words[9])
135 self.__rpc_data['inflightsends'] = long(words[10])
136 self.__rpc_data['backlogutil'] = long(words[11])
137 elif words[1] == 'rdma':
138 self.__rpc_data['port'] = words[2]
139 self.__rpc_data['bind_count'] = int(words[3])
140 self.__rpc_data['connect_count'] = int(words[4])
141 self.__rpc_data['connect_time'] = int(words[5])
142 self.__rpc_data['idle_time'] = int(words[6])
143 self.__rpc_data['rpcsends'] = int(words[7])
144 self.__rpc_data['rpcreceives'] = int(words[8])
145 self.__rpc_data['badxids'] = int(words[9])
146 self.__rpc_data['backlogutil'] = int(words[10])
147 self.__rpc_data['read_chunks'] = int(words[11])
148 self.__rpc_data['write_chunks'] = int(words[12])
149 self.__rpc_data['reply_chunks'] = int(words[13])
150 self.__rpc_data['total_rdma_req'] = int(words[14])
151 self.__rpc_data['total_rdma_rep'] = int(words[15])
152 self.__rpc_data['pullup'] = int(words[16])
153 self.__rpc_data['fixup'] = int(words[17])
154 self.__rpc_data['hardway'] = int(words[18])
155 self.__rpc_data['failed_marshal'] = int(words[19])
156 self.__rpc_data['bad_reply'] = int(words[20])
157 elif words[0] == 'per-op':
158 self.__rpc_data['per-op'] = words
161 self.__rpc_data['ops'] += [op]
162 self.__rpc_data[op] = [long(word) for word in words[1:]]
164 def parse_stats(self, lines):
165 """Turn a list of lines from a mount stat file into a
166 dictionary full of stats, keyed by name
173 if (not found and words[0] != 'RPC'):
174 self.__parse_nfs_line(words)
178 self.__parse_rpc_line(words)
180 def is_nfs_mountpoint(self):
181 """Return True if this is an NFS or NFSv4 mountpoint,
182 otherwise return False
184 if self.__nfs_data['fstype'] == 'nfs':
186 elif self.__nfs_data['fstype'] == 'nfs4':
190 def compare_iostats(self, old_stats):
191 """Return the difference between two sets of stats
193 result = DeviceData()
195 # copy self into result
196 for key, value in self.__nfs_data.iteritems():
197 result.__nfs_data[key] = value
198 for key, value in self.__rpc_data.iteritems():
199 result.__rpc_data[key] = value
201 # compute the difference of each item in the list
202 # note the copy loop above does not copy the lists, just
203 # the reference to them. so we build new lists here
204 # for the result object.
205 for op in result.__rpc_data['ops']:
206 result.__rpc_data[op] = map(difference, self.__rpc_data[op], old_stats.__rpc_data[op])
208 # update the remaining keys we care about
209 result.__rpc_data['rpcsends'] -= old_stats.__rpc_data['rpcsends']
210 result.__rpc_data['backlogutil'] -= old_stats.__rpc_data['backlogutil']
212 for key in NfsEventCounters:
213 result.__nfs_data[key] -= old_stats.__nfs_data[key]
214 for key in NfsByteCounters:
215 result.__nfs_data[key] -= old_stats.__nfs_data[key]
219 def __print_data_cache_stats(self):
220 """Print the data cache hit rate
222 nfs_stats = self.__nfs_data
223 app_bytes_read = float(nfs_stats['normalreadbytes'])
224 if app_bytes_read != 0:
225 client_bytes_read = float(nfs_stats['serverreadbytes'] - nfs_stats['directreadbytes'])
226 ratio = ((app_bytes_read - client_bytes_read) * 100) / app_bytes_read
229 print 'app bytes: %f client bytes %f' % (app_bytes_read, client_bytes_read)
230 print 'Data cache hit ratio: %4.2f%%' % ratio
232 def __print_attr_cache_stats(self, sample_time):
233 """Print attribute cache efficiency stats
235 nfs_stats = self.__nfs_data
236 getattr_stats = self.__rpc_data['GETATTR']
238 if nfs_stats['inoderevalidates'] != 0:
239 getattr_ops = float(getattr_stats[1])
240 opens = float(nfs_stats['vfsopen'])
241 revalidates = float(nfs_stats['inoderevalidates']) - opens
243 ratio = ((revalidates - getattr_ops) * 100) / revalidates
247 data_invalidates = float(nfs_stats['datainvalidates'])
248 attr_invalidates = float(nfs_stats['attrinvalidates'])
251 print '%d inode revalidations, hitting in cache %4.2f%% of the time' % \
253 print '%d open operations (mandatory GETATTR requests)' % opens
255 print '%4.2f%% of GETATTRs resulted in data cache invalidations' % \
256 ((data_invalidates * 100) / getattr_ops)
258 def __print_dir_cache_stats(self, sample_time):
259 """Print directory stats
261 nfs_stats = self.__nfs_data
262 lookup_ops = self.__rpc_data['LOOKUP'][0]
263 readdir_ops = self.__rpc_data['READDIR'][0]
264 if self.__rpc_data.has_key('READDIRPLUS'):
265 readdir_ops += self.__rpc_data['READDIRPLUS'][0]
267 dentry_revals = nfs_stats['dentryrevalidates']
268 opens = nfs_stats['vfsopen']
269 lookups = nfs_stats['vfslookup']
270 getdents = nfs_stats['vfsreaddir']
273 print '%d open operations (pathname lookups)' % opens
274 print '%d dentry revalidates and %d vfs lookup requests' % \
275 (dentry_revals, lookups),
276 print 'resulted in %d LOOKUPs on the wire' % lookup_ops
277 print '%d vfs getdents calls resulted in %d READDIRs on the wire' % \
278 (getdents, readdir_ops)
280 def __print_page_stats(self, sample_time):
281 """Print page cache stats
283 nfs_stats = self.__nfs_data
285 vfsreadpage = nfs_stats['vfsreadpage']
286 vfsreadpages = nfs_stats['vfsreadpages']
287 pages_read = nfs_stats['readpages']
288 vfswritepage = nfs_stats['vfswritepage']
289 vfswritepages = nfs_stats['vfswritepages']
290 pages_written = nfs_stats['writepages']
293 print '%d nfs_readpage() calls read %d pages' % \
294 (vfsreadpage, vfsreadpage)
295 print '%d nfs_readpages() calls read %d pages' % \
296 (vfsreadpages, pages_read - vfsreadpage),
297 if vfsreadpages != 0:
298 print '(%.1f pages per call)' % \
299 (float(pages_read - vfsreadpage) / vfsreadpages)
304 print '%d nfs_updatepage() calls' % nfs_stats['vfsupdatepage']
305 print '%d nfs_writepage() calls wrote %d pages' % \
306 (vfswritepage, vfswritepage)
307 print '%d nfs_writepages() calls wrote %d pages' % \
308 (vfswritepages, pages_written - vfswritepage),
309 if (vfswritepages) != 0:
310 print '(%.1f pages per call)' % \
311 (float(pages_written - vfswritepage) / vfswritepages)
315 congestionwaits = nfs_stats['congestionwait']
316 if congestionwaits != 0:
318 print '%d congestion waits' % congestionwaits
320 def __print_rpc_op_stats(self, op, sample_time):
321 """Print generic stats for one RPC op
323 if not self.__rpc_data.has_key(op):
326 rpc_stats = self.__rpc_data[op]
327 ops = float(rpc_stats[0])
328 retrans = float(rpc_stats[1] - rpc_stats[0])
329 kilobytes = float(rpc_stats[3] + rpc_stats[4]) / 1024
330 rtt = float(rpc_stats[6])
331 exe = float(rpc_stats[7])
333 # prevent floating point exceptions
335 kb_per_op = kilobytes / ops
336 retrans_percent = (retrans * 100) / ops
337 rtt_per_op = rtt / ops
338 exe_per_op = exe / ops
341 retrans_percent = 0.0
346 print '%s' % op.lower().ljust(15),
347 print ' ops/s\t\t kB/s\t\t kB/op\t\tretrans\t\tavg RTT (ms)\tavg exe (ms)'
349 print '\t\t%7.3f' % (ops / sample_time),
350 print '\t%7.3f' % (kilobytes / sample_time),
351 print '\t%7.3f' % kb_per_op,
352 print ' %7d (%3.1f%%)' % (retrans, retrans_percent),
353 print '\t%7.3f' % rtt_per_op,
354 print '\t%7.3f' % exe_per_op
356 def display_iostats(self, sample_time, which):
357 """Display NFS and RPC stats in an iostat-like way
359 sends = float(self.__rpc_data['rpcsends'])
361 sample_time = float(self.__nfs_data['age'])
363 backlog = (float(self.__rpc_data['backlogutil']) / sends) / sample_time
368 print '%s mounted on %s:' % \
369 (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
372 print ' op/s\t\trpc bklog'
373 print '%7.2f' % (sends / sample_time),
374 print '\t%7.2f' % backlog
377 self.__print_rpc_op_stats('READ', sample_time)
378 self.__print_rpc_op_stats('WRITE', sample_time)
380 self.__print_rpc_op_stats('GETATTR', sample_time)
381 self.__print_rpc_op_stats('ACCESS', sample_time)
382 self.__print_attr_cache_stats(sample_time)
384 self.__print_rpc_op_stats('LOOKUP', sample_time)
385 self.__print_rpc_op_stats('READDIR', sample_time)
386 if self.__rpc_data.has_key('READDIRPLUS'):
387 self.__print_rpc_op_stats('READDIRPLUS', sample_time)
388 self.__print_dir_cache_stats(sample_time)
390 self.__print_rpc_op_stats('READ', sample_time)
391 self.__print_rpc_op_stats('WRITE', sample_time)
392 self.__print_page_stats(sample_time)
398 def print_iostat_help(name):
399 print 'usage: %s [ <interval> [ <count> ] ] [ <options> ] [ <mount point> ] ' % name
401 print ' Version %s' % Iostats_version
403 print ' Sample iostat-like program to display NFS client per-mount statistics.'
405 print ' The <interval> parameter specifies the amount of time in seconds between'
406 print ' each report. The first report contains statistics for the time since each'
407 print ' file system was mounted. Each subsequent report contains statistics'
408 print ' collected during the interval since the previous report.'
410 print ' If the <count> parameter is specified, the value of <count> determines the'
411 print ' number of reports generated at <interval> seconds apart. If the interval'
412 print ' parameter is specified without the <count> parameter, the command generates'
413 print ' reports continuously.'
415 print ' Options include "--attr", which displays statistics related to the attribute'
416 print ' cache, "--dir", which displays statistics related to directory operations,'
417 print ' and "--page", which displays statistics related to the page cache.'
418 print ' By default, if no option is specified, statistics related to file I/O are'
421 print ' If one or more <mount point> names are specified, statistics for only these'
422 print ' mount points will be displayed. Otherwise, all NFS mount points on the'
423 print ' client are listed.'
425 def parse_stats_file(filename):
426 """pop the contents of a mountstats file into a dictionary,
427 keyed by mount point. each value object is a list of the
428 lines in the mountstats file corresponding to the mount
429 point named in the key.
435 for line in f.readlines():
439 if words[0] == 'device':
441 new = [ line.strip() ]
443 new += [ line.strip() ]
449 def print_iostat_summary(old, new, devices, time, ac):
450 for device in devices:
452 stats.parse_stats(new[device])
454 stats.display_iostats(time, ac)
456 old_stats = DeviceData()
457 old_stats.parse_stats(old[device])
458 diff_stats = stats.compare_iostats(old_stats)
459 diff_stats.display_iostats(time, ac)
461 def iostat_command(name):
462 """iostat-like command for NFS mount points
464 mountstats = parse_stats_file('/proc/self/mountstats')
467 interval_seen = False
471 if arg in ['-h', '--help', 'help', 'usage']:
472 print_iostat_help(name)
475 if arg in ['-v', '--version', 'version']:
476 print '%s version %s' % (name, Iostats_version)
479 if arg in ['-a', '--attr']:
483 if arg in ['-d', '--dir']:
487 if arg in ['-p', '--page']:
491 if arg == sys.argv[0]:
494 if arg in mountstats:
496 elif not interval_seen:
501 print 'Illegal <interval> value'
508 print 'Illegal <count> value'
511 # make certain devices contains only NFS mount points
514 for device in devices:
516 stats.parse_stats(mountstats[device])
517 if stats.is_nfs_mountpoint():
521 for device, descr in mountstats.iteritems():
523 stats.parse_stats(descr)
524 if stats.is_nfs_mountpoint():
526 if len(devices) == 0:
527 print 'No NFS mount points were found'
530 old_mountstats = None
533 if not interval_seen:
534 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, which)
539 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, which)
540 old_mountstats = mountstats
542 sample_time = interval
543 mountstats = parse_stats_file('/proc/self/mountstats')
547 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, which)
548 old_mountstats = mountstats
550 sample_time = interval
551 mountstats = parse_stats_file('/proc/self/mountstats')
556 prog = os.path.basename(sys.argv[0])
560 except KeyboardInterrupt:
561 print 'Caught ^C... exiting'