Saltar a contenido

Caso: News feed Medio

El feed de una red social: cada usuario ve las publicaciones recientes de las personas que sigue. El reto es leer rápido un contenido que depende de muchas otras personas.

Paso 1 — Requisitos

  • Publicar posts; ver el feed con los posts de las cuentas seguidas, en orden cronológico inverso (o por relevancia).
  • 300 M usuarios activos al día; cada uno consulta el feed 10 veces y publica 1 post al día; un usuario sigue de media a 200 cuentas.
  • Latencia de lectura del feed < 200 ms. Se tolera que un post tarde unos segundos en aparecer (consistencia eventual).
Estima las cargas
  • Lecturas de feed: 300 M × 10 / 10⁵ s ≈ 30 000/s (pico ~60 000).
  • Publicaciones: 300 M / 10⁵ ≈ 3 000/s.
  • El sistema es muy intensivo en lectura (10:1), lo que empuja a precalcular.

Paso 2 — Diseño de alto nivel

flowchart LR
    U[Usuario] --> API[API]
    API --> PS[Servicio de posts]
    PS --> PDB[(Posts)]
    PS --> Q[[Cola]]
    Q --> FO[Workers de fan-out]
    FO --> GS[(Grafo social<br/>quién sigue a quién)]
    FO --> FC[(Caché de feeds<br/>Redis: lista de IDs por usuario)]
    API --> FS[Servicio de feed]
    FS --> FC
    FS --> PC[(Caché de posts)]

Paso 3 — Profundizar: fan-out

La decisión central: ¿cuándo se construye el feed?

Estrategia Cómo Pros Contras
Fan-out on write (push) Al publicar, se añade el ID del post al feed precalculado de cada seguidor Lectura muy rápida (el feed ya está listo) Publicar es caro para cuentas con millones de seguidores; se trabaja para usuarios inactivos
Fan-out on read (pull) Al leer, se consultan los posts recientes de todas las cuentas seguidas y se mezclan Publicar es barato; nada se desperdicia Leer es caro y lento (200 consultas y una mezcla)
Híbrido Push para la mayoría; pull para las cuentas con millones de seguidores ("celebridades") Lo mejor de ambos Más complejo
sequenceDiagram
    participant A as Autor (500 seguidores)
    participant PS as Servicio de posts
    participant FO as Fan-out
    participant FC as Caché de feeds
    A->>PS: publica post 987
    PS->>FO: evento PostCreated(987, autor)
    FO->>FO: obtiene los 500 seguidores
    FO->>FC: LPUSH feed:{seguidor} 987 (×500, con LTRIM a 800 elementos)

Al leer: el servicio de feed obtiene los IDs de feed:{usuario}, añade en ese momento los posts recientes de las celebridades que sigue (pull), ordena, y rellena el contenido de cada post desde la caché de posts.

Detalles

  • El feed en caché guarda solo IDs (8 bytes), no el contenido: ocupa poco y el contenido se actualiza en un solo sitio.
  • Se limita la longitud (p. ej. 800 entradas); lo más antiguo se reconstruye bajo demanda si alguien hace mucho scroll.
  • Usuarios inactivos: no hacer fan-out para quien no entra en 30 días; reconstruir su feed al volver.
  • Ranking: con feed por relevancia, se recuperan candidatos (como arriba) y un modelo los puntúa antes de mostrarlos.

Paso 4 — Cierre

  • Escalado: workers de fan-out horizontales; caché particionada por usuario.
  • Borrados y privacidad: al borrar un post, eliminar de la caché de posts (los IDs huérfanos se filtran al leer).
  • Monitorización: latencia de fan-out (tiempo hasta que un post aparece), latencia de lectura p99, tasa de aciertos de caché.

Preguntas de repaso

¿Por qué no hacer fan-out on write para una cuenta con 50 M de seguidores?

Cada post supondría 50 M escrituras en caché: minutos de retraso, carga enorme, y mucho trabajo para seguidores que ni siquiera leerán. Para esas cuentas es más eficiente mezclar sus posts al leer.

¿Por qué la caché de feed guarda IDs y no los posts completos?

Para ahorrar memoria (un ID frente a KB de contenido replicado en millones de feeds) y para que editar o borrar un post se haga en un único sitio.

Ejercicios

Ejercicio 1 · Básico — Memoria de la caché de feeds

300 M usuarios, 800 IDs de 8 bytes por feed, más ~40 % de sobrecarga de la estructura en Redis. ¿Cuánta memoria?

Solución

300 × 10⁶ × 800 × 8 B = 1,92 × 10¹² B ≈ 1,9 TB, ×1,4 ≈ 2,7 TB. Hace falta un clúster de Redis particionado (p. ej. 30-40 nodos de 100 GB con réplicas). Palanca: guardar solo el feed de usuarios activos en 30 días (por ejemplo la mitad) → ~1,35 TB.

Ejercicio 2 · Medio — Umbral de celebridad

Cada escritura de fan-out cuesta ~0,1 ms de trabajo de worker. ¿A partir de cuántos seguidores pasarías una cuenta a pull si quieres que un post aparezca en menos de 5 s con 100 workers en paralelo?

Solución

Capacidad en 5 s: 100 workers × (5 s / 0,1 ms) = 100 × 50 000 = 5 M escrituras. Pero esa capacidad la comparten todos los posts que se publican a la vez, así que el umbral debe ser mucho menor: p. ej. ~100 000 seguidores deja margen para miles de publicaciones concurrentes. En la práctica se ajusta midiendo la latencia de fan-out.

Ejercicio 3 · Avanzado — Feed de actividad de la flota

Aplica el patrón al feed de actividad de tu plataforma: 2 000 operadores siguen sitios, regiones o equipos; hay 20 000 sitios que generan eventos. ¿Push, pull o híbrido?

Solución

Pocos lectores (2 000) y muchos productores de eventos: el volumen de fan-out es pequeño salvo para "seguir una región" (miles de sitios). Híbrido: push de eventos de sitios seguidos individualmente al feed del operador; pull al leer para las suscripciones amplias (región completa), consultando un índice por región y tiempo. Con estos volúmenes, incluso pull puro con una consulta indexada por (region, tiempo) sería suficiente: no hay que sobrediseñar.