Друзья не позволяют друзьям писать по-разному!

Хотя микросервисы поддерживают свое состояние в частном порядке, они вряд ли работают изолированно. Некоторые бизнес-сценарии требуют, чтобы они сначала изменили свое состояние, а затем уведомили об этом изменении более широкую аудиторию.

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

В этом посте я обсуждаю, почему двойная запись в распределенных системах - это плохо, как вы можете исправить это с помощью шаблона исходящих сообщений и некоторые внутренние механизмы этого шаблона. Этот пост не является руководством по внедрению, а вместо этого знакомит с основами, которые вы должны использовать при внедрении в производственную среду.

Проблема с двойной записью

Представьте, что есть два микросервиса; Служба заказа и служба доставки. Бизнес-логика гласит, что служба заказов должна уведомить службу доставки о получении нового заказа для подготовки отгрузки.

Какие у нас есть варианты?

Синхронный подход. Служба заказов синхронно вызывает метод API службы доставки для отправки уведомления. Недостатком является то, что он вводит связь между двумя сервисами. Услуга заказа должна зависеть от доступности службы доставки, количества повторных попыток, ограничений скорости и т. Д. Учитывая масштабируемость, мы можем исключить этот подход.

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

Асинхронный подход кажется подходящим решением с точки зрения масштабируемости. Но каковы могут быть возможные шансы?

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

Если не обновить две системы атомарно, вся система окажется в несогласованном состоянии.

Согласно приведенной выше диаграмме последовательности, если публикация события не удалась из-за сбоя брокера, у нас будет заказ в системе, но без отправки. Кроме того, если вставка нового заказа не удалась из-за ошибки базы данных, событие все равно будет опубликовано. Таким образом создается отгрузка без соответствующего заказа.

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

Шаблон транзакционных исходящих сообщений

Шаблон Transaction Outbox решает эту проблему, записывая данные в две таблицы базы данных, агрегированную таблицу и таблицу OUTBOX в пределах одной области транзакции, а затем используя содержимое, записанное в таблицу OUTBOX, для управления процессом публикации событий.

Узор состоит из двух компонентов.

  1. Таблица OUTBOX.
  2. Ретранслятор сообщений.

Таблица OUTBOX

Шаблон вводит дополнительную таблицу OUTBOX в базу данных сервиса. В этой таблице хранятся уведомления о событиях, которые предполагается отправлять от службы брокеру сообщений. Когда служба записывает в агрегированную таблицу, она также записывает запись в таблицу OUTBOX как часть той же транзакции.

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

Ретранслятор сообщений

Компонент ретрансляции сообщений асинхронно отслеживает наличие новых записей в таблице OUTBOX. Если есть, они будут преобразованы в события и опубликованы в брокере сообщений.

После публикации сообщение будет удалено из таблицы OUTBOX, чтобы предотвратить повторную обработку и рост таблицы.

Формат таблицы OUTBOX

Таблица OUTBOX должна иметь как минимум следующую структуру.

create table OUTBOX (
 id varchar(255) primary key,
 aggregate_type varchar(255) not null,
 aggregate_id varchar(255) not null,
 type varchar(255) not null,
 payload text not null
);
  • id: уникальный идентификатор каждого сообщения; могут использоваться потребителями для обнаружения любых повторяющихся событий.
  • aggregate_type: тип совокупного корня, с которым связано данное событие. Это происходит из структуры, управляемой доменом (DDD), где экспортируемое событие должно быть связано с агрегированным корнем. В нашем примере это Заказ.
  • aggregate_id: это идентификатор агрегированного объекта, на который влияет операция обновления. Здесь может быть идентификатор заказа. Служба доставки будет использовать его как ссылку на запись о доставке.
  • type: тип события. Например, «OrderCreated
  • полезная нагрузка: представление фактического содержания события в формате JSON. Например, он содержит идентификатор заказа, идентификатор клиента, сумму и т. Д.

Как это решает проблему двойной записи?

Шаблон «Исходящие» позволяет достичь атомарности при записи в базу данных и публикации событий брокеру. Мы можем использовать возможности локальных транзакций для совершения обоих действий или ничего.

По крайней мере, один раз доставка событий изменения

