05. Хранилище и рабочие нагрузки с состоянием
Незнакомый термин? Откройте словарь Kubernetes и терминов курса.
Назначение, предварительные знания и результаты обучения
Нужны главы 00–04, работающий образ и StorageClass по умолчанию (или способность указать имеющийся). После главы вы сможете:
- выбрать временный том или постоянное хранилище;
- объяснить PV, PVC, StorageClass и динамическое выделение;
- не путать режим доступа с согласованностью приложения;
- проверить привязку, node affinity, политику возврата и сохранность данных;
- различить VolumeSnapshot и согласованную с приложением резервную копию;
- диагностировать непривязанный PVC до анализа журналов контейнера.
Ментальная модель: жизненный цикл и владение данными
Файловая система контейнера временна. emptyDir живёт столько же, сколько Pod:
переживает перезапуск контейнера и исчезает при замене Pod. ConfigMap, Secret и
проецируемые тома доставляют конфигурацию, а не изменяемые бизнес-данные.
Постоянное хранилище разделяет запрос и предоставляемый ресурс:
Pod → PVC (запрос) → PV (выделенная ёмкость) → CSI/provisioner → реальный том
↑
политика StorageClass
- PersistentVolumeClaim (PVC) — запрос в namespace: размер, режим доступа и необязательный класс.
- PersistentVolume (PV) — объект предоставления и привязки уровня кластера.
- StorageClass — политика выделения, драйвер, параметры, политика возврата и
volumeBindingMode. - CSI (Container Storage Interface) — контракт дополнения; драйвер не является встроенной реализацией хранилища.
Динамический provisioner создаёт PV и реальный том для PVC.
WaitForFirstConsumer откладывает выделение и привязку до появления контекста
планирования, чтобы выбрать зону.
Режимы доступа — не блокировка и не репликация
ReadWriteOnce(RWO): монтирование для чтения и записи на одном узле; несколько Pods на том же узле возможны.ReadOnlyMany(ROX): чтение с нескольких узлов.ReadWriteMany(RWX): чтение и запись с нескольких узлов, если это поддерживает драйвер.ReadWriteOncePod(RWOP): один Pod во всём кластере для совместимого с CSI хранилища.
Режимы описывают возможность монтирования, не защищают от конкурентных записей и не дают репликацию базы данных. Файловая система, приложение и драйвер должны поддерживать нужную семантику.
Полные манифесты PVC и ресурса-потребителя
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: python-api-data
namespace: kube-course
labels:
app.kubernetes.io/name: python-api
app.kubernetes.io/part-of: kube-python-course
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
Отсутствие storageClassName выбирает класс по умолчанию. Это удобно для
лаборатории, но манифест эксплуатационной среды должен намеренно выбирать политику или
полагаться на документированное значение платформы по умолчанию.
Полный ресурс-потребитель:
apiVersion: v1
kind: Pod
metadata:
name: python-api-storage
namespace: kube-course
labels:
app.kubernetes.io/name: python-api
app.kubernetes.io/instance: storage-lab
spec:
restartPolicy: Never
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: api
image: kube-python-course:1.0.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8000
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 192Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: data
mountPath: /data
- name: tmp
mountPath: /tmp
volumes:
- name: data
persistentVolumeClaim:
claimName: python-api-data
- name: tmp
emptyDir:
sizeLimit: 32Mi
fsGroup помогает настроить разрешения тома, но поведение и стоимость
рекурсивной смены владельца зависят от драйвера и fsGroupChangePolicy. Не
исправляйте права в эксплуатационной среде запуском приложения от root.
Почему базовый Deployment не хранит состояние
Deployment, ориентированный на эксплуатацию, использует emptyDir для
демонстрационного счётчика. Один RWO PVC на три взаимозаменяемые реплики:
- может приковать все реплики к одному узлу;
- может дать
Multi-Attachна разных узлах; - создаёт гонку конкурентной записи в файл;
- ломает предположения HPA и доступности.
Поэтому реальная серверная часть хранит состояние во внешней управляемой БД или объектном хранилище либо в отдельной системе с состоянием и контрактом репликации и резервного копирования. Лабораторный PVC изолирован в одном Pod. Это осознанное решение по хранению, а не пропущенное монтирование.
Практическое упражнение: привязка → запись → замена
Предварительная проверка
kubectl get storageclass
kubectl get storageclass \
-o custom-columns='NAME:.metadata.name,DEFAULT:.metadata.annotations.storageclass\.kubernetes\.io/is-default-class,PROVISIONER:.provisioner,BINDING:.volumeBindingMode,RECLAIM:.reclaimPolicy'
Для манифеста без класса должен быть один provisioner по умолчанию. Если его
нет, укажите в копии PVC точный storageClassName существующего класса; не
делайте случайный класс значением по умолчанию.
Подготовка и привязка
kubectl apply --dry-run=server -f manifests/base/pvc.yaml
kubectl apply -f manifests/base/pvc.yaml
kubectl get pvc python-api-data -n kube-course -w
При Immediate PVC быстро становится Bound; при WaitForFirstConsumer он
останется Pending до появления Pod — это ожидаемо.
kubectl apply -f manifests/labs/storage-pod.yaml
kubectl wait pod/python-api-storage -n kube-course \
--for=condition=Ready --timeout=180s
kubectl get pvc,pv -o wide
kubectl describe pvc python-api-data -n kube-course
Ожидаются PVC в Bound и Pod в Ready. Измените состояние:
kubectl port-forward -n kube-course pod/python-api-storage 8000:8000
curl --fail --request POST http://127.0.0.1:8000/state/increment
curl --fail --request POST http://127.0.0.1:8000/state/increment
curl --fail http://127.0.0.1:8000/state
Ожидается {"value":2}. Остановите туннель, замените Pod:
kubectl delete pod python-api-storage -n kube-course
kubectl apply -f manifests/labs/storage-pod.yaml
kubectl wait pod/python-api-storage -n kube-course \
--for=condition=Ready --timeout=180s
kubectl port-forward -n kube-course pod/python-api-storage 8000:8000
curl --fail http://127.0.0.1:8000/state
Значение остаётся равным двум: UID Pod новый, PVC и PV те же. Это проверка сохранения данных внутри одного локального кластера, а не резервного копирования или аварийного восстановления.
Доказательства политики возврата и очистка
Перед удалением:
PV="$(kubectl get pvc python-api-data -n kube-course \
-o jsonpath='{.spec.volumeName}')"
kubectl get pv "$PV" \
-o jsonpath='{.spec.persistentVolumeReclaimPolicy}{"\n"}'
kubectl delete pod python-api-storage -n kube-course
kubectl delete pvc python-api-data -n kube-course
kubectl get pv "$PV"
При Delete provisioner удалит PV и хранилище; команда get через некоторое
время вернёт NotFound. При Retain PV останется Released, а данные потребуют
ручной безопасной обработки. Не утверждайте, что реальные байты удалены, без
доказательств от драйвера или реализации хранилища.
Снимки и резервные копии
API VolumeSnapshot (snapshot.storage.k8s.io/v1) имеет стабильный статус, но CRDs,
контроллер снимков и поддержка драйвера CSI устанавливаются отдельно. Снимок
обычно представляет согласованную на момент сбоя копию блока или хранилища. Он
не гарантирует, что PostgreSQL сбросил WAL и транзакции на диск или что связанные
тома согласованы.
Контракт резервного копирования для эксплуатационной среды включает:
- приостановку записи приложения или штатную резервную копию базы данных;
- независимые местоположение, учётную запись и область отказа;
- шифрование и контроль доступа;
- срок хранения и юридически корректное удаление;
- регулярную проверку восстановления с измерением RPO/RTO.
Снимок того же хранилища или региона не заменяет резервную копию. Резервная копия etcd не содержит данные томов приложения.
Самостоятельная задача
Сравните emptyDir и PVC: запишите счётчик, выполните перезапуск контейнера
через завершение процесса, затем замену Pod. Таблица доказательств должна
показать, на каком этапе жизненного цикла теряется каждое значение.
Дополнительно найдите node affinity PV и объясните, что произойдёт при drain
этого узла.
Сломанный сценарий: неизвестный StorageClass
kubectl apply -f manifests/broken/07-pvc.yaml
kubectl get pvc broken-pvc -n kube-course
kubectl describe pvc broken-pvc -n kube-course
kubectl get storageclass
Ожидается Pending; Events сообщат, что class-that-does-not-exist не найден
или отсутствует provisioner. Журналов контейнера нет: Pod ещё не понадобился,
проблема находится на уровне PVC и выделения хранилища.
Для восстановления безопаснее пересоздать PVC с правильным классом, потому что
spec.storageClassName после создания и привязки нельзя произвольно менять:
kubectl delete pvc broken-pvc -n kube-course
kubectl apply -f manifests/base/pvc.yaml
Если PVC с данными уже привязан, удаление может удалить хранилище согласно политике возврата; сначала сделайте снимок или резервную копию и зафиксируйте точное доказательство политики.
Дерево диагностических решений
Pod Pending и использует PVC
├─ PVC Pending
│ ├─ нет класса/неверный класс по умолчанию → выбор StorageClass
│ ├─ нет provisioner/драйвера → исправность контроллера/дополнения
│ ├─ топология WaitForFirstConsumer → изучить Events назначения Pod
│ └─ квота/ёмкость/режим доступа → Events PVC/ёмкость CSI
├─ PVC Bound, Pod Pending
│ ├─ node affinity PV конфликтует → Events scheduler/топология
│ └─ multi-attach/лимит томов → Events, существующие подключения
└─ Pod назначен, монтирование завершается ошибкой
├─ плагин узла CSI/учётные данные → kubelet + Events/журналы CSI
├─ файловая система/права → монтирование/безопасность Pod/fsGroup
└─ повреждение приложения → данные приложения/хранилища, а не пересоздание PVC
Типичные ошибки: hostPath для переносимого приложения; RWO как блокировка
единственного писателя; один PVC на реплики HPA; удаление PVC как способ
диагностики; снимок вместо резервной копии; размер хранилища без учёта IOPS и
задержки; StatefulSet без оператора базы данных и регламента эксплуатации.
Эксплуатационные компромиссы, безопасность и стоимость
Хранилище часто переживает namespace и рабочую нагрузку и продолжает стоить
денег после остановки вычислений. Отслеживайте осиротевшие PV и снимки,
ёмкость, IOPS и egress. Retain безопаснее от случайного удаления, но повышает
риск остаточных данных и стоимость. Delete упрощает очистку, но требует
защитных мер резервного копирования. Контроллеры CSI и дополнения узлов имеют
сильные привилегии: фиксируйте версии, аудитируйте и изолируйте их. Шифруйте
том и резервную копию, разделяйте ключи. Не храните учётные данные внутри
образа тома или дампа данных.
Самопроверка
- Что переживает
emptyDir? - Почему RWO не означает один Pod?
- Кто создаёт PV при динамическом выделении?
- Почему PVC может ждать первого потребителя?
- Что делает политика возврата?
- Почему снимок не равен резервной копии?
Ответы: 1) перезапуск контейнера, а не замена Pod; 2) RWO ограничивает монтирование одним узлом; 3) внешний или совместимый встроенный provisioner через StorageClass; 4) выделение с учётом топологии; 5) судьбу PV и хранилища после освобождения PVC; 6) нет независимой области отказа, согласованности приложения и доказательства восстановления.
Резюме и источники
Состояние имеет контракт жизненного цикла, топологии, согласованности и восстановления. PVC — только часть этого договора. Далее: проверки здоровья, ресурсы и планирование.