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

10 KiB
Raw Permalink Blame History

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 для конкретного хоста:

# 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).

Запуск

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-лимит, просто декларативное число).

Диагностика

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