За пределами EXPLAIN: как увидеть выполнение запроса вживую в распределённой СУБД


Представьте, что ваш запрос в Greenplum внезапно завис. Есть план выполнения, но что именно сейчас происходит, непонятно. Перекос данных? Spill на диск? В итоге вы перезапускаете запрос наугад: инструментов для живого наблюдения попросту нет.
Всем привет! Я Алексей Рожок, разработчик ClickHouse в Yandex Cloud. Летом 2026 года я стажировался в Greenplum/Cloudberry и реализовывал проект, который помогает увидеть весь путь запроса. В статье покажу, как достучаться до процессов на всех хостах кластера и почему сбор метрик пришлось вынести с координатора в отдельный сервис YAGPCC.
Чего не показывает EXPLAIN
SQL — язык декларативный: вы пишете, что хотите получить, а как именно это сделать, решает СУБД. В PostgreSQL этот выбор можно подсмотреть через EXPLAIN: вот план, вот узлы, вот оценки. Но пока тяжёлый запрос крутится часами, главный вопрос — «На каком шаге мы сейчас и почему так долго?» — остаётся без ответа. EXPLAIN ANALYZE отдаёт статистику только постфактум, а оценки планировщика часто сильно расходятся с реальностью.
Для ванильного PostgreSQL уже есть решения, которые показывают статистику запроса, пока он выполняется. Но в мире распределённых MPP-СУБД запрос, разбитый на части, может идти параллельно на сотнях узлов кластера. Готовых решений для такого случая нет, поэтому пришлось написать своё.
Расширение pg_query_state для Apache Cloudberry строит дерево плана по ходу выполнения запроса и периодически обновляет статистику в каждом узле. По сути, мы получаем честный live-observability: видно, чем запрос занят прямо сейчас. Ниже расскажу, как достучаться до процессов запроса на всех сегментах кластера и почему собирать их статистику пришлось отдельным сервисом.
Как Cloudberry выполняет запросы
Объяснить архитектуру Cloudberry за пару минут сложно, но я попытаюсь. Кластер состоит из координатора и сегментов. Координатор принимает запрос, строит план, рассылает его по сегментам и собирает итоговый результат.
Сегмент — независимый экземпляр СУБД на основе PostgreSQL в архитектуре shared-nothing. Он владеет собственным шардом данных на локальном диске, не имеет доступа к данным других сегментов и получает от координатора план запроса для своей части. Физически сегменты размещаются на сегмент-хостах, обычно по одному на ядро CPU или диск.
Для примера создадим таблицу test:
create table test (
id serial primary key,
val numeric,
txt text,
created timestamp default now()
); Выполним простой запрос:
select * from test a where a.val > 10Тогда план нашего выполнения в Cloudberry будет таким:
postgres=# explain select * from test a where a.val > 10;
QUERY PLAN
--------------------------------------------------------------------------------------
Gather Motion 4:1 (slice1; segments: 4) (cost=0.00..1620.53 rows=4999950 width=57)
-> Seq Scan on test a (cost=0.00..667.21 rows=1249988 width=57)
Filter: (val > '10'::numeric)
Optimizer: GPORCA
(4 rows)На кластере из четырёх сегментов это выглядит так:

Каждый сегмент последовательно сканирует свою часть таблицы (Seq Scan) и применяет фильтр. Узел Gather Motion собирает результаты с четырёх сегментов на координаторе, поэтому в плане 4:1.
Слайс — это горизонтальный срез плана выполнения, ограниченный точками обмена данными (Motion nodes). Когда оптимизатор строит план для распределённого запроса, он разбивает его на слайсы — логические единицы работы. Слайс выполняется параллельно на всех сегментах в рамках выделенного процесса (QE, Query Executor). Внутри слайса сегменты работают независимо друг от друга и обрабатывают только свои локальные данные.
Между слайсами данные передают Motion-узлы (Gather, Redistribute, Broadcast): они пересылают кортежи по сети между сегментами или на координатор. Слайсы пронумерованы, в плане запроса их границы отмечены отступами и пометками sliceN.
Как достучаться до сегментов

Координатор знает, на каких сегментах выполняется запрос. Но координатор и сегменты — отдельные процессы, которые физически могут находиться на разных машинах. При этом сами сегменты тоже могут быть раскиданы по разным сегмент-хостам.
Чтобы собрать статистику во время выполнения запроса, её нужно снять с каждого активного процесса и куда-то отправить. Возникают как минимум два вопроса: как заставить сегменты собирать данные и куда их отправлять.
В PostgreSQL на одной машине до процесса можно достучаться обычным сигналом прерывания. Здесь это не сработает: сигнал не дойдёт до процесса на другом хосте. Поэтому я использовал диспетчеризацию — механизм, которым координатор рассылает SQL-команды на сегменты:
Отправляем сигнал координатору. В ответ он возвращает массив пар (
segid, pid) для всех активных сегментов.Запускаем SQL-запрос на всех сегментах и передаём ему этот массив аргументом.
Внутри каждого сегмента этот запрос выполняет отдельный новый процесс. Он оставляет из массива только нужные пары — те, где
segidсовпадает с номером текущего сегмента.Локальный бэкенд рассылает сигналы всем активным процессам в пределах текущего сегмент-хоста.
Таким образом, мы смогли прервать процессы на всех нужных сегментах прямо с координатора.
Почему координатор не справится в одиночку

