Распределитель Slab - это основной модуль кэш-системы, который в значительной степени определяет, насколько эффективно может использоваться узкое место - память. Остальные 3 части, а именно алгоритм LRU истечения срока входа; и модель, управляемая событиями, основанная на libevent; и постоянная жесткость в отношении распределения данных построены вокруг этого.

Плита I
Плита II
Плита III
LRU I
LRU II (эта статья)
LRU III
Событийный я

Чаще всего алгоритм LRU сочетается с хэш-картой и называется кешем LRU.

В LRU-кэше хэш-карта обеспечивает быстрый доступ к кешированным объектам; а LRU предотвращает бесконечное увеличение кеша, отмечая просроченные или так называемые наименее недавно использованные объекты. На этот раз мы исследуем реализацию хеш-карты в memcached.

Обзор (учебник перекрывается, пропускать)

Хеш-карта - это, по сути, массив фиксированного размера, который индексирует значения с целыми числами, хешированными из ключа s. В хэш-карте запись массива называется корзиной. Если хеш-значение превышает количество сегментов (т. Е. Размер массива), оно переключается с помощью ‘mod’ (%). Коллизия возникает, когда более двух ключей приводят к одному и тому же хэш-значению или к разным хеш-значениям переходят в того же ведра, то на коллизии формируется связанный список *.

Столкновение снижает скорость поиска для последовательного доступа к связанному списку, поэтому необходимо увеличить номер сегмента и повторно хешировать записи с новым номером сегмента, прежде чем производительность станет слишком низкой. Об этом процессе пойдет речь скоро.

Инициализация модуля

Первым подходящим методом является

hash_init

который просто определяет тип хеш-алгоритма.

int hash_init(enum hashfunc_type type) {
    switch(type) {
        case JENKINS_HASH:
            hash = jenkins_hash;
            settings.hash_algorithm = "jenkins";
            break;
        case MURMUR3_HASH:
            hash = MurmurHash3_x86_32;
            settings.hash_algorithm = "murmur3";
            break;
        default:
            return -1;
    }
    return 0;
}

Этот метод вызывается отсюда как один из шагов инициализации перед тем, как логика перейдет в основной цикл обработки событий.

Параметр hash_type устанавливается в MURMUR3_HASH указанным аргументом командной строки modern.

Второй способ

assoc_init

выделяет массив фиксированного размера, упомянутый в начале.

void assoc_init(const int hashtable_init) {
    if (hashtable_init) {
        hashpower = hashtable_init;
    }
    primary_hashtable = calloc(hashsize(hashpower), sizeof(void *));
    if (! primary_hashtable) {
        fprintf(stderr, "Failed to init hashtable.\n");
        exit(EXIT_FAILURE);
    }
...// scr: stat
}

Этот метод вызывается в том же месте, что и hash_init, как часть процесса начальной загрузки системы.

…
 assoc_init(settings.hashpower_init);
…

Фактический начальный размер определяется параметром hashpower в командной строке.

...
case HASHPOWER_INIT:
    if (subopts_value == NULL) {
        fprintf(stderr, "Missing numeric argument for hashpower\n");
        return 1;
    }
    settings.hashpower_init = atoi(subopts_value);
    if (settings.hashpower_init < 12) {
        fprintf(stderr, "Initial hashtable multiplier of %d is too low\n",
            settings.hashpower_init);
        return 1;
    } else if (settings.hashpower_init > 64) {
        fprintf(stderr, "Initial hashtable multiplier of %d is too high\n"
            "Choose a value based on \"STAT hash_power_level\" from a running instance\n",
            settings.hashpower_init);
        return 1;
    }
    break;
...

Как было сказано ранее, массив можно заменить новым выделенным массивом большего размера, если производительность упадет из-за чрезмерного коллизии. Далее мы обсудим процесс

Масштабирование и миграция входа

В memcached порог равен 1,5, что означает, что если номер элемента превышает 1,5 * номер сегмента, начинается упомянутый процесс расширения.

...
    if (! expanding && hash_items > (hashsize(hashpower) * 3) / 2) {
        assoc_start_expand();
    }
...

assoc_start_expand просто устанавливает флаг (т.е. do_run_maintenance_thread) и отправляет сигнал для пробуждения потока обслуживания, который выполняет фактическую работу.

static void assoc_start_expand(void) {
    if (started_expanding)
        return;
started_expanding = true;
    pthread_cond_signal(&maintenance_cond);
}

