Tracking: first conversion from hashing to key names.

This commit is contained in:
antirez 2020-02-07 14:03:43 +01:00
parent 9c00bdd86e
commit 92357b2d61
3 changed files with 91 additions and 121 deletions

View File

@ -4221,7 +4221,7 @@ sds genRedisInfoString(const char *section) {
"active_defrag_misses:%lld\r\n" "active_defrag_misses:%lld\r\n"
"active_defrag_key_hits:%lld\r\n" "active_defrag_key_hits:%lld\r\n"
"active_defrag_key_misses:%lld\r\n" "active_defrag_key_misses:%lld\r\n"
"tracking_used_slots:%lld\r\n", "tracking_tracked_keys:%lld\r\n",
server.stat_numconnections, server.stat_numconnections,
server.stat_numcommands, server.stat_numcommands,
getInstantaneousMetric(STATS_METRIC_COMMAND), getInstantaneousMetric(STATS_METRIC_COMMAND),
@ -4249,7 +4249,7 @@ sds genRedisInfoString(const char *section) {
server.stat_active_defrag_misses, server.stat_active_defrag_misses,
server.stat_active_defrag_key_hits, server.stat_active_defrag_key_hits,
server.stat_active_defrag_key_misses, server.stat_active_defrag_key_misses,
trackingGetUsedSlots()); (unsigned long long) trackingGetTotalItems());
} }
/* Replication */ /* Replication */

View File

@ -1654,7 +1654,7 @@ void trackingRememberKeys(client *c);
void trackingInvalidateKey(robj *keyobj); void trackingInvalidateKey(robj *keyobj);
void trackingInvalidateKeysOnFlush(int dbid); void trackingInvalidateKeysOnFlush(int dbid);
void trackingLimitUsedSlots(void); void trackingLimitUsedSlots(void);
unsigned long long trackingGetUsedSlots(void); uint64_t trackingGetTotalItems(void);
/* List data type */ /* List data type */
void listTypeTryConversion(robj *subject, robj *value); void listTypeTryConversion(robj *subject, robj *value);

View File

