CobbleDB: Reconstruindo o armazenamento de pesquisa de IA para menor latência e custo
Uma arquitetura desacoplada com estado durável separado, atualizações em lote e recuperação em tempo de consulta.
A pesquisa nativa de IA impõe requisitos diferentes sobre como o conteúdo da web precisa ser processado, armazenado e servido. Para atender a essas demandas em escala, temos reconstruído toda a pilha do zero para melhorar o desempenho e reduzir custos.
Construímos nossa própria infraestrutura de rastreamento, indexação e ranqueamento, modelos de incorporação e mecanismos de inferência para impulsionar o processamento eficiente e o atendimento rápido. Agora estamos estendendo essa abordagem para o armazenamento, o tecido conjuntivo para o qual o processamento grava e do qual o atendimento lê.
Bancos de dados gerenciados, como o DynamoDB, forneciam armazenamento confiável, mas tínhamos controle limitado sobre como os dados eram armazenados e as leituras eram servidas, e os preços baseados no uso cresciam com o tamanho do nosso índice e o volume de consultas. Separadamente, nosso pipeline anterior acoplava o processamento de documentos diretamente ao banco de dados de atendimento, dificultando o gerenciamento de atualizações independentemente das leituras ativas.
Com a ajuda de um novo enxame interno de agentes de codificação, reconstruímos esta camada em torno do CobbleDB, um armazenamento quente de chave-valor, com suporte do Pillar para o estado e publicação de documentos duráveis, e do Lorry para atualizações em lote. O redesenho separa o processamento de documentos da ingestão do armazenamento quente e nos permite ajustar o atendimento à nossa carga de trabalho. As medições de antes e depois da produção mostram uma latência de leitura em lote cerca de 5 vezes menor, e nosso modelo de custo interno estima uma economia de pelo menos 20% em relação ao DynamoDB.
Infraestrutura de armazenamento para pesquisa nativa de IA
Uma pilha de pesquisa clássica é construída para um leitor humano, não para um modelo de linguagem. Essa diferença altera a granularidade com que o sistema precisa preparar, armazenar e servir o conteúdo da web.
Para transformar HTML bruto em conteúdo pronto para modelos, nosso pipeline de processamento limpa cada página, divide-a em passagens semanticamente coerentes, calcula suas incorporações e armazena as passagens e as incorporações juntas no banco de dados. No momento da consulta, o pipeline de atendimento seleciona o texto mais relevante para a consulta e o passa para o modelo.
Dois fluxos de trabalho distintos
Portanto, a camada de armazenamento deve dar suporte a dois fluxos de trabalho com demandas diferentes: ingerir atualizações do pipeline de processamento e servir leituras de baixa latência.
Do lado da gravação, novas páginas chegam continuamente, as páginas existentes mudam e alterações nos métodos de divisão em blocos ou nos modelos de incorporação podem exigir o reprocessamento de grandes partes do corpus. A camada de armazenamento deve incorporar essas atualizações no estado durável do documento, rastrear as versões atuais, publicar as representações corretas para os sistemas a jusante e dar suporte a grandes preenchimentos retroativos sem repetir todo o pipeline a partir do rastreamento bruto.
Em contrapartida, as leituras em tempo de consulta solicitam lotes de chaves de página e recuperam o texto da passagem correspondente e as incorporações para ranqueamento e seleção de passagens. Essas leituras estão no caminho para a resposta final, portanto, precisam ser concluídas rapidamente.
Essas cargas de trabalho exigem designs diferentes. O processamento se beneficia de filas duráveis e da capacidade de entregar atualizações de forma eficiente, repetir falhas e atualizar registros existentes em escala. O atendimento se beneficia de registros pré-computados, caches aquecidos e leituras em lote rápidas com o mínimo de trabalho em tempo de consulta. O acoplamento rígido entre os dois pode fazer com que o trabalho de recuperação e grandes atualizações concorram com leituras sensíveis à latência.
Onde o banco de dados gerenciado fica aquém
O DynamoDB nos forneceu armazenamento altamente disponível sem o ônus de operar um banco de dados distribuído. Uma tabela dedicada de páginas preparadas continha passagens e incorporações para leituras em tempo de consulta, enquanto o conteúdo completo da página e os metadados eram armazenados separadamente. No entanto, à medida que nossa carga de trabalho crescia, três restrições tornaram-se importantes.
Segundo, a precificação baseada no uso cobra por cada byte lido ou gravado. Uma única solicitação da API de pesquisa contém de 100 a 120 chaves correspondentes às páginas que precisamos buscar. O processo de recuperação de dados divide uma solicitação em lotes menores de 10 a 20 chaves de página, com o tamanho médio do item em cerca de 50 KB. A leitura repetida desses registros consumiu capacidade substancial, enquanto rastreamentos contínuos e alterações nos modelos de divisão em blocos ou incorporação geraram mais gravações. A economia também dependia do tamanho e da frequência dos registros, e não apenas do número de chamadas de API.
Primeiro, o serviço gerenciado carece da flexibilidade de que precisamos para o ajuste de desempenho. A operação de que precisábamos era restrita: dado um lote de identificadores de página, retornar seus conteúdos preparados rapidamente. Queríamos controle sobre qual máquina mantinha uma partição, quanta memória era dedicada ao seu cache e qual réplica lidava com uma solicitação. Esses controles importam porque uma leitura não em cache, um salto entre zonas ou uma réplica lenta podem atrasar todo o lote. O DynamoDB gerencia seu próprio comportamento de armazenamento e réplica por trás da API. Podíamos configurar o serviço, mas não podíamos ajustar seu posicionamento interno, caches do mecanismo de armazenamento ou política de seleção de réplicas em torno da nossa carga de trabalho.
Terceiro, o processamento estava vinculado diretamente às gravações no armazenamento quente. Nosso pipeline de processamento anterior gravava cada página preparada diretamente no DynamoDB. Um grande trabalho de reprocessamento, portanto, tornava-se uma onda de atualizações individuais de armazenamento quente, e o processamento precisava acomodar os limites de taxa de transferência e o comportamento de repetição do armazenamento quente. Não podíamos executar o MapReduce em todo o banco de dados em 1 a 2 dias para tentar um novo método de agrupamento, alterar nosso modelo de incorporação ou adicionar um novo campo. Não havia um fluxo independente de atualizações de páginas salvas que um serviço separado pudesse agrupar e entregar ao armazenamento quente em seu próprio ritmo.
Abordar essas restrições exigiu mais do que substituir o banco de dados. Precisábamos separar o estado durável do documento e a entrega de atualizações do armazenamento em tempo de consulta, e então otimizar o armazenamento quente especificamente para leituras em lote rápidas.
Separar o estado do documento, a entrega de atualizações e o atendimento
A nova arquitetura atribui cada responsabilidade a um componente diferente. O Pillar mantém um estado de documento durável e decide o que publicar. O Lorry transforma essas exportações em lotes particionados. O CobbleDB ingere os lotes e atende aos registros preparados por chave de página.
CobbleDB: o caminho de leitura ajustável
O CobbleDB é o nosso armazenamento quente otimizado para servir registros preparados em tempo de consulta. É um repositório distribuído de chave-valor: as chaves são identificadores de página (URLs com hash) e os valores são a representação processada da página (suas passagens pré-divididas em blocos e incorporações vetoriais por bloco).
A alteração importante é o nosso controle sobre a configuração de armazenamento.
Os dados são particionados e divididos entre nós de dados. Esse formato distribuído garante que o banco de dados possa escalar horizontalmente para acomodar um enorme corpus de páginas processadas. Cada partição tem três réplicas em três nós diferentes; se uma cópia ficar indisponível, as outras poderão continuar servindo leituras.
Cada nó de dados recebe um subconjunto das partições. O nó armazena seus dados usando o RocksDB, um mecanismo de armazenamento de chave-valor incorporado de código aberto adequado para cargas de trabalho com uso intenso de leitura alimentadas por ingestão em lote.
Os dados em cache podem ser servidos a partir da memória, enquanto as leituras não em cache usam armazenamento NVMe local. Isso nos dá uma maneira direta de ajustar o equilíbrio entre memória e disco para a carga de trabalho observada, em vez de aceitar a política de cache interna de um serviço gerenciado.
Um roteador de consultas sem estado aplica hash às chaves para partições e direciona solicitações para nós em paralelo. A arquitetura também inclui um serviço de metadados respaldado por PostgreSQL e armazenamento de objetos durável.
Este design suporta os padrões de leitura exigidos pela nossa carga de trabalho. Depois que uma consulta passa por recuperação e ranqueamento para obter um conjunto candidato de páginas relevantes, o armazenamento quente é consultado em massa para o conteúdo dessas páginas. Essas solicitações de leitura chegam ao roteador e são direcionadas a um nó de dados correspondente. O roteador prioriza o envio de solicitações a um nó na mesma zona de disponibilidade para minimizar a latência entre zonas, se possível. Cada nó de dados pode buscar o conteúdo da página usando o MultiGet, uma API de leitura em lote do RocksDB projetada para buscar várias chaves simultaneamente com baixa latência. Se um nó demorar para responder, o sistema também poderá consultar outra réplica de partição em um nó diferente, melhorando a latência de cauda.
Também podemos evitar a implementação de recursos comuns que nosso fluxo de trabalho não exige. Por exemplo, a maioria dos bancos de dados oferece garantias de consistência e suporte a transações, mas nosso armazenamento quente não exige transações ou réplicas sincronizadas. É aceitável que haja uma pequena lacuna entre o momento em que um documento é gravado e o momento em que fica disponível para leitura, e também é aceitável que uma réplica faça a ingestão de dados mais rápido do que outra. Omitir recursos de que não precisamos reduz a sobrecarga e o custo do nosso sistema.
Pillar e Lorry: o caminho de gravação durável
Uma camada de armazenamento separada rastreia tudo o que rastreamos e processamos, mesclando atualizações em um estado de documento versionado e publicando as representações necessárias no CobbleDB e no índice de pesquisa. Construímos o Pillar, nossa camada de publicação e estado de documento, para essa finalidade.
O Pillar é sustentado pelo YTsaurus. Ele armazena componentes de páginas (como metadados, blocos e incorporações) em famílias de tabelas do YTsaurus separadas. Ele também rastreia as versões de blocos e incorporações, permitindo que várias representações coexistam sem sobrescrever umas às outras, para que os modelos subjacentes possam mudar. Como o Pillar e o YTsaurus usam armazenamento HDD barato, podemos armazenar muito mais documentos do que armazenávamos anteriormente no DynamoDB.
O Pillar rastreia subconjuntos, ou grupos de páginas definidos por políticas, como páginas recentes ou páginas de alto valor. A associação a subconjuntos determina quais atualizações precisam ser repassadas para componentes a jusante, como o CobbleDB. O CobbleDB usa armazenamento NVMe mais caro, portanto, exportamos apenas os documentos que realmente queremos usar nos resultados de pesquisa. Quando o conteúdo de uma página é atualizado, ou se ela é adicionada ou removida de um subconjunto, o Pillar é responsável por enfileirar o trabalho de exportação correspondente.
As transações atômicas do YTsaurus cobrem o estado, as intenções de exportação e a entrada consumida para garantir a exatidão. Para que uma transação seja concluída com sucesso, o sistema deve atualizar o estado durável, marcar a entrada como contabilizada e enfileirar todas as exportações necessárias; se a transação for abortada, nenhuma alteração será feita em nenhuma camada.
O CobbleDB espera que as atualizações sejam agrupadas em lotes de acordo com a partição. As exportações do Pillar são convertidas para esse formato por um serviço sem estado que chamamos de Lorry. Sua única função é ler registros de exportação de uma fila persistente alinhada por partição, acumulá-los em arquivos de lote por partição e entregá-los ao CobbleDB.
Entre o Lorry e o Cobble, um protocolo adicional separa o plano de dados (S3) do plano de controle. O Lorry grava o lote no S3 sob um identificador exclusivo e registra o lote dentro do Cobble. As réplicas de partição do Cobble consultam independentemente a API do plano de controle ou de dados para o próximo lote do S3 a ser ingerido e aplicam assincronamente as atualizações em lote em ordem cronológica. Isso significa que um nó lento ou em recuperação pode se atualizar em seu próprio ritmo, em vez de desacelerar o resto do sistema.
O novo caminho separa a transação de processamento imediato da ingestão do armazenamento quente e fornece um caminho reproduzível para atualizações incrementais e reconstruções em massa. Essa divisão de trabalho garante que as atualizações no estado durável do documento não interfiram na latência do caminho de leitura.
Validação
5x mais rápido
Após migrar o armazenamento quente, a latência mediana de leitura em lote caiu de 31,4 ms para 5,60 ms. O p90 caiu de 56,7 ms para 9,77 ms, e o p99 caiu de 123 ms para 24,2 ms. As proporções variam de 5,08× a 5,80×, com melhorias tanto nas solicitações típicas quanto na cauda.
Esses números são derivados de leituras em lote, onde cada consulta solicita cerca de 10 a 15 chaves. O tamanho médio do item de payload é de 50 KB. Ambos os sistemas foram consultados a cerca de 200 mil rps, embora também tenhamos executado testes de carga de até 500 mil rps sem degradação.
Para isolar a contribuição do CobbleDB, todos os outros aspectos da configuração foram mantidos constantes. No entanto, esta continua sendo uma comparação observacional de antes e depois; os sistemas atenderam ao tráfego de produção real em momentos diferentes, em vez de atender às mesmas solicitações em condições controladas. Portanto, também medimos o desempenho de ambos os sistemas em um benchmark sintético. Para aproximar as solicitações vistas em produção, consultamos ambos com lotes de 10 a 15 chaves e valores variando de 100B a 100 KiB.
O DynamoDB tem dificuldades nesse contexto porque seu caminho de leitura gerenciado não pode ser ajustado para leituras em lote desse tamanho. Em contrapartida, o CobbleDB é otimizado exatamente para esse cenário, já que gerenciamos o posicionamento, o cache e os controles de réplica. Ele agrupa chaves por partição, executa leituras em paralelo contra réplicas de partição e pode proteger contra leituras lentas usando outras réplicas. Nossos resultados demonstram que essas escolhas de design se traduzem em leituras mais rápidas em um ambiente de produção.
20% mais barato
O CobbleDB também é significativamente mais barato que o DynamoDB. Além do armazenamento, o DynamoDB também cobra por cada byte lido ou gravado. Na escala do nosso corpus e do tráfego de atendimento, essas cobranças baseadas no uso tornam-se uma parte substancial do custo total. Atualizações contínuas consomem capacidade de gravação, enquanto cada lote de páginas preparadas buscado para uma consulta consome capacidade de leitura.
Calculamos a economia de custos com base em estimativas internas de tamanho de armazenamento, unidades de capacidade de gravação e unidades de capacidade de leitura. Em todos os níveis de compromisso, o CobbleDB é pelo menos 20% mais barato que o DynamoDB. A economia real provavelmente é ainda maior, já que esses resultados não levam em conta a provável economia de backup obtida com a compactação.
Como um enxame de agentes de IA nos ajuda a construí-lo
Construir o CobbleDB e a infraestrutura ao redor não foi nada trivial. Apenas o banco de dados principal do CobbleDB tem cerca de 40.000 linhas de Rust, e a migração também exigiu um trabalho extensivo para verificar a correção e o comportamento de falha em escala de produção, integrar o CobbleDB com os caminhos de processamento e atendimento, e fazer a transição segura do tráfego ativo.
Aceleramos esse esforço usando um sistema interno que opera um enxame de agentes de codificação. O sistema foi projetado para ser altamente persistente; ele retém os objetivos do projeto, riscos ativos, histórico do repositório e decisões anteriores entre sessões.
Atribuímos um coletivo desses agentes proativos e sempre ativos ao projeto CobbleDB. A primeira tarefa deles foi construir um modelo preciso do trabalho já em andamento. Eles auditaram o canal do projeto e os tópicos ativos, conectaram itens pendentes aos seus pull requests e proprietários, e produziram uma leitura atual da integridade do sistema e das próximas ações. A partir daí, eles operaram em várias partes do loop de engenharia:
- Revisou o código e as alterações de infraestrutura.
- Trouxe à tona bloqueadores fáceis de perder, incluindo premissas de restauração inseguras, referências de compilação obsoletas e configurações que falhariam em tempo de execução.
- Preparou correções focadas, testes, alterações de monitoramento e documentação operacional.
- Controlou os portões de CI, revisão e infraestrutura, mantendo as ações de produção explicitamente sob responsabilidade humana.
- Implantações monitoradas e exercícios de backup ou restauração, comparando o estado observado do sistema com os critérios de sucesso da implantação.
- Manteve relatórios de projeto atualizados para que os engenheiros pudessem ver o que havia sido entregue, o que permanecia bloqueado e onde o julgamento humano ainda era necessário.
Os engenheiros ainda definem a arquitetura, revisam alterações consequenciais e autorizam operações de produção. Os agentes lidaram com grande parte da inspeção contínua e do acompanhamento entre essas decisões. Isso permitiu que uma pequena equipe conduzisse o projeto com eficiência — a infraestrutura principal do CobbleDB foi construída por dois engenheiros humanos e centenas de agentes de IA em dois meses.
Conclusão
O CobbleDB não é um banco de dados de uso geral; ele foi criado especificamente para a nossa carga de trabalho de leituras em lote repetidas de registros de páginas preparadas, alimentadas por um pipeline de processamento assíncrono. Ao separar o estado durável, a entrega de atualizações e o atendimento em tempo de consulta, podemos otimizar cada responsabilidade de forma independente, sem forçar uma única interface de banco de dados a acomodar todas elas ou empurrar o trabalho de manutenção para o caminho de consulta ativa. A migração para o CobbleDB melhorou significativamente a latência mediana e de cauda, reduzindo os custos de infraestrutura.
À medida que continuamos a desenvolver infraestrutura nativa de IA internamente, podemos otimizar o desempenho e o custo de maneiras que não são possíveis quando estamos vinculados a implementações e modelos de preços de terceiros. Agentes de IA poderosos mudam ainda mais o cálculo a favor da construção em vez da compra.
Planejamos abrir o código-fonte do CobbleDB em breve, para que outras equipes que constroem sistemas de pesquisa nativos de IA possam se beneficiar da mesma camada de armazenamento que torna a pesquisa mais rápida e barata em nossos produtos.
Citação
@online{perplexity2026Cobbledb,
author = {{Perplexity} and Alexandr Yarats and Marco Montagna and Aleksei Bykov and Alexey Gusev and Ali Abdullaev and Amir Mokhammed-Ali and Andrey Zosimov and Denis Kremko and Gleb Vishnevsky and Micah Zirn and Phi Hoang and Claire Beamer and Jeremy Yang and Jerry Ma and Denis Yarats},
title = {{CobbleDB: Rebuilding AI search storage for lower latency and cost}},
date = {2026-09-14},
url = {https://www.perplexity.ai/hub/blog/cobbledb}
}