Кубернетес создавался для эфемерных рабочих нагрузок. Поды появляются, исчезают, переезжают между нодами, и любые данные внутри контейнера живут ровно до перезапуска. Но как только в кластере появляется база данных, очередь сообщений или просто файловое хранилище для приложения, эта эфемерность превращается из достоинства в проблему. Persistent Volumes решают вопрос, но за их простой абстракцией скрывается выбор, который определит надёжность всего стека на годы вперёд. Ниже разложены три рабочих подхода - локальные тома через local-path-provisioner для скромных задач, сетевое хранилище через NFS для общих файловых ресурсов и распределённый блочный сторадж Longhorn для серьёзных production-нагрузок с репликацией.

Базовая модель хранения в Kubernetes и понимание PV PVC и StorageClass

Прежде чем тащить в кластер NFS-сервер или ставить Longhorn, стоит разобраться с тремя ключевыми сущностями. PersistentVolume - это абстракция реального куска хранилища. Может быть локальной папкой на ноде, экспортом NFS, диском в облаке или тома распределённой системы. PersistentVolumeClaim - это заявка от приложения на нужный объём с указанием режима доступа и желаемого класса хранения. StorageClass - это шаблон, по которому кластер автоматически создаёт PV под каждую новую заявку.

Связка работает так. Приложение в манифесте Pod ссылается на PVC, кластер находит подходящий PV или создаёт новый через провижионер указанного StorageClass, том монтируется в нужное место внутри контейнера. Звучит просто, но дьявол прячется в режимах доступа. ReadWriteOnce разрешает монтирование на одну ноду одновременно и подходит для баз данных. ReadOnlyMany - чтение со многих нод, для статики и образов. ReadWriteMany - запись со многих нод одновременно, и вот это уже могут далеко не все провижионеры. Локальные диски и блочные тома физически не умеют в RWX, для этого нужен сетевой файловый протокол вроде NFS или CephFS.

Базовая подготовка нод выглядит одинаково для всех трёх сценариев:

sudo apt update && sudo apt upgrade -y
sudo apt install -y nfs-common open-iscsi
sudo systemctl enable --now iscsid

Пакет nfs-common нужен любой ноде, которая будет монтировать NFS-тома. Open-iscsi - обязательная зависимость Longhorn для работы блочного хранилища. Без них дальнейшие шаги просто не запустятся, и кластер будет молча возвращать ошибки монтирования.

local-path-provisioner от Rancher как самый простой стартовый вариант

Этот провижионер идёт по умолчанию вместе с k3s и легко ставится в любой ванильный кластер. Принцип работы прост до неприличия. Когда приложение запрашивает PVC, провижионер создаёт обычную папку на той ноде, где запланирован под, и монтирует её через hostPath. Никакого сетевого взаимодействия, никакой репликации, никакой магии.

Установка занимает одну команду:

kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml

После этого в кластере появляется namespace local-path-storage с подом провижионера и StorageClass с именем local-path. Простая заявка на том выглядит так:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: cache-data
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 5Gi

Том привязывается лениво, через VolumeBindingMode WaitForFirstConsumer. Это значит, что физическая папка создаётся только тогда, когда появляется под, который её требует. Логика разумная - провижионер сразу выбирает ноду, где запланировано приложение, и кладёт данные туда. Без такой задержки PV мог бы появиться на одной ноде, а под уехать на другую, что превратило бы тома в недоступный мусор.

У подхода есть жёсткие ограничения, которые важно понимать. Том намертво привязан к ноде. Если железо умирает - данные теряются. Если под переезжает на другую ноду из-за rescheduling, новый под не увидит старых файлов. Никакой репликации, никаких бэкапов из коробки, никакого ReadWriteMany. Поэтому local-path хорош для кешей, временных каталогов сборок CI, тестовых баз данных в dev-окружении и однонодовых кластеров. Тащить на нём prod-PostgreSQL - значит сознательно ходить по минному полю.