Основной цикл потока обслуживания

static void *assoc_maintenance_thread(void *arg) {
mutex_lock(&maintenance_lock);
  while (do_run_maintenance_thread/* scr: the flag*/) {
    int ii = 0;
/* There is only one expansion thread, so no need to global lock. */
    for (ii = 0; ii < hash_bulk_move && expanding; ++ii) { //  2)
      item *it, *next;
      int bucket;
      void *item_lock = NULL;
/* bucket = hv & hashmask(hashpower) =>the bucket of hash table
       * is the lowest N bits of the hv, and the bucket of item_locks is
       *  also the lowest M bits of hv, and N is greater than M.
       *  So we can process expanding with only one item_lock. cool! */
      if ((item_lock = item_trylock(expand_bucket))) { // scr: 3)
        for (it = old_hashtable[expand_bucket]; NULL != it; it = next) {
          next = it->h_next; // scr: ------------------------> 4)
          bucket = hash(ITEM_key(it), it->nkey) & hashmask(hashpower);
          it->h_next = primary_hashtable[bucket];
          primary_hashtable[bucket] = it;
        }
old_hashtable[expand_bucket] = NULL; // scr: --------------> 4.1)
expand_bucket++; // scr: ------------------------------------> 5)
        if (expand_bucket == hashsize(hashpower - 1)) { //     6)
          expanding = false;
          free(old_hashtable);
... // scr: -------------------------------------------> stat & log
        }
      } else {
        usleep(10*1000); // scr: --------------------------> 3.1)
      }
if (item_lock) { // scr: ----------------------------------> 3.2)
        item_trylock_unlock(item_lock);
        item_lock = NULL;
      }
    }
if (!expanding) {
      /* We are done expanding.. just wait for next invocation */
      started_expanding = false;
      pthread_cond_wait(&maintenance_cond, &maintenance_lock); // scr: --------------------------------------------------------> 0)
      /* assoc_expand() swaps out the hash table entirely, so we need
       * all threads to not hold any references related to the hash
       * table while this happens.
       * This is instead of a more complex, possibly slower algorithm to
       * allow dynamic hash table expansion without causing significant
       * wait times.
       */
      pause_threads(PAUSE_ALL_THREADS);
      assoc_expand(); // scr: -------------------------------> 1)
      pause_threads(RESUME_ALL_THREADS);
    }
  }
  return NULL;
}
{% endcodeblock %}

0) Здесь поток ожидает выхода из спящего режима и начинает работать и засыпает, когда нечего делать.

1) assoc_expand выделяет ресурс для новой хэш-карты, которая предназначена для замены старой, инициализированной отсюда.

/* grows the hashtable to the next power of 2. */
static void assoc_expand(void) {
    old_hashtable = primary_hashtable;
primary_hashtable = calloc(hashsize(hashpower + 1), sizeof(void *));
    if (primary_hashtable) {
... // scr: log
        hashpower++;
        expanding = true;
        expand_bucket = 0;
... // scr: stat
    } else {
        primary_hashtable = old_hashtable;
        /* Bad news, but we can keep running. */
    }
}

2) Перенести только определенное количество элементов за один пакет. hash_bulk_move позволяет избежать слишком долгого зависания потока при вызове stop_assoc_maintenance_thread. В отличие от обсуждаемого assoc_start_expand, stop_assoc_maintenance_thread сбрасывает флаг do_run_maintenance_thread и отправляет сигнал для пробуждения потока для выхода.

void stop_assoc_maintenance_thread() {
    mutex_lock(&maintenance_lock);
    do_run_maintenance_thread = 0;
    pthread_cond_signal(&maintenance_cond);
    mutex_unlock(&maintenance_lock);
/* Wait for the maintenance thread to stop */
    pthread_join(maintenance_tid, NULL);
}

3) («Блокировка элемента» фактически работает для всего сегмента, поэтому я назову его вместо этого блокировкой сегмента). Используйте низкий приоритет item_trylock (i.e., pthread_mutex_trylock) для доступа к замок ведра; 3.1) спать на 10 секунд, когда элемент недоступен; и 3.2) снимите блокировку с помощью item_trylock_unlock после завершения миграции (этого сегмента).

void *item_trylock(uint32_t hv) {
    pthread_mutex_t *lock = &item_locks[hv & hashmask(item_lock_hashpower)];
    if (pthread_mutex_trylock(lock) == 0) {
        return lock;
    }
    return NULL;
}

