PostgreSQL major upgrade on Percona Operator without dump, restore, or data loss

- Reading
- 5 min
- Tables
- 1
- Code
- 3
The Percona Operator for PostgreSQL now supports in-place major version upgrades via pg_upgrade, eliminating the need for dump/restore or logical replication. The feature became GA in version 2.9.0, but requires attention to collation, extensions, and logical replicas.
The bottleneck this feature solves
Until this feature arrived, upgrading a Percona Operator for PostgreSQL cluster from one major version to another (from PostgreSQL 16 to 17, for example) was a problem with no elegant solution inside Kubernetes. According to Percona, there were only two practical ways out: manual dump/restore to a new cluster, or logical replication. Running pg_upgrade manually, directly on the pod's volume, was never a path supported by the operator.
Neither alternative is trivial in production. Provisioning a parallel cluster and migrating the data manually is an expensive operation prone to human error. Logical replication has known limitations: not everything replicates transparently, and the setup needs to be reconciled afterward. Touching the storage layer outside the operator's controller is the classic recipe for leaving the cluster in an inconsistent state.
| Approach | Main risk | Operational effort |
|---|---|---|
| Manual dump/restore | Human error, parallel infrastructure | High: provision a new cluster |
| Logical replication | Not everything replicates (DDL, some data types) | Medium: reconcile setup afterward |
| Manual pg_upgrade in the pod | Not supported by the operator, risk of inconsistency | High: operates outside the controller |
| Major Upgrade (PerconaPGUpgrade) | Downtime during the pause; extensions and logical replicas require manual reconciliation | Low: orchestrated by the operator |
Percona addressed this gap with a major upgrade feature launched as a technical release in operator v2.4.0 and promoted to general availability (GA) in operator v2.9.0, after going through a few maturation releases, according to the official engineering post (Anil Joshi, November 9, 2026).
How it works under the hood
The mechanism isn't magic: it's pg_upgrade running in in-place mode, orchestrated by the operator via a custom resource called PerconaPGUpgrade. The documented flow follows a fixed sequence:
- You create a
PerconaPGUpgraderesource pointing to the cluster and the target version. - The operator pauses the cluster and shuts down the pods.
- An upgrade Job runs
pg_upgrade --linkon the primary instance's volume, creating a new data directory (for examplepg17) next to the old one (pg16). - The replicas are resynced from the already-upgraded primary.
- The cluster restarts on the new major version.
Here's the point that, as a DBA, I wouldn't let slide: this is not zero-downtime. The cluster is paused and shut down during the process. The real gain from this feature is not needing dump/restore or logical replication, with a shorter and more predictable maintenance window, not the absence of a window.
The three risks the Percona post lists
Percona's engineering team is direct about what needs attention before triggering the upgrade:
- OS and glibc change: starting with operator v2.6.0, the base image switched from UBI8 to UBI9, which changes the glibc version from 2.28 to 2.34. This directly affects the collation used by indexes.
- Extensions aren't upgraded automatically:
pgaudit, for example, needs to be removed (DROP EXTENSION) and recreated in every database where it's installed. - Logical replicas are invalidated: the major upgrade removes existing logical replicas. After the upgrade, each one needs to be checked and reseeded manually.
We should always compare the complete patch version, as it might happen that we have the same Major/Minor version, say RHEL(8.x); however, the Patched version (8.x.x) is different and ultimately affects the underlying glibc.
Anil Joshi, Percona
Diagnosing the environment before touching anything
Before any upgrade, the first step is to assess the cluster's actual state: operator version, Postgres version, OS, and glibc of the image in use. It's the same reasoning behind any well-done database migration: you don't touch anything without knowing exactly where you're starting from.
kubectl get pods -n postgres-operatorIn the post's reference environment, the output shows the cluster running with three instances, pgBouncer, and the operator active. Inside one of the pods, you can confirm the OS and glibc directly:
cat /etc/os-release
getconf GNU_LIBC_VERSION
rpm -q glibcIn the documented case, the output shows RHEL 8.10 with glibc-2.28-251.el8_10.2.x86_64, exactly the version that changes when the operator switches to UBI9.
The upgrade path: operator first, incrementally
The PostgreSQL 17 image only becomes available starting with operator v2.6.0. But you can't jump straight from v2.4.0: the supported path is incremental (2.4.0 → 2.5.1 → 2.6.0). Each step applies the CRD, the RBAC, and patches the operator deployment's image.
kubectl apply --server-side -f https://raw.githubusercontent.com/percona/percona-postgresql-operator/v2.5.1/deploy/crd.yaml
kubectl apply --server-side -f https://raw.githubusercontent.com/percona/percona-postgresql-operator/v2.5.1/deploy/rbac.yaml -n postgres-operator
kubectl -n postgres-operator patch deployment percona-postgresql-operator \
-p'{"spec":{"template":{"spec":{"containers":[{"name":"operator","image":"docker.io/percona/percona-postgresql-operator:2.5.1"}]}}}}'After confirming the rollout with kubectl rollout status deployments percona-postgresql-operator -n postgres-operator, the same trio of commands is repeated for tag 2.6.0. The important point here: updating the operator doesn't touch the database. The PostgreSQL pods remain on the same version, with the same OS and glibc, because the operator and the data cluster have separate lifecycles. Only after reaching v2.6.0 does the PostgreSQL 17 image become available for PerconaPGUpgrade.
What to do in practice, as a DBA
With the operator already on v2.6.0 (or, even better, straight to GA version v2.9.0, recommended by Percona itself), the checklist I'd follow before triggering PerconaPGUpgrade is this:
- Complete, tested backup of the cluster: the in-place upgrade touches the primary's volume, and without a validated backup there's no plan B.
- Inventory of extensions installed in each database, cross-checked against the list of those that don't upgrade automatically.
- Inventory of every active logical replica, already knowing it will be invalidated and will need to be recreated from scratch.
- Collation validation and, if necessary, reindexing after switching from UBI8 to UBI9.
- Communicated maintenance window: the cluster pauses and restarts, so zero downtime isn't the right promise to make to anyone who depends on this database.
Percona also advises against a tempting shortcut: using custom images to keep UBI8 running with operator v2.6.0 and PostgreSQL 17. The post is clear that this path isn't recommended and can generate untested failures; the supported route is to migrate the OS alongside the operator and treat collation as an expected consequence.
What's still left open
Percona's post doesn't detail the average pause time for large databases, nor how pg_upgrade --link behaves on very large volumes inside Kubernetes PVCs. It's a point worth testing in staging before taking it to production. The material also provides no official guidance on rollback if the upgrade fails partway through: the old directory (pg16) sits next to the new one, but the procedure for reverting isn't described. Anyone adopting the feature should test this failure scenario in isolation before relying on it in a critical environment.
Translated from the Brazilian Portuguese original · Read the original
PostgreSQL 18: Thirteen Small Measurements That Really Change Performance
A hand-crafted benchmark on PostgreSQL 18.6 measures indexes, WAL, and work_mem with real numbers: some gains reach 270 times, others aren't worth the effort.





