Интеграция СХД с Kubernetes: CSI, Persistent Volumes и динамическое выделение | AdminWiki

Интеграция СХД с Kubernetes: CSI, Persistent Volumes и динамическое выделение

16 сентября 2026 14 мин. чтения
Содержание статьи

Подключение хранилища к Kubernetes сводится к трём действиям: развернуть CSI-драйвер или провижионер, создать StorageClass с нужными параметрами и запросить том через PersistentVolumeClaim (PVC). Под монтирует полученный том как обычный volume, но данные переживают перезапуск контейнера и перенос пода на другой узел.

Рабочий минимум для динамического выделения: StorageClass с полем provisioner, PVC на 1 Gi и Deployment, который монтирует этот PVC в каталог /data. Схема работает на Kubernetes 1.24 и новее при условии, что установлен совместимый драйвер: в 1.24 встроенные плагины хранилищ убрали из дерева проекта, и внешние СХД подключаются только через Container Storage Interface. Исключение - NFS, hostPath и local, эти плагины остаются в ядре Kubernetes.

Дальше: устройство PV, PVC и StorageClass, сравнение Rook/Ceph, Longhorn и NFS, пошаговые манифесты, разбор типовых ошибок и команды диагностики.

Как Kubernetes работает с хранилищем: ключевые абстракции

Файловая система контейнера исчезает вместе с ним. Каталог emptyDir живёт в пределах пода: данные сохраняются при рестарте контейнера, но пропадают, когда под удаляют или пересоздают. hostPath цепляет каталог узла и привязывает под к конкретной машине, что ломает планирование и переносимость. Для баз данных, очередей и любых stateful-нагрузок нужен слой, который отделяет запрос приложения от физического носителя.

Этот слой состоит из четырёх объектов. PersistentVolume (PV) - участок хранилища в кластере. PersistentVolumeClaim (PVC) - заявка приложения на этот участок. StorageClass описывает, как такой участок создать автоматически. CSI-драйвер выполняет фактические операции с СХД: создание тома, подключение к узлу, монтирование.

Поток данных выглядит так: разработчик создаёт PVC, контроллер StorageClass находит подходящий провижионер, провижионер через CSI-драйвер создаёт том в СХД, Kubernetes формирует объект PV и связывает его с PVC в статусе Bound, kubelet на узле монтирует том в каталог пода. Один PVC соответствует одному тому в хранилище.

PersistentVolume и PersistentVolumeClaim: как это работает

PV существует на уровне кластера и не привязан к namespace. PVC создаётся в namespace приложения. Связывание идёт по трём условиям: достаточный объём, совместимый режим доступа и совпадающий storageClassName.

Режимы доступа описывают, сколько узлов могут одновременно работать с томом:

  • ReadWriteOnce (RWO) - том монтируется для чтения и записи только на одном узле. Ограничение действует на узел, а не на под, поэтому несколько подов на одной машине могут делить один RWO-том.
  • ReadOnlyMany (ROX) - чтение со многих узлов.
  • ReadWriteMany (RWX) - чтение и запись со многих узлов. Требует файлового протокола: CephFS, NFS, SMB.
  • ReadWriteOncePod (RWOP) - том доступен ровно одному поду в кластере. Режим стабилен с Kubernetes 1.29, в 1.27 и 1.28 он был бета-функцией.

Блочные СХД (iSCSI, RBD, локальные LVM-диски) отдают RWO и ROX. RWX для них либо не поддерживается, либо реализуется надстройкой вроде share-manager у Longhorn. Политика persistentVolumeReclaimPolicy управляет судьбой тома после удаления PVC: Delete стирает том в СХД, Retain оставляет его вместе с данными.

Пример статического связывания для NFS-сервера:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv-app-data
spec:
  capacity:
    storage: 50Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: manual
  nfs:
    server: 10.0.0.20
    path: /export/k8s/app-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data
  namespace: production
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: manual
  resources:
    requests:
      storage: 50Gi

Если подходящего PV нет, PVC останется в статусе Pending, а под зависнет в ContainerCreating. Причину показывает kubectl describe pvc app-data -n production: в секции Events будет указано, чего не хватает. Статическое и динамическое связывание с примерами для локальных дисков и облачных томов разобрано в руководстве по настройке PV и PVC.

StorageClass и динамическое выделение

