#!/usr/bin/perl # Copyright (c) 2011, SIOS Technology, Corp. # Author: Paul Clements use strict; sub msg { printf STDERR @_; } sub dbg { return if (! $ENV{'ROC_DEBUG'}); msg @_; } $0 =~ s@^.*/@@; # basename sub usage { msg "Usage: $0 [dev-list]\n"; msg "\n"; msg "This utility takes a /proc/diskstats output file that contains\n"; msg "output, logged over time, and calculates the rate of change of\n"; msg "the disks in the dataset\n"; msg "OUTPUT_CSV=1 set in env. dumps the full stats to a CSV file on STDERR\n"; msg "\n"; msg "Example: $0 1hour \"jun 23 12pm\" steeleye-iostat.txt sdg,sdh\n"; msg "\n"; msg "interval - interval between samples\n"; msg "start time - the time when the sampling starts\n"; msg "iostat-data-file - collect this with a cron job like:\n"; msg "\t0 * * * * (date ; cat /proc/diskstats) >> /root/diskstats.txt\n"; msg "dev-list - list of disks you want ROC for (leave blank for all)\n"; exit 1; } usage if (@ARGV < 3); my $interval = TimeHuman($ARGV[0]); my $starttime = epoch($ARGV[1]); my $file = $ARGV[2]; my $blksize = 512; # /proc/diskstats is in sectors my %devs = map { $_ => 1 } split /,/, $ARGV[3]; my %stat; my $firsttime; my $lasttime; # datestamp divides output my %days = ( 'Sun' => 1, 'Mon' => 1, 'Tue' => 1, 'Wed' => 1, 'Thu' => 1, 'Fri' => 1, 'Sat' => 1); my %fields = ( 'major' => 0, 'minor' => 1, 'dev' => 2, 'reads' => 3, 'reads_merged' => 4, 'sectors_read' => 5, 'ms_time_reading' => 6, 'writes' => 7, 'writes_merged' => 8, 'sectors_written' => 9, 'ms_time_writing' => 10, 'ios_pending' => 11, 'ms_time_total' => 12, 'weighted_ms_time_total' => 13 ); my $devfield = $fields{'dev'}; my $calcfield = $ENV{'ROC_CALC_FIELD'} || $fields{'sectors_written'}; dbg "using field $calcfield\n"; open(FD, "$file") or die "Cannot open $file: $!\n"; foreach () { chomp; @_ = split; if (exists($days{$_[0]})) { # skip datestamp divider if ($firsttime eq '') { $firsttime = join ' ', @_[0..5]; } $lasttime = join ' ', @_[0..5]; next; } next if ($_[0] !~ /[0-9]/); # ignore if (!%devs || exists $devs{$_[$devfield]}) { push @{$stat{$_[$devfield]}}, $_[$calcfield]; } } @{$stat{'total'}} = totals(\%stat); printf "Sample start time: %s\n", scalar(localtime($starttime)); printf "Sample end time: %s\n", scalar(localtime($starttime + ((@{$stat{'total'}} - 1) * $interval))); printf "Sample interval: %ss #Samples: %s Sample length: %ss\n", $interval, (@{$stat{'total'}} - 1), (@{$stat{'total'}} - 1) * $interval; print "(Raw times from file: $firsttime, $lasttime)\n"; print "Rate of change for devices " . (join ', ', sort keys %stat) . "\n"; foreach (sort keys %stat) { my @vals = @{$stat{$_}}; my ($max, $maxindex, $roc) = roc($_, $blksize, $interval, @vals); printf "$_ peak:%sB/s (%sb/s) (@ %s) average:%sB/s (%sb/s)\n", HumanSize($max), HumanSize($max * 8), scalar localtime($starttime + ($maxindex * $interval)), HumanSize($roc), HumanSize($roc * 8); } # functions sub roc { my $dev = shift; my $blksize = shift; my $interval = shift; my ($max, $maxindex, $i, $first, $last, $total); my $prev = -1; my $first = $_[0]; if ($ENV{'OUTPUT_CSV'}) { print STDERR "$dev," } foreach (@_) { if ($prev != -1) { if ($_ < $prev) { dbg "wrap detected at $i ($_ < $prev)\n"; $prev = 0; } my $this = ($_ - $prev) * $blksize / $interval; if ($this > $max) { $max = $this; $maxindex = $i; } if ($ENV{'OUTPUT_CSV'}) { print STDERR "$this," } } $prev = $_; # store current val for next time around $last = $_; $i++; } if ($ENV{'OUTPUT_CSV'}) { print STDERR "\n" } return ($max, $maxindex, ($last - $first) * $blksize / ($interval * ($i - 1))); } sub totals { # params: stat_hash my $stat = shift; my @totalvals; foreach (keys %$stat) { next if (!defined($stat{$_})); my @vals = @{$stat{$_}}; my $i; foreach (@vals) { $totalvals[$i++] += $_; } } return @totalvals; } # converts to KB, MB, etc. and outputs size in readable form sub HumanSize { # params: bytes/bits my $bytes = shift; my @suffixes = ( '', 'K', 'M', 'G', 'T', 'P' ); my $i = 0; while ($bytes / 1024.0 >= 1) { $bytes /= 1024.0; $i++; } return sprintf("%.1f %s", $bytes, $suffixes[$i]); } # convert human-readable time interval to number of seconds sub TimeHuman { # params: human_time my $time = shift; my %suffixes = ('s' => 1, 'm' => 60, 'h' => 60 * 60, 'd' => 60 * 60 * 24); $time =~ /^([0-9]*)(.*?)$/; $time = $1; my $suffix = (split //, $2)[0]; # first letter from suffix if (exists $suffixes{$suffix}) { $time *= $suffixes{$suffix}; } return $time; } sub epoch { # params: date my $date = shift; my $seconds = `date +'%s' --date "$date" 2>&1`; if ($? != 0) { die "Failed to recognize time stamp: $date\n"; } return $seconds; }