CobbleDB: ricostruzione dell'archiviazione di ricerca IA per latenza e costi ridotti

Un'architettura disaccoppiata con stato persistente separato, aggiornamenti in batch e recupero in fase di query.

AutoriPerplexity Engineering

La ricerca nativa per l'IA impone requisiti diversi su come il contenuto web deve essere elaborato, memorizzato e servito. Per soddisfare tali esigenze su scala, abbiamo ricostruito l'intero stack da zero per migliorare le prestazioni e ridurre i costi.

Abbiamo costruito la nostra infrastruttura di crawling, indicizzazione e ranking, i modelli di incorporamento e i motori di inferenza per alimentare un'elaborazione efficiente e un'erogazione rapida. Stiamo ora estendendo questo approccio all'archiviazione, il tessuto connettivo su cui l'elaborazione scrive e da cui l'erogazione legge.

I database gestiti come DynamoDB fornivano un'archiviazione affidabile, ma avevamo un controllo limitato su come i dati venivano memorizzati e le letture servite, e i prezzi basati sull'utilizzo crescevano con le dimensioni del nostro indice e il volume delle query. Separatamente, la nostra pipeline precedente accoppiava l'elaborazione dei documenti direttamente al database di servizio, rendendo difficile gestire gli aggiornamenti indipendentemente dalle letture live.

Con l'aiuto di un nuovo sciame di agenti di codifica interni, abbiamo ricostruito questo livello attorno a CobbleDB, un archivio hot chiave-valore, supportato da Pillar per lo stato del documento persistente e la pubblicazione, e Lorry per gli aggiornamenti in batch. La riprogettazione separa l'elaborazione dei documenti dall'inserimento nell'archivio hot e ci consente di ottimizzare l'erogazione per il nostro carico di lavoro. Le misurazioni di produzione prima e dopo mostrano una latenza di lettura in batch inferiore di circa 5× e il nostro modello di costo interno stima un risparmio di almeno il 20% rispetto a DynamoDB.

Infrastruttura di archiviazione per la ricerca nativa per l'IA

Uno stack di ricerca classico è costruito per un lettore umano, non per un modello linguistico. Questa differenza modifica la granularità con cui il sistema deve preparare, memorizzare e servire il contenuto web.

Per trasformare HTML grezzo in contenuto pronto per il modello, la nostra pipeline di elaborazione pulisce ogni pagina, la suddivide in passaggi semanticamente coerenti, calcola i relativi incorporamenti e memorizza i passaggi e gli incorporamenti insieme nel database. Al momento della query, la pipeline di servizio seleziona il testo più rilevante per la query e lo passa al modello.

Due flussi di lavoro distinti

Il livello di archiviazione deve quindi supportare due flussi di lavoro con esigenze diverse: l'inserimento di aggiornamenti dalla pipeline di elaborazione e l'erogazione di letture a bassa latenza.

Dal lato della scrittura, le nuove pagine arrivano continuamente, le pagine esistenti cambiano e le modifiche ai metodi di chunking o ai modelli di incorporamento possono richiedere la rielaborazione di grandi porzioni del corpus. Il livello di archiviazione deve incorporare questi aggiornamenti nello stato del documento persistente, tracciare le versioni correnti, pubblicare le rappresentazioni corrette ai sistemi downstream e supportare grandi backfill senza ripetere l'intera pipeline dal crawling grezzo.

Al contrario, le letture in fase di query richiedono batch di chiavi di pagina e recuperano il testo del passaggio corrispondente e gli incorporamenti per il ranking e la selezione del passaggio. Queste letture si trovano sul percorso verso la risposta finale, quindi devono essere completate rapidamente.

Questi carichi di lavoro richiedono design differenti. L'elaborazione trae vantaggio da code persistenti e dalla capacità di consegnare gli aggiornamenti in modo efficiente, ripetere i tentativi in caso di errore e aggiornare i record esistenti su scala. L'erogazione trae vantaggio da record precalcolati, cache calde e letture in batch veloci con un lavoro minimo in fase di query. Uno stretto accoppiamento tra i due può far sì che il lavoro di ripristino e i grandi aggiornamenti competano con le letture sensibili alla latenza.

Dove il database gestito risulta carente

