introduce
A memcache hashtable is a memory area that holds item* of a fixed size, that is, an array of Pointers of a fixed size. The size of the array is initialized at startup. The main core points involved are hashtable dynamic expansion, hashtable segment lock, etc. The corresponding hashtable files are hash.c, hash.h, assoc.c, assoc.h
Memcache hash table
The source code to achieve
Initialize the hash algorithm, hash_init ()
// Currently only two hash algorithms are available Jenkins and Murmur3
enum hashfunc_type {
JENKINS_HASH=0, MURMUR3_HASH
};
// when memcache is started
int hash_init(enum hashfunc_type type) {
switch(type) {
case JENKINS_HASH:
hash = jenkins_hash; // Jenkins_hash function pointer
settings.hash_algorithm = "jenkins";
break;
case MURMUR3_HASH:
hash = MurmurHash3_x86_32; // murmur3_hash function pointer
settings.hash_algorithm = "murmur3";
break;
default:
return - 1;
}
return 0;
}Copy the code
Initialize hashTable, assoc_init()
settings.hashpower_init // The reference value of hashtable, set when memcache is started
void assoc_init(const int hashtable_init) {
// Assign hashpower if it exists; otherwise, use the default hashpower = 16
if (hashtable_init) {
hashpower = hashtable_init;
}
// hashSize Calculates the final hashtable length based on the passed hashpower
// hashsize(n) -> #define hashsize(n) ((ub4)1<<(n))
// 实际上就是进行右移运算,右移 hashpower 位 ( 1 << 16 = 65536 )
Item ** primary_hashtable -> hashtable header pointer
primary_hashtable = calloc(hashsize(hashpower), sizeof(void *));
if (! primary_hashtable) {
fprintf(stderr."Failed to init hashtable.\n");
exit(EXIT_FAILURE);
}
STATS_LOCK();
stats.hash_power_level = hashpower;
stats.hash_bytes = hashsize(hashpower) * sizeof(void *); // How many bytes of memory does hashtable take up
STATS_UNLOCK();
}Copy the code
Hashtable and
In the memcache thread initialization section, some mutex will be initialized, including the segment lock of the hashtable. The segment lock may have multiple keys corresponding to a lock, instead of each key corresponding to a lock, because different keys may hash the same value. So they all correspond to this lock.
Initialize the Hashtable lock
// the memcached_thread_init function is a piece of code
void memcached_thread_init(int nthreads, struct event_base *main_base) {
int i;
int power;
/ /...
// Set the hashtable lock to power based on the number of threads
if (nthreads < 3) {
power = 10;
} else if (nthreads < 4) {
power = 11;
} else if (nthreads < 5) {
power = 12;
} else {
/* 8192 buckets, and central locks don't scale much past 5 threads */
power = 13;
}
// Power is less than hashPower because the number of hashtable locks and the number of hashtables are not one-to-one
// Not every hashtable key has a lock. Memcache uses multiple keys to save memory
// It corresponds to a lock, namely a segment lock
if (power >= hashpower) {
fprintf(stderr."Hash table power size (%d) cannot be equal to or less than item lock table (%d)\n", hashpower, power);
fprintf(stderr."Item lock table grows with `-t N` (worker threadcount)\n");
fprintf(stderr."Hash table grows with `-o hashpower=N` \n");
exit(1);
}
// Calculate the length of the hashtable lock, default 4 threads power = 12, 1 << 12 = 4096/ lock
item_lock_count = hashsize(power);
item_lock_hashpower = power;
/ / to apply for the lock
// Then the item_LOCKS do not expand as the Hashtable expands
Hashsize (power) -> 4096/ lock
// This will also result in more and more keys for each lock
item_locks = calloc(item_lock_count, sizeof(pthread_mutex_t));
if (! item_locks) {
perror("Can't allocate item locks");
exit(1);
}
// Loop to initialize the item_locks
for (i = 0; i < item_lock_count; i++) {
pthread_mutex_init(&item_locks[i], NULL);
}
/ /...
}Copy the code
The memcache HashTable has been initialized with the above code
Insert and expand hashtable
Memcache hashtable capacity expansion processing way is, in the program startup stage, open a thread on standby, when the need to expand the trigger thread, dynamic capacity expansion processing, and capacity expansion operation is not a table lock, but the use of the above mentioned segment lock
Capacity Expansion process:
When hashtable is expanded memcache copies the current primary_hashtable to old_hashtable. Then expand primary_hashtable (1 << hashpower + 1). If you are currently in hashtable expansion and there are requests to access the key, It determines whether the current key-hash index position is smaller than the current migrated index position. If it is smaller, it indicates that it has been migrated to the new primary_hashTable index position. If it is larger, it indicates that it has not been migrated to the new primary_Hashtable index position. So it will still operate at the old old_hashtable location, and then it will free old_hashtable after capacity expansion, and then it will all operate at primary_hashtable.
Description:
When a hashtable is migrated, it migrates index positions from small to large, which is the hash value. Therefore, when a hashtable is migrated, a lock is placed on the current index position, which is the segment lock, but the number of locks is limited. Many index locations share the same lock. Such as: 0 & (4096-1) = 0, 4096&(4096-1) = 0, 8192&(4096-1) = 0 In addition, if the key-hash value corresponds to the index position we are migrating, it will block because it has been locked. If other keys do not match the index position we are migrating, they will be accessed normally. Whether to access primary_hashtable or old_hashtable depends on the conditions described above.
Insert hashtable function, assoc_insert()
// hv = hash(key, nkey); Hash value
int assoc_insert(item *it, const uint32_t hv) {
unsigned int oldbucket;
// Check whether the capacity is being expanded
if (expanding &&
(oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
{
// Insert into hashtable
it->h_next = old_hashtable[oldbucket];
old_hashtable[oldbucket] = it;
} else {
// Insert into hashtable
it->h_next = primary_hashtable[hv & hashmask(hashpower)];
primary_hashtable[hv & hashmask(hashpower)] = it;
}
// Lock to prevent multiple threads from triggering capacity expansion at the same time
pthread_mutex_lock(&hash_items_counter_lock);
hash_items++;
// Determine whether capacity expansion is required
if (! expanding && hash_items > (hashsize(hashpower) * 3) / 2) {
// Capacity expansion is triggered
assoc_start_expand();
}
pthread_mutex_unlock(&hash_items_counter_lock);
MEMCACHED_ASSOC_INSERT(ITEM_key(it), it->nkey, hash_items);
return 1;
}Copy the code
Trigger the expansion operation, assoc_start_expand()
static void assoc_start_expand(void) {
if (started_expanding)
return;
started_expanding = true;
// Wake up the hashtable expansion thread
pthread_cond_signal(&maintenance_cond);
}Copy the code
Hashtable extension thread function, assoc_maintenance_thread()
Pthread_cond_wait (&maintenance_cond, &maintenance_lock);
// Until pthread_cond_signal wakes up the operation above, then proceed.
static void *assoc_maintenance_thread(void *arg) {
mutex_lock(&maintenance_lock);
/ / death cycle
while (do_run_maintenance_thread) {
int ii = 0;
Old_hashtable -> primary_hashtable */
for (ii = 0; ii < hash_bulk_move && expanding; ++ii) {
item *it, *next;
int bucket;
void *item_lock = NULL;
// Lock the index location that is currently being migrated
if ((item_lock = item_trylock(expand_bucket))) {
// Loop through all items at the current index position
// Because there may be more than one item in the current index position
// Hash conflict chain resolution
for (it = old_hashtable[expand_bucket]; NULL! = it; it = next) {// Get the next item
next = it->h_next;
// Relocate an index to the new hashTable length
bucket = hash(ITEM_key(it), it->nkey) & hashmask(hashpower);
// Save the assignment
it->h_next = primary_hashtable[bucket];
primary_hashtable[bucket] = it;
}
// After all items are migrated, the previous hashtable index is empty
old_hashtable[expand_bucket] = NULL;
// Update the current index position
expand_bucket++;
// Determine whether all migrations are complete
if (expand_bucket == hashsize(hashpower - 1)) {
expanding = false; // Disable the expanding HashTable status
free(old_hashtable); / / release old_hashtable
STATS_LOCK();
stats.hash_bytes -= hashsize(hashpower - 1) * sizeof(void *);
stats.hash_is_expanding = 0;
STATS_UNLOCK();
if (settings.verbose > 1)
fprintf(stderr."Hash table expansion done\n"); }}else {
// If the lock fails, another thread may be manipulating the item in the current hashTable index
// So wait until you grab the lock
usleep(10*1000);
}
/ / releases the lock
if (item_lock) {
item_trylock_unlock(item_lock);
item_lock = NULL; }}if(! expanding) {/* We are done expanding.. just wait for next invocation */
started_expanding = false;
// Wait for wake up
pthread_cond_wait(&maintenance_cond, &maintenance_lock);
pause_threads(PAUSE_ALL_THREADS);
// Expand hashtable operation
assoc_expand();
// Execute the while loop downpause_threads(RESUME_ALL_THREADS); }}return NULL;
}Copy the code
Expand hashtable function, assoc_expand()
static void assoc_expand(void) {
// Save a copy of the current HashTable
old_hashtable = primary_hashtable;
// Add hashPower + 1 each time
// (1 << 16) = 65536, (1 << 17) = 131072
primary_hashtable = calloc(hashsize(hashpower + 1), sizeof(void *));
if (primary_hashtable) {
if (settings.verbose > 1)
fprintf(stderr."Hash table expansion starting\n");
hashpower++; // hashpower++
expanding = true; // Indicates that the capacity is being expanded
expand_bucket = 0; // Currently migrated to the primary_hashtable index position
STATS_LOCK();
stats.hash_power_level = hashpower;
stats.hash_bytes += hashsize(hashpower) * sizeof(void *);
stats.hash_is_expanding = 1;
STATS_UNLOCK();
} else {
primary_hashtable = old_hashtable;
/* Bad news, but we can keep running. */}}Copy the code
Find the hashtable function, assoc_find()
item *assoc_find(const char *key, const size_t nkey, const uint32_t hv) {
item *it;
unsigned int oldbucket;
// Whether capacity expansion is being performed
if (expanding &&
(oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
{
/ / get the item
it = old_hashtable[oldbucket];
} else {
/ / get the item
it = primary_hashtable[hv & hashmask(hashpower)];
}
item *ret = NULL;
int depth = 0;
// Loop judgment, since hash conflicts are chain-resolved, we need to traverse the chain to find the keys equal
while (it) {
if ((nkey == it->nkey) && (memcmp(key, ITEM_key(it), nkey) == 0)) {
ret = it;
break;
}
it = it->h_next;
++depth;
}
MEMCACHED_ASSOC_FIND(key, nkey, depth);
return ret;
}Copy the code
The end of the
Item_locks are locked on the key of the current hashtable. As the hashtable grows, the lock contention becomes larger and larger. Performance may also have some impact.