Русский
Русский
English
Статистика
Реклама

Шардинг

Как перестать беспокоиться и начать жить без монолита

28.08.2020 14:05:29 | Автор: admin


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

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

Это сейчас у нас господствует идеология разделения всего и вся, начиная от оборудования и заканчивая бизнес-логикой. Вследствие этого у нас, к примеру, есть два ДЦ, практически независимых на сетевом уровне. А тогда всё было совсем по-другому.

На сегодняшний день для внесения изменений есть куча инструментов и средств в виде CI/CD, K8S и т.п. В монолитное же время нам не требовалось так много всяких иностранных слов. Достаточно было просто поправить хранимочку в базе.

Но время шло вперед, и количество запросов шло вперед вместе с ним, выстреливая RPS порой выше наших возможностей. С выходом на рынок стран СНГ нагрузка на процессоре БД первого монолита не опускалась ниже 90 %, а RPS держались на уровне 2400. И это были не просто маленькие селектики, а здоровенные запросы с кучей проверок и JOINов, которые могли пробежаться чуть ли не по половине данных на фоне большого IO.

Когда же на сцене начали появляться полноценные распродажи на Черную пятницу, а Wildberries начал проводить их одним из первых в России, то ситуация стала совсем печальной. Ведь нагрузка в такие дни возрастает в три раза.
Ох уж, эти монолитные времена! Уверен, что и вы сталкивались с подобным, и до сих пор не можете понять, как такое могло произойти с вами.

Что тут поделаешь мода присуща и технологиям. Еще лет 5 тому назад нам пришлось переосмыслить одну из таких мод в виде имеющегося сайта на .NET и MS SQL-сервера, который бережно хранил в себе всю логику работы самого сайта. Хранил настолько бережно, что распиливать такой монолит оказалось долгим и совсем непростым удовольствием.
Небольшое отступление.

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

И грянул гром

Вернемся к нашему костру. Чтобы распределить нагрузку монолитной функциональности, мы решили разделить систему на микросервисы, основанные на opensource-технологиях. Потому что, как минимум, их масштабирование дешевле. А понимание того, что масштабировать придется (и немало) у нас было на все 100 %. Ведь уже на тот момент получилось выйти на рынки соседних стран, и количество регистраций, равно как и количество заказов, начало расти еще сильнее.

Проанализировав первых претендентов на вылет из монолита в микросервисы, мы поняли, что в 80 % запись в них на 99 % идет из back office-систем, а чтение с передовой. В первую очередь это касалось пары важных для нас подсистем пользовательских данных и системы вычисления конечной стоимости товаров на основании информации о дополнительных клиентских скидок и купонов.

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

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

Вследствие этого у нас родилась схема, хорошо сочетающаяся с Tarantool.

В то время для работы микросервисов были выбраны схемы работы с несколькими ЦОДами на виртуальных и аппаратных машинах. Как показано на рисунках, были применены варианты репликаций Tarantool как в режиме master-master, так и master-slave.


Архитектура. Вариант 1. Сервис пользователей

На текущий момент времени это 24 шарда, в каждом из которых по 2 инстанса (по одному на каждый ДЦ), все в режиме мастер-мастер.

Поверх БД находятся приложения, которые обращаются к репликам БД. Приложения работают с Tarantool через нашу кастомную библиотеку, которая реализует интерфейс Go-драйвера Tarantool. Она видит все реплики и может работать с мастером на чтение и запись. По сути она реализует модель replica set, в которую добавлена логика выбора реплик, выполнения повторных попыток, circuit breaker и rate limit.

При этом имеется возможность конфигурировать политику выбора реплики в разрезе шарды. К примеру, раундробином.


Архитектура. Вариант 2. Сервис расчета конечной стоимости товара

Несколько месяцев назад большая часть запросов по расчету конечной стоимости товаров ушла на новый сервис, который в принципе работает без баз данных, но какое-то время назад все 100% обрабатывал сервис с Tarantool под капотом.

