Введение

История, связанная с этой задачей, началась для нас в мае 2024 года. Один из крупных пользователей Greenplum/Arenadata DB обратился к нам с запросом реализовать возможность отслеживания изменения размеров файлов данных таблиц. Эта функциональность стала бы составной частью, источником событий для системы мониторинга пользовательских кластеров. Задача показалась нам крайне интересной и перспективной. Однако пользователю, как это часто бывает, решение требовалось уже вчера.

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

В этой статье:

  • Представим нашу новую фичу отслеживания размеров файлов данных таблиц (далее я буду называть её треком изменений), которая стала частью расширения arenadata_toolkit, поставляемого в том числе в рамках версии Arenadata DB с открытым исходным кодом (скорее всего, в будущем мы рассмотрим перенос этой фичи в каком-то виде в Greengage DB).

  • Обоснуем основные принципы предлагаемого решения.

  • Расскажем, с какими сложностями нам пришлось столкнуться, и погрузимся в детали реализации.

  • В конце статьи приведём пример простейшей системы мониторинга на основе данного трека изменений.

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

Пользовательские сценарии

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

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

Пусть это будут следующие вводные условия (разумеется тут речь про порядки).

Ориентировочное количество таблиц

Тип таблиц

Общее число таблиц

AO-tables row oriented

10 000

AO-tables column oriented

200 000

Heap tables

1 000 000

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

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

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

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

Подсчёт размера таблиц

В самом начале нужно пояснить, что означает подсчитать полный размер таблиц в PostgreSQL и Greenplum, в частности.

Структура связей heap-таблиц с точки зрения хранения данных
Структура связей heap-таблиц с точки зрения хранения данных

Если рассмотреть схематично heap-таблицы, то с таблицей будут связаны сегментные файлы данных (связь heap table → heap segments). Чем больше таблицы по размеру, тем больше файлов связано с таблицей — для Greenplum максимальный размер heap-сегмента составляет 1 ГБ: RELSEG_SIZE (32768 байт) * BLCKSZ (32768 байт), при превышении создаётся новый сегмент-файл.

Также есть файлы карты видимости и свободного пространства (heap table → vm и heap table → fsm). Если есть атрибуты с хранением в TOAST-таблицах, то присутствуют файлы данных TOAST-таблиц (heap table → toast), у которых в свою очередь есть связанные btree-индексы (heap table → toast → toast btree).

Как и btree-индексы, так и TOAST-таблицы — это heap-таблицы, что означает наличие сегментных файлов, карт видимости (только для TOAST-таблиц) и свободного пространства. У таблицы также могут быть пользовательские индексы.

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

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

Структура связей AO-таблиц с точки зрения хранения данных
Структура связей AO-таблиц с точки зрения хранения данных

Какой существующий инструментарий есть в нашем распоряжении

Во-первых, это базовые системные функции pg_relation_size, pg_table_size и pg_total_relation_size.

Эти функции позволяют вычислить размер таблиц с учетом выбранного fork-а (mainfsmvminit), размера таблицы без индексов и полного размера с индексами.

Размеры, вычисляемые функциями pg_relation_size, pg_table_size, pg_total_relation_size
Размеры, вычисляемые функциями pg_relation_size, pg_table_size, pg_total_relation_size

Во-вторых, системная схема gp_toolkit реализует несколько представлений, которые в том числе опираются на приведенные выше функции. Вот некоторые из них: gp_size_of_table_diskgp_size_of_table_and_indexes_diskgp_size_of_index. Удобство этих представлений в том, что они отображают детализацию с разбивкой по типам хранения (ветка main, данные в TOAST, aux-данные для AO-таблиц) в связке с индексами и без. В схеме arenadata_toolkit есть представления по определению data skew по таблицам и их аналоги в разы более быстрые по производительности.

Расширение diskquota решает немного другую задачу ограничения потребления дискового пространства для таблиц, но в теории можно было бы его доработать каким-то образом.

Насколько это всё может быть затратным

Для тестовых запросов и оценки времени выполнения я создал базу данных со следующим набором тестовых таблиц.

Тестовый набор таблиц

relkind

relstorage

Число таблиц

ordinary relation ('r')

heap

450 092

index ('i')

heap

250 166

internal append-optimized segment files and EOFs ('o')

heap

250 005

ordinary relation ('r')

append optimized table

250 005

append-only visibility map ('M')

heap

250 005

В качестве тестовой нагрузки для получения времени выполнения и картины с точки зрения профилирования этих операций выполнил ряд запросов (3 прогона каждый) со сброшенным и прогретым кешем. Запросы выполнялись на ноутбуке DELL Latitude 5410, Intel Core i7, 256 ГБ SSD, 32 ГБ RAM.

Сравнительные показатели времени получения размеров таблиц в разных условиях

Тип операции

Время операции (кеш сброшен)

Время операции (кеш прогрет)

Вычисление размеров ~1,5 млн. таблиц функцией pg_relation_size

12 мин. 21 сек.

8 мин. 37 сек.

Построение представления gp_size_of_table_and_indexes_disk для 700 тыс. таблиц (только пользовательские таблицы, согласно реализации этого представления)

13 мин. 45 сек.

10 мин. 48 сек.

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

Однако есть и другие причины, по которым нас не устроили базовые механизмы.

Главная из этих причин заключается в том, что для AO-таблиц системные функции вычисляют логический размер — для каждой таблицы требуется открыть свою таблицу с метаданными, откуда нужно прочитать строку со смещением. За это отвечают функции ядра GetAOTotalBytes и GetAOCSTotalBytes. Эти функции получают логический размер из aux-таблицы, связанной с каждой AO-таблицей (OID aux-таблицы можно увидеть в поле segrelid).

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

Создадим AO-таблицу, прервём запись большого блока данных (100 млн. строк) и попробуем вычислить размеры таблицы функцией pg_total_relation_size после прерывании записи и отката транзакции:

andrey=# CREATE TABLE aoro_rollbacked(c1 INT) WITH (appendoptimized=true, orientation=row);
CREATE TABLE
й 
andrey=# INSERT INTO aoro_rollbacked SELECT generate_series(1,1000);
INSERT 0 1000

andrey=# SELECT pg_size_pretty(pg_total_relation_size('aoro_rollbacked'::regclass));

 pg_size_pretty
----------------
 138 kB
(1 row)

andrey=# INSERT INTO aoro_rollbacked SELECT generate_series(1,100000000); 
^CCancel request sent
ERROR:  canceling statement due to user request

andrey=# SELECT pg_size_pretty(pg_total_relation_size('aoro_rollbacked'::regclass));

 pg_size_pretty
----------------
 138 kB  
(1 row)

Как видно, после отката транзакции (строка, отмеченная маркером 1) логический размер не изменился (маркер 2), хотя по факту размер первого сегментного файла (нулевой файл зарезервирован для AO-таблиц) занял на момент отката транзакций 220 МБ на одном из сегментов:

$ PGOPTIONS='-c gp_session_role=utility' psql -p 6002 -c 'SELECT pg_relation_filepath('\''aoro_rollbacked'\''::regclass);' -At | xargs -I % ls -lh '%.1'

-rw------- 1 andrey andrey 220M янв 25 12:13 base/5963210/2422978.1

Теперь касательно деталей, которые оказывают влияние на итоговую производительность.

