Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add maxmemory-reserved-scale parameter for evicting key earlier to avoid OOM #831

Open
wants to merge 12 commits into
base: unstable
Choose a base branch
from
17 changes: 17 additions & 0 deletions src/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -2505,6 +2505,21 @@ static int updateReplBacklogSize(const char **err) {
return 1;
}

hwware marked this conversation as resolved.
Show resolved Hide resolved
/* Adjusts `maxmemory_soft_scale` to ensure it remains within the valid range of 10 to 60, if set.
* Once adjusted, the available memory is recalculated to reflect the new soft maxmemory. */
static int updateMaxmemorySoftScale(const char **err) {
UNUSED(err);
if (server.maxmemory_soft_scale) {
if (server.maxmemory_soft_scale < 10) {
server.maxmemory_soft_scale = 10;
} else if (server.maxmemory_soft_scale > 60) {
server.maxmemory_soft_scale = 60;
}
}
updateSoftMaxmemoryValue();
return 1;
}

static int updateMaxmemory(const char **err) {
UNUSED(err);
if (server.maxmemory) {
Expand All @@ -2516,6 +2531,7 @@ static int updateMaxmemory(const char **err) {
"depending on the maxmemory-policy.",
server.maxmemory, used);
}
updateSoftMaxmemoryValue();
startEvictionTimeProc();
}
return 1;
Expand Down Expand Up @@ -3216,6 +3232,7 @@ standardConfig static_configs[] = {
createIntConfig("lfu-decay-time", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.lfu_decay_time, 1, INTEGER_CONFIG, NULL, NULL),
createIntConfig("replica-priority", "slave-priority", MODIFIABLE_CONFIG, 0, INT_MAX, server.replica_priority, 100, INTEGER_CONFIG, NULL, NULL),
createIntConfig("repl-diskless-sync-delay", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.repl_diskless_sync_delay, 5, INTEGER_CONFIG, NULL, NULL),
createIntConfig("maxmemory-soft-scale", NULL, MODIFIABLE_CONFIG, 0, 100, server.maxmemory_soft_scale, 0, INTEGER_CONFIG, NULL, updateMaxmemorySoftScale),
createIntConfig("maxmemory-samples", NULL, MODIFIABLE_CONFIG, 1, 64, server.maxmemory_samples, 5, INTEGER_CONFIG, NULL, NULL),
createIntConfig("maxmemory-eviction-tenacity", NULL, MODIFIABLE_CONFIG, 0, 100, server.maxmemory_eviction_tenacity, 10, INTEGER_CONFIG, NULL, NULL),
createIntConfig("timeout", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.maxidletime, 0, INTEGER_CONFIG, NULL, NULL), /* Default client timeout: infinite */
Expand Down
40 changes: 29 additions & 11 deletions src/evict.c
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ size_t freeMemoryGetNotCountedMemory(void) {
* limit.
* (Populated both for C_ERR and C_OK)
*/
int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *level) {
int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *level, unsigned long long checked_maxmemory) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would maxmemorry work for you? I feel that checked_memory introduces yet another concept ...

Suggested change
int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *level, unsigned long long checked_maxmemory) {
int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *level, unsigned long long maxmemory) {

size_t mem_reported, mem_used, mem_tofree;

/* Check if we are over the memory usage limit. If we are not, no need
Expand All @@ -398,11 +398,12 @@ int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *lev
if (total) *total = mem_reported;

/* We may return ASAP if there is no need to compute the level. */
if (!server.maxmemory) {
if (!checked_maxmemory) {
if (level) *level = 0;
return C_OK;
}
if (mem_reported <= server.maxmemory && !level) return C_OK;

if (mem_reported <= checked_maxmemory && !level) return C_OK;

/* Remove the size of replicas output buffers and AOF buffer from the
* count of used memory. */
Expand All @@ -411,15 +412,19 @@ int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *lev
mem_used = (mem_used > overhead) ? mem_used - overhead : 0;

/* Compute the ratio of memory usage. */
if (level) *level = (float)mem_used / (float)server.maxmemory;
if (level) {
*level = (float)mem_used / (float)server.maxmemory;
}

if (mem_reported <= server.maxmemory) return C_OK;
if (mem_reported <= checked_maxmemory) return C_OK;

/* Check if we are still over the memory limit. */
if (mem_used <= server.maxmemory) return C_OK;
/* if checked_maxmemory is equal to maxmemory and mem_used > checked_maxmemory then OOM */
/* if checked_maxmemory is equal to maxmemory_soft and mem_used > checked_maxmemory then there is no OOM but eviction happens */
if (mem_used <= checked_maxmemory) return C_OK;

/* Compute how much memory we need to free. */
mem_tofree = mem_used - server.maxmemory;
mem_tofree = mem_used - server.maxmemory_soft;

if (logical) *logical = mem_used;
if (tofree) *tofree = mem_tofree;
Expand Down Expand Up @@ -541,7 +546,7 @@ int performEvictions(void) {
int replicas = listLength(server.replicas);
int result = EVICT_FAIL;

if (getMaxmemoryState(&mem_reported, NULL, &mem_tofree, NULL) == C_OK) {
if (getMaxmemoryState(&mem_reported, NULL, &mem_tofree, NULL, server.maxmemory_soft) == C_OK) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can get mem_used here too and it can be used to drive the logic determining the return value after the eviction loop.

Suggested change
if (getMaxmemoryState(&mem_reported, NULL, &mem_tofree, NULL, server.maxmemory_soft) == C_OK) {
if (getMaxmemoryState(&mem_reported, &mem_used, &mem_tofree, NULL, server.maxmemory_soft) == C_OK) {

result = EVICT_OK;
goto update_metrics;
}
Expand Down Expand Up @@ -707,7 +712,7 @@ int performEvictions(void) {
* across the dbAsyncDelete() call, while the thread can
* release the memory all the time. */
if (server.lazyfree_lazy_eviction) {
if (getMaxmemoryState(NULL, NULL, NULL, NULL) == C_OK) {
if (getMaxmemoryState(NULL, NULL, NULL, NULL, server.maxmemory_soft) == C_OK) {
break;
}
}
Expand All @@ -722,7 +727,20 @@ int performEvictions(void) {
}
}
} else {
goto cant_free; /* nothing to free... */
if (server.maxmemory_soft_scale) {
break;
} else {
goto cant_free; /* nothing to free... */
}
}
}
if (server.maxmemory_soft_scale) {
size_t mem_used = zmalloc_used_memory();
size_t overhead = freeMemoryGetNotCountedMemory();
mem_used = (mem_used > overhead) ? mem_used - overhead : 0;
if (mem_used < server.maxmemory_soft) {
result = EVICT_OK;
goto update_metrics;
Comment on lines +730 to +743
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify the logic and check the more fundamental conditions as follows (and remove L747 too). Note that mem_used comes from L549. Thoughts?

Suggested change
if (server.maxmemory_soft_scale) {
break;
} else {
goto cant_free; /* nothing to free... */
}
}
}
if (server.maxmemory_soft_scale) {
size_t mem_used = zmalloc_used_memory();
size_t overhead = freeMemoryGetNotCountedMemory();
mem_used = (mem_used > overhead) ? mem_used - overhead : 0;
if (mem_used < server.maxmemory_soft) {
result = EVICT_OK;
goto update_metrics;
break;
}
}
if (mem_used - mem_freed < server.maxmemory) {
result = EVICT_OK;
} else if (isEvictionProcRunning) {
result = EVICT_RUNNING;
} else {
result = EVICT_FAIL;
}

}
}
/* at this point, the memory is OK, or we have reached the time limit */
Expand All @@ -736,7 +754,7 @@ int performEvictions(void) {
mstime_t lazyfree_latency;
latencyStartMonitor(lazyfree_latency);
while (bioPendingJobsOfType(BIO_LAZY_FREE) && elapsedUs(evictionTimer) < eviction_time_limit_us) {
if (getMaxmemoryState(NULL, NULL, NULL, NULL) == C_OK) {
if (getMaxmemoryState(NULL, NULL, NULL, NULL, server.maxmemory_soft) == C_OK) {
result = EVICT_OK;
break;
}
Expand Down
6 changes: 3 additions & 3 deletions src/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -3913,7 +3913,7 @@ int VM_GetContextFlags(ValkeyModuleCtx *ctx) {

/* OOM flag. */
float level;
int retval = getMaxmemoryState(NULL, NULL, NULL, &level);
int retval = getMaxmemoryState(NULL, NULL, NULL, &level, server.maxmemory);
if (retval == C_ERR) flags |= VALKEYMODULE_CTX_FLAGS_OOM;
if (level > 0.75) flags |= VALKEYMODULE_CTX_FLAGS_OOM_WARNING;

Expand Down Expand Up @@ -6310,7 +6310,7 @@ ValkeyModuleCallReply *VM_Call(ValkeyModuleCtx *ctx, const char *cmdname, const
/* On background thread we can not count on server.pre_command_oom_state.
* Because it is only set on the main thread, in such case we will check
* the actual memory usage. */
oom_state = (getMaxmemoryState(NULL, NULL, NULL, NULL) == C_ERR);
oom_state = (getMaxmemoryState(NULL, NULL, NULL, NULL, server.maxmemory) == C_ERR);
} else {
oom_state = server.pre_command_oom_state;
}
Expand Down Expand Up @@ -10855,7 +10855,7 @@ size_t VM_MallocSizeDict(ValkeyModuleDict *dict) {
*/
float VM_GetUsedMemoryRatio(void) {
float level;
getMaxmemoryState(NULL, NULL, NULL, &level);
getMaxmemoryState(NULL, NULL, NULL, &level, server.maxmemory);
return level;
}

Expand Down
9 changes: 9 additions & 0 deletions src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -2698,6 +2698,10 @@ void initServer(void) {
server.client_mem_usage_buckets = NULL;
resetReplicationBuffer();

if (server.maxmemory) {
updateSoftMaxmemoryValue();
}

/* Make sure the locale is set on startup based on the config file. */
if (setlocale(LC_COLLATE, server.locale_collate) == NULL) {
serverLog(LL_WARNING, "Failed to configure LOCALE for invalid locale name.");
Expand Down Expand Up @@ -5623,6 +5627,7 @@ sds genValkeyInfoString(dict *section_dict, int all_sections, int everything) {
char used_memory_scripts_hmem[64];
char used_memory_rss_hmem[64];
char maxmemory_hmem[64];
char maxmemory_soft_hmem[64];
size_t zmalloc_used = zmalloc_used_memory();
size_t total_system_mem = server.system_memory_size;
const char *evict_policy = evictPolicyToString();
Expand All @@ -5644,6 +5649,7 @@ sds genValkeyInfoString(dict *section_dict, int all_sections, int everything) {
bytesToHuman(used_memory_scripts_hmem, sizeof(used_memory_scripts_hmem), mh->lua_caches + mh->functions_caches);
bytesToHuman(used_memory_rss_hmem, sizeof(used_memory_rss_hmem), server.cron_malloc_stats.process_rss);
bytesToHuman(maxmemory_hmem, sizeof(maxmemory_hmem), server.maxmemory);
bytesToHuman(maxmemory_soft_hmem, sizeof(maxmemory_soft_hmem), server.maxmemory_soft);

if (sections++) info = sdscat(info, "\r\n");
info = sdscatprintf(
Expand Down Expand Up @@ -5682,6 +5688,9 @@ sds genValkeyInfoString(dict *section_dict, int all_sections, int everything) {
"maxmemory:%lld\r\n", server.maxmemory,
"maxmemory_human:%s\r\n", maxmemory_hmem,
"maxmemory_policy:%s\r\n", evict_policy,
"maxmemory_soft_scale:%d\r\n", server.maxmemory_soft_scale,
"maxmemory_soft:%lld\r\n", server.maxmemory_soft,
"maxmemory_soft_human:%s\r\n", maxmemory_soft_hmem,
"allocator_frag_ratio:%.2f\r\n", mh->allocator_frag,
"allocator_frag_bytes:%zu\r\n", mh->allocator_frag_bytes,
"allocator_rss_ratio:%.2f\r\n", mh->allocator_rss,
Expand Down
7 changes: 6 additions & 1 deletion src/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -2101,6 +2101,8 @@ struct valkeyServer {
ssize_t maxmemory_clients; /* Memory limit for total client buffers */
int maxmemory_policy; /* Policy for key eviction */
int maxmemory_samples; /* Precision of random sampling */
int maxmemory_soft_scale; /* Percent of the soft maxmemory value */
unsigned long long maxmemory_soft; /* Soft maxmemory value to store data */
int maxmemory_eviction_tenacity; /* Aggressiveness of eviction processing */
int lfu_log_factor; /* LFU logarithmic counter factor. */
int lfu_decay_time; /* LFU counter decay factor. */
Expand Down Expand Up @@ -3035,6 +3037,9 @@ void trimStringObjectIfNeeded(robj *o, int trim_small_values);
static inline int canUseSharedObject(void) {
return server.maxmemory == 0 || !(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS);
}
static inline void updateSoftMaxmemoryValue(void) {
server.maxmemory_soft = (unsigned long long)server.maxmemory / 100.0 * (100 - server.maxmemory_soft_scale);
}
#define sdsEncodedObject(objptr) (objptr->encoding == OBJ_ENCODING_RAW || objptr->encoding == OBJ_ENCODING_EMBSTR)

/* Synchronous I/O with timeout */
Expand Down Expand Up @@ -3277,7 +3282,7 @@ int zslLexValueGteMin(sds value, zlexrangespec *spec);
int zslLexValueLteMax(sds value, zlexrangespec *spec);

/* Core functions */
int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *level);
int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *level, unsigned long long checked_maxmemory);
size_t freeMemoryGetNotCountedMemory(void);
int overMaxmemoryAfterAlloc(size_t moremem);
uint64_t getCommandFlags(client *c);
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/maxmemory.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -628,4 +628,4 @@ start_server {tags {"maxmemory" "external:skip"}} {

assert_equal [r dbsize] {0}
}
}
}
7 changes: 7 additions & 0 deletions valkey.conf
Original file line number Diff line number Diff line change
Expand Up @@ -1216,6 +1216,13 @@ acllog-max-len 128
#
# maxmemory-policy noeviction

# `maxmemory-soft-scale` defines a "soft" maxmemory threshold as a scale of the `maxmemory` limit.
# When memory usage exceeds this scale, Valkey begins proactive key eviction. However, exceeding this
# threshold does not immediately reject new write commands; only the hard `maxmemory` limit will do so.
# This scale is specified as a percentage of `maxmemory`, with a valid range between 10 and 60.
#
# maxmemory-soft-scale 0

# LRU, LFU and minimal TTL algorithms are not precise algorithms but approximated
# algorithms (in order to save memory), so you can tune it for speed or
# accuracy. By default the server will check five keys and pick the one that was
Expand Down
Loading