Skip to content

Кластер і ноди

Як влаштований 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 / selectorШедулиться застосунками?Скейл
control-planek3s server: API server · etcd · scheduler · controller-managernode-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/browser in-pod/важкі)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). Він має лишатися відзивним навіть коли навантаження сплескує — а агентні навантаження сплескують жорстко (втікла задача, важка індексація LightRAG, in-pod браузер воркера під навантаженням). Ізоляція його:

  • etcd чутливий до диска й latency. Co-scheduling шумного поду, що зголоднює CPU чи I/O, змушує etcd проґавлювати heartbeat'и → вибори лідера, повільні/невдалі API-виклики → весь кластер сліпне.
  • Доступність API-server має бути незалежною від тиску навантаження. Якщо нода, набита агентними Job-ами, ще й крутить API server, перевантажена нода може покласти контроль над усім кластером.
  • HA через кворум. Prod крутить 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)per-pod requests/limitsright-size вгору/внизstateful + right-sizing (in-place на k8s ≥1.33, без рестарту)
Cluster Autoscalerкількість нод пулудодає, коли поди Pending, прибирає, коли ноди простоюютьпули core і workers

api (та app/admin) — горизонтально + ноди

api — це stateless-Deployment (стан агента вантажиться на запит, без per-agent-поду), тож скейлиться чисто:

  • HPA: minReplicas 2 → maxReplicas N, ціль ~60% CPU плюс кастомна метрика (in-flight запити / глибина черги оркестратора). Трафік вгору → більше реплік; трафік вниз → менше (назад до 2).
  • Cluster-autoscaler на core: коли HPA хоче більше реплік, ніж влазить на поточні ноди, додається нова core-нода; коли репліки зменшуються і нода порожніє, її прибирають.
  • VPA (recommend/auto): тримає requests чесними, щоб відсоткова математика HPA відображала реальність, а шедулер не пере- чи недопакував ноди.

workers — node scale-to-zero, керований попитом

worker — це Job, не Deployment (одна задача, потім вихід), тож він не HPA-скейлиться — попит це кількість задач у черзі:

  • runtime/worker створює один Job на задачу (імперативно, не GitOps).
  • Cluster-autoscaler скейлить пул workers 0 → N від Pending-подів Job, і назад до 0, коли роботи немає (ttlSecondsAfterFinished + idle-reaper прибирають завершені сесії).
  • Right-sized requests Job (≈100m CPU — див. інцидент нижче) дають автоскейлеру щільно пакувати задачі й рости нодою лише коли справді треба.
  • Пізніше (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), і агент не стартував.

Два шари цієї стратегії виправляють це напряму:

  1. Right-sized requests (500m100m) — резерваційна математика шедулера тепер відображає реальність. VPA автоматизує це надалі; на k3s ≥1.33 in-place resize застосовує його без рестарту поду (рівно так інцидент було усунено наживо).
  2. Cluster-autoscalerсправді повний пул workers тепер росте нодою замість того, щоб зависати Job-ами в Pending нескінченно.

Сайзинг (приблизно MVP → prod)

ПулDevProdНотатки
control-plane1× CPX213× CPX31 (швидкий NVMe)etcd хоче CPU + диск з низькою latency; HA-кворум на 3
core1–2× CPX413+× CCX/CPX (автоскейл)Postgres/Redis/LightRAG живуть тут; сайз під стабільне навантаження
workers0–1× CPX310 → N× CPX41/CCX (автоскейл)scale-to-zero; більші ноди під важкі/браузерні профілі

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 кількість нод); workers min = 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 прибирає лише ваш простір імен.

Див. також