Бд сервиса это 4 мастера, в которые синхронизатор собирает данные, и каждый из этих мастеров по репликации раздает данные на readonly-реплики. У каждого мастера примерно по 15 таких реплик.

Что в первой, что во второй схеме, при недоступности одного ДЦ, приложение может получать данные во втором.

Стоит отметить, что в Tarantool репликация довольно гибкая и конфигурируется в runtime. В иных системах бывало возникали сложности. К примеру с изменением параметров max_wal_senders и max_replication_slots в PostgreSQL требуется перезапуск мастера, что в ряде случаев может повлечь за собой разрыв соединений между приложением и СУБД.

Ищите и обрящете!

Почему мы не сделали как нормальные люди, а выбрали нетипичный способ? Смотря что считать нормальным. Многие вообще делают кластер из Mongo и размазывают его по трём гео-распределенным ДЦ.

В то время у нас уже были два проекта на Redis. Первый кэш, а второй представлял собой persistent-хранилище для не слишком критичных данных. Вот с ним приходилось довольно сложно, отчасти по нашей вине. Иногда довольно большие объемы лежали в ключе, и время от времени сайту становилось плохо. Эту систему мы использовали в master-slave варианте. И было много случаев, когда что-то происходило с мастером и репликация ломалась.

То есть Redis хорош для stateless-задач, а не для stateful. В принципе, он позволял решить большинство задач, но только если это были key-value-решения с парой индексов. Но у Redis на тот момент было довольно печально с персистентностью и репликацией. К тому же, были нарекания на производительность.

Думали про MySQL и PostgreSQL. Но первый как-то не прижился у нас, а второй сам по себе довольно навороченный продукт, и строить на нём простые сервисы было бы нецелесообразно.
Пробовали RIAK, Cassandra, даже графовую БД. Всё это достаточно нишевые решения, которые не подходили на роль общего универсального инструмента для создания сервисов.

В конечном счете остановились на Tarantool.

Мы обратились к нему, когда он был в версии 1.6. Нас заинтересовал в нем симбиоз key-value и функциональности реляционной БД. Есть вторичные индексы, транзакции и спейсы, это как таблички, но непростые, можно хранить в них разное количество колонок. Но киллер-фичей Tarantool были вторичные индексы в сочетании с key-value и транзакционностью.

Также свою роль сыграло отзывчивое русскоязычное сообщество, готовое в чате прийти на помощь. Мы этим активно пользовались и прямо жили в чате. И не стоит забывать о приличном persistent без явных ляпов и косяков. Если посмотреть нашу историю с Tarantool, у нас было много болей и факапов с репликацией, но мы ни разу не потеряли данные по его вине!

Внедрение началось трудно

На тот момент у нас основным стеком разработки был .NET, к которому не было коннектора для Tarantool. Мы сразу начали что-то делать на Go. С Lua тоже получалось неплохо. Главная проблема на тот момент была с отладкой: в .NET с этим всё шикарно, а после этого окунуться в мир embedded Lua, когда у тебя, кроме логов, никакого дебага нет, было сложновато. К тому же репликация почему-то периодически разваливалась, пришлось вникать в устройство движка Tarantool. В этом помог чат, в меньшей степени документация, иногда смотрели код. На тот момент документация была так себе.

Так в течение нескольких месяцев получилось набить шишек и получать достойные результаты по работе с Tarantool. Оформили в git эталонные наработки, которые помогали со становлением новых микросервисов. Например, когда возникала задача: сделать очередной микросервис, то разработчик смотрел на исходники эталонного решения в репозитории, и на создание нового уходило не больше недели.

Это были особенные времена. Условно, тогда можно было подойти к админу за соседним столом и попросить: Дай мне виртуалку. Минут через тридцать машина была уже у тебя. Ты сам подключался, всё устанавливал, и тебе заводили на нее трафик.

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

Разделяй и властвуй. Как обстоит дело с Lua?

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

