Потоки событий под нагрузкой: буферы, брокеры и зачем разводить OLTP с OLAP

Представьте вечер пятницы: реклама крутится, в игре идёт акция, в панель «летят» клики по баннерам, спины и ставки. Каждый такой факт хочется сохранить — для биллинга, антифрода, отчётов для партнёров и графиков в админке. Самый простой путь — сразу писать строку в ту же PostgreSQL, где лежат кошельки и сессии. На маленьких объёмах это терпимо. На больших — вы получите рост задержек там, где деньги должны проходить мгновенно, и тяжёлые отчёты, которые мешают обычным транзакциям. Ниже — как обычно выкручиваются без героизма в эксплуатации.

Связанные материалы: Очереди и брокеры: Redis, RabbitMQ, Kafka · API gateway и обмен сообщениями · Sail: очереди и RabbitMQ

Содержание


Что именно ломается, если кормить OLTP каждым событием

OLTP (online transaction processing) — это база, где вы держите актуальное состояние: баланс, статус заказа, активная сессия. Её сильная сторона — короткие согласованные операции и индексы под ваши бизнес-запросы.

Поток событий другой по природе:

  • записей очень много, они часто дополняют друг друга (не обязательно обновлять одну строку — иногда важен сам факт «был клик»);
  • пишут много клиентов одновременно — получаются пики;
  • аналитики потом хотят сканировать диапазоны по времени, строить воронки, джойнить с справочниками — это уже не те паттерны доступа, под которые вы проектировали ядро продукта.

Если всё свалить в одну кучу, типичные симптомы такие: растёт p95/p99 латентности на критичных апдейтах, раздуваются индексы и автovacuum не успевает, бэкапы и репликация начинают отставать. В итоге страдает не «страница отчётов», а то место, где пользователь реально тратит деньги.


Слой приёма: сгладить пики и не блокировать запрос

Идея почти всегда одна: не заставлять пользовательский HTTP-запрос ждать полного пути до аналитического хранилища. Сначала событие попадает в устойчивый к всплескам слой, а дальше его забирают воркеры и раскладывают по назначению.

Практичные приёмы:

  1. Приём в API — валидация, обогащение контекстом (user id, кампания, устройство), присвоение идемпотентного ключа, если повторы возможны.
  2. Буфер — в памяти процесса (осторожно при рестартах), в Redis, в очереди сообщений, в лог-подобном хранилище. Важно понимать что потерять нельзя — тогда буфер должен быть персистентным и с репликацией.
  3. Обработка пачками — вместо «одна вставка — одно событие» писать батчи в целевую систему; уменьшается накладные расходы и давление на диск.
  4. Обратное давление (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 в продовой базе» почти всегда дешевле на старте и дороже на масштабе, чем явный развод с самого начала. Компромисс — отдельная схема или база на том же кластере с ограничением ресурсов и без тяжёлых джойнов из аналитики в ядро.


Короткий чек-лист перед выбором схемы

  1. Какая потеря допустима? Если ни одна — буфер должен быть персистентным, с репликацией и мониторингом лагов.
  2. Нужен ли порядок строго по пользователю или достаточно почти по времени? От этого зависят ключи партиционирования и выбор брокера.
  3. Сколько разных потребителей прочитают один и тот же поток? Чем их больше, тем чаще уместен лог с retention (Kafka и родственники).
  4. Где будут жить дашборды? Если там же, где кошелёк — закладывайте изоляцию (отдельные инстансы, лимиты statement_timeout, read-only реплики).
  5. Есть ли идемпотентность на приёме? Сети дублируют запросы; без ключа повторов вы раздуете счётчики и списания.

В следующих материалах раздела архитектуры разберём соседние темы: устойчивость интеграций, гонки при списании баланса и связку с конкретными паттернами в Laravel. Если вам нужен один вывод из этой статьи — пусть это будет такой: поток событий и деньги в одной корзине без буфера — осознанный риск, а не неизбежность.