Пришел запрос, который нужно боработать, сходив в базу за данными. Например возьмем простой select из базы
Получили из запроса shardKey (в зависимости от запроса может быть либо явным полем, либо вычисляемым). Например считаем, что это явное поле типа айдишника, какой-нибудь группы сущностей
Вычислили hash(shardkey) % 65536, это значение и есть номер бакета, где лежат данные для данного shardKey - bucket_index.
Теперь нужно понять, какой сервер отвечает за диапазон бакетов, в который попадает вычисленный шагом выше
Сходили в интервальное дерево, построенное поверх выбранной из master шарда конфигураци шардированного кластера. Взяли тот узел этого дерева, который отвечает за диапазон, в который попадает вычисленный на шаге 3 бакет.
Если убрать интервальное дерево из рассмотрения, то на базе это будет (в псевдо-SQL) выглядеть как SELECT * FROM shards where bucket_index_start <= bucket_index <= bucket_index_end LIMIT 1
Данному решению неважно, какой load balancing алгоритм используется в кластере, куда задеплоен сервис, оно находится уровнем ниже - на data access level. Потому я про load balancing на уровне k8s в этой статье и не писал.
В разных частях наших k8s кластеров используются различные алгоритмы банасировки, не только round robin, но и он тоже присутствует. Подробнее ответить вам не могу, могу только сказать, что ничего самописного в этой области у нас нет - выезжаем на стандартных механизмах k8s.
Что касается гибридных решений для Postgres — большинство из них основаны на технологии PG FDW. FDW (foreign data wrapper) — это функциональность сервера Postgres, которая позволяет ему рассматривать данные на удаленном сервере как часть большего набора и интерпретировать этот разделенный на несколько серверов набор данных как единое целое. У этой технологии есть свои проблемы, но самая большая из них — доступ к данным через FDW чрезвычайно медленный.
Это выдержка из статьи.
Еще один момент - если мы пользуемся сторонними решениями, то их изменение под наши требования (временной промежуток от постановки задачи до ее выкатки в prod) займет больше времени, чем для случая, когда это полностью самописное решение.
"Приложение" в этом контексте - это back-end сервис, а не мобильное приложение. Мобильное приложение вообще в базы напрямую не ходит, только в back-end сервисы.
Нет, мы сейчас в обсуждении находимся на ноде k8s, в каком-нибудь поде с нашим back-end сервисом. Мобильное приложение оперирует back-end сервисами, выставляющими для него свои API.
Так вот, мы в back-end сервисе. К нему прилетает запрос. Мы понимаем, что этот запрос нужно обслужить, сходив в шардированную базу.
Из запроса берется значение ключа шардирования (обычно это одно из полей запроса, но не всегда), оно прогоняется через преобразование (в том числе - хэширование), из него получается номер бакета. Дальше мы идем в in-memory cache сервиса, где лежит интервальное дерево с диапазонами бакетов и конфигурацией обслуживающих эти диапазоны шардов. Из дерева выбираем узел, который нам подходит по диапазону, в этом узле есть конфигурация соответсвующей ноды кластера, в том числе со строкой подключения.
Далее мы идем в еще один in-memory cache и смотрим, есть ли у нас свободный коннект с такими же параметрами подключения. Если да - берем его. Если нет - создаем новое подключение, которое мы, по завершении работы с ним, положим в этот самый in-memory cache.
У приложения по сути есть вся конфигурация кластера - где какой шард лежит, какой диапазон бакетов обслуживает и прочее. Приложение эту конфигурацию получило, единожды сходив в master шард.
Так что да, можно сказать, что приложение само (при помощи библиотеки шардирования, описанной в этой статье) знает, где какой шард и в какой из доступных шардов нужно сроутить запрос, без proxy и дополнительных внешних маршрутизирующих приложений.
Первый. Мы не ходим в master на каждый запрос конфигурации кластера, а храним ее в in-memory кэше.
Второй. Не всегда подключение именно создается - в большинстве случаев (но не во всех) его можно взять из кэша, чтобы не кушать коннектов больше, чем нужно.
Всегда пожалуйста! Рад, что понравилось и (или) было полезно!
Данные самые разные - таблицы от сотен мегабайт (такие обычно кладем в solid шард, так как шардировать их не имеет смысла) до нескольких терабайт. Структура таблиц также различнавя - от пары-тройки полей до десятков.
Нет. Правильность разрешения шарда многократно тестировалась и проблем обнаружено не было. По вопросам по поиску, пожалуйста, обращайтесь через форму обратной связи в приложении.
Можно, но мы не храним ключ шардирования (только диапазон бакетов, в которые попадает это значение), и от кольца отказались.
Текущее решение получилось именно таким как раз потому, что мы закладывались на потенциальный автоматический решардинг. Плюс готовим решение по группам микрокластеров на основе описанного в статье, где BucketNumber будет не жестко прибит гвоздями.
Для иллюстрации разницы хватит вот такого тела запроса.
В случае, когда shard key - явное поле, берем, group_id и его прогоняем через хэширование.
Когда shard key - вычисляемое поле, например, складываем
group_id
иsub_group_id
и прогоняем через хэширование сумму.Или, например, делаем запрос куда-то еще, чтобы получить по
sub_group_id
shard key для этого запроса.Описанное решение совершенно никак не ограничивает, откуда берется shard key. Его может вообще в запросе не быть.
У нас нет прохода по кольцу далее, если не найден сервер, отвечающий за диапазон.
Пришел запрос, который нужно боработать, сходив в базу за данными. Например возьмем простой select из базы
Получили из запроса shardKey (в зависимости от запроса может быть либо явным полем, либо вычисляемым). Например считаем, что это явное поле типа айдишника, какой-нибудь группы сущностей
Вычислили hash(shardkey) % 65536, это значение и есть номер бакета, где лежат данные для данного shardKey -
bucket_index
.Теперь нужно понять, какой сервер отвечает за диапазон бакетов, в который попадает вычисленный шагом выше
Сходили в интервальное дерево, построенное поверх выбранной из master шарда конфигураци шардированного кластера. Взяли тот узел этого дерева, который отвечает за диапазон, в который попадает вычисленный на шаге 3 бакет.
Если убрать интервальное дерево из рассмотрения, то на базе это будет (в псевдо-SQL) выглядеть как
SELECT * FROM shards where bucket_index_start <= bucket_index <= bucket_index_end LIMIT 1
Из узла взяли connection string к серверу
Данному решению неважно, какой load balancing алгоритм используется в кластере, куда задеплоен сервис, оно находится уровнем ниже - на data access level. Потому я про load balancing на уровне k8s в этой статье и не писал.
В разных частях наших k8s кластеров используются различные алгоритмы банасировки, не только round robin, но и он тоже присутствует. Подробнее ответить вам не могу, могу только сказать, что ничего самописного в этой области у нас нет - выезжаем на стандартных механизмах k8s.
Это выдержка из статьи.
Еще один момент - если мы пользуемся сторонними решениями, то их изменение под наши требования (временной промежуток от постановки задачи до ее выкатки в prod) займет больше времени, чем для случая, когда это полностью самописное решение.
Да, стандарнтый k8s load balancer.
Да.
"Приложение" в этом контексте - это back-end сервис, а не мобильное приложение. Мобильное приложение вообще в базы напрямую не ходит, только в back-end сервисы.
Нет, мы сейчас в обсуждении находимся на ноде k8s, в каком-нибудь поде с нашим back-end сервисом. Мобильное приложение оперирует back-end сервисами, выставляющими для него свои API.
Так вот, мы в back-end сервисе. К нему прилетает запрос. Мы понимаем, что этот запрос нужно обслужить, сходив в шардированную базу.
Из запроса берется значение ключа шардирования (обычно это одно из полей запроса, но не всегда), оно прогоняется через преобразование (в том числе - хэширование), из него получается номер бакета. Дальше мы идем в in-memory cache сервиса, где лежит интервальное дерево с диапазонами бакетов и конфигурацией обслуживающих эти диапазоны шардов. Из дерева выбираем узел, который нам подходит по диапазону, в этом узле есть конфигурация соответсвующей ноды кластера, в том числе со строкой подключения.
Далее мы идем в еще один in-memory cache и смотрим, есть ли у нас свободный коннект с такими же параметрами подключения. Если да - берем его. Если нет - создаем новое подключение, которое мы, по завершении работы с ним, положим в этот самый in-memory cache.
У приложения по сути есть вся конфигурация кластера - где какой шард лежит, какой диапазон бакетов обслуживает и прочее. Приложение эту конфигурацию получило, единожды сходив в master шард.
Так что да, можно сказать, что приложение само (при помощи библиотеки шардирования, описанной в этой статье) знает, где какой шард и в какой из доступных шардов нужно сроутить запрос, без proxy и дополнительных внешних маршрутизирующих приложений.
Два нюанса.
Первый. Мы не ходим в master на каждый запрос конфигурации кластера, а храним ее в in-memory кэше.
Второй. Не всегда подключение именно создается - в большинстве случаев (но не во всех) его можно взять из кэша, чтобы не кушать коннектов больше, чем нужно.
В остальном вы все абсолютно верно поняли.
Всегда пожалуйста! Рад, что понравилось и (или) было полезно!
Данные самые разные - таблицы от сотен мегабайт (такие обычно кладем в solid шард, так как шардировать их не имеет смысла) до нескольких терабайт.
Структура таблиц также различнавя - от пары-тройки полей до десятков.
Нет. Правильность разрешения шарда многократно тестировалась и проблем обнаружено не было. По вопросам по поиску, пожалуйста, обращайтесь через форму обратной связи в приложении.
Можно, но мы не храним ключ шардирования (только диапазон бакетов, в которые попадает это значение), и от кольца отказались.
Текущее решение получилось именно таким как раз потому, что мы закладывались на потенциальный автоматический решардинг. Плюс готовим решение по группам микрокластеров на основе описанного в статье, где BucketNumber будет не жестко прибит гвоздями.
Спасибо за уточнение и совет, отразим этот момент в статье.
Пока не выкладывали, но если выложим - обязательно расскажем об этом.
Изобрели, называется CitusDB. Но нам это решение, увы, не подошло.