Skip to content

Commit

Permalink
Limit the main db and expires dictionaries to expand (redis#7954)
Browse files Browse the repository at this point in the history
As we know, redis may reject user's requests or evict some keys if
used memory is over maxmemory. Dictionaries expanding may make
things worse, some big dictionaries, such as main db and expires dict,
may eat huge memory at once for allocating a new big hash table and be
far more than maxmemory after expanding.
There are related issues: redis#4213 redis#4583

More details, when expand dict in redis, we will allocate a new big
ht[1] that generally is double of ht[0], The size of ht[1] will be
very big if ht[0] already is big. For db dict, if we have more than
64 million keys, we need to cost 1GB for ht[1] when dict expands.

If the sum of used memory and new hash table of dict needed exceeds
maxmemory, we shouldn't allow the dict to expand. Because, if we
enable keys eviction, we still couldn't add much more keys after
eviction and rehashing, what's worse, redis will keep less keys when
redis only remains a little memory for storing new hash table instead
of users' data. Moreover users can't write data in redis if disable
keys eviction.

What this commit changed ?

Add a new member function expandAllowed for dict type, it provide a way
for caller to allow expand or not. We expose two parameters for this
function: more memory needed for expanding and dict current load factor,
users can implement a function to make a decision by them.
For main db dict and expires dict type, these dictionaries may be very
big and cost huge memory for expanding, so we implement a judgement
function: we can stop dict to expand provisionally if used memory will
be over maxmemory after dict expands, but to guarantee the performance
of redis, we still allow dict to expand if dict load factor exceeds the
safe load factor.
Add test cases to verify we don't allow main db to expand when left
memory is not enough, so that avoid keys eviction.

Other changes:

For new hash table size when expand. Before this commit, the size is
that double used of dict and later _dictNextPower. Actually we aim to
control a dict load factor between 0.5 and 1.0. Now we replace *2 with
+1, since the first check is that used >= size, the outcome of before
will usually be the same as _dictNextPower(used+1). The only case where
it'll differ is when dict_can_resize is false during fork, so that later
the _dictNextPower(used*2) will cause the dict to jump to *4 (i.e.
_dictNextPower(1025*2) will return 4096).
Fix rehash test cases due to changing algorithm of new hash table size
when expand.
  • Loading branch information
ShooterIT authored Dec 6, 2020
1 parent 2f41a38 commit 75f9dec
Show file tree
Hide file tree
Showing 18 changed files with 127 additions and 37 deletions.
6 changes: 4 additions & 2 deletions src/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -1085,7 +1085,8 @@ dictType optionToLineDictType = {
NULL, /* val dup */
dictSdsKeyCaseCompare, /* key compare */
dictSdsDestructor, /* key destructor */
dictListDestructor /* val destructor */
dictListDestructor, /* val destructor */
NULL /* allow to expand */
};

dictType optionSetDictType = {
Expand All @@ -1094,7 +1095,8 @@ dictType optionSetDictType = {
NULL, /* val dup */
dictSdsKeyCaseCompare, /* key compare */
dictSdsDestructor, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};

/* The config rewrite state. */
Expand Down
2 changes: 1 addition & 1 deletion src/db.c
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ dbBackup *backupDb(void) {
for (int i=0; i<server.dbnum; i++) {
backup->dbarray[i] = server.db[i];
server.db[i].dict = dictCreate(&dbDictType,NULL);
server.db[i].expires = dictCreate(&keyptrDictType,NULL);
server.db[i].expires = dictCreate(&dbExpiresDictType,NULL);
}

/* Backup cluster slots to keys map if enable cluster. */
Expand Down
16 changes: 14 additions & 2 deletions src/dict.c
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,16 @@ unsigned long dictScan(dict *d,

/* ------------------------- private functions ------------------------------ */

/* Because we may need to allocate huge memory chunk at once when dict
* expands, we will check this allocation is allowed or not if the dict
* type has expandAllowed member function. */
static int dictTypeExpandAllowed(dict *d) {
if (d->type->expandAllowed == NULL) return 1;
return d->type->expandAllowed(
_dictNextPower(d->ht[0].used + 1) * sizeof(dictEntry*),
(double)d->ht[0].used / d->ht[0].size);
}

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
Expand All @@ -966,9 +976,10 @@ static int _dictExpandIfNeeded(dict *d)
* the number of buckets. */
if (d->ht[0].used >= d->ht[0].size &&
(dict_can_resize ||
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
d->ht[0].used/d->ht[0].size > dict_force_resize_ratio) &&
dictTypeExpandAllowed(d))
{
return dictExpand(d, d->ht[0].used*2);
return dictExpand(d, d->ht[0].used + 1);
}
return DICT_OK;
}
Expand Down Expand Up @@ -1173,6 +1184,7 @@ dictType BenchmarkDictType = {
NULL,
compareCallback,
freeCallback,
NULL,
NULL
};

Expand Down
1 change: 1 addition & 0 deletions src/dict.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ typedef struct dictType {
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
void (*keyDestructor)(void *privdata, void *key);
void (*valDestructor)(void *privdata, void *obj);
int (*expandAllowed)(size_t moreMem, double usedRatio);
} dictType;

/* This is our hash table structure. Every dictionary has two of this as we
Expand Down
14 changes: 14 additions & 0 deletions src/evict.c
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,20 @@ int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *lev
return C_ERR;
}

/* Return 1 if used memory is more than maxmemory after allocating more memory,
* return 0 if not. Redis may reject user's requests or evict some keys if used
* memory exceeds maxmemory, especially, when we allocate huge memory at once. */
int overMaxmemoryAfterAlloc(size_t moremem) {
if (!server.maxmemory) return 0; /* No limit. */

/* Check quickly. */
size_t mem_used = zmalloc_used_memory();
if (mem_used + moremem <= server.maxmemory) return 0;

size_t overhead = freeMemoryGetNotCountedMemory();
mem_used = (mem_used > overhead) ? mem_used - overhead : 0;
return mem_used + moremem > server.maxmemory;
}

/* The evictionTimeProc is started when "maxmemory" has been breached and
* could not immediately be resolved. This will spin the event loop with short
Expand Down
3 changes: 2 additions & 1 deletion src/expire.c
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,8 @@ void rememberSlaveKeyWithExpire(redisDb *db, robj *key) {
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
dictSdsDestructor, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};
slaveKeysWithExpire = dictCreate(&dt,NULL);
}
Expand Down
3 changes: 2 additions & 1 deletion src/latency.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ dictType latencyTimeSeriesDictType = {
NULL, /* val dup */
dictStringKeyCompare, /* key compare */
dictVanillaFree, /* key destructor */
dictVanillaFree /* val destructor */
dictVanillaFree, /* val destructor */
NULL /* allow to expand */
};

/* ------------------------- Utility functions ------------------------------ */
Expand Down
2 changes: 1 addition & 1 deletion src/lazyfree.c
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ void freeObjAsync(robj *key, robj *obj) {
void emptyDbAsync(redisDb *db) {
dict *oldht1 = db->dict, *oldht2 = db->expires;
db->dict = dictCreate(&dbDictType,NULL);
db->expires = dictCreate(&keyptrDictType,NULL);
db->expires = dictCreate(&dbExpiresDictType,NULL);
atomicIncr(lazyfree_objects,dictSize(oldht1));
bioCreateBackgroundJob(BIO_LAZY_FREE,NULL,oldht1,oldht2);
}
Expand Down
3 changes: 2 additions & 1 deletion src/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -7552,7 +7552,8 @@ dictType moduleAPIDictType = {
NULL, /* val dup */
dictCStringKeyCompare, /* key compare */
NULL, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};

int moduleRegisterApi(const char *funcname, void *funcptr) {
Expand Down
3 changes: 2 additions & 1 deletion src/redis-benchmark.c
Original file line number Diff line number Diff line change
Expand Up @@ -1299,7 +1299,8 @@ static int fetchClusterSlotsConfiguration(client c) {
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
NULL, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};
/* printf("[%d] fetchClusterSlotsConfiguration\n", c->thread_id); */
dict *masters = dictCreate(&dtype, NULL);
Expand Down
9 changes: 6 additions & 3 deletions src/redis-cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -2294,7 +2294,8 @@ static dictType clusterManagerDictType = {
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
NULL, /* key destructor */
dictSdsDestructor /* val destructor */
dictSdsDestructor, /* val destructor */
NULL /* allow to expand */
};

static dictType clusterManagerLinkDictType = {
Expand All @@ -2303,7 +2304,8 @@ static dictType clusterManagerLinkDictType = {
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
dictSdsDestructor, /* key destructor */
dictListDestructor /* val destructor */
dictListDestructor, /* val destructor */
NULL /* allow to expand */
};

typedef int clusterManagerCommandProc(int argc, char **argv);
Expand Down Expand Up @@ -7360,7 +7362,8 @@ static dictType typeinfoDictType = {
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
NULL, /* key destructor (owned by the value)*/
type_free /* val destructor */
type_free, /* val destructor */
NULL /* allow to expand */
};

static void getKeyTypes(dict *types_dict, redisReply *keys, typeinfo **types) {
Expand Down
9 changes: 6 additions & 3 deletions src/sentinel.c
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,8 @@ dictType instancesDictType = {
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
NULL, /* key destructor */
dictInstancesValDestructor /* val destructor */
dictInstancesValDestructor,/* val destructor */
NULL /* allow to expand */
};

/* Instance runid (sds) -> votes (long casted to void*)
Expand All @@ -431,7 +432,8 @@ dictType leaderVotesDictType = {
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
NULL, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};

/* Instance renamed commands table. */
Expand All @@ -441,7 +443,8 @@ dictType renamedCommandsDictType = {
NULL, /* val dup */
dictSdsKeyCaseCompare, /* key compare */
dictSdsDestructor, /* key destructor */
dictSdsDestructor /* val destructor */
dictSdsDestructor, /* val destructor */
NULL /* allow to expand */
};

/* =========================== Initialization =============================== */
Expand Down
60 changes: 44 additions & 16 deletions src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,20 @@ uint64_t dictEncObjHash(const void *key) {
}
}

/* Return 1 if currently we allow dict to expand. Dict may allocate huge
* memory to contain hash buckets when dict expands, that may lead redis
* rejects user's requests or evicts some keys, we can stop dict to expand
* provisionally if used memory will be over maxmemory after dict expands,
* but to guarantee the performance of redis, we still allow dict to expand
* if dict load factor exceeds HASHTABLE_MAX_LOAD_FACTOR. */
int dictExpandAllowed(size_t moreMem, double usedRatio) {
if (usedRatio <= HASHTABLE_MAX_LOAD_FACTOR) {
return !overMaxmemoryAfterAlloc(moreMem);
} else {
return 1;
}
}

/* Generic hash table type where keys are Redis Objects, Values
* dummy pointers. */
dictType objectKeyPointerValueDictType = {
Expand All @@ -1306,7 +1320,8 @@ dictType objectKeyPointerValueDictType = {
NULL, /* val dup */
dictEncObjKeyCompare, /* key compare */
dictObjectDestructor, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};

/* Like objectKeyPointerValueDictType(), but values can be destroyed, if
Expand All @@ -1317,7 +1332,8 @@ dictType objectKeyHeapPointerValueDictType = {
NULL, /* val dup */
dictEncObjKeyCompare, /* key compare */
dictObjectDestructor, /* key destructor */
dictVanillaFree /* val destructor */
dictVanillaFree, /* val destructor */
NULL /* allow to expand */
};

/* Set dictionary type. Keys are SDS strings, values are not used. */
Expand All @@ -1337,7 +1353,8 @@ dictType zsetDictType = {
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
NULL, /* Note: SDS string shared & freed by skiplist */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};

/* Db->dict, keys are sds strings, vals are Redis objects. */
Expand All @@ -1347,7 +1364,8 @@ dictType dbDictType = {
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
dictSdsDestructor, /* key destructor */
dictObjectDestructor /* val destructor */
dictObjectDestructor, /* val destructor */
dictExpandAllowed /* allow to expand */
};

/* server.lua_scripts sha (as sds string) -> scripts (as robj) cache. */
Expand All @@ -1357,17 +1375,19 @@ dictType shaScriptObjectDictType = {
NULL, /* val dup */
dictSdsKeyCaseCompare, /* key compare */
dictSdsDestructor, /* key destructor */
dictObjectDestructor /* val destructor */
dictObjectDestructor, /* val destructor */
NULL /* allow to expand */
};

/* Db->expires */
dictType keyptrDictType = {
dictType dbExpiresDictType = {
dictSdsHash, /* hash function */
NULL, /* key dup */
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
NULL, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
dictExpandAllowed /* allow to expand */
};

/* Command table. sds string -> command struct pointer. */
Expand All @@ -1377,7 +1397,8 @@ dictType commandTableDictType = {
NULL, /* val dup */
dictSdsKeyCaseCompare, /* key compare */
dictSdsDestructor, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};

/* Hash type hash table (note that small hashes are represented with ziplists) */
Expand All @@ -1387,7 +1408,8 @@ dictType hashDictType = {
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
dictSdsDestructor, /* key destructor */
dictSdsDestructor /* val destructor */
dictSdsDestructor, /* val destructor */
NULL /* allow to expand */
};

/* Keylist hash table type has unencoded redis objects as keys and
Expand All @@ -1399,7 +1421,8 @@ dictType keylistDictType = {
NULL, /* val dup */
dictObjKeyCompare, /* key compare */
dictObjectDestructor, /* key destructor */
dictListDestructor /* val destructor */
dictListDestructor, /* val destructor */
NULL /* allow to expand */
};

/* Cluster nodes hash table, mapping nodes addresses 1.2.3.4:6379 to
Expand All @@ -1410,7 +1433,8 @@ dictType clusterNodesDictType = {
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
dictSdsDestructor, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};

/* Cluster re-addition blacklist. This maps node IDs to the time
Expand All @@ -1422,7 +1446,8 @@ dictType clusterNodesBlackListDictType = {
NULL, /* val dup */
dictSdsKeyCaseCompare, /* key compare */
dictSdsDestructor, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};

/* Modules system dictionary type. Keys are module name,
Expand All @@ -1433,7 +1458,8 @@ dictType modulesDictType = {
NULL, /* val dup */
dictSdsKeyCaseCompare, /* key compare */
dictSdsDestructor, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};

/* Migrate cache dict type. */
Expand All @@ -1443,7 +1469,8 @@ dictType migrateCacheDictType = {
NULL, /* val dup */
dictSdsKeyCompare, /* key compare */
dictSdsDestructor, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};

/* Replication cached script dict (server.repl_scriptcache_dict).
Expand All @@ -1455,7 +1482,8 @@ dictType replScriptCacheDictType = {
NULL, /* val dup */
dictSdsKeyCaseCompare, /* key compare */
dictSdsDestructor, /* key destructor */
NULL /* val destructor */
NULL, /* val destructor */
NULL /* allow to expand */
};

int htNeedsResize(dict *dict) {
Expand Down Expand Up @@ -3004,7 +3032,7 @@ void initServer(void) {
/* Create the Redis databases, and initialize other internal state. */
for (j = 0; j < server.dbnum; j++) {
server.db[j].dict = dictCreate(&dbDictType,NULL);
server.db[j].expires = dictCreate(&keyptrDictType,NULL);
server.db[j].expires = dictCreate(&dbExpiresDictType,NULL);
server.db[j].expires_cursor = 0;
server.db[j].blocking_keys = dictCreate(&keylistDictType,NULL);
server.db[j].ready_keys = dictCreate(&objectKeyPointerValueDictType,NULL);
Expand Down
Loading

0 comments on commit 75f9dec

Please sign in to comment.