K8s Storage System: Three-Layer Abstraction

The Kubernetes storage system decouples storage consumers from providers through three layers of abstraction. This is the core concept for understanding container persistent storage. According to the K8s storage documentation, the three-layer structure is:

┌──────────────────────────────────────────────────────┐
│                   Pod (Consumer)                      │
│                 volumeMounts → volumes                │
├──────────────────────────────────────────────────────┤
│              PVC (Claim) — User requests storage      │
│           "I need 10Gi RWO storage"                   │
├──────────────────────────────────────────────────────┤
│           StorageClass (Dynamic Provisioning)          │
│         "Use ceph-rbd driver, reclaim: Retain"       │
├──────────────────────────────────────────────────────┤
│              PV (Physical Resource) — Actual storage  │
│          "10.0.0.5:/data/pvc-xxx (NFS)"              │
└──────────────────────────────────────────────────────┘

PV (PersistentVolume)

PV is a cluster-level resource representing an abstraction of physical storage. PVs can be created manually by administrators or automatically provisioned through StorageClass:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs-data
spec:
  capacity:
    storage: 50Gi
  accessModes:
  - ReadWriteMany           # Multi-node read-write
  persistentVolumeReclaimPolicy: Retain
  nfs:
    server: 10.0.0.5
    path: /data/k8s-share
  storageClassName: ""      # Static PV doesn't bind to StorageClass

PVC (PersistentVolumeClaim)

PVC is a namespace-level storage claim. Users request storage through PVCs, and the system automatically matches PVs that meet the requirements:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data-pvc
  namespace: production
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: ceph-rbd
  resources:
    requests:
      storage: 20Gi

StorageClass (Dynamic Provisioning)

StorageClass is the core of dynamic storage provisioning, binding CSI drivers and parameter templates to create PVs on demand:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ceph-rbd
provisioner: rbd.csi.ceph.com
reclaimPolicy: Retain
allowVolumeExpansion: true   # Support online expansion
parameters:
  clusterID: ceph-cluster-1
  pool: k8s-rbd-pool
  imageFormat: "2"
  imageFeatures: layering
  csi.storage.k8s.io/provisioner-secret-name: ceph-secret
  csi.storage.k8s.io/provisioner-secret-namespace: ceph-system

AccessMode Explained

AccessModeAbbreviationMeaningTypical Use Case
ReadWriteOnceRWOSingle-node read-writeMySQL, PostgreSQL
ReadOnlyManyROXMulti-node read-onlyConfig files, static assets
ReadWriteManyRWXMulti-node read-writeNFS, CephFS, shared data
ReadWriteOncePodRWOPSingle-Pod read-writeK8s 1.22+, Pod-level precision

CSI Driver Mechanism

CSI (Container Storage Interface) is the standard interface between K8s and storage backends, replacing the earlier in-tree storage plugins. According to the CSI specification, CSI drivers interact with the K8s control plane through Sidecar components.

CSI Architecture

┌───────────────┐    gRPC     ┌──────────────┐
│  K8s Control   │◄──────────►│ CSI Sidecar  │
│  Plane (PV/PVC)│            │ (provisioner │
└───────────────┘            │  attacher)   │
                             └──────┬───────┘
                                    │ gRPC
                             ┌──────▼───────┐
                             │  CSI Driver  │
                             │  (Storage    │
                             │   Backend)   │
                             └──────────────┘

The CSI driver works through three Sidecar components:

  • external-provisioner: Watches PVC events, calls the CSI driver to create/delete storage volumes
  • external-attacher: Manages VolumeAttachment, attaches/detaches storage to Nodes
  • external-snapshotter: Supports storage snapshot functionality

Common CSI Drivers

NFS CSI

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
  server: 10.0.0.5
  share: /data/k8s-share
  subDir: ${pvc.metadata.namespace}/${pvc.name}
reclaimPolicy: Delete
volumeBindingMode: Immediate

NFS is suitable for shared file scenarios. Its advantage is native RWX support; its disadvantage is that network performance depends on the NFS server.

Ceph RBD CSI

Ceph RBD provides block storage, suitable for high-IO scenarios like databases:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ceph-rbd-fast
provisioner: rbd.csi.ceph.com
parameters:
  clusterID: ceph-cluster-1
  pool: ssd-pool              # Use SSD pool
  imageFormat: "2"
  imageFeatures: layering
  csi.storage.k8s.io/fstype: ext4
reclaimPolicy: Retain
allowVolumeExpansion: true
mountOptions:
  - discard                   # Enable TRIM

Local Path Provisioner

Rancher Local Path Provisioner uses Node local disks, suitable for low-latency, high-throughput scenarios:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-path
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer  # Delay binding until Pod is scheduled
reclaimPolicy: Delete

WaitForFirstConsumer is a critical setting for Local Volumes—it ensures the PV is created only after a Pod is scheduled to a specific Node, avoiding scheduling conflicts.

Storage Selection Matrix

Different storage solutions have trade-offs in performance, cost, and portability:

