Что нового в Greenplum 7. Часть 2

gfhz73bsraoqewi3k1-tze4eio0.png

В прошлой части обзора изменений и нововведений 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-таблиц:

  1. Запрашиваем для таблицы число страниц.

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

  3. На первых страницах brin-индекса всегда хранится так называемая обратная карта диапазонов (revmap). Она всегда занимает неразрывный диапазон страниц, и для простоты мы можем считать её массивом указателей, позволяющим получить описание для желаемого диапазона страниц.

  4. Если описание для диапазона блоков не было найдено (например, если информация о диапазоне ещё не была обобщена), все его страницы будут отмечены к сканированию безусловно.

  5. В ином случае будет вызвана опорная функция, которая интерпретирует обобщённую информацию и принимает решение о целесообразности сканирования данного диапазона блоков.

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

А теперь ещё раз смотрим внимательно на шаг 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-индексу будет производиться следующим образом:

  1. Для каждого сегментного файла таблицы формируем последовательность логических страниц (Block Sequence).

  2. Для каждой последовательности страниц извлекаем из мета-страницы номер первой страницы обратной карты диапазонов.

  3. Для каждого диапазона страниц, начиная с диапазона, соответствующего номеру сегментного файла:

  4. Получаем номер страницы обратной карты диапазонов в цепочке, отбросив старшие биты, соответствующие номеру сегментного файла, делением на максимальное количество диапазонов на одну страницу карты.

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

  6. Получаем номер указателя (line pointer) на странице карты как остаток от деления номера страницы на максимальное число диапазонов, которое можно сохранить на ней.

  7. По полученному смещению получаем идентификатор записи (страница + номер записи), описывающий текущий диапазон.

  8. Извлекаем страницу и запись, содержащую описание диапазона.

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

  10. Как только построение битовой карты завершено (оператор Bitmap Index Scan), извлекаем последовательно версии строк с идентификаторами, принадлежащими логическим страницам (Bitmap Heap Scan), и перепроверяем для них указанное пользователем условие. Карта блоков (Block Directory) помогает нам получить смещение физических блоков в файлах, соответствующих началу каждой логической страницы. Тем самым мы снижаем объём данных, которые необходимо прочитать с диска.

В качестве примера рассмотрим сканирование таблицы из трёх столбцов с btree- и brin-индексами по столбцу b целочисленного типа, которое извлечёт диапазон, содержащий примерно 0.7% записей. Ниже приведены планы запросов и затрачиваемое на их выполнение время.

ORCA для версии Greenplum 7.1 сильно промахивается с оценкой стоимости индексного сканирования и по умолчанию выбирает последовательное сканирование:

0xqkvcedtu69mingsxm812klvmm.png

Когда для получения того же результата сегменту, хранившему большую часть искомых строк, достаточно просканировать лишь две логических страницы (Heap Blocks) с помощью brin-индекса. Эти блоки будут содержать лишь 23430 искомых строк, остальные 42105 будут отфильтрованы после перепроверки условия.

3jzfdcxlemzbkngju-jpbsevywi.png

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

qgtdzphsdbd_sxicad6iojsrxbc.png

Параллельные операции в рамках сегмента

Postgres 9.6 привнёс возможность распараллеливать многие операции в рамках запроса. Однако быстрого пути совмещения параллелизации запросов в рамках кластера и в рамках конкретного сегмента кластера разработчики Greenplum не нашли. И от греха подальше запретили параллелизацию запросов в рамках сегмента:

171qjqqi1otgb7tkhti-wiysple.png

Чтобы представить потенциальные трудности реализации, достаточно взглянуть на параллельный план для Postgres:

bwpd7hlbwutwvsqpdnwuwsyzxhc.png

Затем попытаться представить, как будет выглядеть распределённый вариант такого плана:

06sorcummk7ia2xqm6bq7j36feq.png

Где X — число рабочих процессов, а не сегментов в кластере. Замена Gather на Gather Motion выглядит органично. С Redistribute Motion сложнее. Каждый из рабочих процессов на сегменте должен получить только строки, соответствующие данному сегменту, но равномерно распределённые между ними.

К тому же, остается открытым вопрос параллелизации сканирования Append Optimized таблиц. Существующий для heap подход опирается на фиксированный размер страниц, количество которых известно в момент начала сканирования.

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

Расширение возможностей Foreign-таблиц

Postgres предоставляет разработчикам расширение API внешних таблиц, позволяющее получать или изменять данные в произвольных источниках. Таким произвольным источником может быть файл, s3-хранилище, Postgres, Kafka и даже другой кластер Greenplum.

Процесс получения строк из внешнего хранилища спрятан за узлом плана Foreign Scan. Представим, что у нас есть две таблицы во внешнем Postgres, результат соединения которых мы хотим использовать в Greenplum:

yftmi5ct4wy9mkagaqevlajqxby.png

В Postgres 9.4 и Greenplum 6, возможности FDW были ограничены пробросом предикатов в удалённую систему. Само соединение и его последующая обработка будут производиться на инициаторе запроса (в данном случае на координаторе).

Однако в мире Postgres жизнь шла своим чередом. В Postgres 9.6 появилась возможность проброса соединений, сортировок, а также UPDATE/DELETE-операций. Postgres 10 добавил поддержку агрегатов. Postgres 12 расширил поддержку сортировок, а также реализовал поддержку проброса LIMIT. Получив эти изменения, мы можем увидеть следующий план запроса, в котором результат будет полностью вычислен на удалённой системе:

nem4evt2wfo1kpoy_qesbcbvvzi.png

Теперь арсенал этих возможностей будет возможно использовать в коннекторах к различным системам. Также для использования этих возможностей потребуется доработка второго планировщика, доступного в поставке 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). Теперь пользователю для анализа доступен широкий перечень причин текущего ожидания процесса.

2y9vpx3z0th20ukd7py_ptzmhza.png

В данном примере мы можем видеть, что координатор диспетчеризировал запрос на сегменты и ожидает завершения его выполнения. Два из трех сегментов в момент запроса была заблокированы на вводе-выводе: ожидали расширения файлов таблицы для загрузки новых данных. Третий же ожидает, когда его догонит синхронная реплика (зеркало), чтобы продолжить вставку. Полноценного перечисления ожиданий в документации Greenplum на текущий момент нет, можно воспользоваться документацией Postgres или изучать исходники для Greenplum-специфичных ожиданий.

Отдельно следует обратить внимание на новое представление gp_stat_activity для сбора информации со всего кластера — ранее требовалось писать обёртку самому.

Также с патчами Postgres 10 стал доступен мониторинг системных фоновых процессов. В примере ниже процесс WAL-writer был запечатлён в момент ожидания ввода-вывода при сбросе содержимого буфера журнала упреждающей записи на диск:

qi0hpaqzrnbrl3qzcehjcgv5ahw.png

И это ещё не всё

Мы рассмотрели влияние ещё нескольких изменений Postgres на функционал новой версии Greenplum. В заключительной, третьей части мы продолжим рассматривать такие улучшения, а также поговорим о том, как изменились специфичные нашей базе данных возможности. Оставайтесь на связи, спасибо за внимание!

Благодарю за помощь в подготовке данной статьи наших дизайнеров — @kawaii_anya и @nastoika_lera.

© Habrahabr.ru