CobbleDB: Przebudowa pamięci masowej wyszukiwania AI dla mniejszych opóźnień

Rozdzielona architektura z oddzielonym trwałym stanem, aktualizacjami wsadowymi i pobieraniem w czasie zapytań.

AutorzyPerplexity Engineering

Wyszukiwanie oparte na sztucznej inteligencji stawia odmienne wymagania co do sposobu przetwarzania, przechowywania i obsługi treści internetowych. Aby sprostać tym wymaganiom na dużą skalę, przebudowaliśmy cały stos od zera, aby poprawić wydajność i obniżyć koszty.

Zbudowaliśmy własną infrastrukturę indeksowania, pozycjonowania i rankingu, modele wektorowe oraz silniki wnioskowania, aby zapewnić wydajne przetwarzanie i szybką obsługę. Obecnie rozszerzamy to podejście na pamięć masową – tkankę łączną, do której przetwarzanie zapisuje dane, a obsługa odczytuje je.

Zarządzane bazy danych, takie jak DynamoDB, zapewniały niezawodne przechowywanie, ale mieliśmy ograniczoną kontrolę nad sposobem przechowywania danych i obsługi odczytów, a cennik oparty na faktycznym użyciu rósł wraz z rozmiarem naszego indeksu i wolumenem zapytań. Ponadto nasz poprzedni potok ściśle powiązał przetwarzanie dokumentów z bazą danych obsługującą zapytania, co utrudniało niezależne zarządzanie aktualizacjami niezależnie od odczytów na żywo.

Z pomocą nowego wewnętrznego swarmu agentów kodujących przebudowaliśmy tę warstwę wokół CobbleDB, będącej gorącym magazynem typu klucz-wartość, wspieranym przez Pillar w zakresie trwałego stanu dokumentu i publikowania oraz Lorry dla aktualizacji wsadowych. Nowy projekt oddziela przetwarzanie dokumentów od pozyskiwania do gorącego magazynu i pozwala nam dostosować obsługę do naszego obciążenia. Pomiary produkcyjne przed i po pokazują około 5-krotnie niższe opóźnienie odczytu wsadowego, a nasz wewnętrzny model kosztów szacuje co najmniej 20% oszczędności w stosunku do DynamoDB.

Infrastruktura pamięci masowej dla wyszukiwania natywnego dla AI

Klasyczny stos wyszukiwania został zaprojektowany z myślą o ludzkim czytelniku, a nie o modelu językowym. Ta różnica zmienia ziarnistość, z jaką system musi przygotowywać, przechowywać i obsługiwać treści internetowe.

Aby przekształcić surowy kod HTML w treść gotową dla modelu, nasz potok przetwarzania czyści każdą stronę, dzieli ją na spójne semantycznie fragmenty, oblicza ich wektory osadzone i przechowuje fragmenty wraz z wektorami w bazie danych. W czasie zapytania potok obsługi wybiera tekst najbardziej istotny dla zapytania i przekazuje go do modelu.

Dwa odrębne przepływy pracy

Warstwa pamięci masowej musi zatem obsługiwać dwa przepływy pracy o odmiennych wymaganiach: pozyskiwanie aktualizacji z potoku przetwarzania oraz obsługę odczytów o niskich opóźnieniach.

Po stronie zapisu nowe strony napływają bez przerwy, istniejące strony ulegają zmianom, a modyfikacje metod podziału na fragmenty lub modeli wektorowych mogą wymagać ponownego przetworzenia dużych części korpusu. Warstwa pamięci masowej musi włączać te aktualizacje do trwałego stanu dokumentu, śledzić aktualne wersje, publikować odpowiednie reprezentacje w systemach downstream oraz obsługiwać duże uzupełnienia danych bez powtarzania całego potoku od surowego indeksowania.

Z kolei odczyty w czasie zapytań żądają partii kluczy stron i pobierają odpowiadające im fragmenty tekstu oraz wektory do celów rankingu i selekcji fragmentów. Te odczyty znajdują się na ścieżce prowadzącej do ostatecznej odpowiedzi, dlatego muszą być realizowane szybko.

