Kubernetes PersistentVolumes en PersistentVolumeClaims: de opslagcyclus uitgelegd

Pods zijn tijdelijk. Start een pod opnieuw, en alles wat naar het filesysteem is geschreven is weg. PersistentVolumes (PV) en PersistentVolumeClaims (PVC) lossen dit op door opslag los te koppelen van de pod-levenscyclus, zodat data overleeft bij herstarts, herscheduling en schaling. Dit artikel legt uit hoe PVs, PVCs en StorageClasses samenwerken, wat elke fase in de levenscyclus betekent, en waar de veelvoorkomende misverstanden zitten.

Hoe PVs, PVCs en StorageClasses samenwerken

Kubernetes-opslag kent drie actoren, elk met een eigen rol.

Een PersistentVolume (PV) is een cluster-brede resource die een stuk daadwerkelijke opslag representeert: een cloud-disk, een NFS-export, een lokale SSD. Het bestaat onafhankelijk van welke pod dan ook. Een beheerder maakt het aan, of een provisioner doet dat automatisch.

Een PersistentVolumeClaim (PVC) is een namespace-gebonden verzoek om opslag. Een ontwikkelaar schrijft een PVC die zegt "ik heb 10Gi ReadWriteOnce-opslag nodig." Kubernetes zoekt een PV die aan de claim voldoet en bindt ze 1-op-1. De binding is exclusief: een PVC krijgt precies een PV, en die PV kan geen andere claim bedienen zolang hij gebonden is.

Een StorageClass definieert hoe opslag wordt ingericht. Hij benoemt een provisioner (de CSI-driver of in-tree plugin die met de storage-backend praat), stelt parameters in zoals disktype of IOPS-tier, en declareert policies voor reclaim en volume-uitbreiding. Als een PVC naar een StorageClass verwijst, maakt de provisioner een PV on-demand aan. Geen vooraf aangemaakte PV nodig.

# Een PVC die 10Gi vraagt van de "standard" StorageClass
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-postgres-0
  namespace: production
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: standard   # verwijst naar een StorageClass-object
  resources:
    requests:
      storage: 10Gi

In de praktijk leveren de meeste managed clusters (EKS, GKE, AKS) een standaard StorageClass mee. Maak je een PVC aan zonder storageClassName, dan wordt de default gebruikt. De provisioner maakt de PV achter de schermen aan. Dit is dynamic provisioning en dat is de gangbare weg.

De PV-levenscyclus: Available, Bound, Released, Failed

Een PV doorloopt vier fasen:

Available. De PV bestaat en is aan geen enkele claim gebonden. Hij is beschikbaar voor een matchende PVC.

Bound. Een PVC heeft de PV geclaimd. Data kan worden geschreven en gelezen. De PV blijft gebonden zolang de PVC bestaat.

Released. De PVC is verwijderd, maar de PV bestaat nog en bevat data. De PV is nog niet beschikbaar voor een nieuwe claim. Wat er daarna gebeurt, hangt af van de reclaim policy.

Failed. Automatische reclamation is mislukt. Handmatige actie is nodig.

De valkuil zit in Released. Een Released PV zit vast. Zelfs als je een nieuwe PVC aanmaakt met een identieke spec, bindt Kubernetes hem niet opnieuw aan de Released PV. De oude claimRef wijst nog naar de verwijderde PVC. Een beheerder moet ofwel het claimRef-veld handmatig verwijderen om de PV terug naar Available te brengen, ofwel de PV helemaal verwijderen en dynamic provisioning een nieuwe laten aanmaken.

Statische provisioning vs. dynamische provisioning

Statische provisioning is het handmatige pad. Een beheerder maakt vooraf PV-objecten aan die naar bestaande opslag wijzen (een NFS-share, een al geformatteerd EBS-volume). PVCs matchen tegen deze PVs op storageClassName, access mode en capaciteit. De capaciteit van de PV moet >= het verzoek van de PVC zijn. Is er geen match, dan blijft de PVC oneindig in Pending hangen.

