GREENGAGE SHRINK

ggrebalance: Часть 1. Shrink

29.05.2026
В статье рассматривается shrink кластера Greengage DB с использованием ggrebalance: архитектура утилиты, FSM-подход, безопасное перераспределение данных через INSERT, сравнение с CTAS, поддержка rollback и результаты производительных тестов.
Александр Кондаков
Александр Кондаков
C Developer

Введение

Кластер Greengage DB, как и любое распределенное хранилище, не является статичной системой. Используемые объемы данных могут как расти, так и сокращаться, для оптимизации нагрузок может появиться необходимость изменить степень параллелизма системы. Оборудование может требовать замены, либо нужна полноценная миграция инфраструктуры на новые машины. Все эти явления и факторы требуют управляемых изменений в топологии кластера: в числе сегментов, их распределении по хостам, используемой стратегии зеркалирования.

Встроенный набор инструментов Greengage предлагает отдельные решения перечисленных задач: gpexpand для расширения кластера, gpmovemirrors для физического перемещения реплик и т.д. Использование их в кластерах, состоящих из сотен сегментов, вручную или через самописные скрипты — очень трудоемкий процесс, не защищенный от ошибок при распределении данных, выводе сегментов из кластера и перемещении зеркал и требующий тщательного контроля. Более того, в стандартном наборе утилит отсутствует поддержка операции уменьшения (шринк) кластера, а также ситуаций изменения топологии, затрагивающих несколько измерений одновременно (уменьшить число сегментов, добавить новый хост, вывести из использования другой и поменять стратегию зеркалирования). Говоря про шринк, стоит отметить существование решения gpshrink от Cloudberry DB, по реализации являющегося калькой обратной шринку операции расширения gpexpand. Однако его использование также должно быть строго контролируемым, так как в случае шринка производятся необратимые изменения конфигурации кластера.

Описанные пробелы в функционале существующего инструментария заполняет разрабатываемая нами кластерная утилита ggrebalance. ggrebalance — это способ контролируемо реализовывать сценарии изменения топологии Greengage-кластера в безопасной (с обработкой ошибок), реентерабельной (продолжение прерванной операции) манере и с возможностью обратить последовательность действий вспять.

В первом релизе утилиты поддерживаются следующие сценарии:

1: Добавление нового хоста без изменения числа primary-сегментов

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

Добавление нового хоста
Рис. 1. Добавление нового хоста
2: Декомиссия хоста без изменения числа сегментов

Нужно вывести из эксплуатации хост с переносом его сегментов на оставшиеся узлы.

Декомиссия хостов
Рис. 2. Декомиссия хостов
3: Декомиссия хоста с уменьшением количества сегментов

Хост и функционирующие на нем сегменты убираются из Greengage-кластера.

Шринк сегментов с декомиссией хостов
Рис. 3. Шринк сегментов с декомиссией хостов
4: Полная миграция на другой набор хостов (кроме координатора и его реплики)

Целый кластер переносится на новый набор машин, например, в ходе миграции дата-центра. Число сегментов неизменно.

Миграция на новые хосты
Рис. 4. Миграция на новые хосты
5: Перенос на другие хосты с изменением числа сегментов

Как и в сценарии 4, только можно изменить степень параллельной обработки (на данный момент поддерживается только шринк).

6: Изменение стратегии зеркалирования

При изменении набора хостов или количества сегментов можно заодно сменить тип зеркалирования. Обычно для Greengage принято использовать один из двух: либо grouped — когда все зеркала одной primary-группы расположены вместе на отдельном хосте, либо spread — зеркала primary-группы распределены на множестве узлов.

Изменение стратегии зеркалирования
Рис. 5. Изменение стратегии зеркалирования
7: Шринк кластера

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

Шринк кластера
Рис. 6. Шринк кластера

В данной серии статей мы подсвечиваем отдельные аспекты и проблемы масштабирования аналитических СУБД и описываем, как утилита ggrebalance способна их решить в контексте эксплуатации MPP DBMS Greengage. И в первой статье представлен разбор операции сокращения числа сегментов кластера — shrink.

Часть 1. Shrink кластера

Шринк — операция по сокращению числа primary-сегментов и соответствующих им зеркал в Greengage-кластере. Ее суть заключается в перераспределении пользовательских данных с выводимых сегментов на остальные, сохраняя изначальные свойства: индексы, тип распределения (replicated, hashed), иерархические отношения, зависимости и т.д. Это делает реализацию процедуры шринка принципиально отличной от реализации уже имеющейся утилиты gpexpand.

1.1 Как gpexpand работает с таблицами

Для понимания, что ggrebalance делает во время шринка, полезно будет в общих чертах рассмотреть действия gpexpand по добавлению новых сегментов (Рис. 7). На первом этапе gpexpand подготавливает новые сегменты: берет блокировку на возможность обновления каталога, тем самым предотвращая создание таблиц на старом числе сегментов, переносит бэкап координатора в места расположения новых сегментов (каталог координатора становится "шаблоном" для новых сегментов), поднимает их, обновляет gp_segment_configuration (на число сегментов из этого отношения ориентируются все подсистемы, реализующие распределенную работу), отпускает блокировку каталога, и сохраняет в служебной таблице список существующих отношений на основе снимка pg_class всех баз данных.

Работа gpexpand
Рис. 7. Работа gpexpand

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

На втором этапе происходит перераспределение данных через ALTER TABLE …​ EXPAND: для каждой таблицы из подготовленного ранее списка создается временное отношение с обновленным распределением, на старом наборе сегментов запускается сканирование исходной таблицы и ее перераспределение на все сегменты со вставкой во временное отношение. Чтобы не менять свойства старого отношения и просто подвязать к нему перераспределенные данные, ALTER TABLE меняет местами relfilenode нового отношения с relfilenode старого в pg_class, и в конце временное отношение удаляется. Такой метод перераспределения данных называется CTAS-подход, и является достаточно надежным способом реорганизации данных в Greengage (пользователи могут перераспределять свои таблицы через команду ALTER TABLE …​ SET WITH (reorganize=true)). Однако у CTAS-метода есть некоторые недостатки. Во-первых, в данном случае происходит дублирование данных отношения — для больших кластеров это означает кратное увеличение требований к дисковому пространству. Во-вторых, создание нового отношения всякий раз увеличивает счетчик OID, тем самым приближаясь к oid wraparound, что не особо критично, но хотелось бы этого избежать. Условно продемонстрировать создание промежуточного отношения в CTAS-подходе можно, вручную создав таблицу на меньшем числе сегментов и выполнив CTAS во временную таблицу:

create extension gp_debug_numsegments;
select gp_debug_set_create_table_default_numsegments(2);

create table t1 (i int, j int) distributed by (i);
insert into t1 select i, i from generate_series(1,100) i;

select * from gp_distribution_policy where localoid='t1'::regclass;
 localoid | policytype | numsegments | distkey | distclass
----------+------------+-------------+---------+-----------
    19086 | p          |           2 | 1       | 10054
(1 row)

select gp_debug_reset_create_table_default_numsegments();

explain (costs off) create temp table t_expanded as select * from t1;
                   QUERY PLAN
------------------------------------------------
 Redistribute Motion 2:3  (slice1; segments: 2)
   Hash Key: i
   ->  Seq Scan on t1
 Optimizer: Postgres-based planner
(4 rows)

select * from gp_distribution_policy where localoid='t_expanded'::regclass;
 localoid | policytype | numsegments | distkey | distclass
----------+------------+-------------+---------+-----------
    19097 | p          |           3 | 1       | 10054
(1 row)

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

И третий недостаток: в конце ALTER TABLE …​ EXPAND происходит полная перестройка индексов. Это дополнительная нагрузка на I/O и время операции, пропорциональное числу и размерам индексов.

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

1.2 Альтернативный подход: INSERT вместо CTAS

В ggrebalance для шринка таблиц применяется альтернативный подход, которым было доработано ядро СУБД — перераспределение данных через целевой INSERT, реализованный во введенной команде ALTER TABLE <name> REBALANCE <target_segment_count>. Она покрывает как случай расширения (с CTAS в релизе 1.0), так и шринк, о котором идет речь в данной статье. В случае сокращения числа сегментов вместо создания промежуточного отношения, INSERT-подход просто переливает данные с выводимых сегментов на оставшиеся посредством выполнения запроса (планировщик был также доработан для поддержки случая шринка, вне ALTER TABLE …​ REBALANCE план будет отличаться):

select * from gp_distribution_policy where localoid='t1'::regclass;
 localoid | policytype | numsegments | distkey | distclass
----------+------------+-------------+---------+-----------
    19112 | p          |           3 | 1       | 10054
(1 row)

explain (costs off) insert into t1 select * from t1 where gp_segment_id in (2);
                         QUERY PLAN
-------------------------------------------------------------
 Insert on t1
   ->  Redistribute Motion 1:2  (slice1; segments: 1)
         Hash Key: t1_1.i
               ->  Seq Scan on t1 t1_1
                    Filter: (gp_segment_id = ANY'{2}'::integer[])
 Optimizer: Postgres-based planner
(6 rows)

При реализации команды ALTER TABLE …​ REBALANCE добавлены изменения в планировщике для случая шринка. Во-первых, изменено количество сегментов распределения при вставке на целевое, чтобы создать конфликт распределений отношения вставки и подплана INSERT. Планировщик, анализируя конфликт, добавляет Redistribute Motion. Во-вторых, чтобы напрямую диспетчеризировать сканирование t1 на сегменты шринка, добавляется предикат gp_segment_id IN ({set of shrunk content IDs}), указывающий на место выполнения сканирования. Тогда первый слайс выполняется на выводимых сегментах, а корневой слайс, где выполняется вставка, порождается уже на целевых сегментах. Таким образом, решение проблемы перераспределения со стороны INSERT позволяет избежать ненужного промежуточного дублирования данных (кроме, очевидно, кортежей с удаляемых сегментов) и перестроения индексов.

Если сравнивать два подхода, то разница видна уже на концептуальном примере. Пусть имеется кластер с 6 сегментами, и происходит шринк до 4. Для таблицы размером в 600 ГБ, равномерно распределенной по 6 сегментам (~100 ГБ на сегмент), при шринке CTAS-подход создал бы временную копию всей таблицы (600 ГБ новых данных на диске). INSERT-подход переместит лишь строки с сегментов 4 и 5, то есть около 200 ГБ. Временно (до вывода сегмента из кластера) эти данные будут присутствовать в двух экземплярах, но расходуя гораздо меньше ресурсов.

1.3 Реализация в виде конечного автомата

Упомянутый ранее gpshrink применяет простой, императивный подход к реализации (написанный одним Python-файлом по аналогии с gpexpand). Используется простой if/elif/else control flow, прогресс по сути нумерованный список шагов, без формальной валидации допустимых переходов между ними. ggrebalance, в свою очередь, вводит больше контроля над процессом шринка. Операция шринка в ggrebalance реализована в виде детерминированного конечного автомата (finite-state machine, FSM) на основе Python-библиотеки transitions с различными типами состояний: эфемерными (нигде не сохраняются, нужны для общей реализации переходов, но не важны для самой логики шринка), основными состояниями операции (сохраняются в базе, упорядочены), состояниями отката (rollback). Библиотека позволяет определить правила смены состояний и соблюдать их при выполнении. Также библиотека предоставляет гибкий набор хуков и коллбэков, которые можно вызывать перед сменой состояний или после; они могут быть привязаны к определенному состоянию, определять дополнительные условия перехода, сценарий при возникновении исключений и т.д. То есть такого инструментария более чем достаточно для описания сложных процессов.

Частичная архитектура без перечисления всех состояний и переходов изображена на Рис. 8. ggrebalance построен вокруг нескольких вложенных конечных автоматов, такая архитектура позволяет четко разделить ответственность между уровнями и хранить состояние каждого из них в базе данных для возобновления после прерывания.

Реализация конечного автомата GGRebalance
Рис. 8. Реализация конечного автомата GGRebalance

Классы GGRebalanceMainSM и GGShrink описывают явные графы состояний и переходов между ними. Каждый переход сохраняется в служебной схеме (ggrebalance, Рис. 9) внутри базы данных postgres. Именно это обеспечивает реентерабельность: при повторном запуске после любого прерывания утилита считывает последнее зафиксированное состояние и возобновляет выполнение со следующего шага.

Служебные таблицы схемы ggrebalance
Рис. 9. Служебные таблицы схемы ggrebalance

Например, для сценария шринка фиксация статусов может выглядеть так:

select * from ggrebalance.rebalance_status;
                            state                             | state_category |            updated