Для серьёзных нагрузок имеет смысл хотя бы вынести путь хранения на отдельный SSD. Конфиг провижионера правится через ConfigMap local-path-config, где параметр paths указывается явным значением вроде /mnt/fast-ssd. Это даёт быстрый локальный сторадж под кеши Redis или временные файлы сборок без риска переполнить системный раздел.

NFS с автоматическим созданием томов через nfs-subdir-external-provisioner

Когда нужен общий файловый ресурс, доступный сразу нескольким подам с разных нод, NFS остаётся самым простым решением. Протокол существует с 1984 года, обкатан до последнего костыля и поддерживается практически везде. В кластере NFS используется через специальный провижионер, который сам нарезает подкаталоги под каждую заявку.

Сначала поднимается сам NFS-сервер. Он может жить вне кластера на отдельной машине, в виде StatefulSet внутри кластера или вообще на NAS. Простейший вариант на отдельной Ubuntu-ноде:

sudo apt install -y nfs-kernel-server
sudo mkdir -p /srv/nfs/k8s
sudo chown nobody:nogroup /srv/nfs/k8s
sudo chmod 777 /srv/nfs/k8s
echo "/srv/nfs/k8s 10.0.0.0/24(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports
sudo exportfs -ra
sudo systemctl restart nfs-kernel-server

Подсеть в exports заменяется на реальную сеть кластера. Параметр no_root_squash нужен, чтобы поды могли писать от любого UID. На production-серверах его лучше заменять на маппинг UID через no_all_squash и явные anonuid/anongid для большей безопасности.

Дальше в кластер ставится провижионер через Helm:

helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
  --namespace nfs-system --create-namespace \
  --set nfs.server=10.0.0.100 \
  --set nfs.path=/srv/nfs/k8s \
  --set storageClass.name=nfs-client \
  --set storageClass.defaultClass=false

После установки появляется StorageClass nfs-client. Любая заявка на PVC с этим классом приведёт к автоматическому созданию подкаталога на NFS-сервере и появлению PV в кластере. Главное преимущество - поддержка ReadWriteMany. Это значит, что один том можно одновременно монтировать к десятку подов на разных нодах, что критично для веб-серверов с общей статикой, очередей файлов или систем вроде GitLab Runner с кешем артефактов.

Производительность NFS - отдельная тема. Протокол по сети по определению медленнее локального диска. На гигабитной сети комфортный потолок где-то 110 мегабайт в секунду, на 10G - примерно гигабайт. Латентность операций с метаданными выше, чем у локальной файловой системы, поэтому базы данных вроде PostgreSQL на NFS работают заметно хуже, чем на блочном хранилище. Для shared-файлов это терпимо, для транзакционной СУБД - категорически нет. Производители баз данных прямо пишут в документации, что NFS под их продукты не поддерживается.

Ещё один тонкий момент с правами доступа. Контейнеры внутри подов часто запускаются от непривилегированного UID, и без правильной настройки fsGroup в SecurityContext они просто не смогут писать на NFS-том. Решение - указывать в манифесте подходящий fsGroup, который соответствует владельцу каталога на NFS-сервере, либо настроить idmapd для нормального маппинга идентификаторов между клиентом и сервером.

Longhorn как промышленное распределённое блочное хранилище для production

Longhorn - это совсем другая весовая категория. Проект родился внутри Rancher Labs, перешёл под крыло CNCF и за несколько лет превратился в зрелое решение, которое всерьёз конкурирует с Ceph и проприетарными SAN-массивами. Принцип работы фундаментально отличается от NFS. Каждый том нарезается на блоки, реплицируется на несколько нод, и при отказе одной из них данные продолжают жить на репликах. Никаких внешних массивов, никаких выделенных серверов хранения - всё работает на тех же нодах, что и сами приложения.

Подготовка нод обязательная. На каждой ноде, где будет жить Longhorn, нужны open-iscsi и желательно отдельный диск под данные. Самый простой способ проверки готовности - утилита longhornctl:

curl -sSfL -o longhornctl https://github.com/longhorn/cli/releases/download/v1.10.1/longhornctl-linux-amd64
chmod +x longhornctl && sudo mv longhornctl /usr/local/bin/
longhornctl install preflight
longhornctl check preflight

