Auto-Scaling-Konzept
Status: LIVE seit 2026-05-02. Performance-basierte Host-Auswahl + Buffer-Cron (5min) + Active Rebalance (15min) implementiert. Implementation-Details in session-recap-2026-05-02.md Block E.
Heutige Implementation:
findAvailableSharedHost()nutzt Score = max(cpu, ram, disk) im 10min-Windowshared-host-auto-scaleCron alle 5min — Buffer-Provision wenn alle Hosts >75%shared-host-rebalanceCron alle 15min — Tenant-Migration von >85% auf <55% Hosts- Schwellenwerte: HOST_FULL=75, HOST_OVERLOAD=85, REBALANCE_DELTA=30, COOLDOWN=6h
- Audit-Spur: Migration-Badge auf jeder Tenant-Card im Tenant-Board
Ursprüngliche Vision unten — die fortgeschrittenen Features (Health-Score-Calculator mit Trend-Analyse, Score-Warnungen im UI, Predictive Scaling) sind noch nicht implementiert. Was heute live ist, ist der pragmatische "max-of-three"-Score.
Vollautomatische horizontale Skalierung der Shared-Hosting-Infrastruktur auf Basis echter Performance-Metriken. Ziel: bei einem viralen Moment (Medien-Erwaehnung → 500+ Anmeldungen in wenigen Stunden) skaliert die Plattform selbststaendig — keine manuelle Intervention.
Vision in einem Satz
Statt fester maxTenants=15 entscheidet ein Health-Vector aus echten Metriken, ob ein Host noch Kapazitaet hat. Die Decision-Engine balanciert Last automatisch und provisioniert neue Hosts wenn die Buffer-Reserve aufgebraucht ist.
Komponenten-Ueberblick
┌─────────────────────────────────────────────────────────────┐
│ 1. Metric-Collector (Cron 1 min) │
│ Quelle 1: Hetzner Cloud API (CPU, Disk-IO, Network) │
│ Quelle 2: prilog-agent (RAM, Disk-Fuell, /proc-Stats) │
│ → host_metrics (time-series, 30 Tage Retention) │
└──────────────────────────┬──────────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────────┐
│ 2. Health-Score-Calculator (Cron 5 min) │
│ → Vector aus 5-Min-Avg pro Host │
│ → host_health (aktueller Score + Trend + Begruendung) │
└──────────────────────────┬──────────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────────┐
│ 3. Decision-Engine (Cron 5 min) │
│ → Regeln auf alle Hosts anwenden │
│ → Aktionen erzeugen (oder dry-run loggen) │
│ → scaling_decisions table mit Audit-Trail │
└──────────────────────────┬──────────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────────┐
│ 4. Action-Executor │
│ → migration triggern (existing Tenant-Migration-Engine) │
│ → host provisioning (existing Auto-Provisioning) │
│ → Rate-Limits + Manual-Override-Bremsen │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 5. Performance-Tab im Tenant-Board (Kanban) │
│ unter Server-Titel: live Metric-Mini-Charts + Score │
└─────────────────────────────────────────────────────────────┘1. Metric-Collector
Quellen
Hetzner Cloud API:
GET /v1/servers/<id>/metrics?type=cpu,disk,network&start=...&end=...&step=60liefert: cpu (%), disk.0.bandwidth.read/write, disk.0.iops.read/write, network.0.bandwidth.in/out, network.0.pps.in/out.
prilog-agent (eigenes Reporting):
- RAM:
/proc/meminfo→ MemTotal, MemAvailable - Disk:
df -B1 /→ used, total, percent - Inodes:
df -i /→ percent - Pro Tenant: Container-CPU/RAM via
docker stats, MinIO-Bucket-Size viamc du, Postgres-DB-Size viapg_database_size
Schema
CREATE TABLE host_metrics (
id BIGSERIAL PRIMARY KEY,
host_id INTEGER NOT NULL REFERENCES shared_hosts(id),
ts TIMESTAMPTZ NOT NULL,
cpu_pct NUMERIC(5,2), -- 0-100
ram_pct NUMERIC(5,2),
disk_pct NUMERIC(5,2),
disk_iops INTEGER,
network_bps_in BIGINT,
network_bps_out BIGINT,
source VARCHAR(20), -- 'hetzner' | 'agent' | 'merged'
raw JSONB -- volle API-Antwort fuer Debug
);
CREATE INDEX idx_host_metrics_host_ts ON host_metrics(host_id, ts DESC);
-- Retention: 30 TageBeim Collector-Run werden Hetzner- und Agent-Daten zum gleichen Zeitstempel zusammengefuehrt → ein Datensatz pro Host pro Minute.
2. Health-Score-Calculator (der Vector)
Pro Host wird alle 5 Min aus dem 5-Min-Avg ein Score berechnet:
const score = Math.max(
cpu_pct / 70, // 70 % = gelb-grenze
ram_pct / 80,
disk_pct / 70,
disk_iops / 5000, // CCX13: ~5k iops gesund
tenant_count / 30, // soft-cap: doppelt so viele wie aktueller hard-cap
);| Score | Status | Bedeutung |
|---|---|---|
< 0.5 | 🟢 healthy | kann Tenants annehmen, ist Buffer-Kandidat |
0.5 - 0.7 | 🟢 normal | normaler Betrieb |
0.7 - 0.85 | 🟡 warning | keine neuen Tenants mehr automatisch |
0.85 - 0.95 | 🟠 high | Migration eines Tenants pruefen |
> 0.95 | 🔴 critical | Migration sofort ausloesen |
Plus Trend (letzte 30 Min): steigend / stabil / sinkend → fliesst in Decision ein.
Schema
CREATE TABLE host_health (
host_id INTEGER PRIMARY KEY REFERENCES shared_hosts(id),
score NUMERIC(4,2),
status VARCHAR(15), -- healthy | normal | warning | high | critical
trend VARCHAR(10), -- rising | stable | falling
primary_factor VARCHAR(20), -- 'cpu' | 'ram' | 'disk' | 'iops' | 'tenant_count'
cpu_pct NUMERIC(5,2),
ram_pct NUMERIC(5,2),
disk_pct NUMERIC(5,2),
iops INTEGER,
tenant_count INTEGER,
computed_at TIMESTAMPTZ
);Eine Zeile pro Host, wird alle 5 Min upserted. Zusaetzlich Audit-Log:
CREATE TABLE host_health_log (
id, host_id, score, status, computed_at -- jeder run wird angehaengt
);
-- Retention: 90 Tage, dann aggregiert3. Decision-Engine
Cron alle 5 Min, Read-Only auf health, schreibt in scaling_decisions:
CREATE TABLE scaling_decisions (
id VARCHAR(50) PRIMARY KEY,
decision_type VARCHAR(30), -- 'migrate_tenant' | 'provision_host' | 'no_action'
trigger_reason TEXT, -- "shared-1 score=0.92 (cpu)"
source_host_id INTEGER,
target_host_id INTEGER,
tenant_id VARCHAR(64),
status VARCHAR(20), -- 'pending' | 'executed' | 'rejected_rate_limit' | 'manual_override' | 'failed'
decided_at TIMESTAMPTZ,
executed_at TIMESTAMPTZ,
metadata JSONB
);Regelwerk
fuer jeden host H:
if H.status == 'critical':
── Migration-Entscheidung ──
candidate = pick_migration_candidate(H)
// bevorzugt: kleinster Tenant (DB-size + bucket-size + media-size),
// weil schnellste Migration und geringste Downtime
target = find_lowest_load_host(exclude=H, max_score=0.5)
if target:
decisions += { type: 'migrate_tenant', source: H, target, tenant: candidate }
else:
// kein Target → Buffer aufstocken
decisions += { type: 'provision_host', reason: 'no_target_for_critical' }
elif H.status == 'high' AND H.trend == 'rising':
// praeventiv handeln: Migration plus Buffer-Vorbereitung
target = find_lowest_load_host(exclude=H, max_score=0.5)
if target: decisions += { type: 'migrate_tenant', ... }
// Buffer-Strategie:
healthy_count = count(hosts where status == 'healthy')
if healthy_count == 0:
decisions += { type: 'provision_host', reason: 'buffer_empty' }Sicherheits-Bremsen
| Limit | Default | Konfigurierbar via |
|---|---|---|
| max migrations / Stunde global | 3 | auto_scale.max_migrations_per_hour |
| max neue Hosts / Tag | 5 | auto_scale.max_provisions_per_day |
| min Zeit zwischen Decisions pro Host | 30 Min | auto_scale.cool_down_min |
| Pro-Host-Override | Per-Host-Setting auto_scale.disabled blockt Decisions | Admin-UI |
| Dry-Run-Mode | Global Setting auto_scale.dry_run: Decisions loggen, nicht ausfuehren | Admin-UI |
Migration-Candidate-Auswahl
function pickMigrationCandidate(host) {
return tenants.where(host_id == host.id)
.filter(t => !t.opt_out_auto_balance) // Tenant kann opt-outen
.orderBy(t => t.bundle_size_estimate) // kleinster zuerst (kuerzeste Downtime)
.orderBy(t => t.activity_last_hour, 'asc') // inaktiv bevorzugt (kein User merkt's)
.first();
}Fuer den ersten Wurf reicht "kleinster, am wenigsten aktiv". Spaeter koennen ML-basierte Heuristiken kommen.
4. Action-Executor
Verbindet die Decision-Engine mit den existing Engines:
migrate_tenant→ ruftstartMigration()aus tenant-migration.service.ts (existing!)provision_host→ ruftcreateSharedHost()aus shared-hosting.service.ts (existing!)
Schreibt nach Ausfuehrung Status executed oder failed in scaling_decisions.
Idempotenz
Jede Decision hat eine ID, jede Migration / jeder Provision-Vorgang prueft, ob diese ID schon ausgefuehrt wurde — verhindert Doppelausfuehrung bei Cron-Restarts.
5. Performance-Tab im Tenant-Board (Kanban)
UI-Vorschlag
Aktuell pro Host-Spalte oben:
┌──────────────────────┐
│ shared-1 │
│ 49.12.108.38 │
│ active · 5/15 │
└──────────────────────┘Neu mit Performance-Tab unter dem Titel:
┌──────────────────────────────────┐
│ shared-1 │
│ 49.12.108.38 │
│ [active] 🟡 score 0.72 │
│ │
│ CPU ▁▁▂▃▅▇▅▃ 62 % │
│ RAM ▂▃▃▄▅▅▅▅ 71 % │
│ Disk ▃▃▃▄▄▄▅▅ 58 % (87 GB) │
│ │
│ 5/15 Tenants · primary: ram │
└──────────────────────────────────┘Sparkline-Charts (letzte 60 Min) ueber /admin/shared-hosts/:id/metrics. Kompakt, keine separaten Modals — Live-Information beim Hingucken.
Klick auf den Host-Titel oeffnet Detail-Modal mit:
- Vollstaendige Metric-Charts (24h)
- Liste aller Tenants mit Resource-Anteil (CPU%, RAM-MB, Disk-GB, MinIO-GB)
- Decision-History (letzte Aktionen die diesen Host betrafen)
- Manual-Override-Toggle (
auto_scale.disabledflag fuer diesen Host)
Implementierungs-Phasen
Phase 1 — Metric-Collector + Performance-Tab (~2-3 Tage)
- Schema:
host_metrics - Cron: Hetzner-API + Agent-Reporting → host_metrics
- Backend-Endpoint: GET
/admin/shared-hosts/:id/metrics?range=1h|24h|7d - Tenant-Board UI: Sparkline unter Host-Titel + Detail-Modal
- Output: Du siehst live, was deine Hosts machen — schon ohne Auto-Scaling.
Phase 2 — Health-Score + Decision-Engine im Dry-Run (~2 Tage)
- Schema:
host_health+host_health_log+scaling_decisions - Cron: Score-Calculator + Decision-Engine
- Wichtig: dry_run=true — Decisions werden nur geloggt, nicht ausgefuehrt
- Admin-UI: "Empfohlene Aktionen" Sektion zeigt was die Engine tun WUERDE
- Output: Vertrauen aufbauen — wir sehen ob die Decisions sinnvoll sind, ohne Risiko.
Phase 3 — Action-Executor live schalten (~1 Tag)
- dry_run-Flag default false
- Rate-Limits aktivieren
- Manual-Override implementieren
- Slack/Email-Notification bei jeder ausgefuehrten Decision
- Output: Volles Auto-Scaling live.
Phase 4 — Stress-Test-Suite (~2-3 Tage)
- Synthetic Load Test: stress-ng auf einem Test-Host, beobachten ob Migration ausgeloest wird
- Replay-Test: gespeicherte Metric-Sequenzen aus Phase 2 abspielen, Decisions vorhersagen, gegen Erwartung pruefen
- Mass-Signup-Test: Skript meldet 50 Tenants in 10 Minuten an, beobachten ob Auto-Provisioning korrekt skaliert
- Chaos-Test: random Hosts artificial mit
iotop/stress-ngueberlasten
Total: ~7-9 Tage. Jede Phase liefert nutzbares Ergebnis — Phase 1 alleine ist schon wertvoll als Observability-Tool.
Test-Strategie im Detail
Synthetic Load Test (Phase 4.1)
# Auf shared-2 (oder dediziertem Test-Host):
ssh root@<shared-2> 'apt install -y stress-ng && stress-ng --cpu 4 --vm 2 --vm-bytes 6G --timeout 30m'
# Beobachten: nach ~5-10 Min sollte Decision-Engine 'migrate_tenant' triggern (falls Tenants drauf sind)
# oder 'provision_host' (Buffer-Aufstockung) triggernReplay-Test (Phase 4.2)
-- Phase 2 sammelt Metric-Sequenzen. In Phase 4 spielen wir sie ab:
INSERT INTO test_metric_sequences (name, sequence) VALUES
('viral_spike', '<json mit 60 Datenpunkten, CPU steigt von 20% auf 95% in 10 Min>'),
('slow_drift', '<...stetig steigend ueber 3 Tage>'),
('false_alarm', '<kurzer Spike der wieder zurueckgeht>');
-- Decision-Engine im Test-Mode arbeitet auf diesen Sequenzen,
-- statt auf live host_metrics. Output gegen erwartete Decisions assert.Mass-Signup-Test (Phase 4.3)
Bash-Script (analog migration-e2e.sh):
for i in $(seq 1 50); do
curl -X POST $API/api/public/quick-signup \
-d "{\"email\":\"test-$i@example.com\",\"workspaceName\":\"loadtest-$i\"}" &
[ $((i % 10)) = 0 ] && sleep 10
done
wait
# Beobachten: Auto-Scaling sollte
# - vorhandene Hosts gleichmaessig fuellen
# - rechtzeitig neuen Host provisionieren wenn Score > 0.7
# - alle 50 Tenants kommen oben — keiner faellt durchs RasterChaos-Test (Phase 4.4)
Wahllose iotop/stress-ng-Bursts auf zufaelligen Hosts → Decision-Engine soll robust bleiben (keine Domino-Migrations, Rate-Limits halten).
Risiken + Mitigations
| Risiko | Mitigation |
|---|---|
| Migration-Storm: Decision triggert eine Migration die Last auf Target erzeugt → Target wird selbst critical → Migration-Domino | Rate-Limit (3/h global), Cool-Down (30 Min pro Host nach Migration), Target-Auswahl sucht max_score=0.5 (nicht 0.7) |
| False-Positive: kurzer Last-Spike loest Migration aus, Tenant migriert wegen 30s-Spike | 5-Min-Avg statt Spot-Wert, plus "Trend == rising" als zusaetzliche Bedingung |
| Falscher Tenant migriert: aktiver Tenant wird gewaehlt, User merkt Downtime | Activity-Score in Candidate-Pick einfliessen lassen (wer 0 Activity hat, wird zuerst gewaehlt) |
| Cost-Explosion: Engine provisioniert zu viele Hosts | max 5/Tag default, plus Email-Alarm bei jedem neuen Host, Admin kann veto-en bevor Hetzner bestellt wird (opt-in approval mode) |
| Engine-Bug zerlegt Tenant: in dry_run-Mode wuerde nichts passieren | Phase 2 NUR dry_run, mind. 2 Wochen beobachten bevor Phase 3 |
| Hetzner-API-Outage: Metric-Quelle weg, Engine fliegt blind | Agent-Reporting ist redundante Quelle, plus Engine "freezes" wenn > 30 Min keine Metrics |
| Tenant-Opt-out: User will nicht migriert werden | Per-Tenant-Setting auto_balance.disabled, fuer Premium-Pakete denkbar |
SLA-Ziele nach Phase 3
| Metrik | Ziel |
|---|---|
| Skalierung-Reaktionszeit | < 15 Min vom Score>0.95 bis Decision-Execute |
| Buffer-Verfuegbarkeit | mindestens 1 healthy host (score<0.5) jederzeit verfuegbar |
| Mass-Signup-Kapazitaet | 100 neue Tenants in 1 Stunde ohne Service-Degradation |
| False-Positive-Rate | < 5 % der Migrations sind nicht-noetig |
| Engine-Uptime | Decision-Engine darf max 30 Min ausfallen ohne Eskalation |
Was bewusst NICHT enthalten ist
| Was | Warum nicht | Wann nachruesten |
|---|---|---|
| Vertikale Skalierung (CCX13 → CCX23 in-place) | Hetzner kann das nur mit Reboot — Migration ist sauberer | wenn Hetzner Live-Resize bringt |
| Multi-Region-Auto-Scaling | komplex, fuer Schul-SaaS in DE niemand braucht es | wenn EU-DACH Markt verlassen wird |
| Predictive Scaling (ML auf historischen Daten) | Ueberkomplex fuer den Anfang, Reactive funktioniert fuer 99% | wenn 1000+ Hosts erreicht |
| Per-Tenant-Resource-Limits (cgroups pro Container) | Synapse-Container haben schon mem_limit: 512m | wenn ein Tenant alle anderen kannibalisiert |
| Sticky-Tenants (bestimmte Tenants bleiben auf bestimmtem Host) | unueblich, gegen die Idee | wenn ein Premium-Plan das verlangt |
Anschlussfaehig an
- tenant-migration-implementation.md — Action-Executor nutzt diese Engine
- disaster-recovery-konzept.md — Backup-Daten als zusaetzliche Migration-Quelle (z.B. wenn Source-Host weg ist)
- admin-tenant-board.md — Performance-Tab integriert sich hier