DynamoDB ci ha fornito un'archiviazione altamente disponibile senza l'onere di gestire un database distribuito. Una tabella dedicata di pagine preparate conteneva passaggi e incorporamenti per le letture in fase di query, mentre il contenuto completo della pagina e i metadati venivano memorizzati separatamente. Tuttavia, man mano che il nostro carico di lavoro cresceva, sono diventati importanti tre vincoli.

Primo, i prezzi basati sull'utilizzo addebitano ogni byte letto o scritto. Una singola richiesta dell'API di ricerca contiene 100-120 chiavi corrispondenti alle pagine che dobbiamo recuperare. Il processo di recupero dei dati divide una richiesta in batch più piccoli di 10-20 chiavi di pagina con una dimensione media degli elementi di circa 50 KB. La lettura ripetuta di tali record ha consumato una capacità considerevole, mentre le scansioni continue e le modifiche ai modelli di chunking o di incorporamento hanno generato più scritture. L'economia dipendeva anche dalle dimensioni e dalla frequenza dei record, non solo dal numero di chiamate API.

Secondo, il servizio gestito manca della flessibilità di cui abbiamo bisogno per l'ottimizzazione delle prestazioni. L'operazione di cui avevamo bisogno era limitata: dato un batch di identificatori di pagina, restituire rapidamente i loro contenuti preparati. Volevamo il controllo su quale macchina ospitasse una partizione, quanta memoria fosse dedicata alla sua cache e quale replica gestisse una richiesta. Tali controlli sono importanti perché una lettura non memorizzata nella cache, un salto tra zone o una replica lenta possono ritardare l'intero batch. DynamoDB gestisce la propria archiviazione e il comportamento delle repliche dietro l'API. Potevamo configurare il servizio, ma non potevamo ottimizzare il posizionamento interno, le cache del motore di archiviazione o la policy di selezione delle repliche in base al nostro carico di lavoro.

Terzo, l'elaborazione era legata direttamente alle scritture nell'archivio hot. La nostra precedente pipeline di elaborazione scriveva ogni pagina preparata direttamente su DynamoDB. Un grande processo di rielaborazione diventava quindi un'ondata di singoli aggiornamenti dell'archivio hot, e l'elaborazione doveva adattarsi ai limiti di throughput e al comportamento di ripetizione dell'archivio hot. Non potevamo eseguire MapReduce sull'intero database in 1-2 giorni per provare un nuovo metodo di suddivisione in blocchi, cambiare il nostro modello di incorporamento o aggiungere un nuovo campo. Non esisteva un flusso indipendente di aggiornamenti di pagine salvate che un servizio separato potesse raggruppare in batch e consegnare all'archivio hot al proprio ritmo.

Affrontare questi vincoli richiedeva molto più della semplice sostituzione del database. Dovevamo separare lo stato del documento persistente e la consegna degli aggiornamenti dall'archiviazione in fase di query, ottimizzando poi l'archivio hot specificamente per letture in batch veloci.

Separare lo stato del documento, la consegna degli aggiornamenti e l'erogazione

La nuova architettura assegna ciascuna responsabilità a un componente diverso. Pillar mantiene uno stato del documento persistente e decide cosa pubblicare. Lorry trasforma queste esportazioni in batch partizionati. CobbleDB inserisce i batch e serve record preparati per chiave di pagina.

Prima e dopo la migrazione dell'archiviazione. Pillar e Lorry separano lo stato durevole dei documenti e la consegna degli aggiornamenti dalle letture al momento della query servite da CobbleDB.

CobbleDB: il percorso di lettura ottimizzabile

CobbleDB è il nostro archivio hot ottimizzato per l'erogazione di record preparati al momento della query. È un archivio chiave-valore distribuito: le chiavi sono identificatori di pagina (URL con hashing) e i valori sono la rappresentazione elaborata della pagina (i passaggi pre-suddivisi in chunk e gli incorporamenti vettoriali per chunk).

Il cambiamento importante è il nostro controllo sulla configurazione dell'archiviazione.

I dati sono partizionati e suddivisi tra i nodi di dati. Questo formato distribuito garantisce che il database possa scalare orizzontalmente per accogliere un corpus ennesimo di pagine elaborate. Ciascuna partizione ha tre repliche su tre nodi diversi; se una copia diventa non disponibile, le altre possono continuare a servire le letture.

