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

130 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# gpu_model_storage
Выделенный локальный RAID1-раздел под большие файлы моделей (Ollama и т.п.) на GPU-узлах кластера.
## Зачем это нужно
`gpu_workers` намеренно исключены из Longhorn (`longhorn_disks: []` в `group_vars/gpu_workers.yml`) —
GPU-узлы являются compute-only, а не storage-нодами.
При этом проект `k8s_ai` (Ollama + Ollama Proxy + Open WebUI) хранит файлы моделей через статический
`hostPath` PV (`k8s_ai/k8s/ollama/pvc-models.yaml`), привязанный к конкретному узлу через `nodeAffinity`.
Изначально этот `hostPath` указывал на `/root/ollama-models`, что физически находится на **корневой
файловой системе** узла.
Корневой раздел на `k8s-worker-02` — это RAID1-массив `md126` размером всего **~30 ГБ**
(`sda4`/`sdb4`). Первая же загрузка модели среднего размера (обычно десятки гигабайт) заполнила бы
диск полностью и уронила бы узел — на `/` живут `systemd`, `containerd`, `kubelet` и вся остальная ОС.
Kubernetes при этом **не** проверяет заявленную ёмкость `hostPath` PV (`capacity.storage` — чисто
декларативное поле), так что переполнение диска не было бы предотвращено на уровне Kubernetes.
При этом два зеркальных диска узла (`sda`/`sdb`, по 4 ТБ каждый) используют под ОС (`boot`,
`boot_efi`, `root`) только первые ~34 ГБ — оставшиеся ~3.6 ТБ на каждом диске были полностью
неразмеченными и простаивали.
**Решение**: роль нарезает из этого свободного места отдельный RAID1-раздел, форматирует его и
монтирует в отдельную точку — модели больше не могут повлиять на работоспособность ОС.
## Архитектура
```
sda, sdb (по 4 ТБ, зеркало)
├─ sda1/sdb1 → md127 (boot, ext4, ~1 ГБ) — существовало до роли
├─ sda2/sdb2 → md125 (boot_efi, vfat, ~0.6 ГБ) — существовало до роли
├─ sda3/sdb3 → LVM (swap) — существовало до роли
├─ sda4/sdb4 → md126 (root, ext4, ~30 ГБ) — существовало до роли
└─ sda5/sdb5 → md1 (модели, xfs, gpu_model_storage_partition_size_gb ГБ) ← создаёт эта роль
└─ смонтирован в gpu_model_storage_mountpoint (по умолчанию /mnt/ollama-models)
```
Партиция №5 создаётся сразу после существующих ОС-партиций (старт фиксирован на `34GB`, конец —
`34 + gpu_model_storage_partition_size_gb` ГБ), на каждом диске из `gpu_model_storage_devices`.
Из этих партиций собирается новый независимый `mdadm`-массив RAID1 (не имеет отношения к
`md126`/`md127`/`md125` — отдельный массив, отдельная точка монтирования).
## Как включить для узла/группы
Роль **выключена по умолчанию** (`gpu_model_storage_devices: []` в `defaults/main.yml`) — по тому
же паттерну, что и `longhorn_disks` в `longhorn_prereqs`. Чтобы включить, переопределите список
дисков в `group_vars/<group>.yml` или `hosts.yml` для конкретного хоста:
```yaml
# inventory/prod/group_vars/gpu_workers.yml
gpu_model_storage_devices:
- /dev/sda
- /dev/sdb
gpu_model_storage_partition_size_gb: 1000
```
Сейчас это включено для всей группы `gpu_workers` (на данный момент единственный член —
`k8s-worker-02`).
## Запуск
```bash
ansible-playbook -i inventory/prod playbooks/setup_gpu_model_storage.yml
```
Плейбук нацелен на группу `gpu_workers`. Порядок относительно `setup_worker_plane.yml` /
`setup_gpu.yml` **не важен** — роль работает только с локальными дисками узла и не трогает
`containerd`/`kubelet`/сеть. Можно запускать в любой момент, включая до `kubeadm join`.
Все задачи идемпотентны:
- `community.general.parted` не пересоздаёт партицию, если она уже существует с нужными параметрами.
- `raid.yml` проверяет `mdadm --detail <device>` перед созданием массива — повторный запуск не
пересоздаёт RAID.
- `filesystem.yml` (`community.general.filesystem`) не переформатирует уже отформатированный раздел.
- `mount.yml` использует `ansible.posix.mount` с `state: mounted` — идемпотентно монтирует по UUID.
## Все переменные
| Переменная | Значение по умолчанию | Описание |
|---|---|---|
| `gpu_model_storage_devices` | `[]` | Список блочных устройств — членов будущего RAID1 (например `/dev/sda`, `/dev/sdb`). Пустой список = роль ничего не делает. |
| `gpu_model_storage_partition_size_gb` | `1000` | Размер новой партиции (пятой) на каждом устройстве, в гигабайтах. Партиция начинается сразу после существующих ОС-партиций (`34GB`). |
| `gpu_model_storage_raid_device` | `/dev/md1` | Имя нового RAID1-массива. Не должно совпадать с уже существующими (`/dev/md125`, `/dev/md126`, `/dev/md127` на типовом узле). |
| `gpu_model_storage_mountpoint` | `/mnt/ollama-models` | Точка монтирования нового раздела. |
| `gpu_model_storage_fstype` | `xfs` | Файловая система (как в Longhorn-дисках — консистентность в репозитории). |
## Ограничения и подводные камни
- **Начало партиции захардкожено в `34GB`.** Это соответствует текущему размеру существующих
ОС-партиций (`boot` + `boot_efi` + `swap` + `root` ≈ 32.2 ГБ + небольшой запас) на узлах,
разворачиваемых по стандартному Rocky Linux 9 + RAID1 layout. Если разметка ОС на новом узле
отличается — проверьте фактическое свободное место (`parted /dev/sdX unit GB print free`) и
скорректируйте `part_start` в `roles/gpu_model_storage/tasks/partition.yml` перед запуском.
- **Имя RAID-устройства должно быть свободным.** Перед первым запуском на новом узле проверьте
`cat /proc/mdstat`, чтобы `gpu_model_storage_raid_device` не совпадал с уже занятым `/dev/mdN`.
- **Роль не выполняет `dracut -f`** — это осознанно: массив не является частью `root`/`boot`, не
участвует в загрузке системы, поэтому пересборка initramfs не требуется. Ядро подхватывает массив
через udev по суперблоку при каждой загрузке, а запись в `/etc/mdadm.conf` — для надёжности
(явное ARRAY-определение вместо авто-скана).
- **Диски используются только на ~1/4** (при `gpu_model_storage_partition_size_gb: 1000` из ~3967 ГБ
свободных) — сделано намеренно, с запасом на будущее (другие датасеты, второй RAID-раздел и т.д.).
Увеличить размер существующего раздела после создания массива штатными средствами Ansible-роли
нельзя — потребуется resize партиции, RAID и файловой системы вручную (`parted resizepart`,
`mdadm --grow`, `xfs_growfs`).
- **Реальная доступная ёмкость меньше заявленных 1000 ГБ** из-за разницы GB/GiB и служебных данных
файловой системы. В `k8s_ai/k8s/ollama/pvc-models.yaml` `hostPath`/`PVC` объявляют `900Gi`
безопасный запас под это расхождение (это не enforced-лимит, просто декларативное число).
## Диагностика
```bash
# Статус RAID-массива
cat /proc/mdstat
mdadm --detail /dev/md1
# Точка монтирования
lsblk -f
df -h /mnt/ollama-models
# Если массив не поднялся после перезагрузки — проверить запись в mdadm.conf
grep md1 /etc/mdadm.conf
```
Если `/mnt/ollama-models` не смонтирован после перезагрузки — массив собран (`mdadm --detail`
показывает `active`), но запись в `/etc/fstab` (созданная `ansible.posix.mount`) использует
`nofail`, поэтому загрузка ОС не блокируется — нужно смонтировать вручную (`mount -a`) и
разобраться, почему массив не успел подняться до попытки монтирования при следующем прогоне роли.