Хранилище глубже: PV, StorageClass, CSI и StatefulSet

Рейтинг: 65.7% · 17 голосов
Kubernetes для разработчиков: поды, деплойменты, сервисы, ingress, конфиги и отладка. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
anton_k8s
Сообщения: 55
Зарегистрирован: 12 май 2026, 03:23

Хранилище глубже: PV, StorageClass, CSI и StatefulSet

Сообщение anton_k8s »

Оглавление курса (48)
  1. Зачем нужен Kubernetes и из чего состоит кластер
  2. Поднимаем локальный кластер: minikube и kind
  3. Поды: базовая единица запуска
  4. Deployment и ReplicaSet: управляем репликами
  5. Service: сетевой доступ к подам
  6. ConfigMap и Secret: выносим конфигурацию
  7. Ingress: пускаем трафик снаружи
  8. Хранилище: Volumes и PersistentVolumeClaim
  9. Namespaces, requests и limits
  10. Health checks: liveness и readiness пробы
  11. Отладка: почему под не стартует
  12. Helm: пакетный менеджер для Kubernetes
  13. Базовая безопасность: RBAC и доступы
  14. Job и CronJob: разовые и периодические задачи
  15. StatefulSet и DaemonSet: stateful-нагрузки и системные агенты
  16. Стратегии обновления и планирование: rollout и rollback, graceful shutdown, nodeSelector, affinity, taints
  17. Автомасштабирование: HPA по метрикам, обзор VPA и Cluster Autoscaler
  18. Наблюдаемость: логи, метрики, events, обзор Prometheus и Grafana
  19. Безопасность глубже: securityContext, Pod Security Standards, NetworkPolicy, шифрование секретов
  20. Архитектура control plane: api-server, etcd, scheduler
  21. Узел кластера: kubelet, kube-proxy и container runtime
  22. Декларативная модель: reconciliation, контроллеры, CRD
  23. kubectl профессионально: get, describe, explain, jsonpath
  24. Под глубже: init, sidecar, lifecycle hooks и QoS
  25. Метки, селекторы и организация ресурсов
  26. Сервисы и kube-proxy глубже: типы, IPVS, EndpointSlices
  27. Ingress, ingress-контроллеры и Gateway API
  28. Секреты в кластере: шифрование, External Secrets, Vault
  29. Хранилище глубже: PV, StorageClass, CSI и StatefulSet (вы здесь)
  30. Сеть кластера: CNI, NetworkPolicy и CoreDNS
  31. Планировщик: affinity, taints, topology spread
  32. Ресурсы, QoS и вытеснение: requests, limits, eviction
  33. Helm глубже: шаблоны, хуки, зависимости, OCI
  34. Kustomize и управление конфигурацией без шаблонов
  35. RBAC и аутентификация глубже
  36. Admission и Pod Security: контроль на входе
  37. Policy as code: Kyverno и OPA Gatekeeper
  38. GitOps: Argo CD и Flux
  39. Операторы и CRD: расширяем Kubernetes
  40. Сервис-меш: Istio, Linkerd и когда он нужен
  41. Эксплуатация кластера: апгрейд, узлы, бэкап etcd
  42. Где запускать кластер: managed, self-hosted, k3s, Deckhouse
  43. Траблшутинг кластера: Pending, CrashLoop, узлы NotReady
  44. Стоимость и эффективность кластера: FinOps
  45. CI/CD в Kubernetes: сборка, деплой, прогрессивные релизы
  46. Лучшие практики и антипаттерны Kubernetes
  47. Сквозной проект и путь дальше: от манифеста до прода
  48. Деплой приложений через Argo CD: Application, sync и App-of-Apps
Представь: ты выкатил Postgres как обычный Deployment, потерял ноду, под переехал на соседнюю, и база встретила тебя пустым каталогом. Данные жили в emptyDir, а emptyDir умирает вместе с подом. Это классический способ выстрелить себе в ногу на проде. Этот урок про то, как сделать так, чтобы данные пережили под, ноду, рестарт кластера и кривые руки. Разберём kubernetes хранилище профессионально: как PV биндится к PVC под капотом, что реально делает StorageClass, зачем нужен CSI, почему StatefulSet даёт диск на каждую реплику и где тут спрятаны грабли, на которые наступают даже опытные.