Утилита сама проверит ядро, наличие модулей iscsi_tcp и dm_crypt, установит недостающие пакеты и подскажет, где что не так. После успешной проверки идёт установка через Helm:

helm repo add longhorn https://charts.longhorn.io
helm repo update
helm install longhorn longhorn/longhorn \
  --namespace longhorn-system --create-namespace \
  --version 1.10.1 \
  --set defaultSettings.defaultReplicaCount=3 \
  --set defaultSettings.defaultDataPath="/var/lib/longhorn"

Через несколько минут в namespace longhorn-system поднимается с десяток подов. DaemonSet longhorn-manager работает на каждой ноде, отдельные деплойменты обслуживают CSI-интерфейс, веб-UI и драйверы. После установки появляется StorageClass с именем longhorn, который сразу становится дефолтным для всего кластера.

Веб-интерфейс Longhorn - одно из его сильных мест. Доступ обычно делается через port-forward, чтобы не светить дашборд наружу:

kubectl port-forward -n longhorn-system service/longhorn-frontend 8080:80

В UI видно состояние всех нод, использование диска, активные тома и реплики. Здесь же настраиваются регулярные снапшоты, бэкапы на S3-совместимое хранилище и параметры репликации для отдельных томов. Для production обязательно стоит подключить внешнюю target-точку для бэкапов - иначе при катастрофическом отказе всего кластера восстанавливать будет неоткуда.

Полезный набор практик при работе с Longhorn включает несколько неочевидных моментов:

  • Использовать как минимум три ноды для нормальной репликации, иначе при отказе одной останется только одна копия данных
  • Выделять отдельный быстрый диск под /var/lib/longhorn, потому что нагрузка на этот раздел очень неравномерная
  • Включать опцию storage over-provisioning percentage с осторожностью - агрессивное переподписание приводит к out-of-space ситуациям при росте нагрузки
  • Настраивать recurring jobs для автоматических снапшотов критичных томов раз в несколько часов
  • Использовать backup target на отдельной географически разнесённой инфраструктуре, чтобы пожар в датацентре не уничтожил и данные, и копии

Сравнение производительности с локальными дисками показывает падение скорости записи примерно на 20-30 процентов из-за репликации, но это плата за надёжность, которая того стоит. По сетевым требованиям Longhorn рекомендует 10G между нодами для комфортной работы с томами больше нескольких терабайт, на гигабите тоже работает, но синхронизация реплик при отказе ноды займёт ощутимое время.

Стратегия выбора подхода под разные сценарии и совмещение нескольких StorageClass в одном кластере

Никто не заставляет выбирать что-то одно. Зрелые кластеры спокойно держат три-четыре StorageClass одновременно, и каждое приложение получает то хранилище, которое ему подходит. Команда kubectl get storageclass обычно показывает картину вроде того, что local-path работает для дев-сред, longhorn держит prod-базы, nfs-client раздаёт общие файлы.

Логика выбора простая. Если данные можно потерять без серьёзных последствий - кеши, временные файлы, тестовые БД - подходит local-path. Если нужен общий файловый доступ для нескольких подов одновременно - идёт NFS. Если речь о критичной базе данных, очереди сообщений или любом stateful-приложении, где потеря данных недопустима - нужен Longhorn с репликацией и бэкапами.

Дефолтный StorageClass меняется простой аннотацией:

kubectl patch storageclass longhorn -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
kubectl patch storageclass local-path -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'

Что в итоге выходит у того, кто прошёл этот путь. Кластер с продуманной системой хранения, где каждый том лежит там, где ему положено, бэкапится по расписанию и переживает падение отдельных нод без потери данных. Локальный сторадж под быстрые временные задачи, сетевой NFS для общих ресурсов и распределённый Longhorn под критичные production-нагрузки. Главная мудрость здесь не в технологиях, а в трезвой оценке требований. Не каждое приложение нуждается в трёхкратной репликации, и не каждая база данных простит запуск на NFS. Понимание этого избавляет от половины проблем, которые вечером пятницы превращаются в звонок дежурному инженеру в три часа ночи.