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[0] == 'per-op':
138 self.__rpc_data['per-op'] = words
141 self.__rpc_data['ops'] += [op]
142 self.__rpc_data[op] = [long(word) for word in words[1:]]
144 def parse_stats(self, lines):
145 """Turn a list of lines from a mount stat file into a
146 dictionary full of stats, keyed by name
153 if (not found and words[0] != 'RPC'):
154 self.__parse_nfs_line(words)
158 self.__parse_rpc_line(words)
160 def is_nfs_mountpoint(self):
161 """Return True if this is an NFS or NFSv4 mountpoint,
162 otherwise return False
164 if self.__nfs_data['fstype'] == 'nfs':
166 elif self.__nfs_data['fstype'] == 'nfs4':
170 def compare_iostats(self, old_stats):
171 """Return the difference between two sets of stats
173 result = DeviceData()
175 # copy self into result
176 for key, value in self.__nfs_data.iteritems():
177 result.__nfs_data[key] = value
178 for key, value in self.__rpc_data.iteritems():
179 result.__rpc_data[key] = value
181 # compute the difference of each item in the list
182 # note the copy loop above does not copy the lists, just
183 # the reference to them. so we build new lists here
184 # for the result object.
185 for op in result.__rpc_data['ops']:
186 result.__rpc_data[op] = map(difference, self.__rpc_data[op], old_stats.__rpc_data[op])
188 # update the remaining keys we care about
189 result.__rpc_data['rpcsends'] -= old_stats.__rpc_data['rpcsends']
190 result.__rpc_data['backlogutil'] -= old_stats.__rpc_data['backlogutil']
192 for key in NfsEventCounters:
193 result.__nfs_data[key] -= old_stats.__nfs_data[key]
194 for key in NfsByteCounters:
195 result.__nfs_data[key] -= old_stats.__nfs_data[key]
199 def __print_data_cache_stats(self):
200 """Print the data cache hit rate
202 nfs_stats = self.__nfs_data
203 app_bytes_read = float(nfs_stats['normalreadbytes'])
204 if app_bytes_read != 0:
205 client_bytes_read = float(nfs_stats['serverreadbytes'] - nfs_stats['directreadbytes'])
206 ratio = ((app_bytes_read - client_bytes_read) * 100) / app_bytes_read
209 print 'app bytes: %f client bytes %f' % (app_bytes_read, client_bytes_read)
210 print 'Data cache hit ratio: %4.2f%%' % ratio
212 def __print_attr_cache_stats(self, sample_time):
213 """Print attribute cache efficiency stats
215 nfs_stats = self.__nfs_data
216 getattr_stats = self.__rpc_data['GETATTR']
218 if nfs_stats['inoderevalidates'] != 0:
219 getattr_ops = float(getattr_stats[1])
220 opens = float(nfs_stats['vfsopen'])
221 revalidates = float(nfs_stats['inoderevalidates']) - opens
223 ratio = ((revalidates - getattr_ops) * 100) / revalidates
227 data_invalidates = float(nfs_stats['datainvalidates'])
228 attr_invalidates = float(nfs_stats['attrinvalidates'])
231 print '%d inode revalidations, hitting in cache %4.2f%% of the time' % \
233 print '%d open operations (mandatory GETATTR requests)' % opens
235 print '%4.2f%% of GETATTRs resulted in data cache invalidations' % \
236 ((data_invalidates * 100) / getattr_ops)
238 def __print_dir_cache_stats(self, sample_time):
239 """Print directory stats
241 nfs_stats = self.__nfs_data
242 lookup_ops = self.__rpc_data['LOOKUP'][0]
243 readdir_ops = self.__rpc_data['READDIR'][0]
244 if self.__rpc_data.has_key('READDIRPLUS'):
245 readdir_ops += self.__rpc_data['READDIRPLUS'][0]
247 dentry_revals = nfs_stats['dentryrevalidates']
248 opens = nfs_stats['vfsopen']
249 lookups = nfs_stats['vfslookup']
250 getdents = nfs_stats['vfsreaddir']
253 print '%d open operations (pathname lookups)' % opens
254 print '%d dentry revalidates and %d vfs lookup requests' % \
255 (dentry_revals, lookups),
256 print 'resulted in %d LOOKUPs on the wire' % lookup_ops
257 print '%d vfs getdents calls resulted in %d READDIRs on the wire' % \
258 (getdents, readdir_ops)
260 def __print_page_stats(self, sample_time):
261 """Print page cache stats
263 nfs_stats = self.__nfs_data
265 vfsreadpage = nfs_stats['vfsreadpage']
266 vfsreadpages = nfs_stats['vfsreadpages']
267 pages_read = nfs_stats['readpages']
268 vfswritepage = nfs_stats['vfswritepage']
269 vfswritepages = nfs_stats['vfswritepages']
270 pages_written = nfs_stats['writepages']
273 print '%d nfs_readpage() calls read %d pages' % \
274 (vfsreadpage, vfsreadpage)
275 print '%d nfs_readpages() calls read %d pages' % \
276 (vfsreadpages, pages_read - vfsreadpage),
277 if vfsreadpages != 0:
278 print '(%.1f pages per call)' % \
279 (float(pages_read - vfsreadpage) / vfsreadpages)
284 print '%d nfs_updatepage() calls' % nfs_stats['vfsupdatepage']
285 print '%d nfs_writepage() calls wrote %d pages' % \
286 (vfswritepage, vfswritepage)
287 print '%d nfs_writepages() calls wrote %d pages' % \
288 (vfswritepages, pages_written - vfswritepage),
289 if (vfswritepages) != 0:
290 print '(%.1f pages per call)' % \
291 (float(pages_written - vfswritepage) / vfswritepages)
295 congestionwaits = nfs_stats['congestionwait']
296 if congestionwaits != 0:
298 print '%d congestion waits' % congestionwaits
300 def __print_rpc_op_stats(self, op, sample_time):
301 """Print generic stats for one RPC op
303 if not self.__rpc_data.has_key(op):
306 rpc_stats = self.__rpc_data[op]
307 ops = float(rpc_stats[0])
308 retrans = float(rpc_stats[1] - rpc_stats[0])
309 kilobytes = float(rpc_stats[3] + rpc_stats[4]) / 1024
310 rtt = float(rpc_stats[6])
311 exe = float(rpc_stats[7])
313 # prevent floating point exceptions
315 kb_per_op = kilobytes / ops
316 retrans_percent = (retrans * 100) / ops
317 rtt_per_op = rtt / ops
318 exe_per_op = exe / ops
321 retrans_percent = 0.0
326 print '%s' % op.lower().ljust(15),
327 print ' ops/s\t\t Kb/s\t\t Kb/op\t\tretrans\t\tavg RTT (ms)\tavg exe (ms)'
329 print '\t\t%7.3f' % (ops / sample_time),
330 print '\t%7.3f' % (kilobytes / sample_time),
331 print '\t%7.3f' % kb_per_op,
332 print ' %7d (%3.1f%%)' % (retrans, retrans_percent),
333 print '\t%7.3f' % rtt_per_op,
334 print '\t%7.3f' % exe_per_op
336 def display_iostats(self, sample_time, which):
337 """Display NFS and RPC stats in an iostat-like way
339 sends = float(self.__rpc_data['rpcsends'])
341 sample_time = float(self.__nfs_data['age'])
343 backlog = (float(self.__rpc_data['backlogutil']) / sends) / sample_time
348 print '%s mounted on %s:' % \
349 (self.__nfs_data['export'], self.__nfs_data['mountpoint'])
352 print ' op/s\t\trpc bklog'
353 print '%7.2f' % (sends / sample_time),
354 print '\t%7.2f' % backlog
357 self.__print_rpc_op_stats('READ', sample_time)
358 self.__print_rpc_op_stats('WRITE', sample_time)
360 self.__print_rpc_op_stats('GETATTR', sample_time)
361 self.__print_rpc_op_stats('ACCESS', sample_time)
362 self.__print_attr_cache_stats(sample_time)
364 self.__print_rpc_op_stats('LOOKUP', sample_time)
365 self.__print_rpc_op_stats('READDIR', sample_time)
366 if self.__rpc_data.has_key('READDIRPLUS'):
367 self.__print_rpc_op_stats('READDIRPLUS', sample_time)
368 self.__print_dir_cache_stats(sample_time)
370 self.__print_rpc_op_stats('READ', sample_time)
371 self.__print_rpc_op_stats('WRITE', sample_time)
372 self.__print_page_stats(sample_time)
378 def print_iostat_help(name):
379 print 'usage: %s [ <interval> [ <count> ] ] [ <options> ] [ <mount point> ] ' % name
381 print ' Version %s' % Iostats_version
383 print ' Sample iostat-like program to display NFS client per-mount statistics.'
385 print ' The <interval> parameter specifies the amount of time in seconds between'
386 print ' each report. The first report contains statistics for the time since each'
387 print ' file system was mounted. Each subsequent report contains statistics'
388 print ' collected during the interval since the previous report.'
390 print ' If the <count> parameter is specified, the value of <count> determines the'
391 print ' number of reports generated at <interval> seconds apart. If the interval'
392 print ' parameter is specified without the <count> parameter, the command generates'
393 print ' reports continuously.'
395 print ' Options include "--attr", which displays statistics related to the attribute'
396 print ' cache, "--dir", which displays statistics related to directory operations,'
397 print ' and "--page", which displays statistics related to the page cache.'
398 print ' By default, if no option is specified, statistics related to file I/O are'
401 print ' If one or more <mount point> names are specified, statistics for only these'
402 print ' mount points will be displayed. Otherwise, all NFS mount points on the'
403 print ' client are listed.'
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.
415 for line in f.readlines():
419 if words[0] == 'device':
421 new = [ line.strip() ]
423 new += [ line.strip() ]
429 def print_iostat_summary(old, new, devices, time, ac):
430 for device in devices:
432 stats.parse_stats(new[device])
434 stats.display_iostats(time, ac)
436 old_stats = DeviceData()
437 old_stats.parse_stats(old[device])
438 diff_stats = stats.compare_iostats(old_stats)
439 diff_stats.display_iostats(time, ac)
441 def iostat_command(name):
442 """iostat-like command for NFS mount points
444 mountstats = parse_stats_file('/proc/self/mountstats')
447 interval_seen = False
451 if arg in ['-h', '--help', 'help', 'usage']:
452 print_iostat_help(name)
455 if arg in ['-v', '--version', 'version']:
456 print '%s version %s' % (name, Iostats_version)
459 if arg in ['-a', '--attr']:
463 if arg in ['-d', '--dir']:
467 if arg in ['-p', '--page']:
471 if arg == sys.argv[0]:
474 if arg in mountstats:
476 elif not interval_seen:
481 print 'Illegal <interval> value'
488 print 'Illegal <count> value'
491 # make certain devices contains only NFS mount points
494 for device in devices:
496 stats.parse_stats(mountstats[device])
497 if stats.is_nfs_mountpoint():
501 for device, descr in mountstats.iteritems():
503 stats.parse_stats(descr)
504 if stats.is_nfs_mountpoint():
506 if len(devices) == 0:
507 print 'No NFS mount points were found'
510 old_mountstats = None
513 if not interval_seen:
514 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, which)
519 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, which)
520 old_mountstats = mountstats
522 sample_time = interval
523 mountstats = parse_stats_file('/proc/self/mountstats')
527 print_iostat_summary(old_mountstats, mountstats, devices, sample_time, which)
528 old_mountstats = mountstats
530 sample_time = interval
531 mountstats = parse_stats_file('/proc/self/mountstats')
536 prog = os.path.basename(sys.argv[0])
540 except KeyboardInterrupt:
541 print 'Caught ^C... exiting'