Наибольшие затраты при построении системного представления gp_size_of_table_and_indexes_disk
Наибольшие затраты при построении системного представления gp_size_of_table_and_indexes_disk

Если посмотреть на результаты профилирования построения представления gp_size_of_table_and_indexes_disk (рисунок выше), то можно увидеть, что довольно ощутимый вклад в общее время вносят функция открытия таблицы (try_relation_open) и упомянутая выше функция получения логического размера AO-таблиц (GetAOTotalBytes). Затраты на получение самих физических размеров файлов функцией stat теряются на общем фоне.

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

Расширение diskquota

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

Таким образом, у этого расширения есть потенциально тяжелый стейт (как runtime, так и в самой СУБД в виде таблиц).

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

Что мы предлагаем на уровне идеи

Теперь к предлагаемому решению. В его основу мы заложили такие принципы:

  • Обойтись без поддержки тяжелого стейта в самой базе (например, какие-то обновляемые таблицы или что-то подобное).

  • Строить трек изменений "на лету".

  • Не открывать без особой необходимости таблицы (например, метаданные в таблицах, каталогах и т.п.).

  • По возможности использовать компактные структуры данных.

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

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

Основа для решения

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

  • Низкоуровневые модули для работы с физическими файлами smgr.c и md.c.

  • Интересующие нас действия, которые можно отслеживать: createextendtruncate и unlink. (Далее я буду называть их событиями. Маппинг действий на события приведён в таблице ниже).

  • Связанные с этим хуки (созданные, кстати, под diskquota).

  • Идентификация таблиц и файлов (OID таблицы, идентификатор файла relfilenode).

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

smgr — это общий интерфейс для возможных менеджеров хранилищ. Он предоставляет контракт для таких операций как initclosecreateunlinkextendreadwritetruncate и др. Менеджер md их реализует. Также в aomd есть специфичная часть для AO-таблиц.

Основные функции, интересующие нас в контексте отслеживания событий: smgrcreatesmgrcreate_aosmgrextendsmgrtruncate и smgrdounlinkall. Часть событий генерируется из кода реализации AORO- и AOCO-таблиц.

Маппинг функций ядра на события

Функция

Сопоставляемое событие

Тип таблиц

Описание события

smgrcreate

create

heap

Событие создания файла данных heap-таблицы (с учетом требуемого ForkNumber)

smgrcreate_ao

create

AO

Событие создания нового сегментного файла данных AO-таблицы

smgrextend

extend

heap

Событие расширения файла данных heap-таблицы новым блоком (для heap-таблиц размер блока равен BLCKSZ = 32 КБ)

BufferedAppendWrite

extend

AO

Событие расширения сегментного файла данных AO-таблицы новым блоком

smgrtruncate

truncate

heap

Событие сокращения сегментного файла данных heap-таблицы до требуемого количества блоков (BlockNumber nblocks)

TruncateAOSegmentFile

truncate

AO

Событие сокращения сегментного файла данных AO-таблицы, начиная с некоторого смещения и до конца файла

smgrdounlinkall

unlink

heap/AO

Удаление файлов, связанных с таблицей (unlink)

Все эти функции содержат в себе callback-вызовы пользовательских хук-функций, они вызываются после вызова системных реализаций:

typedef void (*file_create_hook_type) (RelFileNodeBackend rnode);
typedef void (*file_extend_hook_type) (RelFileNodeBackend rnode);
typedef void (*file_truncate_hook_type) (RelFileNodeBackend rnode);
typedef void (*file_unlink_hook_type) (RelFileNodeBackend rnode);

Файлы таблиц идентифицируются по идентификатору relfilenode.

Определение RelFileNode

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

Экземпляр RelFileNodeBackend можно упрощенно считать идентификатором файла, с которым произошли какие-то изменения. Характер изменений зависит от типа хука.

Определение RelFileNodeBackend

Связь событий с жизненным циклом транзакций

Теперь предлагаю рассмотреть события в контексте их возникновения при разных вариантах запросов и сценариев выполнения транзакций. Каждое из интересующих нас событий влияет либо на потребление дискового пространства (extend, truncate, unlink), либо, в случае события create, на количественный состав таблиц в базе (некоторые файлы данных изначально могут быть нулевого размера).

Таким образом, наша основная задача — отслеживать события createextend, truncateunlink, оказывая минимальное влияние на производительность СУБД. При это важно тратить на это минимум ресурсов, которые разумнее отдавать под боевую нагрузку.

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

В качестве примеров запросов рассмотрим следующие запросы:

BEGIN; CREATE TABLE t(...); INSERT INTO t ...; COMMIT;
BEGIN; CREATE TABLE t(...); INSERT INTO t ...; ROLLBACK;
BEGIN; CREATE TABLE t(...); DROP TABLE t; ROLLBACK;
CREATE TABLE t(...); BEGIN; ...; DROP TABLE t; ...; COMMIT;
CREATE TABLE t(...); BEGIN; ...; DROP TABLE t; ...; ROLLBACK;

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

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

BEGIN; CREATE TABLE t(...); INSERT INTO t ...; COMMIT;
Создание таблицы внутри транзакции с последующим COMMIT
Создание таблицы внутри транзакции с последующим COMMIT

Отличительной особенностью первого сценария, представленного на рисунке, является то, что события (в данном случае create и extend в моменты времени t1, t2t3) возникают в контексте таблицы, OID которой еще не виден в системной таблице pg_class остальным транзакциям согласно правилам изоляции.

Таким образом, в транзакции получения трека "вытащить" штатными средствами сведения таблицы из pg_class не представляется возможным. Однако мы должны фиксировать эти события и сообщать о них в контексте изменений по таблицам в треке изменений, так как в данном случае транзакция по итогу применяется (COMMIT в момент времени t6).

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

Иная ситуация возникает для событий отката транзакции (ROLLBACK):

BEGIN; CREATE TABLE t(...); INSERT INTO t ...; ROLLBACK;
BEGIN; CREATE TABLE t(...); DROP TABLE t; ROLLBACK;
Откат транзакции с событиями создания таблицы
Откат транзакции с событиями создания таблицы

В первом случае транзакция, создавшая таблицу, откатывается (момент времени t4). Во втором случае есть явный DROP TABLE, но событие unlink всё равно возникает при обработке отката транзакции (момент времени t6). Так что в плане момента возникновения события unlink они равнозначны. Созданные и наполненные данными, файлы таблицы должны быть удалены в smgrDoPendingDeletes. Для этой функции уже найдётся работа (момент времени t5), так как файлы данных, в которые успели попасть строки (произошли события extend), должны быть удалены по итогам транзакции.

Еще пара идентичных случаев с точки зрения событий unlink — событие удаления возникает только в момент применения транзакции:

CREATE TABLE t(...); BEGIN; DROP TABLE t; ...; COMMIT;
BEGIN; CREATE TABLE t(...); DROP TABLE t; ...; COMMIT;
Применение транзакции, содержащей события удаления таблицы
Применение транзакции, содержащей события удаления таблицы

Тут важно отметить, что событие unlink происходит не при вызове DROP TABLE в момент времени t1, а по итогам применения транзакции всё в той же smgrDoPendingDeletes в момент времени t4.

Если таблица была создана ранее, то вызов DROP TABLE с последующим откатом транзакции не сгенерирует никаких событий:

CREATE TABLE t (...); BEGIN; ...; DROP TABLE t; ...; ROLLBACK;
При откате транзакции с DROP TABLE не происходит события unlink
При откате транзакции с DROP TABLE не происходит события unlink

Как работает получение трека изменений

Для получения трека изменений пользователю нужно сделать SELECT-запрос к представлению arenadata_toolkit.tables_track. Внутри это приводит к вызову функции tracking_get_track на мастере и сегмент-серверах.

Трек изменений представляет собой набор записей следующего вида:

  relid  |          relname           | relfilenode | size  | state | segid | relnamespace | relkind | relstorage | parent_relid
---------+----------------------------+-------------+-------+-------+-------+--------------+---------+------------+--------------
 5971396 | pg_aovisimap_5971392       |     2422956 |     0 | a     |    -1 |         6104 | M       | h          |      5971392
 5971394 | pg_aoseg_5971392           |     2422955 | 32768 | a     |    -1 |         6104 | o       | h          |      5971392
 5971402 | pg_aoblkdir_5971392_index  |     2422960 | 32768 | a     |    -1 |         6104 | i       | h          |      5971400
 5971392 | t1                         |     2422954 |     0 | a     |    -1 |         2200 | r       | a          |
 5971400 | pg_aoblkdir_5971392        |     2422959 |     0 | a     |    -1 |         6104 | b       | h          |      5971392
 5971399 | t1_c1_idx                  |     2422958 | 32768 | a     |    -1 |         2200 | i       | h          |      5971392
 5971398 | pg_aovisimap_5971392_index |     2422957 | 32768 | a     |    -1 |         6104 | i       | h          |      5971396
 5971396 | pg_aovisimap_5971392       |     2422949 |     0 | a     |     0 |         6104 | M       | h          |      5971392
 5971394 | pg_aoseg_5971392           |     2422948 | 32768 | a     |     0 |         6104 | o       | h          |      5971392
 5971402 | pg_aoblkdir_5971392_index  |     2422953 | 65536 | a     |     0 |         6104 | i       | h          |      5971400
 5971392 | t1                         |     2422947 |    32 | a     |     0 |         2200 | r       | a          |
 5971400 | pg_aoblkdir_5971392        |     2422952 | 32768 | a     |     0 |         6104 | b       | h          |      5971392
 5971399 | t1_c1_idx                  |     2422951 | 65536 | a     |     0 |         2200 | i       | h          |      5971392
 5971398 | pg_aovisimap_5971392_index |     2422950 | 32768 | a     |     0 |         6104 | i       | h          |      5971396
(14 rows)

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

Значение relfilenode определяет тот файл, с которым происходили изменения. Например, для heap-таблиц изменения разных сегментных файлов все равно будут отображаться с исходным идентификатором из pg_class.relfilenode. Актуальный на момент получения трека физический размер файлов данных таблицы можно увидеть в поле size. Вся информация предоставляется в разрезе каждого сегмента (поле segid), согласно набору отслеживаемым событий и параметрам фильтрации (схема relnamespace, тип таблицы relkind, тип хранения relstorage). Файл однозначно сопоставляется с таблицей (поля relid и relname).

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

Описание полей трека изменений

Поле трека

Описание

relid

OID таблицы (pg_class.oid)

relname

Имя таблицы

relfilenode

Идентификатор relfilenode (pg_class.relfilenode) файла данных таблицы, с которым произошли изменения (произошло одно из событий create, extend, truncate, unlink)

size

Текущий (физический) размер файлов данных, составляющих таблицу

state

Возможные значения:

a — активная таблица;

d — удаленные файлы данных таблицы;

i — начальный снимок (initial snapshot).

segid

Идентификатор сегмента, на котором расположен файл данных таблицы (поле content в терминах таблицы gp_segment_configuration)

relnamespace

OID схемы

relkind

Тип таблицы (pg_class.relkind)

relstorage

Тип хранения данных таблицы (pg_class.relstorage)

parent_relid

OID родительской таблицы для агрегации данных в рамках таблицы (NULL если верхний уровень)

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

Упрощенный алгоритм работы функции tracking_get_track таков:

  1. Открываем таблицу pg_catalog.pg_class на чтение.

  2. Для каждой записи из pg_catalog.pg_class:

    2.1    Проверяем relnamespace по набору отслеживаемых схем arenadata_toolkit.tracking_schemas.

    2.2    Проверяем relkind по набору отслеживаемых типов таблиц arenadata_toolkit.tracking_relkinds.

    2.3    Проверяем relstorage по набору отслеживаемых типов хранения данных таблиц arenadata_toolkit.tracking_relstorages.

    2.4    Если pg_class.relfilenode попадет во множество измененных файлов (проверка по bloom-фильтру, подробности в следующих разделах), то идём дальше, иначе переходим к п.2.

    2.5    Вычисляем размер таблицы.

  3. Если запрашивается начальный снимок (initial snapshot), то:

    3.1    О событиях удаления таблиц не сообщаем.

  4. Иначе:

    4.1    Сообщаем о файлах данных удаленных таблиц с флагом d.

Как обрабатываются события в трекинге изменений

Теперь подробнее про реализацию шага 2.4. в описанном выше алгоритме работы функции tracking_get_track.

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

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

С учетом обозначенных выше принципов построения трека на лету и использования компактных структур данных мы решили построить нашу реализацию на базе сохранения информации о части событий в bloom-фильтре.

Эта структура данных реализует "вероятностный подход" к поиску ответа на вопрос, принадлежит ли искомый элемент к некоторому множеству. Однако с учетом компактности представления и скорости работы, в целом, это подходит для решения поставленной задачи. Даже если при ложном срабатывании мы сообщим, что с некоторым файлом произошли изменения, и добавим информацию об этом событии в трек изменений, то можно воспринимать это как актуализацию информации. Так как хеш-функции bloom-фильтра "зашиты" внутри расширения, то единственным параметром, определяющим минимизацию ложных срабатываний, является размер фильтра. Как известно, bloom-фильтр представляет собой битовый массив. Для настройки его размера мы выбрали значение в байтах, которое определяется GUC-настройкой arenadata_toolkit.tracking_bloom_size. Главное тут подобрать нужный размер bloom-фильтра. Немного поэкспериментировав, я выбрал 2 МБ (при значениях < 1 МБ в треке изменений регулярно встречались не затронутые изменениями файлы).

В качестве примера рассмотрим обработку события extend (truncate принципиально ничем не отличается).

Sequence-диаграмма обработки события extend
Sequence-диаграмма обработки события extend

Sequence-диаграмма обработки события "extend"

Обработчик события extend делает проверку, что данный файл отслеживается (вызов is_file_node_trackable). Это могут быть таблицы и материализованные представления: RELPERSISTENCE_PERMANENTRELPERSISTENCE_TEMPRELPERSISTENCE_UNLOGGED.

Далее идентификатор relfilenode фиксируется (в соответствии с результатами хеш-функций) в bloom-фильтре (bloom_set_set). Это очень быстрая операция. После этого при обходе pg_class и проверке идентификаторов по bloom-фильтру можно узнать, менялся ли какой-либо файл, связанный с этой таблицей (с учетом возможности ложного срабатывания, но такой риск принимается).

