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