Volumes, PVC и StorageClass

Уровень: beginner ~60 мин Практика: Miniflux (AGPL-3.0)

Что нужно знать перед уроком

  • M1.01 — Pod и контейнеры — нужно понимать жизненный цикл Pod

Что нужно знать перед уроком

M1 урок 1 — Pod’ы эфемерны и пересоздаются. Если приложению нужно сохранять данные между пересозданиями (БД, файлы пользователей) — нужен механизм хранения, независимый от жизненного цикла Pod’а. Это и есть тема урока.

Теория

Эфемерные volumes vs Persistent Volumes

Простейший том — emptyDir — директория, создаваемая на узле при старте Pod’а и удаляемая безвозвратно при удалении Pod’а (используется для временных данных, кэшей, обмена файлами между контейнерами одного Pod’а — вспомните M1.02, где Homer использовал похожий подход для конфигурации). Для данных, которые должны переживать пересоздание Pod’а, нужна отдельная пара объектов с жизненным циклом, не привязанным к Pod’у:

  • PersistentVolume (PV) — cluster-scoped объект, представляющий реальный кусок хранилища (диск, NFS-экспорт, блочное устройство CSI-драйвера);
  • PersistentVolumeClaim (PVC) — namespace-scoped запрос на хранилище с нужными параметрами (размер, режим доступа), который Kubernetes связывает («биндит») с подходящим PV.

Pod ссылается не на PV напрямую, а на PVC — это даёт разделение ответственности: администратор кластера управляет реальными PV (или настраивает динамическое провижининг через StorageClass), разработчик Application просит только «дайте мне 10Gi с таким-то режимом доступа», не заботясь о физическом устройстве хранения.

1
2
3
4
5
6
7
8
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: miniflux-db-data }
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests: { storage: 2Gi }
  storageClassName: standard

accessModes: от ReadWriteOnce до строгого ReadWriteOncePod

РежимЗначение
ReadWriteOnce (RWO)монтируется на чтение-запись одним узлом, но потенциально несколькими Pod’ами на этом же узле одновременно
ReadOnlyMany (ROX)монтируется многими узлами только на чтение
ReadWriteMany (RWX)монтируется многими узлами на чтение-запись (требует сетевого хранилища, например NFS/CephFS)
ReadWriteOncePod (RWOP)строгая эксклюзивность на уровне всего кластера — гарантированно только один конкретный Pod, а не просто один узел

Частая ошибка — думать, что ReadWriteOnce защищает от двух Pod’ов, использующих один и тот же PVC одновременно. Это не так: RWO ограничивает узел, а не Pod — если у вас два Pod’а на одном узле оба смонтировали один RWO-PVC, это разрешено и может привести к порче данных, если приложение (например, БД) не готово к конкурентному доступу нескольких процессов. Именно для таких случаев ввели ReadWriteOncePod.

StorageClass.volumeBindingMode

StorageClass описывает «класс» динамически провижинируемого хранилища (например, standard — локальный SSD, fast-nvme — более быстрый диск). Ключевой параметр — volumeBindingMode:

РежимКогда происходит провижинингРиск
Immediate (по умолчанию)сразу при создании PVC, до того как Pod запланирован на узелесли хранилище топологически привязано к конкретной зоне/узлу (например, локальный диск), а планировщик потом выберет другой узел — Pod окажется unschedulable
WaitForFirstConsumerпровижининг откладывается до момента, когда планировщик уже выбрал узел для Pod’а, использующего PVCобязателен для локальных дисков (local-path-provisioner, аналогичные), устраняет риск Immediate
1
2
3
4
5
6
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: { name: standard }
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete

reclaimPolicy: Delete vs Retain

Что происходит с PV после удаления связанного PVC:

  • Delete (типичный дефолт для динамически провижинируемых классов) — PV и физические данные уничтожаются вместе с PVC;
  • Retain — PV остаётся (в статусе Released), данные физически сохраняются, но требуют ручного администрирования для повторного использования.

Это первое касание темы backup/защиты данных, к которой мы вернёмся в M6 уроке 7 (аннотация helm.sh/resource-policy: keep со стороны Nelm) и в M10 уроке 8 (честное разграничение, что бэкапится, а что нет).

Ручное связывание через selector

PVC может явно указать selector.matchLabels/matchExpressions, чтобы связаться с конкретным заранее созданным PV, минуя динамический провижининг — редкий, но иногда нужный случай (например, известный набор дисков с данными, которые нужно смонтировать в определённый Pod).

Частые ошибки и подводные камни

  • Путать ReadWriteOnce с эксклюзивностью на уровне Pod’а. RWO ограничивает узел, не Pod — для истинной эксклюзивности нужен ReadWriteOncePod.
  • Использовать volumeBindingMode: Immediate с топологически привязанным хранилищем. Классическая причина Pod’ов, зависших в Pending с ошибкой планирования «no nodes available» — провижининг случился раньше, чем планировщик выбрал узел.
  • Не проверять reclaimPolicy перед удалением PVC на важных данных. Если класс хранения использует Delete — удаление PVC (даже случайное, например при kubectl delete -f по всему манифесту) стирает данные безвозвратно.

Practice: Linux / Диски

