Рассмотрим базу данных с миллиардами строк и огромными объемами данных. Чтобы получить значения, вам нужно будет запросить базу данных и дождаться результатов. Эффективность и скорость получения результатов зависят от двух основных факторов:

  1. Насколько оптимизирован запрос
  2. Насколько мощное оборудование

Мы сосредоточимся на последнем в рамках этой статьи.

Улучшение аппаратных характеристик машины называется вертикальным масштабированием. Это включает в себя использование более мощного процессора, увеличение оперативной памяти и включение лучшего диска. Допустим, мы достигли пика вертикального масштабирования на нашей машине, но база данных все еще слишком велика, а наш процессор все еще не может обрабатывать запросы. Что дальше?

Вот где на помощь приходят распределенные системы.

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

Ура! проблема решена, использование ЦП низкое, кризис предотвращен. Верно? Неа. В программной инженерии каждое решение проблемы сопровождается своим новым набором промежуточных проблем. Чтобы понять проблему с нашим недавно обнаруженным подходом к распределенным базам данных, давайте сделаем краткий и грубый обзор того, как работают базы данных.

Получение фрагмента данных сводится к выбору значения по его ключу.

Многие базы данных реализуют это, сохраняя ключ значения как его адрес в памяти, обозначаемый индексом. Чтобы получить значение, мы просматриваем индекс и находим сопоставленные значения за постоянное время (молниеносно быстро 🔥). Это довольно простая операция, когда база данных находится на одном компьютере, однако, когда база данных распределена между несколькими серверами, мы сталкиваемся с проблемой незнания того, на каком экземпляре сервера находится наш ключ. Проще говоря, мы не знаем, в каком наборе миллионов строк содержится та или иная строка, которую мы хотим найти. Первое решение этой проблемы выглядит следующим образом:

Рассмотрим массив размером 4 (количество экземпляров сервера).

Серверы = S0, S!, S2, S3

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

Допустим, наш ключ — «Красные туфли». После прохождения хэш-функции

ХЭШ (красные туфли) = 6.

Экземпляр сервера с нашим значением = 6 % 4 = 2 (S2)

Точно так же для «Синих туфель» предположим, что HASH (Синие туфли) = 8.

Экземпляр сервера с нашим значением = 8 % 4 = 0 (S0)

Ура! проблема решена, поиск значений по-прежнему выполняется за постоянное время, и единственными накладными расходами является время, необходимое для выполнения хэш-функции (все еще очень быстро). Кризис предотвращен, верно? Неа.

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

Мы сразу видим проблему.

Первоначально S2 имел значение для ключа «Красные туфли», а S0 имел значение для ключа «Синие туфли», но теперь, после добавления другого сервера,

Экземпляр сервера со значением для «Red Shoes» = HASH(Red Shoes) % 5 = 6 % 5 = 1 (S1)

Экземпляр сервера со значением для «Blue Shoes» = HASH(Blue Shoes) % 5 = 8 % 5 = 3 (S3)

О, согласованность нарушена, и исходные значения ключей перемешаны. Чтобы исправить это, нам потребуется переместить все данные и нарушить работу всего кластера серверов. Это очень дорогостоящая операция, и она определенно того не стоит, учитывая тот факт, что нам может потребоваться несколько раз увеличивать или уменьшать количество экземпляров сервера. Ну и что дальше?

Введите Последовательное хэширование.

Вместо того, чтобы использовать линейную структуру данных для отслеживания экземпляров нашего сервера, давайте представим круг (360 градусов). Мы берем экземпляр каждого сервера, хэшируем его идентификатор (может быть IP-адрес или определенный пользователем идентификатор) с помощью функции C_HASH() и помещаем его в круг после модификации с помощью 360. Предположим:

C_HASH(S0) % 360 = 0

C_HASH(S1) % 360 = 90

C_HASH(S2) % 360 = 180

C_HASH(S3) % 360 = 270

То, как работает последовательное хеширование, заключается в том, что

  1. Мы берем ключ
  2. Вычислите его C_HASH()
  3. Модифицируйте его с помощью 360
  4. Поместите его в круг и найдите ближайший следующий экземпляр сервера в круге.

Этот конкретный экземпляр отвечает за обработку нашего запроса.

Допустим, C_HASH(Red Shoes) % 360 = 40, поэтому S1(90) будет иметь свое значение.

C_HASH(Синие туфли) % 360 = 280, поэтому S0(0) будет иметь свое значение.

Возвращаясь к нашей первоначальной проблеме, что, если мы добавим еще один сервер?

Допустим, мы добавляем сервер S4 и:

C_HASH(S4) % 360 = 50

Мы поместим S4 между S0 и S1, и, согласно нашей логике, любой ключ с C_HASH() ‹= 50 должен обрабатываться S4.

Так что насчет ключей, уже присутствующих в S1 с C_HASH() ‹= 50? Мы устанавливаем соединение между S1 и S4 и передаем все эти данные с S1 на S4. Таким образом, единственное нарушение происходит со следующим непосредственным соседом нашего недавно добавленного сервера, что делает этот процесс намного лучше, чем наше предыдущее решение.

Вам может быть интересно, а что, если серверов больше 360? Как мы наносим их на карту? Ну, я выбрал 360 в качестве примера в этой статье. Мы можем выбрать больше или меньше слотов в зависимости от масштаба нашего приложения.

Ура! Проблема решена, кризис предотвращен, верно? Вы уже знаете ответ.

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

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

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

P.S. Надеюсь, вы уже поняли, что название этой статьи — каламбур с благими намерениями. XD