Особенность события unlink в том, что на момент вызова хука какие-либо сведения по связанной с удаляемыми файлами таблице в pg_class уже отсутствуют. По факту в нашем распоряжении есть только идентификатор relfilenode. Очевидно, что обход pg_class не выдаст данные по этой таблице, поэтому полученный в хуке идентификатор relfilenode мы вынуждены сохранять до момента получения трека изменений в связанном списке.

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

Транзакционная семантика функции tracking_get_track

Трек изменений допускает получение следующими вариантами запросов:

-- Возвращаем информацию об изменениях в таблицах и "сбрасываем" трек.
-- Таким образом, изменения, попавшие в это множество записей, не будут возвращаться в следующем запросе.
BEGIN; SELECT * FROM arenadata_toolkit.tables_track; COMMIT;

-- Возвращаем информацию об изменениях в таблицах, но трек не сбрасывается.
-- В следующем запросе изменения будут "супер-позицией" всех изменений.
BEGIN; SELECT * FROM arenadata_toolkit.tables_track; ROLLBACK;

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

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

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

Таким образом, для сегмента возможны два варианта:

  • Полученное от мастер-сегмента значение счетчика равно текущему значению на сегменте, что означает применение предыдущей транзакции получения трека на уровне всего кластера.

  • Значение отличается, что равнозначно откату транзакции.

Как эти ситуации отражаются на обработке трека, мы увидим в следующим разделе.

Детали реализации на базе bloom-фильтра

С каждой базой связан свой набор bloom-фильтров, которые фиксируют события изменений, произошедших с файлами данных таблицы. В bloom-фильтре, по сути, выставляются нужные биты, которые вычисляются по идентификатору RelFileNodeBackend хеш-функциями фильтра. Для части событий можно обойтись простой фиксацией этих событий в фильтре (extendtruncate). С create и unlink, как уже упоминалось, приходится использовать дополнительные структуры данных и более сложную обработку.

Для поддержки транзакционной семантики получения трека изменений требуется наличие пары фильтров:

  • Первый экземпляр bloom-фильтра необходим для хранения "глобального" контекста (global context), который требуется для отдельных вызовов функции tracking_get_track, соответственно, в разных транзакциях.

  • Второй экземпляр bloom-фильтра требуется для локального контекста (local context) текущего вызова tracking_get_track.

Глобальный и локальный контекст bloom-фильтров
Глобальный и локальный контекст bloom-фильтров

Первый экземпляр bloom-фильтра можно представить себе как единый массив битов, который логически разделяется на две равные части (bloom bits и bloom bits shadow). Одна часть хранит в себе биты для текущего bloom-фильтра, с которым работают функции обратного вызова, выставляющие биты bloom-фильтра (хуки типа file_create_hook_type). Вторая часть отвечает за хранение состояния фильтра на момент предыдущего вызова tracking_get_track.

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

Для каждого вызова tracking_get_track аллоцируется локальный контекст со своим изначально пустым фильтром, текущий фильтр из глобального контекста (условно, левая часть массива) копируется в локальный контекст. Функция tracking_get_track продолжает работать со своим состоянием. Копирование bloom-фильтра, разумеется, происходит под блокировкой, но будет быстрым. При этом глобальный контекст продолжит отслеживать текущие события, не влияя на копию, с которой продолжит работать транзакция tracking_get_track.

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

Практическая часть: реализация базового мониторинга таблиц

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

Начальная конфигурация источника метрик

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

$ gpconfig -c shared_preload_libraries -v 'arenadata_toolkit' 
20250122:16:22:12:164571 gpconfig:ldap:andrey-[INFO]:-completed successfully with parameters '-c shared_preload_libraries -v arenadata_toolkit'

$ gpstop -ar

$ psql
psql (9.4.26)
Type "help" for help.

andrey=# CREATE EXTENSION arenadata_toolkit;
andrey=# \dx
                                          List of installed extensions
       Name        | Version |   Schema   |                             Description
-------------------+---------+------------+---------------------------------------------------------------------
 arenadata_toolkit | 1.7     | public     | extension is used for manipulation of objects created by adb-bundle
 plpgsql           | 1.0     | pg_catalog | PL/pgSQL procedural language
(2 rows)

andrey=# SELECT arenadata_toolkit.tracking_set_relkinds('r,i,t');  
andrey=# SELECT arenadata_toolkit.tracking_set_relstorages('h,a'); 
andrey=# SHOW arenadata_toolkit.tracking_schemas;
                    arenadata_toolkit.tracking_schemas
--------------------------------------------------------------------------
 public,arenadata_toolkit,pg_catalog,pg_toast,pg_aoseg,information_schema  

andrey=# SELECT arenadata_toolkit.tracking_unregister_schema('information_schema'); 
andrey=# SELECT arenadata_toolkit.tracking_unregister_schema('pg_aoseg');           
andrey=# SHOW arenadata_toolkit.tracking_schemas;
     arenadata_toolkit.tracking_schemas
--------------------------------------------
 public,arenadata_toolkit,pg_toast,pg_catalog

andrey=# SELECT arenadata_toolkit.tracking_register_db();                 
andrey=# SELECT arenadata_toolkit.tracking_trigger_initial_snapshot();    

andrey=# SELECT * from arenadata_toolkit.tables_track;    
 relid |                            relname                             | relfilenode |  size  | state | segid | relnamespace | relkind | relstorage | parent_relid
-------+----------------------------------------------------------------+-------------+--------+-------+-------+--------------+---------+------------+--------------
  2840 | pg_toast_2619                                                  |       12542 |      0 | i     |    -1 |           99 | t       | h          |         2619
  2836 | pg_toast_1255                                                  |           0 |      0 | i     |    -1 |           99 | t       | h          |         1255
  2830 | pg_toast_2604                                                  |       12564 |      0 | i     |    -1 |           99 | t       | h          |         2604
  2832 | pg_toast_2606                                                  |       12571 |      0 | i     |    -1 |           99 | t       | h          |         2606
  2838 | pg_toast_2618                                                  |       12600 | 393216 | i     |    -1 |           99 | t       | h          |         2618
  2336 | pg_toast_2620                                                  |       12607 |      0 | i     |    -1 |           99 | t       | h          |         2620
  2834 | pg_toast_2609                                                  |       12616 |      0 | i     |    -1 |           99 | t       | h          |         2609
  2966 | pg_toast_2964                                                  |           0 |      0 | i     |    -1 |           99 | t       | h          |         2964
  2846 | pg_toast_2396                                                  |           0 |      0 | i     |    -1 |           99 | t       | h          |         2396
  5510 | pg_toast_3079                                                  |       12667 |      0 | i     |    -1 |           99 | t       | h          |         3079
  3598 | pg_toast_3596                                                  |       12682 |      0 | i     |    -1 |           99 | t       | h          |         3596
  6092 | pg_toast_5036                                                  |           0 |      0 | i     |    -1 |           99 | t       | h          |         5036
  6233 | pg_toast_6231                                                  |       12732 |      0 | i     |    -1 |           99 | t       | h          |         6231
  9905 | pg_toast_9903                                                  |       12745 |      0 | i     |    -1 |           99 | t       | h          |         9903
  6222 | pg_toast_6220                                                  |       12752 |      0 | i     |    -1 |           99 | t       | h          |         6220
 12202 | pg_toast_12200                                                 |       12765 |      0 | i     |    -1 |           99 | t       | h          |        12200
 12207 | pg_toast_12205                                                 |       12772 |      0 | i     |    -1 |           99 | t       | h          |        12205
 12212 | pg_toast_12210                                                 |       12779 |      0 | i     |    -1 |           99 | t       | h          |        12210
 12217 | pg_toast_12215                                                 |       12786 |      0 | i     |    -1 |           99 | t       | h          |        12215
 12222 | pg_toast_12220                                                 |       12793 |      0 | i     |    -1 |           99 | t       | h          |        12220
 12227 | pg_toast_12225                                                 |       12800 |      0 | i     |    -1 |           99 | t       | h          |        12225
 12232 | pg_toast_12230                                                 |       12807 |      0 | i     |    -1 |           99 | t       | h          |        12230
  2755 | pg_opfamily_oid_index                                          |       10033 |  65536 | i     |    -1 |           11 | i       | h          |         2753
  6058 | pg_statlastshop_classid_objid_staactionname_index              |           0 |      0 | i     |    -1 |           11 | i       | h          |         6056
  6056 | pg_stat_last_shoperation                                       |           0 |      0 | i     |    -1 |           11 | r       | h          |
  2692 | pg_rewrite_oid_index                                           |       10047 |  65536 | i     |    -1 |           11 | i       | h          |         2618
  2677 | pg_authid_oid_index                                            |           0 |      0 | i     |    -1 |           11 | i       | h          |         1260
  2611 | pg_inherits                                                    |       12573 |      0 | i     |    -1 |           11 | r       | h          |
 12214 | pg_toast_12210_index                                           |       10155 |  32768 | i     |    -1 |           99 | i       | h          |        12212