То есть разработчики подготавливают какое-то изменение. Tarantool начинает делать миграцию, а реплика еще со старым кодом; туда прилетает по репликации какой-то DDL, ещё что-нибудь, и код просто разваливается, потому что это не учтено. В результате процедура обновления у админов была расписана на лист А4: остановить репликацию, обновить это, включить репликацию, выключить тут, обновить там. Кошмар!

В итоге, сейчас мы чаще всего пытаемся на Lua ничего не делать. Просто с помощью iproto (бинарный протокол для взаимодействия с сервером), и всё. Возможно, это недостаток знаний у разработчиков, но с этой точки зрения система сложная.

Мы не всегда слепо следуем этому сценарию. Сегодня у нас нет черного и белого: либо всё на Lua, либо всё на Go. Мы уже понимаем, как можно скомбинировать, чтобы потом не получить проблемы с миграцией.

Где сейчас есть Tarantool?

Tarantool используется в сервисе расчета конечной стоимости товаров с учетом купонов на скидку, он же Промотайзер. Как уже говорил ранее, сейчас он отходит от дел: его заменяет новый каталожный сервис с предрасчитанными ценами, но еще полгода назад все расчеты делались в Промотайзере. Раньше половина его логики была написана на Lua. Два года назад из сервиса сделали хранилище, а логику переписали на Go, потому что механика работы скидок немного поменялась и сервису не хватало производительности.

Один из самых критичных сервисов это профиль пользователя. То есть все пользователи Wildberries хранятся в Tarantool, а их около 50 млн. Шардированная по ID пользователя система, разнесенная по нескольким ДЦ с обвязкой на Go-сервисах.
По RPS когда-то лидером был Промотайзер, доходило до 6 тысяч запросов. В какой-то момент у нас было 50-60 экземпляров. Сейчас же лидер по RPS профили пользователей, примерно под 12 тыс. В этом сервисе применяется кастомное шардирование с разбиением по диапазонам пользовательских ID. Сервис обслуживает более 20 машин, но это слишком много, планируем уменьшить выделенные ресурсы, потому что ему достаточно мощностей 4-5 машин.

Сервис сессий это наш первый сервис на vshard и Cartridge. Настройка vshard и обновление Cartridge потребовали от нас определенных трудозатрат, но в итоге все получилось.

Сервис для отображения разных баннеров на сайте и в мобильном приложении был одним из первых, выпущенных сразу на Tarantool. Этот сервис примечателен тем, что ему лет 6-7, он до сих пор в строю и ни разу не перезагружался. Применялась репликация master-master. Никогда ничего не ломалось.

Есть пример использования Tarantool для функциональности быстрых справочников в складской системе, чтобы быстро перепроверять информацию в некоторых случаях. Пробовали использовать под это дело Redis, но данные в памяти занимали больше места, чем у Tarantool.

Сервисы листа ожидания, клиентских подписок, модных ныне сторис и отложенных товаров также работают с Tarantool. Последний сервис в памяти занимает около 120 ГБ. Это самый объемный сервис из вышеперечисленных.

Заключение

Благодаря вторичным индексам в сочетании с key-value и транзакционностью Tarantool отлично подходит для архитектур на основе микросервисов. Однако мы столкнулись с трудностями, когда выкатывали изменения в сервисах с большим количеством логики на Lua сервисы часто переставали работать. Победить это нам не удалось, и со временем мы пришли к разным комбинациям Lua и Go: знаем, где стоит использовать один язык, а где другой.

Что еще почитать по теме

Подробнее..

Шардинг, от которого невозможно отказаться

22.04.2021 10:12:30 | Автор: admin
image

А не пора ли нам шардить коллекции?
Не-е-е:


  • у нас нет времени, мы пилим фичи!
  • CPU занят всего на 80% на 64 ядерной виртуалке!
  • данных всего 2Tb!
  • наш ежедневный бекап идет как раз 24 часа!

В принципе, для большинства проектов вcё оправдано. Это может быть еще прототип или круг пользователей ограничен Да и не факт, что проект вообще выстрелит.
Откладывать можно сколько угодно, но если проект не просто жив, а еще и растет, то до шардинга он доберется. Одна беда, обычно, бизнес логика не готова к таким "внезапным" вызовам.
А вы закладывали возможность шардинга при проектировании коллекций?


