Plan : 100 000 utilisateurs sur un seul VPS, prêt pour le CDN

Version du document 29/05/2026 · Objectif : l’unique VPS actuel doit tenir jusqu’à 100 000 MAU sans tomber, et l’application passer à un CDN sans couture le moment venu.

Chiffres cibles

MetricValeurSource
MAU100,000l’objectif
DAU30,00030 % du MAU, normal pour une application utilitaire
WS simultanés10 000 (pic 15 000)~10 % du MAU en journée, 15 % au pic
Actions par jour90,0003 ouvertures ou appels par utilisateur quotidien
Requêtes de l’API HTTP par seconde (moyenne)3-5surtout key_create, host_sync et crash_report
Requêtes de l’API HTTP par seconde (pic)30-50après une publication massive qui met à jour les numéros

L’architecture actuelle

Phase A — le client Android MAINTENANT, v6.66

Goal: réduire le nombre de requêtes superflues des clients pour que le serveur ait moins à faire.

A1. Attente exponentielle des reprises HTTP pour les clés hôte en attente

File: HostService.kt:1424-1432. Aujourd’hui syncPendingHostKeys() est appelée toutes les 60 s et frappe Api.keyCreate pour chaque clé en attente sans aucune attente.

Stocker dans Prefs per-localId: attempt_at_id et attempt_count_id. Attente : 30 s → 60 s → 2 min → 5 min → 15 min → 30 min (plafond). Remise à zéro en cas de succès.

A2. Attente des reprises HTTP pour les lots invités en attente

File: Vault.kt:213. La même logique.

A3. Comparer avant d’incrémenter overridesVersion

File: GuestConn.kt (5+ endroits). Comparer le nouveau parseNumbers avec le précédent, par hachage ou directement. S’ils sont identiques, ne pas incrémenter AppState.overridesVersion. Cela supprime les vagues de recomposition — 80 lectures de SharedPreferences × 20 cartes — à chaque initialisation WebSocket.

Phase B — protection du serveur et préparation au CDN NOW

Goal: protéger le serveur d’une tempête même avec un client défaillant, et détacher l’adresse de l’APK du domaine.

B1. Une limite de fréquence sur host_hello et guest_hello

In server.php case 'host_hello' / 'guest_hello' : si le même device_id / user_key a envoyé un hello il y a moins de N secondes, fermer la connexion sans toucher la base. N = 5 s.

Protection au cas où un client à la logique cassée enverrait cent hellos par seconde.

B2. Plafond de pending_host_msgs par host_id

In enqueueHostMsg : si ce host_id a déjà plus de 200 lignes, supprimer la plus ancienne avec DELETE FROM pending_host_msgs WHERE host_id=? ORDER BY id ASC LIMIT 1. Protection contre la croissance sans fin de la file en cas d’activité malveillante.

B3. Préparation au CDN : déplacer l’adresse de l’APK dans la configuration

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'].

Lors du passage au CDN, exactement une ligne changera dans _config.php to https://apk.entrixy.com/entrixy.apk (Cloudflare R2 avec un domaine personnalisé). Aucune publication d’application n’est requise.

B4. L’APK sur un CDN — vérification de l’intégrité

Le client vérifie déjà la signature de l’APK au téléchargement (keystore de débogage, voir build.gradle.kts:23-29). Pour un CDN cela suffit : même si un nœud du CDN remplace le fichier, la signature ne correspondra pas et Android refusera de l’installer. Rien de plus n’est à faire côté client.

Phase C — mise à l’échelle de Workerman QUAND ON TOUCHERA LE PLAFOND

L’actuel count=1 Workerman tient sans peine environ 10 000 WebSockets simultanés — server.php démarre déjà avec lui à la ligne 40. Le premier goulet sera le temps CPU de PHP au pic d’échange : 10 000 pongs par seconde plus les actions.

C1. Workerman count=2–4 avec Redis pub/sub pour l’état partagé

Today $hosts, $guests, $calls sont des tableaux en mémoire dans un seul processus. Pour lancer plusieurs processus, il faut les déplacer vers Redis :

Un message arrive sur le worker W1 alors que le destinataire est sur W2 — on le relaie par Redis pub/sub.

Effect: mise à l’échelle linéaire jusqu’au nombre de cœurs du VPS. Sur quatre cœurs, cela fait environ 40 000 simultanées.

C2. Une alternative : une machine, nginx ou HAProxy devant Workerman, partitionné par client_ip

Moins souple et sans Redis. Chaque utilisateur atterrit toujours sur le même worker. Mais cela casse quand l’expéditeur et le destinataire se retrouvent sur des workers différents.

Phase D — quand passer à un CDN LATER

  1. Un bucket Cloudflare R2 → téléverser entrixy.apk.
  2. Domaine personnalisé apk.entrixy.com → le raccorder via le CDN Cloudflare.
  3. In _config.php change $apk_url vers la nouvelle.
  4. Dans le script de déploiement, après assembleRelease add r2 cp app-release.apk apk-bucket/entrixy.apk.

Ce qu’il ne faut PAS faire maintenant

Ce qui est fait en ce moment

  1. La phase A en entier → version v6.66.
  2. La phase B en entier, sans redémarrer le serveur WebSocket, pour tester avant confirmation.
  3. La phase C se fait dans une branche à part, exige Redis sur le VPS et constitue une tâche à part entière.
  4. La phase D est décrite ci-dessus et s’exécute au besoin.