StorageClass избавляет от ручного создания PV. В поле provisioner указывают имя драйвера, в parameters передают настройки хранилища, а Kubernetes сам вызывает создание тома в момент появления PVC.

  • reclaimPolicy: Delete по умолчанию, Retain для критичных данных.
  • volumeBindingMode: Immediate создаёт том сразу, WaitForFirstConsumer откладывает создание до планирования пода. Второй вариант нужен для локальных дисков и зон доступности: том появится в той же зоне, где запустится под.
  • allowVolumeExpansion: 'true' разрешает увеличивать PVC через kubectl edit pvc без пересоздания пода.
  • mountOptions: параметры монтирования, например nfsvers=4.1 или noatime.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: longhorn-fast
provisioner: driver.longhorn.io
allowVolumeExpansion: true
reclaimPolicy: Delete
volumeBindingMode: Immediate
parameters:
  numberOfReplicas: '3'
  staleReplicaTimeout: '30'
  fsType: ext4
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-data
  namespace: production
spec:
  storageClassName: longhorn-fast
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi

После kubectl apply -f pvc.yaml появляется PVC в статусе Bound и объект PV с именем вида pvc- и идентификатором тома. Один StorageClass можно назначить используемым по умолчанию аннотацией storageclass.kubernetes.io/is-default-class: 'true'. Тогда PVC без поля storageClassName получит том именно от него.

Роль CSI-драйверов

Container Storage Interface - спецификация на базе gRPC, по которой Kubernetes общается с хранилищем. Драйвер поставляет вендор СХД, поэтому один и тот же код Kubernetes работает с NetApp, Ceph, Dell PowerStore, Huawei OceanStor и облачными дисками.

Драйвер выполняет набор операций: CreateVolume при появлении PVC, ControllerPublishVolume для привязки тома к узлу, NodeStageVolume и NodePublishVolume для монтирования, DeleteVolume при удалении PVC, CreateSnapshot для снапшотов. Вызывающая сторона - sidecar-контейнеры из поставки драйвера: external-provisioner, external-attacher, external-resizer, external-snapshotter, node-driver-registrar и livenessprobe.

Имена provisioner у популярных драйверов:

  • Ceph RBD: rook-ceph.rbd.csi.ceph.com
  • CephFS: rook-ceph.cephfs.csi.ceph.com
  • Longhorn: driver.longhorn.io
  • NFS CSI: nfs.csi.k8s.io

Установка идёт через Helm или набор манифестов. Проверка после развёртывания: kubectl get csidrivers и kubectl get pods -n kube-system, где должны быть поды вида csi-...-node и csi-...-controller в состоянии Running. Если node-пода драйвера упал на одном узле, поды на этой машине останутся в ContainerCreating. Общие принципы отказоустойчивого кластера, включая HA control plane, RBAC и NetworkPolicy, описаны в руководстве по проектированию production-ready Kubernetes.

Выбор решения для хранения: Rook/Ceph, Longhorn или NFS

Три варианта закрывают большинство задач: Rook/Ceph для продакшена с высокими требованиями, Longhorn для небольших кластеров и быстрого старта, NFS для уже существующего NAS или файлового сервера. Критерии шире, чем работает или нет: важны число узлов, наличие свободных дисков, поддержка RWX и допустимая сложность эксплуатации. Подробное сравнение по отказоустойчивости, стоимости и сложности с рекомендациями для PostgreSQL, MySQL, Kafka и Elasticsearch собрано в руководстве по выбору хранилища для Kubernetes.

Rook/Ceph: возможности и требования

Rook - оператор, который разворачивает Ceph внутри Kubernetes и управляет его жизненным циклом. Ceph отдаёт блочные тома (RADOS Block Device), файловую систему (CephFS) и S3-совместимое объектное хранилище (RGW).

Требования на практике: минимум три worker-узла, на каждом хотя бы один свободный диск без файловой системы и разделов; SSD под журналы OSD заметно поднимают производительность; оперативная память от 4 ГБ на узел только под демоны Ceph плюс запас под приложения. Команды после добавления официального чарт-репозитория Rook под именем rook-release:

helm install rook-ceph rook-release/rook-ceph --namespace rook-ceph --create-namespace
helm install rook-ceph-cluster rook-release/rook-ceph-cluster --namespace rook-ceph --set operatorNamespace=rook-ceph