Te obciążenia wymagają odmiennych projektów. Przetwarzanie korzysta z trwałych kolejek oraz zdolności do wydajnego dostarczania aktualizacji, ponawiania prób w przypadku awarii i aktualizowania istniejących rekordów na dużą skalę. Obsługa zapytań korzysta z wstępnie obliczonych rekordów, ciepłych pamięci podręcznych i szybkich odczytów wsadowych z minimalną ilością pracy w czasie zapytania. Ścisłe powiązanie obu tych elementów może sprawić, że praca związana z odzyskiwaniem danych i duże aktualizacje będą konkurować z odczytami wrażliwymi na opóźnienia.

Gdzie zarządzana baza danych okazuje się niewystarczająca

DynamoDB zapewniła nam wysoce dostępną pamięć masową bez konieczności utrzymywania rozproszonej bazy danych. Dedykowana tabela przygotowanych stron zawierała fragmenty tekstu oraz wektory osadzone do odczytów w czasie zapytania, podczas gdy pełna treść stron i metadane były przechowywane osobno. Jednak wraz ze wzrostem obciążenia zaczęły odgrywać rolę trzy ograniczenia.

Po pierwsze, cennik oparty na użyciu nalicza opłatę za każdy odczytany lub zapisany bajt. Pojedyncze żądanie API wyszukiwania zawiera 100-120 kluczy odpowiadających stronom, które musimy pobrać. Proces pobierania danych dzieli żądanie na mniejsze partie po 10-20 kluczy stron, przy czym średni rozmiar elementu wynosi około 50 KB. Wielokrotne czytanie tych rekordów pochłaniało znaczną wydajność, podczas gdy ciągłe indeksowanie i zmiany w metodach dzielenia na fragmenty czy modelach wektorowych generowały dodatkowe zapisy. Koszty ekonomiczne zależały również od rozmiaru i częstotliwości rekordów, a nie tylko od liczby wywołań API.

Po drugie, zarządzana usługa nie posiada elastyczności, jakiej potrzebujemy do dostrajania wydajności. Operacja, której potrzebowaliśmy, była wąska: mając partię identyfikatorów stron, szybko zwróć ich przygotowaną zawartość. Chcieliśmy mieć kontrolę nad tym, która maszyna posiada partycję, ile pamięci przeznaczono na jej pamięć podręczną oraz która replika obsługuje żądanie. Te elementy kontrolne mają znaczenie, ponieważ odczyt spoza pamięci podręcznej, przeskok między strefami lub wolna replika mogą opóźnić całą partię. DynamoDB zarządza własną pamięcią masową i zachowaniem replik za interfejsem API. Mogliśmy skonfigurować usługę, ale nie mogliśmy dostroić jej wewnętrznego rozmieszczenia, pamięci podręcznych silnika magazynowego czy polityki wyboru replik pod kątem naszego obciążenia.

Po trzecie, proces przetwarzania był powiązany bezpośrednio z zapisami w gorącym magazynie. Nasz poprzedni potok przetwarzania zapisywał każdą przygotowaną stronę bezpośrednio w DynamoDB. Duże zadanie ponownego przetwarzania stawało się zatem falą pojedynczych aktualizacji gorącego magazynu, a proces przetwarzania musiał uwzględniać limity przepustowości gorącego magazynu oraz zachowania związane z ponawianiem prób. Nie mogliśmy uruchomić MapReduce dla całej bazy danych w ciągu 1–2 dni, aby przetestować nową metodę podziału na fragmenty, zmienić nasz model wektorowy lub dodać nowe pole. Brakowało niezależnego strumienia zapisanych aktualizacji stron, który oddzielna usługa mogłaby grupować w partie i dostarczać do gorącego magazynu we własnym tempie.

Rozwiązanie tych ograniczeń wymagało czegoś więcej niż tylko wymiany bazy danych. Musieliśmy oddzielić trwały stan dokumentu i dostarczanie aktualizacji od pamięci masowej obsługującej zapytania, a następnie zoptymalizować gorący magazyn specjalnie pod kątem szybkich odczytów wsadowych.

Oddzielenie stanu dokumentu, dostarczania aktualizacji i obsługi zapytań

