====== 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