A ciascun nodo di dati viene assegnato un sottoinsieme delle partizioni. Il nodo memorizza i suoi dati utilizzando RocksDB, un motore di archiviazione chiave-valore incorporato open-source ben adatto a carichi di lavoro a elevata lettura alimentati dall'inserimento in batch.

I dati memorizzati nella cache possono essere serviti dalla memoria, mentre le letture non memorizzate nella cache utilizzano l'archiviazione NVMe locale. Ciò ci offre un modo diretto per ottimizzare l'equilibrio tra memoria e disco per il carico di lavoro osservato, anziché accettare la policy di cache interna di un servizio gestito.

Un router di query stateless esegue l'hashing delle chiavi sulle partizioni e instrada le richieste ai nodi in parallelo. L'architettura include anche un servizio di metadati basato su PostgreSQL e unOBJECT storage persistente.

Questo design supporta i pattern di lettura richiesti dal nostro carico di lavoro. Dopo che una query ha attraversato il recupero e il ranking per ottenere un insieme candidato di pagine rilevanti, l'archivio hot viene interrogato in blocco per il contenuto di tali pagine. Queste richieste di lettura arrivano al router e vengono instradate a un nodo di dati corrispondente. Il router dà la priorità all'invio di richieste a un nodo nella stessa zona di disponibilità per ridurre al minimo la latenza tra zone, se possibile. Ciascun nodo di dati può recuperare il contenuto della pagina utilizzando MultiGet, un'API di lettura in batch di RocksDB progettata per recuperare più chiavi contemporaneamente a bassa latenza. Se un nodo risponde lentamente, il sistema può anche interrogare un'altra replica di partizione su un nodo diverso, migliorando la latenza di coda.

Possiamo anche evitare di implementare funzionalità comuni che il nostro flusso di lavoro non richiede. Ad esempio, la maggior parte dei database offre garanzie di coerenza e supporto per le transazioni, ma il nostro archivio hot non richiede transazioni o repliche sincronizzate. È accettabile che vi sia un breve intervallo tra il momento in cui un documento viene scritto e quello in cui è disponibile per le letture, ed è anche accettabile che una replica inserisca i dati più velocemente di un'altra. L'omissione di funzionalità di cui non abbiamo bisogno riduce il sovraccarico e il costo del nostro sistema.

Un router stateless raggruppa le chiavi di pagina per partizione e legge le repliche in parallelo. Le letture lente possono essere inviate a un'altra replica; la prima risposta per ciascuna partizione viene unita nel risultato.

Pillar e Lorry: il percorso di scrittura persistente

Un livello di archiviazione separato tiene traccia di tutto ciò che esploriamo ed elaboriamo, unendo gli aggiornamenti nello stato del documento con versioni e pubblicando le rappresentazioni necessarie su CobbleDB e sull'indice di ricerca. A questo scopo abbiamo creato Pillar, il nostro livello di pubblicazione e stato dei documenti.

Pillar è basato su YTsaurus. Memorizza i componenti delle pagine (come metadati, chunk e incorporamenti) in famiglie di tabelle YTsaurus separate. Traccia inoltre le versioni di chunk e incorporamenti, consentendo a più rappresentazioni di coesistere senza sovrascriversi a vicenda in modo che i modelli sottostanti possano cambiare. Poiché Pillar e YTsaurus utilizzano l'archiviazione HDD economica, possiamo memorizzare molti più documenti di quanti ne avessimo precedentemente memorizzati in DynamoDB.

Pillar traccia i sottoinsiemi, ovvero gruppi di pagine definiti da policy come pagine fresche o pagine ad alto valore. L'appartenenza al sottoinsieme determina quali aggiornamenti devono essere trasmessi ai componenti downstream come CobbleDB. CobbleDB utilizza un'archiviazione NVMe più costosa, quindi esportiamo solo i documenti che desideriamo effettivamente utilizzare nei risultati di ricerca. Quando il contenuto di una pagina viene aggiornato, o se viene aggiunta o rimossa da un sottoinsieme, Pillar è responsabile dell'associazione in coda del lavoro di esportazione corrispondente.

Le transazioni atomiche di YTsaurus coprono lo stato, gli intenti di esportazione e l'input consumato per garantire la correttezza. Affinché una transazione venga completata con successo, il sistema deve aggiornare lo stato persistente, contrassegnare l'input come contabilizzato e mettere in coda tutte le esportazioni richieste; se la transazione viene interrotta, non viene apportata alcuna modifica a nessun livello.

