Skip to content

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-Window
  • shared-host-auto-scale Cron alle 5min — Buffer-Provision wenn alle Hosts >75%
  • shared-host-rebalance Cron 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=60

liefert: 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 via mc du, Postgres-DB-Size via pg_database_size

Schema

sql
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 Tage

Beim 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:

ts
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
);
ScoreStatusBedeutung
< 0.5🟢 healthykann Tenants annehmen, ist Buffer-Kandidat
0.5 - 0.7🟢 normalnormaler Betrieb
0.7 - 0.85🟡 warningkeine neuen Tenants mehr automatisch
0.85 - 0.95🟠 highMigration eines Tenants pruefen
> 0.95🔴 criticalMigration sofort ausloesen

Plus Trend (letzte 30 Min): steigend / stabil / sinkend → fliesst in Decision ein.

Schema

sql
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:

sql
CREATE TABLE host_health_log (
  id, host_id, score, status, computed_at  -- jeder run wird angehaengt
);
-- Retention: 90 Tage, dann aggregiert

3. Decision-Engine

Cron alle 5 Min, Read-Only auf health, schreibt in scaling_decisions:

sql
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

LimitDefaultKonfigurierbar via
max migrations / Stunde global3auto_scale.max_migrations_per_hour
max neue Hosts / Tag5auto_scale.max_provisions_per_day
min Zeit zwischen Decisions pro Host30 Minauto_scale.cool_down_min
Pro-Host-OverridePer-Host-Setting auto_scale.disabled blockt DecisionsAdmin-UI
Dry-Run-ModeGlobal Setting auto_scale.dry_run: Decisions loggen, nicht ausfuehrenAdmin-UI

Migration-Candidate-Auswahl

ts
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 → ruft startMigration() aus tenant-migration.service.ts (existing!)
  • provision_host → ruft createSharedHost() 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.disabled flag 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-ng ueberlasten

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)

bash
# 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) triggern

Replay-Test (Phase 4.2)

sql
-- 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):

bash
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 Raster

Chaos-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

RisikoMitigation
Migration-Storm: Decision triggert eine Migration die Last auf Target erzeugt → Target wird selbst critical → Migration-DominoRate-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-Spike5-Min-Avg statt Spot-Wert, plus "Trend == rising" als zusaetzliche Bedingung
Falscher Tenant migriert: aktiver Tenant wird gewaehlt, User merkt DowntimeActivity-Score in Candidate-Pick einfliessen lassen (wer 0 Activity hat, wird zuerst gewaehlt)
Cost-Explosion: Engine provisioniert zu viele Hostsmax 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 passierenPhase 2 NUR dry_run, mind. 2 Wochen beobachten bevor Phase 3
Hetzner-API-Outage: Metric-Quelle weg, Engine fliegt blindAgent-Reporting ist redundante Quelle, plus Engine "freezes" wenn > 30 Min keine Metrics
Tenant-Opt-out: User will nicht migriert werdenPer-Tenant-Setting auto_balance.disabled, fuer Premium-Pakete denkbar

SLA-Ziele nach Phase 3

MetrikZiel
Skalierung-Reaktionszeit< 15 Min vom Score>0.95 bis Decision-Execute
Buffer-Verfuegbarkeitmindestens 1 healthy host (score<0.5) jederzeit verfuegbar
Mass-Signup-Kapazitaet100 neue Tenants in 1 Stunde ohne Service-Degradation
False-Positive-Rate< 5 % der Migrations sind nicht-noetig
Engine-UptimeDecision-Engine darf max 30 Min ausfallen ohne Eskalation

Was bewusst NICHT enthalten ist

WasWarum nichtWann nachruesten
Vertikale Skalierung (CCX13 → CCX23 in-place)Hetzner kann das nur mit Reboot — Migration ist saubererwenn Hetzner Live-Resize bringt
Multi-Region-Auto-Scalingkomplex, fuer Schul-SaaS in DE niemand braucht eswenn 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: 512mwenn ein Tenant alle anderen kannibalisiert
Sticky-Tenants (bestimmte Tenants bleiben auf bestimmtem Host)unueblich, gegen die Ideewenn ein Premium-Plan das verlangt

Anschlussfaehig an