Мониторим и дебажим сложные бизнес-транзакции
В распределенных системах бизнес-транзакции редко совпадают с классическими ACID-транзакциями, ну это #яснопонятно(👆про это постом выше 👆). Помним про один из ключевых способов, решающих эту задачу, — Saga; обсудив различные ее виды, включая разницу между оркестрацией и хореографией, мы так и не ответили на вопросы что делать, когда «все ложится звездой» и «как отследить выполнение бизнес-транзакции, размазанной по нескольким сервисам»? Сама концепция шаблона подразумевает использование множества атомарных шагов, каждый из которых выполняется(или нет) в отдельном сервисе. Тут все ок, но что делать если: 1️⃣ Шаг завершился с ошибкой, а компенсация не сработала. 2️⃣ Сообщение потерялось, из-за чего процесс завис в промежуточном состоянии. 3️⃣ Компенсация запустилась поздно или не запустилась вовсе.
🤷♂️🤯🤷♂️🤯🤷♂️🤯
Для начала стоит вообще поговорить, как бы нам упростить себе жизнь в «распределе», а затем ответить на вопросы выше?
1️⃣ Outbox Pattern Известная проблема Dual Write Problem решается довольно изящно и просто с помощью Outbox Pattern, т.е:
- Вместо прямой отправки события в брокер сообщений, сервис сначала записывает его в отдельную таблицу БД.
- Фоновый процесс читает из этой таблицы и отправляет сообщения в брокер.
- Гарантирует, что сообщение либо будет обработано, либо останется в БД для повторной попытки.
2️⃣ Retry & Backoff Strategy Некоторые ошибки (например, временная недоступность сервиса) можно исправить повторной попыткой, это так, но простые ретраи могут создать дополнительную нагрузку, поэтому стоит обратить внимание на:
- Exponential Backoff — инкриментально увеличивает паузу между попытками достучаться до внешней системы.
- Jitter — добавлять случайность в задержку, чтобы избежать лавинообразных нагрузок.
- Dead Letter Queue (DLQ) — отправлять сообщения в отдельную очередь, если они не обработаны после нескольких попыток, мол «последний привет из ада»
3️⃣ Idempotency Key Чтобы избежать повторного выполнения одного и того же шага в Saga из-за сбоев сети или дублирования сообщений, стоит использовать идемпотентные операции:
- Использовать идемпотентные ключи (например, уникальный
TransactionID). - Проверять, был ли уже обработан этот шаг.
- Перед обработчиком запросов извне применять шаблон Inbox.
- Не выполнять повторные действия, если они уже зафиксированы в БД. (ON CONFLICT DO NOTHING)
4️⃣ Distributed Tracing Jaeger, Zipkin, OpenTelemetry помогают визуализировать поток запросов между сервисами, что добавляет:
- Наглядность в последовательности шагов.
- Выявление узких места и задержки.
- Отслеживание, на каком шаге произошел сбой.
5️⃣ Monitoring & Alerting Тут подход очень простой - мы должны первыми знать, если в системе что-то идет не так, поэтому метрики - ваше все. Что будет полезным собирать?
- Количество успешных / неуспешных завершений Saga.
- Среднее время выполнения бизнес-транзакции.
- Количество запущенных компенсационных транзакций.
- Зависшие процессы, которые требуют ручного вмешательства.
Итак: ❗Шаг завершился с ошибкой, а компенсация не сработала: добавляем метрики, реализуем повторную доссылку компенсаций, на принимающей стороне компенсаций внедряем OUTBOX и/или DLQ ❗Сообщение потерялось, из-за чего процесс завис в промежуточном состоянии: добавляем метрики и мониторинг ❗Компенсация запустилась поздно или не запустилась вовсе: мониторим, DLQ в случае наступления дедлайна для реагирования #dev #arch #softwareengineering