====== Kubernetes: Longhorn, установка ====== Небольшое how-to по базовой настройке //Cloud Native Distributed Block Storage for Kubernetes//. ===== Допущения и ограничения ===== * У вас уже есть готовый кластер K8s. * Вы уже установили необходимые зависимости — [[https://longhorn.io/docs/1.8.0/deploy/install/#installation-requirements|Installation Requirements]]. * Либо воспользовались готовой ролью — [[https://github.com/denisitpro/ansible-all/tree/master/roles/longhorn-dependens|ansible-all (Longhorn dependencies)]]. * У вас отключен multipathd — [[https://longhorn.io/kb/troubleshooting-volume-with-multipath/|https://longhorn.io/kb/troubleshooting-volume-with-multipath/]] **ВАЖНО**: установить ''open-iscsi'' и ''NFS'' можно и через //DaemonSet//. Однако это не самый оптимальный способ, так как отладка будет сложнее — не только на уровне ОС, но и дополнительно через //DaemonSet//. ===== Немного о зависимостях ===== Longhorn требует в зависимостях ''NFS'' и ''open-iscsi'', которые выполняют разные задачи. ==== open-iscsi ==== **Когда нужен:** * Longhorn использует //iSCSI// для подключения к виртуальным дискам (//longhorn-volumes//) с нод, на которых запущены поды. * Требуется, если ты используешь Longhorn в качестве хранилища для блоковых устройств, например, для подов с //ReadWriteOnce (RWO)// томами. * Позволяет подам монтировать диски, даже если они физически находятся на другой ноде. ==== NFS ==== **Когда нужен:** * Если у тебя есть приложения, которым требуется //ReadWriteMany (RWX)//, то есть одновременно монтируемые тома (например, для //WordPress, Nextcloud, shared data storage//). * Если ты используешь Longhorn со встроенным //NFS-гейтом// (''longhorn-nfs-provisioner''), который позволяет использовать Longhorn-тома как NFS-ресурсы. * Если часть приложений не поддерживает //iSCSI//, но нормально работает с //NFS//. Таким образом, в большинстве случаев требуются оба компонента. ===== Валидация установки ===== Чтобы убедится, что ваши воркер ноды корректно настроены воспользуемся [[https://longhorn.io/docs/1.8.0/deploy/install/#using-the-longhorn-command-line-tool|https://longhorn.io/docs/1.8.0/deploy/install/#using-the-longhorn-command-line-tool]] Создайте конфиг с подключениями к кубу admin.conf, после тестовой проверки можно будет удалить curl -sSfL -o longhornctl https://github.com/longhorn/cli/releases/download/v1.8.0/longhornctl-linux-amd64 chmod +x longhornctl export KUBECONFIG=/etc/kubernetes/admin.conf ./longhornctl check preflight Либо используя скрипт sudo apt -y install jq curl -sSfL https://raw.githubusercontent.com/longhorn/longhorn/v1.8.0/scripts/environment_check.sh | bash ===== О стенде ===== Состоит из трёх //мастеров// и четырёх рабочих //нод//. [k8s_master] k8s-01.beta-82.win k8s-02.beta-82.win k8s-03.beta-82.win [k8s_worker] w-01.beta-82.win w-02.beta-82.win w-03.beta-82.win w-04.beta-82.win ===== О replicaCount , degraded и faulted ===== ==== replicaCount=2 ==== * Longhorn хранит каждую //volume// в двух копиях (//репликах//) на разных нодах. * Если нода падает, у тебя остаётся только одна реплика, и том становится **degraded** (//деградированным//), но остаётся доступным. Для большей надёжности можно использовать ''replicaCount=3'', если нужно защититься от второго сбоя. ==== Degraded ( деградированный том ) ==== * Это состояние, когда потеряна одна или несколько реплик, но остаётся достаточно копий для работы. * Том остаётся доступным и продолжает работать. * Longhorn автоматически создаст новую реплику, если есть место на оставшихся нодах. ==== Faulted ( отказавший том ) ==== * Это критическое состояние, при котором все доступные реплики утеряны. * Данные в томе становятся недоступными (он не может быть примонтирован). * Если в Longhorn нет активной реплики тома, он не сможет восстановиться автоматически. ==== Главное отличие Degraded от Faulted ==== * **Degraded** = том ещё работает, но данных меньше, чем ''replicaCount''. * **Faulted** = том полностью вышел из строя, данных нет. * Если том **degraded**, но не **faulted**, Longhorn автоматически восстановится при вводе новых нод. * Если том **faulted**, то даже с новыми нодами он не восстановится автоматически — только из //бэкапа//. ===== Установка Longhorn с использованием Helm ===== Для демонстрации **faulted** при отказе 2 из 4 нод я буду использовать ''replicaCount=3''. Однако вы должны выбирать его самостоятельно, в зависимости от ваших задач. В примере ниже будем устанавливать самую актуальную на данный момент версию **1.8.0**. Подробнее об установке можно почитать здесь: [[https://longhorn.io/docs/1.8.0/deploy/install/install-with-helm/|Официальная документация Longhorn]] Также может быть полезен файл ''longhorn.yml'': [[https://raw.githubusercontent.com/longhorn/longhorn/v1.8.0/deploy/longhorn.yaml|Longhorn YAML]] Приступим helm repo add longhorn https://charts.longhorn.io helm repo update helm install longhorn longhorn/longhorn \ --namespace longhorn-system --create-namespace --version 1.8.0 \ --set defaultSettings.replicaCount=3 \ --set defaultSettings.storageOverProvisioningPercentage=100 \ --set defaultSettings.storageMinimalAvailablePercentage=15 \ --set defaultSettings.nodeDownPodDeletionPolicy="delete-both-statefulset-and-deployment-pod" \ --set defaultSettings.replicaAutoBalance="best-effort" \ --set defaultSettings.dataLocality="best-effort" \ --set defaultSettings.rebalanceDisksWhenLowSpace=true Описание параметров ^**Параметр** ^**Значение** ^**Описание** | |''defaultSettings.replicaCount'' |''3'' |Количество реплик каждого тома. Чем больше, тем выше отказоустойчивость, но выше нагрузка на диск. При ''3'' том остаётся доступным при отказе 1 ноды.| |''defaultSettings.storageOverProvisioningPercentage'' |''100'' |Указывает, на сколько процентов Longhorn может «переподписывать» (overprovision) дисковое пространство. Значение ''100'' означает, что выделять можно в 2 раза больше, чем фактический объём диска.| |''defaultSettings.storageMinimalAvailablePercentage'' |''15'' |Минимальный порог свободного дискового пространства перед тем, как Longhorn перестанет создавать новые реплики. ''15%'' означает, что если остаётся менее 15% места, новые реплики не создаются.| |''defaultSettings.nodeDownPodDeletionPolicy'' |''"delete-both-statefulset-and-deployment-pod"'' |Определяет, что делать с подами, привязанными к сбойным нодам. Данное значение удаляет поды как из ''StatefulSet'', так и из ''Deployment'', если нода ушла в ''NotReady''.| |''defaultSettings.replicaAutoBalance'' |''"best-effort"'' |Автоматический баланс реплик между нодами. ''best-effort'' означает, что Longhorn старается равномерно распределять реплики при изменениях в кластере.| |''defaultSettings.dataLocality'' |''"best-effort"'' |Определяет, будет ли Longhorn пытаться хранить реплику тома на той же ноде, где работает под. ''best-effort'' создаёт локальную копию, если это возможно, но не требует её наличия.| |''defaultSettings.rebalanceDisksWhenLowSpace'' |''true'' |Включает автоматическое перемещение реплик с дисков, на которых заканчивается место, на менее загруженные диски или ноды.| Проверяем наши поды, как видим они поднялись $ k get pods NAME READY STATUS RESTARTS AGE csi-attacher-79866cdcf8-grgs9 1/1 Running 0 2m7s csi-attacher-79866cdcf8-jvg8z 1/1 Running 0 2m7s csi-attacher-79866cdcf8-qknmr 1/1 Running 0 2m7s csi-provisioner-664cb5bdd5-dxpnt 1/1 Running 0 2m7s csi-provisioner-664cb5bdd5-qc85t 1/1 Running 0 2m7s csi-provisioner-664cb5bdd5-r2vfj 1/1 Running 0 2m7s csi-resizer-64f6fb4459-98qtn 1/1 Running 0 2m7s csi-resizer-64f6fb4459-l9v27 1/1 Running 0 2m7s csi-resizer-64f6fb4459-tvqr6 1/1 Running 0 2m7s csi-snapshotter-7b7db78f9-6gpj4 1/1 Running 0 2m7s csi-snapshotter-7b7db78f9-7k52m 1/1 Running 0 2m7s csi-snapshotter-7b7db78f9-jnc4k 1/1 Running 0 2m7s engine-image-ei-c2d50bcc-5jcgl 1/1 Running 0 3m engine-image-ei-c2d50bcc-8fb7v 1/1 Running 0 3m engine-image-ei-c2d50bcc-gmvhc 1/1 Running 0 3m engine-image-ei-c2d50bcc-jnhd4 1/1 Running 0 3m instance-manager-79fbdba3783ea7f6d1ea35496adae27f 1/1 Running 0 2m25s instance-manager-93ee330ab4dd110c09b6c0dba65a6184 1/1 Running 0 2m19s instance-manager-b565b7b70c365483ceeab00de81eb424 1/1 Running 0 2m29s instance-manager-d5b24c31c174dd52fdabdad19038bbff 1/1 Running 0 2m19s longhorn-csi-plugin-6995m 3/3 Running 0 2m7s longhorn-csi-plugin-9fhk5 3/3 Running 0 2m7s longhorn-csi-plugin-sb42r 3/3 Running 0 2m7s longhorn-csi-plugin-zxcpm 3/3 Running 0 2m7s longhorn-driver-deployer-7fc7ffcb7f-vbft2 1/1 Running 2 (2m57s ago) 3m24s longhorn-manager-gfr55 2/2 Running 0 3m24s longhorn-manager-kswnm 2/2 Running 0 3m24s longhorn-manager-rsnw7 2/2 Running 0 3m24s longhorn-manager-wvjvk 2/2 Running 0 3m24s longhorn-ui-5fcc4bcfc7-4wl6s 1/1 Running 0 3m24s longhorn-ui-5fcc4bcfc7-lmrg9 1/1 Running 0 3m24s ===== Longhorn UI ===== Для удобства может потребоваться доступ к **Longhorn UI**. Получить его можно с помощью следующей команды: $ kubectl port-forward svc/longhorn-frontend -n longhorn-system 8080:80 после чего в браузере станет доступно [[http://127.0.0.1:8080/#/dashboard|http://127.0.0.1:8080/#/dashboard]] ===== Запуск тестового приложения ===== Сейчас я запущу небольшое тестовое приложение, на котором протестируем: * **Создание тома** * **Отказ одной ноды** * **Отказ двух нод** Я запустил небольшой манифест, в котором сначала создаётся //PersistentVolumeClaim// (''longhorn-pvc'') для ''StorageClass'' — ''longhorn'', после чего развернул //Deployment// с подом, который использует наш ''PVC'' --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: longhorn-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi storageClassName: longhorn --- apiVersion: apps/v1 kind: Deployment metadata: name: go-app spec: replicas: 1 selector: matchLabels: app: go-app-label template: metadata: labels: app: go-app-label spec: containers: - image: denisitpro/go-example:latest name: go-app env: - name: AI_NAME value: "Ivan" ports: - containerPort: 8082 readinessProbe: failureThreshold: 5 httpGet: path: / port: 8082 periodSeconds: 10 successThreshold: 2 timeoutSeconds: 3 livenessProbe: failureThreshold: 3 httpGet: path: / port: 8082 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 3 initialDelaySeconds: 10 resources: requests: cpu: 100m memory: 64Mi limits: cpu: 300m memory: 512Mi volumeMounts: - mountPath: /opt/example name: data-volume volumes: - name: data-volume persistentVolumeClaim: claimName: "longhorn-pvc" Далее проверим текущее состояние **Pod**, **PVC** и **PV** в кластере. $ k get pods NAME READY STATUS RESTARTS AGE go-app-fd8f8d4c-4s4wg 1/1 Running 0 59s $ k get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE longhorn-pvc Bound pvc-0b7a49a6-352c-4c0a-84af-e2b014dd75c5 1Gi RWO longhorn 62s $ k get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE pvc-0b7a49a6-352c-4c0a-84af-e2b014dd75c5 1Gi RWO Delete Bound go/longhorn-pvc longhorn 58s Имеет смысл также проверить, что наш ''StorageClass'' стал **дефолтным**. $ k get sc NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE longhorn (default) driver.longhorn.io Delete Immediate true 4m31s longhorn-static driver.longhorn.io Delete Immediate true 4m29s ===== Тестирование отказоустойчивости при потере одной рабочей ноды ===== Убеждаемся, что **Pod** действительно запущен на ''w-01''. $k get pv $kubectl -n longhorn-system get volumes pvc-0b7a49a6-352c-4c0a-84af-e2b014dd75c5 -o jsonpath='{.status.ownerID}' ### output w-01 Сначала создадим небольшой тестовый файл, прежде чем отключать рабочие ноды кластера. $ kubectl exec -it go-app-fd8f8d4c-4s4wg -- bash $ echo "42"> /opt/example/42.txt $ cat /opt/example/42.txt 42 Убеждаемся, что **Pod** действительно запущен на ''w-01''. $ k get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES go-app-fd8f8d4c-4s4wg 1/1 Running 0 9m10s 10.48.6.85 w-01 Отключаем рабочую ноду и проверяем, что кластер зафиксировал её как **нерабочую**. **Важно:** Под не завершится сразу — процесс займёт примерно **10 минут**, это время можно сократить правильными настройками, но для данного теста это не имеет значения * **5 минут** — пока Kubernetes посчитает, что нода мертва. * **5 минут** — дополнительная задержка перед удалением пода. $ k get nodes NAME STATUS ROLES AGE VERSION k8s-01 Ready control-plane 43m v1.31.1 k8s-02 Ready control-plane 42m v1.31.1 k8s-03 Ready control-plane 42m v1.31.1 w-01 NotReady 40m v1.31.1 w-02 Ready 40m v1.31.1 w-03 Ready 40m v1.31.1 w-04 Ready 40m v1.31.1 $k get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES go-app-fd8f8d4c-4s4wg 1/1 Terminating 0 16m 10.48.6.85 w-01 go-app-fd8f8d4c-c4trm 0/1 ContainerCreating 0 26s w-03 Теперь проверим, что происходит с данными внутри пода. $ k get pod NAME READY STATUS RESTARTS AGE go-app-fd8f8d4c-5xq2n 1/1 Running 0 4m39s $ kubectl exec -it go-app-fd8f8d4c-5xq2n -- bash $root@go-app-fd8f8d4c-5xq2n:/opt/app# cat /opt/example/42.txt 42 ===== Тестирование при потере двух рабочих нод ===== Продолжим эксперимент — отключим вторую рабочую ноду, предварительно добавив ещё одну строку в файл. $ kubectl exec -it go-app-fd8f8d4c-5xq2n -- bash $ echo "43">> /opt/example/42.txt $ cat /opt/example/42.txt 42 43 Отключаем ''w-02'', так как под сейчас находится на этой ноде. $ k get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES go-app-fd8f8d4c-5xq2n 1/1 Running 0 8m18s 10.48.4.213 w-02 $ kubectl -n longhorn-system get volumes pvc-0b7a49a6-352c-4c0a-84af-e2b014dd75c5 -o jsonpath='{.status.ownerID}' w-02% Теперь проверяем текущее состояние кластера и на какой ноде запущен под. $ k get nodes NAME STATUS ROLES AGE VERSION k8s-01 Ready control-plane 61m v1.31.1 k8s-02 Ready control-plane 61m v1.31.1 k8s-03 Ready control-plane 61m v1.31.1 w-01 NotReady 59m v1.31.1 w-02 NotReady 59m v1.31.1 w-03 Ready 59m v1.31.1 w-04 Ready 59m v1.31.1 $ k get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES go-app-fd8f8d4c-fcwc2 1/1 Running 0 4m39s 10.48.4.23 w-04 Проверка состояния тома после миграции пода, как мы видим, под переместился на ''w-04''. Теперь проверим состояние //volume//. k get volume -n longhorn-system NAME DATA ENGINE STATE ROBUSTNESS SCHEDULED SIZE NODE AGE pvc-0b7a49a6-352c-4c0a-84af-e2b014dd75c5 v1 attached degraded 1073741824 w-04 36m Теперь проверим, сохранились ли данные после миграции пода. $ k get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES go-app-fd8f8d4c-fcwc2 1/1 Running 0 8m10s 10.48.4.23 w-04 $ kubectl exec -it go-app-fd8f8d4c-fcwc2 -- bash $ cat /opt/example/42.txt 42 43 ===== Включение выключенных нод и отключение активной ===== Сейчас добавим данные, включим ранее выключенные ноды, затем отключим активную ноду и проверим состояние данных. Это финальная проверка $ kubectl exec -it go-app-fd8f8d4c-fcwc2 -- bash $ echo "44">> /opt/example/42.txt $ cat /opt/app# cat /opt/example/42.txt 42 43 44 Пришло время провести завершающую проверку $ k get nodes NAME STATUS ROLES AGE VERSION k8s-01 Ready control-plane 79m v1.31.1 k8s-02 Ready control-plane 78m v1.31.1 k8s-03 Ready control-plane 78m v1.31.1 w-01 Ready 76m v1.31.1 w-02 Ready 76m v1.31.1 w-03 Ready 76m v1.31.1 w-04 NotReady 76m v1.31.1 $ k get pods -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES go-app-fd8f8d4c-fpfp4 1/1 Running 0 32s 10.48.6.94 w-01 $ kubectl exec -it go-app-fd8f8d4c-fpfp4 -- bash $ cat /opt/example/42.txt 42 43 44 ===== Нюансы replicaCount=3 ===== **ВАЖНО**: при установке через //Helm// параметр ''auto-salvage'' уже установлен в ''true'', но я решил оставить эту информацию для понимания логики работы. ---- В сценарии, когда у нас есть 4 рабочие ноды, а ''replicaCount=3'', при потере двух нод мы получаем не совсем то, что ожидали — том станет **faulted**. **Печально**. ==== Причина в дизайне высокой доступности ( HA ) Longhorn: ==== - Longhorn требует минимум **2 реплики** для работы тома, если у него ''replicaCount=3''. - Если остаётся только одна реплика, Longhorn не считает данные достаточно надёжными → том переходит в **faulted**. - Это сделано для защиты от //silent data corruption// и случаев, когда последняя оставшаяся реплика может быть повреждена. ==== Как сделать так, чтобы том не переходил в faulted ? ==== Настроить **Replica Auto Salvage**: ''defaultSettings.autoSalvage=true'' В целом, лучше придерживаться схемы: **количество нод должно быть replicaCount + 2**. ===== Полезные команды ===== **Проверка состояния PVC** kubectl get volumes.longhorn.io -n longhorn-system