Почему том - это абстракция, а не папка

В Kubernetes есть две разные сущности, которые новички вечно путают. PersistentVolume (PV) - это кусок реального хранилища в кластере: облачный диск Yandex, том Ceph RBD, шара NFS. Это ресурс кластерного уровня, его создаёт админ или, чаще, провижинер автоматически. PersistentVolumeClaim (PVC) - это заявка от приложения: "дай мне 20 гигабайт с доступом на запись". Приложение не знает и не должно знать, какой диск ему дадут. Это как розетка: ты втыкаешь вилку (PVC), а откуда там электричество (PV) - не твоя забота.

Зачем такое разделение? Чтобы разработчик описывал потребность, а инфраструктура решала, чем её закрыть. На одном кластере PVC превратится в диск Yandex Cloud, на другом - в Ceph, в локальном k3s - в local-path на диске ноды. Манифест пода при этом не меняется ни на байт. Вот минимальный persistentvolumeclaim:

Код: Выделить всё

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pg-data
spec:
  accessModes: ["ReadWriteOnce"]
  storageClassName: yc-network-ssd
  resources:
    requests:
      storage: 20Gi
В поде он подключается через volumes/volumeMounts:

Код: Выделить всё

spec:
  containers:
    - name: postgres
      image: postgres:16
      volumeMounts:
        - name: data
          mountPath: /var/lib/postgresql/data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: pg-data
Изображение

Binding: как PVC находит свой PV

Самое интересное - механика связывания. Контроллер PersistentVolume в kube-controller-manager постоянно сканирует PVC в статусе Pending и ищет PV, который подходит: размер не меньше запрошенного, совместимый accessMode, тот же storageClassName. Нашёл - связывает один к одному (binding) и пишет в обе стороны ссылки. Связь эксклюзивная: один PV - один PVC, даже если PV больше по объёму. Запросил 20Gi, а свободен PV на 100Gi - получишь все 100, остальное пропадёт впустую.

В реальном проде статических PV почти никто не держит. Их создаёт автоматика - dynamic provisioning. Запускаешь kubectl get pvc и в норме видишь так:

Код: Выделить всё

NAME      STATUS   VOLUME                CAPACITY  ACCESS MODES  STORAGECLASS     AGE
pg-data   Bound    pvc-7c3a...e91        20Gi      RWO           yc-network-ssd   12s
Разберём поля. STATUS Bound - заявка нашла том, под можно запускать. Зависнет в Pending - первый подозреваемый: либо нет подходящего PV, либо провижинер не отработал. VOLUME с именем вида pvc-UUID - это автосозданный PV, имя генерит провижинер. ACCESS MODES в выводе сокращены: RWO значит ReadWriteOnce. Если бы тут торчало Lost - PV под заявкой удалили или он сломался, данные с большой вероятностью уже не вернуть.

StorageClass и динамический провижининг: фабрика дисков

storageclass - это рецепт, по которому кластер создаёт диски на лету. Не нужно заранее нарезать PV руками: создал PVC со ссылкой на класс - провижинер тут же выписал реальный диск в облаке и связал с заявкой. Минимальный пример под Yandex Managed Kubernetes:

Код: Выделить всё

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: yc-network-ssd
provisioner: disk-csi-driver.mks.ycloud.io
parameters:
  type: network-ssd
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
Разберём поля, потому что каждое решает судьбу твоих данных.

provisioner - какой CSI-драйвер физически создаёт диск. В Yandex это disk-csi-driver.mks.ycloud.io, в VK Cloud свой, в AWS ebs.csi.aws.com. Это точка входа в CSI.

parameters - параметры, которые драйвер передаёт облаку: тип диска (network-ssd, network-hdd, быстрый non-replicated), файловая система, шифрование. Список полей свой у каждого драйвера, наизусть не учат - смотрят в доке конкретного CSI.

reclaimPolicy - что станет с PV после удаления PVC. Delete - удалить и PV, и реальный диск в облаке (по умолчанию для динамики). Retain - оставить диск, чтобы спасти данные руками. Простое правило: на боевых базах ставь Retain. Снёс namespace одним kubectl delete - и при Delete диск с продакшен-базой растворится в облаке. Это самая дорогая ошибка в этом уроке.

