CobbleDB: перестройка хранилища ИИ-поиска для снижения задержек и затрат
Девяносто символов: разделенное неизменяемое состояние, пакетные обновления и поиск во время запроса.
AI-нативный поиск предъявляет разные требования к тому, как веб-контент должен обрабатываться, храниться и обслуживаться. Чтобы удовлетворить эти требования в масштабе, мы перестраиваем весь стек с нуля для повышения производительности и снижения затрат.
Мы создали собственную инфраструктуру краулинга, индексации и ранжирования, модели эмбеддингов и движки инференса, чтобы обеспечить эффективную обработку и быстрое обслуживание. Теперь мы распространяем этот подход на хранение — связующее звено, в которое обработка осуществляет запись, а обслуживание — чтение.
Управляемые базы данных, такие как DynamoDB, обеспечивали надежное хранение, но мы имели ограниченный контроль над тем, как хранятся данные и обслуживается чтение, а тарификация на основе использования росла вместе с размером нашего индекса и объемом запросов. Кроме того, наш предыдущий конвейер связывал обработку документов напрямую с базой данных обслуживания, что затрудняло независимое управление обновлениями от живых чтений.
С помощью нового внутреннего роя агентов кодирования мы перестроили этот слой вокруг CobbleDB — горячего хранилища «ключ-значение», поддерживаемого Pillar для хранения неизменяемого состояния документов и публикации, а также Lorry для пакетных обновлений. Редизайн отделяет обработку документов от приема в горячее хранилище и позволяет нам настраивать обслуживание под нашу рабочую нагрузку. Производственные измерения до и после показывают примерно в 5 раз меньшую задержку пакетного чтения, а наша внутренняя модель затрат оценивает экономию как минимум в 20% по сравнению с DynamoDB.
Инфраструктура хранения для AI-нативного поиска
Классический поисковый стек создан для человека-читателя, а не для языковой модели. Это различие меняет уровень детализации, с которым системе необходимо подготавливать, хранить и обслуживать веб-контент.
Чтобы превратить сырой HTML в контент, готовый для моделей, наш конвейер обработки очищает каждую страницу, разбивает ее на семантически связанные отрывки, вычисляет их эмбеддинги и сохраняет отрывки вместе с эмбеддингами в базе данных. Во время запроса конвейер обслуживания выбирает текст, наиболее релевантный запросу, и передает его модели.
Два разных рабочих процесса
Поэтому уровень хранения должен поддерживать два рабочих процесса с разными требованиями: прием обновлений из конвейера обработки и обслуживание чтения с низкими задержками.
Со стороны записи новые страницы поступают непрерывно, существующие страницы меняются, а изменения методов чанкирования или моделей эмбеддингов могут потребовать повторной обработки значительных частей корпуса. Уровень хранения должен интегрировать эти обновления в неизменяемое состояние документа, отслеживать текущие версии, публиковать правильные представления в нижестоящие системы и поддерживать крупные бэкфиллы без повторения всего конвейера из сырого краула.
В отличие от этого, чтение во время запроса запрашивает пакеты ключей страниц и извлекает соответствующий текст отрывков и эмбеддинги для ранжирования и выбора отрывков. Эти чтения находятся на пути к окончательному ответу, поэтому они должны выполняться быстро.
Эти рабочие нагрузки требуют разного проектирования. Обработка выигрывает от надежных очередей и возможности эффективно доставлять обновления, повторять попытки при сбоях и актуализировать существующие записи в больших масштабах. Обслуживание выигрывает от предвычисленных записей, теплых кэшей и быстрого пакетного чтения с минимальной работой во время запросов. Тесная связь между ними может привести к тому, что работа по восстановлению и крупные обновления станут конкурировать с чтениями, чувствительными к задержкам.
В чем управляемая база данных не справляется
DynamoDB обеспечила нам высокодоступное хранилище без необходимости администрирования распределенной базы данных. Выделенная таблица подготовленных страниц содержала фрагменты и векторные представления (эмбеддинги) для чтения во время выполнения запросов, в то время как полный контент страниц и метаданные хранились отдельно. Однако по мере роста рабочей нагрузки стали важны три ограничения.
Во-первых, тарификация на основе использования взимается за каждый прочитанный или записанный байт. Один запрос Search API содержит 100–120 ключей, соответствующих страницам, которые нам нужно получить. Процесс извлечения данных делит запрос на более мелкие пакеты по 10–20 ключей страниц при среднем размере элемента около 50 КБ. Многократное чтение этих записей потребляло значительные ресурсы, в то время как непрерывный краулинг и изменения в моделях чанкирования или эмбеддингов генерировали больше записей. Экономика также зависела от размера и частоты записей, а не только от количества вызовов API.
Во-вторых, управляемый сервис не обладает гибкостью, необходимой нам для настройки производительности. Требуемая нам операция была узкой: получив пакет идентификаторов страниц, быстро вернуть их подготовленные данные. Мы хотели контролировать, какая машина удерживает партицию, сколько памяти выделено под ее кэш и какая реплика обрабатывает запрос. Эти средства управления важны, потому что некэшированное чтение, межзонный переход или медленная реплика могут задержать весь пакет. DynamoDB управляет собственным хранилищем и поведением реплик за пределами API. Мы могли настраивать сервис, но не могли оптимизировать его внутреннее размещение, кэши подсистемы хранения или политику выбора реплик под нашу рабочую нагрузку.
В-третьих, обработка была напрямую связана с записью в горячее хранилище. Наш предыдущий конвейер обработки записывал каждую подготовленную страницу напрямую в DynamoDB. Поэтому крупная задача переработки превращалась в волну индивидуальных обновлений горячего хранилища, и обработка была вынуждена подстраиваться под лимиты пропускной способности горячего хранилища и поведение повторных попыток. Мы не могли запустить MapReduce по всей базе данных за 1–2 дня, чтобы опробовать новый метод чанкирования, изменить модель эмбеддингов или добавить новое поле. Не существовало независимого потока сохраненных обновлений страниц, который отдельный сервис мог бы пакетировать и доставлять в горячее хранилище в своем собственном темпе.
Устранение этих ограничений требовало большего, чем просто замена базы данных. Нам нужно было отделить неизменяемое состояние документа и доставку обновлений от хранилища, обслуживающего запросы, а затем оптимизировать горячее хранилище специально для быстрого пакетного чтения.
Разделение состояния документов, доставки обновлений и обслуживания
Новая архитектура назначает каждую задачу отдельному компоненту. Pillar поддерживает неизменяемое состояние документов и решает, что публиковать. Lorry превращает эти экспорты в партиционированные батчи. CobbleDB принимает батчи и обслуживает подготовленные записи по ключу страницы.
CobbleDB: настраиваемый путь чтения
CobbleDB — это наше горячее хранилище, оптимизированное для обслуживания подготовленных записей во время запросов. Это распределенное хранилище «ключ-значение»: ключи являются идентификаторами страниц (хешированными URL-адресами), а значения — обработанным представлением страницы (ее предварительно разбитые на чанки отрывки и векторные эмбеддинги для каждого чанка).
Важным изменением является наш контроль над конфигурацией хранения.
Данные секционируются и распределяются по узлам данных. Такой распределенный формат гарантирует, что база данных может масштабироваться горизонтально для размещения огромного корпуса обработанных страниц. Каждая партиция имеет три реплики на трех разных узлах; если одна копия становится недоступной, остальные могут продолжать обслуживание чтения.
Каждому узлу данных назначается подмножество партиций. Узел хранит свои данные с помощью RocksDB — встроенного хранилища типа «ключ-значение» с открытым исходным кодом, хорошо подходящего для рабочих нагрузок с интенсивным чтением, подпитываемых пакетной загрузкой.
Кэшированные данные могут обслуживаться из памяти, в то время как некэшированное чтение использует локальное NVMe-хранилище. Это дает нам прямой способ настройки баланса между памятью и диском для наблюдаемой рабочей нагрузки, вместо принятия внутренней политики кэширования управляемого сервиса.
Бессерверный роутер запросов хеширует ключи по партициям и параллельно направляет запросы на узлы. Архитектура также включает в себя службу метаданных на базе PostgreSQL и надежное хранилище объектов.
Такая архитектура поддерживает шаблоны чтения, требуемые нашей рабочей нагрузкой. После того как запрос проходит через извлечение и ранжирование для получения пула релевантных страниц, к горячему хранилищу выполняется пакетный запрос содержимого этих страниц. Эти запросы на чтение поступают на роутер и направляются на соответствующий узел данных. Роутер отдает приоритет отправке запросов на узел в той же зоне доступности, чтобы по возможности минимизировать задержку между зонами. Каждый узел данных может получить содержимое страниц с помощью MultiGet — API пакетного чтения RocksDB, предназначенного для получения нескольких ключей одновременно с низкой задержкой. Если узел отвечает медленно, система также может запросить другую реплику партиции на другом узле, что улучшает задержку в хвосте распределения.
Мы также можем избежать реализации общих функций, которые не требуются в нашем рабочем процессе. Например, большинство баз данных предлагают гарантии согласованности и поддержку транзакций, но нашему горячему хранилищу не требуются транзакции или синхронизированные реплики. Допустим небольшой разрыв между моментом записи документа и моментом его доступности для чтения, а также нормально, если одна реплика принимает данные быстрее другой. Отказ от ненужных функций снижает накладные расходы и стоимость нашей системы.
Pillar и Lorry: надежный путь записи
Отдельный уровень хранения отслеживает всё, что мы сканируем и обрабатываем, объединяя обновления в версию состояния документов и публикуя необходимые представления в CobbleDB и поисковый индекс. Для этого мы создали Pillar — наш уровень состояния и публикации документов.
Pillar работает на базе YTsaurus. Он хранит компоненты страниц (такие как метаданные, чанки и эмбеддинги) в отдельных семействах таблиц YTsaurus. Он также отслеживает версии чанков и эмбеддингов, позволяя нескольким представлениям сосуществовать без перезаписи друг друга, чтобы базовые модели могли меняться. Поскольку Pillar и YTsaurus используют недорогие HDD-хранилища, мы можем хранить гораздо больше документов, чем раньше хранили в DynamoDB.
Pillar отслеживает подмножества, или определяемые политикой группы страниц, такие как свежие страницы или страницы высокой ценности. Членство в подмножестве определяет, какие обновления нужно передать нижестоящим компонентам вроде CobbleDB. CobbleDB использует более дорогое NVMe-хранилище, поэтому мы экспортируем только те документы, которые действительно хотим использовать в результатах поиска. Когда содержимое страницы обновляется или она добавляется/удаляется из подмножества, Pillar отвечает за постановку соответствующей задачи экспорта в очередь.
Атомарные транзакции YTsaurus охватывают состояние, намерения экспорта и потребляемые входные данные для обеспечения корректности. Для успешного завершения транзакции система должна обновить неизменяемое состояние, отметить входные данные как учтенные и поставить в очередь все необходимые экспортные операции; в случае прерывания транзакции изменения не вносятся ни на одном уровне.
CobbleDB ожидает, что обновления будут разбиты на батчи в соответствии с партициями. Экспорты Pillar преобразуются в этот формат с помощью безсерверного сервиса, который мы называем Lorry. Его единственная задача — читать записи экспорта из персистентной очереди, выровненной по партициям, аккумулировать их в пакетные файлы для каждой партиции и передавать в CobbleDB.
Между Lorry и Cobble дополнительный протокол отделяет плоскость данных (S3) от плоскости управления. Lorry записывает пакет в S3 под уникальным идентификатором и регистрирует пакет внутри Cobble. Реплики партиций Cobble независимо запрашивают пакет или API плоскости управления для получения следующего пакета S3 для приема и асинхронно применяют пакетные обновления в хронологическом порядке. Это означает, что медленный или восстанавливающийся узел может догнать остальные в своем собственном темпе, а не замедлять работу остальной системы.
Новый путь отделяет мгновенную транзакцию обработки от приема в горячее хранилище и обеспечивает возможность повторного прогона для инкрементальных обновлений и массовых перестроений. Такое разделение труда гарантирует, что обновления неизменяемого состояния документов не будут мешать задержке пути чтения.
Валидация
в 5 раз быстрее
После миграции горячего хранилища медианная задержка пакетного чтения снизилась с 31,4 мс до 5,60 мс. Показатель p90 снизился с 56,7 мс до 9,77 мс, а p99 — со 123 мс до 24,2 мс. Соотношение составляет от 5,08× до 5,80×, при этом улучшения наблюдаются как для типичных запросов, так и для хвоста распределения.
Эти цифры получены на основе пакетных чтений, где каждый запрос запрашивает примерно 10–15 ключей. Средний размер полезной нагрузки составляет 50 КБ. Обе системы опрашивались примерно на скорости 200 тыс. rps, хотя мы также проводили нагрузочные тесты до 500 тыс. rps без деградации.
Чтобы изолировать вклад CobbleDB, все остальные аспекты настройки оставались неизменными. Тем не менее, это остается обсервационным сравнением до и после; системы обслуживали реальный производственный трафик в разное время, а не одни и те же запросы в контролируемых условиях. Поэтому мы также измерили производительность обеих систем на синтетическом бенчмарке. Чтобы приблизить запросы к тем, что возникают в продакшене, мы выполняли к ним запросы пакетами по 10–15 ключей со значениями размером от 100 Б до 100 КиБ.
DynamoDB с трудом справляется в этом контексте, поскольку ее путь управляемого чтения не может быть настроен для пакетного чтения такого размера. Напротив, CobbleDB оптимизирована именно под такие сценарии, так как мы управляем размещением, кэшированием и элементами управления репликами. Она группирует ключи по партициям, выполняет чтение параллельно для реплик партиций и может хеджировать медленные чтения за счет других реплик. Наши результаты показывают, что эти архитектурные решения трансформируются в более быстрое чтение в производственной среде.
на 20% дешевле
CobbleDB также значительно дешевле DynamoDB. В дополнение к хранению, DynamoDB также взимает плату за каждый прочитанный или записанный байт. В масштабах нашего корпуса и обслуживаемого трафика эти расходы на основе использования становятся значительной частью общих затрат. Непрерывные обновления потребляют емкость записи, в то время как каждый батч подготовленных страниц, извлеченных для запроса, потребляет емкость чтения.
Мы рассчитали экономию средств на основе внутренних оценок размера хранилища, единиц емкости записи и единиц емкости чтения. На любом уровне обязательств CobbleDB как минимум на 20% дешевле DynamoDB. Реальная экономия, вероятно, еще выше, так как эти результаты не учитывают потенциальную экономию на резервном копировании за счет сжатия.
Как рой ИИ-агентов помогает нам создавать ее
Создание CobbleDB и сопутствующей инфраструктуры было далеко не простой задачей. Только ядро базы данных CobbleDB насчитывает около 40 000 строк кода на Rust, а миграция также потребовала масштабной работы по проверке корректности и поведения при сбоях в масштабах продакшена, интеграции CobbleDB с путями обработки и обслуживания, а также безопасному переводу живого трафика.
Мы ускорили эту работу с помощью внутренней системы, управляющей роем агентов кодирования. Система спроектирована с расчетом на высокую степень персистентности; она сохраняет цели проекта, активные риски, историю репозитория и предыдущие решения между сеансами.
Мы закрепили за проектом CobbleDB группу таких проактивных, постоянно работающих агентов. Их первой задачей было построить точную модель уже выполняющейся работы. Они провели аудит треда проекта и активных веток, связали нерешенные задачи с их pull request и авторами, а также подготовили актуальную сводку о работоспособности системы и дальнейших действиях. После этого они задействовали несколько частей инженерного цикла:
- Просмотрены изменения кода и инфраструктуры.
- Выявлены легко пропускаемые блокеры, включая небезопасные допущения восстановления, устаревшие ссылки на сборки и конфигурации, которые привели бы к сбою во время выполнения.
- Подготовлены целевые исправления, тесты, изменения мониторинга и операционная документация.
- Отслеживались CI, ревью и инфраструктурные шлюзы, при этом производственные действия оставались явно под контролем человека.
- Проводились мониторинг развертываний и упражнения по резервному копированию и восстановлению с последующим сравнением наблюдаемого состояния системы с критериями успешности развертывания.
- Поддерживались сводки по проекту, чтобы инженеры могли видеть, что было отправлено в продакшн, что осталось заблокированным и где все еще требуется экспертное мнение человека.
Инженеры по-прежнему задают архитектуру, проводят ревью значимых изменений и санкционируют производственные операции. Агенты выполняли большую часть непрерывного контроля и сопровождения между этими решениями. Это позволило небольшой команде эффективно продвигать проект — основная инфраструктура CobbleDB была создана двумя инженерами-людьми и сотнями ИИ-агентов за два месяца.
Заключение
CobbleDB — это не универсальная база данных; она создана специально под нашу конкретную рабочую нагрузку, состоящую из повторяющихся пакетных чтений записей подготовленных страниц, которые поступают из асинхронного конвейера обработки. Разделив неизменяемое состояние, доставку обновлений и обслуживание во время запроса, мы можем оптимизировать каждую задачу независимо, не заставляя единый интерфейс базы данных удовлетворять их все и не перекладывая обслуживание на путь живых запросов. Миграция на CobbleDB значительно улучшила как медианную задержку, так и задержку в хвосте распределения, одновременно снизив затраты на инфраструктуру.
Продолжая собственную разработку AI-нативной инфраструктуры, мы можем оптимизировать производительность и затраты так, как это невозможно привязанным к сторонним реализациям и ценовым моделям образом. Мощные ИИ-агенты еще больше смещают баланс в пользу создания, а не покупки.
Мы планируем вскоре открыть исходный код CobbleDB, чтобы другие команды, создающие 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}
}