-
Notifications
You must be signed in to change notification settings - Fork 656
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
base: unstable
Are you sure you want to change the base?
Conversation
e31dd8b
to
b8a98df
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## unstable #831 +/- ##
============================================
- Coverage 70.68% 70.51% -0.18%
============================================
Files 115 115
Lines 63177 63209 +32
============================================
- Hits 44657 44569 -88
- Misses 18520 18640 +120
|
b8a98df
to
ba13a0c
Compare
ba13a0c
to
e27e9de
Compare
this seem like a interesting feature, did not review, just drop a comment that approve the concept. |
I also like the idea. I just haven't really spent enough time thinking about it. Memory management is a big area in Valkey we need to improve. |
e27e9de
to
0a68013
Compare
0a68013
to
9596c78
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Took a quick look.
+1 on introducing "back pressure" earlier. I feel that this could be used with `maxmemory_eviction_tenacity" to give an even smoother eviction experience. |
35437eb
to
db966fe
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @hwware! LGTM overall. Can you add some tests too?
Sure, ready to work, Thanks |
f7cc8c8
to
7a5584b
Compare
src/evict.c
Outdated
@@ -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 (!server.maxmemory_available) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this more, I feel that it is more preferable to model this new setting as a "soft" maxmemory
, which can trigger key eviction earlier but won't cause OOM to be returned before hitting the actual maxmemory
. Otherwise, we effectively create an alias of maxmemory
. More specifically, I think performEviction
should only return EVICT_FAIL
when the memory usage goes beyond the real maxmemory
.
Additionally, since getMaxmemoryState
is also used outside of the OOM prevention path such as in VM_GetUsedMemoryRatio
, we should consider parameterizing maxmemory
and having it passed in by the caller instead, so that we can maintain the same semantics in these externally facing scenarios.
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like what you suggested, if we can think when "maxmemory_available" is available, it is a soft maxmemory. Then you maybe think we should not return OOM, in the case, how we can return message to client, any idea?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right - how about just processing the command normally?
- if
used_memory
is below soft max, no change to the existing logic - if
used_memory
is above soft max but below hard max, trigger key eviction and continue with the normal processing - if
used_memory
is above hard max, no change to the existing logic, i.e., trigger key eviction and fail the command if theused_memory
is still above hard max after the eviction)
200b203
to
4da32a2
Compare
4da32a2
to
510265f
Compare
510265f
to
c6db98b
Compare
@PingXie According to your last comment suggestion, I made some code changes as below:
Please take a look, Thanks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The main logic LGTM. Thanks @hwware!
src/evict.c
Outdated
result = (isEvictionProcRunning) ? EVICT_RUNNING : EVICT_OK; | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit - one empty line?
src/evict.c
Outdated
if (server.maxmemory_reserved_scale && server.maxmemory_available) { | ||
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) { | ||
result = EVICT_FAIL; | ||
goto cant_free; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should check server.maxmemory_reserved_scale
and server.maxmemory_available
. Also we should not goto cant_free
on L725/728; instead, we should just break out of the while loop there and then always compute the result
based on the current memory state. Otherwise, failing to evict keys due to used memory being greater than the new "soft" limit will always result in EVICT_FAIL/OOM rejection.
if (server.maxmemory_reserved_scale && server.maxmemory_available) { | |
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) { | |
result = EVICT_FAIL; | |
goto cant_free; | |
} | |
} | |
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) { | |
result = EVICT_FAIL; | |
goto cant_free; | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Ping, I think you misunderstand the logic for this condition: server.maxmemory_reserved_scale && server.maxmemory_soft. Let me explain to you here:
There are 2 cases:
Case 1. server.maxmemory_soft == server.maxmemory.
In this case, if getMaxmemoryState() return C_ERR, server should make the key eviction. then in the following for... loop, some keys are evicted and some memory are released. Basically, we can guarantee used_memory is below soft max. We do not need to calculate the used_memory again and just keep current logic.
Case 2. server.maxmemory_soft < server.maxmemory.
In this case, even we make some key eviction behavior, but we are not sure if used_memory is smaller than the soft memory, then we should calculate here.
Thus, only when server.maxmemory_soft_scale is set, we need to recalculate the used_memory (I think keep one condition if (server.maxmemory_soft_scale) is enough because server.maxmemory_soft is always true)
valkey.conf
Outdated
# | ||
# This parameter allows Valkey to evict keys earlier. The value represents | ||
# a percentage of the maxmemory limit, indicating how much memory the Valkey instance reserves | ||
# without storing data. | ||
# The default is 0, and the value can be set between 10 and 60. | ||
# | ||
# maxmemory-reserved-scale 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the recent update, we are essentially introducing a new "soft" version of "maxmemory". What do you think about the following?
# | |
# This parameter allows Valkey to evict keys earlier. The value represents | |
# a percentage of the maxmemory limit, indicating how much memory the Valkey instance reserves | |
# without storing data. | |
# The default is 0, and the value can be set between 10 and 60. | |
# | |
# maxmemory-reserved-scale 0 | |
# | |
# `maxmemory-soft-scale` defines a "soft" memory 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed. Parameter name, variable name, and function name had been changed upon request.
src/server.h
Outdated
@@ -3031,6 +3033,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 updateMaxAvailableMemory(void) { | |||
server.maxmemory_available = (unsigned long long)server.maxmemory / 100.0 * (100 - server.maxmemory_reserved_scale); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are aligned on the config name maxmemory_soft_scale
, I would suggest going with maxmemory_soft
for the field name too
server.maxmemory_available = (unsigned long long)server.maxmemory / 100.0 * (100 - server.maxmemory_reserved_scale); | |
server.maxmemory_soft = (unsigned long long)server.maxmemory / 100.0 * (100 - server.maxmemory_soft_scale); |
01fd9d2
to
62a95e2
Compare
5519dab
to
b40a26f
Compare
Signed-off-by: hwware <[email protected]>
Signed-off-by: hwware <[email protected]>
Signed-off-by: hwware <[email protected]>
Signed-off-by: hwware <[email protected]>
Signed-off-by: hwware <[email protected]>
Signed-off-by: hwware <[email protected]>
Signed-off-by: hwware <[email protected]>
Signed-off-by: Shivshankar-Reddy <[email protected]> Signed-off-by: hwware <[email protected]>
Signed-off-by: hwware <[email protected]>
Signed-off-by: hwware <[email protected]>
Signed-off-by: hwware <[email protected]>
Signed-off-by: hwware <[email protected]>
b40a26f
to
9c3068b
Compare
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you add a few tests?
- validate that when the new
maxmemory-soft-scale
defaults to 0 and we won't start key eviction prematurely - validate that the range is indeed between 10 and 60
@@ -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) { |
There was a problem hiding this comment.
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 ...
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) { |
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; |
There was a problem hiding this comment.
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?
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; | |
} |
@@ -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) { |
There was a problem hiding this comment.
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.
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) { |
Reference: #742 and https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-memory-management (Azure)
Generally, when clients set maxmemory-policy as allkeys-lru or other memory eviction policies, and maxmemory as well, If server runs as write-heavy workloads, the data stored in memory could reach the maxmemory limit very quickly, then OOM message will be reported.
If we have maxmemory-reserved-scale parameter and client enable the lazyfree-lazy feature, for example, we can set maxmemory-reserved-scale as 0.8, it means key eviction begin when used memory reaches 8GB if maxmemory is 10GB, thus, server could continue process clients data and OOM will not happen at this time.
Thus, we can see the benefit is we can delay OOM time, but we need pay for less memory available.
One example for this paramter:
Assume
maxmemory 4GB
maxmemory-reserved-scale 20
Then we could check the detail by info memory command:
maxmemory:4294967296
maxmemory_human:4.00G
maxmemory_policy:allkeys-lru
maxmemory_reserved_scale:20
maxmemory_available:3435973836
maxmemory_available_human:3.20G
We could also update and get the maxmemory-reserved-scale value during runtime as following:
config set maxmemory-reserved-scale value
config get maxmemory-reserved-scale