Plan: 100 mil usuarios en un solo VPS, listo para CDN

Versión del documento 2026-05-29 · Objetivo: que el único VPS actual aguante hasta 100 mil MAU sin caerse y que la aplicación se mude a una CDN sin costuras cuando llegue el momento.

Cifras objetivo

MetricValorSource
MAU100,000el objetivo
DAU30,00030 % del MAU, normal en una aplicación utilitaria
WS simultáneos10 000 (pico 15 mil)~10 % del MAU durante el día, 15 % en el pico
Acciones al día90,0003 aperturas o llamadas por usuario diario
Peticiones de la API HTTP por segundo (media)3-5sobre todo key_create, host_sync y crash_report
Peticiones de la API HTTP por segundo (pico)30-50tras una publicación masiva que actualiza los números

La arquitectura de hoy

Fase A: el cliente Android AHORA, v6.66

Goal: reducir el número de peticiones redundantes de los clientes para que el servidor tenga menos trabajo.

A1. Espera exponencial de los reintentos HTTP para las claves de anfitrión pendientes

File: HostService.kt:1424-1432. Hoy syncPendingHostKeys() se llama cada 60 s y golpea Api.keyCreate por cada clave pendiente sin espera alguna.

Guardar en Prefs per-localId: attempt_at_id y attempt_count_id. Espera: 30 s → 60 s → 2 min → 5 min → 15 min → 30 min (tope). Se reinicia al tener éxito.

A2. Espera de los reintentos HTTP para los paquetes de invitado pendientes

File: Vault.kt:213. La misma lógica.

A3. Comparar antes de incrementar overridesVersion

File: GuestConn.kt (5+ lugares). Comparar el nuevo parseNumbers con el anterior, por hash o directamente. Si coinciden, no incrementar AppState.overridesVersion. Esto elimina las oleadas de recomposición —80 lecturas de SharedPreferences × 20 tarjetas— en cada inicialización del WebSocket.

Fase B: protección del servidor y preparación para la CDN NOW

Goal: proteger el servidor de una tormenta incluso con un cliente defectuoso y desligar la dirección del APK del dominio.

B1. Un límite de frecuencia para host_hello y guest_hello

In server.php case 'host_hello' / 'guest_hello': si el mismo device_id / user_key envió un hello hace menos de N segundos, cerrar la conexión sin tocar la base. N = 5 s.

Protección por si un cliente con la lógica rota envía cien hellos por segundo.

B2. Tope de pending_host_msgs por host_id

In enqueueHostMsg: si este host_id ya tiene más de 200 filas, borrar la más antigua con DELETE FROM pending_host_msgs WHERE host_id=? ORDER BY id ASC LIMIT 1. Protección frente al crecimiento sin fin de la cola ante actividad maliciosa.

B3. Preparación para la CDN: llevar la dirección del APK a la configuración

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

Cuando llegue la mudanza a la CDN, cambiará exactamente una línea en _config.php to https://apk.entrixy.com/entrixy.apk (Cloudflare R2 con dominio propio). No hace falta publicar la aplicación.

B4. El APK en una CDN: comprobación de integridad

El cliente ya verifica la firma del APK al descargarlo (almacén de depuración, véase build.gradle.kts:23-29). Para una CDN eso basta: aunque un nodo de la CDN cambie el archivo, la firma no cuadrará y Android se negará a instalarlo. En el cliente no hace falta nada más.

Fase C: escalar Workerman CUANDO CHOQUEMOS CON EL TECHO

El actual count=1 Workerman aguanta sin problemas unos 10 mil WebSockets simultáneos: server.php ya arranca con él en la línea 40. El primer cuello de botella será el tiempo de CPU de PHP en el pico de intercambio: 10 mil pongs por segundo más las acciones.

C1. Workerman count=2–4 con Redis pub/sub para el estado compartido

Today $hosts, $guests, $calls son arreglos en memoria dentro de un solo proceso. Para levantar varios procesos hay que llevarlos a Redis:

Si un mensaje llega al trabajador W1 pero el destinatario está en W2, se retransmite por Redis pub/sub.

Effect: escalado lineal hasta el número de núcleos del VPS. Con cuatro núcleos son unas 40 mil simultáneas.

C2. Una alternativa: una máquina, nginx o HAProxy delante de Workerman, repartido por client_ip

Menos flexible y sin necesidad de Redis. Cada usuario cae siempre en el mismo trabajador. Pero se rompe cuando emisor y destinatario acaban en trabajadores distintos.

Fase D: cuándo mudarse a una CDN LATER

  1. Un bucket de Cloudflare R2 → subir entrixy.apk.
  2. Dominio propio apk.entrixy.com → conectarlo a través de la CDN de Cloudflare.
  3. In _config.php change $apk_url a la nueva.
  4. En el script de despliegue, tras assembleRelease add r2 cp app-release.apk apk-bucket/entrixy.apk.

Qué NO hay que hacer ahora

Qué se está haciendo ahora mismo

  1. La fase A completa → versión v6.66.
  2. La fase B completa, sin reiniciar el servidor WebSocket, para probar antes de confirmar.
  3. La fase C se hace en una rama aparte, necesita Redis en el VPS y es una tarea en sí misma.
  4. La fase D está descrita arriba y se ejecuta cuando haga falta.