Nowa architektura przypisuje każdą odpowiedzialność innemu komponentowi. Pillar utrzymuje trwały stan dokumentu i decyduje, co opublikować. Lorry przekształca te eksporty w spartyjonowane partie. CobbleDB pozyskuje partie i obsługuje przygotowane rekordy według klucza strony.

Przed i po migracji magazynu. Pillar i Lorry rozdzielają trwały stan dokumentu oraz dostarczanie aktualizacji od odczytów wykonywanych w czasie zapytania, obsługiwanych przez CobbleDB.

CobbleDB: dostrajana ścieżka odczytu

CobbleDB to nasz gorący magazyn zoptymalizowany pod kątem obsługi przygotowanych rekordów w czasie zapytań. Jest to rozproszony magazyn typu klucz-wartość: kluczami są identyfikatorów stron (zahsowane adresy URL), a wartościami – przetworzone reprezentacje stron (ich wstępnie podzielone na fragmenty fragmenty tekstu oraz wektory osadzone dla każdego fragmentu).

Ważną zmianą jest nasza kontrola nad konfiguracją pamięci masowej.

Dane są partycjonowane i rozdzielane pomiędzy węzły danych. Ten rozproszony format gwarantuje, że baza danych może skalować się horyzontalnie, aby pomieścić ogromny zbiór przetworzonych stron. Każda partycja ma trzy repliki na trzech różnych węzłach; jeśli jedna kopia stanie się niedostępna, pozostałe mogą kontynuować obsługę odczytów.

Każdemu węzłowi danych przypisany jest podzbiór partycji. Węzeł przechowuje swoje dane przy użyciu RocksDB – open-source’owego, osadzonego silnika bazy danych typu klucz-wartość, który dobrze sprawdza się w przypadku obciążeń o dużej liczbie odczytów zasilanych przez pozyskiwanie wsadowe.

Pamięć podręczna danych może być obsługiwana z poziomu pamięci RAM, podczas gdy odczyty z pamięci podręcznej korzystają z lokalnej pamięci NVMe. Daje nam to bezpośredni sposób na dostosowanie bilansu pamięci RAM do dysku pod kątem obserwowanego obciążenia, zamiast akceptować wewnętrzną politykę pamięci podręcznej zarządzanej usługi.

Bezstanowy router zapytań haszuje klucze do partycji i równolegle kieruje żądania do węzłów. Architektura obejmuje również usługę metadanych wspieraną przez PostgreSQL oraz trwałą pamięć masową obiektów.

Ten projekt obsługuje wzorce odczytu, jakich wymaga nasze obciążenie. Po przejściu zapytania przez wyszukiwanie i ranking w celu uzyskania zestawu kandydatów istotnych stron, gorący magazyn jest odpytywany masowo o zawartość tych stron. Te żądania odczytu trafiają do routera i są kierowane do odpowiedniego węzła danych. Router nadaje priorytet wysyłaniu żądań do węzła w tej samej strefie dostępności, aby w miarę możliwości zminimalizować opóźnienia między strefami. Każdy węzeł danych może pobrać zawartość strony za pomocą MultiGet, czyli interfejsu API odczytu wsadowego RocksDB zaprojektowanego do szybkiego pobierania wielu kluczy jednocześnie przy niskim opóźnieniu. Jeśli odpowiedź z węzła jest wolna, system może również odpytać inną replikę partycji na innym węźle, poprawiając opóźnienie ogona rozkładu.

Możemy również uniknąć wdrażania powszechnych funkcji, których nasz przepływ pracy nie wymaga. Na przykład większość baz danych oferuje gwarancje spójności i obsługę transakcji, ale nasz gorący magazyn nie wymaga transakcji ani zsynchronizowanych replik. Dopuszczalna jest krótka przerwa między zapisaniem dokumentu a jego udostępnieniem do odczytu, w porządku jest również to, że jedna replika pobiera dane szybciej niż inna. Pominięcie funkcji, których nie potrzebujemy, zmniejsza narzut i koszty naszego systemu.

