# Kubernetes storage - PV, PVC, StorageClass, CSI _Контейнеры (бонус) · LinuxLab Knowledge Base_ **TL;DR:** PV - физический volume. PVC - запрос pod'а. StorageClass + CSI - шаблон для dynamic provisioning. ReadWriteOnce типичный (block), ReadWriteMany нужен NFS/CephFS. Эфемерные (emptyDir, configMap) живут пока жив pod. ## Зачем абстракция PV/PVC Pod в k8s может умереть и переехать на другую ноду. Если данные пишутся в локальный диск pod'а, они теряются. Нужен **persistent volume**: storage, который переживает pod и доступен новым pod'ам где бы они не запустились (или хотя бы на тех же нодах). Причём application-team не должна знать, **какой** именно storage под капотом, EBS, GCE PD, Ceph, NFS, vSAN. Отсюда два слоя: - **PersistentVolume (PV)**, кусок реального хранилища, описан в kube - **PersistentVolumeClaim (PVC)**, заявка pod'а: «дай мне 10 Gi с режимом RWO». kube-scheduler сматчит её с подходящим PV. ``` app dev пишет PVC ──► kube-controller-manager ──► binding к PV │ └─► pod монтирует ``` ## Static vs Dynamic provisioning **Static**, администратор заранее создаёт PV, потом приходят PVC и матчатся. Подход 2014 года, сейчас почти нигде. Используется только для legacy-storage без CSI-драйвера. **Dynamic**, есть **StorageClass** (шаблон с параметрами provisioner'а). Когда приходит PVC c `storageClassName`, controller вызывает CSI-driver, тот идёт в облако/SAN, аллоцирует диск, возвращает имя/handle, controller создаёт PV и биндит к PVC. Всё автоматически. ## CSI, Container Storage Interface Стандартизированный gRPC-интерфейс между k8s и storage-vendor. До CSI каждый driver был встроен в kubelet (`in-tree`). Это создавало жуткую coupling, релиз k8s блокировал обновления драйверов. CSI-driver, это два DaemonSet'а: - **Node plugin**, на каждой ноде, делает Mount/Unmount - **Controller plugin**, обычно на одном pod'е, делает CreateVolume/DeleteVolume/AttachVolume в облаке Популярные: | CSI driver | Где работает | |------------|--------------| | `ebs.csi.aws.com` | AWS EBS (RWO, block) | | `pd.csi.storage.gke.io` | GCE Persistent Disk | | `disk.csi.azure.com` | Azure managed disks | | `rook-ceph.rbd.csi.ceph.com` | Ceph RBD (RWO block) | | `cephfs.csi.ceph.com` | Ceph FS (RWX) | | `nfs.csi.k8s.io` | любой [[nfs|NFS-сервер]] (RWX) | | `local.csi.k8s.io` | локальный диск ноды | | `longhorn.io` | distributed block-storage от Rancher | ## StorageClass ```yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: fast-ssd provisioner: ebs.csi.aws.com parameters: type: gp3 # тип диска EBS encrypted: "true" iops: "3000" throughput: "125" fsType: ext4 # как будет отформатирован reclaimPolicy: Delete # что с PV при удалении PVC volumeBindingMode: WaitForFirstConsumer allowVolumeExpansion: true ``` - **`reclaimPolicy: Delete`**, удалить PV (и реальный диск) при удалении PVC. **`Retain`**, оставить, размечать как Released, чтобы админ вручную почистил (production-friendly). - **`volumeBindingMode: WaitForFirstConsumer`**, отложить provisioning пока pod не запланирован, чтобы зону диска подобрать под зону ноды. `Immediate`, создать сразу, может попасть «не в ту зону». - **`allowVolumeExpansion: true`**, можно увеличить PVC живьём (online resize, подерживается XFS/ext4 поверх EBS). Уменьшать нельзя. ## PVC, заявка от приложения ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: app-data spec: accessModes: [ReadWriteOnce] storageClassName: fast-ssd resources: requests: { storage: 50Gi } ``` В pod'е: ```yaml spec: containers: - name: app volumeMounts: - name: data mountPath: /var/lib/myapp volumes: - name: data persistentVolumeClaim: claimName: app-data ``` ## Access Modes | Mode | Аббр | Что значит | Где работает | |------|------|------------|--------------| | ReadWriteOnce | RWO | один node может монтировать read-write | EBS, GCE PD, RBD - **типичный block** | | ReadOnlyMany | ROX | много node могут монтировать read-only | редко, configMap-style | | ReadWriteMany | RWX | много node могут писать одновременно | NFS, CephFS, Azure Files, **filesystem-уровень** | | ReadWriteOncePod | RWOP | один **pod** (k8s 1.27+) | strict-locking БД | **RWO**, на блок-устройство. Нельзя смонтировать на 2 ноды (FS не consistent). Если pod переехал, PV отвязывается от старой ноды и привязывается к новой (через CSI ControllerUnpublish/Publish, занимает 10-60 сек). **RWX**, нужен **shared filesystem**: NFS, CephFS, GlusterFS, Azure Files. Block-storage не умеет. ## Ephemeral volumes, данные на жизнь pod'а Не всегда нужен persistent storage. Для scratch-данных: - **`emptyDir`**, пустой dir на ноде, удаляется с pod'ом. Backed by node-disk или RAM (`medium: Memory` через [[tmpfs-overlayfs|tmpfs]]). - **`configMap` / `secret`**, readonly mount контента из ConfigMap/Secret. - **`downwardAPI`**, pod metadata как файлы. - **`projected`**, несколько источников в один mount (часто для `serviceAccountToken` + ca-cert). - **`generic ephemeral volume`**, inline PVC, удаляется с pod'ом. Удобно для temporary workloads, не надо отдельный PVC. ```yaml volumes: - name: cache emptyDir: sizeLimit: 1Gi - name: tls-certs secret: secretName: api-tls ``` ## StatefulSet + volumeClaimTemplates Для БД, очередей, KV-store нужны: 1. Стабильное имя pod'а (`mongo-0`, `mongo-1`), даёт StatefulSet 2. Свой PVC на каждый pod, переживающий рескейлинг ```yaml apiVersion: apps/v1 kind: StatefulSet metadata: { name: mongo } spec: serviceName: mongo replicas: 3 selector: { matchLabels: { app: mongo } } template: spec: containers: - name: mongo image: mongo:7 volumeMounts: - { name: data, mountPath: /data/db } volumeClaimTemplates: - metadata: { name: data } spec: accessModes: [ReadWriteOnce] storageClassName: fast-ssd resources: { requests: { storage: 100Gi } } ``` StatefulSet создаст `data-mongo-0`, `data-mongo-1`, `data-mongo-2` каждый со своим PV. При delete StatefulSet **PVC не удаляется** (специально, чтобы не потерять данные). Чистить руками (`kubectl delete pvc data-mongo-{0,1,2}`). ## Snapshot и clone CSI поддерживает VolumeSnapshot (если у driver'а есть capability). ```yaml apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: { name: app-data-2026-05-03 } spec: volumeSnapshotClassName: ebs-snapshot source: { persistentVolumeClaimName: app-data } ``` Из snapshot'а можно создать новый PVC (`dataSource: kind: VolumeSnapshot, name: ...`). Это **облачный snapshot**, не файловый делается через AWS/GCE API, очень быстро (CoW). ## Performance, на что смотреть - **IOPS/throughput** провизионируются через StorageClass parameters (для EBS gp3, `iops`, `throughput`). gp2, IOPS привязан к размеру. - **Latency**, local SSD < cloud SSD < network filesystem. RWX storage всегда заметно медленнее RWO (NFS overhead). - **Resize**, увеличение PVC требует поддержки online-resize в FS. [ext4](/kb/ext4.md) и [xfs](/kb/xfs.md) оба умеют. ZFS, нет (через k8s). - **Backup**, k8s не делает бэкап автоматически. Velero, Stash, Kasten K10, внешние инструменты. ## Когда что-то пошло не так - **PVC `Pending` навсегда**, `kubectl describe pvc`. Часто: no StorageClass с таким именем; CSI driver не работает (`kubectl logs -n kube-system `); WaitForFirstConsumer но pod не scheduled (no nodes). - **Pod `ContainerCreating` бесконечно**, проблема mount. `kubectl describe pod` покажет «MountVolume.SetUp failed». Часто: zone mismatch (PV в зоне A, pod в зоне B), CSI node-plugin crashed. - **`Multi-Attach error`**, RWO PV пытается смонтироваться на 2 ноды (например при rolling update без правильного strategy). Решение: `strategy: Recreate` для StatefulSet или `podManagementPolicy: OrderedReady`. - **Disk full в pod'е, но PVC показывает много free**, забыли `allowVolumeExpansion`, или FS не resized после увеличения PVC. `kubectl get pvc` → новый размер; внутри pod'а `df -h` старый нужно `resize2fs`/`xfs_growfs` вручную (новые CSI делают сами). - **`Persistent volume claim is forbidden: storage size cannot be decreased`**, уменьшение размера запрещено. Создавай новый PVC, мигрируй данные. - **Reclaimed PV не освобождается**, `reclaimPolicy: Retain`, PV в `Released`. Удалять руками: `kubectl delete pv ...`, почистить spec.claimRef и привязывать к новому PVC. ## Команды ```bash kubectl get pv,pvc -A ``` Все PV и PVC во всех namespace'ах с binding-статусом ```bash kubectl get sc ``` Все StorageClass с провижонером и default-маркером ```bash kubectl describe pvc my-pvc ``` Events PVC - первое место смотреть при Pending ```bash kubectl patch pvc my-pvc -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}' ``` Live-расширение PVC - работает если allowVolumeExpansion=true ```bash kubectl get csidriver ``` Список зарегистрированных CSI-драйверов в кластере ```bash kubectl exec -it pod-0 -- df -h /data ``` Реально занятое место в смонтированном volume - сравнить с PVC ```bash kubectl get volumesnapshot,volumesnapshotcontent ``` Снапшоты и их content - для CSI driver'ов с поддержкой snapshot'ов ## См. также - [Kubernetes pod lifecycle - от Pending до Terminated](/kb/kubernetes-pod-lifecycle.md) - [NFS - сетевая файловая система](/kb/nfs.md) - [mount и /etc/fstab - подключение ФС](/kb/mount-and-fstab.md) - [XFS - extents и параллельный I/O](/kb/xfs.md) - [ext4 - рабочая лошадь Linux-ФС](/kb/ext4.md) - [Helm charts - пакетный менеджер для Kubernetes](/kb/helm-charts.md)