--------------------------------------------------------------+----------------+-------------------------------
 STATE_SETUP_SCHEMA_STARTED                                   | MAIN           | 2026-05-07 12:30:28.022483+00
 STATE_SETUP_SCHEMA_DONE                                      | MAIN           | 2026-05-07 12:30:28.07895+00
 STATE_EXECUTOR_STARTED                                       | MAIN           | 2026-05-07 12:30:28.116222+00
 STATE_BACKUP_CATALOG_AND_UPDATE_TARGET_SEGMENT_COUNT_STARTED | SHRINK         | 2026-05-07 12:30:29.728506+00
 STATE_BACKUP_CATALOG_AND_UPDATE_TARGET_SEGMENT_COUNT_DONE    | SHRINK         | 2026-05-07 12:30:29.769091+00
 STATE_PREPARE_SHRINK_SCHEMA_STARTED                          | SHRINK         | 2026-05-07 12:30:29.933573+00
 STATE_PREPARE_SHRINK_SCHEMA_DONE                             | SHRINK         | 2026-05-07 12:30:29.990288+00
 STATE_SHRINK_TABLES_STARTED                                  | SHRINK         | 2026-05-07 12:31:48.671078+00
 STATE_SHRINK_TABLES_DONE                                     | SHRINK         | 2026-05-07 12:31:49.227031+00
 STATE_SHRINK_CATALOG_STARTED                                 | SHRINK         | 2026-05-07 12:32:39.541764+00
 STATE_SHRINK_CATALOG_DONE                                    | SHRINK         | 2026-05-07 12:32:40.085748+00
 STATE_SHRINK_SEGMENTS_STOP_STARTED                           | SHRINK         | 2026-05-07 12:32:55.979319+00
 STATE_SHRINK_SEGMENTS_STOP_DONE                              | SHRINK         | 2026-05-07 12:32:56.177167+00
 STATE_SHRINK_DONE                                            | SHRINK         | 2026-05-07 12:32:56.323938+00
 STATE_SHRINK_STARTED                                         | MAIN           | 2026-05-07 12:32:56.464615+00
 STATE_SHRINK_DONE                                            | MAIN           | 2026-05-07 12:32:56.620682+00
 STATE_EXECUTOR_DONE                                          | MAIN           | 2026-05-07 12:32:57.107649+00
(17 rows)

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

Рассмотрим ключевые этапы потока шринка подробнее.

STATE_BACKUP_CATALOG_AND_UPDATE_TARGET_SEGMENT_COUNT_STARTED

Внутри транзакции берется блокировка каталога (gp_expand_lock_catalog), исключающая создание таблиц со старым числом сегментов в параллельных сессиях. Затем вызывается gp_toolkit.gp_set_rebalance_numsegments(<target_segnum>) — выставляет для новых сессий новое число сегментов, на котором создаются отношения после начала шринка. Также сохраняется снимок текущего состояния gp_segment_configuration в файл на диске: после удаления записей из gp_segment_configuration информация о выводимых сегментах будет недоступна из каталога, но она понадобится для их корректной остановки.

STATE_SHRINK_TABLES_STARTED

Параллельный пул воркеров выполняет ALTER TABLE …​ REBALANCE <target_numsegments> для каждой таблицы из сформированной ранее очереди (наподобие gpexpand). Каждый воркер при успешном завершении проставляет статус таблицы done в служебной таблице. Степень параллелизма задается флагом --parallel. Каждой таблице дается две попытки на случай транзакционных конфликтов (например, параллельный DDL, удаляющий таблицу в момент ее перераспределения).

STATE_SHRINK_CATALOG_STARTED

Точка невозврата. В единой транзакции из gp_segment_configuration удаляются записи сегментов с content >= target_numsegments, и целевое число сегментов для создания новых отношений становится дефолтным для всего кластера через вызов gp_toolkit.gp_reset_rebalance_numsegments(). После коммита этой транзакции кластер официально работает с уменьшенным числом сегментов.

STATE_SHRINK_SEGMENTS_STOP_STARTED

Процессы выведенных сегментов еще работают, несмотря на их удаление из каталога. ggrebalance корректно останавливает их, используя сохраненный ранее снимок конфигурации. Сначала останавливаются primary-сегменты, затем — зеркала, чтобы избежать зависания репликации. Операция выполняется параллельно пулом воркеров с размером, заданным флагом --batch-size.

1.4 Разбор сценария шринка

Рассмотрим практический пример из рисунка 6: кластер из 3 хостов с 12 primary-сегментами (по 4 на хост), и нам требуется уменьшить их количество до 9 (шринк) и сбалансировать до 3 сегментов на хост. Стратегию зеркалирования не меняем (grouped).

select * from gp_segment_configuration ;
 dbid | content | role | preferred_role | mode | status | port | hostname | address |               datadir
