In some cases, users will abuse lua eval. Each EVAL call generates a new lua script, which is added to the lua interpreter and cached to redis-server, consuming a large amount of memory over time. Since EVAL is mostly the one that abuses the lua cache, and these won't have pipeline issues (i.e. the script won't disappear unexpectedly, and cause errors like it would with SCRIPT LOAD and EVALSHA), we implement a plain FIFO LRU eviction only for these (not for scripts loaded with SCRIPT LOAD). ### Implementation notes: When not abused we'll probably have less than 100 scripts, and when abused we'll have many thousands. So we use a hard coded value of 500 scripts. And considering that we don't have many scripts, then unlike keys, we don't need to worry about the memory usage of keeping a true sorted LRU linked list. We compute the SHA of each script anyway, and put the script in a dict, we can store a listNode there, and use it for quick removal and re-insertion into an LRU list each time the script is used. ### New interfaces: At the same time, a new `evicted_scripts` field is added to INFO, which represents the number of evicted eval scripts. Users can check it to see if they are abusing EVAL. ### benchmark: `./src/redis-benchmark -P 10 -n 1000000 -r 10000000000 eval "return __rand_int__" 0` The simple abuse of eval benchmark test that will create 1 million EVAL scripts. The performance has been improved by 50%, and the max latency has dropped from 500ms to 13ms (this may be caused by table expansion inside Lua when the number of scripts is large). And in the INFO memory, it used to consume 120MB (server cache) + 310MB (lua engine), but now it only consumes 70KB (server cache) + 210KB (lua_engine) because of the scripts eviction. For non-abusive case of about 100 EVAL scripts, there's no noticeable change in performance or memory usage. ### unlikely potentially breaking change: in theory, a user can maybe load a script with EVAL and then use EVALSHA to call it (by calculating the SHA1 value on the client side), it could be that if we read the docs carefully we'll realized it's a valid scenario, but we suppose it's extremely rare. So it may happen that EVALSHA acts on a script created by EVAL, and the script is evicted and EVALSHA returns a NOSCRIPT error. that is if you have more than 500 scripts being used in the same transaction / pipeline. This solves the second point in #13102.
233 lines
8.7 KiB
C
233 lines
8.7 KiB
C
#include "server.h"
|
|
#include "bio.h"
|
|
#include "atomicvar.h"
|
|
#include "functions.h"
|
|
#include "cluster.h"
|
|
|
|
static redisAtomic size_t lazyfree_objects = 0;
|
|
static redisAtomic size_t lazyfreed_objects = 0;
|
|
|
|
/* Release objects from the lazyfree thread. It's just decrRefCount()
|
|
* updating the count of objects to release. */
|
|
void lazyfreeFreeObject(void *args[]) {
|
|
robj *o = (robj *) args[0];
|
|
decrRefCount(o);
|
|
atomicDecr(lazyfree_objects,1);
|
|
atomicIncr(lazyfreed_objects,1);
|
|
}
|
|
|
|
/* Release a database from the lazyfree thread. The 'db' pointer is the
|
|
* database which was substituted with a fresh one in the main thread
|
|
* when the database was logically deleted. */
|
|
void lazyfreeFreeDatabase(void *args[]) {
|
|
kvstore *da1 = args[0];
|
|
kvstore *da2 = args[1];
|
|
|
|
size_t numkeys = kvstoreSize(da1);
|
|
kvstoreRelease(da1);
|
|
kvstoreRelease(da2);
|
|
atomicDecr(lazyfree_objects,numkeys);
|
|
atomicIncr(lazyfreed_objects,numkeys);
|
|
}
|
|
|
|
/* Release the key tracking table. */
|
|
void lazyFreeTrackingTable(void *args[]) {
|
|
rax *rt = args[0];
|
|
size_t len = rt->numele;
|
|
freeTrackingRadixTree(rt);
|
|
atomicDecr(lazyfree_objects,len);
|
|
atomicIncr(lazyfreed_objects,len);
|
|
}
|
|
|
|
/* Release the lua_scripts dict. */
|
|
void lazyFreeLuaScripts(void *args[]) {
|
|
dict *lua_scripts = args[0];
|
|
list *lua_scripts_lru_list = args[1];
|
|
lua_State *lua = args[2];
|
|
long long len = dictSize(lua_scripts);
|
|
freeLuaScriptsSync(lua_scripts, lua_scripts_lru_list, lua);
|
|
atomicDecr(lazyfree_objects,len);
|
|
atomicIncr(lazyfreed_objects,len);
|
|
}
|
|
|
|
/* Release the functions ctx. */
|
|
void lazyFreeFunctionsCtx(void *args[]) {
|
|
functionsLibCtx *functions_lib_ctx = args[0];
|
|
size_t len = functionsLibCtxFunctionsLen(functions_lib_ctx);
|
|
functionsLibCtxFree(functions_lib_ctx);
|
|
atomicDecr(lazyfree_objects,len);
|
|
atomicIncr(lazyfreed_objects,len);
|
|
}
|
|
|
|
/* Release replication backlog referencing memory. */
|
|
void lazyFreeReplicationBacklogRefMem(void *args[]) {
|
|
list *blocks = args[0];
|
|
rax *index = args[1];
|
|
long long len = listLength(blocks);
|
|
len += raxSize(index);
|
|
listRelease(blocks);
|
|
raxFree(index);
|
|
atomicDecr(lazyfree_objects,len);
|
|
atomicIncr(lazyfreed_objects,len);
|
|
}
|
|
|
|
/* Return the number of currently pending objects to free. */
|
|
size_t lazyfreeGetPendingObjectsCount(void) {
|
|
size_t aux;
|
|
atomicGet(lazyfree_objects,aux);
|
|
return aux;
|
|
}
|
|
|
|
/* Return the number of objects that have been freed. */
|
|
size_t lazyfreeGetFreedObjectsCount(void) {
|
|
size_t aux;
|
|
atomicGet(lazyfreed_objects,aux);
|
|
return aux;
|
|
}
|
|
|
|
void lazyfreeResetStats(void) {
|
|
atomicSet(lazyfreed_objects,0);
|
|
}
|
|
|
|
/* Return the amount of work needed in order to free an object.
|
|
* The return value is not always the actual number of allocations the
|
|
* object is composed of, but a number proportional to it.
|
|
*
|
|
* For strings the function always returns 1.
|
|
*
|
|
* For aggregated objects represented by hash tables or other data structures
|
|
* the function just returns the number of elements the object is composed of.
|
|
*
|
|
* Objects composed of single allocations are always reported as having a
|
|
* single item even if they are actually logical composed of multiple
|
|
* elements.
|
|
*
|
|
* For lists the function returns the number of elements in the quicklist
|
|
* representing the list. */
|
|
size_t lazyfreeGetFreeEffort(robj *key, robj *obj, int dbid) {
|
|
if (obj->type == OBJ_LIST && obj->encoding == OBJ_ENCODING_QUICKLIST) {
|
|
quicklist *ql = obj->ptr;
|
|
return ql->len;
|
|
} else if (obj->type == OBJ_SET && obj->encoding == OBJ_ENCODING_HT) {
|
|
dict *ht = obj->ptr;
|
|
return dictSize(ht);
|
|
} else if (obj->type == OBJ_ZSET && obj->encoding == OBJ_ENCODING_SKIPLIST){
|
|
zset *zs = obj->ptr;
|
|
return zs->zsl->length;
|
|
} else if (obj->type == OBJ_HASH && obj->encoding == OBJ_ENCODING_HT) {
|
|
dict *ht = obj->ptr;
|
|
return dictSize(ht);
|
|
} else if (obj->type == OBJ_STREAM) {
|
|
size_t effort = 0;
|
|
stream *s = obj->ptr;
|
|
|
|
/* Make a best effort estimate to maintain constant runtime. Every macro
|
|
* node in the Stream is one allocation. */
|
|
effort += s->rax->numnodes;
|
|
|
|
/* Every consumer group is an allocation and so are the entries in its
|
|
* PEL. We use size of the first group's PEL as an estimate for all
|
|
* others. */
|
|
if (s->cgroups && raxSize(s->cgroups)) {
|
|
raxIterator ri;
|
|
streamCG *cg;
|
|
raxStart(&ri,s->cgroups);
|
|
raxSeek(&ri,"^",NULL,0);
|
|
/* There must be at least one group so the following should always
|
|
* work. */
|
|
serverAssert(raxNext(&ri));
|
|
cg = ri.data;
|
|
effort += raxSize(s->cgroups)*(1+raxSize(cg->pel));
|
|
raxStop(&ri);
|
|
}
|
|
return effort;
|
|
} else if (obj->type == OBJ_MODULE) {
|
|
size_t effort = moduleGetFreeEffort(key, obj, dbid);
|
|
/* If the module's free_effort returns 0, we will use asynchronous free
|
|
* memory by default. */
|
|
return effort == 0 ? ULONG_MAX : effort;
|
|
} else {
|
|
return 1; /* Everything else is a single allocation. */
|
|
}
|
|
}
|
|
|
|
/* If there are enough allocations to free the value object asynchronously, it
|
|
* may be put into a lazy free list instead of being freed synchronously. The
|
|
* lazy free list will be reclaimed in a different bio.c thread. If the value is
|
|
* composed of a few allocations, to free in a lazy way is actually just
|
|
* slower... So under a certain limit we just free the object synchronously. */
|
|
#define LAZYFREE_THRESHOLD 64
|
|
|
|
/* Free an object, if the object is huge enough, free it in async way. */
|
|
void freeObjAsync(robj *key, robj *obj, int dbid) {
|
|
size_t free_effort = lazyfreeGetFreeEffort(key,obj,dbid);
|
|
/* Note that if the object is shared, to reclaim it now it is not
|
|
* possible. This rarely happens, however sometimes the implementation
|
|
* of parts of the Redis core may call incrRefCount() to protect
|
|
* objects, and then call dbDelete(). */
|
|
if (free_effort > LAZYFREE_THRESHOLD && obj->refcount == 1) {
|
|
atomicIncr(lazyfree_objects,1);
|
|
bioCreateLazyFreeJob(lazyfreeFreeObject,1,obj);
|
|
} else {
|
|
decrRefCount(obj);
|
|
}
|
|
}
|
|
|
|
/* Empty a Redis DB asynchronously. What the function does actually is to
|
|
* create a new empty set of hash tables and scheduling the old ones for
|
|
* lazy freeing. */
|
|
void emptyDbAsync(redisDb *db) {
|
|
int slotCountBits = server.cluster_enabled? CLUSTER_SLOT_MASK_BITS : 0;
|
|
kvstore *oldkeys = db->keys, *oldexpires = db->expires;
|
|
db->keys = kvstoreCreate(&dbDictType, slotCountBits, KVSTORE_ALLOCATE_DICTS_ON_DEMAND);
|
|
db->expires = kvstoreCreate(&dbExpiresDictType, slotCountBits, KVSTORE_ALLOCATE_DICTS_ON_DEMAND);
|
|
atomicIncr(lazyfree_objects, kvstoreSize(oldkeys));
|
|
bioCreateLazyFreeJob(lazyfreeFreeDatabase, 2, oldkeys, oldexpires);
|
|
}
|
|
|
|
/* Free the key tracking table.
|
|
* If the table is huge enough, free it in async way. */
|
|
void freeTrackingRadixTreeAsync(rax *tracking) {
|
|
/* Because this rax has only keys and no values so we use numnodes. */
|
|
if (tracking->numnodes > LAZYFREE_THRESHOLD) {
|
|
atomicIncr(lazyfree_objects,tracking->numele);
|
|
bioCreateLazyFreeJob(lazyFreeTrackingTable,1,tracking);
|
|
} else {
|
|
freeTrackingRadixTree(tracking);
|
|
}
|
|
}
|
|
|
|
/* Free lua_scripts dict and lru list, if the dict is huge enough, free them in async way.
|
|
* Close lua interpreter, if there are a lot of lua scripts, close it in async way. */
|
|
void freeLuaScriptsAsync(dict *lua_scripts, list *lua_scripts_lru_list, lua_State *lua) {
|
|
if (dictSize(lua_scripts) > LAZYFREE_THRESHOLD) {
|
|
atomicIncr(lazyfree_objects,dictSize(lua_scripts));
|
|
bioCreateLazyFreeJob(lazyFreeLuaScripts,3,lua_scripts,lua_scripts_lru_list,lua);
|
|
} else {
|
|
freeLuaScriptsSync(lua_scripts, lua_scripts_lru_list, lua);
|
|
}
|
|
}
|
|
|
|
/* Free functions ctx, if the functions ctx contains enough functions, free it in async way. */
|
|
void freeFunctionsAsync(functionsLibCtx *functions_lib_ctx) {
|
|
if (functionsLibCtxFunctionsLen(functions_lib_ctx) > LAZYFREE_THRESHOLD) {
|
|
atomicIncr(lazyfree_objects,functionsLibCtxFunctionsLen(functions_lib_ctx));
|
|
bioCreateLazyFreeJob(lazyFreeFunctionsCtx,1,functions_lib_ctx);
|
|
} else {
|
|
functionsLibCtxFree(functions_lib_ctx);
|
|
}
|
|
}
|
|
|
|
/* Free replication backlog referencing buffer blocks and rax index. */
|
|
void freeReplicationBacklogRefMemAsync(list *blocks, rax *index) {
|
|
if (listLength(blocks) > LAZYFREE_THRESHOLD ||
|
|
raxSize(index) > LAZYFREE_THRESHOLD)
|
|
{
|
|
atomicIncr(lazyfree_objects,listLength(blocks)+raxSize(index));
|
|
bioCreateLazyFreeJob(lazyFreeReplicationBacklogRefMem,2,blocks,index);
|
|
} else {
|
|
listRelease(blocks);
|
|
raxFree(index);
|
|
}
|
|
}
|