Чтобы сообщение между системами не терялось и не двоилось, нужны две вещи. На стороне отправителя сообщение записывается в базу вместе с данными (это и есть outbox) и повторяется, пока не придёт подтверждение. На стороне получателя запоминается, что уже обработано, поэтому повтор не создаёт второй заказ. Одной из двух частей недостаточно: без первой сообщения пропадают, без второй двоятся.

Ниже на условном примере показано, почему «просто отправить» не работает, какие бывают сбои, как устроены сами приёмы и что спросить у исполнителя до договора. Услуга по построению такого обмена описана на странице «Гарантированная доставка сообщений», здесь только объяснение.

Коротко. Сеть не отличает потерянное сообщение от потерянного ответа, поэтому повторы неизбежны. Outbox не даёт потерять сообщение, идентификатор и память получателя не дают создать дубль, а человек должен видеть то, что застряло. Если сбой исправится следующим обменом через минуту, всего этого может не потребоваться.

Почему нельзя просто отправить и забыть

Возьмём условный пример: интернет-магазин принял заказ и должен передать его в учётную систему. Код выглядит естественно: сохранить заказ в базе, затем отправить сообщение в учёт. Две строки, и обе работают. Проблемы начинаются между ними и после них.

  • Процесс может упасть после сохранения и до отправки. Заказ в базе есть, в учёте нет. Автоматически никто не заметит.
  • Учёт может принять заказ и не успеть ответить. Магазин считает, что отправка не удалась, и отправляет снова. В учёте два заказа.
  • Учёт может быть недоступен час. Если магазин отправляет один раз и сдаётся, заказ потерян, если повторяет без пауз и без предела, он добивает систему, которая только что поднялась.

Ни один из этих сбоев не требует редкой аварии. Это обычное поведение сетей и процессов: перезапуск после обновления, нехватка памяти, обрыв соединения. Поэтому честный вопрос звучит не «как сделать, чтобы не ломалось», а «что произойдёт, когда сломается, и увидит ли это кто-нибудь».

Слово «гарантированная» означает ответ на этот вопрос: сообщение сохранено, доставляется до подтверждения, обрабатывается один раз по эффекту, а то, что не удалось, лежит в списке и ждёт решения человека.

Четыре момента, когда сообщение пропадает

Путь сообщения состоит из нескольких остановок. На каждой есть свой способ потери, поэтому защиту нужно ставить на каждой, а не «где-нибудь в середине». На условном примере с заказом это выглядит так.

МоментЧто случилосьЧто видит бизнесЧем закрывается
Между записью и отправкойМагазин сохранил заказ и перезапустился до отправкиЗаказ есть на сайте, в учёте его нет, клиент ждётOutbox: сообщение записано вместе с заказом
При передаче посредникуПосредник принял сообщение, но ответ потерялсяДва одинаковых заказа в учётеИдентификатор сообщения и защита от дублей у получателя
В очередиПосредник перезапустился, сообщения лежали в памятиЧасть заказов исчезла без следаХранение на диске и проверка перезапуском на стенде
При обработкеПолучатель создал заказ в учёте и упал до подтвержденияЗаказ создан, сообщение вернулось и создаст второйПолучатель запоминает обработанные идентификаторы

Видно, что outbox закрывает только первую остановку. Остальные закрываются другими приёмами, и без них схема остаётся дырявой. Часто подрядчик говорит «у нас очередь, значит, надёжно». Очередь закрывает только третью остановку, и то если настроена на запись на диск.

Outbox: сначала записать, потом отправить

Идея проста. Вместо двух независимых действий («сохранить», «отправить») делается одно: сохранить заказ и сообщение об этом заказе в одной транзакции базы данных. Для базы это единая операция: либо записано всё, либо ничего. Сообщение лежит в отдельной таблице, которую так и называют «исходящие» (outbox).

Затем отдельный процесс-отправитель читает таблицу, берёт сообщения, которые ещё не отправлены, передаёт их получателю и после подтверждения отмечает отправленными. Если процесс упал, сообщения остались в таблице, и после запуска он продолжит с того же места. Окно между «сохранил» и «отправил» исчезло: пока сообщение не подтверждено, оно числится неотправленным.

Что outbox не решает

  • Он не защищает от дублей. Если отправитель получил подтверждение, но упал до отметки, сообщение уйдёт повторно. Это нормально, если получатель готов к повтору.
  • Он не гарантирует порядок без дополнительных мер. Если важно, чтобы изменение пришло после создания, нужен номер версии или очередь с порядком по ключу.
  • Он не заменяет мониторинг. Если отправитель остановился, сообщения копятся тихо. Нужна метрика «возраст самого старого неотправленного сообщения» и оповещение.

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

Повторы и дубли: почему повтор неизбежен

Отправитель вызывает получателя и может получить один из трёх исходов: «принято», «ошибка» или тишина. Первые два однозначны. Тишина означает, что неизвестно, дошло ли сообщение, обработано ли оно, и потерян ли только ответ. Отправитель не может отличить эти случаи, поэтому единственное безопасное решение при тишине — повторить.

Отсюда следует правило, которое важнее выбора технологии. Если отправитель повторяет при сомнении, получатель обязан уметь принимать повтор. Иначе повтор превращается в дубль, а дубль в деньги, ручной разбор или обращение клиента. Повтор при этом нужно делать с умом.