CobbleDB prevede che gli aggiornamenti siano raggruppati in batch in base alla partizione. Le esportazioni di Pillar vengono convertite in questo formato da un servizio stateless che chiamiamo Lorry. Il suo unico compito è leggere i record di esportazione da una coda persistente allineata alle partizioni, accumularli in file batch per partizione e passarli a CobbleDB.

Tra Lorry e Cobble, un protocollo aggiuntivo separa il piano dei dati (S3) dal piano di controllo. Lorry scrive il batch su S3 con un identificatore univoco e registra il batch all'interno di Cobble. Le repliche di partizione di Cobble interrogano in modo indipendente il batch o l'API del piano di controllo per il batch S3 successivo da inserire e applicano in modo asincrono gli aggiornamenti in batch in ordine cronologico. Ciò significa che un nodo lento o in fase di recupero può mettersi in pari al proprio ritmo, anziché rallentare il resto del sistema.

Pillar pubblica esportazioni durevoli, e Lorry scrive batch partizionati su S3 e li registra nel control plane. Ogni replica di CobbleDB rileva e acquisisce in modo indipendente i propri batch in ordine cronologico.

Il nuovo percorso separa la transazione di elaborazione immediata dall'inserimento nell'archivio hot e fornisce un percorso riproducibile per aggiornamenti incrementali e ricostruzioni complete. Questa divisione del lavoro garantisce che gli aggiornamenti allo stato del documento persistente non interferiscano con la latenza del percorso di lettura.

Validazione

5x più veloce

Dopo la migrazione dell'archivio hot, la latenza mediana di lettura in batch è scesa da 31,4 ms a 5,60 ms. Il p90 è sceso da 56,7 ms a 9,77 ms e il p99 è sceso da 123 ms a 24,2 ms. I rapporti variano da 5,08× a 5,80×, con miglioramenti sia nelle richieste tipiche che nella coda.

Latenza di lettura batch in produzione prima e dopo la migrazione: la P50 è scesa da 31,4 a 5,60 ms, la P90 da 56,7 a 9,77 ms e la P99 da 123 a 24,2 ms. Si tratta di latenze di lettura dell'hot store da traffico reale in momenti diversi, non di richieste abbinate né di latenza di risposta end-to-end.

Questi numeri derivano da letture in batch, in cui ciascuna query richiede circa 10-15 chiavi. La dimensione media dell'elemento del payload è 50 KB. Entrambi i sistemi sono stati interrogati a circa 200k rps, sebbene abbiamo anche eseguito test di carico fino a 500k rps senza alcuna degradazione.

Per isolare il contributo di CobbleDB, tutti gli altri aspetti della configurazione sono stati mantenuti costanti. Tuttavia, questo rimane un confronto osservazionale prima e dopo; i sistemi hanno gestito il traffico di produzione live in momenti diversi anziché le stesse richieste in condizioni controllate. Abbiamo quindi misurato anche le prestazioni di entrambi i sistemi su un benchmark sintetico. Per approssimare le richieste viste in produzione, abbiamo interrogato entrambi con batch di 10-15 chiavi e valori che vanno da 100B a 100 KiB.

Latenza sintetica di lettura batch per batch da 10 e 15 chiavi su dimensioni dei valori da 100 B a 100 KiB. Ogni punto è la media della latenza P50, P90 o P99 a livello di esecuzione di due run per backend e configurazione.

DynamoDB incontra difficoltà in questo contesto perché il suo percorso di lettura gestito non può essere ottimizzato per letture in batch di queste dimensioni. Al contrario, CobbleDB è ottimizzato esattamente per questo scenario, poiché gestiamo il posizionamento, la memorizzazione nella cache e i controlli delle repliche. Raggruppa le chiavi per partizione, esegue le letture in parallelo rispetto alle repliche di partizione e può attenuare le letture lente rispetto ad altre repliche. I nostri risultati dimostrano che queste scelte di progettazione si traducono in letture più veloci in un ambiente di produzione.

Più economico del 20%

CobbleDB è anche notevolmente più economico di DynamoDB. Oltre all'archiviazione, DynamoDB addebita anche ogni byte letto o scritto. Su scala del nostro corpus e del traffico di servizio, questi costi basati sull'utilizzo diventano una parte sostanziale del costo totale. Gli aggiornamenti continui consumano capacità di scrittura, mentre ogni batch di pagine preparate recuperate per una query consuma capacità di lettura.