Storage TypePerformanceCostPortabilityRWXUse Case
Local PathVery highVery lowPoorNoCache, temp data, single-replica databases
NFSMediumLowMediumYesShared files, config sync
Ceph RBDHighMediumMediumNoDatabases, block devices
CephFSMedium-highMediumMediumYesShared storage, big data
Cloud disk (EBS/PD)HighHighPoorNoCloud databases
LonghornMedium-highLowMediumNoK8s-native distributed storage

Selection Decision Tree

Need RWX (multiple Pods read-write simultaneously)?
├─ Yes → NFS (simple) / CephFS (high performance)
└─ No → Need high IOPS?
         ├─ Yes → Local Path (single node) / Ceph RBD SSD (distributed)
         └─ No → Cost sensitive?
                  ├─ Yes → Local Path / Longhorn
                  └─ No → Ceph RBD / Cloud disk

Stateful Application Deployment Considerations

MySQL Deployment

MySQL is a typical stateful application requiring special attention to storage configuration:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql-headless
  replicas: 1               # Master-slave requires Operator management
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        resources:
          requests:
            memory: "2Gi"
            cpu: "1000m"
          limits:
            memory: "4Gi"
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
        - name: config
          mountPath: /etc/mysql/conf.d
      volumes:
      - name: config
        configMap:
          name: mysql-config
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: ceph-rbd
      resources:
        requests:
          storage: 50Gi

Key considerations:

  • Data safety: Use reclaimPolicy: Retain to prevent data loss from accidental PVC deletion
  • Resource limits: Memory limit should not be less than innodb_buffer_pool_size + 1GB
  • Scheduling affinity: Use nodeSelector or podAntiAffinity to pin to high-performance nodes
  • Backup strategy: Use Velero or storage snapshots for regular backups

Redis Deployment

Redis needs to distinguish between caching and persistence scenarios:

# Redis persistence mode requires AOF enabled
volumeClaimTemplates:
- metadata:
    name: redis-data
  spec:
    accessModes: ["ReadWriteOnce"]
    storageClassName: local-path    # Redis is IO-sensitive, prefer Local
    resources:
      requests:
        storage: 10Gi

Elasticsearch Deployment

ES has extremely high storage IO requirements, with each node needing an independent PV:

# ES Pod uses initContainer to optimize system parameters
initContainers:
- name: sysctl
  image: busybox
  command: ["sysctl", "-w", "vm.max_map_count=262144"]
  securityContext:
    privileged: true
containers:
- name: elasticsearch
  image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
  env:
  - name: ES_JAVA_OPTS
    value: "-Xms2g -Xmx2g"    # Heap memory = 50% of container memory
  volumeMounts:
  - name: data
    mountPath: /usr/share/elasticsearch/data

Storage Troubleshooting

PVC Stuck in Pending

# Check PVC events
kubectl describe pvc app-data-pvc -n production

# Common causes to investigate
# 1. StorageClass doesn't exist
kubectl get sc

# 2. No available PV (static provisioning)
kubectl get pv

# 3. StorageClass provisioner not running
kubectl get pods -n kube-system | grep csi

# 4. Storage backend connection failure (check CSI logs)
kubectl logs -n ceph-system ceph-csi-rbd-provisioner-xxx

PV Mount Failure

# Check Pod events for mount errors
kubectl describe pod <pod-name>

# Common errors and actions
# "MountVolume.MountDevice failed" → Storage backend unreachable
# "Unable to attach or mount volumes" → CSI Node plugin issue on the Node

# Log in to the target Node and check
# Check block devices
lsblk
# Check mount points
mount | grep <pv-name>
# Check CSI Node plugin
crictl ps | grep csi

Storage Expansion Failure

# Confirm StorageClass supports expansion
kubectl get sc ceph-rbd -o jsonpath='{.allowVolumeExpansion}'
# Output should be true

# Perform expansion
kubectl patch pvc app-data-pvc -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'

# Check expansion status
kubectl get pvc app-data-pvc -o jsonpath='{.status.conditions}'

Common Troubleshooting Quick Reference

SymptomPossible CauseDiagnostic Command
PVC PendingSC doesn’t exist / Provisioner not runningkubectl describe pvc
Pod ContainerCreatingPV mount failurekubectl describe pod
Pod event “disk pressure”Node disk fullkubectl describe node
High IO latencyPoor storage backend performanceiostat -x 1
PVC expansion no responseSC doesn’t have expansion enabledkubectl get sc

Summary

The core of container persistent storage lies in understanding the three-layer abstraction, making informed selections, and preventing failures. The three-layer abstraction (PV/PVC/StorageClass) decouples consumption from provisioning, and CSI standardizes the driver interface. When selecting storage, you need to balance performance, cost, and portability—Local Path for ultimate performance at the cost of portability, Ceph RBD for a balanced distributed block storage solution, and NFS as an economical choice for shared storage.

Key principles for production: data safety first (Retain policy), monitor storage health, and maintain a solid backup strategy. Regularly verifying backup recoverability is more important than the storage selection itself.

References & Acknowledgments

This article referenced the following materials during writing. We thank the original authors for their contributions:

  1. K8s storage documentation — Kubernetes Official, referenced for K8s storage documentation
  2. CSI specification — Kubernetes-csi, referenced for CSI specification
  3. Rancher Local Path Provisioner — GitHub, referenced for Rancher Local Path Provisioner