4) Перехешируйте все элементы в корзине и перенесите их в новую хэш-карту.

5) Перейдите к следующему сегменту.

6) Достигнут последний сегмент - ›перейти к 0)

Начало обслуживания потока

int start_assoc_maintenance_thread() {
    int ret;
    char *env = getenv("MEMCACHED_HASH_BULK_MOVE");
    if (env != NULL) {
        hash_bulk_move = atoi(env);
        if (hash_bulk_move == 0) {
            hash_bulk_move = DEFAULT_HASH_BULK_MOVE;
        }
    }
    pthread_mutex_init(&maintenance_lock, NULL);
    if ((ret = pthread_create(&maintenance_tid, NULL,
                              assoc_maintenance_thread, NULL)) != 0) {
        fprintf(stderr, "Can't create thread: %s\n", strerror(ret));
        return -1;
    }
    return 0;
}

Подобно методам инициализации, он вызывается при загрузке системы.

Остановка резьбы для обслуживания

Этот метод вызывается в процессе завершения работы системы, поэтому по логике он противоположен start_assoc_maintenance_thread. Тем не менее, действия этого метода противоположны механизму assoc_start_expand.

void stop_assoc_maintenance_thread() {
    mutex_lock(&maintenance_lock);
    do_run_maintenance_thread = 0;
    pthread_cond_signal(&maintenance_cond);
    mutex_unlock(&maintenance_lock);
/* Wait for the maintenance thread to stop */
    pthread_join(maintenance_tid, NULL);
}

Как было сказано ранее, обсуждаемый здесь процесс расширения и миграции влияет на логику всех операций, связанных с хэш-картой. В следующем разделе мы рассмотрим эти операции.

CRUD

Н.б., assoc_delete обсуждалось в предыдущем посте; а в системе "ключ-значение" update и insert по существу одинаковы, поэтому в этом разделе будут обсуждаться операции C (создать) и R (только чтение).

assoc_insert

int assoc_insert(item *it, const uint32_t hv) {
    unsigned int oldbucket;
if (expanding &&
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
    {
        it->h_next = old_hashtable[oldbucket]; // scr: -------> 1)
        old_hashtable[oldbucket] = it;
    } else {
        it->h_next = primary_hashtable[hv & hashmask(hashpower)]; // scr: --------------------------------------------------------------> 2)
        primary_hashtable[hv & hashmask(hashpower)] = it;
    }
pthread_mutex_lock(&hash_items_counter_lock);
    hash_items++; // scr: --------------------------------------> 3)
    if (! expanding && hash_items > (hashsize(hashpower) * 3) / 2) {
        assoc_start_expand();
    }
    pthread_mutex_unlock(&hash_items_counter_lock);
    return 1;
}

1) Если происходит процесс расширения и связанный сегмент хеш-ключ не был перенесен, вставьте элемент в old_hashtable. Обратите внимание, что здесь мы используем старый номер сегмента (т.е. hashmask(hashpower - 1))) для вычисления хеш-индекса.

2) В противном случае вставьте элемент в primary_hashtable напрямую.

3) Увеличьте глобальную переменную hash_items (количество элементов). Если после добавления элемента оно превышает пороговое значение, начните процесс развертывания и миграции. Обратите внимание, что это также преамбула предыдущего раздела.

assoc_find

item *assoc_find(const char *key, const size_t nkey, const uint32_t hv) {
    item *it;
    unsigned int oldbucket;
if (expanding &&
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
    {
        it = old_hashtable[oldbucket]; // scr: ---------------> 1)
    } else {
        it = primary_hashtable[hv & hashmask(hashpower)]; //    2)
    }
item *ret = NULL;
    int depth = 0;
    while (it) { // scr: -------------------------------------> 3)
        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;
}

1) Как и в случае с assoc_insert, этот шаг определяет местонахождение сегмента из old_hashtable, когда ключ еще не был перехэширован.

2) В противном случае используйте primary_hashtable напрямую.

3) Просмотрите связанный список и сравните ключ (вместо хеш-индекса) напрямую, чтобы найти элемент в случае столкновения.

Стоит отметить, что assoc_find очень похож на _hashitem_before, который обсуждался в предыдущем посте. Разница в том, что _hashitem_before возвращает адрес следующего члена элемента перед найденным (pos = &(*pos)->h_next;), что требуется при удалении записей из односвязного списка; в то время как этот метод возвращает непосредственно найденный элемент (ret = it;).

Изначально опубликовано на holmeshe.me.