Abbiamo calcolato il risparmio sui costi in base a stime interne delle dimensioni dell'archiviazione, delle unità di capacità di scrittura e delle unità di capacità di lettura. A ogni livello di impegno, CobbleDB è almeno del 20% più economico di DynamoDB. I risparmi effettivi sono probabilmente ancora superiori, poiché questi risultati non tengono conto dei probabili risparmi di backup derivanti dalla compressione.

Come uno sciame di agenti di IA ci aiuta a costruirlo

La costruzione di CobbleDB e dell'infrastruttura circostante è stata tutt'altro che banale. Il solo database principale di CobbleDB è composto da circa 40.000 righe di Rust e la migrazione ha richiesto anche un lavoro esteso per verificare la correttezza e il comportamento in caso di errore su scala di produzione, integrare CobbleDB sia con i percorsi di elaborazione che di servizio, e transire in sicurezza il traffico live.

Abbiamo accelerato questo sforzo utilizzando un sistema interno che gestisce uno sciame di agenti di codifica. Il sistema è progettato per essere altamente persistente; conserva gli obiettivi del progetto, i rischi attivi, la cronologia del repository e le decisioni precedenti attraverso le Sessioni.

Abbiamo assegnato un collettivo di tali agenti proattivi e sempre attivi al progetto CobbleDB. Il loro primo compito è stato quello di costruire un modello accurato del lavoro già in corso. Hanno verificato il canale del progetto e i thread attivi, collegato gli elementi in sospeso alle loro pull request e ai proprietari, e prodotto una lettura aggiornata dello stato del sistema e delle azioni successive. Da lì, hanno operato in diverse parti del ciclo di ingegneria:

  • Revisionati codice e modifiche all'infrastruttura.
  • Rilevati elementi di blocco facili da trascurare, tra cui presupposti di ripristino non sicuri, riferimenti di build obsoleti e configurazioni che avrebbero generato errori a runtime.
  • Preparate correzioni mirate, test, modifiche al monitoraggio e documentazione operativa.
  • Tracciati i controlli di CI, revisione e infrastruttura, mantenendo al contempo le azioni di produzione esplicitamente di proprietà umana.
  • Rollout monitorati ed esercitazioni di backup o ripristino, confrontando quindi lo stato del sistema osservato con i criteri di successo del rollout.
  • Mantenuti i resoconti di progetto in modo che gli ingegneri potessero vedere cosa era stato rilasciato, cosa rimaneva bloccato e dove era ancora richiesto il giudizio umano.

Gli ingegneri impostano ancora l'architettura, esaminano le modifiche consequenziali e autorizzano le operazioni di produzione. Gli agenti hanno gestito gran parte dell'ispezione continua e del follow-up tra queste decisioni. Ciò ha consentito a un piccolo team di guidare il progetto in modo efficiente: l'infrastruttura principale di CobbleDB è stata costruita da due ingegneri umani e centinaia di agenti di IA in due mesi.

Conclusione

CobbleDB non è un database a uso generico; è costruito appositamente per il nostro specifico carico di lavoro di letture ripetute in batch di record di pagine preparate, alimentato da una pipeline di elaborazione asincrona. Separando lo stato persistente, la consegna degli aggiornamenti e la fornitura in fase di query, possiamo ottimizzare ciascuna responsabilità in modo indipendente senza costringere una singola interfaccia di database ad accoglierle tutte o spingere il lavoro di manutenzione sul percorso di query live. La migrazione a CobbleDB ha migliorato significativamente sia la latenza mediana che quella di coda, riducendo al contempo i costi dell'infrastruttura.

Continuando a sviluppare internamente un'infrastruttura nativa per l'IA, possiamo ottimizzare le prestazioni e i costi in modi non possibili quando siamo vincolati a implementazioni e modelli di prezzo di terze parti. Potenti agenti di IA spostano ulteriormente il calcolo a favore della creazione rispetto all'acquisto.

Prevediamo di rendere open-source CobbleDB a breve, in modo che altri team che costruiscono sistemi di ricerca nativi per l'IA possano trarre vantaggio dallo stesso livello di archiviazione che rende la ricerca più veloce ed economica all'interno dei nostri prodotti.

Citazione

BibTeX
@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}
}