------+---------+------+----------------+------+--------+------+----------+---------+-------------------------------------
    1 |      -1 | p    | p              | n    | u      | 7000 | cdw      | cdw     | /home/gpadmin/.data/gpseg-1
   10 |       8 | p    | p              | s    | u      | 7010 | sdw3     | sdw3    | /home/gpadmin/.data/primary/gpseg8
   22 |       8 | m    | m              | s    | u      | 7060 | sdw1     | sdw1    | /home/gpadmin/.data/mirror/gpseg8
    2 |       0 | p    | p              | s    | u      | 7002 | sdw1     | sdw1    | /home/gpadmin/.data/primary/gpseg0
   14 |       0 | m    | m              | s    | u      | 7052 | sdw2     | sdw2    | /home/gpadmin/.data/mirror/gpseg0
    3 |       1 | p    | p              | s    | u      | 7003 | sdw1     | sdw1    | /home/gpadmin/.data/primary/gpseg1
   15 |       1 | m    | m              | s    | u      | 7053 | sdw2     | sdw2    | /home/gpadmin/.data/mirror/gpseg1
    4 |       2 | p    | p              | s    | u      | 7004 | sdw1     | sdw1    | /home/gpadmin/.data/primary/gpseg2
   16 |       2 | m    | m              | s    | u      | 7054 | sdw2     | sdw2    | /home/gpadmin/.data/mirror/gpseg2
    5 |       3 | p    | p              | s    | u      | 7005 | sdw1     | sdw1    | /home/gpadmin/.data/primary/gpseg3
   17 |       3 | m    | m              | s    | u      | 7055 | sdw2     | sdw2    | /home/gpadmin/.data/mirror/gpseg3
    6 |       4 | p    | p              | s    | u      | 7006 | sdw2     | sdw2    | /home/gpadmin/.data/primary/gpseg4
   18 |       4 | m    | m              | s    | u      | 7056 | sdw3     | sdw3    | /home/gpadmin/.data/mirror/gpseg4
    7 |       5 | p    | p              | s    | u      | 7007 | sdw2     | sdw2    | /home/gpadmin/.data/primary/gpseg5
   19 |       5 | m    | m              | s    | u      | 7057 | sdw3     | sdw3    | /home/gpadmin/.data/mirror/gpseg5
    8 |       6 | p    | p              | s    | u      | 7008 | sdw2     | sdw2    | /home/gpadmin/.data/primary/gpseg6
   20 |       6 | m    | m              | s    | u      | 7058 | sdw3     | sdw3    | /home/gpadmin/.data/mirror/gpseg6
    9 |       7 | p    | p              | s    | u      | 7009 | sdw2     | sdw2    | /home/gpadmin/.data/primary/gpseg7
   21 |       7 | m    | m              | s    | u      | 7059 | sdw3     | sdw3    | /home/gpadmin/.data/mirror/gpseg7
   11 |       9 | p    | p              | s    | u      | 7011 | sdw3     | sdw3    | /home/gpadmin/.data/primary/gpseg9
   23 |       9 | m    | m              | s    | u      | 7061 | sdw1     | sdw1    | /home/gpadmin/.data/mirror/gpseg9
   12 |      10 | p    | p              | s    | u      | 7012 | sdw3     | sdw3    | /home/gpadmin/.data/primary/gpseg10
   24 |      10 | m    | m              | s    | u      | 7062 | sdw1     | sdw1    | /home/gpadmin/.data/mirror/gpseg10
   13 |      11 | p    | p              | s    | u      | 7013 | sdw3     | sdw3    | /home/gpadmin/.data/primary/gpseg11
   25 |      11 | m    | m              | s    | u      | 7063 | sdw1     | sdw1    | /home/gpadmin/.data/mirror/gpseg11
(25 rows)

select hostname,role, array_agg(content) from gp_segment_configuration group by hostname, role order by hostname;
 hostname | role |  array_agg
----------+------+-------------
 cdw      | p    | {-1}
 sdw1     | p    | {0,1,2,3}
 sdw1     | m    | {8,9,10,11}
 sdw2     | m    | {0,1,2,3}
 sdw2     | p    | {4,5,6,7}
 sdw3     | m    | {4,5,6,7}
 sdw3     | p    | {8,9,10,11}
(7 rows)

Важное замечание: шринк кластера подразумевает вывод сегментов, чьи content_id образуют хвост последовательности всех content_id после отбрасывания из нее <target_segment_count>. Произвольный сегмент, ввиду специфики хеширования Greengage, выкинуть не получится — только с конца. Команда запуска:

$ ggrebalance -x 9 --parallel 8 --batch-size 4

Перед стартом ggrebalance убеждается в отсутствии конкурирующих утилит (gpexpand, gprecoverseg, gpbackup и т.д.), проверяет, что все primary-сегменты доступны, и создает PID-файл для предотвращения параллельного запуска. Далее выполняются следующие шаги:

  1. Планируется шринк и балансировка оставшихся сегментов.

  2. Создается служебная схема, сохраняется план шринка.

  3. Берется блокировка каталога, устанавливается gp_set_rebalance_numsegments(9), сохраняется дамп gp_segment_configuration.

  4. Составляется список таблиц с numsegments > 9 по всем базам данных.

  5. Для каждой таблицы параллельно выполняется ALTER TABLE …​ REBALANCE 9 — строки с сегментов 9, 10, 11 вставляются на сегменты 0—​8.

  6. Удаляются записи сегментов 9, 10, 11 из gp_segment_configuration.

  7. Параллельно останавливаются primary-сегменты, затем их зеркала.

Примерный вывод следующий:

20260426:19:06:46:033224 ggrebalance:cdw:gpadmin-[INFO]:-Init gparray from catalog
20260426:19:06:46:033224 ggrebalance:cdw:gpadmin-[INFO]:-Planning shrink
20260426:19:06:46:033224 ggrebalance:cdw:gpadmin-[INFO]:-Validation of rebalance possibility
20260426:19:06:46:033224 ggrebalance:cdw:gpadmin-[INFO]:-Planning rebalance moves. Can take up to 60s.
20260426:19:06:46:033224 ggrebalance:cdw:gpadmin-[INFO]:-Running randomized plan improvement with seed:315919769283260131213658672706621802564
20260426:19:06:46:033224 ggrebalance:cdw:gpadmin-[INFO]:-Estimating resource requirements for 4 segment moves...
20260426:19:06:47:033224 ggrebalance:cdw:gpadmin-[INFO]:-Validating available disk space on target hosts...
20260426:19:06:47:033224 ggrebalance:cdw:gpadmin-[INFO]:-Disk space validation completed successfully
20260426:19:06:47:033224 ggrebalance:cdw:gpadmin-[INFO]:-Estimated total data to move: 102.87 GB
20260426:19:06:47:033224 ggrebalance:cdw:gpadmin-[INFO]:-Final plan:
================================================================================
                                  SHRINK PLAN
================================================================================

Target Segment Count: 9

-------------------------------SEGMENTS TO REMOVE-------------------------------
Total segments to shrink: 3

  [1] Segment Pair:
      Primary:
        Content:  9
        DbId:     11
        Host:     sdw3
        Datadir:  /home/gpadmin/.data/primary/gpseg9
        Port:     7011
      Mirror:
        Content:  9
        DbId:     23
        Host:     sdw1
        Datadir:  /home/gpadmin/.data/mirror/gpseg9
        Port:     7061

  [2] Segment Pair:
      Primary:
        Content:  10
        DbId:     12
        Host:     sdw3
        Datadir:  /home/gpadmin/.data/primary/gpseg10
        Port:     7012
      Mirror:
        Content:  10
        DbId:     24
        Host:     sdw1
        Datadir:  /home/gpadmin/.data/mirror/gpseg10
        Port:     7062

  [3] Segment Pair:
      Primary:
        Content:  11
        DbId:     13
        Host:     sdw3
        Datadir:  /home/gpadmin/.data/primary/gpseg11
        Port:     7013
      Mirror:
        Content:  11
        DbId:     25
        Host:     sdw1
        Datadir:  /home/gpadmin/.data/mirror/gpseg11
        Port:     7063

