summaryrefslogtreecommitdiff
path: root/scripts/mc_slab_mover
blob: ac984d7b7d3c7cffc1852026a3afae7fd67a26b4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
#! /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 E<lt>L<dormando@rydia.net>E<gt>

=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);
}