NEWS Anunciamos US$ 3,2M em rodada pré-seed · OneVC · Maya · Norte Ventures Leia →
Voltar ao blog

A escolha técnica do motor de grafo:
custo e capacidade em volume

TL;DR: Submetemos dois motores de grafo ao mesmo alvo, 200 milhões de nós e 100 milhões de arestas, a partir da mesma fonte federada e da mesma ontologia. O banco híbrido completou os 300 milhões de elementos em 17h11 de ingestão, com 29,7 GiB de RAM fixos, 184 GB de disco e busca pontual entre 1,2 e 3,9 ms, por US$ 353 por mês. O banco em memória serviu 249,3 milhões de elementos, o que coube nos 175 GiB da instância testada, com busca pontual de 0,16 a 0,35 ms e custo de US$ 1.331 por mês. A régua de corte que adotamos ficou em torno de 5 milhões de elementos.

Por que essa escolha é a última milha do Company Brain

O grafo é a camada que os agentes efetivamente consultam. Antes dele existe a ontologia, um arquivo graph_mapping.yaml versionado por cliente, que declara quais tabelas viram nós, quais colunas viram arestas e quais propriedades cada entidade carrega. O Memory Worker lê essa ontologia e materializa o grafo, servido depois via Cypher e MCP.

As fontes podem chegar de dois jeitos: pela camada clean do lakehouse ou por federação, lendo o sistema de origem em lugar, sem copiar. Nesta campanha usamos federação. O objetivo não era medir ingestão de lake, era descobrir onde cada motor de grafo para, quanto custa até lá e o que entrega de leitura.

O cenário

A base veio do Databricks, sob Unity Catalog, lida por federação sem materializar nada em raw ou clean. São 100 milhões de usuários e 100 milhões de tickets, um ticket por usuário, com Z-order aplicado por id e por ticket_id, o que organiza fisicamente os arquivos pela coluna de busca. O leitor federado fatia a origem por faixas de chave e pede uma consulta por fatia, com FEDERATED_SLICE_ROWS em 250 mil linhas.

O grafo alvo é raso e largo: 200 milhões de nós, 100 milhões de arestas, uma única relação. O desenho é deliberado, para medir travessia em vez de complexidade de modelo.

A ontologia cabe em uma tela. User vem da tabela federada de usuários, identificado por id. Ticket vem da de tickets, identificado por ticket_id. A aresta OPENED_BY é construída com duas colunas da própria linha do ticket, ticket_id como origem e user_id como destino, sem precisar consultar a tabela de usuários.

Mesma fonte, mesma ontologia, mesmo pipeline, dois motores com filosofias opostas de memória.

Os dois motores, lado a lado

DimensãoFalkorDB 4.4.1, banco em memóriaNeo4j 5.26 CE, banco híbrido
Elementos servidos249,3M, limite da instância testada300M, alvo completo
Nós e arestas174,6M nós · 74,6M arestas200M nós · 100M arestas
Tempo de carga27h08 nas duas fases de ingestão17h11 nas duas fases de ingestão
Modelo de memóriaemergente, cresce com o dadoconfigurada, teto fixo
RAM em regime175 GiB29,7 GiB
Disco do motor119 GB, só durabilidade184 GB, é o caminho de leitura
Busca pontual p500,16–0,35 ms1,2–3,9 ms
Vazão de leitura~4.100 qps~1.000 qps
p95 com 40 simultâneas11,1 ms57,7 ms
Instânciar7g.8xlarge, 32 vCPU, 256 GiBr7g.2xlarge, 8 vCPU, 61 GiB
Infra mensalUS$ 1.331US$ 353

A diferença de volume servido entre os dois vem da linha de memória, e é ela que explica todo o resto.

Dois modelos de memória, duas contas diferentes

Memória emergente. No banco em memória, a RAM é o próprio dado. A curva medida em 15,8 mil amostras de 15 segundos mostrou 0,92 GiB por milhão de nós na fase de usuários e 1,13 GiB por milhão de pares nó mais aresta na fase de tickets. A média de 0,70 GiB por milhão de elementos é a régua de capacity planning que este benchmark estabeleceu. Cada crescimento relevante do grafo vira uma decisão de instância.

Memória configurada. No banco híbrido, a RAM é definida antes da carga: 8 GiB de heap e 20 GiB de page cache, com o consumo total parando em 29,7 GiB. De 40 milhões para 200 milhões de nós, cinco vezes mais dado, a memória andou 1 GiB. O que cresce é o disco, cerca de 920 bytes por nó, o que leva os 300 milhões de elementos para 184 GB de EBS.

Os 29,7 GiB medidos se dividem assim: 8 GiB reservados de heap, com uso oscilando entre 1,7 e 6,1 GiB, 20 GiB fixos de page cache guardando a parte quente do grafo, principalmente índices, uma fatia de 130 MiB de pilhas de thread e buffers nativos, e o restante como sobrecarga da própria JVM. A fatia nativa é a única sem teto configurável e incha durante os checkpoints, por isso o contêiner precisa ser dimensionado com folga sobre a soma de heap e page cache.

O disco que ninguém orça: o registro de entidades

Os dois motores apoiam-se em um Postgres que guarda o cadastro de entidades e o índice de resolução, e ele não é pequeno. Na campanha do híbrido somou 80 GB; na do banco em memória, 73 GB, divididos entre 29 GB de registro e 43 GB de índice.

Somando motor e registro, a carga do híbrido ocupou cerca de 264 GB. O log de transação ficou em 1,3 GB porque havia política de retenção configurada; sem ela, teria passado de 200 GB durante a carga.

Esses dois números costumam ficar fora do dimensionamento inicial e aparecem como incidente de disco no meio da primeira carga grande.

O peso do nó decide a conta, não o número de elementos

Bytes por elemento não é constante do motor, é consequência do modelo. Duas medições nossas, com a mesma tecnologia:

CenárioElementosBytes por elemento
Nós enxutos, só id e nome21M~126
Mistura de nós e arestas com propriedades249,3M~750

A diferença é quanta propriedade cada nó carrega. O efeito na conta de AWS é direto. Pela régua de 0,70 GiB por milhão, 150 milhões de elementos com propriedades pedem cerca de 105 GiB de RAM, que com folga para consulta e para o fork de persistência empurram a escolha para a classe de 256 GiB. Os mesmos 150 milhões com nós enxutos ficam perto de 18 GiB e cabem em uma instância de 32 GB.

A decisão de modelagem que parece inofensiva, colocar mais três atributos em cada nó porque "pode ser útil depois", é uma decisão de infraestrutura. O critério que adotamos é levar para o nó apenas o que a travessia precisa e manter o resto na origem, recuperado sob demanda.

Ingestão custa uma vez, leitura custa todo dia

As 17 e 27 horas de carga assustam quando vistas isoladas. Elas são eventos únicos, e o regime permanente é a sincronização incremental, que processa apenas as mudanças desde a última execução.

Dentro da carga, aresta custa mais que nó. No híbrido, a fase de tickets levou 10h48 contra 6h23 da fase de usuários, porque cada aresta faz duas buscas no índice, verifica se o relacionamento já existe, grava o registro e atualiza as listas encadeadas dos dois nós. No banco em memória, a mesma assimetria apareceu: 3.188 linhas por segundo nos usuários contra 1.126 na fase de tickets com arestas.

Duas observações operacionais da campanha. O watermark usado foi created_at, e para tabelas mutáveis a recomendação é updated_at ou configuração direta no conector, sob risco de perder atualização de registro existente. E o lote de escrita no grafo, MEMORY_WORKER_BATCH_SIZE, é o tamanho da transação: ele é independente da fatia de leitura federada e é o parâmetro que mais mexe no tempo total de carga.

Busca pontual, que é o que o agente faz o dia inteiro

Busca pontual é a consulta que entra direto em uma entidade conhecida, pelo identificador indexado, e navega dali para os vizinhos. É a pergunta "quem abriu este ticket" e a pergunta "o que esta conta tem em aberto". Em atendimento e em agentes conversacionais, ela é o padrão dominante, e a latência dela não deveria variar com o tamanho do grafo.

CenárioBanco em memóriaBanco híbrido
Lookup por entidade indexada0,16 ms1,3 ms
Ticket e quem o abriu0,35 ms3,9 ms
Histórico do usuário0,21 ms3,1 ms
Contagem total de nós6,0 ms1,3 ms
Lista filtrada com LIMIT 3268 ms1,8 ms

Os dois entregam milissegundos na busca pontual, com uma ordem de grandeza entre eles, e a latência se manteve insensível ao volume nos dois casos.

Sob concorrência, a distância aumenta:

SimultâneasEm memória p50 / p95Híbrido p50 / p95
10,35 / 0,52 ms2,8 / 6,3 ms
101,99 / 3,89 ms8,7 / 19,5 ms
4010,36 / 11,12 ms35,9 / 57,7 ms

O pico de vazão ficou em cerca de 4.100 consultas por segundo no banco em memória e 1.000 no híbrido. Na prática, 1.000 consultas por segundo equivalem a 2,6 bilhões de consultas por mês, bem acima do que uma operação de atendimento consome.

E há o detalhe que supera qualquer escolha de motor. A mesma busca com índice leva 1,3 ms no híbrido e 9.520 ms sem índice, uma diferença de 7.300 vezes. No banco em memória, 0,16 ms contra 10.777 ms. Busca pontual só é pontual se a entidade estiver indexada; sem índice, o motor varre o grafo inteiro e qualquer vantagem de arquitetura desaparece.

A régua de corte que adotamos

A plataforma alterna entre os dois por configuração, com GRAPH_BACKEND, sem mudança de código.

Até cerca de 5 milhões de elementos: banco em memória, como vem. Cabe na fatia de memória da VM inicial, entrega leitura sub-milissegundo e não exige operação adicional. É a situação da maioria dos clientes na largada.

Acima disso: banco híbrido. A memória do motor em memória cresce 0,70 GiB por milhão de elementos, o que obriga a subir de instância a cada crescimento e chega a US$ 1.331 por mês na casa dos 250 milhões. O híbrido segura de 10 milhões a 300 milhões com a mesma RAM configurada e escala em disco, que é barato e elástico.

A validação do corte são os US$ 353 mensais que serviram 300 milhões de elementos com busca pontual entre 1 e 4 ms, suficiente para atendimento e agentes. Com savings plan de um ano esse valor cai para cerca de US$ 230, e para US$ 165 em três anos.

As configurações que sustentam esses números

ParâmetroBanco em memóriaBanco híbrido
Contêiner200 GiB40 GB
Teto de memória do motor180 GiB, noevictionheap 8 GB + page cache 20 GB
PersistênciaAOF e snapshotscheckpoints, log com retenção de 1 GB
Worker de ingestão12 GiB12 GiB
Limite do DuckDB6 GB6 GB
Lote de escrita no grafo40.00040.000
Fatia de leitura federada250.000250.000

Dois limites merecem atenção. O contêiner precisa ficar acima de heap mais page cache, porque a JVM usa memória além dos dois. E o limite do DuckDB precisa ser declarado, senão ele se dimensiona pelo host e não pelo contêiner.

Seis aprendizados que levamos para a construção do Company Brain

  1. O grafo é projeção derivada, não fonte da verdade. Ele é reconstruído de forma determinística a partir da fonte cruzada com uma versão da ontologia, o que permite trocar de motor sem perder nada.
  2. O peso do nó define a fatura. Bytes por elemento variaram de 126 a 750 nas nossas medições, e essa diferença muda a classe de instância.
  3. Latência de leitura é o requisito de produto. Carga acontece uma vez, consulta acontece o dia inteiro, e o padrão real é busca pontual em entidade conhecida.
  4. Índice não é detalhe de afinação. A diferença medida foi de três a quatro ordens de grandeza.
  5. Orce o registro de entidades junto com o motor. Ele somou 73 e 80 GB de Postgres nas duas campanhas, na mesma ordem de grandeza do próprio grafo.
  6. Escolha do watermark é decisão de corretude. created_at não captura atualização de registro existente, e em tabela mutável isso vira dado velho no grafo sem nenhum erro aparente.

Escopo e limitações do estudo

Os dados são sintéticos e deterministas, o que torna os resultados reprodutíveis, e o código exercitado foi o de produção, com os mesmos conectores, o mesmo leitor federado e o mesmo MemoryWorkerPipeline.

O grafo testado tem uma única relação e um ticket por usuário. Grafos com muitas relações por nó mudam tanto o custo de travessia quanto o peso por elemento, então os números valem como régua de dimensionamento e não como garantia.

O dado é limpo por construção. Uma rodada com dado sujo, incluindo referências quebradas, entidades duplicadas e campos malformados, está planejada, assim como cenários de leitura com mais usuários simultâneos do que os testados aqui.

Perguntas frequentes

Por que não usar sempre o motor mais rápido?

Porque o mais rápido na leitura é o mais caro na memória. A 249,3 milhões de elementos, o banco em memória exigiu uma instância de US$ 1.331 por mês, contra US$ 353 do híbrido servindo 300 milhões de elementos com latência adequada para agentes.

Trocar de motor exige refazer a ingestão da fonte?

Não. A ontologia e o pipeline são os mesmos, e a troca é de configuração. O que precisa ser refeito é a carga do grafo, que é reconstruída a partir da fonte.

Dá para começar pequeno e crescer sem migração?

Sim, e é o desenho padrão. Até cerca de 5 milhões de elementos o grafo roda embarcado na VM inicial, e a mudança de motor acontece por configuração quando o volume passar disso.

Por que federar em vez de copiar os dados?

Neste caso, para isolar a medição do motor de grafo. Quando a empresa já tem lakehouse ou warehouse, a federação também evita duplicar dado que já está governado na origem.

O que ainda não foi medido?

Comportamento com dado sujo e leitura sob concorrência acima dos cenários testados. Ambos são próximos passos.

Dimensione o grafo para o seu volume.

Fale com um especialista da Strattum sobre a escolha de motor de grafo, custo de memória e capacidade para o seu caso.