PersistentVolume и PersistentVolumeClaim
Эфемерные volumes из прошлого урока живут вместе с Pod-ом и не подходят для данных, которые должны переживать его удаление. Для persistent storage в Kubernetes придумана пара объектов: PersistentVolume (PV) — представление куска storage в кластере, и PersistentVolumeClaim (PVC) — запрос пользователя на такой кусок.
Это разделение — фундаментальный design choice K8s: storage-инженер описывает доступное (PV), разработчик описывает нужное (PVC), а control plane соединяет их через binding. Разработчик не должен знать про конкретный backend (NFS share, EBS volume, Ceph RBD) — это абстракция от инфраструктуры.
В этом уроке — что такое PV и PVC, как работает binding, какие access modes бывают, как выбрать reclaim policy. Dynamic provisioning через StorageClass — в следующем уроке.
Docker named volumes: lifecycle, drivers, backup
Две стороны одной медали
PersistentVolume — это cluster-scoped ресурс (не привязан к namespace). Представляет конкретное физическое storage.
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-archive-01
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: archive
nfs:
server: 10.0.1.5
path: /exports/archive-01
PersistentVolumeClaim — namespaced. Это user-level запрос: “мне нужен такой-то кусок storage”.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
namespace: my-app
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
storageClassName: archive
Поля совпадают по смыслу: capacity на стороне PV ↔ resources.requests.storage на стороне PVC; accessModes у обоих; storageClassName у обоих.
Жизненный цикл и фазы PV
PV проходит через фазы, которые видны в kubectl get pv:
kubectl get pv
# NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS
# pv-archive-01 100Gi RWO Retain Available archive
# pv-data-02 50Gi RWO Delete Bound my-app/data fast-ssd
# pv-old-03 20Gi RWO Retain Released my-app/old-claim archive
# pv-broken-04 10Gi RWO Retain Failed archive
- Available — PV создан, но не привязан ни к одному PVC. Готов к bind-у.
- Bound — PVC привязан к этому PV.
CLAIMколонка показывает<namespace>/<pvc-name>. - Released — PVC удалён, но PV ещё не очищен (зависит от reclaim policy). Для
Retainостаётся в Released навсегда, пока админ не вмешается. - Failed — PV в неисправном состоянии (cleanup failed, или provisioner вернул ошибку). Нужно ручное вмешательство.
Binding: как PVC находит PV
Когда создаётся PVC, контроллер PersistentVolumeController (часть kube-controller-manager) выполняет matching:
- Перебирает все
AvailablePV - Фильтрует те, что подходят:
storageClassNameсовпадает (или оба пустые)accessModesPV включают все требуемые в PVCcapacity≥resources.requests.storagePVC- Не нарушены selectors (если у PVC есть
spec.selector)
- Если есть candidate — bind. Если нет — PVC остаётся в
Pending. - Если StorageClass с provisioner — запускается dynamic provisioning (создание нового PV).
kubectl get pvc -n my-app
# NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
# data Bound pv-data-02 50Gi RWO fast-ssd
# tmp Pending archive
PVC в Pending — typical issue: нет подходящего PV, или provisioner не сработал. Смотреть kubectl describe pvc tmp.
1:1 mapping: ключевая ловушка
Каждый PV может быть привязан только к одному PVC, даже если access modes позволяют доступ нескольким клиентам.
# PV с ReadWriteMany
apiVersion: v1
kind: PersistentVolume
metadata:
name: shared-nfs
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
nfs:
server: 10.0.1.5
path: /shared
Два PVC хотят на него bind:
# PVC #1
spec:
accessModes: [ReadWriteMany]
resources:
requests:
storage: 50Gi
---
# PVC #2 — НЕ привяжется к этому же PV!
spec:
accessModes: [ReadWriteMany]
resources:
requests:
storage: 30Gi
Bound-ится только первый. Второй PVC останется Pending, потому что PV уже занят. Даже несмотря на то, что NFS share физически может обслужить сотни клиентов.
Sharing идёт через множественный mount одного PVC: несколько Pod-ов используют один и тот же PVC (с RWX) — это работает. А вот два PVC на один PV — нет.
PV ↔ PVC — это всегда 1:1 mapping. Access mode ReadWriteMany / ReadOnlyMany описывает, сколько Pod-ов могут одновременно смонтировать один PVC, а не сколько PVC могут быть bound к одному PV. Это типичная ловушка для новичков, которая приводит к удивлению типа “почему мой второй PVC висит в Pending, если PV ReadWriteMany?”.
Access modes
Access mode определяет, как PV/PVC может быть смонтирован:
| Mode | Аббревиатура | Что означает |
|---|---|---|
ReadWriteOnce | RWO | RW-доступ только с одной node (множество Pod-ов на этой node могут monitor) |
ReadOnlyMany | ROX | RO-доступ со многих node одновременно |
ReadWriteMany | RWX | RW-доступ со многих node одновременно |
ReadWriteOncePod | RWOP | RW-доступ только из одного Pod-а (GA с v1.27) |
Какой mode поддерживает какой backend
Не все volume plugin-ы поддерживают все access modes. Поддерживаемое:
- AWS EBS, GCP PD, Azure Disk — RWO, RWOP (block storage, mount на одной node)
- AWS EFS, Azure Files, NFS, CephFS, GlusterFS — RWO, ROX, RWX (network filesystems)
- Ceph RBD — RWO, ROX
- iSCSI — RWO, ROX
Если попытаться создать PVC с RWX для StorageClass на EBS — PV не создастся, PVC будет в Pending.
RWO с несколькими Pod-ами
Тонкость: ReadWriteOnce — это per node, не per Pod. На той же node можно смонтировать в множество Pod-ов (kubelet делает bind mount). Но если Pod нужно зашедулить на другую node — volume должен сначала отмонтироваться от первой.
Это создаёт проблемы для Deployment с RollingUpdate strategy: новый Pod пытается mount-ить PVC на новой node, пока старый ещё держит его на старой. Решения: Recreate strategy, или RWOP, или StatefulSet (там каждому свой PVC).
Reclaim policy: что делать после удаления PVC
Когда пользователь удаляет PVC, что происходит с PV (и с физическим storage за ним)?
spec:
persistentVolumeReclaimPolicy: Retain
Три варианта (один deprecated):
- Retain (default для статически provisioned PV) — PV переходит в
Released. Данные остаются. Админ должен вручную: пересоздать PV из существующего backing storage, либо удалить storage. Безопасно для production. - Delete (default для dynamically provisioned PV) — PV удаляется, и provisioner удаляет backing storage (EBS volume, EFS access point, etc.). Удобно для эфемерных workloads.
- Recycle — deprecated с v1.11, удалён в новых релизах. Раньше делал
rm -rf /thevolume/*. Не используйте.
Восстановление Released PV
Если PV в Released и хочется его переиспользовать без удаления данных:
# Убрать claimRef — PV вернётся в Available
kubectl patch pv pv-archive-01 -p '{"spec":{"claimRef": null}}'
После этого новый PVC с теми же параметрами может на него bind-нуться. Полезно при ошибочном удалении PVC.
Capacity matching: ≥, не =
Capacity PV должна быть ≥ запрошенной в PVC. Точное равенство не требуется.
PV 100Gi + PVC requests.storage 50Gi → bind, и PVC видит 100Gi (даже если попросил 50). Поэтому в статическом provisioning важно подбирать размеры близко, чтобы не “тратить” big PV на small PVC.
В dynamic provisioning provisioner создаёт PV ровно того размера, что попросил PVC (плюс округление до minimum unit storage backend).
Selectors: точная настройка binding
PVC может ограничить выбор PV через spec.selector:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi
selector:
matchLabels:
type: ssd
zone: us-east-1a
PV с label-ами должны matching:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-ssd-east-a
labels:
type: ssd
zone: us-east-1a
spec:
...
Useful когда есть несколько PV одного StorageClass, но с разными характеристиками (например, разные AZ для multi-AZ кластера, разные диски по типу).
kubectl: типичные операции
# Список PV (cluster-scoped, без -n)
kubectl get pv
# Список PVC в namespace
kubectl get pvc -n my-app
# Подробная информация о PVC, включая events
kubectl describe pvc data -n my-app
# Создать PVC
kubectl apply -f pvc.yaml
# Удалить PVC — PV reclaim согласно policy
kubectl delete pvc data -n my-app
# Освободить PV из Released назад в Available (убрать claimRef)
kubectl patch pv pv-archive-01 -p '{"spec":{"claimRef": null}}'
# Посмотреть, какие Pods используют PVC
kubectl get pod -n my-app -o jsonpath='{range .items[*]}{.metadata.name}{": "}{.spec.volumes[?(@.persistentVolumeClaim)].persistentVolumeClaim.claimName}{"\n"}{end}'