Хабрахабр

Как спланировать ёмкость Apache Ignite кластера

Публикуем расшифровку видеозаписи выступления Алексея Гончарука (Apache Ignite PMC Member и Главный архитектор Grid Gain) на митапе Apache Ignite сообщества в Петербурге 29 марта. Загрузить слайды можно по ссылке.

Забегая вперёд: такое прогнозирование пока что является достаточно сложной, нетривиальной задачей. Участников сообщества Apache Ignite часто спрашивают: «Сколько нужно узлов и памяти для того, чтобы загрузить такой-то объем данных?» Об этом и я хочу сегодня поговорить. Также я расскажу, как упросить себе задачу прогнозирования, и какие можно применять оптимизации.
Итак, очень часто к нам приходят пользователи и говорят: «У нас есть данные, представленные в виде файлов. Для этого нужно немного разбираться в устройстве Apache Ignite. К примеру, файл может быть сжат. Сколько нужно памяти, чтобы транслировать эти данные в Apache Ignite?».
При такой постановке ответить на вопрос практически невозможно, потому что разные форматы файлов транслируются в совершенно разные модели. А если он не сжат в какой-то классический бинарный вид, но может дедуплицирование данных, значит файл сжат в неявном виде.

Так что в общем случае неверно говорить, что единичный индекс добавит какой-то процент от общего объема данных, поскольку индексируемое поле может иметь разный размер. Также не стоит забывать, что Apache Ignite обеспечивает быстрый доступ к данным по различным ключам либо SQL-индексам, поэтому в дополнение к данным, которые лежат в файле, мы строим дополнительные индексы, с которыми связаны дополнительные расходы. То есть неправильно выделять на индекс фиксированный процент памяти.

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

Эмпирический подход

Эмпирический подход может быть немного неточен, но, с точки зрения пользователя, не требует какого-то глубокого погружения в структуру системы. Суть подхода в следующем.
Берёте репрезентативную выборку данных и загружаете в Apache Ignite. Во время загрузки наблюдаете за увеличением размера хранилища. На определённых объёмах можно делать «отсечки», чтобы потом линейно экстраполировать и спрогнозировать необходимый объём хранилища для всего набора данных.

Но оказалось, что генератор собрал данные таким образом, что в набор входили остатки от деления при генерировании. Здесь обычно задают хороший вопрос: «Какой должна быть репрезентативная выборка?»
Однажды мы генерировали выборку случайным образом. Поэтому, когда будете оценивать объём хранилища, убедитесь, что в репрезентативной выборке действительно выполняются распределения, которыми вы оперируете. В какой-то момент выяснилось, что наши случайные строки, которые должны были иметь равномерное распределение, на самом деле этого распределения не имели.

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

Если вы работаете с persistence, можно смотреть на объем файлов, которые у вас получаются. За чем именно нужно следить в ходе загрузки репрезентативной выборки? Или можете просто включить метрики данных региона, метрики в конфигурации Apache Ignite и через MX Bean мониторить увеличение памяти, делая «отсечки» и строя график.

Численная оценка

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

Данные проходят через 4 стадии преобразований: Проанализируем операцию cash put при записи в Apache Ignite.

Некоторые пользователи создают бинарный объект напрямую, поэтому первая стадия конвертации объекта пропускается. Первая стадия опциональная, потому что некоторые пользователи работают с классом и передают в Apache Ignite Java-объект. Но если вы работаете с Java-объектами, то это первая трансформация, которую претерпевает объект.

Суть его в том, что вы можете оперировать бинарными объектами в кластере, не имея описания классов. После конвертации Java-объекта в бинарный объект у нас получается класс-независимый формат. То есть ваша модель растет, изменяется, и вы получаете возможность работать со своими данными не меняя классы, не разворачивая их в кластере. Это позволяет вам изменять структуру класса и выполнять так называемый роллинг изменения структуры объектов.

Единицей работы с диском традиционно является страница. Третья стадия изменений, вносящая дополнительный overhead — запись на диск. 0 мы перешли на страничную архитектуру. И начиная с Apache Ignite 2. Это означает, что у каждой страницы есть опциональные заголовки, какие-то метаданные, которые тоже занимают место при записи объектов в страницу.

Даже если вы не используете SQL, в Apache Ignite у вас есть быстрый доступ по ключу. И последний кусочек, который тоже необходимо учитывать — обновление индекса. Поэтому всегда строится индекс первичного ключа, и на него тоже тратится место. Это основной API кэша Apache Ignite.

Это наш бинарный объект:

Заголовок бинарного объекта занимает 24 байта. Не будем сильно углубляться в структуру, в общем виде её можно представить как некий заголовок, потом поля, наши существенные данные и футер. Возможно, это много, но столько необходимо для поддержки изменяемости объектов без классов.

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

То есть вместо того, чтобы писать порядок полей в самом объекте, мы их сохраняем отдельно. Размер футера зависит от флага compactFooter, который позволяет записывать структуру объекта в дополнительные метаданные. Но при этом Apache Ignite совершает дополнительные действия для сохранения и поддержки этих метаданных. И если compactFooter равен true, то футер будет очень маленьким. Если же compactFooter равен false, то объект является самодостаточным и его структуру можно считать без дополнительных метаданных.

Поэтому если вам это очень интересно, можете сделать хак и привести объект к имплементации, тогда увидите его размер. На данный момент в нашем публичном API нет метода, возвращающего размер бинарного объекта. 5 мы добавим метод, который позволит получить размер объекта. Я думаю, в Apache Ignite 2.

Страничная архитектура

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

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

Единичный раздел, который разбит на множество страниц, можно представить в виде вот такой схемы:

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

  • страницы данных, которые хранят данные;
  • индексные страницы, которые позволяют построить дерево индекса;
  • вспомогательные страницы для таких структур, как freelist или метаданные.

Зачем это нужно, мы поговорим чуть ниже.

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

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

Вполне возможно, что запись № 3 в будущем понадобится передвинуть вправо, чтобы вместить сюда более крупную запись. Если здесь удалить запись № 2, то образуются две свободные зоны. При этом внешняя ссылка, которая ссылается на эту страницу, остается константной. И если у нас есть непрямая адресация, то вы просто меняете смещение соответствующей записи в таблице.

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

Если у вас используется expire policy, туда же пишется expiry time. Помимо ключа и значения в страницу данных записывается и вспомогательная информация для корректной работы системы, например, номер версии. После того, как вы узнали размер бинарного объекта и ключа, вы добавляете 35 байт и получаете объём конкретной записи в странице данных. В общем случае дополнительные метаданные занимают 35 байт. И затем высчитываете, сколько записей умещается в странице.

Тогда в метриках вы увидите fill-фактор, который не равен 1. Может оказаться, что в странице данных останется свободное место, в которое не поместится ни одна из записей.

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

Если в данной реализации и в данный момент страница содержит меньше 8 байтов, она во freelist не попадает, потому что не будет такой пары ключ-значение, которая поместилась бы в 8 байтов. Информация о том, в каких страницах есть свободное место, которое имеет смысл учитывать, хранится в структуре данных «свободный список» (freelist).

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

Это означает, что на нижнем — самом широком — уровне находятся ссылки на абсолютно все пары ключ-значение. Любой индекс Apache Ignite представляет собой B-дерево. На каждой из внутренних страниц есть ссылки на нижерасположенный уровень. Индекс начинается с корневой страницы.

В зависимости от того, на какую страницу вы смотрите, — на внутреннюю или на лист, — у вас будет различное количество максимальных элементов. У индекса, который является первичным ключом, размер элемента, записываемого в индексную страницу, составляет 12 байтов. Для первичного индекса размер ссылки всегда фиксирован и равен 12 байтам. Если возьметесь за такую численную оценку, то посмотреть количество максимальных элементов можно в коде. Чтобы грубо подсчитать максимальное количество элементов страницы, вы можете поделить размер страницы (по умолчанию 4 мегабайта) на 12 байтов.

Учитывая, что нижний уровень дерева содержит все элементы, вы также можете оценить количество страниц, необходимое для хранения индекса.
Что касается SQL-индексов — или вторичных, — то здесь размер элемента, сохраняемого в странице, зависит от сконфигурированного inline size. С учетом роста дерева можно считать, что каждая страница будет заполнена от 50 % до 75 %, в зависимости от порядка загрузки данных. Нужно очень внимательно проанализировать модель данных, чтобы вычислить количество индексных страниц.

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

<иллюстрация>

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

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

Оптимизации

Как можно облегчить жизнь пользователя в будущем? Apache Ignite движется в сторону SQL-систем, и есть много идей по уменьшению накладных расходов.

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

В этом случае Apache Ignite будет самостоятельно дедуплицировать данные уже на уровне страницы. Второе решение — группировать в страницах объекты одного типа и выделять заголовок или его часть.

Если вы понимаете, что страницы будут фрагментироваться таким образом, что в них будет оставаться 100 байт, а ваши данные меньше 100 байт никогда не бывают, то имеет смысл подкрутить freelist, чтобы страницы туда не попадали и не расходовали место в этом самом freelist. Ещё одна разумная идея: реализовать настраиваемый порог учета данных во freelist.

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

Большой вопрос в том, что будет являться входом для такой утилиты. И последняя, очень востребованная оптимизация — калькулятор ёмкости кластера. Видится такая схема: пользователь загружает в калькулятор структуру объектов, указывает, сколько строчек планирует загрузить, а калькулятор говорит, какой объем памяти необходим с учётом всех индексов и внутренних накладных расходов Apache Ignite.

Определение пропорции диск/память

Если у вас заканчивается память, то Apache Ignite поступает приблизительно также, как ОС: выкидывает некоторые данные из памяти и подгружает необходимые. Сколько нам нужно выделить памяти, при условии, что объем сохраняемых данных больше, чем объем имеющейся памяти? Некоторые данные выкинуть нельзя, но для большинства случаев это не важно.

Выкидывание данных из памяти стоит дёшево. Здесь нужно помнить, что сам вид страницы не подразумевает запись. В большинстве случаев самая важная характеристика, на которую стоит обращать внимание, это сколько IOPS может выдавать ваш диск. А последующее чтение данных, для которых произошел промах с диска — операция дорогая. Если вы работаете с облачным развёртыванием, то узнать количество IOPS очень легко.

Но, по нашему опыту, куда полезней знать IOPS. Например, запуская и монтируя образ в Amazon, вы среди дисков можете выбрать те, у которых указана скорость записи в Мб/сек. Так как мы оперируем страницами, то, по факту, IOPS — это максимальное количество операций чтения или записи, которые мы можем выполнить на диске за единицу времени.

Сейчас это делается по алгоритму random RLU. Как выбирается страница, которая будет выкинута из памяти? Когда нам нужно выкинуть какую-то из страниц, мы берем из этой таблицы n случайных страниц и выбираем самую старую. Apache Ignite содержит в памяти страницу, которая хранит отображение того самого идентификатора страницы на конкретный физический адрес в памяти, где лежат данные. Но чаще всего мы будем попадать в одну n-ную часть, где n — количество образцов, которые мы выбираем. Она не всегда будет самая старая в абсолютном смысле.

А когда мы применим random RLU 2 для вытеснения страниц, проблема устойчивости к полному сканированию будет решена. На сегодняшний день алгоритм random RLU не устойчив к полному сканированию, но у нас уже есть реализация алгоритма random RLU 2, который используется в Apache Ignite для другой задачи.

Худшая ситуация: у вас был какой-то малоиспользуемый SQL-индекс или регион в кэше, и получилось так, что абсолютно все страницы этого региона были вытеснены на диск, то есть выкинуты из памяти. Вытеснение страниц существенно влияет на задержку при единичной операции в кэше. И нам нужно минимизировать объём возможного чтения с диска. Если вы обратитесь к какому-то ключу, который будет обращаться ко всем n страницам, они будут последовательно читаться с диска.

3 появилась возможность разделять кэши по разным data-регионам. Начиная с Apache Ignite 2. Если вы знаете, что у вас есть подмножество горячих данных, и вы наверняка будете с ними работать, а также есть подмножество данных, которое является историческим, то имеет смысл эти подмножества разделить по разным data-регионам.

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

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

Поскольку физически диск один, количество IOPS делится между узлами Apache Ignite. Последняя вещь, которую стоит упомянуть: не запускайте много узлов Apache Ignite с включенным persistence на одном и том же физическом носителе. Мало того, что вы делите пропускную способность между узлами, вдобавок к этому каждый из узлов может исчерпать ёмкость по IOPS, и поведение всего кластера станет непредсказуемым.

Это в дополнение к рекомендации выносить write ahead lock на отдельный физический носитель. Если по каким-то причинам хочется запустить несколько узлов Apache Ignite на одной машине, то обязательно следите за тем, чтобы физические хранилища для узлов были разные.

Планирование CPU и пропускной способности сети

Не стоит использовать сеть с пропускной способностью меньше 1 гигабита. Сегодня мало у кого есть сети с меньшей пропускной способностью. Выбор CPU очень сильно зависит от профиля нагрузки, от количества индексов. Здесь стоит вернуться к эмпирическому подходу и просто сгенерировать профиль нагрузки, ожидаемой для вашего приложения, и аккуратно мониторить все показатели системы. Если вы видите, что какой-то из ресурсов полностью исчерпан, то имеет смысл его добавить.

Мы приветствуем любые вопросы или идеи по улучшению Apache Ignite.

Присоединяйтесь к нашим встречам в Москве и Санкт-Петербурге.

Другие интересные видео на нашем канале:

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть