You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
76 lines
14 KiB
76 lines
14 KiB
# Capacidad y escalado — objetivo de 100.000 jugadores simultáneos
|
|
|
|
**Estado:** objetivo de diseño propuesto el 5 de octubre de 2026, sin implementación ni ensayo que lo acredite. El sistema actual de `games2` solo sirve catálogo; este documento define qué debe construirse y medirse antes de afirmar que admite la cifra. Se complementa con la [arquitectura](architecture.md), el [modelo de plataforma](../platform-spec-v2.md) y la [API/tiempo real](api-and-events.md).
|
|
|
|
## 1. Qué significa la cifra
|
|
|
|
Para dimensionar se interpreta **100.000 jugadores humanos autenticados y conectados a partidas a la vez, repartidos entre muchas salas**. No significa 100.000 cuentas registradas, visitantes del catálogo, espectadores ni participantes de una sola partida. El paquete actual declara `players.max` de hasta 64; cada juego fija un máximo menor o igual. Pantallas compartidas, pestañas adicionales y conexiones de recuperación se cuentan por separado.
|
|
|
|
La cifra se convierte en requisito aceptado solo cuando producto fije la mezcla real de juegos, tamaño de sala, horas pico, región, presupuesto y duración exigida. Mientras tanto se ensayan varios perfiles. Ni Go ni WebSocket ni añadir réplicas demuestran por sí solos esa capacidad.
|
|
|
|
| Escenario de cálculo, no capacidad medida | Magnitud resultante |
|
|
| --- | --- |
|
|
| 100.000 jugadores / 4 por sala | 25.000 partidas activas. |
|
|
| 1,25 conexiones personales por jugador + una TV en 20 % de esas salas | 130.000 conexiones simultáneas aproximadas. |
|
|
| Un latido por jugador cada 15 s | Unas 6.667 entradas/s antes de reintentos. |
|
|
| Una acción por jugador cada 30 s de media | Unas 3.333 transacciones de juego/s; con cuatro destinatarios, unas 13.333 entregas/s, sin contar TV, chat, errores o sincronizaciones. |
|
|
| Avisos consultados por HTTP cada 30 s con 100.000 clientes activos | Unas 3.333 lecturas/s adicionales. |
|
|
|
|
Estas son **hipótesis de prueba**, no una predicción de uso. También se ensaya pico de reconexión y de actividad superior a la media. El tamaño real de cada mensaje, la proyección privada por jugador y el reparto de títulos determinan CPU, memoria, tráfico y coste.
|
|
|
|
Las 13.333 entregas/s usan cuatro destinatarios por acción como escenario aislado; no incluyen automáticamente las 1,25 conexiones personales por persona del cálculo anterior. El dimensionado final cuenta suscripciones efectivas, no solo asientos. Con 8 KiB por entrega, ese escenario produciría aproximadamente 104 MiB/s de payload antes de TLS y otros mensajes, sin que ello acredite capacidad de red. Un quiz sincronizado puede concentrar 100.000 respuestas en un segundo aunque su media sea baja: se ensayan esas ráfagas junto a la barrera de cierre. Tamaños, duración y proporción de rondas sincronizadas deben medirse, no asumirse del ejemplo.
|
|
|
|
El escenario de 130.000 conexiones supone TVs comunes; no incluye una TV personal por jugador invitado. El [perfil multidispositivo](../game-engine/multi-device-profile.md) obliga a fijar ambas proporciones por separado. `Desktop`/`Mobil` consumen proyección completa, el móvil `TVBoard` consume mando y las TVs la proyección pública: tener tres contratos no triplica por sí solo personas ni comandos. Sí cambia CPU de proyección, bytes y fanout por suscripción, con vínculos/revocación duraderos y cuotas independientes.
|
|
|
|
Las plazas del [perfil virtual propuesto](../game-engine/virtual-player-profile.md) se miden aparte de los 100.000 humanos: no se cuentan como personas ni exigen una conexión personal de navegador, pero generan trabajos, comandos y proyecciones. La mezcla de carga debe fijar porcentaje de IA, decisiones simultáneas, tiempo/cómputo y, si utiliza modelos externos, cuotas y coste. Sus colas y concurrencia se acotan fuera de las transacciones SQL; la cifra de humanos no acredita por sí sola capacidad para cualquier número de agentes.
|
|
|
|
## 2. Prepararla desde el primer juego sin pagar el clúster ahora
|
|
|
|
La primera entrega puede funcionar como **un backend Go modular y una base PostgreSQL**. La preparación consiste en conservar propiedades que permitan añadir réplicas sin reescribir reglas ni cambiar el protocolo de partidas:
|
|
|
|
| Decisión desde el principio | Comprobación antes del primer juego público |
|
|
| --- | --- |
|
|
| Sala y partida identificadas por `roomId`/`matchId` estables; paquete y versión de reglas fijados, además de `modeId` cuando exista el perfil multidispositivo. | Un reinicio carga la misma partida y acepta solo sus reglas versionadas. |
|
|
| Estado de partida, sesiones, permisos, recibos, plazos y outbox duraderos. La memoria del proceso solo contiene cachés y conexiones descartables. | Reiniciar entre commit y respuesta, reenviar el mismo comando y obtener el mismo recibo sin repetir la jugada. |
|
|
| Todos los comandos entran por el mismo servicio de aplicación y unidad de trabajo, con bloqueo ordenado por sala. | HTTP, tiempo real y tarea de plazo no pueden confirmar transiciones incompatibles. |
|
|
| Proyecciones privadas construidas por destinatario; ningún evento general transporta una mano, rol o dato infantil. | Un segundo jugador no recibe datos ajenos; al habilitar TV, se añade la misma prueba para su proyección compartida. |
|
|
| Protocolo con revisiones, idempotencia, snapshot y recuperación; el socket no es la fuente del estado. | Cambiar de conexión o de proceso recupera la vista autorizada y detecta huecos. |
|
|
| Límites configurables para cuerpos, conexiones, buffers, colas y pool de DB; métricas por sala/operación y trazas sin secretos. | Sobrecarga controlada sin crecimiento indefinido de memoria ni agotamiento de conexiones SQL. |
|
|
| Presencia efímera separada de permisos y reglas; recursos estáticos públicos por digest. | Perder presencia o caché no altera resultados ni revela información privada. |
|
|
|
|
La outbox se consume inicialmente en el mismo despliegue, con un adaptador interno sencillo. Su contrato publica referencias/proyecciones autorizadas, no llama directamente a una instancia concreta ni guarda la única copia en memoria. Así se podrá añadir un transporte entre nodos al desplegar varias réplicas. No se instalan ahora Redis, un broker, particionado de PostgreSQL o microservicios solo para anticipar la cifra.
|
|
|
|
**Orden de crecimiento:** primero medir un proceso y optimizar consultas/proyecciones reales; después añadir réplicas Go y fanout entre nodos conservando la misma base/autoridad; finalmente dividir datos o responsabilidades solo si la base, la outbox o el tráfico medidos son el cuello de botella. Antes de cada salto se prueba corte de un nodo, revocación, duplicados y recuperación. La capacidad publicada siempre corresponde a la topología efectivamente ensayada.
|
|
|
|
## 3. Topología que habría que implementar
|
|
|
|
Se conserva una autoridad transaccional por sala: toda acción confirma estado, recibo y outbox en una unidad de trabajo. Varias réplicas del backend Go pueden atender salas distintas y concurrir sobre la misma base si todas usan el mismo orden de bloqueo y la misma autorización. El estado en memoria de una réplica es caché descartable; reiniciar o mover una conexión no cambia reglas ni versión de partida. La partición de salas entre procesos, si se necesitara después, debe tener propietario/generación y recuperación explícitos; no se introduce como hecho ya resuelto.
|
|
|
|
Se separan presupuestos de **conexiones**, **comandos confirmados**, **lecturas**, **proyecciones/fanout** y **trabajo de fondo**. El balanceador y el proxy distribuyen HTTP/tiempo real entre réplicas con límites de conexiones, tiempo de drenaje y recursos del sistema operativo. Cada instancia limita memoria por conexión, buffers, colas, goroutines y pool de PostgreSQL; la suma de pools no puede agotar la base. Las rutas de catálogo y recursos estáticos pueden escalar/cachearse aparte sin almacenar proyecciones privadas en caché pública.
|
|
|
|
La outbox duradera permanece en PostgreSQL. Un transporte interno de fanout puede avisar a los nodos que mantienen las conexiones de cada sala, pero no sustituye la outbox ni la autorización final por destinatario. Un nodo lento se desacopla mediante colas acotadas: se ordena resincronizar o se desconecta al cliente, sin retener infinitos eventos. Las proyecciones privadas no se difunden en un canal general ni se reconstruyen desde datos de otro jugador. Duplicados, huecos y reconexión siguen las revisiones/recibos del protocolo.
|
|
|
|
La presencia y los latidos son efímeros, con TTL; **no se hace una escritura SQL por cada latido**. Los cambios útiles se agregan y entregan solo a las salas afectadas. Permisos, revocaciones, resultados y recibos sí mantienen persistencia/consistencia de dominio. Un caché de autorización solo se admite si una revocación puede invalidarlo antes de nuevas entregas. Listados y consultas no deben hacer barridos de todas las salas por petición.
|
|
|
|
El perfil Socket.IO documentado y el futuro WebSocket Go requieren planes de clúster distintos. Si se conserva long-polling de Socket.IO, hacen falta afinidad de sesión y adaptador entre nodos; su [documentación del adaptador Redis](https://socket.io/docs/v4/redis-adapter/) indica que Pub/Sub no almacena eventos ni soporta por sí mismo recuperación de estado de conexión. Si se adopta WebSocket propio, el binding versionado debe implementar autenticación, fanout, cursores, reconexión y drenaje antes de sustituirlo. [Socket.IO también deja la recuperación de mensajes perdidos a la aplicación](https://socket.io/docs/v4/delivery-guarantees/). Ninguna opción de transporte cambia la transacción autoritativa.
|
|
|
|
PostgreSQL puede seguir siendo una autoridad inicial, pero su capacidad se mide con los índices, bloqueos, tamaño de posiciones, recibos, outbox, retención y hardware reales. No se crean particiones ni réplicas de lectura por intuición; [PostgreSQL documenta ventajas y límites de particionar](https://www.postgresql.org/docs/current/ddl-partitioning.html). Si el volumen exige dividir datos, se preservan unicidad, transacciones por sala, restauración y revocación antes de migrar. Las lecturas con consistencia de revisión no se envían a una réplica atrasada como si fuese autoridad.
|
|
|
|
## 4. Coste protocolario y ajustes necesarios
|
|
|
|
Los valores iniciales de [API/tiempo real](api-and-events.md#7-límites-avisos-y-recuperación) incluyen presencia cada 15 s, cursores cada 15 s y consulta de avisos cada 30 s. A 100.000 conexiones, incluso el tráfico sin jugadas importa. La implementación debe medirlo y adoptar temporizadores con dispersión, agregación por nodo y envío condicionado a cambio. Antes del perfil de 100.000, el contrato de avisos debe versionarse para que `notification.changed` impulse la consulta y la lectura periódica quede como recuperación con intervalo/dispersión definidos; no basta con omitir el sondeo actual en el cliente. No se reduce la detección de revocaciones: estas se empujan e invalidan del lado del servidor sin esperar al siguiente latido.
|
|
|
|
El fanout es **por sala y por destinatario**, nunca global por cada jugada. Una pantalla compartida recibe una sola proyección pública autorizada; cada jugador su proyección propia. Animaciones, audio y sensores se procesan en cliente y no generan comandos continuos. La entrada `motion.shake` produce a lo sumo una acción idempotente por oferta. Catálogo, recursos de juegos y traducciones se distribuyen por digest/caché pública; estado de partida, chat y datos infantiles no se cachean como contenido público.
|
|
|
|
El [perfil temporal](../game-engine/synchronization-profile.md) añade probes con frecuencia acotada y ráfagas de respuestas al abrir/cerrar una ronda. No se escribe SQL por cada muestra de reloj; configuración, ingresos admitidos, plazos y resultados sí son duraderos. Se mide la cola antes/después del ingreso confiable, el retraso de fanout hacia cada fuente y la época al cambiar de nodo. Capacidad suficiente de sockets no demuestra equidad temporal. Las cargas deben incluir TV personal + mando, ventanas simultáneas, respuestas que compiten con cierre y cambio a móvil completo entre rondas.
|
|
|
|
## 5. Prueba de aceptación antes de anunciar 100.000
|
|
|
|
1. Definir mezcla representativa: número de salas, jugadores por juego, conexiones por persona, pantallas compartidas, latidos, acciones, chat permitido, reconexiones y tamaños de mensajes. Registrar generador, datos semilla, hardware, región, versiones y presupuesto.
|
|
2. Probar escalones de 10.000, 25.000, 50.000 y 100.000 jugadores conectados, con rampa, duración sostenida y ráfagas. Ejecutar acciones reales del motor que escriban PostgreSQL, creen recibos, proyecten vistas privadas y publiquen outbox; una prueba de sockets vacíos no cuenta.
|
|
3. Medir p50/p95/p99 de conexión, acción confirmada, entrega y resincronización; tasa de error/reintento, retraso de outbox, presión de locks/pool, CPU, memoria, red, coste y bytes por destinatario. Contrastar con los [objetivos de operación](../platform-spec-v2.md#10-operación-rendimiento-y-experiencia).
|
|
4. Provocar pérdida de nodo, proxy y transporte de fanout, corte de base, despliegue con drenaje, revocación de permiso, clientes lentos y reconexión masiva con jitter. Ninguna caída duplica jugadas, filtra vistas o deja una pantalla revocada recibiendo datos; los clientes recuperan estado mediante snapshot/cursor.
|
|
5. Repetir al superar límites de recursos y documentar degradación controlada: rechazar nuevas conexiones/comandos con recuperación clara antes de agotar memoria o bloquear todas las salas. Publicar capacidad observada con margen operativo y el coste del entorno probado.
|
|
|
|
**Criterio de salida:** 100.000 personas jugando y el número de conexiones derivado del perfil acordado durante la duración fijada, con los SLO y garantías de privacidad/consistencia cumplidos bajo carga y fallos. Hasta obtener ese resultado, la cifra es un objetivo de ingeniería, no una capacidad del producto.
|