# Statische PV die naar een bestaande NFS-share wijst
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-archive
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteMany
  storageClassName: manual       # gewoon een label voor matching
  nfs:
    server: nfs-prod.internal
    path: /exports/archive

De storageClassName: manual hier is geen echt StorageClass-object. Het is een label dat PV en PVC delen zodat Kubernetes ze aan elkaar koppelt. Er draait geen provisioner.

Dynamische provisioning is het geautomatiseerde pad. De PVC verwijst naar een StorageClass; de provisioner van die StorageClass maakt de PV on-demand aan. Zo werken de meeste productieclusters. Het voordeel is duidelijk: geen beheerder die handmatig disks aanmaakt. Het risico is minder duidelijk: de standaard reclaim policy voor dynamisch aangemaakte volumes is Delete, wat betekent dat data vernietigd wordt als de PVC verwijderd wordt.

WaitForFirstConsumer vs. Immediate binding

StorageClasses hebben een volumeBindingMode die bepaalt wanneer de PV wordt aangemaakt.

Immediate (de standaard) maakt de PV aan zodra de PVC verschijnt. Het probleem: voor topology-gebonden opslag (EBS-volumes, GCE Persistent Disks) kan de PV in de verkeerde availability zone terechtkomen. Als de scheduler de pod later op een node in een andere zone plaatst, kan het volume niet attachen, en de pod zit vast.

WaitForFirstConsumer stelt het aanmaken van de PV uit totdat een pod die de PVC gebruikt wordt gescheduled. De provisioner ziet op welke node de pod terechtkomt en maakt het volume in de juiste zone aan. Voor elke topology-aware storage-backend is dit de juiste instelling.

Access modes: node vs. pod

PVs declareren welke access modes ze ondersteunen. PVCs vragen er een aan. Matching slaagt alleen als de PV de gevraagde mode ondersteunt.

Mode Korte naam Betekenis
ReadWriteOnce RWO Gemount als read-write door een enkele node
ReadOnlyMany ROX Gemount als read-only door meerdere nodes
ReadWriteMany RWX Gemount als read-write door meerdere nodes
ReadWriteOncePod RWOP Gemount als read-write door een enkele pod (GA sinds v1.29)

Het meest voorkomende misverstand: ReadWriteOnce betekent niet een pod. Het betekent een node. Meerdere pods op dezelfde node kunnen tegelijk lezen van en schrijven naar hetzelfde RWO-volume. De Kubernetes-blogpost die RWOP introduceerde zegt dit expliciet: "The ReadWriteOnce access mode restricts volume access to a single node, which means it is possible for multiple pods on the same node to read from and write to the same volume."

Dit is belangrijk voor databases, queues en elke workload die precies een writer verwacht. Als twee replica's op dezelfde node terechtkomen en een RWO-volume delen, kunnen beide schrijven. Datacorruptie is het gevolg.

ReadWriteOncePod is toegevoegd om dit op te lossen. RWOP dwingt single-pod-toegang af op scheduler-niveau. Als een tweede pod hetzelfde RWOP-volume probeert te mounten, weigert de scheduler dat. De huidige Kubernetes-documentatie raadt RWOP aan boven RWO voor single-writer productie-workloads. RWOP vereist wel een CSI-driver; in-tree volume-plugins ondersteunen het niet.

Reclaim policies: Retain, Delete en het verouderde Recycle

De reclaim policy bepaalt wat er met een PV gebeurt als zijn PVC wordt verwijderd.

Delete verwijdert zowel het PV-object als de onderliggende opslag (de cloud-disk, het dynamisch aangemaakte volume). Dit is de standaard voor dynamisch aangemaakte volumes. De data is weg.

Retain bewaart de PV en de data. De PV gaat naar Released. Een beheerder moet handmatig beslissen: data recoveren, opruimen, of verwijderen. Dit is de veilige standaard voor alles waar je om geeft.

