Replies: 8 comments 12 replies
|
@jmealo question: a very common deployment configuration is local NVMe. I need to go look up exactly which CSIs are most common, but if this is on ext3 then I'm pretty sure for most people Total disk: 10G The disk is 80% full. Will the first database show 10% utilized based on the 1G database size, or will it show 80% utilized based on all databases? |
|
Another topic - thresholds. Straight percentages can be problematic. trigger:
step:
|
|
Third minor topic - disk shrinking. At a minimum, probably will need a doc "here's how to shrink your disk"... even if the doc says "currently the only option is to restore a backup" i think it just has to be documented |
|
suggestion for scoping this down:
The idea would be this is narrowly targeting the boring case of slow data growth over time. Cases where there is very rapid growth probably indicates something that should be investigated, or somebody is doing a data load and they just completely sized the disk wrong. Focusing more narrow, and being more conservative like this might also make it a little easier as a V1 to ship. Also in light of the cooldown concerns above I think we really want to avoid frequent automated resize, because if we mistakenly do a 5% increase when someone starts a data load, they could be blocked for a long time until the disk allows a next resize operation - which would mean they're stuck and can't finish their data load as a result of our auto-resize operation. I think we also need to carefully document this, in addition to trying to stick with a reasonably conservative approach In fact, I might even see cases where I want to say "you know, I really only want one automated operation per week" or something ~~ I need to think carefully about implications of getting locked out of a manual disk resize 🤔 Or -- "i want automated operations to only run in this maintenance window" -- that might be a really good answer to operational concerns around getting locked out (in this case the maint window isn't about downtime with the resize op itself, but about the lockout period w subsequent resize afterwards) |
|
Thanks for initiating that discussion, with a comprehensive plan. It's a topic we listed on our todo list in my company and we surely didn't consider all the possible challenges that you outlined. A few points to consider:
|
|
If anyone is interested in taking an alpha of this for a spin I have a branch tested on Azure AKS and the images pushed to ghcr.io. It's more or less the entire RFC. There's two E2E tests that have timing issues on Azure. I'm going to review my branch another couple times before I share. I need to add some additional test coverage, however, I think a dynamic approach is better than this for several reasons I detail in my follow-up RFC for dynamic sizing (request/limit). Working on a branch that implements that. |
|
Nice documentation, thanks 👍 Regarding the PVC shrink (for PGDATA or PGWAL volumes), we have a procedure that can be enriched thanks to the
|
|
This is a fantastic work, what is the status ? We are almost every day resize WAL PVC ^^ |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
RFC: Automatic PVC Resizing with WAL-Aware Safety for CloudNativePG
Companion Feature Request: Automatic PVC Resizing with WAL-Aware Safety Checks
Summary
This RFC proposes adding native automatic PVC resizing to CloudNativePG. The feature monitors disk usage from within PostgreSQL pods using filesystem
statfs()syscalls, exposes usage metrics via Prometheus, and automatically expands PVCs when configurable thresholds are exceeded. Central to the design is WAL-aware safety logic that prevents auto-resize from masking underlying issues like archive failures or replication lag. These conditions, left unaddressed, lead to data loss.The configuration uses a behavior-driven model inspired by the Kubernetes HorizontalPodAutoscaler v2 scaling behaviors. Rather than treating resize as a static value, expansion is defined as a dynamic behavior constrained by clamping logic and rate-limited by cloud provider realities. This design evolved from an earlier, simpler proposal that proved insufficient to handle the full range of volume scales safely.¹
This document covers the full design in four sections: motivation and community context, architecture and data flow, API surface and implementation details, and the testing and rollout strategy.
Motivation
The Problem
PostgreSQL storage requirements are difficult to predict. Databases grow over time, and running out of disk space causes PostgreSQL crashes or read-only mode, WAL archiving failures, replication breakage, and service outages. Today, CNPG supports manual PVC resizing by updating
spec.storage.size, but this requires monitoring disk usage externally, manual intervention to update the Cluster resource, and carries the risk of human error or delayed response.The impact is real and recurring across the CNPG community. Issue #9927 reports clusters that enter unrecoverable states when disk fills. Issue #1808 describes a deadlock where the operator refuses to create the primary instance because disk is full, but can't expand the disk because there's no running instance. Issue #9301 found a circular dependency where "you can't increase storage because CNPG won't operate until you increase storage."
WAL volumes are especially dangerous. Issue #6152 reports a WAL PVC that won't grow even after manual intervention because WAL accumulates faster than it can be archived. Issue #8791 highlights documentation gaps for dealing with WAL disk exhaustion. Issue #7827 found replicas that continue reporting healthy status even after I/O errors from storage exhaustion.
Why Not Use Existing External Solutions?
External solutions like topolvm/pvc-autoresizer exist but have critical limitations for PostgreSQL workloads:
Issue #7100 specifically requested per-PVC label and annotation support to enable TopoLVM integration, and issue #9385 directly requested native storage autoscaling support. PR #7064 (still open as a draft) proposed automatically switching clusters to read-only on high disk usage, a complementary but incomplete approach.
Community Context: Related Issues
The following open and closed issues informed this design:
Open Issues Directly Addressed
statfs()on the actual mount point gives accurate resultsClosed Issues That Informed Design Decisions
limitfield prevents exceeding CSI/platform limitsstatfs()vs. PVC spec comparisonRelated Pull Request
Goals and Non-Goals
Goals
statfs()syscalls (not K8s PVC status or SQL)Non-Goals
Reclaiming Disk Space (PVC Shrinking Alternatives)
Kubernetes does not support shrinking PVCs. Once a volume is expanded, it cannot be made smaller. This is a platform limitation, not a CNPG limitation. However, operators still need a path to reclaim over-provisioned storage. The following approaches are available today and should be documented alongside this feature:
Restore from backup to a smaller cluster. Create a new Cluster resource with a smaller
spec.storage.sizeand restore from an existing backup. This is the simplest approach and preserves PITR capability. CNPG's recovery-from-backup workflow handles this natively.Logical export and re-import. Use
pg_dump/pg_restore(orpg_dumpall) to export data from the existing cluster and import into a new cluster with smaller storage. This works for any PostgreSQL version but does not preserve physical backup history.Logical replication. Set up logical replication from the over-provisioned cluster to a new cluster with smaller storage, then switch over. This approach minimizes downtime but requires PostgreSQL 10+ and may not replicate all object types (sequences, large objects, DDL).
Documentation for these approaches should ship alongside the auto-resize feature so that operators have a clear path for both growth and reclamation.
Background: Current CNPG Storage Architecture
Three PVC Types
CNPG manages three types of PVCs per instance:
{cluster}-{n}/var/lib/postgresql/data{cluster}-{n}-wal/var/lib/postgresql/wal{cluster}-{n}-tbs-{name}/var/lib/postgresql/tablespaces/{name}Current Manual Resize Flow
This is implemented in
pkg/reconciler/persistentvolumeclaim/existing.goviareconcilePVCQuantity(), which compares the requested size against the actual PVC spec and only allows increases. TheShouldResizeInUseVolumes()function on the Cluster type (defaulting totrue) controls whether online resize is attempted.Existing Disk Space Infrastructure
CNPG already has a disk probe mechanism in
pkg/management/postgres/instance.go:This infrastructure exists but is only used at startup and isn't exposed as metrics.
Why
statfs()Over SQL or K8s PVC StatusSQL functions are insufficient:
pg_database_size()returns logical data size but has no knowledge of volume capacity.pg_tablespace_size()provides no free space info.pg_stat_walprovides WAL stats but no volume capacity. PostgreSQL simply does not know how much space is available on the underlying volume.K8s PVC status is insufficient: The PVC
status.capacityshows what Kubernetes believes the size is, but CSI expansion failures can cause the spec to show 150Gi while the actual volume remains 100Gi. PVC status also provides no usage information, only allocated capacity.statfs()from inside the container is the most accurate source available: it reports actual capacity, actual used space, and actual available space from the mounted filesystem. Note thatstatfs()operates at the filesystem level, so its accuracy depends on the CSI driver providing an isolated filesystem per PVC (see the Known Limitations section below).Design Overview
Architecture
Data Flow
statfs()on each mount point (PGDATA, WAL, tablespaces):9187/pg/statusresponsespec.resources.requests.storageif resize is needed and safeAPI Changes
Design Evolution
An earlier draft of this RFC used a flat
AutoResizeConfigurationstruct with fields likeusageThreshold,increase,minIncrease,maxIncrease,maxSize, andcooldownPeriod. During community review, two scaling problems were identified that this simpler model failed to address:The "Petabyte Problem": a 20% resize on a 10TB volume adds 2TB. This can trigger cloud provider timeouts or extended "Optimizing" states that lock the volume for hours.
The "Thundering Herd": a 20% resize on a 1GB volume adds only 200MB. This wastes scarce API quotas (e.g., AWS EBS limits volumes to ~4 modifications per day) on trivial growth.
The "Robot Trap": a time-based cooldown (e.g., 1 hour) doesn't account for cloud provider rate limits. An operator that burns all 4 daily EBS modification slots leaves no room for manual human intervention during an emergency.
These problems led to a redesign using a behavior-driven configuration model, inspired by the Kubernetes HorizontalPodAutoscaler v2 scaling behaviors. The configuration now separates concerns into three blocks: triggers (when to act), expansion (how to grow, with clamping), and strategy (rate limiting and safety).
New
ResizeConfigurationinStorageConfigurationResizeConfigurationResizeTriggersExpansionPolicyResizeStrategyWALSafetyPolicyCluster Status Additions
Example Configurations
Large production cluster (percentage step with clamped ceiling):
Small development cluster (absolute floor protects small volumes):
Multi-tier with tablespaces (mixed strategies):
Instance Manager Changes
Disk Probe (
pkg/management/postgres/disk/probe.go)A new
disk.Probestruct wrapsstatfs()calls for each CNPG volume mount point. It exposesGetDataStats(),GetWALStats(), andGetTablespaceStats()methods that return aVolumeStatsstruct containingTotalBytes,UsedBytes,AvailableBytes,PercentUsed, and inode statistics.This builds on the existing
DiskProbefrom themachinerypackage but structures the output for Prometheus metric collection and status reporting.WAL Health Checker (
pkg/management/postgres/wal/health.go)A
HealthCheckerevaluates WAL health by:.readyfiles inpg_wal/archive_status/pg_stat_archiver: checkinglast_archived_time,last_failed_time, andfailed_countpg_replication_slots: identifying inactive physical slots and calculating their WAL retention viapg_wal_lsn_diff()The checker returns a
HealthStatusstruct that the operator uses to make resize decisions.Metrics Collection
New Prometheus gauges are registered on the existing
:9187metrics endpoint. The metric collector runs on a configurable interval (default: 30 seconds) and updates all disk and WAL health metrics. See the Metrics section below for the complete list.Status Endpoint Extension
The existing
/pg/statusendpoint on:8000is extended to include disk status and WAL health in its JSON response. The operator fetches this during reconciliation to make resize decisions without needing to scrape Prometheus.Operator Changes
Auto-Resize Reconciler
A new
autoresize.Reconcileris called from the cluster controller's main reconciliation loop. On each invocation it:resizeconfiguration is enabled (early exit if not)/pg/statusendpointThe reconciler returns a
RequeueAfterof 30 seconds to ensure continuous monitoring.Trigger Evaluation
The
triggers.usageThresholdandtriggers.minAvailablefields work together. Resize triggers when either condition is met:usageThreshold: 85): fires when used space exceeds 85% of total capacityminAvailable: "10Gi"): fires when free space drops below 10GiThis dual-trigger design addresses the scaling problem where a single percentage is too aggressive for large volumes and too late for small ones. Consider a 1Gi volume: at 80% usage only 200Mi remains free, which may already be critical. Conversely, a 1Ti volume at 80% still has 200Gi free, far more headroom than most workloads need.
When only
usageThresholdis set, behavior is purely percentage-based. When onlyminAvailableis set, behavior is purely absolute. When both are set, the more protective condition wins.Expansion Clamping Logic
The
expansion.stepfield uses the KubernetesIntOrStringpattern to accept either a percentage or an absolute value:"20%"): new size = current size x 1.20 (exponential, adapts to volume size)"10Gi"): new size = current size + 10Gi (linear, predictable for budgeting)When using percentage-based step, the
minStepandmaxStepfields clamp the calculated value:This clamping addresses two specific problems:
minStepdefault:2Gi): 20% of 1Gi = 200Mi, which is too small to justify consuming a daily modification slot. The floor ensures each resize is meaningful.maxStepdefault:500Gi): 20% of 10Ti = 2Ti, which can trigger extended cloud provider "Optimizing" states that lock the volume for hours. The ceiling keeps individual resize operations within safe bounds.When
stepis an absolute value,minStepandmaxStepare ignored since the step is already fixed.Rate Limiting
The
strategy.maxActionsPerDayfield replaces the earliercooldownPeriodconcept with a budget-based model that maps directly to cloud provider realities.Cloud providers impose daily modification limits on volumes. AWS EBS, for example, historically limits volumes to approximately 4 modifications per 24 hours. The default
maxActionsPerDay: 3reserves one modification slot for manual human intervention during emergencies. If the operator consumes all available slots autonomously, an administrator cannot resize a volume manually when they need to.The reconciler tracks resize events per volume in the cluster status and maintains a 24-hour rolling window. When the budget is exhausted, resize is blocked with a
resize_blocked{reason="rate_limit"}metric and a warning event.Webhook Validation
The validating webhook enforces:
resize.enabledistrueandwalStorageis not configured,strategy.walSafetyPolicy.acknowledgeWALRiskmust betruestepis absoluteminStepmust not exceedmaxStepMetrics
Complete Metrics List
cnpg_disk_total_bytesvolume_type,tablespacestatfs()cnpg_disk_used_bytesvolume_type,tablespacecnpg_disk_available_bytesvolume_type,tablespacecnpg_disk_percent_usedvolume_type,tablespacecnpg_disk_inodes_totalvolume_type,tablespacecnpg_disk_inodes_usedvolume_type,tablespacecnpg_disk_inodes_freevolume_type,tablespacecnpg_disk_at_limitvolume_type,tablespaceexpansion.limitcnpg_disk_resize_blockedvolume_type,tablespace,reasoncnpg_disk_resizes_totalvolume_type,tablespace,resultcnpg_disk_resize_budget_remainingvolume_typecnpg_wal_archive_healthycnpg_wal_pending_archive_filescnpg_wal_inactive_slotscnpg_wal_slot_retention_bytesslot_nameLabel Values
volume_type:data,wal,tablespacetablespace: empty for data/wal volumes; tablespace name for tablespace volumesreason(forresize_blocked):archive_unhealthy,too_many_pending_wal,inactive_slots,rate_limit,at_limitresult(forresizes_total):success,failed,blockedAlerting
Proposed PrometheusRule Alerts
Critical alerts:
CNPGDiskCritical: Fires when any data or WAL volume exceeds 95% usage for 5 minutesCNPGWALGrowthArchiveFailure: Fires when WAL disk usage is growing (>1GB/hour) while archive is failing. This is the most dangerous conditionWarning alerts:
CNPGDiskWarning: Volume usage exceeds 80% for 15 minutesCNPGAutoResizeBlocked: Auto-resize is blocked (any reason) for 5 minutesCNPGAtLimit: Volume has reachedexpansion.limitand usage exceeds 80%CNPGArchiveUnhealthy: WAL archiving is failing for 10 minutesCNPGInactiveReplicationSlots: Inactive slots detected for 30 minutesCNPGResizeBudgetExhausted: All daily resize slots consumed; no manual intervention slot availableInfo alerts:
CNPGAutoResizeOccurred: An auto-resize operation completed successfully in the last hourCNPGTablespaceHighUsage: Tablespace usage exceeds 85%Safety Mechanisms
Decision Flow
Safety Check Summary
acknowledgeWALRiskrequireArchiveHealthytruemaxPendingWALFiles100maxSlotRetentionBytesexpansion.limitmaxActionsPerDay3expansion.maxStep500GiThe Single-Volume Foot-Gun in Detail
When
walStorageis not configured, WAL files live inside the PGDATA directory:If WAL archiving fails, WAL files accumulate on the data volume. A naive auto-resizer would grow the volume, masking the archive failure until the expansion limit is reached and the archive backlog becomes unrecoverable, potentially compromising point-in-time recovery.
For this reason, single-volume clusters must set
strategy.walSafetyPolicy.acknowledgeWALRisk: truein the webhook validation. The WAL health checks (requireArchiveHealthy,maxPendingWALFiles) are then especially important for these clusters.Dashboard Updates
New Grafana panels for the CNPG dashboard:
cnpg_disk_percent_usedwith color thresholds (green < 70%, yellow < 85%, orange < 95%, red >= 95%)cnpg_disk_available_bytesfor data and WAL volumesincrease(cnpg_disk_resizes_total[1h])cnpg_disk_resize_budget_remainingper volumeTesting Strategy
Unit Tests
ResizeConfigurationvalidation (trigger ranges, step format, limit format, clamping bounds)Integration Tests
E2E Tests
A comprehensive E2E testing plan has been developed separately. Key test scenarios include:
acknowledgeWALRisk; verify webhook rejectsexpansion.limitcnpg_disk_*andcnpg_wal_*metrics are exposed and reasonableTests use small initial PVC sizes (500Mi-1Gi) with
ddto quickly fill to threshold. Storage classes that don't supportallowVolumeExpansionare automatically skipped.Implementation Phases
Phase 1: Metrics Foundation
Goal: Expose accurate disk metrics from the instance manager.
disk.Probeusingstatfs():9187Deliverables: New metrics on
:9187/metrics, disk status in cluster status, basic dashboard.Phase 2: Auto-Resize Core
Goal: Implement auto-resize for data and WAL volumes with behavior-driven configuration.
ResizeConfigurationto the CRD (withtriggers,expansion,strategysub-structs)maxActionsPerDay)expansion.limitenforcementDeliverables: Working auto-resize for data and WAL volumes with clamped expansion and rate limiting.
Phase 3: WAL Safety
Goal: Implement WAL-aware safety logic.
WALSafetyPolicyto thestrategyblockacknowledgeWALRiskenforcementDeliverables: WAL-aware auto-resize, single-volume protection, archive health enforcement.
Phase 4: Tablespaces and Polish
Goal: Complete the feature with tablespace support and tooling.
kubectl cnpg disk statuscommandDeliverables: Feature-complete with documentation, monitoring, and CLI support.
Migration and Compatibility
CNPG_FEATURE_GATES=AutoResize=trueAlternatives Considered
Alternative 1: Integrate with topolvm/pvc-autoresizer
Rejected. Lacks PostgreSQL/WAL awareness, requires Prometheus as a hard dependency, uses generic PVC annotations that don't fit the Cluster CRD model, and cannot block resize based on archive health.
Alternative 2: Use kubelet metrics via Prometheus
Rejected. Adds Prometheus as a hard dependency, is less accurate than direct
statfs(), cannot detect CSI resize failures, and provides no PostgreSQL-specific context.Alternative 3: SQL-based monitoring only
Rejected. PostgreSQL's
pg_database_size()andpg_tablespace_size()provide no volume capacity or free space information. The database cannot answer the question "how full is my disk?"Alternative 4: Sidecar container for monitoring
Rejected. Adds unnecessary complexity when the instance manager already runs inside the pod with access to all mount points.
Alternative 5: Simple boolean (
autoResize: true) with hardcoded defaultsRejected. Dangerous at scale. A 10TB volume growing by 20% adds 2TB, which is operationally risky on cloud providers with volume modification timeouts. Lacks protections against API quota exhaustion. No mechanism for human override during emergencies.
Alternative 6: Separate "percent" and "absolute" fields (mutually exclusive)
Rejected. Creates invalid states (what happens when both are set?). The Kubernetes
IntOrStringpattern (used bymaxUnavailablein Deployments) handles mixed units cleanly with a single field.Alternative 7: Flat configuration with
cooldownPeriodRejected after initial proposal. A time-based cooldown (e.g., 1 hour) doesn't account for cloud provider rate limits. An operator that happens to resize 4 times in 4 hours (perfectly valid under a 1-hour cooldown) may exhaust the daily EBS modification quota, leaving no room for manual intervention. The budget-based
maxActionsPerDaymaps directly to the real constraint.Known Limitations
Directory-Based Storage Provisioners
statfs()returns statistics for the filesystem backing a given mount point. Most production CSI drivers (AWS EBS, GCE Persistent Disk, Ceph RBD, TopoLVM) create an isolated block device and filesystem per PVC, sostatfs()accurately reflects per-PVC usage.However, directory-based provisioners like local-path-provisioner implement PVCs as directories on a shared host filesystem. In this configuration,
statfs()returns the host filesystem stats for every PVC on the same node. This means:Mitigation: Auto-resize requires a CSI driver that provides isolated filesystems per PVC. This should be validated in documentation and could be detected at runtime by comparing device IDs across mount points. Directory-based provisioners (commonly used in development/test environments) are not suitable for this feature.
Open Questions
Should tablespace auto-resize be included in Phase 1?
Recommendation: Defer to Phase 4 for a simpler initial release.
Should we support inode-based triggers?
Recommendation: Yes. Many small files (e.g.,
pg_wal/archive_status/) can exhaust inodes. Add atriggers.inodeThresholdfield.How should we handle CSI drivers that don't support expansion?
Recommendation: Pre-flight check in webhook validation + clear error message. Skip resize gracefully if
allowVolumeExpansionis false.Should we integrate with VPA (Vertical Pod Autoscaler)?
Recommendation: Out of scope for initial release.
Should we add a "dry-run" mode for testing policies?
Recommendation: Consider for a future enhancement. For now, metrics + events provide visibility.
Should the feature be gated behind a feature flag for the initial release?
Recommendation: Yes.
CNPG_FEATURE_GATES=AutoResize=truefor the first release cycle, then enabled by default.Should resize decisions consider growth rate / trajectory?
A predictive approach could calculate time-to-full based on historical growth rate and trigger resize earlier when growth is accelerating. This would be more intelligent than static thresholds but adds complexity (requires tracking historical data points, choosing a time window, handling bursty vs. steady growth). Recommendation: Defer to a future enhancement. The
strategy.modefield is designed to accommodate a future"Predictive"mode without breaking the API.Should
maxActionsPerDaybe per-volume or per-cluster?Recommendation: Per-volume. Cloud provider rate limits typically apply per-volume (e.g., each EBS volume has its own modification cooldown), not per-cluster.
References
This RFC was prepared alongside a detailed E2E testing design. I welcome feedback on the approach, API surface, safety mechanisms, and implementation phasing.
¹ Design Evolution: The initial version of this RFC used a flat
AutoResizeConfigurationstruct withusageThreshold,increase,minIncrease,maxIncrease,maxSize, andcooldownPeriod. Community feedback (particularly from @ardentperf) identified that straight percentages are problematic across different volume scales, and that time-based cooldowns don't map to cloud provider rate limits. The redesigned behavior-driven model withtriggers,expansion(with clamping), andstrategy(with budget-based rate limiting) addresses these concerns while remaining simple for the common case.All reactions