Эта статья для продвинутых разработчиков.
Для тех, кто планирует шардинг своего кластера.
Для тех, кто уже шардировал кластер ранее, но админы все еще плачут.
Для тех, кто руками перемещал jumbo-чанки.


Сначала, мы будем учится жить со слонами.
Потом, мы их победим, но не сможем вернуться назад.


Всем привет, от команды разработки Smartcat и наших счастливых админов!


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


Зачем нам шардинг


Шардинг штатная возможность горизонтального масштабирования в MongoDB. Но, чтобы стоимость нашего шардинга была линейной, нам надо чтобы балансировщик MongoDB мог:


  • выровнять размер занимаемых данных на шард. Это сумма размера индекса и сжатых данных на диске.
  • выровнять нагрузку по CPU. Его расходуют: поиск по индексу, чтение, запись и агрегации.
  • выровнять размер по update-трафику. Его объем определяет скорость ротации oplog. Это время за которое мы можем: поднять упавший сервер, подключить новую реплику, снять дамп данных.

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


Особенности шардинга в MongoDB


Как мы знаем балансировщик MongoDB при отсутствии ограничений (превышение размера данных на шард, нарушение границ зон) выравнивает только количество чанков на шард. Это простой, надежный и сходящийся алгоритм.
Есть документация про лимиты количества чанков и про порядок выбора размещения чанка.
Этот алгоритм и определяет главные ограничения на балансируемые данные:


  1. Ключ шардирования должен быть высокоселективный. В противном случае мы не получим достаточного числа интервалов данных для балансировки.
  2. Данные должны поступать с равномерным распределением на весь интервал значений ключа. Тривиальный пример неудачного ключа это возрастающий int или ObjectId. Все операции по вставке данных будут маршрутизироваться на последний шард (maxKey в качестве верхней границы).

Самое значимое ограничение гранулярность ключа шардирования.
Или, если сформулировать отталкиваясь от данных, на одно значение ключа должно приходиться мало данных. Где "мало" это предельный размер чанка (от 1Mb до 1Gb) и количество документов не превышает вот эту вот величину.


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


Бизнес логика требует слонов


Теперь давайте посмотрим, с чем мы будем сталкиваться при проектировании бизнес логики.
Я рассмотрю только самый чувствительный для шардирования случай, который заставляет наши данные группироваться в неравномерные объемы.
Рассмотрим следующий сценарий:


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

Пример модели:


{    _id: ObjectId("507c7f79bcf86cd7994f6c0e"),    projectId: UUID("3b241101-e2bb-4255-8caf-4136c566a962"),    name: "job name 1",    creation: ISODate("2016-05-18T16:00:00Z"),    payload: "any additional info"}

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


Теперь, надо выбрать второе поле ключа или оставить только первое.
Например, у нас 20% запросов используют только поле name, еще 20% только поле creation, а остальные опираются на другие поля.
Если в ключ шардирования включить второе поле, то крупные проекты, те у которых объем работ не помещается в одном чанке, будут разделены на несколько чанков. В процессе разделения, высока вероятность, что новый чанк будет отправлен на другой шард и для сбора результатов запроса нам придётся обращаться к нескольким серверам. Если мы выберем name, то до 80% запросов будут выполняться на нескольких шардах, тоже самое с полем creation. Если до шардирования запрос выполнялся на одном сервере, а после шардирования на нескольких, то нам придется дополнительную читающую нагрузку компенсировать дополнительными репликами, что увеличивает стоимость масштабирования.