--More--

Поясним некоторые шаги:

  • Для работы трека изменений необходимо включить arenadata_toolkit.so как загружаемую shared_preload_libraries.

  • GUC с именем arenadata_toolkit.tracking_relkinds определяет набор типов таблиц, события по которым отслеживаются. По умолчанию осуществляется мониторинг таких типов таблиц: ritmobM. Это соответствует следующему набору типов:

  RELKIND_RELATION		  'r'		/* ordinary table */
  RELKIND_INDEX			  'i'		/* secondary index */
  RELKIND_TOASTVALUE	  't'		/* for out-of-line values */
  RELKIND_MATVIEW		  'm'		/* materialized view */
  RELKIND_AOSEGMENTS	  'o'		/* AO segment files and eof's */
  RELKIND_AOBLOCKDIR	  'b'		/* AO block directory */
  RELKIND_AOVISIMAP		  'M'		/* AO visibility map */

Как видно из строки с маркером 2, мы сокращаем этот набор до таблиц, индексов и toast. К примеру, вспомогательные aux-таблицы для AO-таблиц не будут попадать в трек изменений.

  • GUC с именем arenadata_toolkit.tracking_relstorages определяет набор типов хранения таблиц. По умолчанию осуществляется мониторинг таких типов таблиц: hac. Это соответствует набору:

  RELSTORAGE_HEAP	'h'
  RELSTORAGE_AOROWS	'a'
  RELSTORAGE_AOCOLS	'c'

Из строки с маркером 3 видно, что этот набор сокращается до heap- и AO-таблиц со строковой организацией хранения.

  • Отображает список схем, которые попадают в выборку из трека изменений по умолчанию.

  • Исключаем из списка интересующих нас схем information_schema.

  • Исключаем из списка интересующих нас схем pg_aoseg.

  • Регистрируем текущую базу как отслеживаемую треком изменений.

  • В целях демонстрации функции tracking_trigger_initial_snapshot мы выставляем признак получения полного трека при следующем запросе.

  • Таким образом, запрос трека из представления tables_track выдаст нам полный срез, о чем говорит признак в каждой записи поля state.

В итоге мы получили полный срез, включающий схемы publicarenadata_toolkitpg_toastpg_catalog, но из которого исключаются колоночные AO-таблицы и все вспомогательные aux-таблицы. Это легко проверить и перенастроить на лету:

andrey=# CREATE TABLE aoro (c1 INT) WITH (appendoptimized=true, orientation=row);
CREATE TABLE

andrey=# CREATE TABLE aoco (c1 INT) WITH (appendoptimized=true, orientation=column);
CREATE TABLE

andrey=# BEGIN; SELECT * FROM arenadata_toolkit.tables_track WHERE relnamespace = 2200; ROLLBACK;
BEGIN
 relid | relname | relfilenode | size | state | segid | relnamespace | relkind | relstorage | parent_relid
-------+---------+-------------+------+-------+-------+--------------+---------+------------+--------------
 16542 | aoro    |       16438 |    0 | a     |    -1 |         2200 | r       | a          |
 16542 | aoro    |       16438 |    0 | a     |     1 |         2200 | r       | a          |
 16542 | aoro    |       16438 |    0 | a     |     2 |         2200 | r       | a          |
 16542 | aoro    |       16438 |    0 | a     |     0 |         2200 | r       | a          |
(4 rows)
)

ROLLBACK

andrey=# SELECT arenadata_toolkit.tracking_set_relstorages('h,a,c');    
andrey=# SELECT arenadata_toolkit.tracking_set_relkinds('r,i,t,o,M');   
andrey=# SELECT arenadata_toolkit.tracking_register_schema('pg_aoseg'); 

andrey=# BEGIN; SELECT * FROM arenadata_toolkit.tables_track WHERE relnamespace IN (2200, 6104); ROLLBACK;
BEGIN
 relid |         relname          | relfilenode | size  | state | segid | relnamespace | relkind | relstorage | parent_relid