На уровне узла PV, использующий local-path-provisioner-подобный механизм, — это просто директория на файловой системе узла (в кластере kind — внутри Docker-контейнера узла). Через docker exec на узел можно увидеть эти директории напрямую — полезно для понимания, что «постоянное хранилище» в разработческом кластере физически живёт ровно до тех пор, пока жив Docker-контейнер узла (kind delete cluster уничтожит и эти данные тоже — это не противоречит PVC-абстракции, просто напоминание о природе kind).

1
docker exec -it kind-control-plane sh -c "find /var/local-path-provisioner -maxdepth 2"

Практика в кластере

🧩 Практика урока: Miniflux · лицензия AGPL-3.0

Miniflux — минималистичный self-hosted RSS-читалка на Go, обязательно требует PostgreSQL — хороший повод развернуть Postgres с PVC «вручную» (без Module — это чистый K8s-пример, зависимость от managed-postgres в терминах Application появится только в M6).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
kubectl create namespace course-m1-storage

kubectl apply -n course-m1-storage -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: miniflux-db-data }
spec:
  accessModes: ["ReadWriteOnce"]
  resources: { requests: { storage: 1Gi } }
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: miniflux-db }
spec:
  replicas: 1
  strategy: { type: Recreate }
  selector: { matchLabels: { app: miniflux-db } }
  template:
    metadata: { labels: { app: miniflux-db } }
    spec:
      containers:
        - name: postgres
          image: postgres:16-alpine
          env:
            - { name: POSTGRES_USER, value: miniflux }
            - { name: POSTGRES_PASSWORD, value: miniflux }
            - { name: POSTGRES_DB, value: miniflux }
          volumeMounts:
            - { name: data, mountPath: /var/lib/postgresql/data, subPath: pgdata }
      volumes:
        - name: data
          persistentVolumeClaim: { claimName: miniflux-db-data }
---
apiVersion: v1
kind: Service
metadata: { name: miniflux-db }
spec:
  selector: { app: miniflux-db }
  ports: [{ port: 5432 }]
EOF
  1. Убедитесь, что PVC связался (Bound) с автоматически созданным PV:
1
2
kubectl get pvc,pv -n course-m1-storage
kubectl get storageclass    # какой volumeBindingMode использует ваш кластер kind
  1. Запишите тестовые данные и пересоздайте Pod, чтобы убедиться, что данные переживают пересоздание:
1
2
3
4
kubectl exec -n course-m1-storage deploy/miniflux-db -- psql -U miniflux -c "CREATE TABLE probe(id int); INSERT INTO probe VALUES (1);"
kubectl delete pod -n course-m1-storage -l app=miniflux-db
kubectl wait --for=condition=Ready pod -l app=miniflux-db -n course-m1-storage --timeout=60s
kubectl exec -n course-m1-storage deploy/miniflux-db -- psql -U miniflux -c "SELECT * FROM probe;"   # данные на месте
  1. Проверьте reclaimPolicy вашего PV и что произойдёт при удалении PVC (осторожно, это удалит данные):
1
kubectl get pv -n course-m1-storage -o jsonpath='{.items[0].spec.persistentVolumeReclaimPolicy}'

Практика разработки

Используются официальные образы postgres и (при желании расширить пример) miniflux/miniflux; собственная сборка — в M3.

Шпаргалка команд урока

1
2
3
4
kubectl get pvc,pv [-n <ns>]
kubectl get storageclass
kubectl describe pvc <name> [-n <ns>]
kubectl get pv -o jsonpath='{.items[0].spec.persistentVolumeReclaimPolicy}'

Вопросы для самопроверки

Что ограничивает accessMode ReadWriteOnce?

RWO ограничивает узел, а не конкретный Pod — для гарантии единственного Pod'а нужен ReadWriteOncePod.

Источник: Kubernetes — persistent volumes

Почему для локальных дисков StorageClass почти всегда должен использовать volumeBindingMode: WaitForFirstConsumer?

WaitForFirstConsumer откладывает провижининг до момента, когда планировщик уже выбрал узел, устраняя риск несовпадения зоны хранилища и назначенного узла.

Источник: Kubernetes — storage classes

Что произойдёт с физическими данными PV при reclaimPolicy: Delete и удалении связанного PVC?

Delete — типичный дефолт динамически провижинируемых классов: PV и его физические данные уничтожаются сразу вместе с PVC.

Источник: Kubernetes — persistent volumes

Что представляет собой PersistentVolumeClaim с точки зрения ответственности?

PVC — namespace-scoped запрос; администратор/StorageClass отвечают за реальное предоставление PV, разработчик — только за параметры запроса.

Источник: Kubernetes — persistent volumes

Чем ReadWriteOncePod отличается от ReadWriteOnce?

RWOP — более строгий режим, появившийся именно для устранения слабости RWO, которая ограничивает лишь узел, а не конкретный Pod.

Источник: Kubernetes — persistent volumes

Рекомендуемая литература

Официальная документация

Статьи и блоги

Книги

  • Marko Lukša. Kubernetes in Action. 2nd ed., 2022. ISBN 978-1617297618.
  • Kelsey Hightower, Brendan Burns, Joe Beda. Kubernetes: Up and Running. 3rd ed., 2022. ISBN 978-1098110201.
  • Michael Hausenblas, Stefan Schimanski. Programming Kubernetes. 2019. ISBN 978-1492047107.

Связанные материалы