Bezstanowy router grupuje klucze stron według partycji i odczytuje repliki równolegle. Wolne odczyty mogą zostać wysłane do innej repliki; pierwsza odpowiedź dla każdej partycji jest scalana z wynikiem.

Pillar i Lorry: trwała ścieżka zapisu

Oddzielna warstwa pamięci masowej śledzi wszystko, co pobieramy i przetwarzamy, scalając aktualizacje w stan dokumentu w wersji i publikując niezbędne reprezentacje w CobbleDB oraz w indeksie wyszukiwania. W tym celu stworzyliśmy Pillar – naszą warstwę stanu dokumentu i publikowania.

Pillar bazuje na YTsaurus. Przechowuje komponenty stron (takie jak metadane, fragmenty i wektory osadzone) w osobnych rodzinach tabel YTsaurus. Śledzi również wersje fragmentów i wektorów, pozwalając na współistnienie wielu reprezentacji bez nadpisywania się nawzajem, dzięki czemu leżące u ich podstaw modele mogą ulegać zmianom. Ponieważ Pillar i YTsaurus korzystają z tanich dysków HDD, możemy przechowywać znacznie więcej dokumentów niż wcześniej w DynamoDB.

Pillar śledzi podzbiory lub zdefiniowane przez politykę grupy stron, takie jak świeże strony lub strony o wysokiej wartości. Przynależność do podzbioru określa, które aktualizacje muszą zostać przekazane do komponentów podrzędnych, takich jak CobbleDB. CobbleDB korzysta z droższej pamięci masowej NVMe, dlatego eksportujemy tylko te dokumenty, których faktycznie chcemy użyć w wynikach wyszukiwania. Gdy zawartość strony ulegnie aktualizacji lub gdy zostanie ona dodana bądź usunięta z podzbioru, Pillar odpowiada za umieszczenie odpowiedniego zadania eksportu w kolejce.

Atomiczne transakcje YTsaurus obejmują stan, zamiary eksportu oraz zużyte dane wejściowe, aby zapewnić poprawność. Aby transakcja zakończyła się pomyślnie, system musi zaktualizować trwały stan, oznaczyć dane wejściowe jako uwzględnione i umieścić w kolejce wszystkie wymagane eksporty; jeśli transakcja zostanie przerwana, na żadnej warstwie nie zostaną wprowadzone żadne zmiany.

CobbleDB oczekuje, że aktualizacje będą grupowane w partie według partycji. Eksporty z Pillar są konwertowane do tego formatu przez bezstanową usługę, którą nazywamy Lorry. Jej jedynym zadaniem jest odczytywanie rekordów eksportu z trwałych kolejek dopasowanych do partycji, gromadzenie ich w pliki wsadowe dla poszczególnych partycji i przekazywanie ich do CobbleDB.

Pomiędzy Lorry a Cobble dodatkowy protokół oddziela płaszczyznę danych (S3) od płaszczyzny sterowania. Lorry zapisuje partię w S3 pod unikalnym identyfikatorem i rejestruje partię wewnątrz Cobble. Repliki partycji Cobble niezależnie odpytują partię lub interfejs API płaszczyzny sterowania o następną partię S3 do pozyskania i asynchronicznie stosują aktualizacje wsadowe w porządku chronologicznym. Oznacza to, że wolny lub odzyskujący się węzeł może nadrabiać zaległości we własnym tempie, zamiast spowalniać resztę systemu.

Pillar publikuje trwałe eksporty, a Lorry zapisuje partycjonowane partie do S3 i rejestruje je w warstwie sterującej. Każda replika CobbleDB samodzielnie wykrywa swoje partie i wczytuje je w porządku chronologicznym.

Nowa ścieżka oddziela natychmiastowe przetwarzanie transakcji od pozyskiwania danych do gorącego magazynu oraz zapewnia ścieżkę z możliwością ponownego odtwarzania dla aktualizacji przyrostowych i pełnych przebudów. Ten podział pracy gwarantuje, że aktualizacje trwałego stanu dokumentu nie zakłócają opóźnień ścieżki odczytu.

Walidacja

5x szybciej