Recycle (verouderd) draaide rm -rf /thevolume/* en maakte de PV opnieuw beschikbaar. Onbetrouwbaar en onveilig. Gebruik dynamic provisioning.

De gevaarlijke default: als je een PVC aanmaakt tegen een StorageClass en defaults accepteert, is de reclaim policy Delete. Verwijder de PVC, verlies je data. Voor productiedatabases en stateful workloads: patch de reclaim policy van de PV naar Retain voordat je het nodig hebt:

kubectl patch pv pvc-7a8b3c4d -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'

StatefulSet PVC retention policy

StatefulSets hebben historisch gezien PVCs bewaard, ook als de StatefulSet werd verwijderd of geschaald. Sinds Kubernetes 1.23 (alpha), 1.27 (beta) en 1.32 (GA) geeft spec.persistentVolumeClaimRetentionPolicy expliciete controle:

spec:
  persistentVolumeClaimRetentionPolicy:
    whenDeleted: Retain   # PVCs overleven verwijdering van de StatefulSet
    whenScaled: Delete    # PVCs worden opgeruimd bij downscaling

Beide velden staan standaard op Retain, wat het historische gedrag behoudt.

Volume-uitbreiding

PVC-uitbreiding is stabiel sinds Kubernetes 1.24. Om een PVC te vergroten, pas je spec.resources.requests.storage aan naar een hogere waarde:

kubectl patch pvc data-postgres-0 -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'

Drie voorwaarden moeten kloppen:

  1. De StorageClass moet allowVolumeExpansion: true hebben. Zonder dat weigert de API het verzoek.
  2. De CSI-driver moet de EXPAND_VOLUME-capability ondersteunen. Check de documentatie van je driver.
  3. Je vergroot, niet verkleint. Een PVC verkleinen wordt nergens ondersteund. De API weigert het.

De meeste CSI-drivers doen online expansion: het filesystem groeit terwijl de pod gewoon doordraait. Sommige oudere drivers vereisen dat de pod wordt verwijderd en opnieuw aangemaakt voordat de resize effect heeft. Als een resize mislukt (backend-quota bereikt, driver-fout), staat Kubernetes 1.23+ een gebruiker-geinitieerde retry toe door de PVC opnieuw te patchen.

Wat PersistentVolumes niet zijn

PVs zijn geen backups. Een PV bewaart data bij pod-herstarts, maar beschermt niet tegen per ongeluk verwijderen, corruptie of het uitvallen van een availability zone. Cloud-disk-snapshots, Velero of applicatie-level backups blijven nodig.

PVs zijn niet standaard gedeelde filesystemen. De meeste cloud block storage (EBS, Persistent Disk, Azure Disk) ondersteunt alleen RWO. Voor multi-pod read-write-toegang over nodes heen heb je een storage-backend nodig die RWX ondersteunt: NFS, CephFS, Amazon EFS, Azure Files, of iets vergelijkbaars.

PVCs zijn niet draagbaar tussen clusters. Een PVC verwijst naar een PV in een cluster. Stateful workloads migreren tussen clusters vereist datamigratie-tooling (Velero, storage-level replicatie, of rsync).

StorageClasses zijn geen storage pools. Een StorageClass is een template voor provisioning, geen vooraf gealloceerde pool van capaciteit. Capaciteitslimieten komen van de backend (cloud-accountquota's, fysieke diskgrootte), niet van het StorageClass-object.

Waar je hierna verder kunt

  • Om te begrijpen hoe resource requests en limits de scheduling van StatefulSet-pods beinvloeden (en waarom een database-pod met een gekoppelde PV in Pending kan blijven hangen), zie Kubernetes resource requests en limits
  • Voor networking-concepten die opslag aanvullen bij het ontwerpen van stateful services, zie Kubernetes Services uitgelegd

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.