Репликация задаётся в пуле CRUSH: size=3, min_size=2. При отказе одного узла запись продолжается, при отказе двух одновременно кластер переходит в режим read-only. StorageClass для блочных томов создаётся с provisioner rook-ceph.rbd.csi.ceph.com, для RWX - с rook-ceph.cephfs.csi.ceph.com. Эксплуатация требует мониторинга состояния OSD, ёмкости пулов и ребалансировки после замены дисков. Развёртывание Rook/Ceph с манифестами и настройкой пулов показано в практическом руководстве по Rook/Ceph.

Longhorn: простое распределённое хранилище

Longhorn - проект CNCF, который хранит данные на локальных дисках узлов и синхронизирует реплики между ними. Установка сводится к одному Helm-чарту или манифесту, требования скромнее, чем у Ceph.

helm install longhorn longhorn/longhorn --namespace longhorn-system --create-namespace

На каждом узле нужен пакет open-iscsi и запущенный демон iscsid, иначе том не смонтируется. Для репликации требуется минимум три узла, на одном Longhorn тоже запустится, но без запаса по отказоустойчивости. Число копий задаётся параметром numberOfReplicas: '3'. Параметр dataLocality: 'strict-local' привязывает реплику к узлу с подом и ускоряет чтение. RWX реализуется через share-manager: Longhorn поднимает NFS-сервер внутри кластера и отдаёт том по NFSv4. Снапшоты, бэкапы в S3 или NFS и расписания входят в комплект, поэтому Longhorn часто берут для кластеров на 3-10 узлов и edge-инсталляций.

NFS как внешнее хранилище для Kubernetes

NFS подключает к кластеру уже работающий файловый сервер: NAS, TrueNAS, отдельную виртуальную машину. Для динамического выделения ставят nfs-subdir-external-provisioner, который создаёт на сервере подкаталог под каждый PVC.

helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
  --namespace nfs-provisioner --create-namespace \
  --set nfs.server=10.0.0.20 \
  --set nfs.path=/export/k8s/data \
  --set storageClass.name=nfs-client \
  --set storageClass.reclaimPolicy=Retain

Плюсы: RWX из коробки, нулевая нагрузка на узлы кластера, привычные инструменты администрирования файлового сервера. Минусы: единая точка отказа на стороне сервера, репликации средствами Kubernetes нет, задержки зависят от сети и загрузки NAS, а случайные операции ввода-вывода обычно медленнее, чем на локальных SSD. NFS подходит для тестовых стендов, CI/CD, файловых архивов и приложений с невысокими требованиями к диску. Если своего сервера нет, его поднимают на виртуальной машине: облачный провайдер вроде Timeweb Cloud даёт VPS с сетевыми дисками, которые экспортируются по NFS. Практические детали подключения, права доступа и параметры монтирования разобраны в руководстве по настройке NFS для контейнеров.

КритерийRook/CephLonghornNFS
Сложность установкиОператор, 3 и более узла с дискамиHelm-чарт, open-iscsi на узлахСервер NFS плюс провижионер
Минимум узлов3 с неразмеченными дисками3 для репликации, 1 без отказоустойчивостиТребований к узлам нет
Репликация3 копии в пуле: size=3, min_size=2numberOfReplicas 2-3На стороне сервера, вне Kubernetes
ReadWriteManyCephFSshare-manager по NFSv4Из коробки
Снапшоты и бэкапыRBD-снапшоты, экспорт в S3Встроенные снапшоты, бэкап в S3 или NFSrsync, tar, снапшоты NAS
Типовой сценарийПродакшен, базы данных, S3Кластеры 3-10 узлов, edgeТесты, CI/CD, архивы

Рекомендации по выбору: для продакшена с базами данных и жёсткими требованиями к отказоустойчивости берите Ceph или Longhorn в зависимости от размера команды и числа узлов; для быстрого старта на трёх узлах Longhorn даёт рабочее решение за вечер; при минимальных затратах и наличии NAS достаточно NFS.

Пошаговая настройка динамического выделения хранилища

Порядок одинаков для любого бэкенда: драйвер, StorageClass, PVC, под. Различаются команды установки и параметры.

Установка CSI-драйвера или провижионера

Добавьте официальный Helm-репозиторий выбранного решения, затем установите чарт. Для Longhorn и Rook команды приведены выше, для NFS-провижионера - в разделе про NFS. Проверка после установки:

kubectl get pods -n longhorn-system
kubectl get pods -n nfs-provisioner
kubectl get pods -n rook-ceph
kubectl get csidrivers