С другой стороны, если оставить в ключе только первое поле, то у нас "идеальное" линейное разделение нагрузки. Т.е. каждый запрос с полем projectId будет сразу отправлен на единственный шард. Поэтому имеет смысл остановиться на этом выборе.
Какие риски есть у этого выбора:


  • у нас могут появляться не перемещаемые jumbo-чанки. При большом объеме работ в одном проекте мы обязательно превысим предельный размер чанка. Эти чанки не будут разделены балансировщиком.
  • у нас будут появляться пустые чанки. При увеличении проекта, балансировщик будет уменьшать диапазон данных чанка для этого проекта. В пределе, там останется только один идентификатор. Когда большой проект будет удален, то на этот узкий чанк данные больше не попадут.

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


В общем, мы можем посмотреть текущее распределение данных или просто прикинуть развитие проекта и допустить, что хоть jumbo-чанки и будут появляться, но их должно быть не много.


Проблемы с масштабированием


Итак, мы, вопреки ожиданиям, набрали достаточно неперемещаемых чанков, чтобы это стало заметно. То есть буквально, случай из практики. Вы заказываете админам новый шард за X$ в месяц. По логам видим равномерное распределение чанков, но занимаемое место на диске не превышает половины. С одной стороны весьма странное расходование средств было бы, а с другой стороны возникает вопрос: мы что же, не можем прогнозировать совершенно рутинную операцию по добавлению шарда? Нам совсем не нужно участие разработчика или DBA в этот момент.


Что интересно, на этапе проектирования схемы данных, у нас и нет особого выбора. Мы не можем предсказать характер использования на годы вперед. А владельцы продукта очень не охотно формулируют строгие ограничения при проектировании. Даже оглядываясь назад, не всегда понятно, как можно было бы перепроектироваться, чтобы успеть все сделать тогда. Так что продолжаем работать с тем шардированием, которое заложили.


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


Надеюсь, тут уже все уже запуганы и потеряли надежду. ;)


Решение


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


1-й воспользоваться командой moveChunk прямое указание балансировщику о перемещении конкретного чанка.


2-й воспользоваться командой addTagRange привязка диапазона значений ключа шардирования к некоторому шарду и их группе.


В обоих случаях потребуется также точное знание размера чанка. Это можно сделать командой dataSize, которая возвращает некоторую статистику по объектам в коллекции на заданном интервале ключей.


Предварительное прототипирование 1-го варианта выявило дополнительные особенности.


Команды moveChunk не отменяют работы штатного балансировщика. Он вполне может принять решение об обратном переносе. Тут надо блокировать работу штатного балансировщика. Или кроме перемещения больших чанков на недогруженный шард, надо искать на нем маленькие чанки и синхронно перемещать их на перегруженный шард.


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


Массовые сканирования dataSize ухудшают отзывчивость сервера на боевых запросах.


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


По совокупности проблем с прямой балансировкой, я решил развивать балансировку через привязку диапазона значений ключа шардирования.


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


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


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


Группировка данных


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


Итак, коллекция уже шардирована.


  • Читаем все ее чанки из коллекции config.chunks с сортировкой по возрастанию ключа {min: 1}
  • Распределяем чанки по шардам, так чтобы их было примерно одинаковое количество. Но при этом все чанки на одном шарде должны объединяться в один интервал.

Например:


У нас есть три шарда sh0, sh1, sh2 с одноименными тегами.
Мы вычитали поток из 100 чанков по возрастанию в массив


var chunks = db.chunks.find({ ns: "demo.coll"}).sort({ min: 1}).toArray();

Первые 34 чанка будем размещать на sh0
Следующие 33 чанка разместим на sh1
Последние 33 чанка разместим на sh2
У каждого чанка есть поля min и max. По этим полям мы выставим границы.


sh.addTagRange( "demo.coll", {shField: chunks[0].min}, {shField: chunks[33].max}, "sh0");sh.addTagRange( "demo.coll", {shField: chunks[34].min}, {shField: chunks[66].max}, "sh1");sh.addTagRange( "demo.coll", {shField: chunks[67].min}, {shField: chunks[99].max}, "sh2");

Обратите внимание, что поле max совпадает с полем min следующего чанка. А граничные значения, т.е. chunks[0].min и chunks[99].max, всегда будут равны MinKey и MaxKey соответственно.
Т.е. мы покрываем этими зонами все значения ключа шардирования.


