The Daily Newsstand · Free, Always
Friday, September 11, 2026

Миллионы товаров и миллисекунды: как устроен специализированный движок каталога

Translate

Хочу показать проект, который сейчас запускаю для польского рынка: https://wszyst.pl.

Это агрегатор товаров из разных источников. Сейчас в системе уже больше миллиона реальных товаров, а архитектура рассчитана примерно на три миллиона.

Но на самом деле количество товаров здесь не самое интересное. Гораздо интереснее то, как быстро система работает с таким объёмом данных.

Например, обычная страница каталога с фильтрами, сортировкой и пагинацией обрабатывается примерно за 12 мс. Если необходимые данные уже находятся в горячем пути, время обработки может составлять порядка 0,1 - 0,7 мс.

Это не отдельный синтетический тест базы данных. Это время обработки реальных запросов приложения.

При этом я отдельно тестировал сам движок под нагрузкой. В предыдущих материалах я подробно разбирал результаты тестов, поэтому здесь не буду повторять весь benchmark.

Если коротко, система стабильно выдерживает около 50 тысяч запросов в секунду без ошибок и заметной деградации. В одном из десятиминутных тестов максимальное время ответа страницы оставалось около 0,2 секунды.

При дальнейшем увеличении нагрузки узким местом постепенно становится уже не поиск данных, а пропускная способность самого сервера и сети.

Почему это получилось

В основе проекта нет PostgreSQL, Elasticsearch или другой универсальной системы, которую обычно используют для подобных задач.

Вместо этого я сделал специализированное mmap-based key-value хранилище.

Причина довольно простая: мне не нужна была универсальная база данных.

У проекта очень специфический характер нагрузки. Данные постоянно поступают и индексируются, а затем многократно читаются. Пользователь в основном делает одно и то же: открывает каталог, выбирает категорию, применяет несколько фильтров, сортирует результат и переходит на следующую страницу.

Поэтому вместо того, чтобы заставлять универсальную базу каждый раз выполнять большое количество работы, я решил большую часть этой работы перенести на момент загрузки данных.

Индексы вместо поиска по данным

Поверх хранилища работают собственные индексы, которые я называю Turbo Indexes.

В упрощённом виде это отсортированные массивы идентификаторов документов.

Допустим, у нас есть:

category = phones
brand = Apple
availability = true

Каждому условию соответствует свой отсортированный список ID.

Дальше вместо построения сложных временных структур можно выполнить пересечение этих списков:

phones → Apple → available
        intersection

Для таких операций достаточно последовательного прохода по отсортированным данным.

То же самое относится к объединению условий и исключениям. Часть числовых условий, например диапазоны цен, также заранее представлена индексами, чтобы на чтении не приходилось просматривать сами записи товаров.

В результате основной путь запроса становится довольно коротким:

запрос → индексы → ID товаров → данные из mmap → JSON → HTTP

Самая важная идея находится не в индексе

Индексы сами по себе – только часть решения.

Самое существенное изменение архитектуры заключается в том, когда именно выполняется работа.

Обычная схема часто выглядит примерно так:

запрос пользователя → найти данные → отфильтровать → отсортировать → подготовить результат → вернуть

Я стараюсь делать наоборот:

поступление данных → нормализация → подготовка → построение индексов → сохранение

После этого запрос пользователя становится максимально простым:

запрос → индексы → ID → данные → ответ

Это означает, что большая часть вычислений происходит один раз — во время подготовки данных — вместо того чтобы повторяться для каждого пользователя.

У такого подхода есть очевидная обратная сторона.

Загрузка данных становится сложнее. Нужно проверять входную информацию, нормализовать её, строить индексы, следить за их согласованностью и обновлять их при изменениях.

Зато чтение становится очень предсказуемым.

И для каталога, который в основном читается, это оказывается хорошим обменом.

Хранилище и бизнес-логика

Есть ещё один эффект, который оказался для меня довольно важным.

Физическое хранение данных и логика работы каталога достаточно сильно разделены.

Если я завтра полностью изменю способ фильтрации, сортировки или формирования страниц, мне не обязательно менять само хранилище.

В большинстве случаев достаточно изменить индексы и способ их использования.

Это позволяет экспериментировать с бизнес-логикой, не превращая каждое изменение функциональности в изменение всей системы хранения.

Это, конечно, не универсальная архитектура.

Для обычного веб-приложения я бы не стал без необходимости писать собственную базу данных и собственную систему индексации.

Но когда задача очень узкая и хорошо известна заранее, иногда оказывается выгоднее оптимизировать не отдельный компонент, а весь путь прохождения данных.

Ещё один узкий участок – JSON

После того как поиск и чтение данных стали достаточно быстрыми, следующим заметным участком оказался JSON.

Для этого проекта я сделал отдельный инструмент – SilentJSON.

Это zero-allocation JSON encoder/decoder, рассчитанный на структуры с заранее известным layout.

Он намеренно не пытается заменить encoding/json во всех возможных сценариях.

У такого подхода есть ограничения. В частности, он плохо подходит для произвольных динамических структур, большого количества interface{} и других случаев, где структура данных заранее неизвестна.

Зато если структура предсказуема, можно отказаться от значительной части универсальности стандартного JSON-парсера и работать непосредственно с известными полями.

Особенно хорошо SilentJSON показывает себя при обработке больших JSON-файлов. Он поддерживает потоковую обработку и многопоточность, поэтому для некоторых задач импорта получается очень существенная разница.

Но это уже отдельная тема, и о SilentJSON я писал отдельно.

Что в итоге получилось

Сейчас wszyst.pl – это уже не просто эксперимент с базой данных.

Это попытка построить весь путь обработки каталога вокруг одной идеи: максимально перенести работу из момента запроса пользователя в момент подготовки данных.

В итоге получается довольно простой путь чтения:

индекс → ID → mmap → JSON → HTTP

При миллионах товаров этого оказалось достаточно, чтобы получить очень небольшое время обработки запроса и стабильную работу под высокой нагрузкой.

Сейчас в каталоге больше миллиона реальных товаров, а следующая цель – несколько миллионов.

Если интересно посмотреть, как это выглядит не в benchmark, а в работающем приложении, можно просто открыть https://wszyst.pl, выбрать категорию, применить фильтры, поменять сортировку и перейти по страницам.

Мне сейчас особенно интересны реальные сценарии использования. Если найдёте место, где каталог начинает заметно тормозить, это будет гораздо полезнее ещё одного синтетического теста.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.