Все поды драйвера должны быть в состоянии Running, а в выводе csidrivers появиться строка с именем вашего драйвера. Если node-пода висит в CrashLoopBackOff, проверьте наличие open-iscsi и доступность каталога /var/lib/kubelet на узле.

Создание StorageClass и PVC

Для NFS StorageClass описывается так:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
  archiveOnDelete: 'false'
  onDelete: delete
reclaimPolicy: Retain
volumeBindingMode: Immediate
mountOptions:
  - nfsvers=4.1
  - noatime
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: web-uploads
  namespace: production
spec:
  storageClassName: nfs-client
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi

Примените оба объекта одной командой kubectl apply -f storage.yaml и проверьте результат: kubectl get pvc -n production. Ожидаемый статус - Bound. Параметр archiveOnDelete: 'false' удаляет каталог на NFS-сервере вместе с PVC, значение 'true' переименовывает его и оставляет данные.

Использование PVC в поде

Deployment монтирует готовый PVC, StatefulSet создаёт PVC для каждой реплики автоматически через volumeClaimTemplates.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: production
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      securityContext:
        fsGroup: 1000
        fsGroupChangePolicy: OnRootMismatch
      containers:
        - name: web
          image: nginx:1.27
          volumeMounts:
            - name: uploads
              mountPath: /data
      volumes:
        - name: uploads
          persistentVolumeClaim:
            claimName: web-uploads
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: kafka
spec:
  serviceName: kafka
  replicas: 3
  selector:
    matchLabels:
      app: kafka
  template:
    metadata:
      labels:
        app: kafka
    spec:
      containers:
        - name: kafka
          image: bitnami/kafka:3.7
          volumeMounts:
            - name: data
              mountPath: /bitnami/kafka
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: longhorn-fast
        resources:
          requests:
            storage: 200Gi

У StatefulSet появятся PVC с именами data-kafka-0, data-kafka-1 и data-kafka-2. Проверка монтирования внутри пода: kubectl exec -it web-7d9f -n production -- df -h /data.

Типичные ошибки и подводные камни при интеграции хранилища

Проблемы с режимами доступа и монтированием

ReadWriteOnce относится к узлу. Если два пода с одним RWO-PVC планируются на разные машины, второй зависнет в ContainerCreating, а в событиях появится сообщение Multi-Attach error: volume is already exclusively attached to one node. Решения: перевести поды на один узел через affinity, раздать каждому поду свой PVC или перейти на RWX-хранилище с файловым протоколом.

Попытка подключить RWX к блочному бэкенду без поддержки файлового слоя даёт ошибку провижионера вида unsupported access mode. Блочные драйверы принимают только RWO и ROX, а RWX доступен в CephFS, NFS и Longhorn через share-manager.

Ошибки при динамическом выделении

PVC в статусе Pending почти всегда связан с одной из причин:

  1. В StorageClass указан несуществующий provisioner или другое имя драйвера. Проверьте вывод kubectl get csidrivers и поле provisioner.
  2. Нет StorageClass по умолчанию, а в PVC не задан storageClassName. Смотрите kubectl get storageclass и аннотацию is-default-class.
  3. Провижионер не может достучаться до хранилища: закрыты порты, неверный адрес NFS-сервера, истёкший ключ доступа к облаку. Логи лежат в подах драйвера, например kubectl logs -n rook-ceph deploy/csi-rbdplugin-provisioner -c csi-provisioner.
  4. Том не помещается в пул или сработала квота. В событиях PVC будет сообщение об исчерпании ёмкости.
  5. Топология: при volumeBindingMode: WaitForFirstConsumer под не планируется из-за нехватки ресурсов, а значит и том не создаётся.

Отдельная группа проблем связана с версиями. Манифесты StorageClass и CSIStorageCapacity используют apiVersion storage.k8s.io/v1, который стабилен с Kubernetes 1.6, а CSI-драйверы выпускаются под конкретные минорные версии кластера. Перед обновлением Kubernetes сверяйтесь с матрицей совместимости вендора драйвера: после обновления control plane старые CSI-поды могут не запуститься, и новые тома перестанут создаваться.

Права доступа и безопасность

Блочный том монтируется с правами root, и непривилегированный процесс в контейнере не сможет писать в каталог. Решение - fsGroup в securityContext пода: Kubernetes выставит группу на файлы тома. Значение fsGroupChangePolicy: OnRootMismatch ускоряет старт, меняя права только при несовпадении группы.

