130 lines
10 KiB
Markdown
130 lines
10 KiB
Markdown
# 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`) и
|
||
разобраться, почему массив не успел подняться до попытки монтирования при следующем прогоне роли.
|