Отправка сообщений в брокер из бизнес-операций

Довольно часто, при реализации API или бизнес-операции требуется отчитаться о запуске или выполненной работе, например – отправить сообщение в брокер с последующим приземлением в хранилище логов. И вот тут, в силу отсутствия компетентности, сроков или нацеленности на MVP-дизайн, очень часто программисты реализуют дешевый антипаттерн – отправление сообщений в брокер прямо в бизнес-операции, взаимодействующей с базой данных.

Какие проблемы возникают при такой реализации?

  1. Задержка транзакции и ответа клиенту, иногда значительно превышающая время операции.

  2. Зависимость сервиса от доступности брокера и сетевой инфраструктуры.

  3. Вероятность потерять или произвести фантомные сообщения.

Не стану описывать сценарии, которые приводят к таким результатам, они достаточно очевидны. Всех этих проблем можно избежать, если применить умное решение – задействовать дополнительный транзитный компонент, удовлетворяющий требованиям ACID: PgQ или простую табличку. Сразу предостерегу от попытки использовать файл дисковой системы, это решение не удовлетворит требование атомарности, т.е. проблемы по пункту №3 все равно могут реализоваться.

Последовательность действий при использовании транзитного компонента …

  1. Транзакция открывается.
  2. Выполняется бизнес-операция.
  3. Данные сохраняются в транзитную таблицу.
  4. Транзакция закрывается.
  5. Асинхронно запускается worker.

Далее, worker может спокойно переносить данные в брокер. Если нужно воспроизвести последовательность сохранения данных, то worker нужно реализовать по паттерну одиночка. Если бизнес-операция будет выполняться редко, то запускать worker рекомендуется не только из бизнес-операции, но и по расписанию. Не забудьте подключить worker к мониторингу.

Последовательность действий при перекладке данных в брокер …

  1. Транзакция открывается.
  2. Вычитывается пачка сообщений.
  3. Сообщения отправляются в брокер.
  4. Транзакция закрывается.
  5. При необходимости, последовательность повторяется.

Если в процессе произойдет сбой, то транзакция откатывается и worker завершает работу. Весь долг будет закрыт в последующих запусках.

Теперь рассмотрим, на примере Kafka, как сделать, чтобы в брокере не оказалось повторных сообщений.

Вспомним, что у Kafka есть транзакции, но увы – нам это никак не поможет, поскольку вы не сможете в 100% случаев гарантированно атомарно завершить обе транзакции Postgres и Kafka.

К счастью, в Kafka есть семантика Exacly-Once, которая гарантирует, что от поставщика не будут приняты сообщения с одинаковым идентификатором. Отличный выбор, но … Какой бы умный не был брокер, легко привести сценарий, в котором «глупый» потребитель может повторно считать сообщение. И если сообщение должно приводить к вставке данных, то это, скорее всего, будет проблемой.

Поэтому, для достижения строго однократной доставки в конечную точку, потребитель обязан реализовывать семантику Exacly-Once. В этом случае вполне можно снизить требования к транспорту до семантики At-Least-Once.

Кстати, если потребителем будет БД ClickHouse, то вы можете получить требуемый функционал в коробке (см. движки ReplacingMergeTree или ReplicatedReplacingMergeTree) на базе бизнес-ключей.

Начать дискуссию