Caso: Sistema de chat Avanzado¶
Paso 1 — Requisitos¶
- Chats 1 a 1 y grupos pequeños (hasta 100 miembros); estado en línea (presencia); historial persistente; varios dispositivos por usuario.
- Escala: 50 M usuarios activos al día, 40 mensajes por usuario y día.
- Latencia de entrega < 500 ms; orden de mensajes dentro de cada conversación; no perder mensajes.
Estima el volumen
50 M × 40 = 2 000 M mensajes/día ≈ 23 000 mensajes/s de media (pico ~50 000). Con ~100 bytes de media: 200 GB/día ≈ 73 TB/año de mensajes. Además, decenas de millones de conexiones persistentes simultáneas.
Paso 2 — Diseño de alto nivel¶
flowchart LR
U1[Cliente A] <-- WebSocket --> CS1[Chat server 1]
U2[Cliente B] <-- WebSocket --> CS2[Chat server 2]
CS1 --> MQ[[Bus de mensajes<br/>Kafka]]
MQ --> CS2
CS1 & CS2 --> SR[(Registro de sesiones<br/>usuario → servidor, Redis)]
CS1 & CS2 --> ST[(Almacén de mensajes<br/>Cassandra / ScyllaDB)]
CS1 & CS2 --> PR[Servicio de presencia]
MQ --> PN[Push a usuarios<br/>desconectados]
API[API HTTP<br/>login, perfiles, historial] --> ST
- Conexión persistente (WebSocket) entre cliente y un chat server: permite al servidor empujar mensajes.
- Servicios HTTP sin estado para lo demás (login, perfiles, historial, subida de ficheros).
- Los chat servers sí tienen estado (las conexiones abiertas) → un registro de sesiones indica en qué servidor está cada usuario.
Paso 3 — Profundizar¶
Flujo de un mensaje¶
- A envía el mensaje por su WebSocket a CS1 con un ID de cliente (para idempotencia).
- CS1 asigna un ID ordenable por conversación, lo persiste y confirma a A (✓ enviado).
- CS1 publica en el bus; el servidor donde está conectado B (CS2) lo recibe y lo empuja a B.
- B confirma la recepción (✓✓ entregado); más tarde, la lectura (✓✓ azul).
- Si B no está conectado → notificación push; al reconectar, sincroniza desde su último ID recibido.
Orden e IDs¶
- Orden por conversación, no global (sería un cuello de botella).
- IDs tipo Snowflake (ordenables por tiempo) o una secuencia por conversación.
- En Kafka, particionar por
conversation_idgarantiza el orden dentro de cada conversación.
Almacenamiento¶
Patrón de acceso: "últimos N mensajes de la conversación X" y "mensajes posteriores al ID Y". Encaja con una base
columnar ancha (Cassandra, ScyllaDB, HBase): clave de partición conversation_id, clave de ordenación message_id.
Particionar además por periodo si una conversación es enorme.
Varios dispositivos¶
Cada dispositivo guarda su último message_id sincronizado; al conectarse pide lo posterior. El envío se hace a todos
los dispositivos activos del usuario.
Presencia¶
- Heartbeat del cliente cada N segundos; si deja de llegar, pasa a "desconectado" (tolerando cortes breves de red).
- Difundir cambios de presencia solo a contactos interesados; en grupos grandes, consultar bajo demanda en vez de difundir.
Escalar las conexiones¶
- Decenas de miles o más de conexiones por servidor (E/S asíncrona, hilos virtuales).
- El balanceador debe soportar conexiones largas; al desplegar, drenar conexiones poco a poco (los clientes reconectan con backoff).
Paso 4 — Cierre¶
| Tema | Decisión |
|---|---|
| Cifrado de extremo a extremo | Protocolo Signal: el servidor solo ve mensajes cifrados (limita búsqueda y moderación en el servidor) |
| Ficheros | Subida directa a almacenamiento de objetos con URL prefirmada; en el mensaje solo la referencia |
| Multi-región | Usuarios conectados a su región; enrutado entre regiones para conversaciones mixtas |
| Monitorización | Conexiones activas, latencia de entrega p99, mensajes pendientes, tasa de reconexión |
¿Por qué WebSocket y no sondeo HTTP?
El sondeo (polling) genera muchísimas peticiones vacías y añade latencia; el long polling mejora pero sigue siendo costoso. WebSocket mantiene una conexión bidireccional por la que el servidor empuja mensajes al instante.
¿Cómo garantizas el orden sin un secuenciador global?
Ordenando solo dentro de cada conversación: IDs ordenables por conversación y particionado por conversation_id en el
bus y en el almacenamiento.
¿Cómo evitas duplicados si el cliente reenvía un mensaje tras perder la confirmación?
Con un ID generado por el cliente que el servidor usa como clave de idempotencia: si ya existe, devuelve la confirmación original sin duplicar el mensaje.
Ejercicios¶
Ejercicio 1 · Básico — Dimensionar servidores de chat
Hay 20 M de usuarios conectados a la vez en el pico y cada servidor soporta 50 000 conexiones WebSocket. ¿Cuántos servidores necesitas, con qué margen, y qué pasa al desplegar una versión nueva?
Solución
20 M / 50 000 = 400 servidores; con margen para fallos y picos (~30 %), unos 520. Al desplegar, cada servidor que se reinicia desconecta a sus 50 000 usuarios, que reconectan a la vez: hay que drenar poco a poco (dejar de aceptar conexiones nuevas, cerrar las existentes de forma escalonada) y que los clientes reconecten con backoff y jitter para no crear una avalancha contra los demás servidores.
Ejercicio 2 · Medio — Sincronizar un dispositivo
Un usuario abre la app en su portátil tras 3 días desconectado; en su móvil ha seguido chateando. ¿Cómo se sincroniza el portátil sin descargar todo el historial?
Solución
El portátil guarda, por conversación, el último message_id que recibió. Al conectar, pide la lista de conversaciones
con actividad posterior a su última sincronización y, para cada una, los mensajes con message_id > último, paginados.
Las conversaciones sin actividad no se tocan. Como los IDs son ordenables por tiempo, la consulta es un rango sobre la
clave de ordenación: eficiente en una base de datos columnar ancha.
Ejercicio 3 · Avanzado — Chat de operadores de la flota
Quieres un canal de chat por incidente en tu plataforma, donde operadores y un bot publican eventos de la flota en tiempo real (máximo 200 operadores simultáneos). ¿Qué parte de la arquitectura de esta página usarías?
Solución
A esta escala sobra casi todo: un único servicio (con 2-3 réplicas) con WebSockets o Server-Sent Events, un
pub/sub simple entre réplicas (Redis Pub/Sub o PostgreSQL LISTEN/NOTIFY) y los mensajes guardados en PostgreSQL con
índice (incident_id, created_at). El bot publica mediante la misma API. Lo que sí se mantiene: IDs ordenables,
confirmación de recepción, reconexión con sincronización desde el último mensaje e idempotencia al enviar. Dimensiona
según los números, no según el caso de libro.