Балансировщик начнёт перемещать чанки в указанные диапазоны.
А мы просто ждем окончания работы балансировщика. Т.е. когда все чанки займут свое место назначения. Ну за исключением jumbo-чанков конечно.


Коррекция размера


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


sh.addTagRange( "demo.coll", {shField: MinKey}, {shField: 1025}, "sh0");sh.addTagRange( "demo.coll", {shField: 1025}, {shField: 5089}, "sh1");sh.addTagRange( "demo.coll", {shField: 5089}, {shField: MaxKey}, "sh2");

Вот так будет выглядеть размещение чанков:


Командой db.demo.coll.stats() можно получить объем данных, которые хранятся на каждом шарде. По всем шардам можно вычислить среднее значение, к которому мы хотели бы привести каждый шард.


Если шард надо увеличить, то его границы надо перемещать наружу, если уменьшить, то внутрь.
Так как уже явно заданы диапазоны ключей, то балансировщик не будет их перемещать с шарда на шард. Следовательно, мы можем пользоваться командой dataSize, мы точно знаем данные какого шарда мы сканируем.
Например, нам надо увеличить sh0 за счет h1. Границу с ключем будем двигать в бОльшую сторону. Сколько именно данных мы сместим перемещением конкретного 34-го чанка, мы можем узнать командой dataSize с границами этого чанка.


db.runCommand({ dataSize: "demo.coll", keyPattern: { shField: 1 }, min: { shField: 1025 }, max: { shField: 1508 } })

Последовательно сканируя чанки по одному мы мы можем смещать границу до нужного нам размера sh0.


Вот так будет выглядеть смещение границы на один чанк



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


sh.removeTagRange( "demo.coll", {shField: MinKey}, {shField: 1025}, "sh0");sh.removeTagRange( "demo.coll", {shField: 1025}, {shField: 5089}, "sh1");sh.addTagRange( "demo.coll", {shField: MinKey}, {shField: 1508}, "sh0");sh.addTagRange( "demo.coll", {shField: 1508}, {shField: 5089}, "sh1");

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


Выгодные особенности этого подхода:


  • до смещения границ, данные не перемещаются. Мы можем их сканировать без опасения, что эта информация устареет. Ну конечно, исключая работу приложения для которого и заведена эта БД.
  • команда dataSize применяется только к перемещаемым данным. Мы не сканируем лишнего.
  • путь к целевому состоянию непрерывный. Т.е. мы можем проводить частичные коррекции, например если видим, что сканирование идет долго и реальное распределение данных успевает сильно измениться.

Дополнительные возможности


Вообще, на практике требуется выравнивание используемого объема диска на шардах, а не только части шардированных коллекции. Частенько, нет времени или возможности проектировать шардирование вообще всех БД и коллекций. Эти данных лежат на своих primary-shard. Если их объем мал, то его легко учесть при коррекции размера и просто часть данных оттащить на другие шарды.


С течением времени, особенно после частых удалений, будут образовываться пустые чанки. Не то чтобы они теперь сильно мешают, но их может быть сильно больше чанков с данными. Чтобы они не заставляли страдать роутер, было бы хорошо их прибрать. Раньше (до дефрагментации) их надо было найти, переместить к другому чанку-соседу на один шард, потом запустить команду mergeChunks. Перемещение нужно, т.к. команда объединяет только соседние чанки на одном шарде.
Есть даже скрипты для автоматизации, но они долго работают, и есть один тикет с печально низким приоритетом.


Теперь все значительно проще. Соседние чанки и так на одном шарде. Можно объединять хоть весь интервал целиком. Если он конечно без jumbo-чанков, их надо исключить. Есть на интервале пустые чанки или нет, это уже не важно. Балансировщик заново сделает разбиение по мере добавления или изменения данных.


Почти итог


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


