summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordormando <dormando@rydia.net>2016-06-23 15:22:15 -0700
committerdormando <dormando@rydia.net>2016-06-24 00:56:55 -0700
commit31541b371a79ead88d0f57cfa6c54958d819371c (patch)
treea49676b9eef84c55218a34e2d7297db7c537be98
parent8d82383f0fc82b79975cd6e16d462b8ff0d2dd39 (diff)
downloadmemcached-31541b371a79ead88d0f57cfa6c54958d819371c.tar.gz
cache_memlimit command for tuning runtime maxbytes
Allows dynamically increasing the memory limit of a running system, if memory isn't being preallocated. If `-o modern` is in use, can also dynamically lower memory usage. pages are free()'ed back to the OS via the slab rebalancer as memory is freed up. Does not guarantee the OS will actually give the memory back for other applications to use, that depends on how the OS handles memory.
-rw-r--r--doc/protocol.txt8
-rw-r--r--memcached.c27
-rw-r--r--slabs.c38
-rw-r--r--slabs.h3
-rw-r--r--t/dyn-maxbytes.t78
5 files changed, 153 insertions, 1 deletions
diff --git a/doc/protocol.txt b/doc/protocol.txt
index ea153bd..79a9fed 100644
--- a/doc/protocol.txt
+++ b/doc/protocol.txt
@@ -614,7 +614,7 @@ integers separated by a colon (treat this as a floating point number).
| | | from network |
| bytes_written | 64u | Total number of bytes sent by this server |
| | | to network |
-| limit_maxbytes | 32u | Number of bytes this server is allowed to |
+| limit_maxbytes | size_t | Number of bytes this server is allowed to |
| | | use for storage. |
| accepting_conns | bool | Whether or not server is accepting conns |
| listen_disabled_num | 64u | Number of times server has stopped |
@@ -984,6 +984,12 @@ The delay option allows you to have them reset in e.g. 10 second
intervals (by passing 0 to the first, 10 to the second, 20 to the
third, etc. etc.).
+"cache_memlimit" is a command with a numeric argument. This allows runtime
+adjustments of the cache memory limit. It returns "OK\r\n" or an error (unless
+"noreply" is given as the last parameter). If the new memory limit is higher
+than the old one, the server may start requesting more memory from the OS. If
+the limit is lower, and slabs_reassign+automove are enabled, free memory may
+be released back to the OS asynchronously.
"version" is a command with no arguments:
diff --git a/memcached.c b/memcached.c
index ad42e59..09ced3e 100644
--- a/memcached.c
+++ b/memcached.c
@@ -3611,6 +3611,31 @@ static void process_watch_command(conn *c, token_t *tokens, const size_t ntokens
}
}
+static void process_memlimit_command(conn *c, token_t *tokens, const size_t ntokens) {
+ uint32_t memlimit;
+ assert(c != NULL);
+
+ set_noreply_maybe(c, tokens, ntokens);
+
+ if (!safe_strtoul(tokens[1].value, &memlimit)) {
+ out_string(c, "ERROR");
+ } else {
+ if (memlimit < 8) {
+ out_string(c, "MEMLIMIT_TOO_SMALL cannot set maxbytes to less than 8m");
+ } else {
+ if (slabs_adjust_mem_limit(memlimit * 1024 * 1024)) {
+ if (settings.verbose > 0) {
+ fprintf(stderr, "maxbytes adjusted to %llum\n", (unsigned long long)memlimit);
+ }
+
+ out_string(c, "OK");
+ } else {
+ out_string(c, "MEMLIMIT_ADJUST_FAILED out of bounds or unable to adjust");
+ }
+ }
+ }
+}
+
static void process_command(conn *c, char *command) {
token_t tokens[MAX_TOKENS];
@@ -3854,6 +3879,8 @@ static void process_command(conn *c, char *command) {
}
} else if (ntokens > 1 && strcmp(tokens[COMMAND_TOKEN].value, "watch") == 0) {
process_watch_command(c, tokens, ntokens);
+ } else if ((ntokens == 3 || ntokens == 4) && (strcmp(tokens[COMMAND_TOKEN].value, "cache_memlimit") == 0)) {
+ process_memlimit_command(c, tokens, ntokens);
} else if ((ntokens == 3 || ntokens == 4) && (strcmp(tokens[COMMAND_TOKEN].value, "verbosity") == 0)) {
process_verbosity_command(c, tokens, ntokens);
} else {
diff --git a/slabs.c b/slabs.c
index 879b69c..9bc42c6 100644
--- a/slabs.c
+++ b/slabs.c
@@ -446,6 +446,22 @@ static void *memory_allocate(size_t size) {
return ret;
}
+/* Must only be used if all pages are item_size_max */
+static void memory_release() {
+ void *p = NULL;
+ if (mem_base != NULL)
+ return;
+
+ if (!settings.slab_reassign)
+ return;
+
+ while (mem_malloced > mem_limit &&
+ (p = get_page_from_global_pool()) != NULL) {
+ free(p);
+ mem_malloced -= settings.item_size_max;
+ }
+}
+
void *slabs_alloc(size_t size, unsigned int id, unsigned int *total_chunks,
unsigned int flags) {
void *ret;
@@ -468,6 +484,25 @@ void slabs_stats(ADD_STAT add_stats, void *c) {
pthread_mutex_unlock(&slabs_lock);
}
+static bool do_slabs_adjust_mem_limit(size_t new_mem_limit) {
+ /* Cannot adjust memory limit at runtime if prealloc'ed */
+ if (mem_base != NULL)
+ return false;
+ settings.maxbytes = new_mem_limit;
+ mem_limit = new_mem_limit;
+ mem_limit_reached = false; /* Will reset on next alloc */
+ memory_release(); /* free what might already be in the global pool */
+ return true;
+}
+
+bool slabs_adjust_mem_limit(size_t new_mem_limit) {
+ bool ret;
+ pthread_mutex_lock(&slabs_lock);
+ ret = do_slabs_adjust_mem_limit(new_mem_limit);
+ pthread_mutex_unlock(&slabs_lock);
+ return ret;
+}
+
void slabs_adjust_mem_requested(unsigned int id, size_t old, size_t ntotal)
{
pthread_mutex_lock(&slabs_lock);
@@ -812,6 +847,9 @@ static void slab_rebalance_finish(void) {
memset(slab_rebal.slab_start, 0, (size_t)settings.item_size_max);
split_slab_page_into_freelist(slab_rebal.slab_start,
slab_rebal.d_clsid);
+ } else if (slab_rebal.d_clsid == SLAB_GLOBAL_PAGE_POOL) {
+ /* mem_malloc'ed might be higher than mem_limit. */
+ memory_release();
}
slab_rebal.done = 0;
diff --git a/slabs.h b/slabs.h
index 23e713c..e8bf99e 100644
--- a/slabs.h
+++ b/slabs.h
@@ -28,6 +28,9 @@ void slabs_free(void *ptr, size_t size, unsigned int id);
/** Adjust the stats for memory requested */
void slabs_adjust_mem_requested(unsigned int id, size_t old, size_t ntotal);
+/** Adjust global memory limit up or down */
+bool slabs_adjust_mem_limit(size_t new_mem_limit);
+
/** Return a datum for stats in binary protocol */
bool get_stats(const char *stat_type, int nkey, ADD_STAT add_stats, void *c);
diff --git a/t/dyn-maxbytes.t b/t/dyn-maxbytes.t
new file mode 100644
index 0000000..5a66280
--- /dev/null
+++ b/t/dyn-maxbytes.t
@@ -0,0 +1,78 @@
+#!/usr/bin/perl
+# Test the 'stats items' evictions counters.
+
+use strict;
+use Test::More tests => 309;
+use FindBin qw($Bin);
+use lib "$Bin/lib";
+use MemcachedTest;
+
+my $server = new_memcached("-m 3 -o modern");
+my $sock = $server->sock;
+my $value = "B"x66560;
+my $key = 0;
+
+# These aren't set to expire.
+for ($key = 0; $key < 40; $key++) {
+ print $sock "set key$key 0 0 66560\r\n$value\r\n";
+ is(scalar <$sock>, "STORED\r\n", "stored key$key");
+}
+
+my $stats = mem_stats($sock, "items");
+my $evicted = $stats->{"items:31:evicted"};
+isnt($evicted, "0", "check evicted");
+
+# We're past the memory limit. Try adjusting maxbytes upward.
+$stats = mem_stats($sock, "settings");
+my $pre_maxbytes = $stats->{"maxbytes"};
+print $sock "cache_memlimit 8\r\n";
+is(scalar <$sock>, "OK\r\n", "bumped maxbytes from 3m to 8m");
+
+# Confirm maxbytes updated.
+$stats = mem_stats($sock, "settings");
+isnt($stats->{"maxbytes"}, $pre_maxbytes, "stats settings maxbytes updated");
+
+# Check for total_malloced increasing as new memory is added
+$stats = mem_stats($sock, "slabs");
+my $t_malloc = $stats->{"total_malloced"};
+
+print $sock "set toast 0 0 66560\r\n$value\r\n";
+is(scalar <$sock>, "STORED\r\n", "stored toast");
+$stats = mem_stats($sock, "slabs");
+cmp_ok($stats->{"total_malloced"}, '>', $t_malloc, "stats slabs total_malloced increased");
+
+$stats = mem_stats($sock, "items");
+my $new_evicted = $stats->{"items:31:evicted"};
+cmp_ok($new_evicted, '==', $evicted, "no new evictions");
+
+# Bump up to 16, fill a bit more, then delete everything.
+print $sock "cache_memlimit 16\r\n";
+is(scalar <$sock>, "OK\r\n", "bumped maxbytes from 8m to 16m");
+for (;$key < 150; $key++) {
+ print $sock "set key$key 0 0 66560\r\n$value\r\n";
+ is(scalar <$sock>, "STORED\r\n", "stored key$key");
+}
+
+# Grab total_malloced after filling everything up.
+$stats = mem_stats($sock, "slabs");
+$t_malloc = $stats->{"total_malloced"};
+print $sock "cache_memlimit 8\r\n";
+is(scalar <$sock>, "OK\r\n", "bumped maxbytes from 16m to 8m");
+
+# Remove all of the keys, allowing the slab rebalancer to push pages toward
+# the global page pool.
+for ($key = 0; $key < 150; $key++) {
+ print $sock "delete key$key\r\n";
+ like(scalar <$sock>, qr/(DELETED|NOT_FOUND)\r\n/, "deleted key$key");
+}
+
+# If memory limit is lower, it should free those pages back to the OS.
+my $reduced = 0;
+for (my $tries = 0; $tries < 6; $tries++) {
+ sleep 1;
+ $stats = mem_stats($sock, "slabs");
+ $reduced = $stats->{"total_malloced"} if ($t_malloc > $stats->{"total_malloced"});
+ last if $reduced;
+}
+
+isnt($reduced, 0, "total_malloced reduced to $reduced");