CobbleDB: Преизграждане на хранилището за AI търсене за по-ниска латентност и разходи
Декуплирана архитектура с разделено трайно състояние, пакетни актуализации и извличане по време на заявка.
AI-нативното търсене налага различни изисквания към начина, по който уеб съдържанието трябва да бъде обработвано, съхранявано и обслужвано. За да отговорим на тези изисквания в мащаб, преизграждаме целия стек от нулата, за да подобрим производителността и да намалим разходите.
Изградихме собствени инфраструктура за обхождане, индексиране и класиране, embedding модели и двигатели за инференс, за да поддържаме ефективна обработка и бързо обслужване. Сега разширяваме този подход и към съхранението — съединителната тъкан, в която обработката записва и от която обслужването чете.
Управляваните бази данни като DynamoDB осигуряваха надеждно съхранение, но имахме ограничен контрол върху начина на съхранение на данните и обслужване на четенията, а ценообразуването на базата на използването нарастваше с размера на нашия индекс и обема на заявките. Отделно от това, предишният ни тръбопровод обвързваше обработката на документите директно с базата данни за обслужване, което затрудняваше управлението на актуализациите независимо от четенията в реално време.
С помощта на нов вътрешен рояк от кодиращи агенти преизградихме този слой около CobbleDB — горещо хранилище за тип „ключ-стойност“, поддържано от Pillar за трайно състояние и публикуване на документи, и Lorry за пакетни актуализации. Препроектирането отделя обработката на документите от приемането в горещото хранилище и ни позволява да настроим обслужването според нашето работно натоварване. Производствените измервания преди и след показват приблизително 5 пъти по-ниска латентност при пакетно четене, а нашият вътрешен модел на разходите оценява поне 20% спестявания спрямо DynamoDB.
Инфраструктура за съхранение за AI-нативно търсене
Класическият стек за търсене е изграден за човешки читател, а не за езиков модел. Тази разлика променя гранулярността, с която системата трябва да подготвя, съхранява и обслужва уеб съдържание.
За да превърне суровия HTML в съдържание, готово за модели, нашият тръбопровод за обработка почиства всяка страница, разделя я на семантично кохерентни откъси, изчислява техните ембединги и съхранява откъсите и ембедингите заедно в базата данни. По време на заявка тръбопроводът за обслужване избира най-релевантния за заявката текст и го подава към модела.
Два различни работни процеса (workflows)
Следователно слой за съхранение трябва да поддържа два работни процеса (workflows) с различни изисквания: приемане на актуализации от тръбопровода за обработка и обслужване на четения с ниска латентност.
От страна на записите непрекъснато пристигат нови страници, съществуващите страници се променят, а промените в методите за сегментиране или embedding моделите могат да изискват повторна обработка на големи части от масива от данни. Слой за съхранение трябва да интегрира тези актуализации в трайното състояние на документите, да проследява актуалните версии, да публикува правилните представяния към надолу по веригата системи и да поддържа мащабни попълвания, без да повтаря целия тръбопровод от суровото обхождане.
За разлика от тях, четенията по време на заявка изискват партиди от ключове на страници и извличат съответния текст на пасажа и ембединги за класиране и избор на пасаж. Тези четения са част от пътя към крайния отговор, така че те трябва да бъдат завършени бързо.
Тези работни натоварвания изискват различен дизайн. Обработката се възползва от трайни опашки и възможността за ефективно доставяне на актуализации, повторен опит при грешки и актуализиране на съществуващи записи в мащаб. Обслужването се възползва от предварително изчислени записи, топли кешове и бързи пакетни четения с минимална работа по време на заявка. Плътната обвързаност между двете може да доведе до конкуренция между работата по възстановяване и малките актуализации с четенията, чувствителни към латентността.
Къде управляваната база данни не достига
DynamoDB ни осигури високо достъпно хранилище без тежестта от поддръжката на разпределена база данни. Специализирана таблица с предварително приготвени страници съхраняваше откъси и ембединги за четене по време на заявка, докато пълното съдържание на страниците и метаданните се съхраняваха отделно. С нарастването на работното ни натоварване обаче излязоха на преден план три ограничения.
Първо, ценообразуването на базата на използването таксува всеки прочетен или записан байт. Една заявка към API за търсене съдържа 100–120 ключа, съответстващи на страници, които трябва да извлечем. Процесът на извличане на данни разделя заявката на по-малки партиди от 10–20 ключа на страници, като средният размер на елемента е около 50 KB. Повтарящото се четене на тези записи консумира съществен капацитет, докато непрекъснатите обхождания и промените в методите за сегментиране или embedding моделите генерират повече записи. Икономиката също така зависеше от размера и честотата на записите, а не само от броя на API заявките.
Второ, управляваната услуга няма гъвкавостта, от която се нуждаем за настройка на производителността. Операцията, която ни беше необходима, беше стегната: при дадена партида от идентификатори на страници, върни бързо подготвеното им съдържание. Искахме контрол върху това коя машина държи даден дял, колко памет е отделена за нейния кеш и коя реплика обработва дадена заявка. Тези контроли са от значение, тъй като некешираното четене, прескачането между зони или бавната реплика могат да забавят цялата партида. DynamoDB управлява собственото си хранилище и поведението на репликите зад API. Можехме да конфигурираме услугата, но не и да настройваме вътрешното ѝ разположение, кешовете на двигателя за съхранение или политиката за избор на реплики спрямо нашето работно натоварване.
Трето, обработката бе пряко обвързана със записите в горещото хранилище. Предишният ни тръбопровод за обработка записваше всяка подготвена страница директно в DynamoDB. Поради това голяма задача за повторна обработка се превръщаше в вълна от индивидуални актуализации в горещото хранилище и обработката трябваше да се съобразява с лимитите на пропускателната способност и поведението при повторни опити на горещото хранилище. Не можехме да пуснем MapReduce върху цялата база данни за 1-2 дни, за да изпробваме нов метод за сегментиране, да променим своя embedding модел или да добавим ново поле. Липсваше независим поток от запазени актуализации на страници, който отделна услуга да може да пакетира и достави до горещото хранилище със собствено темпо.
Преодоляването на тези ограничения изискваше нещо повече от смяна на базата данни. Трябваше да отделим трайното състояние на документите и доставката на актуализации от хранилището за заявки, след което да оптимизираме горещото хранилище специално за бързи пакетни четения.
Разделно състояние на документите, доставка на актуализации и обслужване
Новата архитектура възлага всяка отговорност на различен компонент. Pillar поддържа трайно състояние на документите и решава какво да публикува. Lorry превръща тези експортирания в партиди, разделени на дялове. CobbleDB поглъща партидите и обслужва подготвени записи по ключ на страница.
CobbleDB: настройващият се път за четене
CobbleDB е нашето горещо хранилище, оптимизирано за обслужване на подготвени записи по време на заявка. Това е разпределено хранилище от тип „ключ-стойност“: ключовете са идентификатори на страници (хеширани URL адреси), а стойностите са обработеното представяне на страницата (предварително раздробените им откъси и векторни ембединги за всяко парче).
Важната промяна е нашият контрол върху конфигурацията на хранилището.
Данните се разделят на дялове и се разпределят между работните възли. Този разпределен формат гарантира, че базата данни може да се мащабира хоризонтално, за да побере огромен масив от обработени страници. Всеки дял има три реплики на три различни възела; ако едно копие стане недостъпно, останалите могат да продължат да обслужват четения.
На всеки работен възел (data node) е назначено подмножество от дяловете. Възелът съхранява данните си с помощта на RocksDB — вграден двигател за съхранение от тип „ключ-стойност“ с отворен код, който е добре адаптиран към натоварени с четене работни натоварвания, захранвани от пакетно поглъщане.
Кешираните данни могат да бъдат обслужвани от паметта, докато некешираните четения използват локално NVMe хранилище. Това ни дава директен начин да настроим баланса между памет и диск за наблюдаваното работно натоварване, вместо да приемаме вътрешната политика за кеширане на управляваната услуга.
Рутер за заявки без съдържание хешира ключовете към дялове и насочва заявките към възлите паралелно. Архитектурата включва още услуга за метаданни, базирана на PostgreSQL, и трайно обектно хранилище.
Този дизайн поддържа моделите на четене, които нашето работно натоварване изисква. След като дадена заявка премине през извличане и класиране за получаване на набор от кандидатури от релевантни страници, към горещото хранилище се отправя масова заявка за съдържанието на тези страници. Тези заявки за четене пристигат в рутера и се насочват към съответния работен възел. Рутерът дава приоритет на изпращането на заявки към възел в същата зона на наличност, за да се минимизира латентността между зоните, ако е възможно. Всеки работен възел може да извлече съдържанието на страницата с помощта на MultiGet — API за пакетно четене на RocksDB, проектиран да извлича множество ключове едновременно с ниска латентност. Ако даден възел отговаря бавно, системата може да направи заявка и към друга реплика на дял на различен възел, което подобрява опашната латентност.
Можем също така да избегнем внедряването на общи функции, които нашият работен процес (workflow) не изисква. Например повечето бази данни предлагат гаранции за последователност и поддръжка на транзакции, но нашето горещо хранилище не изисква транзакции или синхронизирани реплики. Допустимо е да има кратък интервал между момента, в който документът е записан, и момента, в който той е достъпен за четене, а също така е нормално една реплика да поглъща данни по-бързо от друга. Пропускането на функции, от които не се нуждаем, намалява режийните разходи и цената на нашата система.
Pillar и Lorry: устойчивият път за запис
Отделен слой за съхранение следи всичко, което обхождаме и обработваме, като обединява актуализациите във версионно състояние на документите и публикува необходимите представяния в CobbleDB и индекса за търсене. За тази цел създадохме Pillar — нашия слой за състояние и публикуване на документи.
Pillar се поддържа от YTsaurus. Той съхранява компонентите на страниците (като метаданни, парчета и ембединги) в отделни семейства таблици на YTsaurus. Освен това той проследява версиите на парчетата и ембедингите, което позволява на множество представяния да съществуват едновременно, без да се презаписват взаимно, така че базовите модели да могат да се променят. Тъй като Pillar и YTsaurus използват евтино HDD хранилище, можем да съхраняваме много повече документи, отколкото преди в DynamoDB.
Pillar проследява подмножества или дефинирани от политиката групи страници, като например пресни страници или страници с висока стойност. Членството в подмножество определя кои актуализации трябва да бъдат предадени на компоненти надолу по веригата като CobbleDB. CobbleDB използва по-скъпо NVMe хранилище, затова експортираме само документите, които наистина искаме да използваме в резултатите от търсенето. Когато съдържанието на страницата се актуализира или тя бъде добавена или премахната от подмножество, Pillar отговаря за поставянето на съответната работа по експортиране на опашка.
Атомните транзакции в YTsaurus обхващат състоянието, намеренията за експортиране и консумирания вход, за да се гарантира коректност. За да завърши една транзакция успешно, системата трябва да актуализира трайното състояние, да маркира входа като отчетен и да постави на опашка всички необходими експортирания; ако транзакцията бъде прекъсната, на нито един слой не се правят промени.
CobbleDB очаква актуализациите да бъдат пакетирани според дяла. Експортите от Pillar се конвертират в този формат от услуга без съдържание (stateless), която наричаме Lorry. Единствената ѝ задача е да чете записи за експортиране от постоянна опашка, подредена по дялове, да ги натрупва в партидни файлове за отделните дялове и да ги подава към CobbleDB.
Между Lorry и Cobble допълнителен протокол отделя контролния план (control plane) от равнината на данните (data plane, S3). Lorry записва партидата в S3 под уникален идентификатор и регистрира партидата в Cobble. Репликите на дяловете в Cobble правят независими заявки към API на партидата или контролния план за следващата S3 партида за приемане и асинхронно прилагат актуализациите на партидите в хронологичен ред. Това означава, че бавен или възстановяващ се възел може да навакса със собствено темпо, вместо да забавя останалата част от системата.
Новият път отделя незабавната транзакция за обработка от приемането в горещото хранилище и предоставя път с възможност за възпроизвеждане за малки актуализации и масивни реконструкции. Това разпределение на задачите гарантира, че актуализациите на трайното състояние на документите няма да попречат на латентността по пътя за четене.
Валидация
5 пъти по-бърза
След мигрирането на горещото хранилище средната латентност при пакетно четене спадна от 31,4 ms на 5,60 ms. Показателят p90 падна от 56,7 ms на 9,77 ms, а p90 — от 123 ms на 24,2 ms. Коефициентите варират от 5,08× до 5,80×, с подобрения както при типичните заявки, така и при опашката.
Тези числа са получени от пакетни четения, при които всяка заявка изсква около 10–15 ключа. Средният размер на елемента на полезния товар е 50 KB. И към двете системи бяха отправяни заявки с около 200 хил. rps, въпреки че проведоха и тестове на натоварване до 500 хил. rps без влошаване на производителността.
За да се изолира приносът на CobbleDB, всички останали аспекти на настройката бяха запазени постоянни. Това обаче остава наблюдателно сравнение „преди и след“; системите обслужваха реален производствен трафик в различни моменти, а не едни и същи заявки при контролирани условия. Затова измерихме и производителността на двете системи на синтетичен бенчмарк. За да приближим заявките до тези в производството, изпратихме към двете системи заявки с партиди от 10–15 ключа и стойности от 100B до 100 KiB.
DynamoDB се затруднява в този контекст, защото управляваният от него път за четене не може да бъде настройван за пакетни четения с такъв размер. За разлика от него CobbleDB е оптимизиран именно за тази среда, тъй като ние управляваме разполагането, кеширането и контролите на репликите. Той групира ключовете по партиции, изпълнява четенията паралелно спрямо репликите на партициите и може да хеджиране бавни четения срещу други реплики. Нашите резултати показват, че тези архитектурни решения водят до по-бързи четения в производствена среда.
20% по-евтина
CobbleDB е и значително по-евтина от DynamoDB. В допълнение към съхранението, DynamoDB таксува и всеки прочетен или записан байт. При мащаба на нашия корпус и обслужващ трафик тези такси на базата на използването се превръщат в съществена част от общите разходи. Непрекъснатите актуализации консумират капацитет за запис, докато всяка партида подготвени страници, извлечена за заявка, консумира капацитет за четене.
Изчислихме икономията на разходи въз основа на вътрешни оценки на размера на хранилището, единиците капацитет за запис и единиците капацитет за четене. При всяко ниво на ангажираност CobbleDB е с поне 20% по-евтина от DynamoDB. Реалните спестявания вероятно са дори по-високи, тъй като тези резултати не отчитат потенциалните икономии от компресия при архивиране.
Как група AI агенти ни помогнаха да го създадем
Изграждането на CobbleDB и прилежащата инфраструктура далеч не беше лесно. Само основната база данни на CobbleDB е с дължина около 40 000 реда на Rust, а миграцията изискваше и мащабна работа за проверка на правилността и поведението при грешки в производствен мащаб, интегриране на CobbleDB с пътищата за обработка и обслужване, както и безопасно преминаване към жив трафик.
Ускорихме това усилие, като използвахме вътрешна система, която управлява рояк от кодиращи агенти. Системата е проектирана да бъде изключително устойчива; тя съхранява целите на проекта, активните рискове, историята на хранилището и предишните решения през сесиите (sessions).
Назначихме група от такива проактивни агенти, работещи денонощно, към проекта CobbleDB. Първата им задача беше да изградят точен модел на работата, която вече е в ход. Те одитираха проектантската тема (thread) и активните нишки, свързаха неизчистените елементи с техните pull request-и и отговорници и изготвиха актуална справка за състоянието на системата и следващите действия. Оттам нататък те действаха в няколко части на инженерния цикъл:
- Прегледан код и инфраструктурни промени.
- Изведени на преден план блокиращи проблеми, които лесно могат да бъдат пропуснати, включително несигурни предположения за възстановяване, остарели препратки към сборки и конфигурации, които биха довели до грешка по време на изпълнение.
- Подготвени целеви корекции, тестове, промени в мониторинга и оперативна документация.
- Проследени портове за CI, ревюта и инфраструктура, като същевременно се гарантира, че производствените действия са изрично собственост на хора.
- Наблюдавани пускания на пазара и упражнения за архивиране или възстановяване, след което наблюдаваното състояние на системата беше сравнено с критериите за успех на пускането.
- Поддържани справки по проекта, така че инженерите да виждат какво е пуснато, какво остава блокирано и къде все още се изисква човешка преценка.
Инженерите все пак задаваха архитектурата, преглеждаха важните промени и одобряваха производствените операции. Агентите се справяха с голяма част от непрекъснатите проверки и последващи действия между тези решения. Това позволи на малък екип (team) да движи проекта ефективно — основната инфраструктура на CobbleDB беше изградена от двама човешки инженери и стотици AI агенти за два месеца.
Заключение
CobbleDB не е база данни с общо предназначение; тя е създадена специално за нашето конкретно работно натоварване от повтарящи се пакетни четения на записи от подготвени страници, захранвани от асинхронен тръбопровод за обработка. Чрез разделяне на трайното състояние, доставянето на актуализации и обслужването по време на заявка можем да оптимизираме всяка отговорност поотделно, без да принуждаваме единен интерфейс на база данни да ги побира всичките или да прехвърляме работата по поддръжката към пътя за активни заявки. Мигрирането към CobbleDB значително подобри както средната, така и опашната латентност, като същевременно намали разходите за инфраструктура.
Продължавайки да разработваме AI-нативна инфраструктура собствено производство, можем да оптимизираме производителността и разходите по начин, който е невъзможен, когато сме обвързани с реализации и ценови модели на трети страни. Мощните AI агенти наклоняват везните още повече в полза на разработването пред закупуването.
Планираме скоро да направим CobbleDB с отворен код, така че други екипи (teams), изграждащи AI-нативни системи за търсене, да могат да се възползват от същия слой за съхранение, който прави търсенето по-бързо и по-евтино в рамките на нашите продукти.
Цитиране
@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}
}