k8s/roles/longhorn/README.md
2026-07-15 11:14:21 +03:00

32 KiB
Raw Permalink Blame History

Longhorn — стандарт постоянного хранилища в кластере

Этот документ описывает стандарт работы с Longhorn в нашем кластере:
как подключать хранилище к приложениям, добавлять диски, управлять репликами и резервными копиями.
Является эталоном для разработчиков и DevOps.


Содержание

  1. Архитектура: как Longhorn работает в кластере
  2. Два этапа деплоя: longhorn_prereqs и longhorn
  3. Конфигурация дисков
  4. Пример 1: PVC для одного Pod (ReadWriteOnce)
  5. Пример 2: StatefulSet с несколькими репликами
  6. Пример 3: база данных (PostgreSQL) с Longhorn
  7. Пример 4: несколько компонентов с общим хранилищем
  8. Реплики: сколько и когда менять
  9. Резервное копирование и восстановление
  10. Расширение тома
  11. Добавление нового worker-узла с дисками
  12. Диагностика и типичные ошибки
  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 по умолчанию в кластере.

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 — глобальный дефолт

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 — переопределение для конкретной ноды

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

# Посмотреть ноды и их диски в 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

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data
  namespace: my-app
spec:
  accessModes:
    - ReadWriteOnce        # один узел в режиме чтения-записи
  storageClassName: longhorn
  resources:
    requests:
      storage: 5Gi

Deployment с PVC

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 создан и привязан

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

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
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-томом.

Объекты

---
# 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)
# Логический дамп из 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)

# Принудительно разместить все Pod на одной ноде
spec:
  nodeSelector:
    kubernetes.io/hostname: k8s-worker-01

Тогда PVC типа ReadWriteOnce работает с несколькими Pod на одном узле.

Решение B: NFS через Longhorn (ReadWriteMany)

Longhorn поддерживает ReadWriteMany через встроенный NFS-шлюз начиная с версии 1.5.

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shared-media
  namespace: cms
spec:
  accessModes:
    - ReadWriteMany         # несколько узлов одновременно
  storageClassName: longhorn
  resources:
    requests:
      storage: 100Gi
---
# 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; потеря одной ноды — данные доступны

Как изменить количество реплик

Глобально (для новых томов):

# inventory/prod/group_vars/longhorn.yml или group_vars/k8s_cluster.yml
longhorn_default_replica_count: 2   # изменить при добавлении worker-нод
ansible-playbook -i inventory/prod playbooks/setup_longhorn.yml
# Применит только Helm-апгрейд, не затронет prereqs и диски

Для конкретного тома (через UI или kubectl):

# Через Longhorn API
kubectl patch volume.longhorn.io pvc-abc12345 -n longhorn-system \
  --type=merge -p '{"spec":{"numberOfReplicas":2}}'

Для конкретного PVC через StorageClass:

# Создать отдельный 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
# PVC с кастомным StorageClass
spec:
  storageClassName: longhorn-replicated   # вместо longhorn

soft anti-affinity (при числе нод < числа реплик)

Если numberOfReplicas: 2, но доступна только 1 нода — Longhorn не создаст том (нет места для реплики).
Для обхода в dev-окружении включить replicaNodeLevelSoftAntiAffinity:

longhorn_replica_node_soft_anti_affinity: "true"   # НЕ использовать в production

9. Резервное копирование и восстановление

Longhorn Snapshot (мгновенный снапшот тома)

Снапшот — point-in-time копия тома на том же диске. Защищает от случайного удаления данных, но не от потери диска.

# Создать снапшот через kubectl
kubectl create -f - <<EOF
apiVersion: longhorn.io/v1beta2
kind: Snapshot
metadata:
  name: myapp-snapshot-$(date +%Y%m%d-%H%M)
  namespace: longhorn-system
spec:
  volume: pvc-abc12345         # имя Longhorn-тома (не PVC)
EOF

# Список снапшотов
kubectl get snapshots.longhorn.io -n longhorn-system | grep pvc-abc12345

Через UI: Longhorn → Volumes → → Create Snapshot.

Backup в S3-совместимое хранилище

Для полноценного backup необходимо настроить внешнее хранилище (S3, NFS):

# Настройка backup target через Longhorn Settings
# UI: http://10.203.0.96:10002 → Settings → Backup Target
# Значение: s3://bucket-name@region/prefix (для S3)
#           nfs://server/path             (для NFS)
# Создать backup существующего снапшота
kubectl create -f - <<EOF
apiVersion: longhorn.io/v1beta2
kind: BackupVolume
metadata:
  name: pvc-abc12345
  namespace: longhorn-system
EOF

Восстановление из снапшота (rollback)

# Через UI: Longhorn → Volumes → <volume> → 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

# Отредактировать 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

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: добавить в инвентарь

# 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 только для нового узла

# Подготовка ОС только для новой ноды
ansible-playbook -i inventory/prod playbooks/setup_longhorn.yml \
  --limit k8s-worker-02 \
  --tags longhorn_prereqs

Шаг 3: запустить join нового воркера в кластер

ansible-playbook -i inventory/prod playbooks/setup_worker_plane.yml \
  --limit k8s-worker-02

Шаг 4: аннотировать ноду и обновить Longhorn

# Полный плейбук обновит аннотации для всех нод включая новую
ansible-playbook -i inventory/prod playbooks/setup_longhorn.yml

Шаг 5: увеличить количество реплик

# group_vars/k8s_cluster.yml или longhorn.yml
longhorn_default_replica_count: 2   # было 1
ansible-playbook -i inventory/prod playbooks/setup_longhorn.yml

12. Диагностика и типичные ошибки

Основные команды

# Состояние всех томов
kubectl get volumes.longhorn.io -n longhorn-system

# Детали тома
kubectl describe volume.longhorn.io <volume-name> -n longhorn-system

# Состояние реплик
kubectl get replicas.longhorn.io -n longhorn-system | grep <volume-name>

# Состояние нод 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 <pvc-name> -n <namespace>
kubectl get events -n <namespace> --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 на ноде

# Выполнить через SSH на worker-ноде или kubectl exec

# Статус iscsid
systemctl status iscsid

# Список активных iSCSI-сессий (примонтированные тома)
iscsiadm -m session

# Загружены ли нужные модули ядра
lsmod | grep iscsi_tcp
lsmod | grep dm_crypt

Принудительный detach застрявшего тома

Если Pod удалён, но том остался в статусе attached:

# Через UI: Longhorn → Volumes → <volume> → Detach

# Или через kubectl
kubectl patch volume.longhorn.io <volume-name> -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 <namespace>
  • Pod в статусе Running: kubectl get pods -n <namespace>
  • Том виден в Longhorn UI со статусом Healthy
  • Данные доступны внутри Pod: kubectl exec -n <ns> <pod> -- ls /mountpath
  • (для продакшн-данных) Настроен регулярный snapshot или backup