Записывая запись в таблицу OUTBOX, мы получаем гарантию доставки хотя бы один раз для события изменения. В случае сбоя брокера ретранслятор сообщений может надежно повторить попытку после прочтения сообщений OUTBOX. Брокер может быть недоступен в течение нескольких часов или дней. Но у нас есть постоянная запись сообщения, которое будет отправлено, когда брокер снова в сети. Таким образом, мы можем гарантировать, что событие change достигнет брокера хотя бы один раз.

Предотвратить фальшивую публикацию событий с помощью чистых откатов

Кроме того, мы можем извлечь выгоду из локальной транзакции ACID при одновременной записи в обе таблицы. Если запись в любую из таблиц завершится неудачно, мы выполним полный откат. Это предотвращает публикацию поддельных сообщений брокеру. Поддельное сообщение, например, обновление сводной таблицы могло завершиться неудачно, но брокеру было опубликовано событие.

Задачи - отправка повторяющихся событий

После публикации события ретранслятор сообщений удаляет соответствующую запись в таблице OUTBOX, чтобы предотвратить повторную обработку. Логика выглядела бы так:

for (OutboxMessage message:messages) {
    brokerConnection.publish(message.toOutboxEvent());
    outboxTable.delete(message);
}

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

Это одна из проблем, часто связанных с шаблоном «Исходящие». Мы можем исправить это, сделав идемпотентного потребителя событий нижестоящего.

Обнаружение дубликатов с идемпотентным потребителем

Ошибка ретранслятора сообщений при удалении записи OUTBOX, перезапуск брокера сообщений или неизвестная ошибка могут привести к тому, что потребитель событий получит повторяющиеся события. Достижение семантики ровно один раз в распределенной системе - задача, с которой мы все сталкиваемся.

Служба доставки в нашем примере может получить один и тот же заказ дважды, чтобы дважды подготовить одну и ту же отправку, что неприемлемо. Потребитель может предотвратить это, проверив, было ли ранее обработано событие с данным UUID. В этом случае любые дальнейшие вызовы того же события будут игнорироваться.

Подобно таблице OUTBOX, мы можем поддерживать таблицу INBOX в базе данных службы потребителей. Он просто отслеживает, какие события были обработаны, записывая их UUID.

После обработки события в первый раз потребитель отмечает событие как обработанное в таблице INBOX. Это должно быть транзакционным - чтобы можно было улавливать любые откаты на уровне потребителя, чтобы он мог повторить попытку получения события.

Варианты реализации паттерна

Я бы сказал, что реализовать этот шаблон несложно. Все, что вам нужно, - это язык программирования, поддерживающий транзакционную запись в базу данных.

Простой пример кода Java для отправки событий в OUTBOX должен выглядеть следующим образом:

Вызов приходит после. То есть реализация ретрансляции сообщений. Вы можете рассмотреть две стратегии.

Издатель опроса

Этот издатель публикует сообщения, опрашивая таблицу OUTBOX на наличие новых записей. Частые опросы скоро исчерпают базу данных. Следовательно, это не масштабируемое решение. Вы можете прочитать сообщение Камиля Гжибека о реализации ретрансляции сообщений опроса с .NET.

Ретрансляция сообщений на основе системы отслеживания измененных данных (CDC)

Здесь у вас есть компонент интеллектуального анализа журнала транзакций, который отслеживает журнал транзакций базы данных и фиксирует изменения, внесенные в таблицу OUTBOX. Журнал транзакций записывает все транзакции, совершенные с базой данных, в строго упорядоченном виде. Майнер читает журнал транзакций и публикует каждое изменение в виде сообщения брокеру сообщений.

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

Я бы рекомендовал Дебезиум как лучшее решение для реализации ретрансляции сообщений на базе CDC. После того, как вы настроите Debizium для прослушивания вашей таблицы OUTBOX, он будет брать данные оттуда. Таким образом, изменения, внесенные в таблицу OUTBOX, останутся в теме Kafka.

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

В общем, этот шаблон можно использовать для надежного распространения изменений состояния между микросервисами. Вот некоторые варианты использования.

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

Выводы

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

использованная литература

Transaction Outbox - Крис Ричардсон

Надежный обмен данными микросервисов с шаблоном исходящих сообщений - Гуннар Морлинг