-------+--------------------------+-------------+-------+-------+-------+--------------+---------+------------+--------------
 16546 | pg_aovisimap_16542       |       16440 |     0 | a     |    -1 |         6104 | M       | h          |        16542
 16553 | pg_aovisimap_16549       |       16444 |     0 | a     |    -1 |         6104 | M       | h          |        16549
 16551 | pg_aocsseg_16549         |       16443 |     0 | a     |    -1 |         6104 | o       | h          |        16549
 16544 | pg_aoseg_16542           |       16439 |     0 | a     |    -1 |         6104 | o       | h          |        16542
 16542 | aoro                     |       16438 |     0 | a     |    -1 |         2200 | r       | a          |
 16555 | pg_aovisimap_16549_index |       16445 | 32768 | a     |    -1 |         6104 | i       | h          |        16553
 16548 | pg_aovisimap_16542_index |       16441 | 32768 | a     |    -1 |         6104 | i       | h          |        16546
 16549 | aoco                     |       16442 |     0 | a     |    -1 |         2200 | r       | c          |
 16553 | pg_aovisimap_16549       |       16444 |     0 | a     |     0 |         6104 | M       | h          |        16549
 16553 | pg_aovisimap_16549       |       16444 |     0 | a     |     1 |         6104 | M       | h          |        16549
 16553 | pg_aovisimap_16549       |       16444 |     0 | a     |     2 |         6104 | M       | h          |        16549
 16548 | pg_aovisimap_16542_index |       16441 | 32768 | a     |     0 |         6104 | i       | h          |        16546
 16548 | pg_aovisimap_16542_index |       16441 | 32768 | a     |     1 |         6104 | i       | h          |        16546
 16548 | pg_aovisimap_16542_index |       16441 | 32768 | a     |     2 |         6104 | i       | h          |        16546
 16551 | pg_aocsseg_16549         |       16443 |     0 | a     |     0 |         6104 | o       | h          |        16549
 16551 | pg_aocsseg_16549         |       16443 |     0 | a     |     1 |         6104 | o       | h          |        16549
 16551 | pg_aocsseg_16549         |       16443 |     0 | a     |     2 |         6104 | o       | h          |        16549
 16549 | aoco                     |       16442 |     0 | a     |     0 |         2200 | r       | c          |
 16549 | aoco                     |       16442 |     0 | a     |     1 |         2200 | r       | c          |
 16549 | aoco                     |       16442 |     0 | a     |     2 |         2200 | r       | c          |
 16555 | pg_aovisimap_16549_index |       16445 | 32768 | a     |     1 |         6104 | i       | h          |        16553
 16555 | pg_aovisimap_16549_index |       16445 | 32768 | a     |     0 |         6104 | i       | h          |        16553
 16555 | pg_aovisimap_16549_index |       16445 | 32768 | a     |     2 |         6104 | i       | h          |        16553
 16546 | pg_aovisimap_16542       |       16440 |     0 | a     |     1 |         6104 | M       | h          |        16542
 16546 | pg_aovisimap_16542       |       16440 |     0 | a     |     0 |         6104 | M       | h          |        16542
 16546 | pg_aovisimap_16542       |       16440 |     0 | a     |     2 |         6104 | M       | h          |        16542
 16544 | pg_aoseg_16542           |       16439 |     0 | a     |     1 |         6104 | o       | h          |        16542
 16544 | pg_aoseg_16542           |       16439 |     0 | a     |     0 |         6104 | o       | h          |        16542
 16544 | pg_aoseg_16542           |       16439 |     0 | a     |     2 |         6104 | o       | h          |        16542
 16542 | aoro                     |       16438 |     0 | a     |     1 |         2200 | r       | a          |
 16542 | aoro                     |       16438 |     0 | a     |     0 |         2200 | r       | a          |
 16542 | aoro                     |       16438 |     0 | a     |     2 |         2200 | r       | a          |
(32 rows)

ROLLBACK

Обратите внимание, что в строках с маркерами 1-мы вернули колоночные AO-таблицы, карты видимости (RELKIND_AOVISIMAP), основную таблицу метаданных AO-таблиц (RELKIND_AOSEGMENTS) и служебную схему pg_aoseg.

Причем сведения по этим таблицам попали и во вторую выборку, так как мы делали ROLLBACK получения трека изменений!

Реализуем простейший мониторинг метрик

Давайте построим простой, но тем не менее полезный базовый мониторинг на базе нашего решения.

Схема компонентов мониторинга
Схема компонентов мониторинга

Архитектура взаимодействия между компонентами будет следующей:

  • Кластер Arenadata DB с установленным arenadata_toolkit с поддержкой трека изменений.

  • Отдельный инстанс PostgreSQL 16, который будет запрашивать трек каждые 30 секунд, сохранять сырые данные трека и вычислять агрегаты.

  • Инстанс Grafana для сохранения и отображения метрик.

Структура таблиц для хранения данных трека и агрегации данных
Структура таблиц для хранения данных трека и агрегации данных

Для получения метрик от кластера Arenadata DB воспользуемся расширением postgres_fdw, которое установим на кластер PostgreSQL, и сконфигурируем сервер, маппинг пользователей и внешние таблицы:

postgres=# CREATE EXTENSION IF NOT EXISTS postgres_fdw;

CREATE SERVER adb6_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', port '6000', dbname 'andrey');

CREATE USER MAPPING FOR CURRENT_USER
SERVER adb6_server
OPTIONS (user 'adb_track_files', password 'qwerty');

CREATE FOREIGN TABLE public.adb_tables_track (
  relid oid,
  relname name,
  relfilenode oid,
  size bigint,
  state "char",
  segid int,
  relnamespace oid,
  relkind "char",
  relstorage "char",
  parent_relid oid
)
SERVER adb6_server
OPTIONS (schema_name 'arenadata_toolkit', table_name 'tables_track');

CREATE FOREIGN TABLE public.is_initial_snapshot_triggered (is_triggered int)
SERVER adb6_server
OPTIONS (schema_name 'arenadata_toolkit', table_name 'is_initial_snapshot_triggered');

На стороне Arenadata DB настроим пользователя для получения трека со стороны PostgreSQL:

$ psql -c 'CREATE USER adb_track_files WITH ENCRYPTED PASSWORD '\''qwerty'\'';'
$ echo 'host andrey adb_track_files 127.0.0.1/32 md5' >> $MASTER_DATA_DIRECTORY/pg_hba.conf
$ gpstop -u

На стороне PostgreSQL проверим возможность получения трека:

postgres=# BEGIN; SELECT * FROM public.adb_tables_track; ROLLBACK;
 relid |                   relname                   | relfilenode | size  | state | segid | relnamespace | relkind | relstorage | parent_relid
