Привет!

Хочу показать проект, который сейчас запускаю для польского рынка: 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, выбрать категорию, применить фильтры, поменять сортировку и перейти по страницам.

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