Conversation
The pgBackRest retention policy is enforced at the end of every base backup, dropping the expired backups from the repositories, but nothing removed the Backup resources that describe them. CloudNativePG garbage collects those resources only for the backups it takes itself: deleteBackupsNotInCatalog runs from the in-tree barmanObjectStore backup path, and its useSameBackupLocation filter skips every backup of a cluster without spec.backup.barmanObjectStore, which is every plugin based backup. The barman-cloud plugin reimplements that cleanup in its own sidecar, this plugin had no equivalent. The result is one Backup resource per backup per cluster piling up for as long as the cluster lives. On the deployment where this was found that had reached 3417 resources over 120 clusters, growing by 60 a day, the oldest of them five months old and describing data that had left the repository months earlier. Besides the etcd footprint, those resources keep advertising restore points that cannot be restored. The instance plugin now reads the catalog right after the backup command returns, which is exactly when the retention policy has just been enforced, and deletes the Backup resources whose backup ID is no longer in it. That costs one additional pgbackrest info call per backup. Deleting a resource on the strength of a catalog means the catalog has to be trustworthy, so it is acted upon only when it describes exactly the repositories that are configured and every one of them could be read in full. "pgbackrest info" does not fail when a repository of a stanza cannot be read: it flags that repository and returns what the others hold, and acting on such an answer would delete the resources of every backup that lives only in the repository we could not reach. A repository holding no backup yet is a complete answer rather than a partial one, and it is the normal state of the second repository of a stanza, since pgBackRest writes a backup to a single one and reports the stanza as "mixed" from then on. An empty catalog is skipped as well, a backup having just been taken, and a catalog describing another stanza is an error. Only the resources this plugin can attribute to itself and to this location are considered: the backup has to belong to this cluster, to carry this plugin in its pluginMetadata and to have completed. The cluster UID, the stanza and the repositories are now recorded in the pluginMetadata, so that the resources of a cluster recreated under the same name, of another stanza, or of an Archive since pointed at a different bucket are left alone. Resources written before those entries existed carry no location and are attributed to the current one, which is the only one they can be checked against; the README says so. Deletions carry the resource UID as a precondition, so a resource recreated between the listing and the deletion is not removed by mistake. A failure of the cleanup is logged and swallowed, since it must not turn a backup that succeeded into a failed one. The Backup resources are kept out of the sidecar client cache: they are listed once per backup, and caching them would require the cluster wide watch that the namespaced RBAC of the instance does not grant. One consequence worth expecting: the operator logs a "Reconciler error ... not found" line for every Backup resource that is deleted, so the first backup after an upgrade, which clears the accumulated backlog, emits a burst of them. Refs cloudnative-pg/cloudnative-pg#11483 Signed-off-by: chobostar <chobostar85@gmail.com>
Contributor
Author
|
Closing in favour of #152, opened first and implementing the same cleanup as a periodic runnable |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: delete the Backup resources of expired backups
pgBackRest enforces the retention policy at the end of every base backup and drops the expired
backups from the repository. Nothing removes the
Backupresources that describe them, so theyaccumulate for as long as the cluster lives and keep advertising restore points whose data is
long gone.
Why nothing removes them today
CloudNativePG does have this cleanup, but only for the backups it takes itself:
deleteBackupsNotInCatalogis called from
backupMaintenanceon the in-tree
barmanObjectStorepath, and itsuseSameBackupLocationfilter returns false for any cluster without
spec.backup.barmanObjectStore, which is everyplugin based backup. So even if the code were reached, it would skip our resources.
The barman-cloud plugin ran into the same gap and reimplemented the cleanup in its own sidecar:
deleteBackupsNotInCatalog,attributing resources through a
clusterUIDrecorded in theplugin metadata.
This plugin had no equivalent.
What it looks like in practice
Numbers from one of our staging environments, running this plugin on 120 CloudNativePG clusters
with a two day retention:
backups.postgresql.cnpg.ioresourcescompleted, describing data the repository dropped after two daysThose 5 MB are not about to fill anything up on their own: at 60 objects a day, backups alone
would need decades to matter. What makes it worth fixing is the kind of budget they spend.
etcd's backend is hard capped, at
2 GiB by default and 8 GiB as the largest size upstream suggests.
That cap is per member and shared by every object in the Kubernetes cluster, and reaching it does
not degrade backups, it stops the cluster: etcd raises a cluster-wide
NOSPACEalarm and enters"a maintenance mode which only accepts key reads and deletes"
(maintenance guide) until an
operator frees space, defragments and disarms the alarm. Every controller in the cluster is
read-only until then.
Against that budget this is a leak, in the precise sense: it grows linearly with the size of the
fleet and with uptime, it is never reclaimed, and nothing about it is load bearing. It also costs
more than the size of the objects. A
Backupis created and then patched as it runs andcompletes, and every revision stays in the backend until the apiserver's next compaction, every
five minutes by
default.
Compaction only frees those revisions inside the file; the file itself does not shrink until
someone defragments, since
"deleting application data does not reclaim the space on disk".
Beyond the storage, every one of those objects is also carried by the operator's informer cache
and returned by every
LISTover backups.And the other half of the motivation has nothing to do with space: each of those 3400 resources
claims a backup exists.
What the change does
The instance sidecar reads the catalog right after
pgbackrest backupreturns, which is exactlywhen the retention policy has just run, and deletes the
Backupresources whosestatus.backupIDis no longer in it. That costs one additionalpgbackrest infoper backup.What keeps it from deleting a valid resource
This code deletes Kubernetes resources, so most of the diff is about not deleting the wrong
ones:
pgbackrest infodoes not fail when a repository of astanza cannot be read: it flags that repository
(
infoStanzaErrorAddsets code 99) and still returns what the others hold. Acting on that answer would delete the
resources of every backup living only in the unreachable repository. The catalog is now used
only when it reports exactly the configured number of repositories and each of them could be
read in full.
writes a backup to a single repository
(
backup.c:"repo option not specified, defaulting to repo1"), so the second repository of a stanza sits at
code 2 and the stanza reports 4 "mixed"
(
info.c),which is the normal steady state rather than a failure. Codes 0 and 2 are accepted; 1, 3, 5 and
99 are not.
(endpoint, bucket and path, in pgBackRest's own order) go into
status.pluginMetadata, and aresource is only considered when all of the entries it carries match the current ones. This
covers an
Archiverepointed at another bucket, a stanza rename, and a cluster recreated undera name whose previous
Backupresources outlived it.the failure, running ones are none of our business.
means the repository is not the one we expect, not that everything expired.
listing and the deletion is not removed by mistake; the resulting conflict is tolerated, as is
a
NotFound.failed one, which is also what CloudNativePG's own
backupMaintenancedoes.Backupresources are excluded from the sidecar client cache: they are listed once per backup,and caching them would need a cluster wide watch, while the Role CloudNativePG builds for the
instance grants
list,getanddeletenamespaced.
No RBAC change is needed, and no CRD change either.
What to expect after upgrading
current one, since that is the only thing they can be checked against, and they never acquire
one: if the stanza or the repositories of a cluster ever change, those older resources are
deleted by the next backup. Their data is not touched. This is the deliberate trade that makes
the change useful for existing installations; the README says so.
the repositories of an
Archive, leaves the resources written before the change behind forgood. They have to be removed by hand. This direction leaks rather than over-deletes, which is
the way round we chose.
Backupcontroller log aReconciler error … not foundline (cloudnative-pg#11483),
so that first backup emits a burst of them. Nothing is wrong, but log based alerting will
notice.
Out of scope
Deleting a
Backupresource still removes nothing from the repository, which is#30 and the opposite
direction. It cannot be done the same way: the
Backupservicein cnpg-i has no deletion RPC, so the plugin would have to own a finalizer, and pgBackRest only
exists in the sidecar image, so that finalizer could only be cleared while a primary is running.
I have a separate write-up of the design and its failure modes and will post it on that issue.
Worth noting that this change makes resource deletion routine, so whatever semantics #30 lands
on will apply to many more deletions than today.
Testing
internal/cnpgi/instanceand 18 ininternal/pgbackrest/catalog, covering eachattribution rule and each catalog criterion separately.
pgbackrest infooutput, so the JSON tagscannot silently stop matching, and from literal catalogs, so each criterion fails on its own.
the recorded
clusterUID, the stanza comparison, or the error aggregation each makes the suitefail.
golangci-lintwith the repository configuration reports no issues;controller-genandkustomizeproduce no changes toconfig/,api/ormanifest.yaml.retention.full: 1, two backups and an assertion that the firstBackupresource disappears would pin the whole path including RBAC and the real status codes.Happy to add it if you would like it in this PR.
Open questions
existing installation clean up its backlog, but it means a later repoint deletes them.
barman-cloud chose the opposite: its
comparison
requires the
clusterUIDto match, so resources without one are never deleted. A middleground would be a one time adoption that writes the current location into matching legacy
resources on the first run. Tell me which you prefer.
runs inside the backup RPC, which is exactly when expiry happened and needs no extra runnable
or interval setting, at the cost of not reacting to an
expirerun out of band.PgbackrestBackupDatabase.RepoKeyis tagged
json:"repo_key"while pgBackRest emitsrepo-key, so the field never unmarshals.It is unused today, so nothing is broken, but it is exactly what Deleting the
Backupresource doesn't delete the backup files from the MinIO bucket #30 would need to tell whichrepository a backup lives in. Say the word and I will send a separate PR.