LFU: Redis object level implementation.

Implementation of LFU maxmemory policy for anything related to Redis
objects. Still no actual eviction implemented.
This commit is contained in:
antirez 2016-07-15 12:12:52 +02:00
parent ada70c7c53
commit 5d07984c5d
5 changed files with 142 additions and 15 deletions

View File

@ -45,9 +45,11 @@ typedef struct configEnum {
configEnum maxmemory_policy_enum[] = { configEnum maxmemory_policy_enum[] = {
{"volatile-lru", MAXMEMORY_VOLATILE_LRU}, {"volatile-lru", MAXMEMORY_VOLATILE_LRU},
{"volatile-lfu", MAXMEMORY_VOLATILE_LFU},
{"volatile-random",MAXMEMORY_VOLATILE_RANDOM}, {"volatile-random",MAXMEMORY_VOLATILE_RANDOM},
{"volatile-ttl",MAXMEMORY_VOLATILE_TTL}, {"volatile-ttl",MAXMEMORY_VOLATILE_TTL},
{"allkeys-lru",MAXMEMORY_ALLKEYS_LRU}, {"allkeys-lru",MAXMEMORY_ALLKEYS_LRU},
{"allkeys-lfu",MAXMEMORY_ALLKEYS_LFU},
{"allkeys-random",MAXMEMORY_ALLKEYS_RANDOM}, {"allkeys-random",MAXMEMORY_ALLKEYS_RANDOM},
{"noeviction",MAXMEMORY_NO_EVICTION}, {"noeviction",MAXMEMORY_NO_EVICTION},
{NULL, 0} {NULL, 0}

View File

@ -53,7 +53,13 @@ robj *lookupKey(redisDb *db, robj *key, int flags) {
server.aof_child_pid == -1 && server.aof_child_pid == -1 &&
!(flags & LOOKUP_NOTOUCH)) !(flags & LOOKUP_NOTOUCH))
{ {
val->lru = LRU_CLOCK(); if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
unsigned long ldt = val->lru >> 8;
unsigned long counter = LFULogIncr(val->lru & 255);
val->lru = (ldt << 8) | counter;
} else {
val->lru = LRU_CLOCK();
}
} }
return val; return val;
} else { } else {

View File

@ -214,6 +214,99 @@ void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evic
} }
} }
/* ----------------------------------------------------------------------------
* LFU (Least Frequently Used) implementation.
* We have 24 total bits of space in each object in order to implement
* an LFU (Least Frequently Used) eviction policy, since we re-use the
* LRU field for this purpose.
*
* We split the 24 bits into two fields:
*
* 16 bits 8 bits
* +----------------+--------+
* + Last decr time | LOG_C |
* +----------------+--------+
*
* LOG_C is a logarithmic counter that provides an indication of the access
* frequency. However this field must also be decremented otherwise what used
* to be a frequently accessed key in the past, will remain ranked like that
* forever, while we want the algorithm to adapt to access pattern changes.
*
* So the remaining 16 bits are used in order to store the "decrement time",
* a reduced-precision Unix time (we take 16 bits of the time converted
* in minutes since we don't care about wrapping around) where the LOG_C
* counter is halved if it has an high value, or just decremented if it
* has a low value.
*
* New keys don't start at zero, in order to have the ability to collect
* some accesses before being trashed away, so they start at COUNTER_INIT_VAL.
* The logarithmic increment performed on LOG_C takes care of COUNTER_INIT_VAL
* when incrementing the key, so that keys starting at COUNTER_INIT_VAL
* (or having a smaller value) have a very high chance of being incremented
* on access.
*
* During decrement, the value of the logarithmic counter is halved if
* its current value is greater than two times the COUNTER_INIT_VAL, otherwise
* it is just decremented by one.
* --------------------------------------------------------------------------*/
/* Return the current time in minutes, just taking the least significant
* 16 bits. The returned time is suitable to be stored as LDT (last decrement
* time) for the LFU implementation. */
unsigned long LFUGetTimeInMinutes(void) {
return (server.unixtime/60) & 65535;
}
/* Given an object last decrement time, compute the minimum number of minutes
* that elapsed since the last decrement. Handle overflow (ldt greater than
* the current 16 bits minutes time) considering the time as wrapping
* exactly once. */
unsigned long LFUTimeElapsed(unsigned long ldt) {
unsigned long now = LFUGetTimeInMinutes();
if (now > ldt) return now-ldt;
return 65535-ldt+now;
}
/* Logarithmically increment a counter. The greater is the current counter value
* the less likely is that it gets really implemented. Saturate it at 255. */
#define LFU_LOG_FACTOR 10
uint8_t LFULogIncr(uint8_t counter) {
if (counter == 255) return 255;
double r = (double)rand()/RAND_MAX;
double baseval = counter - LFU_INIT_VAL;
if (baseval < 0) baseval = 0;
double p = 1.0/(baseval*LFU_LOG_FACTOR+1);
if (r < p) counter++;
return counter;
}
/* If the object decrement time is reached, decrement the LFU counter and
* update the decrement time field. Return the object frequency counter.
*
* This function is used in order to scan the dataset for the best object
* to fit: as we check for the candidate, we incrementally decrement the
* counter of the scanned objects if needed. */
#define LFU_DECR_INTERVAL 1
unsigned long LFUDecrAndReturn(robj *o) {
unsigned long ldt = o->lru >> 8;
unsigned long counter = o->lru & 255;
if (LFUTimeElapsed(ldt) > LFU_DECR_INTERVAL && counter) {
if (counter > LFU_INIT_VAL*2) {
counter /= 2;
} else {
counter--;
}
o->lru = (LFUGetTimeInMinutes()<<8) | counter;
}
return counter;
}
/* ----------------------------------------------------------------------------
* The external API for eviction: freeMemroyIfNeeded() is called by the
* server when there is data to add in order to make space if needed.
* --------------------------------------------------------------------------*/
int freeMemoryIfNeeded(void) { int freeMemoryIfNeeded(void) {
size_t mem_reported, mem_used, mem_tofree, mem_freed; size_t mem_reported, mem_used, mem_tofree, mem_freed;
int slaves = listLength(server.slaves); int slaves = listLength(server.slaves);

View File

@ -43,8 +43,13 @@ robj *createObject(int type, void *ptr) {
o->ptr = ptr; o->ptr = ptr;
o->refcount = 1; o->refcount = 1;
/* Set the LRU to the current lruclock (minutes resolution). */ /* Set the LRU to the current lruclock (minutes resolution), or
o->lru = LRU_CLOCK(); * alternatively the LFU counter. */
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
} else {
o->lru = LRU_CLOCK();
}
return o; return o;
} }
@ -82,7 +87,11 @@ robj *createEmbeddedStringObject(const char *ptr, size_t len) {
o->encoding = OBJ_ENCODING_EMBSTR; o->encoding = OBJ_ENCODING_EMBSTR;
o->ptr = sh+1; o->ptr = sh+1;
o->refcount = 1; o->refcount = 1;
o->lru = LRU_CLOCK(); if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
} else {
o->lru = LRU_CLOCK();
}
sh->len = len; sh->len = len;
sh->alloc = len; sh->alloc = len;
@ -394,8 +403,7 @@ robj *tryObjectEncoding(robj *o) {
* because every object needs to have a private LRU field for the LRU * because every object needs to have a private LRU field for the LRU
* algorithm to work well. */ * algorithm to work well. */
if ((server.maxmemory == 0 || if ((server.maxmemory == 0 ||
(server.maxmemory_policy != MAXMEMORY_VOLATILE_LRU && !(server.maxmemory & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
server.maxmemory_policy != MAXMEMORY_ALLKEYS_LRU)) &&
value >= 0 && value >= 0 &&
value < OBJ_SHARED_INTEGERS) value < OBJ_SHARED_INTEGERS)
{ {
@ -715,8 +723,12 @@ void objectCommand(client *c) {
if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk)) if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk))
== NULL) return; == NULL) return;
addReplyLongLong(c,estimateObjectIdleTime(o)/1000); addReplyLongLong(c,estimateObjectIdleTime(o)/1000);
} else if (!strcasecmp(c->argv[1]->ptr,"freq") && c->argc == 3) {
if ((o = objectCommandLookupOrReply(c,c->argv[2],shared.nullbulk))
== NULL) return;
addReplyLongLong(c,o->lru&255);
} else { } else {
addReplyError(c,"Syntax error. Try OBJECT (refcount|encoding|idletime)"); addReplyError(c,"Syntax error. Try OBJECT (refcount|encoding|idletime|freq)");
} }
} }

