{"id":14285,"url":"\/distributions\/14285\/click?bit=1&hash=346f3dd5dee2d88930b559bfe049bf63f032c3f6597a81b363a99361cc92d37d","title":"\u0421\u0442\u0438\u043f\u0435\u043d\u0434\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0442\u0440\u0430\u0442\u0438\u0442\u044c \u043d\u0430 \u043e\u0431\u0443\u0447\u0435\u043d\u0438\u0435 \u0438\u043b\u0438 \u043f\u0443\u0442\u0435\u0448\u0435\u0441\u0442\u0432\u0438\u044f","buttonText":"","imageUuid":""}

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

Довольно часто, при реализации 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) на базе бизнес-ключей.

0
Комментарии
-3 комментариев
Раскрывать всегда