Что нового в Greenplum 7. Часть 2
В прошлой части обзора изменений и нововведений Greenplum 7 мы рассмотрели миграцию движка Append Optimized таблиц на использование интерфейса табличных методов доступа, оптимизацию добавления столбцов в таблицы, а также изменения, связанные с поддержкой индексов.
Сегодня мы поговорим об ещё одном новом для Greenplum типе индексов и не только.
BRIN-индексы
Все нововведения с точки зрения индексов, упомянутые ранее, не выглядят как must have для аналитической нагрузки. Сколь часто вам приходится извлекать штучные или, тем более, уникальные значения из таблицы, содержащей миллионы и миллиарды записей? То-то и оно. Данные улучшения, скорей, будут полезны для вспомогательных таблиц либо справочников.
Куда более распространённым сценарием является выборка некоторого подмножества строк, отвечающих желаемому критерию для последующего соединения, агрегации и так далее. И здесь востребованными оказываются техники, позволяющие сузить круг поиска, исключить те блоки данных, которые заведомо не нужны: data skipping, pruning metadata. Ранее для этих целей пользователю были доступны:
-
Партицирование таблицы. Данные физически разделяются по разным таблицам и, как следствие, файлам. Появляется возможность сканировать лишь те партиции, которые могут содержать желаемые данные, есть возможность обслуживать, осуществлять резервное копирование и перезаливку таблицы частями. Платой становится жёсткость структуры, повышение нагрузки на каталог базы данных, более высокие требования к планировщикам, которые должны быть в курсе топологии таблицы, чтобы построить для нее эффективный план.
-
bitmap-индексы, которые будут хороши для столбцов с низкой кардинальностью (иначе занимаемый ими размер на диске перекроет все их преимущества). Они позволяют комбинировать в одном плане запроса несколько индексов по разным столбцам путём операций между битовыми масками.
-
btree-индексы, которые, в отличие от bitmap, смогут работать и по столбцам с высоким процентом уникальных значений, но потребуют значительных накладных расходов на сопровождение и использование.
Начиная с Postgres 9.5 и Greenplum 7 пользователям стал доступен ещё один вариант — BRIN-индексы (Block Range Index). Суть этого типа индексов сводится к тому, что всё адресное пространство в таблице разбивается на диапазоны фиксированного размера. Для heap-таблиц это конфигурируемое для каждого индекса таблицы число страниц, номера которых составляют старшие биты в идентификаторе версии строки. Для каждого диапазона в индексе хранится мета-информация, описывающая хранимые в нём значения. В самом простом варианте для упорядочиваемых типов — минимальное и максимальное значение. В более сложном — охватывающие значения, например, область, включающая в себя координаты всех точек, сохранённых в блоке. И так как мы храним лишь несколько значений на диапазон — такой индекс получается намного компактнее b-tree.
При обращении к такому индексу мы можем получить идентификаторы всех блоков, которые потенциально могут хранить запрошенные нами значения и затрачивать ресурсы только на их сканирование. Очевидно, что каждое извлекаемое значение потребуется перепроверить, поскольку запрошенный интервал не обязательно охватывает весь диапазон блоков, отобранных для сканирования. И чем уже и точнее описание каждого диапазона блоков при одном и том же его размере, тем эффективней будет работать сканирование по индексу, поскольку меньше блоков будет отбираться при сканировании и они будут содержать меньше «ненужных» значений.
Поэтому хорошими кандидатами на применение данного типа индексов будут столбцы, по которым характер загрузки данных в таблицу будет коррелировать с последующими запросами к ней. Будут данные загружаться в хронологическом порядке или посуточно и запрашиваться затем за более продолжительные временные промежутки (неделя, месяц) — отлично. Зачастую в качестве ориентира предлагается использовать столбцы, для которых согласно статистике есть прямая или обратная корреляция между значением и его расположением в таблице. Корреляцию можно создать принудительно, переписав таблицу в нужном порядке с помощью команды CLUSTER
при наличии индекса, а с помощью ALTER TABLE REPACK BY COLUMNS
, реализованной для Greenplum 7 (8ee44e8) — даже при его отсутствии. Для последней потребовалась ещё одна дополнительная функция в интерфейсе табличных методов — table_relation_copy_for_repack
.
Рассмотрим, как осуществляется поиск с помощью BRIN-индекса (см. реализацию функции amgetbitmap
индексного метода доступа — BRINgetbitmap
), чтобы понять, какие подводные камни возникли при адаптации индекса для AO-таблиц:
-
Запрашиваем для таблицы число страниц.
-
Описание в индексе хранится не для каждой страницы, а для их диапазона. Следовательно, мы будем обходить каждый диапазон страниц таблицы и с помощью индекса определять, может ли он хранить что-нибудь интересное для нас.
-
На первых страницах BRIN-индекса всегда хранится так называемая обратная карта диапазонов (
revmap
). Она всегда занимает неразрывный диапазон страниц, и для простоты мы можем считать её массивом указателей, позволяющим получить описание для желаемого диапазона страниц. -
Если описание для диапазона блоков не было найдено (например, если информация о диапазоне ещё не была обобщена), все его страницы будут отмечены к сканированию безусловно.
-
В ином случае будет вызвана опорная функция, которая интерпретирует обобщённую информацию и принимает решение о целесообразности сканирования данного диапазона блоков.
-
В итоге будет построена битовая карта, в которой будут отмечены все страницы, потенциально содержащие запрошенные строки.
А теперь ещё раз смотрим внимательно на шаг 3 и вспоминаем, что из себя представляет идентификатор версии строки для AO-таблицы. Напомню, что страниц фиксированного размера у нас нет, да и вообще, номер строки никак не отражает номер физической страницы. Зато в старших 7 битах хранится номер сегментного файла, который содержит конкретную версию строки. Таким образом, если хотя бы одна строка хранится в старшем из доступных 127-ом сегментном файле, нам потребуется обратная карта диапазонов, позволяющая адресовать как минимум 0xFE000000
страниц. На одну страницу BRIN-индекса размером 32 Кб в сборке Greenplum по умолчанию помещается около 5454 указателей. Таким образом, карта займет более 781 тысячи страниц или 23 Gb+. Для сравнения, в сборке Postgres со страницами по 8 Кб номер этой страницы соответствовал бы таблице, занимающей на диске более 32 Тб. В случае же АО-таблицы, такой идентификатор может иметь единственная строка во всей таблице.
По этой причине BRIN-индексы были подвергнуты существенной доработке ради поддержки AO-таблиц (63d916b). Весь диапазон доступных идентификаторов версий строк может разбиваться табличным методом доступа на последовательности (BlockSequences
) без столь существенных разрывов внутри каждой из них. Да, для этого появилась ещё одна функция в интерфейсе методов доступа — relation_get_block_sequences
. Для heap-таблиц она возвращает строго одну последовательность от первой и до последней страницы. Для Append Optimized таких последовательностей будет по числу сегментных файлов. Для каждого сегментного файла первой будет страница, номер которой состоит из номера сегментного файла (старшие 7 бит) и 25 бит, заполненных нолями. Последней будет страница, соответствующая текущему (!) значению генератора номеров версий строк для данного сегментного файла (FastSequence
). Так как значение генератора не сбрасывается в течение времени жизни таблицы, интенсивные обновления с последующей сборкой мусора всё равно будут приводить к хранению информации о несуществующих страницах в обратной карте диапазонов. Сами страницы будут сугубо логические, охватывающие диапазоны по 32к идентификаторов версий строк, что соответствует двум младшим байтам ctid
. Такая логическая страница будет минимальной единицей, для которой производится хранение обобщённой информации в индексе. А значит, минимальное количество информации, прочитанное с диска, будет зависеть от ширины строки конкретной таблицы, а при колоночном хранении — от необходимых запросу столбцов, в отличие от heap-таблицы, для которой размер страницы фиксирован. Это важно учитывать при выборе значения pages_per_range
во время создания индекса.
Таким образом, для AO-таблиц сканирование по BRIN-индексу будет производиться следующим образом:
-
Для каждого сегментного файла таблицы формируем последовательность логических страниц (
Block Sequence
). -
Для каждой последовательности страниц извлекаем из мета-страницы номер первой страницы обратной карты диапазонов.
-
Для каждого диапазона страниц, начиная с диапазона, соответствующего номеру сегментного файла:
-
Получаем номер страницы обратной карты диапазонов в цепочке, отбросив старшие биты, соответствующие номеру сегментного файла, делением на максимальное количество диапазонов на одну страницу карты.
-
Переходим к следующей странице обратной карты диапазонов, если номер текущей страницы меньше желаемого.
-
Получаем номер указателя (line pointer) на странице карты как остаток от деления номера страницы на максимальное число диапазонов, которое можно сохранить на ней.
-
По полученному смещению получаем идентификатор записи (страница + номер записи), описывающий текущий диапазон.
-
Извлекаем страницу и запись, содержащую описание диапазона.
-
Проверяем с помощью опорной функции соответствие обобщённой информации диапазона запрошенному условию. Если диапазон может содержать интересующие пользователя значения или обобщённая информация отсутствует (например, не была собрана), каждая его логическая страница помечается в битовой карте.
-
-
Как только построение битовой карты завершено (оператор
Bitmap Index Scan
), извлекаем последовательно версии строк с идентификаторами, принадлежащими логическим страницам (Bitmap Heap Scan
), и перепроверяем для них указанное пользователем условие. Карта блоков (Block Directory
) помогает нам получить смещение физических блоков в файлах, соответствующих началу каждой логической страницы. Тем самым мы снижаем объём данных, которые необходимо прочитать с диска.
В качестве примера рассмотрим сканирование таблицы из трёх столбцов с btree- и BRIN-индексами по столбцу b
целочисленного типа, которое извлечёт диапазон, содержащий примерно 0.7% записей. Ниже приведены планы запросов и затрачиваемое на их выполнение время.
ORCA для версии Greenplum 7.1 сильно промахивается с оценкой стоимости индексного сканирования и по умолчанию выбирает последовательное сканирование.
Когда для получения того же результата сегменту, хранившему большую часть искомых строк, достаточно просканировать лишь две логических страницы (Heap Blocks
) с помощью BRIN-индекса. Эти блоки будут содержать лишь 23430 искомых строк, остальные 42105 будут отфильтрованы после перепроверки условия.
Схожего результата можно добиться с помощью btree-индекса. С той разницей, что данный метод доступа и количество извлекаемых значений позволяют построить точную (вплоть до идентификатора версии строки) битовую карту и избежать перепроверки условия. Но ценой этому станет больший на два порядка размер индексов.
Параллельные операции в рамках сегмента
Postgres 9.6 привнёс возможность распараллеливать многие операции в рамках запроса. Однако быстрого пути совмещения параллелизации запросов в рамках кластера и в рамках конкретного сегмента кластера разработчики Greenplum не нашли. И от греха подальше запретили параллелизацию запросов в рамках сегмента.
Чтобы представить потенциальные трудности реализации, достаточно взглянуть на параллельный план для Postgres.
Затем попытаться представить, как будет выглядеть распределённый вариант такого плана.
Где X
— число рабочих процессов, а не сегментов в кластере. Замена Gather
на Gather Motion
выглядит органично. С Redistribute Motion
сложнее. Каждый из рабочих процессов на сегменте должен получить только строки, соответствующие данному сегменту, но равномерно распределённые между ними.
К тому же остается открытым вопрос параллелизации сканирования Append Optimized таблиц. Существующий для heap подход опирается на фиксированный размер страниц, количество которых известно в момент начала сканирования.
С другой стороны, это могло бы существенно упростить управление кластером, а также распределение ресурсов в рамках одного хоста, в том числе за счёт регуляции числа рабочих процессов. При условии, что один экземпляр Postgres сможет утилизировать ресурсы одной физической машины.
Расширение возможностей Foreign-таблиц
Postgres предоставляет разработчикам расширение API внешних таблиц, позволяющее получать или изменять данные в произвольных источниках. Таким произвольным источником может быть файл, s3-хранилище, Postgres, Kafka и даже другой кластер Greenplum.
Процесс получения строк из внешнего хранилища спрятан за узлом плана Foreign Scan
. Представим, что у нас есть две таблицы во внешнем Postgres, результат соединения которых мы хотим использовать в Greenplum.
В Postgres 9.4 и Greenplum 6, возможности FDW были ограничены пробросом предикатов в удалённую систему. Само соединение и его последующая обработка будут производиться на инициаторе запроса (в данном случае на координаторе).
Однако в мире Postgres жизнь шла своим чередом. В Postgres 9.6 появилась возможность проброса соединений, сортировок, а также операций UPDATE
/DELETE
. Postgres 10 добавил поддержку агрегатов. Postgres 12 расширил поддержку сортировок, а также реализовал поддержку проброса LIMIT
. Получив эти изменения, мы можем увидеть следующий план запроса, в котором результат будет полностью вычислен на удалённой системе.
Теперь арсенал этих возможностей будет возможно использовать в коннекторах к различным системам. Также для использования этих возможностей потребуется доработка второго планировщика, доступного в поставке Greenplum — ORCA. В актуальных версиях ORCA строит план, аналогичный тому, что мы могли увидеть в Greenplum 6.
Возможности мониторинга рабочих процессов базы данных
Долго выполняется запрос. EXPLAIN ANALYZE
не дождаться. Всё, что нам могли предоставить штатные инструменты в Greenplum 6 — это столбец wait_reason
представления pg_stat_activity
со значениями lock
, replication
или resgroup
. Не густо. Приходилось ходить по всему кластеру в поисках узкого места, подключать трассировщик, отладчик и т.д. В Postgres 9.6 данная ситуация начала меняться (53be0b1), затем была развита в Postgres 10 (6f3bd98 и 249cf07). Теперь пользователю для анализа доступен широкий перечень причин текущего ожидания процесса.
В данном примере мы можем видеть, что координатор диспетчеризировал запрос на сегменты и ожидает завершения его выполнения. Два из трех сегментов в момент запроса была заблокированы на вводе-выводе: ожидали расширения файлов таблицы для загрузки новых данных. Третий же ожидает, когда его догонит синхронная реплика (зеркало), чтобы продолжить вставку. Полноценного перечисления ожиданий в документации Greenplum на текущий момент нет, можно воспользоваться документацией Postgres или изучать исходники для Greenplum-специфичных ожиданий.
Отдельно следует обратить внимание на новое представление gp_stat_activity
для сбора информации со всего кластера — ранее требовалось писать обёртку самому.
Также с патчами Postgres 10 стал доступен мониторинг системных фоновых процессов. В примере ниже процесс WAL-writer был запечатлён в момент ожидания ввода-вывода при сбросе содержимого буфера журнала упреждающей записи на диск.
И это ещё не всё
Мы рассмотрели влияние ещё нескольких изменений Postgres на функционал новой версии Greenplum. В заключительной, третьей части мы продолжим рассматривать такие улучшения, а также поговорим о том, как изменились специфичные для нашей базы данных возможности. Оставайтесь на связи, спасибо за внимание!