-------+---------------------------------------------+-------------+-------+-------+-------+--------------+---------+------------+--------------
  5002 | gp_distribution_policy                      |       12704 | 32768 | a     |    -1 |           11 | r       | h          |
  6231 | pg_attribute_encoding                       |       12727 | 32768 | a     |    -1 |           11 | r       | h          |
  6105 | pg_appendonly                               |       12717 | 32768 | a     |    -1 |           11 | r       | h          |
  6067 | gp_fastsequence_objid_objmod_index          |       10127 | 65536 | a     |    -1 |           11 | i       | h          |         5043
 16555 | pg_aovisimap_16549_index                    |       16445 | 32768 | a     |    -1 |         6104 | i       | h          |        16553
 16546 | pg_aovisimap_16542                          |       16440 |     0 | a     |    -1 |         6104 | M       | h          |        16542
  5007 | pg_appendonly_relid_index                   |       10126 | 65536 | a     |    -1 |           11 | i       | h          |         6105
 16553 | pg_aovisimap_16549                          |       16444 |     0 | a     |    -1 |         6104 | M       | h          |        16549
 16548 | pg_aovisimap_16542_index                    |       16441 | 32768 | a     |    -1 |         6104 | i       | h          |        16546
 16551 | pg_aocsseg_16549                            |       16443 |     0 | a     |    -1 |         6104 | o       | h          |        16549
 16544 | pg_aoseg_16542                              |       16439 |     0 | a     |    -1 |         6104 | o       | h          |        16542
 16542 | aoro                                        |       16438 |     0 | a     |    -1 |         2200 | r       | a          |
  6236 | pg_attribute_encoding_attrelid_index        |       10137 | 65536 | a     |    -1 |           11 | i       | h          |         6231
 16549 | aoco                                        |       16442 |     0 | a     |    -1 |         2200 | r       | c          |
  6103 | gp_policy_localoid_index                    |       10121 | 65536 | a     |    -1 |           11 | i       | h          |         5002
  5043 | gp_fastsequence                             |       12719 | 32768 | a     |    -1 |           11 | r       | h          |
  6237 | pg_attribute_encoding_attrelid_attnum_index |       10138 | 65536 | a     |    -1 |           11 | i       | h          |         6231
  5002 | gp_distribution_policy                      |       12704 | 32768 | a     |     1 |           11 | r       | h          |
  6231 | pg_attribute_encoding                       |       12727 | 32768 | a     |     1 |           11 | r       | h          |
  6105 | pg_appendonly                               |       12717 | 32768 | a     |     1 |           11 | r       | h          |
  6067 | gp_fastsequence_objid_objmod_index          |       10127 | 65536 | a     |     1 |           11 | i       | h          |         5043
 16555 | pg_aovisimap_16549_index                    |       16445 | 32768 | a     |     1 |         6104 | i       | h          |        16553
 16546 | pg_aovisimap_16542                          |       16440 |     0 | a     |     1 |         6104 | M       | h          |        16542
  5007 | pg_appendonly_relid_index                   |       10126 | 65536 | a     |     1 |           11 | i       | h          |         6105
 16553 | pg_aovisimap_16549                          |       16444 |     0 | a     |     1 |         6104 | M       | h          |        16549
 16548 | pg_aovisimap_16542_index                    |       16441 | 32768 | a     |     1 |         6104 | i       | h          |        16546
 16551 | pg_aocsseg_16549                            |       16443 |     0 | a     |     1 |         6104 | o       | h          |        16549
 16544 | pg_aoseg_16542                              |       16439 |     0 | a     |     1 |         6104 | o       | h          |        16542
 16542 | aoro                                        |       16438 |     0 | a     |     1 |         2200 | r       | a          |
  6236 | pg_attribute_encoding_attrelid_index        |       10137 | 65536 | a     |     1 |           11 | i       | h          |         6231
 16549 | aoco                                        |       16442 |     0 | a     |     1 |         2200 | r       | c          |
  6103 | gp_policy_localoid_index                    |       10121 | 65536 | a     |     1 |           11 | i       | h          |         5002
  5043 | gp_fastsequence                             |       12719 | 32768 | a     |     1 |           11 | r       | h          |
  6237 | pg_attribute_encoding_attrelid_attnum_index |       10138 | 65536 | a     |     1 |           11 | i       | h          |         6231
  5002 | gp_distribution_policy                      |       12704 | 32768 | a     |     2 |           11 | r       | h          |
  6231 | pg_attribute_encoding                       |       12727 | 32768 | a     |     2 |           11 | r       | h          |
  6105 | pg_appendonly                               |       12717 | 32768 | a     |     2 |           11 | r       | h          |
  6067 | gp_fastsequence_objid_objmod_index          |       10127 | 65536 | a     |     2 |           11 | i       | h          |         5043
 16555 | pg_aovisimap_16549_index                    |       16445 | 32768 | a     |     2 |         6104 | i       | h          |        16553
 16546 | pg_aovisimap_16542                          |       16440 |     0 | a     |     2 |         6104 | M       | h          |        16542
  5007 | pg_appendonly_relid_index                   |       10126 | 65536 | a     |     2 |           11 | i       | h          |         6105
:

Создадим служебные таблицы для сохранения "сырых" данных трека:

CREATE TABLE public.adb_tables_track_snapshot (relid oid,
            relname name,
            relfilenode oid,
            size bigint,
            state "char",
            segid int,
            relnamespace oid,
            relkind name,
            relstorage name,
            parent_relid oid,
            ts timestamp);

CREATE UNIQUE INDEX relfilenode_segid_relid_idx ON public.adb_tables_track_snapshot (relfilenode, segid);

CREATE OR REPLACE VIEW public.table_sizes AS
WITH RECURSIVE t AS (
  SELECT
    relid as toprelid,
    relid,
    relname,
    relkind,
    relstorage,
    relnamespace,
    size,
    segid
  FROM public.adb_tables_track_snapshot
  WHERE
  parent_relid IS NULL
  AND
  state <> 'd'
  UNION
  SELECT
    c.toprelid,
    p.relid,
    c.relname,
    c.relkind,
    c.relstorage,
    c.relnamespace,
    p.size,
    p.segid
  FROM public.adb_tables_track_snapshot p
  INNER JOIN t c ON c.relid = p.parent_relid
  WHERE
  state <> 'd'
)
SELECT toprelid, relname, relkind, relstorage, relnamespace, SUM(size) AS total_size
FROM t
GROUP BY toprelid, relname, relkind, relstorage, relnamespace;

Для сбора и обработки данных трека будем использовать такую функцию:

CREATE OR REPLACE FUNCTION public.collect_track()
RETURNS void
LANGUAGE plpgsql
AS $function$
BEGIN

    DROP TABLE IF EXISTS adb_tables_track_snapshot_temp;

    CREATE TEMP TABLE adb_tables_track_snapshot_temp AS
    SELECT
        relid,
        relname,
        relfilenode,
        size,
        state,
        segid,
        relnamespace,
        case
            when relkind='r' then 'ordinary table'
            when relkind='i' then 'secondary index'
            when relkind='s' then 'sequence object'
            when relkind='t' then 'toast'
            when relkind='v' then 'view'
            when relkind='m' then 'materialized view'
            when relkind='c' then 'composite type'
            when relkind='f' then 'foreign table'
            when relkind='p' then 'partitioned table'
            when relkind='I' then 'partitioned index'
            when relkind='o' then 'AO segment files'
            when relkind='b' then 'AO block directory'
            when relkind='M' then 'AO visibility map'
            else 'unknown relkind'
        end as relkind,
        case
            when relstorage='h' then 'heap storage'
            when relstorage='a' then 'append only storage'
            when relstorage='c' then 'append only column storage'
            when relstorage='v' then 'virtual storage'
            when relstorage='x' then 'stored externally'
            when relstorage='f' then 'stored in another server'
            else 'unknown relstorage'
        end as relstorage,
        parent_relid,
        current_timestamp AS ts
    FROM adb_tables_track;

	IF EXISTS (SELECT 1 FROM is_initial_snapshot_triggered WHERE is_triggered = 1) 
	THEN

		TRUNCATE TABLE adb_tables_track_snapshot;

		INSERT INTO adb_tables_track_snapshot
		SELECT *
		FROM adb_tables_track_snapshot_temp
		WHERE relfilenode <> 0; 

	ELSE

        INSERT INTO adb_tables_track_snapshot
		SELECT
			relid,
			relname,
			relfilenode,
			size,
			state,
			segid,
			relnamespace,
			relkind,
			relstorage,
			parent_relid,
			current_timestamp AS ts
		FROM adb_tables_track_snapshot_temp
		WHERE relfilenode <> 0
		ON CONFLICT (relfilenode, segid) 
		DO UPDATE SET
		  size = EXCLUDED.size,
		  ts = EXCLUDED.ts,
		  state = EXCLUDED.state;

	END IF;

END$function$;

Важно отметить некоторые моменты:

  1. Условием WHERE relfilenode <> 0 мы исключаем из выборки разделяемые между разным кластерами (в терминах PostgreSQL) системные таблицы (shared relations), для которых поле relfilenode всегда равно 0. Например, это таблицы pg_typepg_attributepg_databasepg_tablespace и ряд других. Ограничений на отслеживание трека для них нет, но обработка с точки зрения получения трека и актуализации данных в целевой базе (в нашем случае на стороне PostgreSQL) усложнила бы пример. Идентификатор relfilenode для таких таблиц будет равен 0, и идентифицировать такие записи в треке пришлось бы по паре segid и relid, а не по relfilenodesegid (строка с маркером 2).

  2. Для обновления информации по таблицам задействована конструкция INSERT INTO … ON CONFLICT (…​) DO UPDATE SET. Она позволит актуализировать данные в срезе исходя из полученных изменений в треке.

  3. Если на отдающей стороне был запрошен начальный снимок, то сохраняется полный начальный срез (initial snapshot).