ПриёмЗачемЕсли его нет
Пауза между попытками, растущая от раза к разуДать недоступной системе подняться и не создать шквал запросовОдин упавший получатель ломается повторно от лавины повторов
Предел числа попытокОдно безнадёжное сообщение не должно крутиться вечно и занимать место остальныхОчередь забита сообщениями, которые не обработаются никогда
Список для ручного разбораМесто, куда уходят исчерпавшие попытки; человек видит причину и решаетСообщения исчезают или копятся там, где их никто не смотрит
Разные правила для разных ошибокНедоступность лечится повтором, неизвестный справочник нужно завести руками, неверный формат отклоняетсяБесполезные повторы и скрытые отказы

Как получатель отличает повтор от нового сообщения

Тут появляется слово «идемпотентность»: операция идемпотентна, если выполненная дважды даёт тот же результат, что выполненная один раз. «Установить статус заказа „оплачен“» идемпотентно. «Добавить к остатку десять штук» нет: дважды добавленные десять штук портят данные. Сделать операцию безопасной для повтора можно четырьмя способами, и выбирается способ по типу сообщения.

СпособКак работаетПодходит дляПодвох
Идентификатор сообщенияПолучатель записывает идентификатор в таблицу «входящие» в той же транзакции, что и результат. Повтор находит запись и возвращает прежний итогКоманды: создать заказ, принять платёж, провести документТаблица растёт; срок хранения должен быть больше срока, в течение которого возможны повторы
Бизнес-ключБаза не даёт завести вторую запись с тем же номером заказа партнёраСущности с естественным номером, который не повторяетсяНомер может измениться при правке заказа
Версия записиСообщение несёт номер версии; применяется только более новаяПередача состояния: статус, остаток, карточкаВерсии разных источников нельзя сравнивать между собой
Операция «установить»Сообщение говорит, каким стать, а не как изменитьсяПростые обновленияЗа повтором может скрываться побочный эффект: письмо, документ

Самая частая ошибка — идентификатор, который создаётся заново при каждой отправке. Тогда получатель видит каждый повтор как новое сообщение. Идентификатор должен рождаться вместе с сообщением и не меняться при повторах. Именно поэтому его записывают в outbox в момент создания, а не при отправке.

Порядок сообщений и «ядовитые» сообщения

Два последних затруднения не связаны с потерей и дублями, но доставляют не меньше хлопот.

Сообщения обгоняют друг друга

Заказ создан, затем изменён, затем отменён. Три сообщения могут прийти в другом порядке, особенно при повторах: первая попытка отправки задержалась, вторая прошла. Если получатель выбросит отмену, потому что не знает такого заказа, данные разойдутся. Решения два. Либо сообщения несут номер версии, и получатель применяет только более новые, а «опередившие» откладывает. Либо порядок обеспечивается очередью по ключу: все сообщения одного заказа идут через один обработчик по очереди. Первое проще, второе удобнее, когда сообщений много.

Одно сообщение блокирует остальные

«Ядовитым» называют сообщение, которое не обработается никогда: в нём неизвестный справочник или нарушен формат. Если обработчик обрабатывает сообщения строго по очереди и каждый раз падает на этом, вся очередь стоит. Для этого нужны предел числа попыток и отдельный список для разбора. Человек видит причину («нет соответствия для склада») и либо исправляет справочник и повторяет, либо отклоняет сообщение с комментарием.

Именно этот список и экран разбора превращают гарантию из слов в работающий механизм: без него сообщения исчезают, только красивее.

Как проверить, что подрядчик сделал это правильно

Схему можно нарисовать красиво и не реализовать. Реальную проверку делают испытания, а не презентация. Вот вопросы, которые стоит задать до договора и при приёмке.

  1. Покажите, где в коде сообщение записывается вместе с данными. Если это два отдельных вызова, outbox нет.
  2. Что произойдёт, если остановить отправитель между записью и отправкой? Ответ должен быть подтверждён тестом, а не словами.
  3. Что произойдёт, если отправить одно сообщение три раза? Результат должен быть один.
  4. Откуда берётся идентификатор и когда он создаётся?
  5. Где лежат сообщения, которые не удалось обработать, и кто их видит?
  6. Какая метрика сработает, если отправитель остановится? Если никакой, вы узнаете о проблеме от клиента.
  7. Как проверялась сохранность после перезапуска посредника?
ПризнакЧто за ним стоитЧто спросить
«У нас очередь, значит, ничего не теряется»Закрыта одна остановка из четырёх, и неизвестно как настроена запись на дискКак закрыты остальные три и что будет при перезапуске очереди
«У нас доставка ровно один раз»Обещание, которое на уровне сети недостижимо; скорее всего, дубли просто не заметилиЧто делает получатель при повторе и как это проверено
Нет экрана или списка застрявшихСообщения будут разбирать программисты по журналамКто и как увидит застрявшее сообщение в понедельник утром
Повторы без паузы и без пределаСистема будет добивать упавшего получателяКакие паузы и после скольких попыток сообщение уходит на разбор
Нет сверкиОшибки, которые схема не видит, накапливаютсяКак раз в сутки сравниваются число и суммы между системами

Когда всего этого не нужно

Не каждый обмен требует такой дисциплины. Простая схема со временем обходится дешевле, и это нормально. Обычно хватает повторов внутри приложения без отдельных таблиц и посредника, если выполняется большинство условий:

  • Потеря или дубль одного сообщения не стоит денег и не заметна клиенту.
  • Следующий обмен через минуту исправит расхождение сам: например, передаются полные остатки, а не изменения.
  • Получатель почти всегда доступен, а сообщений мало.
  • Операция по природе идемпотентна: «установить», а не «изменить».

Брокер сообщений при этом чаще всего тоже не нужен. Для небольшого потока хватает таблицы в той же базе и процесса, который её читает. 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-интеграции.