Наши плюсы:


  • точная балансировка занимаемого размера
  • лучшее распределение update-трафика
  • минимальное сканирование при коррекциях
  • бесплатная уборка пустых чанков
  • минимальные допуски на запасное/неиспользуемое дисковое пространство на каждом шарде
  • мы всегда можем удалить привязки диапазонов ключей и вернуться к исходному состоянию

Минусы:


  • требуется настройка и сопровождение привязок диапазонов ключей.
  • усложняется процесс добавления нового шарда.

Но это было бы слишком скучно Время победить слонов и не вернуться!


Победа над слонами!


Как мы уже отмечали, единственная причина неперемещаемых чанков недостаточная селективность ключа шардирования. Мы сами снижали селективность ключа для привязки запроса к шарду. Но мы уже сгруппировали данные, а теперь мы можем этим коварно воспользоваться!


Можно увеличить селективность ключа шардирования, если добавить к нему второе поле с достаточной селективностью. Поле может быть любым. Подойдет даже _id.


Вспомним пример модели:


{    _id: ObjectId("507c7f79bcf86cd7994f6c0e"),    projectId: UUID("3b241101-e2bb-4255-8caf-4136c566a962"),    name: "job name 1",    creation: ISODate("2016-05-18T16:00:00Z"),    payload: "any additional info"}

Ранее мы выбрали ключ шардирования {projectId: 1}
Но теперь при проектировании можно выбрать любые уточняющие поля для ключа шардирования:


  • {projectId: 1, name: 1}
  • {projectId: 1, creation: 1}
  • {projectId: 1, _id: 1}

А если вы пользуетесь MongoDB версии 4.4 и выше, то там есть удобная функция уточнение ключа шардирования на существующих данных.


Данные дефрагментированы, а это дает нам гарантии того, что основной объем документов работ с одинаковым projectId будет находиться на одном шарде. Как это выглядит на практике?


Вот иллюстрация примеров размещения работ по чанкам. В случае, если мы выбрали ключ шардирования {projectId: 1, _id: 1}



Здесь, для упрощения примера, идентификаторы представлены целыми числами.
А термином "проект" я буду называть группу работ с одинаковым projectId.


Некоторые проекты будут полностью умещаться в один чанк. Например, проекты 1 и 2 размещены в 1м чанке, а 7-й проект во 2-м.
Некоторые проекты будут размещены в нескольких чанках, но это будут чанки с соседними границами. Например, проект 10 размещен в 3, 4 и 5 чанках, а проект 18 в 6 и 7 чанках.
Если мы будем искать работу по ее полю projectId, но без _id, то как будет выглядеть роутинг запросов?


Планировщик запросов MongoDB отлично справляется с исключением из плана запроса тех шардов, на которых точно нет нужных данных.
Например, поиск по условию {projectId: 10, name: "job1"} будет только на шарде sh0


А если проект разбит границей шарда? Вот как 18-й проект например. Его 6-й чанк находится на шарде sh0, а 7-й чанк находится на шарде sh1.
В этом случае поиск по условию {projectId: 18, name: "job1"} будет только на 2х шардах sh0 и sh1. Если известно, что размер проектов у нас меньше размера шарда, то поиск будет ограничен только этими 2-мя шардами.


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


Теперь переформулируем для более общего случая.
У нас есть составной ключ шардирования. По первому полю нам требуется группировка, но оно слабоселективное. Второе поле, нужно просто чтобы увеличить общую селективность ключа.
С помощью дефрагментации, мы добиваемся следующих свойств:


  • группа располагается на одном, максимум двух шардах.
  • число групп которые имели несчастье разместиться на 2х шардах ограничено числом границ. Для N шардов будет N-1 граница.

Больше одно шардовых запросов, меньше лишней нагрузки на чтение, меньше дополнительных реплик!
Мало нам было бы радости, но с таких сильно селективным ключом мы и от jumbo-чанков избавляемся.


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


Точно итог


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


Теперь уже можно оценить достижения и потери.
Достижения:


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

Потери:


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

Осталось спроектировать весь процесс дефрагментации, расчета поправок и коррекции границ Ждите!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru