¿Por Qué Netflix Construyó Su Propia Abstracción de Grafos?

Netflix tiene un montón de casos de uso de grafos — desde grafos sociales en gaming hasta análisis de topología de servicios en tiempo real. Se dividen en dos categorías: OLAP (analítico, consultas abiertas) y OLTP (alto rendimiento, baja latencia). El OLTP exige millones de operaciones por segundo con respuestas en milisegundos — algo que las bases de datos de grafos listas para usar no pueden entregar sin hacer concesiones pesadas.

Este post es el primero de una serie que detalla la arquitectura de la Graph Abstraction personalizada de Netflix, que actualmente maneja ~10 millones de ops/seg en 650 TB de datos de grafos con latencias p99 de un solo dígito.

Fuente: Netflix Tech Blog – High-Throughput Graph Abstraction at Netflix: Part I

Netflix Graph Abstraction architecture diagram showing nodes and edges connected across distributed servers Development Concept Image

Arquitectura: Construyendo Sobre Abstracciones Existentes

En lugar de reinventar la capa de almacenamiento, Netflix construyó la Graph Abstraction sobre su Key-Value (KV) Abstraction para índices en tiempo real y TimeSeries (TS) Abstraction para vistas históricas. EVCache proporciona caché de baja latencia, y el Data Gateway Control Plane gestiona esquemas y aprovisionamiento.

Modelo Property Graph con Esquemas Fuertemente Tipados

Los nodos y aristas se almacenan como property graphs con esquemas explícitos definidos en el control plane. Ejemplo de configuración de mapeo de aristas:

{
  "edgeConfig": {
    "edgeMappings": [
      {
        "edgeMappingKey": {
          "fromNodeType": "account",
          "edgeType": "owns",
          "toNodeType": "profile"
        },
        "directionType": "UNIDIRECTIONAL"
      },
      {
        "edgeMappingKey": {
          "fromNodeType": "profile",
          "edgeType": "linked_to",
          "toNodeType": "device"
        },
        "directionType": "BIDIRECTIONAL"
      }
    ]
  }
}

Los esquemas de propiedad extienden los mapeos con nombres y tipos permitidos:

{
  "edgeMappingKey": {
    "fromNodeType": "profile",
    "edgeType": "linked_to",
    "toNodeType": "device"
  },
  "propertySchema": {
    "propertyMappings": [
      { "propertyKey": "registration_time", "propertyValueType": "TIMESTAMP" },
      { "propertyKey": "status", "propertyValueType": "STRING" }
    ]
  }
}

El esquema permite validación de calidad de datos, planificación de consultas, deduplicación de aristas recorridas y eliminación de caminos imposibles.

Índice en Tiempo Real: Almacenamiento Key-Value

Los nodos se almacenan por tipo en namespaces KV dedicados, permitiendo búsquedas eficientes en una sola partición. Las aristas usan dos índices separados:

  • Índice de links: lista de adyacencia que mapea nodos fuente a vecinos.
  • Índice de propiedades: almacena propiedades de las aristas por separado.

Esta separación permite upserts eficientes de propiedades sin filas anchas, a costa de escrituras no atómicas entre namespaces.

Estrategias de Caché

  • Caché write-aside de links de aristas reduce amplificación de escritura.
  • Caché read-aside de propiedades (vía EVCache) reduce amplificación de lectura.
  • Caché write-through (en desarrollo) organizará índices por diferentes órdenes de clasificación.

Garantía de Consistencia

La replicación multi-región es asíncrona, resultando en consistencia eventual. Un mecanismo de reparo de entropía usa Kafka para reintentar escritas fallidas entre índices. Las eliminaciones de nodos son asíncronas con resolución de conflictos LWW.

API de Traversal

La API gRPC personalizada (inspirada en Gremlin) soporta traversals encadenados, filtros, ordenamiento y limitación. Ejemplo de consulta hipotética para recomendar series en un dispositivo compartido:

TraversalRequest.newBuilder()
    .setNamespace("")
    .setTraversalQuery(
        TraversalQuery.newBuilder()
            .setStartNode(node("device", "my-device-id"))
            .setTraversal(
                Traversal.newBuilder()
                    .setEdgeLimit(5)
                    .setDirectionTraversal(
                        DirectionTraversal.newBuilder()
                            .setDirection(IN)
                            .addNodePropertiesSelections(propSelection("account", "created_at"))
                            .addNodePropertiesSelections(propSelection("profile", "last_active"))
                            .setDirectionFilter(
                                DirectionFilter.newBuilder()
                                    .setTypeMatchingStrategy(EXCLUDE_NON_TARGETED)
                                    .addAllNodeFilters(typeFilters("account", "profile"))))
                    .addNextTraversals(
                        Traversal.newBuilder()
                            .setOrder(LATEST)
                            .setEdgeLimit(200)
                            .setDirectionTraversal(
                                DirectionTraversal.newBuilder()
                                    .setDirection(OUT)
                                    .addEdgePropertiesSelections(propSelection("watched", "view_time"))
                                    .addEdgePropertiesSelections(propSelection("has_plan", "active"))
                                    .setDirectionFilter(
                                        DirectionFilter.newBuilder()
                                            .setTypeMatchingStrategy(EXCLUDE_NON_TARGETED)
                                            .addAllNodeFilters(typeFilters("title", "plan"))))))
    .build();

Real-time graph traversal flow for OLTP use cases with low-latency caching layers Coding Session Visual

Rendimiento y Métricas Reales

  • Rendimiento: hasta 10 millones de ops/seg en pico.
  • Latencia: un solo dígito para lecturas puntuales y traversals de 1 salto (p99).
  • Traversals de 2 saltos (usados por RDG): p90 por debajo de 50ms.
  • Volumen de datos: ~650 TB globalmente.

Limitaciones y Precauciones

  • Consistencia eventual es un intercambio deliberado por alta disponibilidad. Las aplicaciones deben tolerar lecturas desactualizadas de corta duración.
  • Escrituras no atómicas entre múltiples namespaces requieren lógica de reintento cuidadosa.
  • Profundidad de traversal limitada para mantener latencia predecible; traversals profundos (3+ saltos) pueden requerir estrategias diferentes.

Próximos Pasos

La Parte II de esta serie se sumergirá en la planificación y ejecución de traversals, además de mecanismos de conteo. La Parte III cubrirá el índice temporal y la integración con Time Series. Si estás construyendo un sistema de grafos de alto rendimiento, empieza con una separación clara entre almacenamiento de links y propiedades, e invierte en un planificador de consultas orientado a esquemas.

Lectura Relacionada

Performance metrics dashboard for graph database operations showing p50 p90 p99 latencies Dev Environment Setup

Conclusión

La Graph Abstraction de Netflix es una clase magistral de cómo construir una base de datos de grafos OLTP a medida sobre infraestructura existente. Separando índices de links y propiedades, usando optimizaciones orientadas a esquemas y empleando caché en capas, logran un rendimiento y latencia impresionantes a escala. Los intercambios — consistencia eventual, escrituras no atómicas, profundidad de traversal limitada — están claramente documentados y son aceptables para sus casos de uso.

Si estás arquitectando un sistema de grafos para cargas de trabajo OLTP de alto rendimiento, inspírate en el enfoque de Netflix: no construyas todo desde cero; compón abstracciones existentes sabiamente.

Este contenido fue redactado con la asistencia de herramientas de IA, basándose en fuentes confiables, y fue revisado por nuestro equipo editorial antes de su publicación. No reemplaza el asesoramiento de un profesional especializado.