---------------------------------BALANCE MOVES----------------------------------
Total moves planned: 4

  [1] Move Segment(content=3, dbid=5, role=p) [8.92 GB]
      From: sdw1:7005 → /home/gpadmin/.data/primary/gpseg3
      To:   sdw3:7005 → /home/gpadmin/.data/primary/gpseg3

  [2] Move Segment(content=3, dbid=17, role=m) [7.63 GB]
      From: sdw2:7055 → /home/gpadmin/.data/mirror/gpseg3
      To:   sdw1:7055 → /home/gpadmin/.data/mirror/gpseg3

  [3] Move Segment(content=7, dbid=9, role=p) [8.57 GB]
      From: sdw2:7009 → /home/gpadmin/.data/primary/gpseg7
      To:   sdw3:7009 → /home/gpadmin/.data/primary/gpseg7

  [4] Move Segment(content=7, dbid=21, role=m) [10.1 GB]
      From: sdw3:7059 → /home/gpadmin/.data/mirror/gpseg7
      To:   sdw1:7059 → /home/gpadmin/.data/mirror/gpseg7

================================================================================
20260426:19:06:48:033224 ggrebalance:cdw:gpadmin-[INFO]:-Created "ggrebalance" schema
20260426:19:06:49:033224 ggrebalance:cdw:gpadmin-[INFO]:-Updated target segment count to 9
20260426:19:06:50:033224 ggrebalance:cdw:gpadmin-[INFO]:-Initiated list of tables to rebalance
20260426:19:06:50:033224 ggrebalance:cdw:gpadmin-[INFO]:-Start tables rebalance for shrink
20260426:19:06:50:033224 ggrebalance:cdw:gpadmin-[INFO]:-Tables to process 0
20260426:19:06:50:033224 ggrebalance:cdw:gpadmin-[INFO]:-Tables rebalance complete
20260426:19:06:50:033224 ggrebalance:cdw:gpadmin-[INFO]:-Start catalog shrink
20260426:19:06:50:033224 ggrebalance:cdw:gpadmin-[INFO]:-Catalog shrink complete
20260426:19:06:50:033224 ggrebalance:cdw:gpadmin-[INFO]:-Stopping shrinked segments...
20260426:19:06:51:033224 ggrebalance:cdw:gpadmin-[INFO]:-Summary of shrinked segments:
20260426:19:06:51:033224 ggrebalance:cdw:gpadmin-[INFO]:-segment stopped ok - sdw3:/home/gpadmin/.data/primary/gpseg9:content=9:dbid=11:role=p:preferred_role=p:mode=s:status=u
20260426:19:06:51:033224 ggrebalance:cdw:gpadmin-[INFO]:-segment stopped ok - sdw3:/home/gpadmin/.data/primary/gpseg10:content=10:dbid=12:role=p:preferred_role=p:mode=s:status=u
20260426:19:06:51:033224 ggrebalance:cdw:gpadmin-[INFO]:-segment stopped ok - sdw3:/home/gpadmin/.data/primary/gpseg11:content=11:dbid=13:role=p:preferred_role=p:mode=s:status=u
20260426:19:06:51:033224 ggrebalance:cdw:gpadmin-[INFO]:-segment stopped ok - sdw1:/home/gpadmin/.data/mirror/gpseg9:content=9:dbid=23:role=m:preferred_role=m:mode=s:status=u
20260426:19:06:51:033224 ggrebalance:cdw:gpadmin-[INFO]:-segment stopped ok - sdw1:/home/gpadmin/.data/mirror/gpseg10:content=10:dbid=24:role=m:preferred_role=m:mode=s:status=u
20260426:19:06:51:033224 ggrebalance:cdw:gpadmin-[INFO]:-segment stopped ok - sdw1:/home/gpadmin/.data/mirror/gpseg11:content=11:dbid=25:role=m:preferred_role=m:mode=s:status=u
20260426:19:06:51:033224 ggrebalance:cdw:gpadmin-[INFO]:-Shrink is complete

1.5 Сравнение с CTAS

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

Первая серия измерений посвящена сравнению скорости выполнения ALTER TABLE …​ REBALANCE, основанной на логике INSERT, и скорости выполнения этой же команды, реализованной через CTAS. В облаке был развернут кластер Greengage, состоящий из 8 хостов и 64 primary-сегментов. Характеристики сегмент-хостов и настройки кластера вкратце приведены в таблице 1.

Таблица 1. Характеристики окружения

Облачный провайдер

Yandex Cloud

ОС

Ubuntu 22.04

Количество vCPU

32

Оперативная память

64 ГБ

Диски

2 SSD-диска по 1 ТБ

MTU

9000

txqueuelen

10000

gp_interconnect_type

udpifc

gp_max_packet_size

8192

gp_interconnect_queue_depth

4

Рассматривались следующие сценарии:

  • Легкий шринк: 64 → 56 — перемещаются 12,5% данных (при допущении, что они хранятся равномерно).

  • Шринк 64 → 32 — 50% данных.

  • Агрессивный шринк 64 → 16 — 75% данных.

Кластер заполнялся адаптированным для Greengage DB датасетом TPC-DS размером около 2 ТБ (генерация с scale factor = 3000).

Таблица 2. Генерация TPC-DS
table_name access_method compression_type total_size uncompressed_size compression_ratio

store_sales

ao_column

zstd, level=5

464 GB

983 GB

2.12

catalog_sales

ao_column

zstd, level=5

348 GB

741 GB

2.13

web_sales

ao_row

zstd, level=5

227 GB

453 GB

1.99

store_returns

heap

115 GB

catalog_returns

ao_column

71 GB

web_returns

ao_column

25 GB

inventory

ao_column

zstd, level=5

4901 MB

16 GB

3.25

customer

heap

4483 MB

customer_address

heap

2021 MB

customer_demographics

heap

138 MB

item

heap

112 MB

time_dim

heap

12 MB

date_dim

heap

12 MB

catalog_page

heap

6464 kB

call_center

heap

3136 kB

web_page

heap

2048 kB

household_demographics

heap

2048 kB

promotion

heap

2048 kB

store

heap

2048 kB

reason

heap

1376 kB

web_site

heap

1344 kB

warehouse

heap

640 kB

ship_mode

heap

576 kB

income_band

heap

576 kB

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

Таблица 3. Выборка таблиц
table_name total_size access_method

store_sales

464 GB

ao_column

catalog_sales

348 GB

ao_column

web_sales

179 GB

ao_row

store_returns

87 GB

heap

catalog_returns

59 GB

ao_column + add btree index

Для каждой из 5 таблиц трижды выполнялся ее шринк CTAS и INSERT-методами — всего 30 прогонов. После каждого шринка таблица восстанавливалась на исходное число сегментов также через ALTER TABLE …​ REBALANCE <nsegs_origin>, вызывающий CTAS перераспределение, то есть шаг восстановления после шринка вносит одинаковый шум в возможные результаты прогонов. Измерялись величины:

  • Полное время блокировки клиентского соединения при выполнении ребаланса, включая сетевой round-trip от координатора до клиента.

  • Дисковая амплификация: каждые 10 секунд запускался du -sb по всем primary-директориям всех сегментов кластера (т.е. включаются все файлы PGDATA), после шринка бралось пиковое значение из этой выборки и вычислялось отношение максимального разрастания объема к размеру данных кластера до запуска шринка. Их можно сравнить с теоретическими:

    где  — размер перераспределяемого отношения,  — размер i-го первичного сегмента.

Теоретическая оценка степени разрастания основана на предположении, что данные таблиц равномерно распределены по всему кластеру. Для CTAS-метода ожидается, что в какой-то момент появится полный дубликат отношения. А для INSERT — лишь доля объема, пропорциональная количеству сегментов, на которых происходит сканирование.

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

Ниже приведены результаты измерений.

Таблица 4. Дисковая амплификация
Таблица Метод До шринка После шринка Размер отношения до шринка Размер отношения после шринка Размер кластера до шринка Пиковый размер кластера во время шринка Дисковая амплификация таблицы Теоретическое значение Время

store_sales

ctas

64

56

463,96 GB

464,53 GB

1,32 TB

1,77 TB

1.3438

1,3514

00h:43m:51s

store_sales

insert

64

56

464,23 GB

464,3 GB

1,32 TB

1,37 TB

1.0428

1,0439

00h:08m:28s

catalog_sales

ctas

64

56

348,22 GB

349,23 GB

1,32 TB

1,66 TB

1.2555

1,2638

00h:30m:39s

catalog_sales

insert

64

56

348,93 GB

348,94 GB

1,32 TB

1,36 TB

1.0317

1,0330

00h:05m:34s

web_sales

ctas

64

56

227,7 GB

228,2 GB

1,32 TB

1,54 TB

1.1610

1,1725

00h:13m:42s

web_sales

insert

64

56

228,18 GB

228,19 GB

1,32 TB

1,35 TB

1.0213

1,0216

00h:02m:18s

store_returns

ctas

64

56

115,4 GB

115,4 GB

1,32 TB

1,43 TB

1.0813

1,0874

00h:05m:08s

store_returns

insert

64

56

115,4 GB

115,4 GB

1,32 TB

1,33 TB

1.1990

1,0109

00h:00m:41s

catalog_returns

ctas

64

56

72,63 GB

68,23 GB

1,32 TB

1,37 TB

1.0433

1,0550

00h:04m:24s

catalog_returns

insert

64

56

68,52 GB

74,73 GB

1,31 TB

1,33 TB

1.0110

1,0065

00h:02m:02s

store_sales

ctas

64

32

464,43 GB

470,75 GB

1,31 TB

1,77 TB

1.3479

1,3545

01h:03m:24s

store_sales

insert

64

32

470,31 GB

470,25 GB

1,32 TB

1,56 TB

1.1758

1,1781

00h:29m:18s

catalog_sales

ctas

64

32

348,96 GB

353,59 GB

1,32 TB

1,67 TB

1.2582

1,2643

00h:44m:29s

catalog_sales

insert

64

32

352,91 GB

352,94 GB

1,33 TB

1,5 TB

1.1298

1,1326

00h:20m:01s

web_sales

ctas

64

32

228,21 GB

230,68 GB

1,33 TB

1,55 TB

1.1668

1,1715

00h:20m:11s

web_sales

insert

64

32

230,73 GB

230,69 GB

1,33 TB

1,44 TB

1.0852

1,0867

00h:09m:49s

store_returns

ctas

64

32

115,4 GB

115,39 GB

1,33 TB

1,44 TB

1.0798

1,0867

00h:07m:55s

store_returns

insert

64

32

115,4 GB

115,39 GB

1,33 TB

1,38 TB

1.0407

1,0433

00h:03m:59s

catalog_returns

ctas

64

32

68,52 GB

67,36 GB

1,33 TB

1,39 TB

1.0487

1,0515

00h:05m:25s

catalog_returns

insert

64

32

68,52 GB

67,97 GB

1,33 TB

1,36 TB

1.0245

1,0257

00h:03m:50s

store_sales

ctas

64

16

470,65 GB

475,66 GB

1,33 TB

1,79 TB

1.3495

1,3538

01h:51m:26s

store_sales

insert

64

16

475,09 GB

475,03 GB

1,33 TB

1,68 TB

1.2640

1,2679

01h:26m:28s

catalog_sales

ctas

64

16

352,95 GB

357,41 GB

1,33 TB

1,68 TB

1.2607

1,2653

01h:18m:48s

catalog_sales

insert

64

16

356,63 GB

356,77 GB

1,34 TB

1,6 TB

1.1964

1,1996

01h:00m:54s

web_sales

ctas

64

16

230,82 GB

232,56 GB

1,34 TB

1,56 TB

1.1679

1,1722

00h:36m:32s

web_sales

insert

64

16

232,69 GB

232,6 GB

1,34 TB

1,51 TB

1.1282

1,1302

00h:30m:22s

store_returns

ctas

64

16

115,4 GB

115,39 GB

1,34 TB

1,45 TB

1.0815

1,0861

00h:13m:41s

store_returns

insert

64

16

115,4 GB

115,38 GB

1,34 TB

1,42 TB

1.0621

1,0645

00h:11m:38s

catalog_returns

ctas

64

16

68,52 GB

66,79 GB

1,34 TB

1,4 TB

1.0477

1,0511

00h:07m:57s

catalog_returns

insert

64

16

68,52 GB

68,6 GB

1,34 TB

1,39 TB

1.0363

1,0383

00h:08m:27s

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

Во-вторых, можно наблюдать ускорение INSERT по сравнению с CTAS (таблица 5).

Таблица 5. Анализ времени перераспределения
Таблица Сценарий CTAS INSERT Ускорение

store_sales

64 → 56

43m 51s

08m 28s

5.2×

store_sales

64 → 32

1h 03m 24s

29m 18s

2.2×

store_sales

64 → 16

1h 51m 26s

1h 26m 28s

1.3×

catalog_sales

64 → 56

30m 39s

05m 34s

5.5×

catalog_sales

64 → 32

44m 29s

20m 01s

2.2×

catalog_sales

64 → 16

1h 18m 48s

1h 00m 54s

1.3×

web_sales

64 → 56

13m 42s

02m 18s

6.0×

web_sales

64 → 32

20m 11s

09m 49s

2.1×

web_sales

64 → 16

36m 32s

30m 22s

1.2×

store_returns

64 → 56

05m 08s

00m 41s

7.5×

store_returns

64 → 32

07m 55s

03m 59s

2.0×

store_returns

64 → 16

13m 41s

11m 38s

1.2×

catalog_returns

64 → 56

04m 24s

02m 02s

2.2×

catalog_returns

64 → 32

05m 25s

03m 50s

1.4×

catalog_returns

64 → 16

07m 57s

08m 27s

0.94×

CTAS: Рост пропорционален целевому числу сегментов: 56, 32, 16. Чем оно меньше, тем дольше выполняется перераспределение, время складывается из двух независимых I/O-фаз примерно равного веса. Для INSERT картина аналогичная: время масштабируется линейно с объемом перемещаемых данных, здесь же большее влияние вносит нагрузка на сетевое перераспределение кортежей.

Таким образом, по дисковой амплификации INSERT выигрывает при любом сценарии без исключений. Разница наиболее показательна при 64 → 56, где INSERT создает в 7—​8 раз меньше пикового дискового давления, чем CTAS. По времени INSERT выигрывает при 64 → 56 и 64 → 32 для всех таблиц с большим отрывом. При 64 → 16 преимущество INSERT сокращается до 20—​30% на больших таблицах и исчезает на маленьких AO-таблицах без компрессии. Предположительно, с увеличением перераспределяемых данных растет объем генерируемых WAL, так как INSERT вставляет данные построчно через стандартный путь heap_insert / appendonly_insert, который при wal_level >= replica пишет WAL для каждого блока. При перемещении 50% данных store_sales (232 ГБ) это колоссальный объем WAL. Также при INSERT в существующую AO-таблицу каждый сегмент должен обновлять строки в pg_aoseg — системный каталог, который хранит метаданные AO-сегментных файлов. При множестве параллельных вставок возникает конкуренция за доступ к этой системной таблице.

Также можно грубо оценить пропускную способность (throughput) в ГБ/с по формуле:

Таблица 6. Throughput шринка
Сценарий Throughput

INSERT 64 → 56

0.114 GB/s

INSERT 64 → 32

0.132 GB/s

INSERT 64 → 16

0.067 GB/s

CTAS 64 → 56

0.154 GB/s

CTAS 64 → 32

0.061 GB/s

CTAS 64 → 16

0.017 GB/s

CTAS при 64 → 56 имеет наивысший throughput 0.154 ГБ/с — потому что все 56 целевых сегментов пишут параллельно и объем записи велик. При уменьшении целевого числа сегментов throughput деградирует. Причина в том, что формула учитывает только объем данных, записываемых в новую таблицу, тогда как реальное время определяется еще и чтением полного объема исходной таблицы, которое при агрессивном шринке становится доминирующей фазой. При 64 → 16 CTAS читает 100% данных, но пишет лишь 25% — оставшиеся 75% прочитанных данных просто перераспределяются на меньшее число сегментов, и именно этот дисбаланс между объемом чтения и записи объясняет столь низкий эффективный throughput CTAS при агрессивном шринке.

Проведенные эксперименты дают количественную основу для сравнения двух фундаментально различных стратегий перераспределения данных в Greengage-кластере. У нас есть измеренные точки: INSERT-подход выигрывает по дисковой амплификации при любом сценарии и по времени при умеренном шринке, но теряет временно́е преимущество при агрессивном сжатии из-за роста WAL-нагрузки и contention на метаданных AO-хранилища. CTAS-подход проигрывает по пиковому росту занимаемой памяти, но имеет меньше накладных расходов при агрессивном шринке. В следующих релизах определенно планируется оптимизация этих моментов.

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

1.6 Реентерабельность и откат изменений

Благодаря персистентному конечному автомату повторный запуск ggrebalance после любого прерывания (сбой сети, нехватка дискового пространства, сигнал SIGINT и т.д.) безопасен. Утилита считывает последнее зафиксированное состояние FSM и сравнивает его с реальным состоянием кластера:

def on_enter_STATE_CHECK_PREVIOUS_RUN(self) -> None:
    state_from_prev_run = self.rebalance_schema.getShrinkStateFromPreviousRun()
    # ...
    next_state = self.get_state_after_interrupt(state_from_prev_run)
    self.trigger(f'to_{next_state}')

На уровне таблиц каждая запись в служебной очереди имеет статус: none — не обработана, done — обработана. При возобновлении пул воркеров забирает только необработанные таблицы, уже перераспределенные пропускаются. Это исключает двойную работу даже при многократных прерываниях в середине шага SHRINK_TABLES.

Раскроем необходимость обеспечения реентерабельности на примере. Допустим, был запущен процесс шринка (все тот же Рис. 6), который начал перераспределять данные. И в это время произошел некритичный инцидент, затрагивающий работу кластера Greengage, после которого появилась необходимость перезапустить систему и прервать шринк. ggrebalance может быть сам по себе прерван через:

  • сигнал;

  • истечение времени, заданного опцией --duration, с момента начала шринка.

Пусть шринк был прерван, находясь в следующем состоянии:

20260518:01:44:00:215237 ggrebalance:cdw:gpadmin-[INFO]:-Complete table rebalance for "postgres"."public"."t1"
20260518:01:48:25:215310 ggrebalance:cdw:gpadmin-[ERROR]:-Failed to process the db object "postgres"."public"."t1" for 2 attempts
20260518:01:48:25:215310 ggrebalance:cdw:gpadmin-[INFO]:-Shrink was interrupted
20260518:01:48:25:215310 ggrebalance:cdw:gpadmin-[ERROR]:-ggrebalance failed: Shrink was interrupted
select db_name, schema_name, rel_name, status, rebalance_type, rebalance_finished from ggrebalance.table_rebalance_status_detail;

 db_name  | schema_name | rel_name | status | rebalance_type |      rebalance_finished
----------+-------------+----------+--------+----------------+-------------------------------
 postgres | public      | t2       | none   | SHRINK         |
 postgres | public      | t1       | done   | SHRINK         | 2026-05-18 01:55:13.607961+00
(2 rows)

select * from ggrebalance.rebalance_status;

                            state                             | state_category |            updated
--------------------------------------------------------------+----------------+---------
 STATE_SETUP_SCHEMA_STARTED                                   | MAIN           | 2026-05-18 01:53:08.035607+00
 STATE_SETUP_SCHEMA_DONE                                      | MAIN           | 2026-05-18 01:53:08.085633+00
 STATE_EXECUTOR_STARTED                                       | MAIN           | 2026-05-18 01:53:08.149228+00
 STATE_BACKUP_CATALOG_AND_UPDATE_TARGET_SEGMENT_COUNT_STARTED | SHRINK         | 2026-05-18 01:53:09.511508+00
 STATE_BACKUP_CATALOG_AND_UPDATE_TARGET_SEGMENT_COUNT_DONE    | SHRINK         | 2026-05-18 01:53:09.56808+00
 STATE_PREPARE_SHRINK_SCHEMA_STARTED                          | SHRINK         | 2026-05-18 01:53:09.691198+00
 STATE_PREPARE_SHRINK_SCHEMA_DONE                             | SHRINK         | 2026-05-18 01:53:09.744374+00
 STATE_SHRINK_TABLES_STARTED                                  | SHRINK         | 2026-05-18 01:54:02.855362+00
(7 rows)

Из лога видно, что таблица t1 была успешно обработана, после чего ggrebalance приступил к t2 и не смог завершить ее — произошло прерывание. Это подтверждается и таблицей table_rebalance_status_detail: t1 имеет статус done с временной меткой завершения, тогда как таблица t2 осталась в статусе none, то есть ее перераспределение так и не было зафиксировано как начатое. Таблица rebalance_status показывает, что последним сохраненным состоянием машины является STATE_SHRINK_TABLES_STARTED — машина вошла в цикл обработки таблиц, но не успела из него выйти. Казалось бы, достаточно просто возобновить работу с того же состояния и перераспределить оставшуюся t2. Однако здесь кроется первая особенность реализации операции шринка, когда в состоянии STATE_BACKUP_CATALOG_AND_UPDATE_TARGET_SEGMENT_COUNT_STARTED, которое предшествовало шринку таблиц, ggrebalance выполнил вызов gp_toolkit.gp_set_rebalance_numsegments(target_count) — установил в каталоге целевое количество сегментов. Этот параметр является сессионно-независимым и глобальным. После перезагрузки кластера этот параметр сбрасывается в значение по умолчанию — полный набор сегментов. Это означает, что если в промежутке между прерыванием шринка и его возобновлением в кластере были созданы новые таблицы, они окажутся распределены по полному набору сегментов, а не по целевому. Такой сценарий достаточно опасен для целостности шринка, чреват потерей данных с выводимых сегментов.

ggrebalance учитывает такую ситуацию наряду с другими краевыми случаями, гарантируя, что сегменты не будут выведены, пока все таблицы не перераспределятся. ggrebalance не продолжает с точки прерывания, а делает шаг назад и восстанавливает rebalance_numsegments для параллельных DDL-запросов, затем заново обходит все базы данных и формирует актуальную очередь, отбирая таблицы по условию распределения. Без описанной логики любой перезапуск кластера в ходе шринка превращался бы в инцидент: новые таблицы оставались бы "за бортом" операции, а следующий запуск ggrebalance либо завершал бы шринк, игнорируя их, либо требовал бы ручного анализа расхождений. В этом отношении gpshrink подвержен риску потери данных при параллельных операциях с кластером.

Операция отката шринка (ggrebalance --rollback) доступна только до момента обновления gp_segment_configuration — то есть не позднее состояния STATE_SHRINK_TABLES_DONE. Если каталог уже обновлен, откат невозможен по определению — кластер работает с новым числом сегментов. При допустимом откате запускается поток отката, который:

  1. Сбрасывает целевое число сегментов (gp_reset_rebalance_numsegments) — новые таблицы снова создаются с исходным числом сегментов.

  2. Формирует список таблиц со статусом done — тех, которые уже были перераспределены на меньшее число сегментов и должны быть возвращены обратно.

  3. Параллельно выполняет ALTER TABLE …​ REBALANCE <original_numsegments> для таблиц из списка, возвращая строки на исходное число сегментов.

Откат сам по себе полностью реентерабелен: каждый шаг персистируется в том же потоке состояний states_rollback_flow, и повторный запуск ggrebalance --rollback корректно продолжит прерванный откат. При этом уже обработанные при откате таблицы (их статус обновлен обратно на none) не затрагиваются повторно. Таким образом, оба крайних сценария — "операция прервана, хочу продолжить" и "операция прервана, хочу вернуть все как было" — обрабатываются ggrebalance детерминировано и без ручного вмешательства в состояние кластера.

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

Выводы первой части

В данной статье мы познакомились с возможностями масштабирования Greengage-кластера с помощью утилиты ggrebalance — мощного инструмента для менеджмента ресурсов и объемов хранимых данных. Подробно был описан процесс шринка кластера, когда необходимо уменьшить количество первичных сегментов без потери данных выводимых партиций. Операция шринка — это наглядный пример того, насколько значительным может быть разрыв между концептуальной простотой задачи ("убрать несколько сегментов") и сложностью ее корректной реализации в production-системе. Ответом на эту сложность стали три взаимосвязанных архитектурных решения, которые пронизывают всю реализацию ggrebalance:

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

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

  • Возможность отката изменений. До удаления строк из gp_segment_configuration (точка невозврата) таблицы можно перераспределить на изначальное число сегментов. Сам rollback реализован как полноценный, независимый и также реентерабельный поток, который можно прервать и возобновить столько раз, сколько потребуется.

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