При кластеризации становится актуальным вопрос распределения ключей: как наиболее эффективным образом распределить ключи по серверам (шардинг/решардинг). Для этого необходимо определить функцию распределения ключей, которая по ключу возвращает номер сервера, на котором он должен храниться (или номера серверов, если хранение происходит с избыточностью).
Исторически первой функцией распределения была функция модуля:
номер_сервера = crc32(ключ) % количество_серверов
Такая функция обеспечивает равномерное распределение ключей по серверам, однако проблемы возникают при переконфигурировании кластера: изменение количества серверов приводит к перемещению значительной части ключей по серверам, что эквивалентно потере значительной части ключей.
Альтернативой для выше описанной функции является механизм консистентного хэширования, который при переконфигурации кластера сохраняет положение ключей по серверам. Этот подход например был реализован в клиентах memcached впервые разработчиками сервиса Last.fm в апреле 2007 года.

Суть алгоритма заключается в следующем: мы рассматриваем набор целых чисел от 0 до 232, «закручивая» числовую ось в кольцо (склеиваем 0 и 232). Каждому сервера из пула memcached-серверов мы сопоставляем число на кольце (рисунок слева, сервера A, B и C). Ключ хэшируется в число в том же диапазоне (на рисунке – синие точки 1-4), в качестве сервера для хранения ключа мы выбираем сервер в точке, ближайшей к точке ключа в направлении по часовой стрелке. Если сервер удаляется из пула или добавляется в пул, на оси появляется или исчезает точка сервера, в результате чего лишь часть ключей перемещается на другой сервер. На рисунке 2 справа показана ситуация, когда сервер C был удалён из пула серверов и добавлен новый сервер D. Легко заметить, что ключи 1 и 2 не поменяли привязки к серверам, а ключи 3 и 4 переместились на другие сервера. На самом деле одному серверу ставится в соответствие 100-200 точек на оси (пропорционально его весу в пуле), что улучшает равномерность распределения ключей по серверам в случае изменения их конфигурации.
Разберем все на простом примере, без учета значения 232 и без учета веса серверов, предварительно договорившись, что наша функция crc32 возвращает указанные ниже значения (это нужно для наглядности и понимания). Итак, имеем 3 сервера:
| сервер | crc32(сервер) |
| 1.1.1.1 | 10 |
| 1.1.1.2 | 20 |
| 1.1.1.3 | 30 (будем считать данное значение максимальным, а т.к. кольцо должно замкнуться, то и одновременно минимальным - равным нулю) |
Как мы видим, получились некие цепочки: 0-10, 10-20, 20-30.
Рассмотрим ситуацию, когда все сервера доступны
| ключ | crc32(ключ) | попадет на сервер |
| 4 | 9 | 1.1.1.1 |
| 5 | 16 | 1.1.1.2 |
| 6 | 26 | 1.1.1.3 |
| 7 | 36 | 1.1.1.1 (т.к. 36/30 = 1, остаток 6, а 6 ближе к 10 чем к 0) |
| 8 | 66 | 1.1.1.1 (т.к. 66/30 = 2, остаток 6, а 6 ближе к 10 чем к 0) |
Рассмотрим ситуацию, когда сервер 2 недоступен
| ключ | crc32(ключ) | попадет на сервер |
| 4 | 9 | 1.1.1.1 |
| 5 | 16 | 1.1.1.1 (т.к. 16 ближе к 10, чем к 30) |
| 6 | 26 | 1.1.1.3 |
| 7 | 36 | 1.1.1.1 |
| 8 | 66 | 1.1.1.1 |
Рассмотрим ситуацию, когда сервер 1 недоступен
| ключ | crc32(ключ) | попадет на сервер |
| 4 | 9 | 1.1.1.3 (т.к. 9 ближе к 0, чем к 20) |
| 5 | 16 | 1.1.1.2 (т.к. 16 ближе к 20, чем к 0) |
| 6 | 26 | 1.1.1.3 |
| 7 | 36 | 1.1.1.3 (т.к. 6 ближе к 0, чем к 20) |
| 8 | 66 | 1.1.1.3 (т.к. 6 ближе к 0, чем к 20) |
Фактически расчет выглядит так:
В реальной жизни цепочки будут не такими пропорциональными, например 0-12, 12-14, 14-30 что повлечет за собой неравномерное распределение данных. Поэтому и появляются разного рода оптимизации.
Чтобы не реализовывать подобный алгоритм самому, в интернете полно реализаций на множестве языков программирования, например на php и python
Если вы знаете, что у вас будет максимум 10 серверов, возможно, вам нужно простое решение. Например, взять максимально возможное значение crc32 и вручную создать диапазоны по серверам, например:
| сервер | диапазон |
| 1.1.1.1 | 0 - 20 (потому что на сервере много памяти) |
| 1.1.1.2 | 20 - 26 (потому что на сервере немного памяти) |
| 1.1.1.3 | 27 - 30 (потому что на сервере мало памяти) |
Таким образом, если у Вас выйдет из строя какой-то сервер, то все будет предсказуемо.
Если думать дальше, то тут однозначного решения не найти, например Вы решите добавить еще какой-то сервер, то тут возможно понадобится ручная миграция данных (до миграции лучше не доводить - трудозатратно). Значит надо брать сервера с запасом или в процессе можно просто докупать память на эти сервера. Если решили увеличить память на серверах, то неплохим решением будет предварительное создание реплики того сервера, который будет обновлен.
Еще мне понравился вариант используемый в компании Badoo, там добавляют новый сервер, на старый сервер данные больше не пишут (только читают, при этом новый сервер в приоритете), таким образом данные на старом сервере протухают и самоудяляются и сервер выводится из строя.
Комментарии