CobbleDB: Az AI-keresőtárhely újjáépítése az alacsonyabb késleltetésért és költségért
Decoupled architektúra különálló tartós állapottal, kötegelt frissítésekkel és lekérdezéskori visszakereséssel.
A mesterségesintelligencia-alapú keresés eltérő elvárásokat támaszt a webes tartalmak feldolgozásával, tárolásával és kiszolgálásával szemben. Annak érdekében, hogy ezeknek az igényeknek nagy skálán megfeleljünk, a teljes stacket a semmiből újjáépítettük a teljesítmény javítása és a költségek csökkentése érdekében.
Felépítettük saját feltérképezési, indexelési és rangsorolási infrastruktúránkat, beágyazási modelljeinket és inferencia-motorjainkat a hatékony feldolgozás és a gyors kiszolgálás érdekében. Ezt a megközelítést most kiterjesztjük a tárolásra is, amely a kötőszövetként szolgál a feldolgozás írásai és a kiszolgálás olvasásai között.
Az olyan felügyelt adatbázisok, mint a DynamoDB, megbízható tárat biztosítottak, de korlátozott felügyelettel rendelkeztünk az adatok tárolása és az olvasások kiszolgálása felett, a használatalapú árazás pedig az index méretével és a lekérdezések mennyiségével párhuzamosan növekedett. Emellett a korábbi pipeline-unk közvetlenül összekapcsolta a dokumentumfeldolgozást az éles adatbázissal, ami megnehezítette a frissítések független kezelését az élő olvasásoktól.
Egy új belső kódolásiügynök-raj segítségével újjáépítettük ezt a réteget a CobbleDB köré, amely egy kulcs-érték alapú forró tárhely, és a tartós dokumentumállapotért és közzétételért felelős Pillar, valamint a kötegelt frissítésekért felelős Lorry támogatását élvezi. Az újratervezés elválasztja a dokumentumfeldolgozást a forró tárhelyre történő betöltéstől, és lehetővé teszi számunkra, hogy a kiszolgálást a munkaterhelésünkhöz hangoljuk. Az éles előtte-utána mérések nagyságrendileg 5× alacsonyabb kötegelt olvasási késleltetést mutatnak, belső költségmodellünk pedig legalább 20%-os megtakarítást becsül a DynamoDB-hez képest.
Infrastruktúra a mesterségesintelligencia-alapú kereséshez
A klasszikus keresési stack emberi olvasó számára készült, nem pedig nyelvi modell számára. Ez a különbség megváltoztatja azt a részletességi szintet, amelyen a rendszernek elő kell készítenie, tárolnia és ki kell szolgálnia a webes tartalmakat.
Annak érdekében, hogy a nyers HTML-ből modellkész tartalom legyen, feldolgozó pipeline-unk megtisztítja az egyes oldalakat, szemantikailag koherens részekre osztja őket, kiszámítja a beágyazásaikat, majd a szövegrészleteket és a beágyazásokat együtt tárolja az adatbázisban. Lekérdezéskor a kiszolgáló pipeline kiválasztja a lekérdezéshez leginkább releváns szöveget, és átadja a modellnek.
Két különböző munkafolyamat
A tárolási rétegnek ezért két, eltérő igényű munkafolyamatot kell támogatnia: a feldolgozó pipeline-ból érkező frissítések betöltését és az alacsony késleltetésű olvasások kiszolgálását.
Írási oldalon folyamatosan érkeznek új oldalak, a meglévő oldalak megváltoznak, a chunkolási módszerek vagy beágyazási modellek változásai pedig megkövetelhetik a korpusz nagy részeinek újrafeldolgozását. A tárolási rétegnek be kell építenie ezeket a frissítéseket a tartós dokumentumállapotba, nyomon kell követnie az aktuális verziókat, közzé kell tennie a megfelelő reprezentációkat a lefelé áramló (downstream) rendszerekben, és támogatnia kell a nagy háttérbetöltéseket (backfills) anélkül, hogy megismételné a teljes pipeline-t a nyers crawl-tól kezdve.
Ezzel szemben a lekérdezéskori olvasások oldalazási kulcsok kötegeit kérik le, és kinyerik a megfelelő szövegrészleteket és beágyazásokat a rangsoroláshoz és a szövegrészlet-kiválasztáshoz. Ezek az olvasások a végső váshoz vezető úton helyezkednek el, ezért gyorsan kell befejeződniük.
Ezek a munkaterhelések eltérő kialakítást igényelnek. A feldolgozás a tartós sorokból, valamint a frissítések hatékony kézbesítésének, a hibák újrapróbálásának, valamint a meglévő rekordok nagy skálán történő naprakésszé tételének képességéből profitál. A kiszolgálás az előre kiszámított rekordokból, a meleg cache-ekből és a minimális lekérdezési munkával járó, gyors kötegelt olvasásokból profitál. A kettő közötti szoros csatolás miatt a helyreállítási munkák és a nagy frissítések versenyezhetnek a késleltetésre érzékeny olvasásokkal.
Ahol a felügyelt adatbázis kevésnek bizonyul
A DynamoDB nagy rendelkezésre állású tárat biztosított anélkül, hogy egy elosztott adatbázis üzemeltetésének terhét viselnünk kellett volna. Egy dedikált előkészített-oldalak tábla tárolta a lekérdezéskori olvasásokhoz tartozó szövegrészleteket és beágyazásokat, míg a teljes oldalakat és a metaadatokat külön tároltuk. Ahogy azonban a munkaterhelésünk nőtt, három korlát vált fontossá.
Először is, a használatalapú árazás minden egyes leolvasott vagy megírt bájtot számlál. Egyetlen Search API-kérés 100-120 kulcsot tartalmaz, amelyek a lekérni kívánt oldalaknak felelnek meg. Az adatlekérdezési folyamat a kérést 10-20 oldalkulcsból álló kisebb kötegekre bontja, az átlagos elemméret pedig körülbelül 50 KB. Ezen rekordok ismételt olvasása jelentős kapacitást emésztett fel, míg a folyamatos crawlok, valamint a chunkolási vagy beágyazási modellek módosításai több írást generáltak. A gazdaságosság a rekordok méretétől és gyakoriságától is függött, nem csak az API-hívások számától.
Másodszor, a felügyelt szolgáltatásból hiányzik a teljesítményhangoláshoz szükséges rugalmasság. A szükséges művelet szűk körű volt: adott oldalazonosítók kötege alapján gyorsan adja vissza azok előkészített tartalmát. Ellenőrzést akartunk felette, hogy melyik gép tartja a partíciót, mennyi memória van fenntartva a cache-ének, és melyik replika kezel egy kérést. Ezek a vezérlők azért számítanak, porque egy nem cache-elt olvasás, a zónák közötti ugrás vagy egy lassú replika késleltetheti az egész köteget. A DynamoDB az API mögött saját maga kezeli a tárolást és a replikaviselkedést. Konfigurálhattuk a szolgáltatást, de nem tudtuk a munkaterhelésünkhöz igazítani a belső elhelyezését, a tárolómotor gyorsítótárait vagy a replikakiválasztási politikáját.
Harmadszor, a feldolgozás közvetlenül kapcsolódott a forró tárhelyre történő írásokhoz. Korábbi feldolgozó pipeline-unk minden egyes előkészített oldalt közvetlenül a DynamoDB-be írt. Egy nagy újra-feldolgozási feladat így az egyedi forró tárhely frissítések hullámává vált, és a feldolgozásnak igazodnia kellett a forró tárhely áteresztőképességi korlátaihoz és újrapróbálási (retry) viselkedéséhez. Nem tudtunk MapReduce-t futtatni az egész adatbázison 1-2 nap alatt egy új chunkolási módszer kipróbálásához, a beágyazási modellünk megváltoztatásához vagy egy új mező hozzáadásához. Nem létezett a mentett oldalfrissítéseknek olyan független streame, amelyet egy külön szolgáltatás kötegelhetett és a saját tempójában kézbesíthetett volna a forró tárhelynek.
Ezen korlátok kezelése nem merült ki az adatbázis cseréjében. Szét kellett választanunk a tartós dokumentumállapotot és a frissítések kézbesítését a lekérdezéskori tárolástól, majd kifejezetten a gyors, kötegelt olvasásokra kellett optimalizálnunk a forró tárhelyet.
A dokumentumállapot, a frissítéskézbesítés és a kiszolgálás szétválasztása
Az új architektúra minden felelősséget egy másik komponensre bíz. A Pillar fenntartja a tartós dokumentumállapotot, és eldönti, mit kell közzétenni. A Lorry particionált kötegekké alakítja ezeket az exportokat. A CobbleDB betölti a kötegeket, és oldalkulcs alapján kiszolgálja az előkészített rekordokat.
CobbleDB: a hangolható olvasási útvonal
A CobbleDB a lekérdezéskori kiszolgáláshoz optimalizált forró tárhelyünk. Ez egy elosztott kulcs-érték tároló: a kulcsok oldalazonosítók (hashelt URL-ek), az értékek pedig az oldal feldolgozott reprezentációi (az előre feldarabolt szövegrészletek és a részenkénti vektorbeágyazások).
A fontos változás a tárolási konfiguráció feletti kontrollunk.
Az adatok partícionálva vannak, és megoszlanak az adatcsomópontok között. Ez az elosztott formátum biztosítja, hogy az adatbázis horizontálisan skálázható legyen a feldolgozott oldalak hatalmas korpuszának kiszolgálásához. Minden partíciónak három replikája van három különböző csomóponton; ha az egyik másolat elérhetetlenné válik, a többiek folytathatják az olvasások kiszolgálását.
Minden adatcsomópont megkapja a partíciók egy részhalmazát. A csomópont az adatait a RocksDB segítségével tárolja, amely egy nyílt forráskódú beágyazott kulcs-érték tárolómotor, és kiválóan alkalmas a kötegelt betöltéssel táplált, olvasásintenzív munkaterhelésekhez.
A cache-elt adatok kiszolgálhatók memóriából, míg a nem cache-elt olvasások helyi NVMe-tárat használnak. Ez közvetlen módot biztosít arra, hogy a megfigyelt munkaterheléshez hangoljuk a memória és a lemez egyensúlyát, ahelyett hogy elfadnánk a felügyelt szolgáltatás belső gyorsítótár-politikáját.
Egy állapottal nem rendelkező (stateless) lekérdezési router hasheli a kulcsokat partíciókra, és párhuzamosan útvonalozza a kéréseket a csomópontokhoz. Az architektúra magában foglal egy PostgreSQL-alapú metaadat-szolgáltatást és egy tartós objektumtárat is.
Ez a kialakítás támogatja a munkaterhelésünk által megkövetelt olvasási mintákat. Miután egy lekérdezés áthalad a visszakeresésen és a rangsoroláson a releváns oldalak jelölt készletének megszerzése érdekében, a forró tárhelyet kötegelve lekérdezzük ezen oldalak tartalmáért. Ezek az olvasási kérések megérkeznek a routerhez, amely továbbítja őket a megfelelő adatcsomóponthoz. A router prioritásként kezeli a kérések küldését egy azonos rendelkezésre állási zónában lévő csomóponthoz, hogy ha lehetséges, minimalizálja a zónák közötti késleltetést. Minden adatcsomópont be tudja tölteni az oldal tartalmát a MultiGet használatával, amely egy olyan RocksDB kötegelt olvasási API, amelyet arra terveztek, hogy alacsony késleltetéssel kérjen le egyszerre több kulcsot. Ha egy csomópont lassan válaszol, a rendszer lekérdezhet egy másik partíció-replikát is egy másik csomóponton, javítva a szélsőérték (tail) késleltetését.
Elkerülhetjük továbbá olyan általános funkciók implementálását is, amelyekre a munkafolyamatunk nincs rászorulva. A legtöbb adatbázis például konzisztencia-garanciákat és tranzakciókezelést kínál, de a forró tárhelyünk nem igényel tranzakciókat vagy szinkronizált replikákat. Teljesen elfogadható, hogy van egy rövidebb időbeli rés a dokumentum megírása és az olvasás számára való elérhetővé válása között, és az sem jelent problémát, ha az egyik replika gyorsabban tölt be adatokat, mint a másik. A nem szükséges funkciók elhagyása csökkenti a rendszerünk többletterhelését és költségeit.
Pillar és Lorry: a tartós írási útvonal
Egy külön tárolási réteg nyomon követ minden feltérképezett és feldolgozott elemet, egyesíti a frissítéseket a verziózott dokumentumállapotban, és közzéteszi a szükséges reprezentációkat a CobbleDB-ben és a keresési indexben. Ebből a célból hoztuk létre a Pillar nevű dokumentumállapot- és közzétételi rétegünket.
A Pillart YTsaurus támogatja. Oldalalkotó elemeket (például metaadatokat, chunkokat és beágyazásokat) tárol különálló YTsaurus táblacsaládokban. Emellett nyomon követi a chunkok és beágyazások verzióit, lehetővé téve több reprezentáció egymás melletti létezését anélkül, hogy felülírnák egymást, így az alatta lévő modellek változhatnak. Mivel a Pillar és az YTsaurus olcsó HDD-tárolót használ, sokkal több dokumentumot tudunk tárolni, mint korábban a DynamoDB-ben.
A Pillar részhalmazokat, vagyis szabályok által definiált oldalcsoportokat (például friss oldalakat vagy nagy értékű oldalakat) követ nyomon. A részhalmaz-tagság határozza meg, hogy mely frissítéseket kell továbbítani a lefelé áramló (downstream) komponenseknek, például a CobbleDB-nek. A CobbleDB drágább NVMe-tárat használ, ezért csak azokat a dokumentumokat exportáljuk, amelyeket valóban fel akarunk használni a keresési eredményekben. Amikor egy oldal tartalma frissül, vagy ha hozzáadják/eltávolítják egy részhalmazból, a Pillar felelős a megfelelő exportálási munka sorba állításáért.
Az atomi YTsaurus tranzakciók lefedik az állapotot, az exportálási szándékokat és a feldolgozott bemenetet a helyesség biztosítása érdekében. Ahhoz, hogy egy tranzakció sikeresen befejeződjön, a rendszernek frissítenie kell a tartós állapotot, el kell könyvelnie a bemenetet, és be kell sorba állítania az összes szükséges exportálást; ha a tranzakció megszakad, egyik rétegen sem történik változás.
A CobbleDB azt várja el, hogy a frissítések partíciók szerint legyenek kötegelve. A Pillar exportjait egy Lorry nevű, állapottal nem rendelkező (stateless) szolgáltatás konvertálja ebbe a formátumba. Ennek kizárólagos feladata, hogy exportrekordokat olvasson egy perzisztens, partíciókhoz igazított sorból (queue), partíciónkénti kötegelt fájlokba gyűjtse őket, és átadja a CobbleDB-nek.
A Lorry és a Cobble között egy további protokoll választja el az adatívet (S3) a vezérlőívtől. A Lorry egyedi azonosító alatt írja a köteget az S3-ra, és regisztrálja a köteget a Cobble-ben. A Cobble partícióreplikái függetlenül lekérdezik a köteget vagy a vezérlőív API-ját a következő betöltendő S3-kötegért, és aszinkron módon, kronológiai sorrendben alkalmazzák a kötegfrissítéseket. Ez azt jelenti, hogy egy lassú vagy helyreállási fázisban lévő csomópont a saját tempójában zárkózhat fel, ahelyett hogy lelassítaná a rendszer többi részét.
Az új útvonal elválasztja az azonnali feldolgozási tranzakciót a forró tárhelyre történő betöltéstől, és újrahasznosítható (replayable) útvonalat biztosít a növekményes frissítésekhez és a tömeges újraépítésekhez. Ez a feladatmegosztás biztosítja, hogy a tartós dokumentumállapot frissítései ne zavarják az olvasási útvonal késleltetését.
Validálás
5x gyorsabb
A forró tárhely migrálását követően a medián kötegelt olvasási késleltetés 31,4 ms-ról 5,60 ms-ra csökkent. A p90 56,7 ms-ról 9,77 ms-ra, a p99 pedig 123 ms-ról 24,2 ms-ra esett vissza. Az arányok 5,08× és 5,80× között mozognak, javulást mutatva mind a tipikus kérések, mind a szélsőértékek (tail) terén.
Ezek a számok kötegelt olvasásokból származnak, ahol minden lekérdezés körülbelül 10-15 kulcsot kér le. Az átlagos hasznos adat (payload) mérete 50 KB. Mindkét rendszert körülbelül 200k rps sebességgel kérdeztük le, bár teszteltünk terhelést 500k rps-ig is, teljesítményromlás nélkül.
A CobbleDB hozzájárulásának elkülönítése érdekében a beállítás minden egyéb aspektusát változatlanul hagytuk. Ez azonban továbbra is egy megfigyelésen alapuló előtte-utána összehasonlítás; a rendszerek éles (production) forgalmat szolgáltak ki különböző időpontokban, nem pedig ugyanazokat a kéréseket ellenőrzött körülmények között. Ezért mindkét rendszer teljesítményét szintetikus benchmarkon is mernékk, illetve mértük. A gyártási környezetben látható kérések közelítése érdekében mindkettőt 10-15 kulcsból álló kötegekkel, valamint 100B és 100 KiB közötti értékekkel kérdeztük le.
A DynamoDB ebben a kontextusban azért küzd nehézségekkel, mert a felügyelt olvasási útvonala nem hangolható ilyen méretű kötegelt olvasásokhoz. Ezzel szemben a CobbleDB pontosan erre a beállításra van optimalizálva, mivel mi kezeljük az elhelyezést, a gyorsítótárazást és a replikavezérléseket. Kulcsok szerint csoportosítja a partíciókat, párhuzamosan hajt végre olvasásokat a partíció-replikákkal szemben, és képes a lassú olvasásokat más replikákkal fedezni. Eredményeink azt mutatják, hogy ezek a tervezési döntések gyorsabb olvasásokat eredményeznek termelési környezetben.
20%-kal olcsóbb
A CobbleDB ráadásul jelentősen olcsóbb, mint a DynamoDB. A tároláson túl a DynamoDB minden egyes leolvasott vagy megírt bájtot is felszámít. Korpuszunk és kiszolgálási forgalmunk skáláján ezek a használatalapú díjak a teljes költség jelentős részét teszik ki. A folyamatos frissítések írási kapacitást emésztenek fel, míg a lekérdezéshez lehívott előkészített oldalak minden egyes kötege olvasási kapacitást fogyaszt.
A költségmegtakarítást a tárolási méretre, az írási kapacitás egységekre és az olvasási kapacitás egységekre vonatkozó belső becslések alapján számítottuk ki. Minden elköteleződési szinten a CobbleDB legalább 20%-kal olcsóbb, mint a DynamoDB. A valós megtakarítás valószínűleg még ennél is magasabb, mivel ezek az eredmények nem veszik figyelembe a tömörítésből eredő lehetséges biztonsági mentési megtakarításokat.
Hogyan segít nekünk egy AI-ügynökökből álló raj ennek felépítésében?
A CobbleDB és a kapcsolódó infrastruktúra felépítése minden volt, csak nem triviális. Maga a CobbleDB magadatabázisa körülbelül 40 000 sornyi Rust kódból áll, és a migráció emellett kiterjedt munkát igényelt a helyesség és a hibakezelési viselkedés gyártási skálán történő ellenőrzéséhez, a CobbleDB integrálásához a feldolgozási és a kiszolgálási útvonalakhoz, valamint az éles forgalom biztonságos átvezetéséhez.
Ezt az erőfeszítést egy olyan belső rendszerrel gyorsítottuk fel, amely kódolási ügynökök raját működteti. A rendszert úgy tervezték, hogy rendkívül perzisztens legyen; a munkamenetek során megőrzi a projekt céljait, az aktív kockázatokat, a repository előzményeit és a korábbi döntéseket.
Proaktív, folyamatosan futó ügynökökből álló csoportot rendeltünk a CobbleDB projekthez. Első feladatuk az volt, hogy pontos modellt készítsenek a már folyamatban lévő munkáról. Átnézték a projektcsatornát és az aktív szálakat, összekapcsolták a függőben lévő elemeket a pull requestekkel és a felelősökkel, valamint aktuális jelentést készítettek a rendszer egészségügyi állapotáról és a következő lépésekről. Ezt követően a mérnöki ciklus számos területén végeztek tevékenységet:
- Áttekintettük a kód- és infrastrukturális változtatásokat.
- Felszínre hoztunk olyan, könnyen figyelmen kívül hagyható blokkoló tényezőket, mint a nem biztonságos visszaállítási feltételezések, elavult build-hivatkozások és futásidőben hibára futó konfigurációk.
- Előkészített fókuszált javítások, tesztek, figyelési (monitoring) változtatások és üzemeltetési dokumentáció.
- Nyomon követtük a CI-, felülvizsgálati (review) és infrastrukturális kapukat, miközben a gyártási műveleteket kifejezetten emberi felügyelet alatt tartottuk.
- Felügyelt bevezetések, valamint biztonsági mentési és visszaállítási gyakorlatok, amelyek során a megfigyelt rendszerállapotot összehasonlították a bevezetés sikerességi kritériumaival.
- Karbantartottuk a projektriportokat, hogy a mérnökök láthassák, mi került ki (shipped), mi maradt blokkolva, és hol van még szükség emberi döntéshozatalra.
A mérnökök továbbra is ők állították össze az architektúrát, hagyták jóvá a lényeges változtatásokat, és engedélyezték a gyártási műveleteket. Az ügynökök kezelték a folyamatos ellenőrzések és a döntések közötti végrehajtások nagy részét. Ez lehetővé tette, hogy egy kis csapat hatékonyan vigye véghez a projektet – a CobbleDB maginfrastruktúráját két emberi mérnök és több száz AI-ügynök építette fel két hónap alatt.
Összegzés
A CobbleDB nem egy általános célú adatbázis; kifejezetten az aszinkron feldolgozási folyamat által betáplált, előkészített oldalrekordok ismételt kötegelt olvasásából álló, specifikus munkaterhelésünkhöz lett igazítva. A tartós állapot, a frissítések kézbesítése és a lekérdezéskori kiszolgálás szétválasztásával minden egyes feladatot függetlenül optimalizálhatunk anélkül, hogy egyetlen adatbázis-interfésznek kellene mindegyiket kiszolgálnia, vagy karbantartási munkát kellene áttolnunk az éles lekérdezési útvonalra. A CobbleDB-re való áttérés jelentősen javította a medián és a szélsőérték (tail) késleltetését is, miközben csökkentette az infrastrukturális költségeket.
Ahogy továbbra is saját fejlesztésű, AI-natív infrastruktúrát építünk ki házi készítésű módon, olyan teljesítmény- és költségoptimalizálást érhetünk el, amelyre harmadik féltől származó implementációk és árképzési modellek mellett nincs lehetőség. Az erőteljes mesterségesintelligencia-ügynökök még inkább az építés javára billentik a mérleget a vásárlással szemben.
Hamarosan nyílt forráskódúvá tesszük a CobbleDB-t, így más, AI-natív keresési rendszereket építő csapatok is élvezhetik ugyanannak a tárolási rétegnek az előnyeit, amely gyorsabbá és olcsóbbá teszi a keresést a termékeinken belül.
Hivatkozás
@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}
}