1#! /usr/bin/perl 2# See memcached for LICENSE 3# Copyright 2011 Dormando ([email protected]) 4 5=head1 NAME 6 7mc_slab_mover -- example utility for slab page reassignment for memcached 8 9=head1 SYNOPSIS 10 11 $ mc_slab_mover --host="127.0.0.1:11211" --verbose 12 $ mc_slab_mover --host="127.0.0.1:11211" --automove 13 $ mc_slab_mover --host="127.0.0.1:11211" --sleep=60 --loops=4 --automove 14 15=head1 DESCRIPTION 16 17This utility is an example implementation of an algorithm for reassigning 18slab memory in a running memcached instance. If memcached's built-in 19automover isn't working for you, you may use this script as an example 20base and expand on it. We welcome modifications or alternatives on the 21mailing list. 22 23=head1 ALGORITHM 24 25The default algorithm is simple, and may serve for a common case: over 26time one slab may grow in use compare to others, and as evictions stop 27in one slab and start in another it will reassign memory. 28 29If a slab has the most evictions three times in a row, it will pull a page 30from a slab which has had zero evictions three times in a row. 31 32There are many traffic patterns where this does not work well. IE: If you 33never use expirations and rely on the LRU (so all slabs always evict), 34it will not be as likely to find source pages to move. 35 36=head1 OPTIONS 37 38=over 39 40=item --host="IP:PORT" 41 42The hostname to connect to. NOTE: If connection to the host breaks, script 43will stop. 44 45=item --sleep=10 46 47How long to wait between loops for gathering stats. 48 49=item --loops=3 50 51How many loops to run before making a decision for a move. 52 53=item --verbose 54 55Prints a formatted dump of some common statistics per loop. 56 57=item --automove 58 59Enables the automover, and will attempt to move memory around if it finds 60viable candidates. 61 62=back 63 64=head1 AUTHOR 65 66Dormando E<lt>L<[email protected]>E<gt> 67 68=head1 LICENSE 69 70Licensed for use and redistribution under the same terms as Memcached itself. 71 72=cut 73 74use warnings; 75use strict; 76 77use IO::Socket::INET; 78 79use FindBin; 80use Data::Dumper qw/Dumper/; 81use Getopt::Long; 82 83my %opts = ('sleep' => 10, automove => 0, verbose => 0, loops => 3); 84GetOptions( 85 "host=s" => \$opts{host}, 86 "sleep=i" => \$opts{'sleep'}, 87 "loops=i" => \$opts{loops}, 88 "automove" => \$opts{automove}, 89 "verbose" => \$opts{verbose}, 90 ) or usage(); 91 92die "Must specify at least --host='127.0.0.1:11211'" unless $opts{host}; 93my $sock = IO::Socket::INET->new(PeerAddr => $opts{host}, 94 Timeout => 3); 95die "$!\n" unless $sock; 96 97my %stats = (); 98my %move = (winner => 0, wins => 0); 99 100$SIG{INT} = sub { 101 print "STATS: ", Dumper(\%stats), "\n"; 102 exit; 103}; 104$SIG{USR1} = sub { 105 print "STATS: ", Dumper(\%stats), "\n"; 106}; 107run(); 108 109sub usage { 110 print qq{Usage: 111 mc_slab_ratios --host="127.0.0.1:11211" --verbose --automove 112 run `perldoc mc_slab_ratios` for full information 113 114}; 115 exit 1; 116} 117 118sub run { 119 my $slabs_before = grab_stats(); 120 121 while (1) { 122 sleep $opts{'sleep'}; 123 my $slabs_after = grab_stats(); 124 125 my ($totals, $sorted) = calc_results_evicted($slabs_before, $slabs_after); 126# my ($totals, $sorted) = calc_results_numratio($slabs_before, $slabs_after); 127 128 my $pct = sub { 129 my ($num, $divisor) = @_; 130 return 0 unless $divisor; 131 return ($num / $divisor); 132 }; 133 if ($opts{verbose}) { 134 printf " %02s: %-8s (pct ) %-10s (pct ) %-6s (pct ) get_hits (pct ) cmd_set (pct )\n", 135 'sb', 'evicted', 'items', 'pages'; 136 for my $slab (@$sorted) { 137 printf " %02d: %-8d (%.2f%%) %-10s (%.4f%%) %-6d (%.2f%%) %-8d (%.3f%%) %-7d (%.2f%%)\n", 138 $slab->{slab}, $slab->{evicted_d}, 139 $pct->($slab->{evicted_d}, $totals->{evicted_d}), 140 $slab->{number}, 141 $pct->($slab->{number}, $totals->{number}), 142 $slab->{total_pages}, 143 $pct->($slab->{total_pages}, $totals->{total_pages}), 144 $slab->{get_hits_d}, 145 $pct->($slab->{get_hits_d}, $totals->{get_hits_d}), 146 $slab->{cmd_set_d}, 147 $pct->($slab->{cmd_set_d}, $totals->{cmd_set_d}); 148 } 149 } 150 151 next unless @$sorted; 152 my $highest = $sorted->[-1]; 153 $stats{$highest->{slab}}++; 154 print " (winner: ", $highest->{slab}, " wins: ", $stats{$highest->{slab}}, ")\n"; 155 automove_basic($totals, $sorted) if ($opts{automove}); 156 157 $slabs_before = $slabs_after; 158 } 159} 160 161sub grab_stats { 162 my %slabs = (); 163 for my $stat (qw/items slabs/) { 164 print $sock "stats $stat\r\n"; 165 while (my $line = <$sock>) { 166 chomp $line; 167 last if ($line =~ m/^END/); 168 if ($line =~ m/^STAT (?:items:)?(\d+):(\S+) (\S+)/) { 169 my ($slab, $var, $val) = ($1, $2, $3); 170 $slabs{$slab}->{$var} = $val; 171 } 172 } 173 } 174 175 return \%slabs; 176} 177 178# Really stupid algo, same as the initial algo built into memcached. 179# If a slab "wins" most evictions 3 times in a row, pick from a slab which 180# has had 0 evictions 3 times in a row and move it over. 181sub automove_basic { 182 my ($totals, $sorted) = @_; 183 184 my $source = 0; 185 my $dest = 0; 186 my $high = $sorted->[-1]; 187 return unless $high->{evicted_d} > 0; 188 if ($move{winner} == $high->{slab}) { 189 $move{wins}++; 190 $dest = $move{winner} if $move{wins} >= $opts{loops}; 191 } else { 192 $move{wins} = 1; 193 $move{winner} = $high->{slab}; 194 } 195 for my $slab (@$sorted) { 196 my $id = $slab->{slab}; 197 if ($slab->{evicted_d} == 0 && $slab->{total_pages} > 2) { 198 $move{zeroes}->{$id}++; 199 $source = $id if (!$source && $move{zeroes}->{$id} >= $opts{loops}); 200 } else { 201 delete $move{zeroes}->{$slab->{slab}} 202 if exists $move{zeroes}->{$slab->{slab}}; 203 } 204 } 205 206 if ($source && $dest) { 207 print " slabs reassign $source $dest\n"; 208 print $sock "slabs reassign $source $dest\r\n"; 209 my $res = <$sock>; 210 print " RES: ", $res; 211 } elsif ($dest && !$source) { 212 print "FAIL: want to move memory to $dest but no valid source slab available\n"; 213 } 214} 215 216# Using just the evicted stats. 217sub calc_results_evicted { 218 my ($slabs, $totals) = calc_slabs(@_); 219 my @sorted = sort { $a->{evicted_d} <=> $b->{evicted_d} } values %$slabs; 220 return ($totals, \@sorted); 221} 222 223# Weighted ratios of evictions vs total stored items 224# Seems to fail as an experiment, but it tries to weight stats. 225# In this case evictions in underused classes tend to get vastly inflated 226sub calc_results_numratio { 227 my ($slabs, $totals) = calc_slabs(@_, sub { 228 my ($sb, $sa, $s) = @_; 229 if ($s->{evicted_d}) { 230 $s->{numratio} = $s->{evicted_d} / $s->{number}; 231 } else { $s->{numratio} = 0; } 232 }); 233 my @sorted = sort { $a->{numratio} <=> $b->{numratio} } values %$slabs; 234 return ($totals, \@sorted); 235} 236 237sub calc_slabs { 238 my ($slabs_before, $slabs_after, $code) = @_; 239 my %slabs = (); 240 my %totals = (); 241 for my $id (keys %$slabs_after) { 242 my $sb = $slabs_before->{$id}; 243 my $sa = $slabs_after->{$id}; 244 next unless ($sb && $sa); 245 my %slab = %$sa; 246 for my $key (keys %slab) { 247 # Add totals, diffs 248 if ($slab{$key} =~ m/^\d+$/) { 249 $totals{$key} += $slab{$key}; 250 $slab{$key . '_d'} = $sa->{$key} - $sb->{$key}; 251 $totals{$key . '_d'} += $sa->{$key} - $sb->{$key}; 252 } 253 } 254 # External code 255 $code->($sb, $sa, \%slab) if $code; 256 $slab{slab} = $id; 257 $slabs{$id} = \%slab; 258 } 259 return (\%slabs, \%totals); 260} 261