Очереди и вебхуки: как не потерять сообщения
Как только в системе появляется слово «уведомить», «синхронизировать» или «обработать в фоне», появляются очереди. А вместе с ними — целый класс проблем, которые не воспроизводятся на тестовом стенде и приходят в три часа ночи.
Общее правило, из которого мы исходим: доставка «ровно один раз» — это миф. На практике доступны две гарантии: «не более одного раза» (можно потерять) и «хотя бы один раз» (можно получить дубль). Почти всегда выбирают вторую и делают обработчик устойчивым к дублям. Дубль пережить можно, потерянную оплату — нет.
Отсюда первое требование: идемпотентность обработчика. Одно и то же событие, обработанное дважды, должно давать тот же результат, что и обработанное один раз. Простейшая реализация — таблица обработанных идентификаторов событий: перед работой проверяем, не видели ли уже, после работы записываем. Важно, чтобы запись факта обработки и сама работа были в одной транзакции, иначе остаётся окно, в котором можно упасть между ними.
Второе — ретраи с экспоненциальной задержкой. Внешний сервис не ответил — не долбим его в цикле, а откладываем: через секунду, через две, через четыре, через восемь. Без этого ваша система превращается в инструмент добивания уже упавшего сервиса, и он не поднимется, пока вы не перестанете.
К ретраям обязательно нужен потолок. Бесконечно повторяемая задача, которая не может выполниться в принципе — например, ссылается на удалённый объект, — будет крутиться вечно и займёт воркеры. Поэтому третье: dead-letter очередь. После N неудачных попыток сообщение уходит туда и ждёт человека.
Про dead-letter стоит сказать отдельно, потому что её обычно заводят и забывают. Очередь, в которую никто не смотрит, — это просто медленный способ терять данные. На неё нужен алерт: появилось сообщение — кто-то должен узнать. Мы обычно шлём в рабочий чат.
Отдельная ловушка — порядок сообщений. Многие очереди не гарантируют порядок вообще, а те, что гарантируют, делают это только внутри раздела. Если у вас событие «заказ создан» может прийти после «заказ оплачен», обработчик должен это пережить. Обычно решается тем, что события несут состояние целиком, а не дельту, либо номер версии, и обработчик игнорирует устаревшее.
Теперь про вебхуки, то есть про случай, когда события к вам приходят снаружи. Здесь добавляется то, чего нет во внутренней очереди: вы не контролируете отправителя.
Мы всегда принимаем вебхук в две стадии. Первая: проверить подпись, положить сырое тело в хранилище, ответить 200. Вторая: обработать асинхронно. Соблазн обработать сразу в обработчике велик, но тогда любая ваша внутренняя ошибка превращается в неуспешную доставку у отправителя, а некоторые сервисы после нескольких неудач отключают вебхук совсем.
Сырое тело сохраняем всегда. Когда через месяц выяснится, что события разбирались неправильно, единственный способ восстановить данные — переразобрать оригиналы. Если их нет, данные потеряны навсегда.
Подпись проверяем до разбора. Эндпоинт вебхука открыт наружу, и туда прилетает всякое.
Из практики: почти все инциденты с очередями, которые мы разбирали, сводились к трём вещам. Обработчик не был идемпотентным, и дубль создал вторую запись. Не было потолка ретраев, и одна плохая задача забила всех воркеров. Или dead-letter была, но в неё никто не смотрел.
Ничего экзотического. Надёжная обработка событий — это не выбор брокера, а несколько дисциплин, которые проще заложить сразу, чем добавлять после первого ночного инцидента.
Понравилась статья?
У нас много таких историй — и ещё больше идей для вашего продукта.
Обсудить проектЧитать дальше
Как мы пришли к ScanClick
Продукт вырос из чужой боли, которую мы увидели на клиентском проекте: склад, тетрадь и три часа сверки каждый вечер.
Как мы держим нагрузку на Go-бэкенде
История одного сервиса: от 200 rps до 4000 rps без переписывания. Очереди, пулы и честный профайлинг.
Архитектура API с нуля: наш подход
Контракты, версионирование и ошибки, которые не стыдно отдавать наружу. Делимся структурой, которая работает.