
Кластер Greengage DB, как и любое распределенное хранилище, не является статичной системой. Используемые объемы данных могут как расти, так и сокращаться, для оптимизации нагрузок может появиться необходимость изменить степень параллелизма системы. Оборудование может требовать замены, либо нужна полноценная миграция инфраструктуры на новые машины. Все эти явления и факторы требуют управляемых изменений в топологии кластера: в числе сегментов, их распределении по хостам, используемой стратегии зеркалирования.
Встроенный набор инструментов Greengage предлагает отдельные решения перечисленных задач: gpexpand для расширения кластера, gpmovemirrors для физического перемещения реплик и т.д.
Использование их в кластерах, состоящих из сотен сегментов, вручную или через самописные скрипты — очень трудоемкий процесс, не защищенный от ошибок при распределении данных, выводе сегментов из кластера и перемещении зеркал и требующий тщательного контроля.
Более того, в стандартном наборе утилит отсутствует поддержка операции уменьшения (шринк) кластера, а также ситуаций изменения топологии, затрагивающих несколько измерений одновременно (уменьшить число сегментов, добавить новый хост, вывести из использования другой и поменять стратегию зеркалирования).
Говоря про шринк, стоит отметить существование решения gpshrink от Cloudberry DB, по реализации являющегося калькой обратной шринку операции расширения gpexpand.
Однако его использование также должно быть строго контролируемым, так как в случае шринка производятся необратимые изменения конфигурации кластера.
Описанные пробелы в функционале существующего инструментария заполняет разрабатываемая нами кластерная утилита ggrebalance.
ggrebalance — это способ контролируемо реализовывать сценарии изменения топологии Greengage-кластера в безопасной (с обработкой ошибок), реентерабельной (продолжение прерванной операции) манере и с возможностью обратить последовательность действий вспять.
В первом релизе утилиты поддерживаются следующие сценарии:
Например, доступно новое железо, и хочется перераспределить существующие сегменты так, чтобы новые хосты взяли на себя долю рабочей нагрузки без увеличения степени параллелизма Greengage DB.
Нужно вывести из эксплуатации хост с переносом его сегментов на оставшиеся узлы.
Хост и функционирующие на нем сегменты убираются из Greengage-кластера.
Целый кластер переносится на новый набор машин, например, в ходе миграции дата-центра. Число сегментов неизменно.
Как и в сценарии 4, только можно изменить степень параллельной обработки (на данный момент поддерживается только шринк).
При изменении набора хостов или количества сегментов можно заодно сменить тип зеркалирования.
Обычно для Greengage принято использовать один из двух: либо grouped — когда все зеркала одной primary-группы расположены вместе на отдельном хосте, либо spread — зеркала primary-группы распределены на множестве узлов.
Набор хостов не меняется, но число primary-сегментов (и соответствующих зеркал) уменьшается. Это может быть удобным в случае, когда рабочая нагрузка стала легче, или же планируемый бюджет требует сокращения выделяемых под инфраструктуру ресурсов.
В данной серии статей мы подсвечиваем отдельные аспекты и проблемы масштабирования аналитических СУБД и описываем, как утилита ggrebalance способна их решить в контексте эксплуатации MPP DBMS Greengage.
И в первой статье представлен разбор операции сокращения числа сегментов кластера — shrink.
Шринк — операция по сокращению числа primary-сегментов и соответствующих им зеркал в Greengage-кластере.
Ее суть заключается в перераспределении пользовательских данных с выводимых сегментов на остальные, сохраняя изначальные свойства: индексы, тип распределения (replicated, hashed), иерархические отношения, зависимости и т.д.
Это делает реализацию процедуры шринка принципиально отличной от реализации уже имеющейся утилиты gpexpand.
Для понимания, что ggrebalance делает во время шринка, полезно будет в общих чертах рассмотреть действия gpexpand по добавлению новых сегментов (Рис. 7).
На первом этапе gpexpand подготавливает новые сегменты: берет блокировку на возможность обновления каталога, тем самым предотвращая создание таблиц на старом числе сегментов, переносит бэкап координатора в места расположения новых сегментов (каталог координатора становится "шаблоном" для новых сегментов), поднимает их, обновляет gp_segment_configuration (на число сегментов из этого отношения ориентируются все подсистемы, реализующие распределенную работу), отпускает блокировку каталога, и сохраняет в служебной таблице список существующих отношений на основе снимка pg_class всех баз данных.
Наконец, синхронизирует зеркала вновь добавленных сегментов.
На втором этапе происходит перераспределение данных через 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.
Это означает, что во время перераспределения кластер формально все еще располагает старым числом сегментов, и все механизмы маршрутизации запросов работают по-прежнему.
В 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 ГБ.
Временно (до вывода сегмента из кластера) эти данные будут присутствовать в двух экземплярах, но расходуя гораздо меньше ресурсов.
Упомянутый ранее gpshrink применяет простой, императивный подход к реализации (написанный одним Python-файлом по аналогии с gpexpand).
Используется простой if/elif/else control flow, прогресс по сути нумерованный список шагов, без формальной валидации допустимых переходов между ними.
ggrebalance, в свою очередь, вводит больше контроля над процессом шринка.
Операция шринка в ggrebalance реализована в виде детерминированного конечного автомата (finite-state machine, FSM) на основе Python-библиотеки transitions с различными типами состояний: эфемерными (нигде не сохраняются, нужны для общей реализации переходов, но не важны для самой логики шринка), основными состояниями операции (сохраняются в базе, упорядочены), состояниями отката (rollback).
Библиотека позволяет определить правила смены состояний и соблюдать их при выполнении.
Также библиотека предоставляет гибкий набор хуков и коллбэков, которые можно вызывать перед сменой состояний или после; они могут быть привязаны к определенному состоянию, определять дополнительные условия перехода, сценарий при возникновении исключений и т.д.
То есть такого инструментария более чем достаточно для описания сложных процессов.
Частичная архитектура без перечисления всех состояний и переходов изображена на Рис. 8.
ggrebalance построен вокруг нескольких вложенных конечных автоматов, такая архитектура позволяет четко разделить ответственность между уровнями и хранить состояние каждого из них в базе данных для возобновления после прерывания.
Классы GGRebalanceMainSM и GGShrink описывают явные графы состояний и переходов между ними.
Каждый переход сохраняется в служебной схеме (ggrebalance, Рис. 9) внутри базы данных postgres.
Именно это обеспечивает реентерабельность: при повторном запуске после любого прерывания утилита считывает последнее зафиксированное состояние и возобновляет выполнение со следующего шага.
Например, для сценария шринка фиксация статусов может выглядеть так:
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 работы утилиты (оркестрация всех операций), откат основного потока, поток шринка (отвечает только за шринк), поток отката шринка, поток балансировки сегментов, поток отката балансировки сегментов. Про балансировку кластера и планирование перемещений для достижения равномерного распределения сегментов речь пойдет в следующих частях цикла.
Рассмотрим ключевые этапы потока шринка подробнее.
Внутри транзакции берется блокировка каталога (gp_expand_lock_catalog), исключающая создание таблиц со старым числом сегментов в параллельных сессиях.
Затем вызывается gp_toolkit.gp_set_rebalance_numsegments(<target_segnum>) — выставляет для новых сессий новое число сегментов, на котором создаются отношения после начала шринка.
Также сохраняется снимок текущего состояния gp_segment_configuration в файл на диске: после удаления записей из gp_segment_configuration информация о выводимых сегментах будет недоступна из каталога, но она понадобится для их корректной остановки.
Параллельный пул воркеров выполняет ALTER TABLE … REBALANCE <target_numsegments> для каждой таблицы из сформированной ранее очереди (наподобие gpexpand).
Каждый воркер при успешном завершении проставляет статус таблицы done в служебной таблице.
Степень параллелизма задается флагом --parallel.
Каждой таблице дается две попытки на случай транзакционных конфликтов (например, параллельный DDL, удаляющий таблицу в момент ее перераспределения).
Точка невозврата.
В единой транзакции из gp_segment_configuration удаляются записи сегментов с content >= target_numsegments, и целевое число сегментов для создания новых отношений становится дефолтным для всего кластера через вызов gp_toolkit.gp_reset_rebalance_numsegments().
После коммита этой транзакции кластер официально работает с уменьшенным числом сегментов.
Процессы выведенных сегментов еще работают, несмотря на их удаление из каталога.
ggrebalance корректно останавливает их, используя сохраненный ранее снимок конфигурации.
Сначала останавливаются primary-сегменты, затем — зеркала, чтобы избежать зависания репликации.
Операция выполняется параллельно пулом воркеров с размером, заданным флагом --batch-size.
Рассмотрим практический пример из рисунка 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-файл для предотвращения параллельного запуска.
Далее выполняются следующие шаги:
Планируется шринк и балансировка оставшихся сегментов.
Создается служебная схема, сохраняется план шринка.
Берется блокировка каталога, устанавливается gp_set_rebalance_numsegments(9), сохраняется дамп gp_segment_configuration.
Составляется список таблиц с numsegments > 9 по всем базам данных.
Для каждой таблицы параллельно выполняется ALTER TABLE … REBALANCE 9 — строки с сегментов 9, 10, 11 вставляются на сегменты 0—8.
Удаляются записи сегментов 9, 10, 11 из gp_segment_configuration.
Параллельно останавливаются 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
Теоретическое преимущество INSERT-подхода перед CTAS заключается в том, что объем перемещаемых данных пропорционален доле удаляемых сегментов, а не полному размеру таблиц.
Для проверки этого утверждения был проведен ряд экспериментов по оценке скорости шринка.
Первая серия измерений посвящена сравнению скорости выполнения ALTER TABLE … REBALANCE, основанной на логике INSERT, и скорости выполнения этой же команды, реализованной через CTAS.
В облаке был развернут кластер Greengage, состоящий из 8 хостов и 64 primary-сегментов.
Характеристики сегмент-хостов и настройки кластера вкратце приведены в таблице 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).
| 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 |
Для целостной картины измерялось время выполнения шринка и степень разрастания дискового пространства, или дисковая амплификация (увеличение занимаемого места относительно логического объема самих данных), для следующих таблиц (все партицированные):
| 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 первичных сегментов) будет опубликован в будущем.
Ниже приведены результаты измерений.
| Таблица | Метод | До шринка | После шринка | Размер отношения до шринка | Размер отношения после шринка | Размер кластера до шринка | Пиковый размер кластера во время шринка | Дисковая амплификация таблицы | Теоретическое значение | Время |
|---|---|---|---|---|---|---|---|---|---|---|
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).
| Таблица | Сценарий | 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) в ГБ/с по формуле:
| Сценарий | 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 больше стремится обеспечить безопасный шринк без потери данных через поддержку состояний процесса выполнения.
Благодаря персистентному конечному автомату повторный запуск 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.
Если каталог уже обновлен, откат невозможен по определению — кластер работает с новым числом сегментов.
При допустимом откате запускается поток отката, который:
Сбрасывает целевое число сегментов (gp_reset_rebalance_numsegments) — новые таблицы снова создаются с исходным числом сегментов.
Формирует список таблиц со статусом done — тех, которые уже были перераспределены на меньшее число сегментов и должны быть возвращены обратно.
Параллельно выполняет ALTER TABLE … REBALANCE <original_numsegments> для таблиц из списка, возвращая строки на исходное число сегментов.
Откат сам по себе полностью реентерабелен: каждый шаг персистируется в том же потоке состояний states_rollback_flow, и повторный запуск ggrebalance --rollback корректно продолжит прерванный откат.
При этом уже обработанные при откате таблицы (их статус обновлен обратно на none) не затрагиваются повторно.
Таким образом, оба крайних сценария — "операция прервана, хочу продолжить" и "операция прервана, хочу вернуть все как было" — обрабатываются ggrebalance детерминировано и без ручного вмешательства в состояние кластера.
Не следует путать откат шринка с откатом балансировки кластера (в следующих частях подробнее про перемещение сегментов между хостами). В будущих релизах также планируется полноценный откат шринка через обратный ему expand.
В данной статье мы познакомились с возможностями масштабирования Greengage-кластера с помощью утилиты ggrebalance — мощного инструмента для менеджмента ресурсов и объемов хранимых данных.
Подробно был описан процесс шринка кластера, когда необходимо уменьшить количество первичных сегментов без потери данных выводимых партиций.
Операция шринка — это наглядный пример того, насколько значительным может быть разрыв между концептуальной простотой задачи ("убрать несколько сегментов") и сложностью ее корректной реализации в production-системе.
Ответом на эту сложность стали три взаимосвязанных архитектурных решения, которые пронизывают всю реализацию ggrebalance:
Персистентная машина состояний. Гарантирует, что прерывание в любой точке не приводит к неопределенному состоянию системы. Каждый значимый переход фиксируется в долговременном хранилище до того, как он производит эффект.
Реентерабельность на каждом этапе. Обеспечивает корректность при повторном выполнении без ручного анализа произошедшего. Машина состояний проверяет реальное состояние базы данных, а не делает предположений. Перераспределение каждой таблицы происходит под четким контролем, определяемым возможными переходами между состояниями стейт-машины.
Возможность отката изменений.
До удаления строк из gp_segment_configuration (точка невозврата) таблицы можно перераспределить на изначальное число сегментов.
Сам rollback реализован как полноценный, независимый и также реентерабельный поток, который можно прервать и возобновить столько раз, сколько потребуется.
В следующих статьях серии, посвященной ggrebalance, мы поговорим об изменении топологии кластера, которая происходит после завершения шринка: физическое перемещение сегментов между хостами для достижения равномерного распределения нагрузки в кластере.