@ -30,37 +30,22 @@
#include "server.h" #include "server.h"
/* The tracking table is constituted by 2^24 radix trees (each tree, and the /* The tracking table is constituted by a radix tree of keys, each pointing
* table itself, are allocated in a lazy way only when needed) tracking * to a radix tree of client IDs, used to track the clients that may have
* clients that may have certain keys in their local, client side, cache. * certain keys in their local, client side, cache.
*
* Keys are grouped into 2^24 slots, in a way similar to Redis Cluster hash
* slots, however here the function we use is crc64, taking the least
* significant 24 bits of the output.
* *
* When a client enables tracking with "CLIENT TRACKING on", each key served to * When a client enables tracking with "CLIENT TRACKING on", each key served to
* the client is hashed to one of such slots, and Redis will remember what * the client is remembered in the table mapping the keys to the client IDs.
* client may have keys about such slot. Later, when a key in a given slot is * Later, when a key is modified, all the clients that may have local copy
* modified, all the clients that may have local copies of keys in that slot * of such key will receive an invalidation message.
* will receive an invalidation message. There is no distinction of database
* number: a single table is used.
* *
* Clients will normally take frequently requested objects in memory, removing * Clients will normally take frequently requested objects in memory, removing
* them when invalidation messages are received. A strategy clients may use is * them when invalidation messages are received. */
* to just cache objects in a dictionary, associating to each cached object rax *TrackingTable = NULL;
* some incremental epoch, or just a timestamp. When invalidation messages are uint64_t TrackingTableTotalItems = 0; /* Total number of IDs stored across
* received clients may store, in a different table, the timestamp (or epoch) the whole tracking table. This givesn
* of the invalidation of such given slot: later when accessing objects, the an hint about the total memory we
* eviction of stale objects may be performed in a lazy way by checking if the are using server side for CSC. */
* cached object timestamp is older than the invalidation timestamp for such
* objects.
*
* The output of the 24 bit hash function is very large (more than 16 million
* possible slots), so clients that may want to use less resources may only
* use the most significant bits instead of the full 24 bits. */
#define TRACKING_TABLE_SIZE (1<<24)
rax **TrackingTable = NULL;
unsigned long TrackingTableUsedSlots = 0;
robj *TrackingChannelName; robj *TrackingChannelName;
/* Remove the tracking state from the client 'c'. Note that there is not much /* Remove the tracking state from the client 'c'. Note that there is not much
@ -90,7 +75,7 @@ void enableTracking(client *c, uint64_t redirect_to) {
c->client_tracking_redirection = redirect_to; c->client_tracking_redirection = redirect_to;
server.tracking_clients++; server.tracking_clients++;
if (TrackingTable == NULL) { if (TrackingTable == NULL) {
TrackingTable = zcalloc(sizeof(rax*) * TRACKING_TABLE_SIZE); TrackingTable = raxNew();
TrackingChannelName = createStringObject("__redis__:invalidate",20); TrackingChannelName = createStringObject("__redis__:invalidate",20);
} }
} }
@ -108,19 +93,20 @@ void trackingRememberKeys(client *c) {
for(int j = 0; j < numkeys; j++) { for(int j = 0; j < numkeys; j++) {
int idx = keys[j]; int idx = keys[j];
sds sdskey = c->argv[idx]->ptr; sds sdskey = c->argv[idx]->ptr;
uint64_t hash = crc64(0, rax *ids = raxFind(TrackingTable,(unsigned char*)sdskey,sdslen(sdskey));
(unsigned char*)sdskey,sdslen(sdskey))&(TRACKING_TABLE_SIZE-1); if (ids == raxNotFound) {
if (TrackingTable[hash] == NULL) { ids = raxNew();
TrackingTable[hash] = raxNew(); int inserted = raxTryInsert(TrackingTable,(unsigned char*)sdskey,
TrackingTableUsedSlots++; sdslen(sdskey),ids, NULL);
serverAssert(inserted == 1);
} }
raxTryInsert(TrackingTable[hash], if (raxTryInsert(ids,(unsigned char*)&c->id,sizeof(c->id),NULL,NULL))
(unsigned char*)&c->id,sizeof(c->id),NULL,NULL); TrackingTableTotalItems++;
} }
getKeysFreeResult(keys); getKeysFreeResult(keys);
} }
void sendTrackingMessage(client *c, long long hash) { void sendTrackingMessage(client *c, char *keyname, size_t keylen) {
int using_redirection = 0; int using_redirection = 0;
if (c->client_tracking_redirection) { if (c->client_tracking_redirection) {
client *redir = lookupClientByID(c->client_tracking_redirection); client *redir = lookupClientByID(c->client_tracking_redirection);
@ -146,49 +132,44 @@ void sendTrackingMessage(client *c, long long hash) {
if (c->resp > 2) { if (c->resp > 2) {
addReplyPushLen(c,2); addReplyPushLen(c,2);
addReplyBulkCBuffer(c,"invalidate",10); addReplyBulkCBuffer(c,"invalidate",10);
addReplyLongLong(c,hash); addReplyBulkCBuffer(c,keyname,keylen);
} else if (using_redirection && c->flags & CLIENT_PUBSUB) { } else if (using_redirection && c->flags & CLIENT_PUBSUB) {
robj *msg = createStringObjectFromLongLong(hash); /* We use a static object to speedup things, however we assume
addReplyPubsubMessage(c,TrackingChannelName,msg); * that addReplyPubsubMessage() will not take a reference. */
decrRefCount(msg); robj keyobj;
initStaticStringObject(keyobj,keyname);
addReplyPubsubMessage(c,TrackingChannelName,&keyobj);
serverAssert(keyobj.refcount == 1);
} }
} }
/* Invalidates a caching slot: this is actually the low level implementation
* of the API that Redis calls externally, that is trackingInvalidateKey(). */
void trackingInvalidateSlot(uint64_t slot) {
if (TrackingTable == NULL || TrackingTable[slot] == NULL) return;
raxIterator ri;
raxStart(&ri,TrackingTable[slot]);
raxSeek(&ri,"^",NULL,0);
while(raxNext(&ri)) {
uint64_t id;
memcpy(&id,ri.key,sizeof(id));
client *c = lookupClientByID(id);
if (c == NULL || !(c->flags & CLIENT_TRACKING)) continue;
sendTrackingMessage(c,slot);
}
raxStop(&ri);
/* Free the tracking table: we'll create the radix tree and populate it
* again if more keys will be modified in this caching slot. */
raxFree(TrackingTable[slot]);
TrackingTable[slot] = NULL;
TrackingTableUsedSlots--;
}
/* This function is called from signalModifiedKey() or other places in Redis /* This function is called from signalModifiedKey() or other places in Redis
* when a key changes value. In the context of keys tracking, our task here is * when a key changes value. In the context of keys tracking, our task here is
* to send a notification to every client that may have keys about such caching * to send a notification to every client that may have keys about such caching
* slot. */ * slot. */
void trackingInvalidateKey(robj *keyobj) { void trackingInvalidateKey(robj *keyobj) {
if (TrackingTable == NULL || TrackingTableUsedSlots == 0) return; if (TrackingTable == NULL) return;
sds sdskey = keyobj->ptr; sds sdskey = keyobj->ptr;
uint64_t hash = crc64(0, rax *ids = raxFind(TrackingTable,(unsigned char*)sdskey,sdslen(sdskey));
(unsigned char*)sdskey,sdslen(sdskey))&(TRACKING_TABLE_SIZE-1); if (ids == raxNotFound) return;;
trackingInvalidateSlot(hash);
raxIterator ri;
raxStart(&ri,ids);
raxSeek(&ri,"^",NULL,0);
while(raxNext(&ri)) {
uint64_t id;
memcpy(&id,ri.key,sizeof(id));
client *c = lookupClientByID(id);
if (c == NULL || !(c->flags & CLIENT_TRACKING)) continue;
sendTrackingMessage(c,sdskey,sdslen(sdskey));
}
raxStop(&ri);
/* Free the tracking table: we'll create the radix tree and populate it
* again if more keys will be modified in this caching slot. */
TrackingTableTotalItems -= raxSize(ids);
raxFree(ids);
raxRemove(TrackingTable,(unsigned char*)sdskey,sdslen(sdskey),NULL);
} }
/* This function is called when one or all the Redis databases are flushed /* This function is called when one or all the Redis databases are flushed
@ -205,6 +186,10 @@ void trackingInvalidateKey(robj *keyobj) {
* we just send the invalidation message to all the clients, but don't * we just send the invalidation message to all the clients, but don't
* flush the table: it will slowly get garbage collected as more keys * flush the table: it will slowly get garbage collected as more keys
* are modified in the used caching slots. */ * are modified in the used caching slots. */
void freeTrackingRadixTree(void *rt) {
raxFree(rt);
}
void trackingInvalidateKeysOnFlush(int dbid) { void trackingInvalidateKeysOnFlush(int dbid) {
if (server.tracking_clients) { if (server.tracking_clients) {
listNode *ln; listNode *ln;
@ -213,84 +198,69 @@ void trackingInvalidateKeysOnFlush(int dbid) {
while ((ln = listNext(&li)) != NULL) { while ((ln = listNext(&li)) != NULL) {
client *c = listNodeValue(ln); client *c = listNodeValue(ln);
if (c->flags & CLIENT_TRACKING) { if (c->flags & CLIENT_TRACKING) {
sendTrackingMessage(c,-1); sendTrackingMessage(c,"",1);
} }
} }
} }
/* In case of FLUSHALL, reclaim all the memory used by tracking. */ /* In case of FLUSHALL, reclaim all the memory used by tracking. */
if (dbid == -1 && TrackingTable) { if (dbid == -1 && TrackingTable) {
for (int j = 0; j < TRACKING_TABLE_SIZE && TrackingTableUsedSlots > 0; j++) { raxFreeWithCallback(TrackingTable,freeTrackingRadixTree);
if (TrackingTable[j] != NULL) { TrackingTableTotalItems = 0;
raxFree(TrackingTable[j]);
TrackingTable[j] = NULL;
TrackingTableUsedSlots--;
}
}
/* If there are no clients with tracking enabled, we can even
* reclaim the memory used by the table itself. The code assumes
* the table is allocated only if there is at least one client alive
* with tracking enabled. */
if (server.tracking_clients == 0) {
zfree(TrackingTable);
TrackingTable = NULL;
}
} }
} }
/* Tracking forces Redis to remember information about which client may have /* Tracking forces Redis to remember information about which client may have
* keys about certian caching slots. In workloads where there are a lot of * certain keys. In workloads where there are a lot of reads, but keys are
* reads, but keys are hardly modified, the amount of information we have * hardly modified, the amount of information we have to remember server side
* to remember server side could be a lot: for each 16 millions of caching * could be a lot, with the number of keys being totally not bound.
* slots we may end with a radix tree containing many entries.
* *
* So Redis allows the user to configure a maximum fill rate for the * So Redis allows the user to configure a maximum number of keys for the
* invalidation table. This function makes sure that we don't go over the * invalidation table. This function makes sure that we don't go over the
* specified fill rate: if we are over, we can just evict informations about * specified fill rate: if we are over, we can just evict informations about
* random caching slots, and send invalidation messages to clients like if * a random key, and send invalidation messages to clients like if the key was
* the key was modified. */ * modified. */
void trackingLimitUsedSlots(void) { void trackingLimitUsedSlots(void) {
static unsigned int timeout_counter = 0; static unsigned int timeout_counter = 0;
if (TrackingTable == NULL) return;
if (server.tracking_table_max_fill == 0) return; /* No limits set. */ if (server.tracking_table_max_fill == 0) return; /* No limits set. */
unsigned int max_slots = size_t max_keys = server.tracking_table_max_fill;
(TRACKING_TABLE_SIZE/100) * server.tracking_table_max_fill; if (raxSize(TrackingTable) <= max_keys) {
if (TrackingTableUsedSlots <= max_slots) {
timeout_counter = 0; timeout_counter = 0;
return; /* Limit not reached. */ return; /* Limit not reached. */
} }
/* We have to invalidate a few slots to reach the limit again. The effort /* We have to invalidate a few keys to reach the limit again. The effort
* we do here is proportional to the number of times we entered this * we do here is proportional to the number of times we entered this
* function and found that we are still over the limit. */ * function and found that we are still over the limit. */
int effort = 100 * (timeout_counter+1); int effort = 100 * (timeout_counter+1);
/* Let's start at a random position, and perform linear probing, in order /* We just remove one key after another by using a random walk. */
* to improve cache locality. However once we are able to find an used raxIterator ri;
* slot, jump again randomly, in order to avoid creating big holes in the raxStart(&ri,TrackingTable);
* table (that will make this funciton use more resourced later). */
while(effort > 0) { while(effort > 0) {
unsigned int idx = rand() % TRACKING_TABLE_SIZE; effort--;
do { raxSeek(&ri,"^",NULL,0);
effort--; raxRandomWalk(&ri,0);
idx = (idx+1) % TRACKING_TABLE_SIZE; rax *ids = ri.data;
if (TrackingTable[idx] != NULL) { TrackingTableTotalItems -= raxSize(ids);
trackingInvalidateSlot(idx); raxFree(ids);
if (TrackingTableUsedSlots <= max_slots) { raxRemove(TrackingTable,ri.key,ri.key_len,NULL);
timeout_counter = 0; if (raxSize(TrackingTable) <= max_keys) {
return; /* Return ASAP: we are again under the limit. */ timeout_counter = 0;
} else { raxStop(&ri);
break; /* Jump to next random position. */ return; /* Return ASAP: we are again under the limit. */
} }
}
} while(effort > 0);
} }
/* If we reach this point, we were not able to go under the configured
* limit using the maximum effort we had for this run. */
raxStop(&ri);
timeout_counter++; timeout_counter++;
} }
/* This is just used in order to access the amount of used slots in the /* This is just used in order to access the amount of used slots in the
* tracking table. */ * tracking table. */
unsigned long long trackingGetUsedSlots(void) { uint64_t trackingGetTotalItems(void) {
return TrackingTableUsedSlots; return TrackingTableTotalItems;
} }