Po migracji pamięci podręcznej gorących danych mediana opóźnienia odczytu partii spadła z 31,4 ms do 5,60 ms. Percentyl p90 spadł z 56,7 ms do 9,77 ms, a p99 spadł ze 123 ms do 24,2 ms. Wskaźniki mieszczą się w zakresie od 5,08× do 5,80×, wykazując poprawę zarówno w przypadku typowych żądań, jak i ogona rozkładu.

Opóźnienie odczytu wsadowego w produkcji przed migracją i po niej: P50 spadło z 31,4 do 5,60 ms, P90 z 56,7 do 9,77 ms, a P99 z 123 do 24,2 ms. Są to opóźnienia odczytu z gorącego magazynu z ruchu produkcyjnego w różnych momentach, a nie dopasowane żądania ani opóźnienie odpowiedzi end-to-end.

Te liczby pochodzą z odczytów wsadowych, gdzie każde zapytanie żąda około 10-15 kluczy. Średni rozmiar elementu ładunku wynosi 50 KB. Oba systemy były odpytywane przy obciążeniu około 200 tys. rps, chociaż przeprowadziliśmy również testy obciążeniowe do 500 tys. rps bez żadnej degradacji wydajności.

Aby wyizolować wkład CobbleDB, wszystkie inne aspekty konfiguracji pozostały niezmienione. Jest to jednak obserwacyjne porównanie typu „przed i po”; systemy obsługiwały ruch produkcyjny na żywo w różnym czasie, a nie te same żądania w kontrolowanych warunkach. Z tego powodu zmierzyliśmy również wydajność obu systemów za pomocą syntetycznego benchmarku. Aby przybliżyć żądania występujące w środowisku produkcyjnym, odpytywaliśmy oba systemy za pomocą partii składających się z 10–15 kluczy oraz wartości o rozmiarach od 100B do 100 KiB.

Syntetyczne opóźnienie odczytu wsadowego dla partii po 10 i 15 kluczy w zakresie rozmiarów wartości od 100 B do 100 KiB. Każdy punkt jest średnią opóźnień P50, P90 lub P99 na poziomie przebiegu z dwóch uruchomień dla każdego backendu i konfiguracji.

DynamoDB radzi sobie w tym kontekście słabo, ponieważ jej zarządzana ścieżka odczytu nie może być dostrojona do odczytów wsadowych tej wielkości. W przeciwieństwie do niej, CobbleDB jest zoptymalizowana dokładnie pod kątem takiego scenariusza, ponieważ sami zarządzamy rozmieszczeniem, pamięcią podręczną i kontrolą replik. Grupuje ona klucze według partycji, wykonuje odczyty równolegle względem replik partycji oraz potrafi zabezpieczać wolne odczyty, odpytując inne repliki. Nasze wyniki pokazują, że te wybory projektowe przekierowują się na szybsze odczyty w środowisku produkcyjnym.

o 20% taniej

CobbleDB jest również znacząco tańsza od DynamoDB. Oprócz samej pamięci masowej, DynamoDB nalicza również opłatę za każdy odczytany lub zapisany bajt. Przy skali naszego korpusu i ruchu związanego z obsługą te opłaty oparte na użyciu stają się istotną częścią całkowitych kosztów. Ciągłe aktualizacje pochłaniają pojemność zapisu, podczas gdy każda partia przygotowanych stron pobrana na potrzeby zapytania zużywa pojemność odczytu.

Obliczyliśmy oszczędności kosztów na podstawie wewnętrznych szacunków wielkości magazynu, jednostek wydajności zapisu (WCU) oraz jednostek wydajności odczytu (RCU). Na każdym poziomie zobowiązań CobbleDB jest co najmniej o 20% tańsza od DynamoDB. Rzeczywiste oszczędności są prawdopodobnie jeszcze wyższe, ponieważ wyniki te nie uwzględniają potencjalnych oszczędności na kopiach zapasowych wynikających z kompresji.

Jak rój agentów AI pomaga nam to zbudować