allowVolumeExpansion: true - разрешает потом увеличивать диск. Без него PVC не расширишь, придётся пересоздавать. Включай сразу, выключить не получится без боли.

volumeBindingMode - тонкий, но критичный момент. Дефолт Immediate: диск создаётся сразу при появлении PVC, ещё до планирования пода. И вот ловушка зональности: в Yandex и любом облаке диск привязан к зоне доступности (ru-central1-a и т.д.). Создали диск в зоне a, а планировщик отправил под на ноду в зону b - под навсегда застрял в Pending, потому что сетевой диск нельзя примонтировать через зону. Лечит это WaitForFirstConsumer: провижинер ждёт, пока планировщик выберет ноду под этот PVC, и только потом создаёт диск - в той же зоне, что и нода. Для облачных дисков WaitForFirstConsumer - почти всегда правильный выбор. Запомни это как мантру.

CSI: почему хранилище живёт вне кода Kubernetes

csi kubernetes (Container Storage Interface) - это стандартный контракт между Kubernetes и любой системой хранения. Раньше код всех драйверов (AWS, GCE, Ceph) жил прямо в ядре Kubernetes - так называемые in-tree volume plugins. Это был ад: чтобы починить баг в драйвере диска, надо было ждать релиз всего Kubernetes. CSI вынес драйверы наружу: теперь это отдельные поды в кластере, которые ставятся как обычное приложение и обновляются независимо. Все in-tree плагины уже вырезаны через миграцию на CSI - в 2026 это пройденный этап, дискового кода вендоров в ядре больше нет.

Драйвер CSI обычно состоит из двух частей. Controller (как Deployment) общается с API облака: создать диск, удалить, снять снапшот, расширить. Node-плагин (как DaemonSet, по поду на каждой ноде) делает грязную работу на самой машине: подключает диск к ноде, форматирует, монтирует в под. Между ними - sidecar-контейнеры (external-provisioner, external-attacher, external-resizer, external-snapshotter), которые переводят события Kubernetes в вызовы драйвера.

Что под это есть в RU-реалиях:
  • Yandex Managed Kubernetes - disk-csi-driver, сетевые SSD/HDD, снапшоты, расширение из коробки.
  • VK Cloud - свой CSI поверх блочного хранилища.
  • Deckhouse - умеет Ceph, LVM на локальных дисках, sds-replicated-volume для реплицируемого хранилища без облака.
  • Ceph RBD через ceph-csi - блочные тома в своём железе, классика для on-prem.
  • NFS через csi-driver-nfs - шара, которую можно монтировать сразу в много подов.
  • local-path-provisioner (идёт в k3s) - PV прямо на диске ноды, идеально для дев-кластера и тестов, но без какой-либо отказоустойчивости.
accessModes: кто и как может писать

Это поле порождает больше всего боли на практике. Режимов несколько:
  • ReadWriteOnce (RWO) - том монтируется на запись на одной ноде. Внимание: именно на одной ноде, а не одним подом. Два пода на одной ноде RWO-том делить могут. Это режим всех облачных блочных дисков (Yandex, VK, EBS).
  • ReadWriteOncePod (RWOP) - стабилен с версии 1.29, только для CSI. Гарантирует ровно один под на весь кластер. Нужен, когда даже два пода на одной ноде - недопустимо (например, чтобы случайно не запустить две реплики БД на одном диске).
  • ReadOnlyMany (ROX) - много нод читают, никто не пишет.
  • ReadWriteMany (RWX) - много нод пишут одновременно.
Ключевая грабля: обычный облачный блочный диск RWX не умеет. Сетевой SSD Yandex - это RWO, точка. Хочешь RWX (общий том на несколько реплик веб-приложения, на которые льют загрузки пользователей) - нужна файловая система, а не блочное устройство: NFS, CephFS, или managed-файловое хранилище провайдера. Попытка прописать accessModes: ReadWriteMany на блочный CSI закончится тем, что второй под не примонтирует том и зависнет. Сначала проверь, что хранилище физически умеет RWX, потом проси.

