
The previous article covered the planning mechanism in detail: how the ggrebalance planner builds a list of segment moves to distribute them evenly across hosts, and which scenarios it handles — shrinking, adding hosts, and decommissioning hosts.
Part 3 is about how that plan is actually executed: how segments are physically moved between machines in a running cluster.
Moving a segment with data is a fundamentally hard problem.
A Greengage segment is a full-fledged PostgreSQL instance. It serves its share of every distributed query, takes part in a two-phase commit, streams WAL continuously, and interacts with the FTS (Fault Tolerance Service) mechanism.
Its address, port, and data path are stored in gp_segment_configuration, the table the dispatcher consults when routing every query.
Any mismatch between the catalog and the segment’s actual state immediately breaks the cluster.
Moving a segment can disrupt the cluster just as easily, so without careful preparation a move risks data loss, consistency violations, or query errors.
In theory, you could take the offline route: stop the whole cluster, copy PGDATA to the new hosts, update the gp_segment_configuration catalog in coordinator-only mode, and restart everything.
It’s straightforward, but error-prone, and the downtime it causes is too long for production.
ggrebalance instead moves segments online, relying on Greengage’s own capabilities: streaming replication, FTS probing, and role switching between primary and mirror.
Greengage itself does not provide a single tool for arbitrarily moving segments between hosts.
ggrebalance combines two standard tools from the ecosystem, each covering half the job:
gpmovemirrors — physical data relocation.
The utility moves mirror segments.
It works in three phases. First, it creates a new data directory on the target host via pg_basebackup, physically copying data from the primary segment. Then it waits for the new instance to finish synchronizing with the primary. Finally, it updates the corresponding entry in the gp_segment_configuration system catalog.
None of this touches the primary segments; the operation is transparent to users running queries.
The key limitation of gpmovemirrors is that it works exclusively with mirrors.
Moving primary segments is a different story, and it needs its own mechanism.
gprecoverseg -r — role switching.
The utility restores the "preferred" role assignment, switching segments to the roles recorded in the preferred_role field of gp_segment_configuration.
Here’s how it works: the utility finds pairs whose current role diverges from preferred_role, then stops the primary segments in fast mode.
FTS then automatically detects the failure and promotes the mirror to primary.
A regular gprecoverseg run restores synchronization after that — the former primary becomes a mirror and starts replicating from the newly promoted segment.
Role switching is one stage of moving a primary segment (the full process is covered in detail in the next section).
Move operations inevitably affect cluster availability and reliability, so this must be considered during planning.
During a mirror move (gpmovemirrors), the primary segment keeps serving queries without interruption.
The mirror, however, is temporarily unavailable during copying and the subsequent synchronization.
This temporarily reduces fault tolerance: if the primary fails during this window, its data becomes unrecoverable.
Role switching works differently. The primary segment is stopped forcibly, and any transactions it was processing at that moment are aborted.
FTS promotes the mirror to primary.
That takes some time, and that time counts as downtime — though far shorter than the offline approach.
ggrebalance provides the --replay-lag option, forwarded to gprecoverseg, which sets the maximum mirror lag allowed before triggering a switchover.
It’s a way to trade operation speed against consistency guarantees.
Because ggrebalance is reentrant, the role-switching stage can be postponed to a scheduled maintenance window, leaving the cluster in an intermediate but fully functional configuration.
Consider the process of balancing a cluster after a shrink (Figure 1).
The planner component described in the second article of the series builds a move plan that, once executed, results in an even distribution of segments across hosts. The plan looks like this:
20260412:20:09:41:014374 ggrebalance:cdw:gpadmin-[INFO]:-Final plan:
---------------------------BALANCE MOVES---------------------------
Total moves planned: 4
[1] Move Segment(content=3, dbid=5, role=p) [16.25 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) [16.19 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) [16.25 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) [16.19 GB]
From: sdw3:7059:/home/gpadmin/.data/mirror/gpseg7
To: sdw1:7059:/home/gpadmin/.data/mirror/gpseg7
=====================================================================
The printed plan is logical: it shows which segments need to move where so the cluster becomes balanced.
For moving a mirror, this representation is more than enough, since a mirror can be relocated to another host in a single step with one call to gpmovemirrors.
Primary segments are a bit trickier — Greengage doesn’t support online migration of a primary segment without a service interruption.
So ggrebalance moves a primary segment through a three-step scheme: promote the mirror to primary, move the former primary (now a mirror) to the target host, then revert to original roles.
ggrebalance implements this sequence as follows (Figure 2):
For gprecoverseg -r to trigger a role switch, the segments' role and preferred_role attributes in gp_segment_configuration must first disagree.
To create that mismatch, ggrebalance atomically updates two rows of gp_segment_configuration during the switchover: it flips preferred_role from p to m for the former primary, and from m to p for the mirror.
Then gprecoverseg -r is called, which promotes to primary every mirror whose catalog attributes disagree.
The old primary, now running as a mirror, is relocated to the target host via gpmovemirrors.
The cluster operates normally throughout: the new primary serves queries, while the relocated mirror synchronizes with it from its new location.
Another gprecoverseg -r call, preceded by another manual catalog update, restores the preferred roles: the segment on the target host becomes the primary, and the former primary (on the original host) becomes the mirror.
Once this step finishes, the primary segment has effectively "moved"; its mirror sits exactly where it was before.
Because pg_basebackup runs for a fairly long time during a gpmovemirrors call, executing plan steps strictly sequentially is impractical in production.
So ggrebalance parallelizes move execution instead.
The executor component converts the logical plan produced by the planner into a physical plan.
At the execution level, the whole plan is divided into atomic steps of three kinds, implemented as subclasses of RebalanceStep and described in Table 1.
| Step type | Python class | Tool | What it does |
|---|---|---|---|
Move mirror |
RebalanceStepMoveMirror |
gpmovemirrors |
Physically relocates mirror data |
Switch to mirror |
RebalanceStepSwitchoverToMirror |
gprecoverseg -r |
Primary → mirror |
Switch to primary |
RebalanceStepSwitchoverToPrimary |
gprecoverseg -r |
Mirror → primary |
Steps of the same type that can run in parallel are grouped into batches.
For example, when moving several primary segments, the implementation doesn’t interleave switchover and move steps (switchover1 → move1 → switchover_back1 → switchover2 → move2 → …); instead, it groups steps of the same type together.
That way, gprecoverseg runs once for a whole group of switchovers instead of once per segment.
Operations within a batch run in parallel, and batch size can be controlled with the --parallel option.
Like the shrink operation, cluster balancing is implemented on top of a state machine.
Explicit states and transitions give this design a few real advantages: the current stage is stored in persistent storage, execution is reproducible after a failure, and each stage’s logic stays cleanly separated.
What makes RebalanceSM reentrant is that each state is responsible for exactly one action, and immediately persists the fact that it was entered.
The state diagrams for the rebalance operation and its components are shown in Figures 3 and 4.
Entry point on every RebalanceSM run.
It reads the last saved state from the ggrebalance service schema (more on status tracking in the next section) and determines whether this run is new or a continuation of an interrupted operation.
One-time initialization: the plan is transformed into concrete execution steps, each is assigned a sequence number, and all steps are saved to the schema.
The main control loop re-enters after every executed batch.
All the real work happens here: it checks whether every step is complete, handles steps left in ERROR status from interrupted runs, forms the next batch, and executes it by calling gpmovemirrors or gprecoverseg -r, depending on what kind of steps the batch contains.
An interactive checkpoint.
By default, ggrebalance explicitly asks the user to confirm before every batch of switchovers.
That’s because role switching causes downtime — both the primary and the mirror are stopped at that moment.
Manual confirmation of every such stage can be skipped by passing the -y / --approve-swap-roles or --non-interactive-mode option, which auto-approves role switches.
A rollback mechanism for moves is also provided.
Rollback execution mirrors the forward rebalance flow, with only a few implementation and state-tracking nuances.
The rollback flow deliberately converges on STATE_REBALANCE_EXECUTION_STARTED, which eliminates execution logic duplication.
The rollback scenario is covered in more detail in the following sections.
As covered in the previous articles, every stage of what ggrebalance does — whether a shrink step or a cluster-balancing step — has a corresponding status, which Greengage stores in the ggrebalance service schema of the postgres database (Figure 5).
The schema holds the saved plan (for recovery after an interruption), the current state of the main state machine and the shrink and rebalance state machines, and tables with detailed information about every stage of the work.
Storing statuses directly in the DBMS was a deliberate choice, made to keep the utility’s work traceable and avoid losing information about completed operations.
For the balancing process, the ggrebalance.segment_move_steps table stores one of the following statuses for each planned move.
| Status | Meaning |
|---|---|
PLANNED |
Planned, ready to run |
APPROVE_REQUIRED |
Waiting for operator approval |
IN_PROGRESS |
Currently running |
DONE |
Completed successfully |
ERROR |
Failed with an error |
CANCELLED |
Cancelled because another step failed |
Switchover steps are deliberately created with APPROVE_REQUIRED status, as mentioned earlier — role switching briefly affects cluster behavior, and the operator has to consciously confirm readiness for it.
ggrebalance also logs extensively, the same way other cluster commands do.
Both standard output and the log file in the gpAdminLogs directory contain detailed information about the progress of ggrebalance.
Log verbosity can be controlled with two options: --verbose for detailed debug output on the Python utilities' work at each step, and --quiet to suppress normal logging.
Any long-running operation in a distributed system must assume it will be interrupted — by a network outage, a coordinator failure, or the maintenance window expiring.
ggrebalance was designed around this assumption — every run of the utility restores its context from the service schema and resumes work from where the previous run stopped.
On every run, STATE_CHECK_PREVIOUS_RUN reads the last saved state from the schema and determines the next state to transition into so the interrupted process can resume.
The mapping logic is as follows:
If there’s no saved state (STATE_NOT_DEFINED), this is a fresh run, and the machine transitions to STATE_REBALANCE_STARTED.
If a final state is recorded, the cluster is already balanced, and there’s nothing to do.
STATE_REBALANCE_EXECUTION_STARTED, STATE_REBALANCE_MOVES_SUCCEEDED, and STATE_REBALANCE_EXECUTION_AWAITING_SWITCHOVER_APPROVE_DONE all resolve to STATE_REBALANCE_EXECUTION_STARTED, because that state knows how to "heal" itself on entry: it first analyzes and fixes steps in ERROR status, and only then continues execution.
Rollback states transition to the next state within the same flow.
All other states transition to the next state in the main flow (Figure 3).
For RebalanceStepMoveMirror steps, ggrebalance performs a detailed analysis of every failed step on restart, based on the cluster’s actual state:
Catalog check.
It queries gp_segment_configuration for the given dbid.
If the segment’s hostname already matches the target, the catalog was updated before the failure.
Configuration file check.
If the catalog was updated, GpConfigHelper reads postgresql.conf on the target host to check the port parameter.
That shows how far gpmovemirrors had gotten before the failure.
Waiting for the segment to start.
If the port was also updated, a polling loop checks the segment’s state.
If the segment has started and entered synchronized mode, the step is marked DONE — no further action is needed.
Interactively extending the wait.
If the wait times out, interactive mode prompts the operator to extend it.
Retry/Rollback.
If the step still hasn’t recovered on its own, the operator is offered a choice: retry it or roll it back.
For switchover steps, the algorithm is much simpler: since gprecoverseg is idempotent, the operator is simply offered the choice to repeat the operation or roll it back.
Consider three example scenarios.
The cluster used here has eight hosts, sdw-1 through sdw-8, with eight primaries on each, for a total of 64 segments.
Mirrors are distributed across neighboring hosts using the grouped scheme.
A rebalance runs after shrinking to 56 segments and decommissioning the sdw-8 host:
$ ggrebalance -x 56 --remove-hosts="sdw-8" -v
SHRINK PLAN
================================================================================
Target Segment Count: 56
-------------------------------SEGMENTS TO REMOVE-------------------------------
Total segments to shrink: 8
[1] Segment Pair:
Primary:
Content: 56
DbId: 58
Host: sdw-8
Datadir: /data1/primary/gpseg56
Port: 10000
Mirror:
Content: 56
DbId: 122
Host: sdw-1
Datadir: /data1/mirror/gpseg56
Port: 10500
[2] Segment Pair:
Primary:
Content: 57
DbId: 59
Host: sdw-8
Datadir: /data1/primary/gpseg57
Port: 10001
Mirror:
Content: 57
DbId: 123
Host: sdw-1
Datadir: /data1/mirror/gpseg57
Port: 10501
[3] Segment Pair:
Primary:
Content: 58
DbId: 60
Host: sdw-8
Datadir: /data1/primary/gpseg58
Port: 10002
Mirror:
Content: 58
DbId: 124
Host: sdw-1
Datadir: /data1/mirror/gpseg58
Port: 10502
[4] Segment Pair:
Primary:
Content: 59
DbId: 61
Host: sdw-8
Datadir: /data1/primary/gpseg59
Port: 10003
Mirror:
Content: 59
DbId: 125
Host: sdw-1
Datadir: /data1/mirror/gpseg59
Port: 10503
[5] Segment Pair:
Primary:
Content: 60
DbId: 62
Host: sdw-8
Datadir: /data1/primary/gpseg60
Port: 10004
Mirror:
Content: 60
DbId: 126
Host: sdw-1
Datadir: /data1/mirror/gpseg60
Port: 10504
[6] Segment Pair:
Primary:
Content: 61
DbId: 63
Host: sdw-8
Datadir: /data1/primary/gpseg61
Port: 10005
Mirror:
Content: 61
DbId: 127
Host: sdw-1
Datadir: /data1/mirror/gpseg61
Port: 10505
[7] Segment Pair:
Primary:
Content: 62
DbId: 64
Host: sdw-8
Datadir: /data1/primary/gpseg62
Port: 10006
Mirror:
Content: 62
DbId: 128
Host: sdw-1
Datadir: /data1/mirror/gpseg62
Port: 10506
[8] Segment Pair:
Primary:
Content: 63
DbId: 65
Host: sdw-8
Datadir: /data1/primary/gpseg63
Port: 10007
Mirror:
Content: 63
DbId: 129
Host: sdw-1
Datadir: /data1/mirror/gpseg63
Port: 10507
---------------------------------BALANCE MOVES----------------------------------
Total moves planned: 8
[1] Move Segment(content=48, dbid=114, role=m) [7.85 GB]
From: sdw-8:10500:/data1/mirror/gpseg48
To: sdw-1:10508:/data1/mirror/gpseg48
[2] Move Segment(content=49, dbid=115, role=m) [7.85 GB]
From: sdw-8:10501:/data1/mirror/gpseg49
To: sdw-1:10509:/data1/mirror/gpseg49
[3] Move Segment(content=50, dbid=116, role=m) [7.85 GB]
From: sdw-8:10502:/data1/mirror/gpseg50
To: sdw-1:10510:/data1/mirror/gpseg50
[4] Move Segment(content=51, dbid=117, role=m) [7.85 GB]
From: sdw-8:10503:/data1/mirror/gpseg51
To: sdw-1:10511:/data1/mirror/gpseg51
[5] Move Segment(content=52, dbid=118, role=m) [7.85 GB]
From: sdw-8:10504:/data1/mirror/gpseg52
To: sdw-1:10512:/data1/mirror/gpseg52
[6] Move Segment(content=53, dbid=119, role=m) [7.85 GB]
From: sdw-8:10505:/data1/mirror/gpseg53
To: sdw-1:10513:/data1/mirror/gpseg53
[7] Move Segment(content=54, dbid=120, role=m) [7.85 GB]
From: sdw-8:10506:/data1/mirror/gpseg54
To: sdw-1:10514:/data1/mirror/gpseg54
[8] Move Segment(content=55, dbid=121, role=m) [7.85 GB]
From: sdw-8:10507:/data1/mirror/gpseg55
To: sdw-1:10515:/data1/mirror/gpseg55
================================================================================
The operator sent kill to ggrebalance while pg_basebackup was running for one of the batches.
The signal is intercepted, and the code calls shutdown().
By default, SIGTERM is forwarded to the child gpmovemirrors process, which then aborts pg_basebackup.
For the interrupted steps, the segment_move_steps table is left with IN_PROGRESS status, gp_segment_configuration has an already-updated segment address (gpmovemirrors updates the catalog before the backup starts), and the target host is left with a partially written PGDATA.
20260521:17:24:52:181448 ggrebalance:cdw:gpadmin-[DEBUG]:-Running Command: $GPHOME/bin/gpmovemirrors --skip-resource-estimation -a -i /tmp/ggrebalance_move_config_pid181448 -B 16 -b 16 20260521:17:27:38:182714 gpmovemirrors:cdw:gpadmin-[INFO]:-Shutting down gpmovemirrors... 20260521:17:27:38:181448 ggrebalance:cdw:gpadmin-[ERROR]:-ggrebalance failed: Failed to execute 'gpmovemirrors --skip-resource-estimation -a -i /tmp/ggrebalance_move_config_pid181448 -B 16 -b 16'
select * from gp_segment_configuration where content in (48, 49, 50, 51, 52, 53, 54, 55) and role='m';
dbid | content | role | preferred_role | mode | status | port | hostname | address | datadir
------+---------+------+----------------+------+--------+-------+----------+---------+-------------------------
114 | 48 | m | m | n | d | 10500 | sdw-1 | sdw-1 | /data1/mirror/gpseg48
115 | 49 | m | m | n | d | 10501 | sdw-1 | sdw-1 | /data1/mirror/gpseg49
116 | 50 | m | m | n | d | 10502 | sdw-1 | sdw-1 | /data1/mirror/gpseg50
117 | 51 | m | m | n | d | 10503 | sdw-1 | sdw-1 | /data1/mirror/gpseg51
118 | 52 | m | m | n | d | 10504 | sdw-1 | sdw-1 | /data1/mirror/gpseg52
119 | 53 | m | m | n | d | 10505 | sdw-1 | sdw-1 | /data1/mirror/gpseg53
120 | 54 | m | m | n | d | 10506 | sdw-1 | sdw-1 | /data1/mirror/gpseg54
121 | 55 | m | m | n | d | 10507 | sdw-1 | sdw-1 | /data1/mirror/gpseg55
(8 rows)
On the next ggrebalance run:
STATE_CHECK_PREVIOUS_RUN finds the STATE_REBALANCE_EXECUTION_STARTED state and resumes from there;
reset_in_progress_execution_steps() moves steps from IN_PROGRESS to ERROR:
select move_order, status, is_rollback from ggrebalance.segment_move_steps;
move_order | status | is_rollback
------------+-------------+-------------
0 | IN_PROGRESS | f
1 | IN_PROGRESS | f
2 | IN_PROGRESS | f
3 | IN_PROGRESS | f
4 | IN_PROGRESS | f
5 | IN_PROGRESS | f
6 | IN_PROGRESS | f
7 | IN_PROGRESS | f
(8 rows)
process_error_execution_steps_mirror_moves() reads the real state for each step, polling the remote host;
the rebalance then continues:
select * from gp_segment_configuration where content in (48, 49, 50, 51, 52, 53, 54, 55) and role='m';
dbid | content | role | preferred_role | mode | status | port | hostname | address | datadir
------+---------+------+----------------+------+--------+-------+----------+---------+-------------------------
116 | 50 | m | m | s | u | 10502 | sdw-1 | sdw-1 | /data1/mirror/gpseg50
120 | 54 | m | m | s | u | 10506 | sdw-1 | sdw-1 | /data1/mirror/gpseg54
121 | 55 | m | m | s | u | 10507 | sdw-1 | sdw-1 | /data1/mirror/gpseg55
114 | 48 | m | m | s | u | 10500 | sdw-1 | sdw-1 | /data1/mirror/gpseg48
118 | 52 | m | m | s | u | 10504 | sdw-1 | sdw-1 | /data1/mirror/gpseg52
119 | 53 | m | m | s | u | 10505 | sdw-1 | sdw-1 | /data1/mirror/gpseg53
115 | 49 | m | m | s | u | 10501 | sdw-1 | sdw-1 | /data1/mirror/gpseg49
117 | 51 | m | m | s | u | 10503 | sdw-1 | sdw-1 | /data1/mirror/gpseg51
(8 rows)
The -T 02:00 option is set.
After two hours, the timer triggers a soft shutdown and then sends itself SIGINT.
In soft mode, the running gpmovemirrors isn’t interrupted — ggrebalance waits for the current batch to finish processing and then exits.
You can start ggrebalance again the next evening in the same window and simply continue from the saved state.
Among all failure modes, this one is the most dangerous.
If the data-transfer phase for one of the mirrors on sdw-3 was in progress at that moment, the content is left with a primary and no mirror.
FTS won’t mark the primary as unavailable, and the cluster keeps serving queries, but without redundancy until the host recovers or gprecoverseg is run explicitly.
If sdw-3 is instead the original primary host temporarily acting as a mirror at the time of failure (phase 2 of the three-step scheme), the corresponding content also loses redundancy.
That’s why ggrebalance calls check_down_segments at the start of every run: it checks whether any primary is in down status, and if so, refuses to start and requires manual recovery.
That way, a rebalance can’t start on a cluster that’s already partially broken.
Rollback is triggered with the ggrebalance --rollback command (or -r).
Rollback begins by reading the saved plan, checking which steps have already completed, and building a reverse plan.
Three situations are possible:
The original plan execution was interrupted midway.
Some steps completed, others didn’t.
Incomplete steps are marked DONE without any action; completed steps are converted into reverse steps.
Rollback after a successful completion.
If the rebalance completed successfully (but cleanup wasn’t run), --rollback still works and restores the original distribution.
That’s handy if performance problems or issues with one of the new hosts surface after the rebalance.
The rollback itself was interrupted.
is_rollback_flow is determined by whether STATE_REBALANCE_ROLLBACK_STARTED is present in the status history, and the rollback resumes from where it stopped.
When rollback starts, the original list of moves is transformed according to the following rules in on_enter_STATE_REBALANCE_ROLLBACK_PREPARE_MOVES_STARTED:
move_order is reversed (the last move executed is the first one rolled back).
RebalanceStepMoveMirror keeps the same type but is flagged with is_rollback=True.
This swaps the source and target hosts in the gpmovemirrors config.
RebalanceStepSwitchoverToMirror becomes RebalanceStepSwitchoverToPrimary and vice versa.
Once preparation is done, the steps are saved to the schema, and control passes to the standard rebalance state machine loop.
No additional execution code is needed — rollback uses exactly the same infrastructure as the initial rebalance run.
The rollback itself is fully reentrant: if it was interrupted, running ggrebalance --rollback again detects that the rollback flow has already started and resumes from where it stopped, without re-preparing the steps.
For example, rolling back the interrupted rebalance from scenario 1 in the previous section looks like this:
$ ggrebalance -r
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-No time limit is set
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-local Greengage Version: 'postgres (Greengage Database) 7.4.1+dev.118.g033b9133b1 build 1+git033b913'
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-coordinator Greengage Version: 'PostgreSQL 12.22 (Greengage Database 7.4.1+dev.118.g033b9133b1 build 1+git033b913) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0, 64-bit compiled on May 14 2026 20:19:21 Bhuvnesh C.'
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-Init gparray from catalog
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-Starting rebalance rollback
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-Start preparing steps for rollback...
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-Saving following rollback rebalance execution steps:
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 0, status: PLANNED, type: mirror move:
Move Segment(content=55, dbid=121, role=m) [7.96 GB]
From: sdw-8:10507:/data1/mirror/gpseg55
To: sdw-1:10507:/data1/mirror/gpseg55
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 1, status: PLANNED, type: mirror move:
Move Segment(content=54, dbid=120, role=m) [7.96 GB]
From: sdw-8:10506:/data1/mirror/gpseg54
To: sdw-1:10506:/data1/mirror/gpseg54
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 2, status: PLANNED, type: mirror move:
Move Segment(content=53, dbid=119, role=m) [7.96 GB]
From: sdw-8:10505:/data1/mirror/gpseg53
To: sdw-1:10505:/data1/mirror/gpseg53
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 3, status: PLANNED, type: mirror move:
Move Segment(content=52, dbid=118, role=m) [7.96 GB]
From: sdw-8:10504:/data1/mirror/gpseg52
To: sdw-1:10504:/data1/mirror/gpseg52
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 4, status: PLANNED, type: mirror move:
Move Segment(content=51, dbid=117, role=m) [7.96 GB]
From: sdw-8:10503:/data1/mirror/gpseg51
To: sdw-1:10503:/data1/mirror/gpseg51
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 5, status: PLANNED, type: mirror move:
Move Segment(content=50, dbid=116, role=m) [7.96 GB]
From: sdw-8:10502:/data1/mirror/gpseg50
To: sdw-1:10502:/data1/mirror/gpseg50
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 6, status: PLANNED, type: mirror move:
Move Segment(content=49, dbid=115, role=m) [7.96 GB]
From: sdw-8:10501:/data1/mirror/gpseg49
To: sdw-1:10501:/data1/mirror/gpseg49
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 7, status: PLANNED, type: mirror move:
Move Segment(content=48, dbid=114, role=m) [7.96 GB]
From: sdw-8:10500:/data1/mirror/gpseg48
To: sdw-1:10500:/data1/mirror/gpseg48
20260521:18:24:46:192582 ggrebalance:cdw:gpadmin-[INFO]:-Saved rollback rebalance execution steps
20260521:18:24:47:192582 ggrebalance:cdw:gpadmin-[INFO]:-Rebalance - start moving segments:
20260521:18:24:47:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 0, status: IN_PROGRESS, type: mirror move:
Move Segment(content=55, dbid=121, role=m) [7.96 GB]
From: sdw-8:10507:/data1/mirror/gpseg55
To: sdw-1:10507:/data1/mirror/gpseg55
20260521:18:24:47:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 1, status: IN_PROGRESS, type: mirror move:
Move Segment(content=54, dbid=120, role=m) [7.96 GB]
From: sdw-8:10506:/data1/mirror/gpseg54
To: sdw-1:10506:/data1/mirror/gpseg54
20260521:18:24:47:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 2, status: IN_PROGRESS, type: mirror move:
Move Segment(content=53, dbid=119, role=m) [7.96 GB]
From: sdw-8:10505:/data1/mirror/gpseg53
To: sdw-1:10505:/data1/mirror/gpseg53
20260521:18:24:47:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 3, status: IN_PROGRESS, type: mirror move:
Move Segment(content=52, dbid=118, role=m) [7.96 GB]
From: sdw-8:10504:/data1/mirror/gpseg52
To: sdw-1:10504:/data1/mirror/gpseg52
20260521:18:24:47:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 4, status: IN_PROGRESS, type: mirror move:
Move Segment(content=51, dbid=117, role=m) [7.96 GB]
From: sdw-8:10503:/data1/mirror/gpseg51
To: sdw-1:10503:/data1/mirror/gpseg51
20260521:18:24:47:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 5, status: IN_PROGRESS, type: mirror move:
Move Segment(content=50, dbid=116, role=m) [7.96 GB]
From: sdw-8:10502:/data1/mirror/gpseg50
To: sdw-1:10502:/data1/mirror/gpseg50
20260521:18:24:47:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 6, status: IN_PROGRESS, type: mirror move:
Move Segment(content=49, dbid=115, role=m) [7.96 GB]
From: sdw-8:10501:/data1/mirror/gpseg49
To: sdw-1:10501:/data1/mirror/gpseg49
20260521:18:24:47:192582 ggrebalance:cdw:gpadmin-[INFO]:-[ROLLBACK] Rebalance step with move_order: 7, status: IN_PROGRESS, type: mirror move:
Move Segment(content=48, dbid=114, role=m) [7.96 GB]
From: sdw-8:10500:/data1/mirror/gpseg48
To: sdw-1:10500:/data1/mirror/gpseg48
20260521:18:28:12:192582 ggrebalance:cdw:gpadmin-[INFO]:-Rebalance - end moving segments
20260521:18:28:12:192582 ggrebalance:cdw:gpadmin-[INFO]:-Rebalance rollback is complete
20260521:18:28:12:192582 ggrebalance:cdw:gpadmin-[INFO]:-------------------------------------SUMMARY-------------------------------------
20260521:18:28:12:192582 ggrebalance:cdw:gpadmin-[INFO]:-====================================================================================
20260521:18:28:12:192582 ggrebalance:cdw:gpadmin-[INFO]:-REBALANCE
20260521:18:28:12:192582 ggrebalance:cdw:gpadmin-[INFO]:-====================================================================================
20260521:18:28:12:192582 ggrebalance:cdw:gpadmin-[INFO]:-Segments moved: 0
20260521:18:28:12:192582 ggrebalance:cdw:gpadmin-[INFO]:-Rolled back moves: 8
20260521:18:28:12:192582 ggrebalance:cdw:gpadmin-[INFO]:-Cancelled moves: 0
After the rollback completes, the ggrebalance schema is dropped, and the cluster returns to exactly the distribution it had before the original run.
One warning, though: a shrink can’t be rolled back once it’s done, since the segments are already gone from the cluster.
Consider a scenario on our 8x8 cluster.
Initially, there are 8 hosts, sdw-1 through sdw-8, with 8 primaries on each, using the grouped mirroring strategy.
The scenario:
Some data has been deleted, and the number of primary segments needs to shrink from 64 to 48.
The sdw-5 host is marked for decommissioning (planned hardware replacement).
A new host, sdw-9, is introduced in its place.
The entire workflow can be performed with a single command:
$ ggrebalance -x 48 --remove-hosts="sdw-5" --add-hosts="sdw-9" \
--target-datadirs="/data1/primary/gpseg{content}, /data1/mirror/gpseg{content}"
For clarity and to demonstrate some additional options, we’ll cover the scenario step by step:
$ ggrebalance -x 48 -S --skip-rebalance
The -S (--simple-progress) option turns on basic progress reporting via the ggrebalance.rebalance_progress view.
At this step, ggrebalance identifies the 16 excess content IDs from the end, redistributes table data onto the remaining 48 segments via ALTER TABLE … REBALANCE, updates the gp_segment_configuration catalog, and stops the shrunk segments.
Passing -S lets you track shrink progress:
select * from ggrebalance.rebalance_progress;
stat_name | stat_value
---------------------------------+------------
1.1. Tables shrunk | 17
1.2. Tables shrink in progress | 7
1.3. Tables left to shrink | 0
(3 rows)
-D (--detailed-progress) adds shrink speed metrics and a remaining-time estimate to the progress output.
$ ggrebalance -x 48 -R sdw-5 -A sdw-9 -d /data1/primary/gpseg{content},/data1/mirror/gpseg{content}
The planner sees that one host is leaving while another is being added and builds a move-minimizing plan that removes segments from sdw-5 and distributes all segments evenly across hosts while respecting the mirroring strategy.
You can preview the plan before running it:
$ ggrebalance -x 48 -R sdw-5 -A sdw-9 -d /data1/primary/gpseg{content},/data1/mirror/gpseg{content} -p
There are situations where scaling a Greengage cluster or changing its topology can only happen during a specific period of time — a maintenance window. Suppose the maintenance window runs from 22:00 to 06:00. In that case, you can run the rebalance with a timer and automatic switchover approval:
$ ggrebalance -x 48 -R sdw-5 -A sdw-9 \
-d /data1/primary/gpseg{content},/data1/mirror/gpseg{content} \
-E '2026-04-13 06:00:00' \
-y \
-n 8 -B 16
-E — soft termination at 6 a.m. without interrupting the currently running gpmovemirrors.
-y — skip confirmation prompts before switchover.
-n 8 — up to 8 parallel operations (table redistribution, segment moves).
-B 16 — up to 16 parallel workers per host.
If it doesn’t finish in time, just run ggrebalance with no arguments the next night, and the operation continues.
Additional options:
Build the plan and print it without executing anything. Useful for auditing before making changes.
A fixed seed for the solver. Reproducibility matters for testing and for getting sign-off from the DBA team.
Allows temporarily placing the primary and mirror of the same content on the same host. Makes the plan more flexible in tight configurations but reduces fault tolerance during execution.
Perform only the shrink or catalog expansion without moving segments between hosts. Useful when you want to spread the stages across time.
Choose the mirror placement strategy after the rebalance.
The allowed mirror lag, in GB, when running gprecoverseg to move segments.
A smaller value means higher consistency but a greater chance of timing out.
Remove the service schema after a successful completion. Without it, a subsequent run fails, assuming there’s an unfinished operation.
Before covering the recommendations, it’s important to understand exactly what downtime ggrebalance can cause and what contributes to it.
ggrebalance was designed to be "online", but that doesn’t mean zero downtime.
As already mentioned when describing the data-transfer method, moving mirrors doesn’t affect availability at all, since mirrors don’t serve user queries.
While gpmovemirrors copies data and replays the outstanding WAL, the primary continues operating normally.
The only consequence is that the content has no backup copy during this time, which raises risk but doesn’t cause downtime.
Role switchover, on the other hand, causes some downtime.
Every primary/mirror role switch via gprecoverseg -r introduces downtime that consists of:
stopping the old primary in fast mode;
waiting for the FTS probe to detect the unavailability — up to gp_fts_probe_interval;
promoting the mirror to primary — depends on data volume;
updating the gp_segment_configuration catalog.
In a test run, fully moving a primary segment (two switchovers plus two mirror moves) totaled about 20 minutes of unavailability per content, spread across the whole operation.
Note that the --parallel option increases the number of concurrent switchovers.
Before starting, it’s a good idea to:
Run a cluster health check: confirm all segments are up and synchronized with their mirrors, and that role matches preferred_role.
Keep in mind that moving one mirror requires as much free space on the target host as the source primary’s data occupies. For our 8x8 cluster with a 200 GB segment size, that means 1.6 TB of free space on each receiving host.
Remember that pg_basebackup is bottlenecked by the network between the source and target hosts: transfer time is, roughly speaking, directly proportional to segment size and inversely proportional to interface bandwidth.
Accordingly, sufficient network bandwidth is needed to make the operations faster.
During the run:
Monitor progress through the logs in gpAdminLogs/ggrebalance[_timestamp].log.
Track replication between segments.
Watch host resources: iostat, nload, df -h on the hosts involved.
If a disk or network is already saturated, increasing --parallel won’t make things faster — if anything, it will make things slower due to increased contention.
After completion:
gpstate -e. Confirm all segments are up and role == preferred_role.
gpcheckcat. Check catalog consistency.
ANALYZE. Or ggrebalance --analyze at startup.
ggrebalance --clean. Remove the ggrebalance service schema.
What to do in case of a failure:
Don’t run gprecoverseg or other utilities manually.
That can break the state ggrebalance relies on.
Read the log in gpAdminLogs/ggrebalance[_timestamp].log. The last messages usually indicate the cause.
Run ggrebalance again.
It will analyze the state and suggest what to do next.
If the suggested path isn’t possible, use --rollback to roll back or --clean to reset (only if the cluster is actually in a healthy state).
In case of a critical failure: gpstop -ar, check gp_segment_configuration with a query, manually restore the original state, and contact support.
This series of articles on ggrebalance covered the utility’s key aspects: its architecture and usage scenarios, the planner implementation, and the execution side.
Taken together, that’s the full picture of a tool built for a specific operational problem: keeping the load evenly distributed as the topology continues to change.
ggrebalance covers something Greengage operations lacked before: a way to change cluster topology without taking the cluster offline.
The approach relies on a custom planner, a shrink implementation with an optimized table-redistribution mechanism, and a segment-move executor that supports recovery after interruption and operation rollback, all combined with the standard gpmovemirrors/gprecoverseg utilities.
That combination is what lets ggrebalance execute online, recover cleanly after interruptions, and roll back changes when needed.
How much this matters in practice is clear from a comparison with the alternatives: gpshrink (a shrink-only tool) and the manual approach using gpmovemirrors + gprecoverseg separately.
| Feature | ggrebalance | gpshrink (CloudberryDB) | Manual |
|---|---|---|---|
Scaling and topology changes |
|||
Shrink |
+ |
+ |
- |
Balancing segments across hosts |
+ |
- |
- |
Adding hosts |
+ |
- |
+ |
Decommissioning hosts |
+ |
- |
+ |
Moving and reducing segment count |
|||
Table redistribution method |
INSERT (performance optimized) |
CTAS |
- |
Moving mirrors |
+ |
- |
+ |
Moving primary without stopping the cluster |
+ |
- |
+/- |
Parallel execution of moves |
+ |
+ |
+ |
Planning |
|||
Automatic move-plan generation |
+ |
- |
- |
Plan preview without execution |
+ |
- |
- |
Accounts for the mirroring strategy (grouped / spread) |
+ |
- |
- |
Free-space estimation before running |
+ |
- |
+ |
Reliability and recovery |
|||
Shrink reentrancy |
+ |
- |
- |
Shrink rollback (partial, before redistribution) |
+/- |
+/- |
- |
Rebalance reentrancy |
+ |
- |
- |
Rebalance rollback |
+ |
- |
- |
Maintenance window support (timer) |
+ |
+ |
+ |
Storing operation state in the DBMS |
+ |
+/- |
- |
Cluster health check before running |
+ |
+ |
- |
Observability |
|||
Runtime monitoring via SQL views |
+ |
+ |
- |
Operation status tracking |
+ |
+ |
- |
Logging |
+ |
+ |
+ |
Log verbosity control |
+ |
+ |
+/- |
The manual approach covers only basic segment operations, and only if the operator takes on planning, state tracking, and failure recovery themselves.
gpshrink automates data redistribution during a shrink but doesn’t address balancing the physical placement of segments across hosts.
ggrebalance combines both tasks — shrink and rebalance — into a single managed process with the full set of properties needed for long-running operations in production.
Across these three articles, we’ve covered the main scenarios: shrink, balancing, adding hosts, and decommissioning them. But that’s only the first stage in the tool’s evolution — a few directions are next in line for further work.
Right now, ggrebalance can add hosts only as long as the segment count doesn’t increase.
Full expand support meaning an increase in segment count along with data redistribution is the next big chunk of work.
It wasn’t included in the first release because of the existing, well-established gpexpand tool.
In the current implementation, every primary/mirror role switch causes a brief segment outage.
pg_basebackup is the bottleneck for large volumes.
The cluster also temporarily loses redundancy during a gpmovemirrors call, and we’re currently exploring ways to shrink that window.
The current planner builds the plan statically, without accounting for segment size estimates or potential downtime. An adaptive planner could minimize downtime by reducing the number of switchovers, at the cost of more moves overall. There’s also demand for a scenario that changes the mirroring strategy, from grouped to spread and back.
Currently, progress is available only through an SQL view in the ggrebalance schema.
Exporting metrics to dashboards and other monitoring tools would make it easier to monitor long-running operations.
The long-term goal is to make topology changes in a Greengage cluster as routine as running vacuum — no lengthy planning, no maintenance window, no manual oversight of every step.