Остаётся второй вопрос: куда отправлять статистику. Первым напрашивается координатор, но посчитаем, во что это обойдётся на большом кластере.
Представим крупный продакшн-кластер: 300 сегментов на нескольких десятках физических хостов. Аналитик запускает сложный отчётный запрос: цепочка из нескольких JOIN, пара подзапросов, оконные функции, агрегации с GROUP BY. Оптимизатор разбивает план на 25 слайсов. Значит, одновременно на сегментах работают 300 × 25 = 7500 процессов — каждый обслуживает ровно один слайс на своём сегменте.
Допустим, мы собираем runtime-статистику с каждого такого процесса. Внутри одного процесса строится собственное дерево плана: пусть в нём порядка 20 узлов, как в типовом плане с несколькими Scan, Join, Aggregation и Motion-узлами. Для каждого узла нужно передать около 32 полей: фактическое число полученных и отброшенных строк, время ожидания ввода-вывода, количество spill-файлов, объём использованной памяти, прогноз до завершения и другие. Не умаляя общности, будем считать, что каждое поле — это 8-байтовое число с плавающей точкой.
Тогда наши гипотетические данные таковы:
размер одного узла: 32 поля × 8 байт = 256 байт;
размер дерева с одного процесса: 20 узлов × 256 байт = 5120 байт (5 КБ);
суммарный объём данных со всего кластера за один сбор: 7500 процессов × 5 КБ ≈ 38 МБ.
Раз в секунду, а то и чаще, координатор:
Принимает 7500 входящих сообщений. Это 7500 системных вызовов
read(), прерываний и переключений контекста, которые крадут процессорное время у основного цикла обработки запросов.Десериализует каждое сообщение и восстанавливает из него 20-узловое дерево.
Сливает 7500 частичных деревьев в одно общее и агрегирует статистику по каждому узлу плана. Сложность слияния —
, где
— число процессов,
— число узлов. При 7500 процессах и 20 узлах это 150 000 операций слияния за один такт сбора.
И это только один запрос. В час пик на реальном кластере одновременно могут выполняться два-три таких отчётных запроса и десятки более лёгких. Тогда 7500 процессов легко превращаются в 15 000–20 000, а объём данных за один сбор переваливает за 100 МБ. В таком режиме координатор полностью занят задачей, для которой его не проектировали. Задержки растут, пропускная способность падает, кластер деградирует — и всё ради красивого дашборда.
Поэтому я отказался от прямой доставки статистики на координатор и вынес сбор и агрегацию в отдельный сервис YAGPCC, изолированный от СУБД.
Решение: агенты YAGPCC на каждом хосте

На каждом хосте кластера работает написанный на Go агент. Он слушает UDS, и процессы сегментов сбрасывают туда сырую статистику без предобработки. Агенты на сегмент-хостах временно хранят данные в памяти; у хранилищ есть фоновая сборка мусора и экспорт метрик в Prometheus.
Собирают статистику по требованию через HTTP-ручку, по протоколу gRPC с Protobuf. Мастер-агент агрегирует всё в одно дерево плана и обновляет его атомарно. Сбор обходится дёшево: если пакет потерялся, просто перезапрашиваем его.

Нагрузка на YAGPCC-мастер начинает зависеть исключительно от количества хостов — величины, которая на два-три порядка меньше числа процессов. Асимптотически это , где
— число хостов в кластере.
Что даёт сбор через агентов?
Горизонтальное масштабирование больше не ломает observability. Если добавить 100 новых сегментов на существующие хосты, нагрузка на мастер-агент не изменится. Добавили новые хосты? Нагрузка выросла линейно и предсказуемо, а не мультипликативно.
Сложность запроса перестала быть проблемой. Раньше 30 слайсов вместо пяти означали шестикратный рост нагрузки на сборщик. Теперь число слайсов влияет только на локальные агенты на каждом хосте, а до мастер-агента это не доходит.
Система стала предсказуемой. Вместо скачкообразной нагрузки, зависящей от того, сколько и каких запросов одновременно выполняется в кластере, YAGPCC-мастер получает стабильный поток из N сообщений за такт.
Координатор БД изолирован. Он не тратит ресурсы на сбор телеметрии, не получает лавину сообщений и продолжает выполнять свою основную работу — планировать, диспетчеризовать и собирать финальные результаты запросов.
По сути мы перенесли вычислительную сложность агрегации с центрального сборщика, который стал бы бутылочным горлышком, на агентов: их много, и каждый работает только со своими данными. Это классический паттерн map-reduce наоборот: reduce выполняется локально на каждом хосте, а мастер-агенту остаётся только сделать финальный merge небольшого числа уже агрегированных результатов.
Дерево плана в реальном времени
Я записал видео, на котором видно, как это выглядит.
На скриншотах видно дерево плана выполнения: прогресс каждого узла обновляется в реальном времени. Любой узел можно раскрыть и посмотреть статистику по каждому сегменту.


Что дальше
Интеграция pg_query_state в Apache Cloudberry и YAGPCC показала, что live-observability в распределённых СУБД — решаемая инженерная задача. Сейчас фича только начинает свой путь, и ей ещё предстоит пережить испытания в проде. Именно там, в боях с реальными нагрузками, мы поймём, насколько хорошо архитектура выдерживает большие объёмы данных, и какие узкие места ещё предстоит оптимизировать.
Если вы тоже следите за запросами в Greenplum или Cloudberry, расскажите в комментариях, какими инструментами пользуетесь и чего вам в них не хватает. А если вам интересно следить за тем, что мы делаем внутри Yandex Cloud — присоединяйтесь к нашему каналу!
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.