Plan: 100 000 Nutzer auf einem VPS, CDN-bereit

Dokumentversion 29.05.2026 · Ziel: Der heutige einzelne VPS soll bis zu 100 000 MAU tragen, ohne umzufallen, und die App soll zu gegebener Zeit nahtlos auf ein CDN umziehen.

Zielzahlen

MetricWertSource
MAU100,000das Ziel
DAU30,00030 % der MAU, normal für eine Zweck-App
Gleichzeitige WS10 000 (Spitze 15 000)~10 % der MAU tagsüber, 15 % in der Spitze
Aktionen pro Tag90,0003 Öffnungen oder Anrufe je Tagesnutzer
HTTP-API-Anfragen pro Sekunde (Durchschnitt)3-5überwiegend key_create, host_sync und crash_report
HTTP-API-Anfragen pro Sekunde (Spitze)30-50nach einem Massen-Release, das Nummern aktualisiert

Die heutige Architektur

Phase A — der Android-Client JETZT, v6.66

Goal: die Zahl überflüssiger Anfragen der Clients senken, damit der Server weniger zu tun hat.

A1. Exponentieller HTTP-Wiederholungs-Backoff für ausstehende Host-Schlüssel

File: HostService.kt:1424-1432. Heute syncPendingHostKeys() wird alle 60 s aufgerufen und trifft Api.keyCreate für jeden ausstehenden Schlüssel ohne jeden Backoff.

Speichern in Prefs per-localId: attempt_at_id und attempt_count_id. Backoff: 30 s → 60 s → 2 min → 5 min → 15 min → 30 min (Deckel). Bei Erfolg zurückgesetzt.

A2. HTTP-Wiederholungs-Backoff für ausstehende Gast-Bundles

File: Vault.kt:213. Dieselbe Logik.

A3. Vergleich vor dem Hochzählen von overridesVersion

File: GuestConn.kt (5+ Stellen). Das neue parseNumbers mit dem vorherigen vergleichen, per Hash oder direkt. Sind sie gleich, nicht hochzählen: AppState.overridesVersion. Das beseitigt die Recompose-Wellen — 80 SharedPreferences-Lesevorgänge × 20 Karten — bei jeder WebSocket-Initialisierung.

Phase B — Serverschutz und CDN-Bereitschaft NOW

Goal: den Server auch bei fehlerhaftem Client vor einem Sturm schützen und die APK-Adresse von der Domain lösen.

B1. Eine Ratenbegrenzung für host_hello und guest_hello

In server.php case 'host_hello' / 'guest_hello': Hat derselbe device_id / user_key vor weniger als N Sekunden ein hello geschickt, die Verbindung schließen, ohne die Datenbank zu berühren. N = 5 s.

Schutz für den Fall, dass ein Client mit kaputter Logik hundert hellos pro Sekunde schickt.

B2. Obergrenze für pending_host_msgs je host_id

In enqueueHostMsg: Hat dieser host_id bereits mehr als 200 Zeilen, die älteste löschen mit DELETE FROM pending_host_msgs WHERE host_id=? ORDER BY id ASC LIMIT 1. Schutz vor endlosem Wachstum der Warteschlange bei böswilliger Aktivität.

B3. CDN-Bereitschaft: die APK-Adresse in die Konfiguration

In _config.php:

$GLOBALS['apk_url'] = 'https://entrixy.com/entrixy.apk';
$GLOBALS['download_page_url'] = 'https://entrixy.com/download';

In api/version_check.php: 'download_url' => $GLOBALS['apk_url'].

Beim Umzug auf ein CDN ändert sich genau eine Zeile in _config.php to https://apk.entrixy.com/entrixy.apk (Cloudflare R2 mit eigener Domain). Ein App-Release ist nicht nötig.

B4. Das APK auf einem CDN — Integritätsprüfung

Der Client prüft die APK-Signatur beim Herunterladen bereits (Debug-Keystore, siehe build.gradle.kts:23-29). Für ein CDN genügt das: Selbst wenn ein CDN-Knoten die Datei austauscht, passt die Signatur nicht und Android verweigert die Installation. Am Client ist nichts weiter zu tun.

Phase C — Workerman skalieren WENN WIR AN DIE DECKE STOSSEN

Der aktuelle count=1 Workerman hält rund 10 000 gleichzeitige WebSockets mühelos — server.php startet damit bereits in Zeile 40. Der erste Engpass wird die CPU-Zeit von PHP beim Spitzenaustausch: 10 000 Pongs pro Sekunde plus die Aktionen.

C1. Workerman count=2–4 mit Redis pub/sub für gemeinsamen Zustand

Today $hosts, $guests, $calls sind In-Memory-Arrays innerhalb eines Prozesses. Für mehrere Prozesse müssen sie nach Redis wandern:

Kommt eine Nachricht beim Worker W1 an, während der Empfänger auf W2 sitzt, wird sie über Redis pub/sub weitergereicht.

Effect: lineare Skalierung bis zur Kernanzahl des VPS. Auf vier Kernen sind das etwa 40 000 gleichzeitig.

C2. Eine Alternative: eine Maschine, nginx oder HAProxy vor Workerman, geshardet nach client_ip

Weniger flexibel und ohne Redis. Jeder Nutzer landet stets beim selben Worker. Es bricht jedoch zusammen, wenn Sender und Empfänger auf verschiedenen Workern liegen.

Phase D — wann auf ein CDN umziehen LATER

  1. Ein Cloudflare-R2-Bucket → hochladen entrixy.apk.
  2. Eigene Domain apk.entrixy.com → sie über das Cloudflare-CDN anbinden.
  3. In _config.php change $apk_url auf die neue.
  4. Im Deploy-Skript, nach assembleRelease add r2 cp app-release.apk apk-bucket/entrixy.apk.

Was jetzt NICHT zu tun ist

Was gerade getan wird

  1. Phase A vollständig → Release v6.66.
  2. Phase B vollständig, ohne Neustart des WebSocket-Servers, zum Testen vor der Bestätigung.
  3. Phase C entsteht in einem eigenen Branch, braucht Redis auf dem VPS und ist eine eigene Aufgabe.
  4. Phase D ist oben beschrieben und wird bei Bedarf ausgeführt.