Перед получением первоначального снимка вызовем на стороне Arenadata DB функцию arenadata_toolkit.tracking_trigger_initial_snapshot():

andrey=# SELECT arenadata_toolkit.tracking_trigger_initial_snapshot();
 tracking_trigger_initial_snapshot
-----------------------------------
 t
(1 row)

Проверим получение и актуализацию трека изменений:

postgres=# SELECT public.collect_track();
NOTICE:  table "adb_tables_track_snapshot_temp" does not exist, skipping
 collect_track
---------------

(1 row)

postgres=# SELECT * FROM table_sizes WHERE relnamespace = 2200;

 toprelid |         relname         |    relkind     |         relstorage         | relnamespace | total_size
----------+-------------------------+----------------+----------------------------+--------------+------------
    16542 | aoro                    | ordinary table | append only storage        |         2200 |     131072
    16549 | aoco                    | ordinary table | append only column storage |         2200 |     131072
(2 rows)

На стороне Arenadata DB сделаем вставку в таблицу aoro, а таблицу aoco удалим:

andrey=# INSERT INTO aoro SELECT generate_series(1,100000000);
INSERT 0 100000000

andrey=# DROP TABLE aoco;

andrey=# BEGIN; SELECT * from arenadata_toolkit.tables_track; ROLLBACK;
BEGIN
 relid |    relname     | relfilenode |   size    | state | segid | relnamespace | relkind | relstorage | parent_relid
-------+----------------+-------------+-----------+-------+-------+--------------+---------+------------+--------------
       |                |       16442 |         0 | d     |    -1 |              |         |            |
       |                |       16443 |         0 | d     |    -1 |              |         |            |
       |                |       16444 |         0 | d     |    -1 |              |         |            |
       |                |       16445 |         0 | d     |    -1 |              |         |            |
 16544 | pg_aoseg_16542 |       16439 |     32768 | a     |    -1 |         6104 | o       | h          |        16542
       |                |       16442 |         0 | d     |     2 |              |         |            |
       |                |       16443 |         0 | d     |     2 |              |         |            |
       |                |       16444 |         0 | d     |     2 |              |         |            |
       |                |       16445 |         0 | d     |     2 |              |         |            |
 16544 | pg_aoseg_16542 |       16439 |     32768 | a     |     2 |         6104 | o       | h          |        16542
 16542 | aoro           |       16438 | 333872320 | a     |     2 |         2200 | r       | a          |
       |                |       16442 |         0 | d     |     0 |              |         |            |
       |                |       16443 |         0 | d     |     0 |              |         |            |
       |                |       16444 |         0 | d     |     0 |              |         |            |
       |                |       16445 |         0 | d     |     0 |              |         |            |
 16544 | pg_aoseg_16542 |       16439 |     32768 | a     |     0 |         6104 | o       | h          |        16542
 16542 | aoro           |       16438 | 333860888 | a     |     0 |         2200 | r       | a          |
       |                |       16442 |         0 | d     |     1 |              |         |            |
       |                |       16443 |         0 | d     |     1 |              |         |            |
       |                |       16444 |         0 | d     |     1 |              |         |            |
       |                |       16445 |         0 | d     |     1 |              |         |            |
 16544 | pg_aoseg_16542 |       16439 |     32768 | a     |     1 |         6104 | o       | h          |        16542
 16542 | aoro           |       16438 | 333829336 | a     |     1 |         2200 | r       | a          |
(23 rows)

Актуализируем метрики и проверим изменения (для сокращения количества вывода отфильтруем по схеме public и relnamespace = 2200):

postgres=# SELECT public.collect_track();
 collect_track
---------------

(1 row)

postgres=# SELECT * FROM table_sizes WHERE relnamespace = 2200;
toprelid |         relname         |    relkind     |         relstorage         | relnamespace | total_size
----------+-------------------------+----------------+----------------------------+--------------+------------
    16542 | aoro                    | ordinary table | append only storage        |         2200 | 1001824688
(1 row)

Как видно из актуализированного трека размер таблицы aoro обновился, а таблица aoco пропала из выборки, так как была удалена.

Для того чтобы периодически запрашивать трек и актуализировать метрики, предлагаю воспользоваться расширением pg_cron:

postgres=# CREATE EXTENSION IF NOT EXISTS pg_cron;
SELECT cron.schedule('vacuum_track', '0 10 * * *', 'VACUUM adb_tables_track_snapshot');
SELECT cron.schedule('collect_track', '30 seconds', 'SELECT collect_track()');

postgres=# SELECT * FROM cron.job_run_details ORDER BY start_time DESC LIMIT 5;
 jobid | runid | job_pid | database | username |        command         |  status   | return_message |          start_time           |           end_time
-------+-------+---------+----------+----------+------------------------+-----------+----------------+-------------------------------+-------------------------------
     2 |     4 |  185730 | postgres | andrey   | SELECT collect_track() | succeeded | 1 row          | 2025-01-24 18:21:17.044724+03 | 2025-01-24 18:21:17.096463+03
     2 |     3 |  185688 | postgres | andrey   | SELECT collect_track() | succeeded | 1 row          | 2025-01-24 18:20:47.044481+03 | 2025-01-24 18:20:47.103201+03
     2 |     2 |  185660 | postgres | andrey   | SELECT collect_track() | succeeded | 1 row          | 2025-01-24 18:20:17.044327+03 | 2025-01-24 18:20:17.097553+03
     2 |     1 |  185617 | postgres | andrey   | SELECT collect_track() | succeeded | 1 row          | 2025-01-24 18:19:47.043519+03 | 2025-01-24 18:19:47.102101+03

Рисуем дашборды Grafana

Далее я сконфигурировал источник для метрик в виде PostgreSQL-базы и создал базовые дашборды Grafana.

Первая группа с детализацией по таблицам в виде общего списка, упорядоченного по размеру.

Дашборды со списками топ 10 и топ 200 таблиц по размерам
Дашборды со списками топ 10 и топ 200 таблиц по размерам

Вторая группа с аналитикой по типам таблиц (relkind), типу хранения (relstorage), распределению по схемам (relnamespace) и распределению данных по сегментам крайне полезна для отслеживания перекосов (data skew):

Дашборды с аналитикой по типам и распределению по сегментам
Дашборды с аналитикой по типам и распределению по сегментам

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

andrey=# CREATE TABLE aoro_part (id INT, part INT) WITH (appendoptimized=true, orientation=row) DISTRIBUTED BY (part);
CREATE TABLE

andrey=# INSERT INTO aoro_part SELECT generate_series(1,10000), 1;
INSERT 0 10000
andrey=# INSERT INTO aoro_part SELECT generate_series(1,9000000), 2;
INSERT 0 1000000

После обновления трека можно увидеть, что таблица aoro_part стала занимать ощутимо больше места.

Трекинг изменений после вставки в таблицу aoro_part
Трекинг изменений после вставки в таблицу aoro_part

Также можно увидеть перекос по данным на первом сегменте (seg0).

Перекос данных между сегментами таблицы aoro_part
Перекос данных между сегментами таблицы aoro_part

Таким образом, при подобной обработке данных наше решение может быть альтернативой таких представлений, как gp_skew_coefficients и, возможно, gp_skew_idle_fractions.

Комментарии (0)