#! /usr/bin/perl # See memcached for LICENSE # Copyright 2011 Dormando (dormando@rydia.net) =head1 NAME mc_slab_mover -- example utility for slab page reassignment for memcached =head1 SYNOPSIS $ mc_slab_mover --host="127.0.0.1:11211" --verbose $ mc_slab_mover --host="127.0.0.1:11211" --automove $ mc_slab_mover --host="127.0.0.1:11211" --sleep=60 --loops=4 --automove =head1 DESCRIPTION This utility is an example implementation of an algorithm for reassigning slab memory in a running memcached instance. If memcached's built-in automover isn't working for you, you may use this script as an example base and expand on it. We welcome modifications or alternatives on the mailing list. =head1 ALGORITHM The default algorithm is simple, and may serve for a common case: over time one slab may grow in use compare to others, and as evictions stop in one slab and start in another it will reassign memory. If a slab has the most evictions three times in a row, it will pull a page from a slab which has had zero evictions three times in a row. There are many traffic patterns where this does not work well. IE: If you never use expirations and rely on the LRU (so all slabs always evict), it will not be as likely to find source pages to move. =head1 OPTIONS =over =item --host="IP:PORT" The hostname to connect to. NOTE: If connection to the host breaks, script will stop. =item --sleep=10 How long to wait between loops for gathering stats. =item --loops=3 How many loops to run before making a decision for a move. =item --verbose Prints a formatted dump of some common statistics per loop. =item --automove Enables the automover, and will attempt to move memory around if it finds viable candidates. =back =head1 AUTHOR Dormando ELE =head1 LICENSE Licensed for use and redistribution under the same terms as Memcached itself. =cut use warnings; use strict; use IO::Socket::INET; use FindBin; use Data::Dumper qw/Dumper/; use Getopt::Long; my %opts = ('sleep' => 10, automove => 0, verbose => 0, loops => 3); GetOptions( "host=s" => \$opts{host}, "sleep=i" => \$opts{'sleep'}, "loops=i" => \$opts{loops}, "automove" => \$opts{automove}, "verbose" => \$opts{verbose}, ) or usage(); die "Must specify at least --host='127.0.0.1:11211'" unless $opts{host}; my $sock = IO::Socket::INET->new(PeerAddr => $opts{host}, Timeout => 3); die "$!\n" unless $sock; my %stats = (); my %move = (winner => 0, wins => 0); $SIG{INT} = sub { print "STATS: ", Dumper(\%stats), "\n"; exit; }; $SIG{USR1} = sub { print "STATS: ", Dumper(\%stats), "\n"; }; run(); sub usage { print qq{Usage: mc_slab_ratios --host="127.0.0.1:11211" --verbose --automove run `perldoc mc_slab_ratios` for full information }; exit 1; } sub run { my $slabs_before = grab_stats(); while (1) { sleep $opts{'sleep'}; my $slabs_after = grab_stats(); my ($totals, $sorted) = calc_results_evicted($slabs_before, $slabs_after); # my ($totals, $sorted) = calc_results_numratio($slabs_before, $slabs_after); my $pct = sub { my ($num, $divisor) = @_; return 0 unless $divisor; return ($num / $divisor); }; if ($opts{verbose}) { printf " %02s: %-8s (pct ) %-10s (pct ) %-6s (pct ) get_hits (pct ) cmd_set (pct )\n", 'sb', 'evicted', 'items', 'pages'; for my $slab (@$sorted) { printf " %02d: %-8d (%.2f%%) %-10s (%.4f%%) %-6d (%.2f%%) %-8d (%.3f%%) %-7d (%.2f%%)\n", $slab->{slab}, $slab->{evicted_d}, $pct->($slab->{evicted_d}, $totals->{evicted_d}), $slab->{number}, $pct->($slab->{number}, $totals->{number}), $slab->{total_pages}, $pct->($slab->{total_pages}, $totals->{total_pages}), $slab->{get_hits_d}, $pct->($slab->{get_hits_d}, $totals->{get_hits_d}), $slab->{cmd_set_d}, $pct->($slab->{cmd_set_d}, $totals->{cmd_set_d}); } } next unless @$sorted; my $highest = $sorted->[-1]; $stats{$highest->{slab}}++; print " (winner: ", $highest->{slab}, " wins: ", $stats{$highest->{slab}}, ")\n"; automove_basic($totals, $sorted) if ($opts{automove}); $slabs_before = $slabs_after; } } sub grab_stats { my %slabs = (); for my $stat (qw/items slabs/) { print $sock "stats $stat\r\n"; while (my $line = <$sock>) { chomp $line; last if ($line =~ m/^END/); if ($line =~ m/^STAT (?:items:)?(\d+):(\S+) (\S+)/) { my ($slab, $var, $val) = ($1, $2, $3); $slabs{$slab}->{$var} = $val; } } } return \%slabs; } # Really stupid algo, same as the initial algo built into memcached. # If a slab "wins" most evictions 3 times in a row, pick from a slab which # has had 0 evictions 3 times in a row and move it over. sub automove_basic { my ($totals, $sorted) = @_; my $source = 0; my $dest = 0; my $high = $sorted->[-1]; return unless $high->{evicted_d} > 0; if ($move{winner} == $high->{slab}) { $move{wins}++; $dest = $move{winner} if $move{wins} >= $opts{loops}; } else { $move{wins} = 1; $move{winner} = $high->{slab}; } for my $slab (@$sorted) { my $id = $slab->{slab}; if ($slab->{evicted_d} == 0 && $slab->{total_pages} > 2) { $move{zeroes}->{$id}++; $source = $id if (!$source && $move{zeroes}->{$id} >= $opts{loops}); } else { delete $move{zeroes}->{$slab->{slab}} if exists $move{zeroes}->{$slab->{slab}}; } } if ($source && $dest) { print " slabs reassign $source $dest\n"; print $sock "slabs reassign $source $dest\r\n"; my $res = <$sock>; print " RES: ", $res; } elsif ($dest && !$source) { print "FAIL: want to move memory to $dest but no valid source slab available\n"; } } # Using just the evicted stats. sub calc_results_evicted { my ($slabs, $totals) = calc_slabs(@_); my @sorted = sort { $a->{evicted_d} <=> $b->{evicted_d} } values %$slabs; return ($totals, \@sorted); } # Weighted ratios of evictions vs total stored items # Seems to fail as an experiment, but it tries to weight stats. # In this case evictions in underused classes tend to get vastly inflated sub calc_results_numratio { my ($slabs, $totals) = calc_slabs(@_, sub { my ($sb, $sa, $s) = @_; if ($s->{evicted_d}) { $s->{numratio} = $s->{evicted_d} / $s->{number}; } else { $s->{numratio} = 0; } }); my @sorted = sort { $a->{numratio} <=> $b->{numratio} } values %$slabs; return ($totals, \@sorted); } sub calc_slabs { my ($slabs_before, $slabs_after, $code) = @_; my %slabs = (); my %totals = (); for my $id (keys %$slabs_after) { my $sb = $slabs_before->{$id}; my $sa = $slabs_after->{$id}; next unless ($sb && $sa); my %slab = %$sa; for my $key (keys %slab) { # Add totals, diffs if ($slab{$key} =~ m/^\d+$/) { $totals{$key} += $slab{$key}; $slab{$key . '_d'} = $sa->{$key} - $sb->{$key}; $totals{$key . '_d'} += $sa->{$key} - $sb->{$key}; } } # External code $code->($sb, $sa, \%slab) if $code; $slab{slab} = $id; $slabs{$id} = \%slab; } return (\%slabs, \%totals); }