#!/usr/bin/perl -w ################################################################################ # memcache-top.pl # # "top" for memcache - watch the traffic and other stats in real-time. Yoikes. # # NOTES: # # If Getopt::Long is installed: # - Specify instances w/ --instances (multiple times or comma separated) # - Specify default port w/ --port (defaults to 11211) # - Specify sleep time w/ --sleep (default 3) # - Specify color output w/ --color (default) or --nocolor # - Specify lifetime stats w/ --lifetime or --nolifetime (default) # NOTE: lifetime stats break thresholds for evictions, bytes. # - Specify read and write bytes w/ --bytes (default) or --nobytes # - Specify get and set commands w/ --commands or --nocommands (default) # - Specify cumulative numbers w/ --cumulative (don't use with lifetime) # # If Getopt::Long is not installed: # - Specify sleep time by typing a number after the command. # Specify instances in @default_instances. # Specify thresholds by defining %threshold. # Written against memcached v 1.2.3, but works fine w/ later versions. # # HISTORY: # # v0.3 - 2009-04-22 - ntang # Minor cleanups. First release to google code: # http://code.google.com/p/memcache-top/ # v0.4 - 2009-04-23 - ntang # Added ability to specify color, sleep time, and servers on command line. # Also added checks for Getopt::Long and Term::ANSIColor. # Added $default_port = "11211" and padding for short server names. Server # names over 23 characters inc. port will break the column lineups for now. # v0.4b - 2009-04-23 - ntang # Added total capacity, and changed "SERVER" to "INSTANCE" to be more clear. # Server now is the hostname, instance is hostname + port. It will # truncate the instance and/or server to fit inside the first column # correctly. (Yay!) It'll also truncate long reads/ writes (or # technically any number) to K or M or G if it exceeds certain limits for # readability. # v0.5 - 2009-04-24 - ntang # Cleaned up instances vs. servers so it's internally consistent. # Redid printing so that now it stores it all and only refreshes/ prints when # it has the full set of data. Warning: major hackishness. # Switched to per-second stats by default w/ lifetime stats available. # v0.6 - 2009-04-28 - ntang # I lied. One more change... the ability to specify read/write bytes, or # get/set commands, or both. Bear in mind if you specify both you will # exceed the width of a standard terminal! You've been warned. :P # Also, some minor display changes, etc. etc. # ################################################################################ use strict; use IO::Socket; use Time::HiRes 'time'; my (@default_instances, @instances, $remote, $sleep, %threshold, %laststats, $usecolor, @keys, $default_port, $version, @out, $lifetime, $bytes, $commands, $cumulative); $version = "0.6"; ################################################################################ # CONFIGURATION # Set $usecolor to 1 to push @out,in, gasp, color. $usecolor = 1; # 'Alert' threshold values at which to color the text red. %threshold = ( cache_hit => 60, # Cache hit ratio usage => 90, # % space used time => 5, # Number of ms to run the stats query evictions => 0, # Number of evictions per second curr_connections => 3500, # Number of current connections bytes_read => 1000000, # Bytes read, per second bytes_written => 1000000, # Bytes written, per second limit_maxbytes => 0, # Total space allocated bytes => 0, # Total space used cmd_get => 1000, # Get commands cmd_set => 1000, # Set commands ); # Display lifetime stats instead of per-second stats $lifetime = 0; # Display read/write bytes $bytes = 1; # Display get/set commands $commands = 0; # Show cumulative stats (since start of run) $cumulative = 0; # Default time to sleep in-between refreshes. $sleep = 3; # List of servers/ ports to query. @default_instances = ( '127.0.0.1:11211', ); # Default port to connect to, if not specified $default_port = "11211"; # END CONFIGURATION ################################################################################ @keys = ('usage', 'cache_hit', 'curr_connections', 'time', 'cmd_get', 'cmd_set', 'bytes_read', 'bytes_written', 'evictions', 'limit_maxbytes', 'bytes'); if (@ARGV) { eval { require Getopt::Long; }; if ($@) { if ( $ARGV[0] =~ /^\d+$/ ) { $sleep = $ARGV[0]; } else { die "USAGE: memcache-top.pl \n"; } } else { use Getopt::Long; GetOptions ( 'instances=s' => \@instances, 'sleep=i' => \$sleep, 'port=i' => \$default_port, 'color!' => \$usecolor, 'lifetime!' => \$lifetime, 'bytes!' => \$bytes, 'commands!' => \$commands, 'cumulative!' => \$cumulative, ); if (@instances) { @instances = split(/,/,join(',',@instances)); } else { @instances = @default_instances; } } } else { @instances = @default_instances; } if ( $lifetime && $cumulative ) { $lifetime = 0; } if ( $usecolor ) { eval { require Term::ANSIColor; }; if ($@) { $usecolor = 0; } else { use Term::ANSIColor; } } my $i = 1; my (%original); while ($i) { @out = (); push @out,"\033[2J"; # This clears the screen, yo. push @out,color 'bold' if $usecolor; push @out,"\nmemcache-top v$version\t"; push @out,color 'reset' if $usecolor; push @out,"(default port: " . sprintf("%5d",$default_port) . ", color: "; push @out,"on," if $usecolor; push @out,"off," unless $usecolor; push @out," refresh: $sleep seconds)\n\n"; push @out,color 'bold' if $usecolor; push @out,"INSTANCE\t\tUSAGE\tHIT %\tCONN\tTIME\t"; if ( $lifetime || $cumulative ) { push @out,"EVICT\t"; push @out,"GETS\tSETS\t" if $commands; push @out,"READ\tWRITE\t" if $bytes; push @out,"\n"; } else { push @out,"EVICT/s "; push @out,"GETS/s\tSETS/s\t" if $commands; push @out,"READ/s\tWRITE/s\t" if $bytes; push @out,"\n"; } push @out,color 'reset' if $usecolor; my %tot; foreach my $key (@keys) { $tot{$key} = 0; } my $count = 0; foreach my $instance (@instances) { my ($port, $server); my @split = split(/:/,$instance); unless ( $split[1] ) { $instance = $instance . ":" . $default_port; $port = $default_port; } else { $port = $split[1]; } # Some exhaustive (exhausting?) logic to determine the ideal text to push @out,for # the server name. if ( length($instance) > 22 ) { if ( $port ne $default_port ) { $server = substr($split[0],0,17) . ":" . $port; } else { if ( length($split[0]) < 18 ) { $server = $instance; } else { $server = substr($split[0],0,23); } } } elsif ( length($instance) < 8 ) { $server = "$instance\t\t"; } elsif ( length($instance) < 16 ) { $server = "$instance\t"; } else { $server = $instance; } my $t0 = time(); $remote = IO::Socket::INET->new($instance); unless ( defined($remote) ) { push @out,color 'red' if $usecolor; push @out,$instance . " is DOWN.\n"; $count++; push @out,color 'reset' if $usecolor; next; } $remote->autoflush(1); $count++; print $remote "stats\n"; my (%stats, %outstats); foreach my $key (@keys) { $outstats{$key} = 0; } LINE: while ( defined ( my $line = <$remote> ) ) { last LINE if ( $line =~ /END/ ); chomp $line; my @bits = split(' ',$line); $stats{$bits[1]} = $bits[2]; next LINE; } close $remote; my $t1 = time(); $outstats{time} = ($t1 - $t0) * 1000; if ( $lifetime || $cumulative) { foreach my $key ('cmd_get', 'cmd_set', 'get_hits', 'get_misses', 'evictions', 'bytes_read', 'bytes_written') { if ( $cumulative ) { if ( $i == 1 ) { $original{$instance}{$key} = $stats{$key}; } else { $outstats{$key} = $stats{$key} - $original{$instance}{$key}; } } else { $outstats{$key} = $stats{$key}; } } $outstats{cache_hit} = ( $stats{get_hits} / $stats{cmd_get} ) * 100; } else { foreach my $key ('cmd_get', 'cmd_set', 'get_hits', 'get_misses', 'evictions', 'bytes_read', 'bytes_written') { if ( defined ( $laststats{$instance}{$key} ) ) { $outstats{$key} = ($stats{$key} - $laststats{$instance}{$key}) / $sleep; } } $outstats{cache_hit} = 0; if ( defined($outstats{get_misses}) && $outstats{get_misses} > 0 ) { $outstats{cache_hit} = ( $laststats{$instance}{get_hits} / $laststats{$instance}{cmd_get} ) * 100; } } $outstats{limit_maxbytes} = $stats{limit_maxbytes}; $outstats{bytes} = $stats{bytes}; $outstats{usage} = ( $stats{bytes} / $stats{limit_maxbytes} * 100 ); $outstats{curr_connections} = $stats{curr_connections}; if ( $cumulative ) { foreach my $key ('cmd_get', 'cmd_set', 'get_hits', 'get_misses', 'evictions', 'bytes_read', 'bytes_written') { $threshold{$key} = $threshold{$key} * $i if $threshold{$key}; } } push @out,"$server\t"; threshold_print( $outstats{usage}, $threshold{usage}, 1, 0, '%', '%.1f'); threshold_print( $outstats{cache_hit}, $threshold{cache_hit}, 0, 0, '%', '%.1f'); threshold_print( $outstats{curr_connections}, $threshold{curr_connections}, 1, 0, '', '%.0d'); if ( $outstats{time} >= 1000 ) { threshold_print( $outstats{time}/1000, $threshold{time}/1000, 1, 0, 's', '%.2f'); } else { threshold_print( $outstats{time}, $threshold{time}, 1, 0, 'ms', '%.1f'); } threshold_print( $outstats{evictions}, $threshold{evictions}, 1, 0, '', '%.1f'); if ( $commands ) { threshold_print( $outstats{cmd_get}, $threshold{cmd_get}, 1, 0, '', '%.0f'); threshold_print( $outstats{cmd_set}, $threshold{cmd_set}, 1, 0, '', '%.0f'); } if ( $bytes ) { threshold_print( $outstats{bytes_read}, $threshold{bytes_read}, 1, 0, '', '%.0f'); threshold_print( $outstats{bytes_written}, $threshold{bytes_written}, 1, 0, '', '%.0f'); } push @out,"\n"; foreach my $key (@keys) { $tot{$key} = $tot{$key} + $outstats{$key}; } unless ( $lifetime || $cumulative ) { foreach my $key ('cmd_get', 'cmd_set', 'get_hits', 'get_misses', 'evictions', 'bytes_read', 'bytes_written') { $laststats{$instance}{$key} = $stats{$key}; } } } push @out,color 'bold' if $usecolor; push @out,"\nAVERAGE:\t\t"; threshold_print( $tot{usage}/$count, $threshold{usage}, 1, 1, '%', '%.1f'); threshold_print( $tot{cache_hit}/$count, $threshold{cache_hit}, 0, 1, '%', '%.1f'); threshold_print( $tot{curr_connections}/$count, $threshold{curr_connections}, 1, 1, '', '%.0d'); if ( ( $tot{time}/$count ) >= 1000 ) { threshold_print( ($tot{time}/$count)/1000, $threshold{time}/1000, 1, 1, 's', '%.2f'); } else { threshold_print( $tot{time}/$count, $threshold{time}, 1, 1, 'ms', '%.1f'); } threshold_print( $tot{evictions}/$count, $threshold{evictions}, 1, 1, '', '%.1f'); if ( $commands ) { threshold_print( $tot{cmd_get}/$count, $threshold{cmd_get}, 1, 1, '', '%.0f'); threshold_print( $tot{cmd_set}/$count, $threshold{cmd_set}, 1, 1, '', '%.0f'); } if ( $bytes ) { threshold_print( $tot{bytes_read}/$count, $threshold{bytes_read}, 1, 1, '', '%.0f'); threshold_print( $tot{bytes_written}/$count, $threshold{bytes_written}, 1, 1, '', '%.0f'); } push @out,"\n"; push @out,"\nTOTAL:\t\t"; threshold_print( $tot{bytes}, $threshold{bytes}, 0, 1, 'B/', '%.0f'); threshold_print( $tot{limit_maxbytes}, $threshold{limit_maxbytes}, 0, 1, "B\t", '%.0f'); threshold_print( $tot{curr_connections}, $threshold{curr_connections}*$count, 1, 1, '', '%.0d'); if ( $tot{time} >= 1000 ) { threshold_print( $tot{time}/1000, ($threshold{time}*$count)/1000, 1, 1, 's', '%.2f'); } else { threshold_print( $tot{time}, $threshold{time}*$count, 1, 1, 'ms', '%.1f'); } threshold_print( $tot{evictions}, $threshold{evictions}*$count, 1, 1, '', '%.1f'); if ( $commands ) { threshold_print( $tot{cmd_get}, $threshold{cmd_get}*$count, 1, 1, '', '%.0f'); threshold_print( $tot{cmd_set}, $threshold{cmd_set}*$count, 1, 1, '', '%.0f'); } if ( $bytes ) { threshold_print( $tot{bytes_read}, $threshold{bytes_read}*$count, 1, 1, '', '%.0f'); threshold_print( $tot{bytes_written}, $threshold{bytes_written}*$count, 1, 1, '', '%.0f'); } push @out,color 'reset' if $usecolor; push @out,"\n(ctrl-c to quit.)\n"; sleep($sleep); print @out; $i++; } ################################################################################ # threshold_print # takes two variables, compares them (greater then if $gt == 1), and then prints # it. It uses red as the default color for successful comparisons, but sets # it to red bold if $bold == 1. $trail specifies trailing characters to print. # $sprintf lets you specify the format for sprintf(). # sub threshold_print { my ($stat, $threshold, $gt, $bold, $trail, $sprintf) = @_; my $color = 'red'; my $offcolor = 'reset'; if ( $bold ) { $color = 'bold red'; $offcolor = 'reset bold'; } if ( $gt ) { if ( $stat > $threshold ) { push @out, color $color if $usecolor; } } else { if ( $stat < $threshold ) { push @out, color $color if $usecolor; } } if ( $stat > 999999999999 ) { $stat = $stat / (1024*1024*1024*1024); $trail = 'T' . $trail; $sprintf = '%.1f'; } elsif ( $stat > 99999999 ) { $stat = $stat / (1024*1024*1024); $trail = "G" . $trail; $sprintf = '%.1f'; } elsif ( $stat > 999999 ) { $stat = $stat / (1024*1024); $trail = 'M' . $trail; $sprintf = '%.1f'; } elsif ( $stat > 9999 ) { $stat = $stat/1024; $trail = 'K' . $trail; $sprintf = '%.1f'; } push @out,sprintf($sprintf,$stat) . $trail; push @out,color $offcolor if $usecolor; push @out,"\t"; } ################################################################################