Кластер и ноды
Как разложен Hetzner-кластер k3s, почему control plane крутится на своей(их) ноде(ах) и как автоскейлинг растит и ужимает и поды, и ноды — автоматически, в обе стороны.
У того, что работает сегодня, другая форма
Эта страница — замысел, и кластеры, реально обслуживающие трафик, построены не по нему. В Agentfy/gitops: terraform/mgmt.tf и terraform/db.tf поднимают одну management-ноду и две ноды PostgreSQL в плоской сети Hetzner, а ansible/k3s.yml ставит на management-ноду односерверный k3s — это control-кластер, где живут Argo CD, Rancher и Vault. Кластеры dev и prod провижинятся через Rancher (terraform/loadbalancer.tf фиксирует, что метку lb-target проставляет Rancher), поэтому каждый рабочий Application смотрит на https://10.0.1.3:6443 или https://10.0.1.9:6443.
То есть сегодня: нет трёх затейнченных пулов, нет cluster-autoscaler, нет CloudNativePG и нигде нет нодпула workers. Postgres стоит на своих VM (ansible/roles/postgres); Redis — обычный Deployment (helm/agentfy-redis). Какой репозиторий что держит: Два репозитория.
Три нодпула (не два)
В кластере три пула, у каждого своя роль. Control plane — свой собственный пул: он не делит ноды с прикладными нагрузками.
| Пул | Что крутит | Taint / селектор | Шедулится приложениями? | Скейл |
|---|---|---|---|---|
control-plane | k3s server: API server · etcd · scheduler · controller-manager | node-role.kubernetes.io/control-plane:NoSchedule | Нет — tainted, изолирован | Фикс: 1 (dev) / 3 (prod HA) |
core (всегда онлайн) | api, app, admin, LightRAG, Postgres (CNPG), Redis, Ingress, ArgoCD, мониторинг | дефолт (шедулится) | Да | HPA на приложение + cluster-autoscaler на пуле |
workers (эфемерный) | worker-Job'ы (bash/fs/браузер in-pod/heavy) | node-role: workers + workload=worker:NoSchedule | Только Job'ы, которые это толерируют | Scale-to-zero; cluster-autoscaler 0 → N |
┌──────────────────────────────────────────┐
│ control-plane pool (tainted, isolated) │
│ api-server · etcd · scheduler · ctrl-mgr │ 1 (dev) / 3 (prod, HA quorum)
└──────────────────────────────────────────┘
│ manages
┌──────────────────────────────┴──────────────────────────────┐
▼ ▼
┌────────────────────────────────────┐ ┌────────────────────────────────────┐
│ core pool (always-on) │ │ workers pool (ephemeral, tainted) │
│ api / app / admin (HPA Deployments)│ │ worker Jobs — one task, then exit │
│ LightRAG (browser runs in worker) │ │ scale-to-zero between tasks │
│ Postgres (CNPG) · Redis · Ingress │ │ cluster-autoscaler 0 → N │
│ ArgoCD · Loki/Grafana │ │ node-role: workers · NoSchedule tol │
│ cluster-autoscaler grows the pool │ └────────────────────────────────────┘
└────────────────────────────────────┘Почему control plane нужна своя(и) нода(ы)
k3s server — это мозг кластера (API server + etcd). Он должен оставаться отзывчивым даже когда нагрузки скачут — а агентские нагрузки скачут жёстко (взбесившаяся задача, тяжёлый ingest LightRAG, браузерный пул под нагрузкой). Изоляция его:
- etcd чувствителен к диску и латентности. Со-шедулинг шумного пода, который выедает CPU или I/O, заставляет etcd пропускать heartbeat'ы → выборы лидера, медленные/падающие API-вызовы → весь кластер слепнет.
- Доступность API-server должна быть независима от давления нагрузок. Если нода, забитая агентскими Job'ами, ещё и крутит API server, перегруженная нода может снести контроль над всем кластером.
- HA через кворум. Прод гоняет 3 control-plane-ноды (нечётное число → кворум etcd переживает потерю одной ноды). Dev гоняет 1 (без HA, дёшево). Никогда не гоняй 2 (профита от кворума нет, только стоимость).
- Taint, не доверие. Пул tainted'ится
control-plane:NoSchedule, чтобы туда ничего не приземлилось случайно — изоляция обеспечивается шедулером, а не соглашением.
Это и есть ответ на «у control plane должна быть своя нода»: да — выделенный, tainted-пул
control-plane, 1 нода в dev и 3 в prod. Старое описание «двух пулов» вкладывало его вcore; теперь он вынесен отдельно.
Автоскейлинг — три независимых слоя
Скейлинг происходит на трёх уровнях, которые компонуются. Два про поды, один про ноды — и каждый скейлит вверх и вниз автоматически.
| Слой | Что ресайзит | Направление | Применяется к |
|---|---|---|---|
| HPA (Horizontal Pod Autoscaler) | число реплик Deployment | вверх/вниз по CPU·mem·кастомной метрике | api, app, admin, LightRAG |
| VPA (Vertical Pod Autoscaler) | requests/limits на под | right-size вверх/вниз | stateful + right-sizing (in-place на k8s ≥1.33, без рестарта) |
| Cluster Autoscaler | число нод пула | добавляет, когда поды Pending, убирает, когда ноды простаивают | пулы core и workers |
api (и app/admin) — горизонталь + ноды
api — это stateless Deployment (состояние агента грузится на запрос, нет пода на агента), поэтому он скейлится чисто:
- HPA:
minReplicas2 →maxReplicasN, таргет ~60% CPU плюс кастомная метрика (in-flight запросы / глубина очереди оркестратора). Трафик вверх → больше реплик; трафик вниз → меньше (назад к 2). - Cluster-autoscaler на
core: когда HPA хочет больше реплик, чем влезает на текущие ноды, добавляется новаяcore-нода; когда реплики ужимаются и нода пустеет, она убирается. - VPA (recommend/auto): держит
requestsчестными, чтобы процентная математика HPA отражала реальность и шедулер не пере- и не недо-паковал ноды.
workers — scale-to-zero нод, по спросу
worker — это Job, а не Deployment (одна задача, потом выход), поэтому он не HPA-скейлится — спрос это число задач в очереди:
runtime/workerсоздаёт один Job на задачу (императивно, не GitOps).- Cluster-autoscaler скейлит пул
workers0 → N отPending-подов Job'ов и обратно к 0, когда работы нет (ttlSecondsAfterFinished+ idle-reaper подчищают завершённые сессии). - Right-size'нутые requests Job'ов (≈
100mCPU — см. инцидент ниже) позволяют автоскейлеру плотно паковать задачи и растить ноду только когда действительно нужно. - Позже (P2, не MVP): KEDA
ScaledJobможет скейлить воркеры напрямую от глубины очереди BullMQ для более резкой реакции. Отложено по зафиксированному решению «без KEDA для MVP» — HPA + cluster-autoscaler покрывают MVP.
Почему это важно — инцидент июня 2026
Реальный сбой, который этот дизайн предотвращает: агентские Job'ы запрашивали 500m CPU, но использовали ~10m. Четырнадцать из них зарезервировали workers-ноду на 100% (7700m), пока она сидела на 6% реального использования — так что новые Job'ы зависали в Pending (FailedScheduling: Insufficient cpu) и агент не стартовал.
Два слоя этой стратегии чинят это напрямую:
- Right-size'нутые requests (
500m→100m) — математика резервирования шедулера теперь отражает реальность. VPA автоматизирует это дальше; на k3s ≥1.33 in-place-ресайз применяет это без рестарта пода (ровно так инцидент и был починен на лету). - Cluster-autoscaler — по-настоящему полный пул
workersтеперь растит ноду вместо бесконечного зависания Job'ов вPending.
Сайзинг (грубо MVP → prod)
| Пул | Dev | Prod | Заметки |
|---|---|---|---|
control-plane | 1× CPX21 | 3× CPX31 (быстрый NVMe) | etcd хочет CPU + низколатентный диск; HA-кворум на 3 |
core | 1–2× CPX41 | 3+× CCX/CPX (автоскейл) | Postgres/Redis/LightRAG живут здесь; сайзить под стабильную нагрузку |
workers | 0–1× CPX31 | 0 → N× CPX41/CCX (автоскейл) | scale-to-zero; ноды побольше под heavy/browser-профили |
Per-tenant ResourceQuota + LimitRange капают namespace каждой команды, а дефолтный LimitRange гарантирует, что ни один под не крутится без requests (под без границ ломает математику автоскейлера и пересоздаёт инцидент выше).
Провижининг
- IaC (kube-hetzner Terraform /
hcloud) определяет три пула; членство задаётся лейблами + taint'ами нод (node-role: workers,control-plane:NoSchedule). - Cluster-autoscaler подключён к провайдеру Hetzner cloud по пулам (min/max число нод);
workersmin = 0. - GitOps (ArgoCD) дальше деплоит нагрузки поверх — см. GitOps. Сами автоскейлеры (CA, VPA, metrics-server) — часть чарта
platform.
IaC из первого пункта — это terraform/ этого репозитория, и он не поднимал кластеры, обслуживающие трафик: они пришли из Agentfy/gitops, в форме, описанной во врезке в начале страницы. Последний пункт — единственный, который действительно случился, только в другом репозитории: Argo CD деплоит нагрузки.
Локально это настоящий кластер
Разработка идёт не против заглушки. Кластер k3d — настоящий k3s внутри Docker — поднимается командой ./.superset/k3d.sh up, и от продакшена он отличается kubeconfig'ом и пространством имён, а не клиентом, манифестами или RBAC.
Пригодным для работы нескольких человек одновременно его делают три свойства:
- Один кластер, по пространству имён на рабочее место. Шесть k3d-кластеров на одном ноутбуке — это шесть виртуальных машин; один кластер с пространством имён на каждого стоит доли этого и всё так же изолирует.
- Его kubeconfig — отдельный файл. Подъём никогда не трогает
~/.kube/config: инструмент, который переставляет ваш контекст по умолчанию, — это инструмент, который однажды направит команду в продакшен. - Под достукивается до api на хосте. В DNS кластера вписаны
host.k3d.internalиhost.docker.internal, поэтому адрес, по которому воркер звонит обратно, — обычное имя хоста, а не особый случай. Без этого локально не работает вообще ничего из остального.
Все шаги идемпотентны: запустите up дважды, и второй запуск всё найдёт и ничего не изменит. Сам кластер общий, поэтому остановка или удаление останавливает его для всех на машине, — clean убирает только ваше пространство имён.
См. также
- Ресурсы и реализация — полный инвентарь деплоев и датасторов.
- Worker на Kubernetes — жизненный цикл Job, который гоняет пул
workers. - Облачная стратегия — когда (если вообще) что-то из этого переезжает в AWS.