StatefulSet и volumeClaimTemplates: диск на каждую реплику

Deployment с PVC не масштабируется на состояние: все реплики цепляются к одному PVC, а RWO-диск пустит только одну ноду. Для баз, кафок, эластиков нужен StatefulSet. Его суперсила - volumeClaimTemplates: это не один PVC, а шаблон, из которого Kubernetes штампует персональный PVC для каждой реплики.

Код: Выделить всё

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: pg
spec:
  serviceName: pg
  replicas: 3
  selector:
    matchLabels: { app: pg }
  template:
    metadata:
      labels: { app: pg }
    spec:
      containers:
        - name: postgres
          image: postgres:16
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: yc-network-ssd
        resources:
          requests:
            storage: 20Gi
Что произойдёт: создадутся поды pg-0, pg-1, pg-2 и под каждый - свой PVC data-pg-0, data-pg-1, data-pg-2. Проверяем:

Код: Выделить всё

$ kubectl get pvc -l app=pg
NAME         STATUS   VOLUME           CAPACITY   ACCESS MODES   STORAGECLASS     AGE
data-pg-0    Bound    pvc-a1b2...      20Gi       RWO            yc-network-ssd   3m
data-pg-1    Bound    pvc-c3d4...      20Gi       RWO            yc-network-ssd   2m
data-pg-2    Bound    pvc-e5f6...      20Gi       RWO            yc-network-ssd   1m
Стабильность здесь - не просто слово. Имя PVC привязано к порядковому номеру пода. Под pg-1 умер, переехал на другую ноду - StatefulSet снова поднимет именно pg-1 и примонтирует именно data-pg-1 с его старыми данными. Это и делает StatefulSet пригодным для баз. И вот важный нюанс эксплуатации: при kubectl delete statefulset эти PVC по умолчанию не удаляются - данные специально берегут. Удалять их надо руками, осознанно. Это спасает от случайной потери, но захламляет кластер мёртвыми дисками, если про них забыть.

Снапшоты, расширение и бэкап

Снапшоты (kubernetes pv можно снять как снимок) делаются через CSI, ресурс VolumeSnapshot, актуальный apiVersion - snapshot.storage.k8s.io/v1 (GA с давних времён). Сначала нужен VolumeSnapshotClass с указанием драйвера и deletionPolicy, потом сам снапшот:

Код: Выделить всё

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: pg-snap-2026-06-15
spec:
  volumeSnapshotClassName: yc-snapclass
  source:
    persistentVolumeClaimName: data-pg-0
Из снапшота можно поднять новый PVC (в его spec.dataSource ссылаешься на VolumeSnapshot) - так делают клон базы для тестов или откат. Важно понимать границу: снапшот диска - это crash-consistent копия блоков, а не логически согласованный дамп. Для базы под нагрузкой это как выдернуть шнур: при восстановлении СУБД пойдёт в recovery. Для настоящей согласованности либо тормози запись на момент снимка, либо используй логический бэкап (pg_dump) поверх.

Расширение PVC: если в StorageClass стоит allowVolumeExpansion: true, диск растят на лету - правишь spec.resources.requests.storage в PVC на большее значение и применяешь. Современные CSI умеют online-расширение без перезапуска пода. Запомни два ограничения: уменьшить диск нельзя (только вверх), и расширять надо именно PVC, а не PV напрямую.

Бэкап снапшотами закрывает диски, но не сам объект Kubernetes - манифесты, секреты, конфиги. Для полноценного DR в экосистеме стандарт - Velero: он сохраняет ресурсы кластера в объектное хранилище (S3-совместимое, тот же Yandex Object Storage) и через свою CSI-интеграцию снимает снапшоты томов. Это даёт восстановление всего namespace, а не только данных одного диска. Снапшот без бэкапа манифестов восстановит диск, но не приложение вокруг него.

