Learning Platform
Глоссарий Troubleshooting
Урок 08.02 · 25 мин
Продвинутый
PersistentVolumePersistentVolumeClaimAccess modesReclaim policyBindingPV phases

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 vs PVC: разделение ответственности
Storage admin / dynamic provisionerКто создаёт PV. В статической модели — администратор кластера руками регистрирует доступные volumes. В dynamic модели — provisioner за StorageClass автоматически создаёт PV при появлении PVC.
App developerКто создаёт PVC. Разработчик описывает, что нужно его приложению: размер, access mode, класс. Не знает, что лежит за PV — это абстракция.
K8s соединяет через binding
PV (cluster-scoped)cluster-level ресурс. Хранит координаты конкретного физического storage: NFS server+path, EBS volumeID, Ceph image, и т.д. Видим во всех namespace.
PVC (namespaced)Запрос пользователя. Описывает требования (storage, accessModes, storageClassName, selectors), а не конкретный backend.
binding 1:1
BoundОдин PVC привязан к ОДНОМУ PV (и наоборот). Даже если PV имеет ReadOnlyMany — другие PVC к нему привязаться НЕ могут. Sharing volume идёт через множественный mount одного PVC в Pods.

Жизненный цикл и фазы 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:

  1. Перебирает все Available PV
  2. Фильтрует те, что подходят:
    • storageClassName совпадает (или оба пустые)
    • accessModes PV включают все требуемые в PVC
    • capacityresources.requests.storage PVC
    • Не нарушены selectors (если у PVC есть spec.selector)
  3. Если есть candidate — bind. Если нет — PVC остаётся в Pending.
  4. Если 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 — нет.

WARNING

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АббревиатураЧто означает
ReadWriteOnceRWORW-доступ только с одной node (множество Pod-ов на этой node могут monitor)
ReadOnlyManyROXRO-доступ со многих node одновременно
ReadWriteManyRWXRW-доступ со многих node одновременно
ReadWriteOncePodRWOPRW-доступ только из одного Pod-а (GA с v1.27)
Access modes: что разрешено
ReadWriteOnce (RWO)Block storage обычно: AWS EBS, GCP PD, Azure Disk, Ceph RBD. Можно mount только на одной node одновременно (kernel attaches block device). Если на этой node 5 Pod-ов используют PVC — все 5 работают.
ReadOnlyMany (ROX)RO с нескольких node — для shared read-only data (большие dataset-ы, ML модели). Поддерживается NFS, CephFS, или RWO volumes mount-ом как RO.
ReadWriteMany (RWX)RW с нескольких node — требует distributed file system: NFS, CephFS, GlusterFS, EFS на AWS, Azure Files. Block storage (EBS) ROW не умеет. Производительность зависит от FS.
ReadWriteOncePod (RWOP)GA с v1.27. RW только из ОДНОГО Pod-а (даже несколько Pod-ов на одной node не могут). Защита от двойного writer, критично для базов данных и систем с lock-free write protocol.

Какой 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/*. Не используйте.
Reclaim policy: что после удаления PVC
User: kubectl delete pvc dataПользователь удаляет PVC. PV переходит в фазу Released — PVC удалён, PV всё ещё связан с этим claimRef.
что дальше зависит от policy
RetainPV остаётся Released навсегда. Админ должен: посмотреть на данные, скопировать что нужно, потом kubectl delete pv (или удалить claimRef и вернуть PV в Available). Безопасно для prod.
DeletePV-controller вызывает provisioner.DeleteVolume. EBS volume удаляется в AWS, GCP PD в GCP, и т.д. Данные исчезают навсегда. После этого PV удаляется из etcd. Удобно для dev/test.

Восстановление 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}'

Проверка знанийKnowledge check
Администратор создал PV с accessModes: [ReadWriteMany] и capacity 100Gi. Затем в кластере появились два PVC, оба с RWX и storage 50Gi, в разных namespace. Первый PVC привязался к PV, второй остался в Pending. Почему второй PVC не привязался к тому же PV — ведь RWX по определению поддерживает много writers?
ОтветAnswer
Это типичная ловушка PV/PVC binding. ReadWriteMany описывает режим **mount-а** одного PVC из множества Pod-ов на множестве nodes — не сколько PVC могут одновременно ссылаться на один PV. Binding всегда 1:1: один PV привязан строго к одному PVC, и наоборот. Когда первый PVC привязался к PV, его claimRef записался в PV.spec.claimRef. Второй PVC не может привязаться, потому что PV уже занят, какие бы access modes у него ни были. Чтобы оба namespace получили RWX доступ к одному backend storage (например, NFS share), нужно: (1) создать ОДИН PVC и shared его между namespace через cross-namespace mechanism (нет встроенного — обычно решается через StorageClass с одним PV, или duplicate PVC); (2) создать ДВА отдельных PV с разными именами, каждый указывает на тот же NFS path — но это работает только если storage backend позволяет независимое управление; (3) использовать dynamic provisioning с StorageClass, который создаёт отдельный PV под каждый PVC.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Чем PV и PVC принципиально различаются по scope в Kubernetes?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5