Кластер і ноди
Як влаштований 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-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/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/limits | right-size вгору/вниз | stateful + right-sizing (in-place на k8s ≥1.33, без рестарту) |
| Cluster Autoscaler | кількість нод пулу | додає, коли поди Pending, прибирає, коли ноди простоюють | пули core і workers |
api (та app/admin) — горизонтально + ноди
api — це stateless-Deployment (стан агента вантажиться на запит, без per-agent-поду), тож скейлиться чисто:
- HPA:
minReplicas2 →maxReplicasN, ціль ~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 скейлить пул
workers0 → N відPending-подів Job, і назад до 0, коли роботи немає (ttlSecondsAfterFinished+ idle-reaper прибирають завершені сесії). - Right-sized 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-sized requests (
500m→100m) — резерваційна математика шедулера тепер відображає реальність. VPA автоматизує це надалі; на k3s ≥1.33 in-place resize застосовує його без рестарту поду (рівно так інцидент було усунено наживо). - Cluster-autoscaler — справді повний пул
workersтепер росте нодою замість того, щоб зависати Job-ами вPendingнескінченно.
Сайзинг (приблизно MVP → prod)
| Пул | Dev | Prod | Нотатки |
|---|---|---|---|
control-plane | 1× CPX21 | 3× CPX31 (швидкий NVMe) | etcd хоче CPU + диск з низькою latency; HA-кворум на 3 |
core | 1–2× CPX41 | 3+× CCX/CPX (автоскейл) | Postgres/Redis/LightRAG живуть тут; сайз під стабільне навантаження |
workers | 0–1× CPX31 | 0 → 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 кількість нод);
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.