Грабли и антипаттерны
  • RWO + несколько подов на запись - том примонтируется только на одной ноде, остальные поды зависнут на attach. Деплоить базу как Deployment с replicas больше 1 на одном PVC бессмысленно. Нужно состояние на много реплик - StatefulSet с volumeClaimTemplates.
  • Зональность диска - Immediate binding в мультизональном кластере регулярно роняет поды в вечный Pending. Ставь WaitForFirstConsumer для облачных дисков.
  • reclaimPolicy: Delete на проде - удалил PVC или namespace, и реальный диск с данными исчез вместе с PV. Для боевых данных - Retain.
  • RWX там, где его нет - запросил ReadWriteMany на блочном диске и удивляешься, почему второй под не стартует. RWX дают NFS, CephFS, файловые хранилища - не блочные.
  • Снапшот вместо бэкапа - снапшоты живут в том же облаке и том же аккаунте, что и диски. Накрылся аккаунт - накрылось всё. Бэкап обязан уезжать наружу.
  • Брошенные PVC от удалённых StatefulSet тихо капают в счёт за облако. Заведи привычку чистить осиротевшие PVC.
Мини-лаба: повторить руками

Нужен любой кластер с динамическим провижинером (k3s с local-path или Minikube подойдут):
  • Посмотри доступные классы: kubectl get storageclass. Найди, какой помечен как default (в имени звёздочка в выводе).
  • Создай PVC на 1Gi со ссылкой на этот класс, примени, проверь kubectl get pvc. Если стоит WaitForFirstConsumer - заметь, что PVC висит Pending, пока нет пода.
  • Подними под, который монтирует этот PVC, и убедись, что PVC стал Bound. Запиши файл в смонтированный каталог.
  • Удали под, создай заново с тем же PVC - проверь, что файл на месте. Это и есть персистентность.
  • Разверни StatefulSet с replicas: 2 и volumeClaimTemplates, посмотри, что создалось два отдельных PVC с именами по номеру пода.
  • Если драйвер умеет снапшоты - сними VolumeSnapshot с одного PVC и подними из него новый PVC.
Контрольные вопросы
  • Чем отличается PV от PVC и почему их разделили на два ресурса?
  • Что делает volumeBindingMode: WaitForFirstConsumer и какую проблему зональности он решает для облачных дисков?
  • Почему обычный облачный блочный диск не даёт ReadWriteMany и чем закрыть потребность в RWX?
  • Что произойдёт с PVC при удалении StatefulSet и почему так сделано?
Итог

PVC - это заявка приложения, PV - реальный том, StorageClass - фабрика, которая выписывает диски на лету через CSI-драйвер. Для состояния на много реплик берём StatefulSet с volumeClaimTemplates: персональный диск на каждую реплику с стабильным именем. Три правила, которые спасают данные на проде: WaitForFirstConsumer для облачных дисков, reclaimPolicy Retain на боевых базах, и бэкап (Velero плюс снапшоты), который уезжает за пределы кластера. Запомни границу: снапшот - это быстрый откат, но не замена настоящему бэкапу.
👍3 ❤️1 🔥3 😄 🤔1
Аватара пользователя
midnightpanic
Сообщения: 1
Зарегистрирован: 04 июн 2026, 07:20

Re: Хранилище глубже: PV, StorageClass, CSI и StatefulSet

Сообщение midnightpanic »

Вот про WaitForFirstConsumer прямо боль, у меня под две недели падал в Pending в трехзональном кластере и я не понимал почему. Диск был в зоне a, нода в b. Поменял binding mode и все взлетело, спасибо что разжевали.
👍 ❤️1 🔥 😄 🤔
Аватара пользователя
Berkeley
Сообщения: 1
Зарегистрирован: 12 май 2026, 05:48

Re: Хранилище глубже: PV, StorageClass, CSI и StatefulSet

Сообщение Berkeley »

Вопрос по StatefulSet: если я уменьшу replicas с 3 до 2, PVC от pg-2 останется висеть? И если потом снова скейлну до 3 - подцепит старый диск или создаст новый?
👍1 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Секреты в кластере: шифрование, External Secrets, Vault
Следующая глава →
Сеть кластера: CNI, NetworkPolicy и CoreDNS

Все главы курса «Kubernetes: оркестрация контейнеров от основ до продакшена»

Поделиться темой: ✈ Telegram VK
Похожие запросы: statefulset в kubernetes для баз данных и хранилищаkubernetes pvc и storageclass как подключить хранилище

Вернуться в «Kubernetes на практике»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя