Чтобы сообщение между системами не терялось и не двоилось, нужны две вещи. На стороне отправителя сообщение записывается в базу вместе с данными (это и есть outbox) и повторяется, пока не придёт подтверждение. На стороне получателя запоминается, что уже обработано, поэтому повтор не создаёт второй заказ. Одной из двух частей недостаточно: без первой сообщения пропадают, без второй двоятся.
Ниже на условном примере показано, почему «просто отправить» не работает, какие бывают сбои, как устроены сами приёмы и что спросить у исполнителя до договора. Услуга по построению такого обмена описана на странице «Гарантированная доставка сообщений», здесь только объяснение.
Почему нельзя просто отправить и забыть
Возьмём условный пример: интернет-магазин принял заказ и должен передать его в учётную систему. Код выглядит естественно: сохранить заказ в базе, затем отправить сообщение в учёт. Две строки, и обе работают. Проблемы начинаются между ними и после них.
- Процесс может упасть после сохранения и до отправки. Заказ в базе есть, в учёте нет. Автоматически никто не заметит.
- Учёт может принять заказ и не успеть ответить. Магазин считает, что отправка не удалась, и отправляет снова. В учёте два заказа.
- Учёт может быть недоступен час. Если магазин отправляет один раз и сдаётся, заказ потерян, если повторяет без пауз и без предела, он добивает систему, которая только что поднялась.
Ни один из этих сбоев не требует редкой аварии. Это обычное поведение сетей и процессов: перезапуск после обновления, нехватка памяти, обрыв соединения. Поэтому честный вопрос звучит не «как сделать, чтобы не ломалось», а «что произойдёт, когда сломается, и увидит ли это кто-нибудь».
Слово «гарантированная» означает ответ на этот вопрос: сообщение сохранено, доставляется до подтверждения, обрабатывается один раз по эффекту, а то, что не удалось, лежит в списке и ждёт решения человека.
Четыре момента, когда сообщение пропадает
Путь сообщения состоит из нескольких остановок. На каждой есть свой способ потери, поэтому защиту нужно ставить на каждой, а не «где-нибудь в середине». На условном примере с заказом это выглядит так.
| Момент | Что случилось | Что видит бизнес | Чем закрывается |
|---|---|---|---|
| Между записью и отправкой | Магазин сохранил заказ и перезапустился до отправки | Заказ есть на сайте, в учёте его нет, клиент ждёт | Outbox: сообщение записано вместе с заказом |
| При передаче посреднику | Посредник принял сообщение, но ответ потерялся | Два одинаковых заказа в учёте | Идентификатор сообщения и защита от дублей у получателя |
| В очереди | Посредник перезапустился, сообщения лежали в памяти | Часть заказов исчезла без следа | Хранение на диске и проверка перезапуском на стенде |
| При обработке | Получатель создал заказ в учёте и упал до подтверждения | Заказ создан, сообщение вернулось и создаст второй | Получатель запоминает обработанные идентификаторы |
Видно, что outbox закрывает только первую остановку. Остальные закрываются другими приёмами, и без них схема остаётся дырявой. Часто подрядчик говорит «у нас очередь, значит, надёжно». Очередь закрывает только третью остановку, и то если настроена на запись на диск.
Outbox: сначала записать, потом отправить
Идея проста. Вместо двух независимых действий («сохранить», «отправить») делается одно: сохранить заказ и сообщение об этом заказе в одной транзакции базы данных. Для базы это единая операция: либо записано всё, либо ничего. Сообщение лежит в отдельной таблице, которую так и называют «исходящие» (outbox).
Затем отдельный процесс-отправитель читает таблицу, берёт сообщения, которые ещё не отправлены, передаёт их получателю и после подтверждения отмечает отправленными. Если процесс упал, сообщения остались в таблице, и после запуска он продолжит с того же места. Окно между «сохранил» и «отправил» исчезло: пока сообщение не подтверждено, оно числится неотправленным.
Что outbox не решает
- Он не защищает от дублей. Если отправитель получил подтверждение, но упал до отметки, сообщение уйдёт повторно. Это нормально, если получатель готов к повтору.
- Он не гарантирует порядок без дополнительных мер. Если важно, чтобы изменение пришло после создания, нужен номер версии или очередь с порядком по ключу.
- Он не заменяет мониторинг. Если отправитель остановился, сообщения копятся тихо. Нужна метрика «возраст самого старого неотправленного сообщения» и оповещение.
Есть вариант с зеркальным названием: inbox, таблица «входящие» у получателя. В неё записывается идентификатор каждого принятого сообщения. Чем это полезно, разбираем в следующем разделе.
Повторы и дубли: почему повтор неизбежен
Отправитель вызывает получателя и может получить один из трёх исходов: «принято», «ошибка» или тишина. Первые два однозначны. Тишина означает, что неизвестно, дошло ли сообщение, обработано ли оно, и потерян ли только ответ. Отправитель не может отличить эти случаи, поэтому единственное безопасное решение при тишине — повторить.
Отсюда следует правило, которое важнее выбора технологии. Если отправитель повторяет при сомнении, получатель обязан уметь принимать повтор. Иначе повтор превращается в дубль, а дубль в деньги, ручной разбор или обращение клиента. Повтор при этом нужно делать с умом.
| Приём | Зачем | Если его нет |
|---|---|---|
| Пауза между попытками, растущая от раза к разу | Дать недоступной системе подняться и не создать шквал запросов | Один упавший получатель ломается повторно от лавины повторов |
| Предел числа попыток | Одно безнадёжное сообщение не должно крутиться вечно и занимать место остальных | Очередь забита сообщениями, которые не обработаются никогда |
| Список для ручного разбора | Место, куда уходят исчерпавшие попытки; человек видит причину и решает | Сообщения исчезают или копятся там, где их никто не смотрит |
| Разные правила для разных ошибок | Недоступность лечится повтором, неизвестный справочник нужно завести руками, неверный формат отклоняется | Бесполезные повторы и скрытые отказы |
Как получатель отличает повтор от нового сообщения
Тут появляется слово «идемпотентность»: операция идемпотентна, если выполненная дважды даёт тот же результат, что выполненная один раз. «Установить статус заказа „оплачен“» идемпотентно. «Добавить к остатку десять штук» нет: дважды добавленные десять штук портят данные. Сделать операцию безопасной для повтора можно четырьмя способами, и выбирается способ по типу сообщения.
| Способ | Как работает | Подходит для | Подвох |
|---|---|---|---|
| Идентификатор сообщения | Получатель записывает идентификатор в таблицу «входящие» в той же транзакции, что и результат. Повтор находит запись и возвращает прежний итог | Команды: создать заказ, принять платёж, провести документ | Таблица растёт; срок хранения должен быть больше срока, в течение которого возможны повторы |
| Бизнес-ключ | База не даёт завести вторую запись с тем же номером заказа партнёра | Сущности с естественным номером, который не повторяется | Номер может измениться при правке заказа |
| Версия записи | Сообщение несёт номер версии; применяется только более новая | Передача состояния: статус, остаток, карточка | Версии разных источников нельзя сравнивать между собой |
| Операция «установить» | Сообщение говорит, каким стать, а не как измениться | Простые обновления | За повтором может скрываться побочный эффект: письмо, документ |
Самая частая ошибка — идентификатор, который создаётся заново при каждой отправке. Тогда получатель видит каждый повтор как новое сообщение. Идентификатор должен рождаться вместе с сообщением и не меняться при повторах. Именно поэтому его записывают в outbox в момент создания, а не при отправке.
Порядок сообщений и «ядовитые» сообщения
Два последних затруднения не связаны с потерей и дублями, но доставляют не меньше хлопот.
Сообщения обгоняют друг друга
Заказ создан, затем изменён, затем отменён. Три сообщения могут прийти в другом порядке, особенно при повторах: первая попытка отправки задержалась, вторая прошла. Если получатель выбросит отмену, потому что не знает такого заказа, данные разойдутся. Решения два. Либо сообщения несут номер версии, и получатель применяет только более новые, а «опередившие» откладывает. Либо порядок обеспечивается очередью по ключу: все сообщения одного заказа идут через один обработчик по очереди. Первое проще, второе удобнее, когда сообщений много.
Одно сообщение блокирует остальные
«Ядовитым» называют сообщение, которое не обработается никогда: в нём неизвестный справочник или нарушен формат. Если обработчик обрабатывает сообщения строго по очереди и каждый раз падает на этом, вся очередь стоит. Для этого нужны предел числа попыток и отдельный список для разбора. Человек видит причину («нет соответствия для склада») и либо исправляет справочник и повторяет, либо отклоняет сообщение с комментарием.
Именно этот список и экран разбора превращают гарантию из слов в работающий механизм: без него сообщения исчезают, только красивее.
Как проверить, что подрядчик сделал это правильно
Схему можно нарисовать красиво и не реализовать. Реальную проверку делают испытания, а не презентация. Вот вопросы, которые стоит задать до договора и при приёмке.
- Покажите, где в коде сообщение записывается вместе с данными. Если это два отдельных вызова, outbox нет.
- Что произойдёт, если остановить отправитель между записью и отправкой? Ответ должен быть подтверждён тестом, а не словами.
- Что произойдёт, если отправить одно сообщение три раза? Результат должен быть один.
- Откуда берётся идентификатор и когда он создаётся?
- Где лежат сообщения, которые не удалось обработать, и кто их видит?
- Какая метрика сработает, если отправитель остановится? Если никакой, вы узнаете о проблеме от клиента.
- Как проверялась сохранность после перезапуска посредника?
| Признак | Что за ним стоит | Что спросить |
|---|---|---|
| «У нас очередь, значит, ничего не теряется» | Закрыта одна остановка из четырёх, и неизвестно как настроена запись на диск | Как закрыты остальные три и что будет при перезапуске очереди |
| «У нас доставка ровно один раз» | Обещание, которое на уровне сети недостижимо; скорее всего, дубли просто не заметили | Что делает получатель при повторе и как это проверено |
| Нет экрана или списка застрявших | Сообщения будут разбирать программисты по журналам | Кто и как увидит застрявшее сообщение в понедельник утром |
| Повторы без паузы и без предела | Система будет добивать упавшего получателя | Какие паузы и после скольких попыток сообщение уходит на разбор |
| Нет сверки | Ошибки, которые схема не видит, накапливаются | Как раз в сутки сравниваются число и суммы между системами |
Когда всего этого не нужно
Не каждый обмен требует такой дисциплины. Простая схема со временем обходится дешевле, и это нормально. Обычно хватает повторов внутри приложения без отдельных таблиц и посредника, если выполняется большинство условий:
- Потеря или дубль одного сообщения не стоит денег и не заметна клиенту.
- Следующий обмен через минуту исправит расхождение сам: например, передаются полные остатки, а не изменения.
- Получатель почти всегда доступен, а сообщений мало.
- Операция по природе идемпотентна: «установить», а не «изменить».
Брокер сообщений при этом чаще всего тоже не нужен. Для небольшого потока хватает таблицы в той же базе и процесса, который её читает. RabbitMQ оправдан, когда много обработчиков и нужны приоритеты, а Kafka — когда поток нужно хранить и перечитывать заново. Обмен с внешними партнёрами в согласованных форматах — отдельная тема, см. страницу про EDI-интеграции, а общие вопросы выбора архитектуры разобраны в статье «Микросервисы или монолит».
Что делаем мы и с чего начать
Начинаем с обследования: 200 000 ₽ и 2–3 недели за обмен между двумя-тремя системами, 350 000 ₽ и 3–4 недели, если цепочка длиннее. На выходе карта сообщений, места, где данные уже терялись или двоились (по вашим журналам и обращениям), выбранный для каждого типа уровень гарантии и вилка бюджета. Стоимость вычитается из первого этапа, если продолжаете с нами, а документом можно пользоваться и без нас.
Дальше лёгкий пилот одного потока (150 000–400 000 ₽, 2–8 недель) с outbox, защитой от дублей и экраном разбора, затем тяжёлый пилот с цепочкой и испытаниями отказами (400 000–1 200 000 ₽, 2–3 месяца). Остановиться можно после любой ступени. Мы делаем один обмен или одну цепочку, а не перестройку всей архитектуры, не дежурим круглосуточно и не вводим брокер там, где хватает таблицы. Исходники лежат в вашем репозитории с первого коммита. Цены и сроки — на странице «Тарифы», описание услуги — на странице «Гарантированная доставка сообщений».
Частые вопросы
Можно ли доставить сообщение ровно один раз?
На уровне сети нет: она не отличает потерянное сообщение от потерянного ответа. Поэтому отправитель повторяет при сомнении, а получатель узнаёт повтор по идентификатору. Эффект получается ровно один раз, хотя сообщение могло прийти дважды.
Что такое outbox одним предложением?
Таблица исходящих сообщений в вашей базе, в которую сообщение записывается в той же транзакции, что и данные, а отдельный процесс отправляет его потом и отмечает отправленным.
Что такое идемпотентность?
Свойство операции: выполненная дважды, она даёт тот же результат, что выполненная один раз. «Установить статус» идемпотентно, «добавить десять штук к остатку» нет. Если операция не идемпотентна по природе, повтор отсекается идентификатором сообщения.
Нужен ли для гарантированной доставки RabbitMQ или Kafka?
Не обязательно. Для небольшого потока хватает таблицы в базе и процесса-отправителя. Брокер нужен при многих обработчиках, приоритетах и отложенных повторах, Kafka при необходимости хранить и перечитывать поток. Выбор записывается на обследовании.
Что такое inbox?
Таблица входящих у получателя, куда записываются идентификаторы обработанных сообщений. Повторное сообщение находит запись и получает прежний результат вместо второго выполнения.
Как узнать, что сообщения застряли?
Нужны метрики: возраст самого старого неотправленного сообщения, число попыток и размер списка для разбора, плюс оповещение, когда они выходят за границу. Без этого очередь копит проблемы так же тихо, как раньше терялись данные.
Сколько стоит построить надёжный обмен?
Обследование — 200 000 ₽ (350 000 ₽ при нескольких системах), лёгкий пилот одного потока — 150 000–400 000 ₽, тяжёлый пилот — 400 000–1 200 000 ₽, вывод в эксплуатацию — от 1 500 000 ₽ этапами. Обследование вычитается из первого этапа, если продолжаете с нами.
Это нужно для обмена с партнёрами по EDI?
Принципы те же: идентификаторы, повторы, подтверждения, список для разбора. У EDI добавляются форматы оператора, подтверждения партнёра и подключение новых контрагентов, поэтому этому посвящена отдельная страница про EDI-интеграции.