Zbudowanie CobbleDB i otaczającej jej infrastruktury nie było zadaniem trywialnym. Sama rdzenna baza danych CobbleDB liczy około 40 000 linijek kodu w języku Rust, a migracja wymagała również rozległych prac mających na celu weryfikację poprawności i zachowania w przypadku awarii na skalę produkcyjną, integrację CobbleDB z ścieżkami przetwarzania i obsługi oraz bezpieczne przeniesienie ruchu produkcyjnego.

Przyspieszyliśmy te działania za pomocą wewnętrznego systemu zarządzającego swarm agentów piszących kod. System ten został zaprojektowany z myślą o wysokiej trwałości; zachowuje cele projektu, aktywne ryzyka, historię repozytorium oraz wcześniejsze decyzje podejmowane w trakcie różnych sesji.

Do projektu CobbleDB przydzieliliśmy grupę takich proaktywnych, stale aktywnych agentów. Ich pierwszym zadaniem było zbudowanie dokładnego modelu pracy, która jest już w toku. Przeprowadzili audyt kanału projektu i aktywnych wątków, powiązali nierozwiązane elementy z ich pull requestami i osobami odpowiedzialnymi, a także przygotowali aktualny raport na temat kondycji systemu i kolejnych kroków. Stamtąd działali w kilku obszarach pętli inżynieryjnej:

  • Przejrzano kod i zmiany w infrastrukturze.
  • Wskazano blokady, które łatwo było pominąć, w tym niebezpieczne założenia dotyczące przywracania danych, przestarzałe odwołania do kompilacji oraz konfiguracje, które mogłyby prowadzić do awarii w czasie wykonywania.
  • Przygotowano celowe poprawki, testy, zmiany w monitorowaniu oraz dokumentację operacyjną.
  • Śledzono bramki CI, recenzji i infrastruktury, zachowując jednocześnie jawny ludzki nadzór nad działaniami produkcyjnymi.
  • Monitorowane wdrożenia oraz ćwiczenia z tworzenia kopii zapasowych lub przywracania danych, a następnie porównano zaobserwowany stan systemu z kryteriami sukcesu wdrożenia.
  • Utrzymywano raporty projektowe, dzięki czemu inżynierowie widzieli, co zostało dostarczone, co pozostało zablokowane i gdzie nadal wymagana była ludzka ocena.

Inżynierowie nadal ustalają architekturę, przeglądają istotne zmiany i autoryzują operacje produkcyjne. Agenci zajmowali się większością ciągłych inspekcji i działań następczych pomiędzy tymi decyzjami. Pozwoliło to maściemu zespołowi na sprawne prowadzenie projektu – rdzenna infrastruktura CobbleDB została zbudowana przez dwóch inżynierów i setki agentów AI w ciągu dwóch miesięcy.

Podsumowanie

CobbleDB nie jest ogólnotematyczną bazą danych; została stworzona specjalnie z myślą o naszym konkretnym obciążeniu polegającym na wielokrotnych wsadowych odczytach przygotowanych rekordów stron, zasilanych przez asynchroniczny potok przetwarzania. Oddzielając trwały stan, dostarczanie aktualizacji i obsługę zapytań w czasie rzeczywistym, możemy niezależnie optymalizować każdą odpowiedzialność bez zmuszania jednego interfejsu bazy danych do obsługi ich wszystkich lub przenoszenia prac konserwacyjnych na ścieżkę zapytań na żywo. Migracja do CobbleDB znacząco poprawiła zarówno medianę, jak i ogon rozkładu opóźnień, jednocześnie obniżając koszty infrastruktury.

Ponieważ nadal rozwijamy własną infrastrukturę natywną dla sztucznej inteligencji, możemy optymalizować wydajność i koszty w sposób niedostępny, gdy jesteśmy związani implementacjami i modelami cenowymi firm trzecich. Potężni agenci AI przesuwają bilans jeszcze bardziej na korzyść budowania we własnym zakresie zamiast zakupu.

Planujemy wkrótce udostępnić CobbleDB jako open-source, aby inne zespoły budujące systemy wyszukiwania oparte na sztucznej inteligencji mogły korzystać z tej samej warstwy pamięci masowej, która sprawia, że wyszukiwanie w naszych produktach jest szybsze i tańsze.

Cytat

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