# Longhorn — стандарт постоянного хранилища в кластере Этот документ описывает стандарт работы с Longhorn в нашем кластере: как подключать хранилище к приложениям, добавлять диски, управлять репликами и резервными копиями. Является эталоном для разработчиков и DevOps. --- ## Содержание 1. [Архитектура: как Longhorn работает в кластере](#1-архитектура-как-longhorn-работает-в-кластере) 2. [Два этапа деплоя: longhorn_prereqs и longhorn](#2-два-этапа-деплоя-longhorn_prereqs-и-longhorn) 3. [Конфигурация дисков](#3-конфигурация-дисков) 4. [Пример 1: PVC для одного Pod (ReadWriteOnce)](#4-пример-1-pvc-для-одного-pod-readwriteonce) 5. [Пример 2: StatefulSet с несколькими репликами](#5-пример-2-statefulset-с-несколькими-репликами) 6. [Пример 3: база данных (PostgreSQL) с Longhorn](#6-пример-3-база-данных-postgresql-с-longhorn) 7. [Пример 4: несколько компонентов с общим хранилищем](#7-пример-4-несколько-компонентов-с-общим-хранилищем) 8. [Реплики: сколько и когда менять](#8-реплики-сколько-и-когда-менять) 9. [Резервное копирование и восстановление](#9-резервное-копирование-и-восстановление) 10. [Расширение тома](#10-расширение-тома) 11. [Добавление нового worker-узла с дисками](#11-добавление-нового-worker-узла-с-дисками) 12. [Диагностика и типичные ошибки](#12-диагностика-и-типичные-ошибки) 13. [Чеклист перед использованием хранилища](#13-чеклист-перед-использованием-хранилища) --- ## 1. Архитектура: как Longhorn работает в кластере ### Схема ``` ┌─────────────────────────────────────────────────────────────────┐ │ k8s-worker-01 (10.203.0.96) │ │ │ │ ┌────────────────┐ ┌────────────────┐ │ │ │ /dev/sdb │ │ /dev/sdc │ │ │ │ XFS │ │ XFS │ │ │ │ /mnt/longhorn-│ │ /mnt/longhorn-│ │ │ │ disk1 │ │ disk2 │ │ │ └───────┬────────┘ └───────┬────────┘ │ │ │ │ │ │ Longhorn Manager Pod ─────────┘ │ │ Longhorn Engine Pod (управляет репликами) │ │ Instance Manager Pod (iSCSI-таргет для kubelet) │ │ │ │ kubelet → iSCSI → Longhorn Engine → Longhorn Replica │ │ (файлы на диске) │ └─────────────────────────────────────────────────────────────────┘ PersistentVolumeClaim (ns: myapp) ──► StorageClass: longhorn │ ▼ PersistentVolume (Longhorn Volume) │ Replicas: 1 (сейчас) → расположены на worker-нодах ``` ### Компоненты | Компонент | Назначение | |---|---| | **longhorn-manager** | Оркестрирует тома, реплики, бэкапы; CRD-контроллер | | **longhorn-engine** | Процесс ввода-вывода для каждого тома | | **instance-manager** | Управляет engine и replica инстансами | | **longhorn-ui** | Web-интерфейс (доступен через Traefik на порту 10002) | | **CSI driver** | Kubernetes CSI-интерфейс; обрабатывает PVC → PV | ### StorageClass Longhorn создаёт StorageClass `longhorn` при установке. Это **StorageClass по умолчанию** в кластере. ```bash kubectl get storageclass # NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ... # longhorn (default) driver.longhorn.io Delete Immediate ... ``` `ReclaimPolicy: Delete` — при удалении PVC том удаляется. Если нужно сохранить данные — делайте снапшот или бэкап перед удалением PVC. --- ## 2. Два этапа деплоя: longhorn_prereqs и longhorn Longhorn разворачивается через два плейбука, которые нужно запускать в порядке: ``` 1. setup_longhorn.yml ├── role: longhorn_prereqs (runs on: workers) │ ├── packages.yml — iscsi-initiator-utils, nfs-utils, cryptsetup │ ├── kernel.yml — iscsi_tcp, dm_crypt модули │ ├── firewall.yml — NFS (2049/tcp), portmapper (111/tcp, 111/udp) │ ├── selinux.yml — CIL-политика для iscsid (Rocky Linux 9) │ └── disks.yml — форматирование XFS, монтирование по UUID │ └── role: longhorn (runs on: manager_nodes) ├── annotate.yml — kubectl annotate node: disk config JSON └── helm.yml — helm upgrade --install longhorn/longhorn ``` ### Когда перезапускать плейбук | Ситуация | Что запускать | |---|---| | Первичная установка | `setup_longhorn.yml` полностью | | Добавление нового worker-узла | `setup_longhorn.yml` с `--limit k8s-worker-NN` для prereqs, затем полностью для annotate | | Добавление диска к существующей ноде | `setup_longhorn.yml` (prereqs: disks.yml пропустится для существующих; annotate обновит аннотацию) | | Обновление версии Longhorn | Изменить `longhorn_chart_version`, запустить только `longhorn` role | | Изменение параметров Helm | Изменить переменные, запустить только `longhorn` role | --- ## 3. Конфигурация дисков ### inventory/prod/group_vars/workers.yml — глобальный дефолт ```yaml longhorn_disks: - device: /dev/sdb mountpoint: /mnt/longhorn-disk1 - device: /dev/sdc mountpoint: /mnt/longhorn-disk2 ``` Это применяется ко **всем** worker-нодам. Если у разных нод разные диски — переопределить в `hosts.yml` (host_vars). ### inventory/prod/hosts.yml — переопределение для конкретной ноды ```yaml workers: hosts: k8s-worker-01: ansible_host: 10.203.0.96 longhorn_disks: # переопределяет group_vars для этой ноды - device: /dev/sdb mountpoint: /mnt/longhorn-disk1 - device: /dev/nvme0n1 mountpoint: /mnt/longhorn-nvme # SSD на этой ноде k8s-worker-02: ansible_host: 10.203.0.X longhorn_disks: - device: /dev/sdb mountpoint: /mnt/longhorn-disk1 # только один диск ``` ### Что происходит с дисками при деплое 1. `filesystem` форматирует устройство в XFS (идемпотентно — пропускает уже отформатированные) 2. Создаётся директория mountpoint 3. `blkid` получает UUID устройства 4. `/etc/fstab` добавляется запись `UUID=... /mnt/... xfs defaults,nofail` 5. Диск монтируется немедленно 6. Аннотация ноды: `node.longhorn.io/default-disks-config=[{"path":"/mnt/longhorn-disk1",...}]` 7. Longhorn читает аннотацию при старте и регистрирует диски > `nofail` в fstab критически важен: если диск недоступен при загрузке — нода загрузится, а не зависнет. ### Проверка дисков в Longhorn ```bash # Посмотреть ноды и их диски в Longhorn kubectl get nodes.longhorn.io -n longhorn-system # Детали ноды kubectl describe node.longhorn.io k8s-worker-01 -n longhorn-system # Через UI: http://10.203.0.96:10002 → Node ``` --- ## 4. Пример 1: PVC для одного Pod (ReadWriteOnce) **Сценарий:** одиночный Pod с постоянным хранилищем (файловый кэш, локальные данные). ### PersistentVolumeClaim ```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: app-data namespace: my-app spec: accessModes: - ReadWriteOnce # один узел в режиме чтения-записи storageClassName: longhorn resources: requests: storage: 5Gi ``` ### Deployment с PVC ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: my-app namespace: my-app spec: replicas: 1 # ВАЖНО: ReadWriteOnce — только 1 реплика! selector: matchLabels: app: my-app template: metadata: labels: app: my-app spec: containers: - name: my-app image: registry.gigacoms.info/myteam/my-app:v1.0.0 volumeMounts: - name: data mountPath: /app/data volumes: - name: data persistentVolumeClaim: claimName: app-data ``` > **ReadWriteOnce** означает: том примонтирован к одному узлу одновременно. > Несколько Pod могут использовать один RWO-том, **только если все они на одном узле**. > С `replicas: >1` и DaemonSet это создаст проблемы при планировании на разные ноды. ### Проверить что PVC создан и привязан ```bash kubectl get pvc -n my-app # NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE # app-data Bound pvc-abc12345 5Gi RWO longhorn 30s # Посмотреть на том в Longhorn kubectl get volumes.longhorn.io -n longhorn-system | grep app-data ``` --- ## 5. Пример 2: StatefulSet с несколькими репликами **Сценарий:** очередь сообщений или key-value хранилище (Redis, RabbitMQ) с несколькими репликами. Каждая реплика StatefulSet получает **свой отдельный PVC**. ### StatefulSet с volumeClaimTemplates ```yaml apiVersion: apps/v1 kind: StatefulSet metadata: name: redis namespace: cache spec: serviceName: redis replicas: 3 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis:7-alpine ports: - containerPort: 6379 command: - redis-server - --appendonly yes - --dir /data volumeMounts: - name: data mountPath: /data resources: requests: cpu: 100m memory: 256Mi limits: cpu: 500m memory: 512Mi volumeClaimTemplates: # Kubernetes создаёт PVC для каждой реплики - metadata: name: data spec: accessModes: ["ReadWriteOnce"] # каждая реплика = свой том storageClassName: longhorn resources: requests: storage: 10Gi ``` При `replicas: 3` Kubernetes создаст три PVC: - `data-redis-0` → 10Gi - `data-redis-1` → 10Gi - `data-redis-2` → 10Gi ```bash kubectl get pvc -n cache # NAME STATUS VOLUME CAPACITY ACCESS MODES # data-redis-0 Bound pvc-aaa11111 10Gi RWO # data-redis-1 Bound pvc-bbb22222 10Gi RWO # data-redis-2 Bound pvc-ccc33333 10Gi RWO ``` > **Важно:** при уменьшении `replicas` PVC **не удаляются автоматически**. Это намеренное поведение Kubernetes — защита от потери данных. Удалять PVC вручную после проверки. --- ## 6. Пример 3: база данных (PostgreSQL) с Longhorn **Сценарий:** продакшн PostgreSQL с выделенным Longhorn-томом. ### Объекты ```yaml --- # PVC отдельно от StatefulSet — для явного управления жизненным циклом apiVersion: v1 kind: PersistentVolumeClaim metadata: name: postgres-data namespace: myapp annotations: # Документировать содержимое тома для оператора longhorn.io/description: "PostgreSQL data for myapp — created 2026-01-01" spec: accessModes: - ReadWriteOnce storageClassName: longhorn resources: requests: storage: 50Gi --- apiVersion: apps/v1 kind: StatefulSet metadata: name: postgres namespace: myapp spec: serviceName: postgres replicas: 1 selector: matchLabels: app: postgres template: metadata: labels: app: postgres spec: securityContext: fsGroup: 999 # postgres GID — Longhorn создаст том с нужными правами containers: - name: postgres image: postgres:16-alpine ports: - containerPort: 5432 env: - name: POSTGRES_DB value: myapp - name: POSTGRES_USER value: myuser - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: postgres-secret key: password - name: PGDATA value: /var/lib/postgresql/data/pgdata # подпапка, не корень тома volumeMounts: - name: data mountPath: /var/lib/postgresql/data resources: requests: cpu: 200m memory: 512Mi limits: cpu: 1000m memory: 2Gi readinessProbe: exec: command: [pg_isready, -U, myuser, -d, myapp] initialDelaySeconds: 10 periodSeconds: 5 volumes: - name: data persistentVolumeClaim: claimName: postgres-data ``` > Переменная `PGDATA=/var/lib/postgresql/data/pgdata` — подпапка внутри mountPath. > PostgreSQL требует пустую директорию при инициализации. Если смонтировать том прямо в `/var/lib/postgresql/data`, Longhorn создаёт там `lost+found` и PostgreSQL падает с ошибкой. ### Резервное копирование PostgreSQL Два уровня резервирования: 1. **Longhorn snapshot** — снапшот файловой системы (быстрый, crash-consistent) 2. **pg_dump** — логический дамп (переносимый, consistent) ```bash # Логический дамп из Pod kubectl exec -n myapp postgres-0 -- \ pg_dump -U myuser myapp | gzip > backup-$(date +%Y%m%d).sql.gz # Восстановление kubectl exec -i -n myapp postgres-0 -- \ psql -U myuser myapp < backup-20260101.sql ``` --- ## 7. Пример 4: несколько компонентов с общим хранилищем **Сценарий:** несколько Pod должны читать один и тот же набор файлов (например, общий медиа-каталог между CMS и CDN-прокси). ### Проблема: ReadWriteOnce не подходит `ReadWriteOnce` разрешает запись только с одного узла. При нескольких Pod на разных узлах — ошибка монтирования. ### Решение A: все Pod на одном узле (nodeSelector) ```yaml # Принудительно разместить все Pod на одной ноде spec: nodeSelector: kubernetes.io/hostname: k8s-worker-01 ``` Тогда PVC типа `ReadWriteOnce` работает с несколькими Pod на одном узле. ### Решение B: NFS через Longhorn (ReadWriteMany) Longhorn поддерживает `ReadWriteMany` через встроенный NFS-шлюз начиная с версии 1.5. ```yaml --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: shared-media namespace: cms spec: accessModes: - ReadWriteMany # несколько узлов одновременно storageClassName: longhorn resources: requests: storage: 100Gi ``` ```yaml --- # CMS Pod — пишет медиафайлы apiVersion: apps/v1 kind: Deployment metadata: name: cms namespace: cms spec: replicas: 1 template: spec: containers: - name: cms image: registry.gigacoms.info/myteam/cms:v2.0.0 volumeMounts: - name: media mountPath: /app/media volumes: - name: media persistentVolumeClaim: claimName: shared-media --- # CDN-прокси Pod — читает медиафайлы с того же тома apiVersion: apps/v1 kind: Deployment metadata: name: cdn-proxy namespace: cms spec: replicas: 3 # несколько реплик, разные ноды — RWX позволяет template: spec: containers: - name: cdn-proxy image: nginx:1.27-alpine volumeMounts: - name: media mountPath: /usr/share/nginx/html/media readOnly: true # прокси только читает volumes: - name: media persistentVolumeClaim: claimName: shared-media ``` > **Ограничение RWX в Longhorn:** производительность ниже, чем RWO (идёт через NFS). Для баз данных использовать только RWO. RWX — для файлов с редкой записью и частым чтением. --- ## 8. Реплики: сколько и когда менять ### Текущая конфигурация кластера ``` Кластер: 1 worker-нода (k8s-worker-01) Реплики: 1 (longhorn_default_replica_count: 1) ``` Одна реплика означает: **данные не реплицируются**. При потере ноды — данные недоступны до восстановления узла. ### Когда увеличивать количество реплик | Число worker-нод | Рекомендуемые реплики | Причина | |---|---|---| | 1 | **1** | Некуда реплицировать | | 2 | **2** | Реплика на каждой ноде; потеря одной ноды — нет прерывания | | 3+ | **3** | Quorum; потеря одной ноды — данные доступны | ### Как изменить количество реплик **Глобально** (для новых томов): ```yaml # inventory/prod/group_vars/longhorn.yml или group_vars/k8s_cluster.yml longhorn_default_replica_count: 2 # изменить при добавлении worker-нод ``` ```bash ansible-playbook -i inventory/prod playbooks/setup_longhorn.yml # Применит только Helm-апгрейд, не затронет prereqs и диски ``` **Для конкретного тома** (через UI или kubectl): ```bash # Через Longhorn API kubectl patch volume.longhorn.io pvc-abc12345 -n longhorn-system \ --type=merge -p '{"spec":{"numberOfReplicas":2}}' ``` **Для конкретного PVC через StorageClass**: ```yaml # Создать отдельный StorageClass с другим количеством реплик apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: longhorn-replicated provisioner: driver.longhorn.io parameters: numberOfReplicas: "2" staleReplicaTimeout: "30" dataLocality: "disabled" reclaimPolicy: Delete allowVolumeExpansion: true volumeBindingMode: Immediate ``` ```yaml # PVC с кастомным StorageClass spec: storageClassName: longhorn-replicated # вместо longhorn ``` ### soft anti-affinity (при числе нод < числа реплик) Если `numberOfReplicas: 2`, но доступна только 1 нода — Longhorn не создаст том (нет места для реплики). Для обхода в dev-окружении включить `replicaNodeLevelSoftAntiAffinity`: ```yaml longhorn_replica_node_soft_anti_affinity: "true" # НЕ использовать в production ``` --- ## 9. Резервное копирование и восстановление ### Longhorn Snapshot (мгновенный снапшот тома) Снапшот — point-in-time копия тома на том же диске. Защищает от случайного удаления данных, но не от потери диска. ```bash # Создать снапшот через kubectl kubectl create -f - < → Create Snapshot. ### Backup в S3-совместимое хранилище Для полноценного backup необходимо настроить внешнее хранилище (S3, NFS): ```yaml # Настройка backup target через Longhorn Settings # UI: http://10.203.0.96:10002 → Settings → Backup Target # Значение: s3://bucket-name@region/prefix (для S3) # nfs://server/path (для NFS) ``` ```bash # Создать backup существующего снапшота kubectl create -f - < → Snapshots → Revert # Или через kubectl (том должен быть detached) # 1. Масштабировать Deployment в 0 (отмонтировать том) kubectl scale deployment my-app -n my-app --replicas=0 # 2. Revert через UI или API # 3. Вернуть Deployment kubectl scale deployment my-app -n my-app --replicas=1 ``` --- ## 10. Расширение тома Longhorn поддерживает расширение тома **без остановки Pod** (online expansion). ### Расширить PVC ```bash # Отредактировать PVC — увеличить storage (уменьшение не поддерживается) kubectl patch pvc app-data -n my-app \ --type=merge -p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}' # Проверить прогресс kubectl get pvc app-data -n my-app # STATUS: пока идёт расширение, будет Bound с условием FileSystemResizePending # После завершения kubectl describe pvc app-data -n my-app | grep -A5 Conditions ``` Расширение на уровне файловой системы происходит автоматически при следующем монтировании Pod (или сразу, если `allowVolumeExpansion: true` в StorageClass — а у Longhorn это включено по умолчанию). ### Проверить размер тома внутри Pod ```bash kubectl exec -n my-app $(kubectl get pod -n my-app -l app=my-app -o name) \ -- df -h /app/data ``` --- ## 11. Добавление нового worker-узла с дисками При добавлении `k8s-worker-02`: ### Шаг 1: добавить в инвентарь ```yaml # inventory/prod/hosts.yml workers: hosts: k8s-worker-01: ansible_host: 10.203.0.96 k8s-worker-02: # новый узел ansible_host: 10.203.0.XX longhorn_disks: # если диски отличаются от дефолта - device: /dev/sdb mountpoint: /mnt/longhorn-disk1 ``` ### Шаг 2: запустить prereqs только для нового узла ```bash # Подготовка ОС только для новой ноды ansible-playbook -i inventory/prod playbooks/setup_longhorn.yml \ --limit k8s-worker-02 \ --tags longhorn_prereqs ``` ### Шаг 3: запустить join нового воркера в кластер ```bash ansible-playbook -i inventory/prod playbooks/setup_worker_plane.yml \ --limit k8s-worker-02 ``` ### Шаг 4: аннотировать ноду и обновить Longhorn ```bash # Полный плейбук обновит аннотации для всех нод включая новую ansible-playbook -i inventory/prod playbooks/setup_longhorn.yml ``` ### Шаг 5: увеличить количество реплик ```yaml # group_vars/k8s_cluster.yml или longhorn.yml longhorn_default_replica_count: 2 # было 1 ``` ```bash ansible-playbook -i inventory/prod playbooks/setup_longhorn.yml ``` --- ## 12. Диагностика и типичные ошибки ### Основные команды ```bash # Состояние всех томов kubectl get volumes.longhorn.io -n longhorn-system # Детали тома kubectl describe volume.longhorn.io -n longhorn-system # Состояние реплик kubectl get replicas.longhorn.io -n longhorn-system | grep # Состояние нод Longhorn kubectl get nodes.longhorn.io -n longhorn-system # Логи manager kubectl logs -n longhorn-system -l app=longhorn-manager --tail=100 # Логи CSI driver kubectl logs -n longhorn-system -l app=longhorn-csi-plugin --tail=50 # PVC не привязывается — посмотреть события kubectl describe pvc -n kubectl get events -n --sort-by='.lastTimestamp' # Посмотреть все PVC в кластере kubectl get pvc -A ``` ### Таблица ошибок | Симптом | Причина | Решение | |---|---|---| | PVC в статусе `Pending` | Нет доступных дисков / нода не готова | `kubectl get nodes.longhorn.io -n longhorn-system` — проверить schedulable | | PVC `Pending`: `no schedulable node` | Включён soft anti-affinity=false, только 1 нода, replicas=2 | Уменьшить реплики до 1 или включить soft anti-affinity | | Pod не стартует: `volume not found` | Том деградировал или удалён | Проверить `kubectl get volumes.longhorn.io -n longhorn-system` | | Том в статусе `Degraded` | Нода с репликой недоступна | Подождать восстановления ноды или rebuild replica | | Том в статусе `Faulted` | Все реплики потеряны | Восстановить из бэкапа | | `iscsiadm` ошибки в логах | SELinux блокирует iscsid | Перепроверить CIL-политику: `semodule -l \| grep local_longhorn` | | Медленная запись | Все реплики на одном диске | Разнести реплики по дискам (Disk Tags) | | `lost+found` в PostgreSQL data dir | PGDATA смонтирован прямо на корень тома | Установить `PGDATA=/var/lib/postgresql/data/pgdata` (подпапка) | | Диск не виден в Longhorn | Аннотация ноды устарела | `ansible-playbook setup_longhorn.yml` — обновит annotate | ### Проверка iSCSI на ноде ```bash # Выполнить через SSH на worker-ноде или kubectl exec # Статус iscsid systemctl status iscsid # Список активных iSCSI-сессий (примонтированные тома) iscsiadm -m session # Загружены ли нужные модули ядра lsmod | grep iscsi_tcp lsmod | grep dm_crypt ``` ### Принудительный detach застрявшего тома Если Pod удалён, но том остался в статусе attached: ```bash # Через UI: Longhorn → Volumes → → Detach # Или через kubectl kubectl patch volume.longhorn.io -n longhorn-system \ --type=merge -p '{"spec":{"nodeID":""}}' ``` --- ## 13. Чеклист перед использованием хранилища ### Первое использование Longhorn в кластере - [ ] `setup_longhorn.yml` выполнен успешно (prereqs + helm install) - [ ] Все worker-ноды показывают статус `Ready` в UI Longhorn (`http://10.203.0.96:10002`) - [ ] Диски видны в UI Longhorn: Node → Disks - [ ] StorageClass `longhorn` существует: `kubectl get storageclass` - [ ] Тестовый PVC создаётся и переходит в `Bound`: `kubectl apply -f test-pvc.yaml` ### Перед каждым новым PVC / StatefulSet - [ ] Выбран правильный `accessMode`: - `ReadWriteOnce` — один узел, обычные Pod и StatefulSet - `ReadWriteMany` — несколько узлов, только через Longhorn NFS - [ ] `storageClassName: longhorn` указан явно (не полагаться на default при нескольких StorageClass) - [ ] Указан разумный размер тома (расширить можно, уменьшить — нет) - [ ] Для PostgreSQL: `PGDATA` установлен в подпапку, `fsGroup: 999` в securityContext - [ ] Для StatefulSet: использован `volumeClaimTemplates`, не `volumes + PVC` - [ ] `resources.requests` и `limits` заданы для контейнеров ### После деплоя - [ ] PVC в статусе `Bound`: `kubectl get pvc -n ` - [ ] Pod в статусе `Running`: `kubectl get pods -n ` - [ ] Том виден в Longhorn UI со статусом `Healthy` - [ ] Данные доступны внутри Pod: `kubectl exec -n -- ls /mountpath` - [ ] (для продакшн-данных) Настроен регулярный snapshot или backup