Por Que a Netflix Criou Sua Própria Abstração de Grafos?

A Netflix tem casos de uso de grafos bem variados — desde grafos sociais em jogos até análise de topologia de serviços em tempo real. Eles se dividem em duas categorias: OLAP (analítico, queries abertas) e OLTP (alta vazão, baixa latência). O OLTP exige milhões de operações por segundo com respostas em milissegundos — algo que bancos de grafos prontos geralmente não entregam sem fazer concessões pesadas.

Este post é o primeiro de uma série que detalha a arquitetura da Graph Abstraction personalizada da Netflix, que atualmente lida com ~10 milhões de ops/seg em 650 TB de dados de grafos com latências p99 de dígito único.

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

Netflix Graph Abstraction architecture diagram showing nodes and edges connected across distributed servers Technical Structure Concept

Arquitetura: Construindo Sobre Abstrações Existentes

Em vez de reinventar a camada de armazenamento, a Netflix construiu a Graph Abstraction sobre sua Key-Value (KV) Abstraction para índices em tempo real e TimeSeries (TS) Abstraction para visões históricas. O EVCache fornece cache de baixa latência, e o Data Gateway Control Plane gerencia schemas e provisionamento.

Modelo Property Graph com Schemas Fortemente Tipados

Nós e arestas são armazenados como property graphs com schemas explícitos definidos no control plane. Exemplo de configuração de mapeamento de arestas:

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

Schemas de propriedade estendem os mapeamentos com nomes e tipos permitidos:

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

O schema permite validação de qualidade de dados, planejamento de queries, deduplicação de arestas percorridas e eliminação de caminhos impossíveis.

Índice em Tempo Real: Armazenamento Key-Value

Nós são armazenados por tipo em namespaces KV dedicados, permitindo lookups eficientes em uma única partição. Arestas usam dois índices separados:

  • Índice de links: lista de adjacência mapeando nós fonte para vizinhos.
  • Índice de propriedades: armazena propriedades das arestas separadamente.

Essa separação permite upserts eficientes de propriedades sem linhas largas, ao custo de escritas não atômicas entre namespaces.

Estratégias de Cache

  • Cache write-aside de links de arestas reduz amplificação de escrita.
  • Cache read-aside de propriedades (via EVCache) reduz amplificação de leitura.
  • Cache write-through (em andamento) organizará índices por diferentes ordens de classificação.

Garantia de Consistência

A replicação multi-região é assíncrona, resultando em consistência eventual. Um mecanismo de reparo de entropia usa Kafka para retentar escritas com falha entre índices. Exclusões de nós são assíncronas com resolução de conflitos LWW.

API de Travessia

A API gRPC personalizada (inspirada no Gremlin) suporta travessias encadeadas, filtros, ordenação e limitação. Exemplo de query hipotética para recomendar séries em um dispositivo compartilhado:

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

Performance e Métricas Reais

  • Vazão: até 10 milhões de ops/seg no pico.
  • Latência: dígito único para leituras pontuais e travessias de 1 salto (p99).
  • Travessias de 2 saltos (usadas pelo RDG): p90 abaixo de 50ms.
  • Volume de dados: ~650 TB globalmente.

Limitações e Cuidados

  • Consistência eventual é uma troca deliberada por alta disponibilidade. Aplicações devem tolerar leituras desatualizadas de curta duração.
  • Escritas não atômicas entre múltiplos namespaces exigem lógica de retry cuidadosa.
  • Profundidade de travessia limitada para manter latência previsível; travessias profundas (3+ saltos) podem exigir estratégias diferentes.

Próximos Passos

A Parte II desta série vai mergulhar no planejamento e execução de travessias, além de mecanismos de contagem. A Parte III cobrirá o índice temporal e integração com Time Series. Se você está construindo um sistema de grafos de alta vazão, comece com uma separação clara entre armazenamento de links e propriedades, e invista em um planejador de queries orientado a schema.

Leitura Relacionada

Performance metrics dashboard for graph database operations showing p50 p90 p99 latencies Development Concept Image

Conclusão

A Graph Abstraction da Netflix é uma aula de como construir um banco de grafos OLTP sob medida sobre infraestrutura existente. Separando índices de links e propriedades, usando otimizações orientadas a schema e empregando cache em camadas, eles alcançam vazão e latência impressionantes em escala. As trocas — consistência eventual, escritas não atômicas, profundidade de travessia limitada — são claramente documentadas e aceitáveis para seus casos de uso.

Se você está arquitetando um sistema de grafos para workloads OLTP de alta vazão, inspire-se na abordagem da Netflix: não construa tudo do zero; componha abstrações existentes com sabedoria.

Este conteúdo foi elaborado com o auxílio de ferramentas de IA, com base em fontes confiáveis, e revisado pela nossa equipe editorial antes da publicação. Não substitui o aconselhamento de um profissional especializado.