Мільйони подій без колапсу основної бази

У спокійний день все здається простим: подія прийшла — зберегли рядок. А потім злітає кампанія, гравці масово натискають кнопки, партнери просять сирі логи показів, і ваш PostgreSQL, де вже сидять гаманці й сесії, раптом отримує ще один потік записів — без окремого дихання для диска й індексів. Найкоротший шлях «все в одну базу» часто перший, який доводиться розбирати ніччю. Нижче — робочі прийоми без обіцянок «вічної масштабованості».

Пов’язані матеріали: Черги й брокери: Redis, RabbitMQ, Kafka · API gateway і обмін повідомленнями · Sail: черги та RabbitMQ

Зміст


Чому змішувати телеметрію з грошима небезпечно

OLTP заточений під короткі узгоджені операції: зняти кошти, змінити статус, перевірити унікальність. Індекси й обслуговування таблиць розраховані саме на цей ритм.

Потік подій інший:

  • записів багато, вони часто не зводяться до одного оновлення рядка — важливий сам факт «сталося о ось такій миті»;
  • навантаження нерівномірне, піки різко піднімають конкуренцію за буфери та WAL;
  • для аналітики потрібні скани за часом, воронки, з’єднання з довідниками — це вже не ті запити, під які проєктували ядро.

Якщо все в одній купі, зазвичай ростуть хвости латентності на критичних транзакціях, роздуваються індекси, відстає реплікація. Зовні це виглядає як «повільні звіти», а всередині страждає те, де користувач платить або ставить.


Як приймати події, не блокуючи клієнта

Логіка проста: HTTP-відповідь не повинна чекати, поки подія ляже в остаточне сховище для BI. Спочатку — надійний проміжний шар, далі — воркери.

Що зазвичай роблять:

  1. На краї — перевірка формату, контекст (користувач, кампанія, пристрій), за потреби ключ ідемпотентності, щоб повтори мережі не подвоїли наслідки.
  2. Буфер — не нескінченна RAM у процесі, а Redis з політикою персистентності, брокер або інший append-friendly шар, ризик втрати якого ви назвали вголос.
  3. Пакетний запис у ціль — менше поїздок і менше тиску на диск, ніж «рядок за рядком».
  4. Back-pressure — якщо споживач відстав, обмежити чергу, сповільнити продьюсера або відсіяти в dead letter з алертом, а не ловити OOM.

У Laravel природно виглядає черга після легкої валідації. Якщо RPS величезний, часто виокремлюють сервіс прийому. Важливо не плутати: запис у Redis-list без реплікації й AOF — це не те саме, що аудиторський слід, який переживе падіння вузла.


Redis: списки й стріми як амортизатор

Список (типу LPUSH / BRPOP) — мінімальний канал: поклали, забрали. Зручно, якщо Redis уже є під кеш або rate limit. Але кілька воркерів без дублікатів доведеться проєктувати самим — у брокера це вже «з коробки» для черги.

Streams (XADD, XREADGROUP, XACK) додають групи споживачів, ідентифікатори повідомлень і pending для завислих. Це ближче до короткого журналу: порядок у межах ключа, кілька незалежних читачів, без обов’язкового стрибка в Kafka «завтра».

Обмеження ті самі: пам’ять, eviction. Якщо політика витіснення зачепить ключі з подіями — частина історії зникне. Для критичних шляхів — ліміти, моніторинг довжини стріму, або перехід на дисковий брокер.


Черга завдань проти журналу подій: RabbitMQ і Kafka

RabbitMQ — це маршрутизація й черги: routing keys, TTL, dead-letter, знайома модель для Laravel Horizon. Добре лягає на задачі (лист, вебхук, перерахунок) і середні обсяги; при екстремальному throughput доведеться дробити черги і тюнити кластер.

Kafka — це партиційний лог з retention і offset. Зручно, коли багато сервісів читають одну й ту саму стрічку, потрібна історія й повторне програвання. Ціна — інша експлуатація й мислення потоком, а не «одна джоба виконана».

Вибір — не про моду. Це обсяг, кількість читачів, скільки часу зберігати стрічку і хто вміє це підтримувати. Часто довго живе тандем: Rabbit для імперативних задач, Kafka (або керований аналог) для сирих подій.


Де має жити аналітика

OLAP тут — узагальнення: будь-яке сховище під важкі читання великих діапазонів — колонковий DWH, озеро, або окрема Postgres з іншими індексами, без джойнів звітів до гарячих таблиць гаманця.

Типовий ланцюжок: OLTP лишається джерелом правди для грошей → події йдуть у потік/чергуворкер або ELT наповнює вітрини → дашборди читають лише цей контур. «Свіженькі» цифри — через матеріалізовані уявлення, розклад або стрімінгову агрегацію.

Одна таблиця events у продовій базі економить тижні на старті й може коштувати місяців рефакторингу пізніше. Компроміс — окрема база чи схема з лімітами ресурсів і без важких запитів з BI в ядро.


Що уточнити до вибору схеми

  1. Чи можна втратити хоч один івент? Якщо ні — буфер має бути персистентним, реплікованим, з моніторингом відставання.
  2. Наскільки жорсткий порядок — по користувачу, по сесії, чи достатньо часу з похибкою?
  3. Скільки різних споживачів прочитають той самий потік? Чим більше — тим частіше виграє лог із збереженням.
  4. Де малюються графіки? Якщо там же, де баланс — потрібна ізоляція: репліка read-only, таймаути, окремий інстанс.
  5. Чи є ідемпотентність на вході? Без неї мережеві повтори псують метрики й облік.

Далі в розділі архітектури розберемо стійкі інтеграції, гонки при списанні і практичні патерни в Laravel. Головна думка: злити потік подій і грошовий контур без буфера — це рішення з відомою ціною, а не технічна необхідність.