Kube/Pythonproduction course
Курс · RU05-storage-and-state.md

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 и ресурса-потребителя

manifests/base/pvc.yaml:

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 и дополнения узлов имеют сильные привилегии: фиксируйте версии, аудитируйте и изолируйте их. Шифруйте том и резервную копию, разделяйте ключи. Не храните учётные данные внутри образа тома или дампа данных.

Самопроверка

  1. Что переживает emptyDir?
  2. Почему RWO не означает один Pod?
  3. Кто создаёт PV при динамическом выделении?
  4. Почему PVC может ждать первого потребителя?
  5. Что делает политика возврата?
  6. Почему снимок не равен резервной копии?

Ответы: 1) перезапуск контейнера, а не замена Pod; 2) RWO ограничивает монтирование одним узлом; 3) внешний или совместимый встроенный provisioner через StorageClass; 4) выделение с учётом топологии; 5) судьбу PV и хранилища после освобождения PVC; 6) нет независимой области отказа, согласованности приложения и доказательства восстановления.

Резюме и источники

Состояние имеет контракт жизненного цикла, топологии, согласованности и восстановления. PVC — только часть этого договора. Далее: проверки здоровья, ресурсы и планирование.