Start/Dokumentation/Multi-Instanz-Bereitstellung
⚖️ Infrastruktur

Multi-Instanz-Bereitstellung

Beliebig viele Tenant-Server-Replikas hinter einem Load Balancer betreiben — ohne Einschränkung "muss auf demselben Pod laufen". Jede Dienstkomponente ist zustandslos konzipiert — geteilter Zustand liegt in Postgres und Redis.

💡
Kurzfassung

TENANT_BASE_URL auf Ihren Load Balancer zeigen lassen. Alle Replikas teilen dasselbe Postgres und Redis. Keine Sticky Sessions für Voice erforderlich. Der Customization-Agent-Worker kann jede Replik erreichen.

Zustandslos by design

Der Tenant-Server hält keinen In-Memory-Zustand, der einen Round-Robin zu einer anderen Replik überstehen muss. JWT-Signierungsschlüssel liegen in Postgres (im livekit_server-Namespace) und in Ihrem Deployment-Zeit-passwords.yaml. Jede Replik liest aus derselben Quelle, sodass jeder Pod für jeden Benutzer einen LiveKit-JWT ausstellen kann.

Der Customization-Agent (Voice/Avatar-Worker) ruft /internal/customization-agent/*-Routen zurück. Diese Routen suchen den Organisations-Benutzer in Postgres, validieren den Tenant und fahren fort — kein Pro-Replik-In-Memory-Zustand wird herangezogen. TENANT_BASE_URL auf den Load Balancer zeigen und jede Replik kann antworten.

Koordination zwischen Replikas

Überall dort, wo Replikas unter Load Balancing in Konflikt geraten könnten, koordiniert YOffice über Redis. Jeder der folgenden Bereiche wird einem verteilten Primitiv zugeordnet, das alle Replikas synchron hält:

KoordinationsbereichWarum es bei mehreren Replikas relevant istWie YOffice es löst
Periodische Sweeps in server.dartOhne Koordination würde jede Replik jeden Sweep ausführen → N× die Arbeit und doppelte AuslöserLeaderOnlyTimer.periodic(taskName: …) — ein Redis-gewählter Leader führt den Sweep durchein Redis-gewählter Leader führt den Sweep durch
Anfrage-Rate-Limiting pro OrganisationEin In-Memory-Zähler würde jeder Replik erlauben, das Limit auf ihrem eigenen Anteil durchzusetzen, sodass N Replikas das N-fache Limit erlaubenDistributedRateLimiteratomares Redis INCRBY pro Tenant + Zeitfenster
IP-Rate-Limit für öffentliche FormulareAnfragen könnten das Limit umgehen, indem sie verschiedene Replikas treffenDistributedRateLimiter mit IP + Bucket als Schlüssel
Embedding-ParallelitätEin Pro-Replik-Semaphor würde sich mit N Replikas multiplizieren und Provider-Kontingente überschreitenDistributedSemaphore mit clusterweiter Kapazität
Direktivencache-InvalidierungEine nur lokale Invalidierung würde andere Replikas bis zu 5 Minuten veraltete Direktiven liefern lassenRedis-Generationszähler — die Invalidierung einer Replik erhöht ihn; alle Replikas sehen die Änderung
Echtzeit-Pub/Sub-FanoutpostMessage ohne erreicht nur die Abonnenten der veröffentlichenden Replikglobal: true reaches only the publishing replica's subscribersAlle 137 Aufrufstellen verwenden ClusterPubsub.broadcast (global fest verdrahtet)

Skalierungs-Primitive

Alle Primitive befinden sich in command_center_tenant_server/lib/src/scaling/horizontal_scaling.dart. Sie benötigen nur einen gemeinsamen Redis-Cluster — keinen separaten Koordinationsdienst.

  1. DistributedLock

    Redis SET NX PX Advisory Lock. Dient zur Koordinierung gegenseitiger Ausschlüsse über Replikas hinweg. Läuft nach einem konfigurierbaren TTL automatisch ab, damit ein abgestürzter Eigentümer den Cluster nicht dauerhaft blockiert. Langfristig haltende Sperren müssen renew() innerhalb des TTL-Fensters aufrufen.

  2. LeaderOnlyTimer

    Ein Drop-in-Ersatz für Timer.periodic, der ein Pro-Task Redis-Leader-Lease verwendet. Nur die gewählte Leader-Replik führt den Body aus; alle anderen Replikas bleiben inaktiv. Jede Task wählt ihren eigenen Leader unabhängig, was die Last natürlich über Pods verteilt.

  3. DistributedSemaphore

    Ein zählender Semaphor, der die clusterweite Parallelität mit Redis-Slot-Keys begrenzt. Ersetzt Pro-Isolate-Semaphore, die Limits nur lokal durchsetzen — wichtig für Massen-Knowledge-Folder-Embeddings und ähnliche Provider-kontingentsensitive Operationen.

  4. DistributedRateLimiter

    Atomares INCRBY in Redis mit Tenant + Zeitfenster als Schlüssel. Erzwingt Pro-Org-Anfrageobergrenzen über alle Replikas gleichzeitig. Pro-Org-Überschreibungen werden über den Admin-Rate-Limit-Einstellungsbildschirm geschrieben und innerhalb von ~30 Sekunden an Redis übertragen.

  5. ClusterPubsub

    Ein schlanker Wrapper um Serverpod session.messages, der global: true übergibt, damit Pub/Sub-Ereignisse Abonnenten auf jeder Replik erreichen, nicht nur der veröffentlichenden. Alle Echtzeit-Chat-, Präsenz- und Workflow-Stream-Ereignisse verwenden diesen Wrapper.

Deployment-Topologie

Ein typisches Produktions-Setup sieht so aus:

Clients (Flutter / browser)
        │  HTTPS / WSS
  Load balancer (nginx / Cloudflare / ALB)
     │                   │
tenant-server pod   tenant-server pod   … × N
     │                   │
     ├── Postgres (shared)
     ├── Redis (shared)
     ├── LiveKit Cloud
     └── MinIO / Supabase Storage

customization-agent worker(s)
  └── HTTP callbacks → TENANT_BASE_URL/internal/…
      (any pod can answer)

Deployment-Checkliste

  1. TENANT_BASE_URL auf den Load Balancer zeigen

    TENANT_BASE_URL auf den Hostnamen Ihres Load Balancers setzen (z. B. https://tenant.ihre-domain). Jeder Callback des Customization-Agent-Workers — /internal/customization-agent/* — enthält tenantId, organizationUserId und einen Secret-Header. Jede Replik kann antworten; kein In-Memory-Zustand der JWT-ausstellenden Replik wird benötigt.

  2. Gemeinsames Postgres und Redis konfigurieren

    Alle Replikas müssen von derselben Postgres-Instanz und demselben Redis-Cluster lesen. Die verteilten Primitive (Sperren, Leader-Wahlen, Semaphore, Rate-Limits, Pub/Sub) funktionieren nur korrekt, wenn alle Pods einen einzigen Redis-Keyspace teilen.

  3. Sticky Sessions für WebSocket aktivieren (optional, empfohlen)

    Sticky Sessions sind für Voice nicht erforderlich — LiveKit Cloud verbindet Clients nach der JWT-Ausstellung direkt mit seiner eigenen Media-Plane. Sticky Sessions sind jedoch HILFREICH für den Serverpod-WebSocket, damit Chat-Clients beim Neustart eines Pods nicht dauernd neu verbinden. ip_hash auf nginx oder eine Session-Affinität-Richtlinie auf einem verwalteten Load Balancer verwenden.

  4. CC_INSTANCE_ID pro Pod setzen (optional)

    Jeder Pod leitet eine stabile Identität aus CC_INSTANCE_ID → HOSTNAME → Plattform-Hostname → Zufallsfallback ab. Das explizite Setzen von CC_INSTANCE_ID im Deployment-Manifest gibt Ihnen konsistente Pod-Namen in Logs, Distributed-Lock-Owner-Tokens und der Debug-Route /internal/instance-info.

Voice und LiveKit Cloud

ℹ️
Keine Sticky Sessions für Voice erforderlich

LiveKit Cloud verbindet Clients nach der JWT-Ausstellung durch den Tenant-Server direkt mit seiner eigenen WebRTC-Media-Plane. Der Tenant-Server ist nach der Token-Ausstellung nicht mehr im Medienpfad, daher beeinflusst Session-Affinität die Sprachqualität nicht.

Der Customization-Agent-Worker registriert sich bei LiveKit Cloud mit dem Bezeichner agent_name=customization. Er sendet Callbacks an TENANT_BASE_URL — Ihren Load Balancer — und jede Replik kann sie verarbeiten. Der Worker selbst kann ebenfalls als mehrere Instanzen laufen; jede Instanz registriert sich unabhängig bei LiveKit.

Rate-Limits pro Organisation

Organisations-Admins können das Callback-Rate-Limit pro Organisation unter Einstellungen → Rate-Limits anpassen. Der neue Wert wird in Postgres geschrieben und innerhalb von ~30 Sekunden an Redis übertragen, sodass alle Replikas ihn ohne Neustart übernehmen. Der Standardwert beträgt 240 Anfragen pro Minute.Settings → Rate Limits

⚠️
Redis ist für den Multi-Instanz-Modus erforderlich

Die verteilten Primitive (Sperren, Leader-Wahlen, Rate-Limits, Pub/Sub) setzen Redis voraus. Ist Redis nicht verfügbar, fällt der Server auf prozessinterne Verarbeitung zurück, was für Einzelreplik-Bereitstellungen sicher ist, aber unter Load Balancing die oben beschriebenen Probleme zeigt.