Для NFS права зависят от настроек экспорта на сервере. По умолчанию действует root_squash: запросы от root внутри пода отображаются на анонимного пользователя, и запись падает с Permission denied. Варианты решения: подобрать uid и gid приложения под владельца каталога, выставить fsGroup, при необходимости добавить no_root_squash для конкретного клиента. Широкий no_root_squash на весь экспорт открывает доступ к файлам сервера, поэтому применяйте его точечно.

Проверка и диагностика хранилища в Kubernetes

Команды kubectl для проверки статуса

kubectl get pvc -A
kubectl get pv
kubectl describe pvc postgres-data -n production
kubectl get storageclass
kubectl get csidrivers
kubectl get pods -n longhorn-system
kubectl logs -n rook-ceph deploy/csi-rbdplugin-provisioner -c csi-provisioner
kubectl get volumesnapshot -A

Статус Bound у PVC означает, что том создан и привязан. Статус Released у PV появляется после удаления PVC при политике Retain, такой том нельзя переиспользовать без ручной очистки поля claimRef. У StatefulSet проверьте, что каждый под получил собственный PVC с ожидаемым именем.

Тестирование записи и чтения

Проверка сохранности данных занимает две минуты. Создайте тестовый под с монтированием PVC:

apiVersion: v1
kind: Pod
metadata:
  name: storage-test
  namespace: production
spec:
  containers:
    - name: shell
      image: busybox:1.36
      command: ['sh', '-c', 'sleep 3600']
      volumeMounts:
        - name: data
          mountPath: /data
  volumes:
    - name: data
      persistentVolumeClaim:
        claimName: web-uploads

Запишите файл, удалите под, создайте заново и прочитайте данные:

kubectl exec -it storage-test -n production -- sh -c 'echo hello > /data/check.txt'
kubectl delete pod storage-test -n production
kubectl apply -f storage-test.yaml
kubectl exec -it storage-test -n production -- cat /data/check.txt
kubectl exec -it storage-test -n production -- mount | grep /data

Если файл на месте, том переживает пересоздание пода. Дополнительно проверьте восстановление из снапшота: создайте PVC из VolumeSnapshot и убедитесь, что данные внутри. Такой тест ловит ошибки в настройках бэкапов до того, как они понадобятся в аварии.

Обеспечение отказоустойчивости и резервного копирования

Репликация данных в Rook/Ceph и Longhorn

В Ceph число копий задаётся в пуле: size=3 и min_size=2 означают, что запись подтверждается после двух реплик и переживает отказ одного узла. Раскладку реплик по узлам и стойкам определяет CRUSH-карта, для стойко-устойчивой схемы в неё добавляют правило failure domain по стойке.

В Longhorn число копий задаётся параметром numberOfReplicas в StorageClass, значение 3 распределяет реплики по разным узлам. Уменьшение до 1 экономит место и допустимо только для невосстановимых данных. Проверить состояние реплик можно через kubectl get replicas.longhorn.io -n longhorn-system: статус running и healthy означает синхронизацию.

Снапшоты и бэкапы

Стандартный механизм снапшотов в CSI - ресурсы VolumeSnapshotClass и VolumeSnapshot. Для них нужны CRD snapshot.storage.k8s.io и snapshot-controller в кластере; после установки создаётся снапшот без остановки приложения:

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: postgres-snap-01
  namespace: production
spec:
  volumeSnapshotClassName: csi-snapclass
  source:
    persistentVolumeClaimName: postgres-data

Longhorn умеет расписания через recurring jobs: снапшот каждый час и бэкап в S3 или NFS раз в сутки. В Ceph блочные тома копируют через rbd snap и rbd export либо через CSI-снапшоты с последующей загрузкой в объектное хранилище. Для NFS резервное копирование делают на стороне сервера: снапшоты ZFS или rsync в другой каталог. Комплексный бэкап ресурсов Kubernetes и томов закрывает Velero, команда создания выглядит так:

velero backup create prod-daily --include-namespaces production --snapshot-volumes
velero backup describe prod-daily --details

Правило, которое стоит соблюдать: пока восстановление не проверено на стенде, бэкапа нет. Раз в квартал разворачивайте PVC из снапшота и сверяйте контрольные суммы данных.

Поделиться:
Сохранить гайд? В закладки браузера