View File

@ -341,13 +341,22 @@ typedef long long mstime_t; /* millisecond time type. */
#define SET_OP_DIFF 1 #define SET_OP_DIFF 1
#define SET_OP_INTER 2 #define SET_OP_INTER 2
/* Redis maxmemory strategies */ /* Redis maxmemory strategies. Instead of using just incremental number
#define MAXMEMORY_VOLATILE_LRU 0 * for this defines, we use a set of flags so that testing for certain
#define MAXMEMORY_VOLATILE_TTL 1 * properties common to multiple policies is faster. */
#define MAXMEMORY_VOLATILE_RANDOM 2 #define MAXMEMORY_FLAG_LRU (1<<0)
#define MAXMEMORY_ALLKEYS_LRU 3 #define MAXMEMORY_FLAG_LFU (1<<1)
#define MAXMEMORY_ALLKEYS_RANDOM 4 #define MAXMEMORY_FLAG_NO_SHARED_INTEGERS \
#define MAXMEMORY_NO_EVICTION 5 (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU)
#define MAXMEMORY_VOLATILE_LRU ((0<<8)|MAXMEMORY_FLAG_LRU)
#define MAXMEMORY_VOLATILE_LFU ((1<<8)|MAXMEMORY_FLAG_LFU)
#define MAXMEMORY_VOLATILE_TTL (2<<8)
#define MAXMEMORY_VOLATILE_RANDOM (3<<8)
#define MAXMEMORY_ALLKEYS_LRU ((4<<8)|MAXMEMORY_FLAG_LRU)
#define MAXMEMORY_ALLKEYS_LFU ((5<<8)|MAXMEMORY_FLAG_LFU)
#define MAXMEMORY_ALLKEYS_RANDOM (6<<8)
#define MAXMEMORY_NO_EVICTION (7<<8)
#define CONFIG_DEFAULT_MAXMEMORY_POLICY MAXMEMORY_NO_EVICTION #define CONFIG_DEFAULT_MAXMEMORY_POLICY MAXMEMORY_NO_EVICTION
/* Scripting */ /* Scripting */
@ -528,7 +537,9 @@ typedef struct RedisModuleIO {
typedef struct redisObject { typedef struct redisObject {
unsigned type:4; unsigned type:4;
unsigned encoding:4; unsigned encoding:4;
unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */ unsigned lru:LRU_BITS; /* LRU time (relative to server.lruclock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits decreas time). */
int refcount; int refcount;
void *ptr; void *ptr;
} robj; } robj;
@ -1606,6 +1617,9 @@ void activeExpireCycle(int type);
/* evict.c -- maxmemory handling and LRU eviction. */ /* evict.c -- maxmemory handling and LRU eviction. */
void evictionPoolAlloc(void); void evictionPoolAlloc(void);
#define LFU_INIT_VAL 5
unsigned long LFUGetTimeInMinutes(void);
uint8_t LFULogIncr(uint8_t value);
/* Git SHA1 */ /* Git SHA1 */
char *redisGitSHA1(void); char *redisGitSHA1(void);