Потоки событий под нагрузкой: буферы, брокеры и зачем разводить OLTP с OLAP
Представьте вечер пятницы: реклама крутится, в игре идёт акция, в панель «летят» клики по баннерам, спины и ставки. Каждый такой факт хочется сохранить — для биллинга, антифрода, отчётов для партнёров и графиков в админке. Самый простой путь — сразу писать строку в ту же PostgreSQL, где лежат кошельки и сессии. На маленьких объёмах это терпимо. На больших — вы получите рост задержек там, где деньги должны проходить мгновенно, и тяжёлые отчёты, которые мешают обычным транзакциям. Ниже — как обычно выкручиваются без героизма в эксплуатации.
Связанные материалы: Очереди и брокеры: Redis, RabbitMQ, Kafka · API gateway и обмен сообщениями · Sail: очереди и RabbitMQ
Содержание
- Что именно ломается, если кормить OLTP каждым событием
- Слой приёма: сгладить пики и не блокировать запрос
- Redis Streams и списки: когда хватает «лёгкого» буфера
- RabbitMQ и Kafka: две разные философии
- OLTP и OLAP: разделить роли хранилищ
- Короткий чек-лист перед выбором схемы
Что именно ломается, если кормить OLTP каждым событием
OLTP (online transaction processing) — это база, где вы держите актуальное состояние: баланс, статус заказа, активная сессия. Её сильная сторона — короткие согласованные операции и индексы под ваши бизнес-запросы.
Поток событий другой по природе:
- записей очень много, они часто дополняют друг друга (не обязательно обновлять одну строку — иногда важен сам факт «был клик»);
- пишут много клиентов одновременно — получаются пики;
- аналитики потом хотят сканировать диапазоны по времени, строить воронки, джойнить с справочниками — это уже не те паттерны доступа, под которые вы проектировали ядро продукта.
Если всё свалить в одну кучу, типичные симптомы такие: растёт p95/p99 латентности на критичных апдейтах, раздуваются индексы и автovacuum не успевает, бэкапы и репликация начинают отставать. В итоге страдает не «страница отчётов», а то место, где пользователь реально тратит деньги.
Слой приёма: сгладить пики и не блокировать запрос
Идея почти всегда одна: не заставлять пользовательский HTTP-запрос ждать полного пути до аналитического хранилища. Сначала событие попадает в устойчивый к всплескам слой, а дальше его забирают воркеры и раскладывают по назначению.
Практичные приёмы:
- Приём в API — валидация, обогащение контекстом (user id, кампания, устройство), присвоение идемпотентного ключа, если повторы возможны.
- Буфер — в памяти процесса (осторожно при рестартах), в Redis, в очереди сообщений, в лог-подобном хранилище. Важно понимать что потерять нельзя — тогда буфер должен быть персистентным и с репликацией.
- Обработка пачками — вместо «одна вставка — одно событие» писать батчи в целевую систему; уменьшается накладные расходы и давление на диск.
- Обратное давление (back-pressure) — если потребитель не успевает, не бесконечно накапливать в памяти: замедлить продьюсера, ответить «503 / retry later» на некритичных путях или сбрасывать в dead-letter с алертом.
Для Laravel-приложения естественная точка входа — очередь (dispatch job после минимальной записи) или отдельный сервис приёма, если трафик действительно большой. Не путайте «положили в Redis list» с «гарантированно не потеряли»: смотрите на AOF/replication и сценарии падения ноды.
Redis Streams и списки: когда хватает «лёгкого» буфера
Список (LPUSH + BRPOP или аналог) — простой канал «в стену»: один продьюсер кладёт, воркер забирает. Минус — нет встроенной модели consumer group как у полноценного брокера: если воркеров несколько, делите ключи сами или рискуете гонками. Плюс — вы уже, скорее всего, держите Redis под кэш или rate limit.
Redis Streams (команды XADD, XREADGROUP, XACK) ближе к логу с позициями: сообщениям можно давать ID, несколько групп потребителей читают поток независимо, есть pending для недообработанного. Это удобно, когда нужен буфер + порядок внутри ключа + несколько подписчиков без немедленного введения Kafka.
Ограничения Redis помните честно: это оперативная память (или дисковые модули, но это другая эксплуатация), размер кластера и политика eviction — если память кончится и ключи начнут вытесняться, вы потеряете события. Для критичного трейла обычно либо жёсткий лимит + мониторинг длины стрима, либо переход к брокеру с явным хранением на диске.
RabbitMQ и Kafka: две разные философии
RabbitMQ (модель очередей AMQP) хорошо знаком тем, кто уже гоняет Laravel Horizon / queue workers: понятные очереди, routing keys, TTL, dead-letter exchanges. Сильная сторона — гибкая маршрутизация и относительно простой операционный контур для средних объёмов. Слабое место при экстремальном throughput — может понадобиться тюнинг и осмысленное разделение очередей, чтобы одна тяжёлая очередь не застопорила всё.
Apache Kafka (лог партиций) другой по духу: вы пишете в топик, сообщения хранятся с политикой retention, потребители читают со смещения (offset). Это естественно ложится на огромные объёмы, много независимых читателей (разные сервисы читают один и тот же лог) и аудит «что произошло». Цена — другой стек эксплуатации, зоопарк конфигов и осознание, что модель мышления — «поток», а не «задача в очереди».
Выбор не в пользу «модного Kafka», а в пользу объёма, числа потребителей и требований к хранению истории. Для многих продуктов долго живёт связка: RabbitMQ для задач (отправить письмо, пересчитать баланс) и отдельный контур для сырых событий (Kafka, облачный аналог, или managed stream).
OLTP и OLAP: разделить роли хранилищ
OLAP (online analytical processing) здесь — не обязательно отдельный продукт с буквами OLAP на коробке, а хранилище и схема под тяжёлые чтения по большим диапазонам: витрины, колоночные движки, облачные DWH, иногда просто отдельная PostgreSQL с другими индексами и без смешивания с горячими транзакциями.
Типовая схема:
- OLTP остаётся источником правды для денег и состояний;
- события улетают в поток или очередь;
- воркер или ELT-пайплайн складывает их в аналитическое хранилище (часто с денормализацией под конкретные дашборды);
- отчёты и панели читают только аналитический контур; если нужны свежие цифры — материализованные представления, периодический refresh или потоковая агрегация.
Перенос «всё в одну таблицу events в продовой базе» почти всегда дешевле на старте и дороже на масштабе, чем явный развод с самого начала. Компромисс — отдельная схема или база на том же кластере с ограничением ресурсов и без тяжёлых джойнов из аналитики в ядро.
Короткий чек-лист перед выбором схемы
- Какая потеря допустима? Если ни одна — буфер должен быть персистентным, с репликацией и мониторингом лагов.
- Нужен ли порядок строго по пользователю или достаточно почти по времени? От этого зависят ключи партиционирования и выбор брокера.
- Сколько разных потребителей прочитают один и тот же поток? Чем их больше, тем чаще уместен лог с retention (Kafka и родственники).
- Где будут жить дашборды? Если там же, где кошелёк — закладывайте изоляцию (отдельные инстансы, лимиты
statement_timeout, read-only реплики). - Есть ли идемпотентность на приёме? Сети дублируют запросы; без ключа повторов вы раздуете счётчики и списания.
В следующих материалах раздела архитектуры разберём соседние темы: устойчивость интеграций, гонки при списании баланса и связку с конкретными паттернами в Laravel. Если вам нужен один вывод из этой статьи — пусть это будет такой: поток событий и деньги в одной корзине без буфера — осознанный риск, а не неизбежность.