Обеспечение согласованности данных в распределенных системах. Гарантии доставки сообщений
Легко делать покупки в онлайн магазине. Зашел — выбрал — купил — ждешь — получаешь товар и радуешься приобретению. Легко покупателю, но не проектировщику/разработчику этого магазина. В “жизненном цикле” заказа важно не потерять товар и деньги. Еще важнее – не потерять данные. Что если деньги со счета клиента спишутся, а на счет продавца не придут? Или придут, но не в том объеме? Случится что-то плохое, если не выдать чек или выдать его невовремя \ не на ту сумму. Как сделать так, чтобы списанная со склада позиция добралась до службы доставки и чтобы об этом знали склад c доставкой одновременно? А если товар успешно приехал в пункт выдачи, как обеспечить одинаковую информацию у службы доставки, пункта выдачи, продавца и получателя товара? Столько вопросов!
Я попытался разобраться, что делают для того, чтобы данные в разных частях распределенных систем были согласованными, каким образом они транспортируются и что гарантирует доставку.
В этой статье поговорим о том:
- что такое Согласованность данных;
- как добиться Согласованности в распределенных системах;
- какие бывают Гарантии доставки;
- основные принципы передачи данных через Брокер сообщений;
- точки отказа при передаче данных через Брокер сообщений.
Распределенная система — это совокупность подсистем, которые работают вместе для выполнения общей задачи. Состоит из нескольких автономных компонентов, соединенных между собой через сеть. Каждый компонент решает узкий перечень задач только своей предметной области. Например: сервис управления заказами, сервис уведомлений, сервис контроля доставки, и другие – как части распределенной системы интернет-магазина.
Данные путешествуют по сети при помощи протоколов. Чтобы это происходило быстрее, применяют Асинхронный тип обмена сообщениями – способ передачи данных, при котором отправитель не ожидает немедленного ответа от получателя. Пристал другу фото своей новой машины и пошел на совещание. Потом на обед. Затем зашел в мессенджер и узнал что друг думает по поводу твоего приобретения. Примерно так, но только не для людей, а для компонентов распределенной системы.
Существует специально обученный сервис для асинхронного обмена сообщениями, который так и называется — message broker (Брокер сообщений). Он умеет обрабатывать сообщения, сохранять их до момента доставки, управлять потоком и обеспечивать надежную доставку даже при возможных сбоях в системе.
Согласованность данных
Согласованность данных в распределенных системах — это состояние, при котором данные находятся в соответствии с ожидаемым состоянием или правилами бизнес-логики в определенный момент времени. Это означает, что при выполнении определенных сценариев, данные в “Сервиса А” должны соответствовать данным в “Сервисе Б”.
Пример согласованности данных между двумя сервисами.
Предположим, у нас есть онлайн магазин, который продает товары и система управления инвентаризацией, которая отслеживает количество товаров в наличии. Когда клиент делает заказ в магазине, происходит следующее:
Сервис магазина: Уменьшает количество доступных товаров в системе инвентаризации.
Система управления инвентаризацией: Уменьшает фактическое количество товаров в наличии на складе.
Согласованность данных между двумя сервисами нужна, чтобы информация о количестве товаров в онлайн магазине соответствовала фактическому количеству товаров на складе.
Обеспечение согласованности (консистентности) данных
Прежде чем рассказать про согласованность данных, приведу пример несогласованности/неконсистентности. “Сервис А” выполнил операцию, которая должна попасть в историю операций. История операций реализована в “Сервисе Б”. Взаимодействие между сервисами реализовано через “Брокер сообщений”. Если “Сервис А” обработает операцию у себя, но информация об этом не будет доставлена “Сервису Б”, данные будут НЕ согласованы.
Основные принципы передачи данных через Брокер сообщений
Логика взаимодействия систем при асинхронном обмене сообщениями:
- “Сервис А” отправляет сообщение в “Брокер сообщений”.
- “Брокер сообщений” подтверждает получение сообщения.
- “Брокер сообщений” отправляет сообщение “Сервису Б”.
- “Сервис Б” подтверждает получение сообщения.

Гарантии доставки сообщения
Гарантия доставки сообщений, в контексте распределенной архитектуры ПО, это обязательство между компонентами системы о том, что сообщение будет успешно доставлено от Отправителя к Получателю, даже при возможных сбоях в сети или в отдельных частях системы.
В контексте Брокера сообщений, Сообщение считается успешно доставленным при условиях, что сообщение Отправителя не должно удаляться из очереди (считаться обработанным), пока не будет сохранено у Получателя (подтверждено получение сообщения).
В контексте распределенной архитектуры ПО, гарантия доставки сообщения обеспечивается комбинацией условий: отправить гарантирует, что сообщение будет помещено в Брокер сообщений, а БС гарантирует его хранение и доставку Получателю, который, в свою очередь, гарантирует корректную обработку сообщения.
Существуют различные типы гарантий доставки сообщений. Они определяют уровень надежности передачи данных и степень обработки ошибок:
At most once (Как максимум один раз): сообщение будет доставлено не больше, чем один раз, и может быть не доставлено вообще в случае сбоев или ошибок.
At least once (Как минимум один раз): сообщение должно быть доставлено один раз, но может прийти несколько из-за повторных попыток доставки в случае сбоев или ошибок.
Exactly once (Точно один раз): сообщение будет доставлено получателю ровно один раз, без потерь и дублирования. Это самый строгий уровень гарантии доставки, который обеспечивает точность и надежность.
Примеры для каждого типа гарантии доставки
At most once Как мАксимум один раз
Кто-то опубликовал пост социальной сети. Ничего страшного не случится, если его друзья не получат уведомление о публикации поста. Но будет неприятно, если они станут получать уведомления о публикации одного и того же поста несколько раз.
At least once Как мИнимум один раз
Пользователь оформляет заказ на доставку товара. С гарантией доставки «Как минимум один раз» запрос может быть обработан несколько раз из-за временных сбоев или ошибок. Пользователь получит свой заказ как минимум один раз, а в случае сбоев получит два одинаковых заказа.
Exactly once Точно один раз
Клиент инициирует перевод средств со своего банковского счета на другой счет. С гарантией доставки «Точно один раз» схема передачи данных позаботится о том, чтобы операция будет выполнена ровно один раз без потерь и без дублирования.
Точки отказа при передаче данных через Брокер сообщений
Большинство популярных брокеров сообщений “из коробки” реализуют логику работы со сбоями своих компонентов системы. Сценарий полного отказа “Брокера сообщений” маловероятен при правильной настройке, но возможен.
Рассмотрим примеры сбоев передачи данных на абстрактном сценарии:
На стороне “Сервиса А” произошло событие. Информацию об этом нужно передать “Сервису Б”.
Сценарий 1:
Брокер сообщений упал до того, как “Сервис А” отправил сообщение

Если брокер сообщений недоступен, «Сервис А» не сможет отправить сообщение, а «Сервис Б» не получит его.
При гарантии доставки At most once Как мАксимум один раз «Сервис А» попытается отправить сообщение, но оно не будет доставлено из-за отсутствия связи с брокером сообщений. Повторно «Сервис А» не будет выполнять попытку доставки того же сообщения.
При гарантии доставки At least once Как мИнимум один раз. «Сервис А» попытается отправить сообщение и, если не получит от брокера подтверждения о получении, «Сервис А» будет пытаться отправить сообщение столько раз, сколько указано в правилах сервиса или пока не получит подтверждение получения сообщения брокером.
Сценарий 2:
Брокер сообщений упал после того, как он получил сообщение от “Сервиса А”, но до того, как подтвердил его получение

Если брокер сообщений получил сообщение от “Сервиса А”, но упал до того как подтвердил получение, то после восстановления работоспособности брокера, “Сервис А” может повторно направить сообщение брокеру. Брокер сообщения в таком случае отправит “Сервису Б” минимум два сообщения об одном и том же событии.
Данный пример соответствует гарантии доставки At least once Как мИнимум один раз.
Сценарий 3
Брокер сообщений получил сообщение от “Сервиса А” и подтвердил получение
Брокер сообщений не может доставить сообщение “Сервису Б”, потому что сервис недоступен или брокер сообщений упал.

Если упал “Сервис Б”, то брокер автоматически доставит сообщение после восстановления работоспособности сервиса. Брокер отправит “Сервису Б” столько сообщений сколько накопится у него в очереди на отправку. Тип гарантии доставки для брокера сообщений будет зависеть от его настроек.
Если упал брокер, то после восстановления его работоспособности, в зависимости от его настроек, сообщение будет восстановлено и доставлено “Сервису Б” или сообщение будет потеряно.
Сценарий 4
Брокер сообщений получил сообщение от “Сервиса А”, подтвердил его получение и доставил сообщение “Сервису Б”. “Сервис Б” упал до того как подтвердил получение сообщения.

Так как брокер сообщений не получил от “Сервиса Б” подтверждение о доставке сообщения. Тип гарантии доставки будет зависеть от настроек брокера.
Согласованность данных
Согласованность данных — это состояние, при котором данные находятся в соответствии с ожидаемым состоянием или правилами бизнес-логики в определенный момент времени. Это означает, что при выполнении определенных сценариев, данные в “Сервиса А” должны соответствовать данным в “Сервисе Б”.
Пример согласованности данных между двумя сервисами.
Предположим, у нас есть онлайн магазин, который продает товары, и система управления инвентаризацией, которая отслеживает количество товаров в наличии. Когда клиент делает заказ в магазине, происходит следующее:
Сервис магазина: Уменьшает количество доступных товаров в системе инвентаризации.
Система управления инвентаризацией: Уменьшает фактическое количество товаров в наличии на складе.
Согласованность данных между двумя сервисами нужна, чтобы информация о количестве товаров в онлайн магазине соответствовала фактическому количеству товаров на складе.
Обеспечение согласованности (консистентности) данных
Прежде чем рассказать про согласованность данных, приведу пример несогласованности/неконсистентности. “Сервис А” выполнил операцию, которая должна попасть в историю операций. История операций реализована в “Сервисе Б”. Взаимодействие между сервисами реализовано через “Брокер сообщений”. Если “Сервис А” обработает операцию у себя, но информация об этом не будет доставлена “Сервису Б”, данные будут не согласованы.
Как обеспечить согласованность данных со стороны сервиса-отправителя сообщения
Для решения данной задачи, есть минимум два подхода:
- Запретить для Клиента выполнение операции (ограничить доступность), если “Сервис А” не может отправить сообщение в “Брокер сообщений”. Операция не будет выполнена и данные останутся согласованными.
- Реализовать в “Сервисе А” механизм гарантированной доставки сообщений в “Брокер сообщений”. Если упадет “Сервис Б”, то брокер доставит ему сообщение после восстановления его работоспособности. Данные будут согласованы.
Для реализации 1-го способа (ограничить доступность) достаточно для “Сервиса А” в одной транзакции:
- записывать данные к себе в базу данных
- отправлять сообщение брокеру
Если брокер недоступен, то транзакция не будет выполнена и “Сервис А” не сможет записать данные к себе в БД. Сервис вернет Клиенту ошибку выполнения операции.
Для 2-го способа (гарантированная доставка сообщения в очередь) можно спроектировать логику, реализующую сразу два архитектурных паттерна:
- Transactional outbox (Публикация событий)
- Polling publisher (Опрашивающий издатель)
Transactional outbox (Публикация событий)
Паттерн реализуется через добавление в БД сервиса-отправителя таблицы “OUTBOX”. Она выполняет роль временной очереди сообщений. В таблицу могут записываться изменения состояния объектов (агрегатов) из нескольких таблиц.
Таблица “OUTBOX” может содержать следующие поля:
- event_id — идентификатор события
- object — вид агрегата (сущности) в котором произошло событие
- event_type — тип события
- payload — полезная нагрузка (само сообщение)
- сreated_at — дата и время создания записи
- published_at — дата и время публикации записи в очередь
- retry_count — счетчик повторных попыток публикации события
- status — статус доставки сообщения в очередь
event_id | object | event_type | payload | сreated_at | published_at | retry_count | status |
---|---|---|---|---|---|---|---|
a54-4ht | order | orderCreated | {“id”:123, .. } | 2024-04-27 09:00 | 2024-04-27 09:02 | 0 | Published |
Polling publisher (Опрашивающий издатель)
Паттерн реализуется путем добавления обработчика записей в таблице “OUTBOX”. Он с указанным интервалом времени проверяет таблицу “OUTBOX” на наличие новых записей и отправляет их в брокер сообщений.

На данной схеме в “Сервисе А” реализовано 2 таблицы:
- Таблица “MAIN” хранит “полезную информацию” для обеспечения бизнес-логики.
- Таблица “OUTBOX” реализует логику хранилища упорядоченных по дате добавления сообщений, полезных внешним сервисам.
Сценарий данной реализации:
- “Сервис А” при операциях изменения данных в БД (insert, update, delete) в рамках одной транзакции записывает данные сразу в 2 таблицы. В таблице “MAIN” попадает полная информацию по событию для обеспечения бизнес-логики, а в таблицу “OUTBOX” складывается информация о совершенном событии, которая нужна сторонним сервисам.
- Далее компонент “polling publisher” с указанным интервалом времени читает записи из таблицы “OUTBOX” и публикует их в брокер сообщений.
Если данные успешно отправлены в очередь, то “polling publisher” обновляет запись в таблице “OUTBOX” отмечая признаком “отправлена в очередь”.
Если “Очередь сообщений” в момент публикации данных недоступна, то “polling publisher” будет с заданным интервалом пытаться отправить сообщение в очередь и только после успешной публикации отметит запись в таблице “OUTBOX” признаком “отправлена в очередь”.
Такая реализация обеспечивает доступность функционала “Сервиса А” для Клиента и реализует отложенную согласованность данных между двумя сервисами.
Паттерн “polling publisher” — это простой подход, который неплохо работает на небольших нагрузках для реляционных баз данных (и некоторых NoSQL), но частое обращение к БД за обновлениями грузит базу данных.
Transaction log tailing (отслеживание транзакционного журнала)
Иначе отложенную согласованность можно реализовать через паттерн Transaction log tailing (отслеживание транзакционного журнала).

Общая реализация похожа на прошлый пример, за исключением того, что теперь не нужно постоянно проверять наличие новых записей в таблице “OUTBOX”. “Анализатор журнала транзакций” прослушивает журнал транзакций БД и каждую запись публикует в виде события в брокер сообщений.
Журнал транзакций — это физический файл, который хранится внутри кластера БД – один на весь кластер. В него записываются изменения данных до их применения к реальной БД.
Когда происходит восстановление данных после сбоя или восстановления из резервной копии, информация из журнала транзакций применяется ко всем базам данных в кластере, чтобы привести их в состояние, соответствующее моменту времени, когда была сделана последняя запись в журнале транзакций. Так обеспечивается поддержка транзакционной согласованности в пределах всего кластера.
Чтение журнала транзакций не грузит БД, поэтому данный способ подойдет для анализа большого количества сообщений и трансляции их брокеру.
На рынке уже есть готовые решения реализующие функционал анализатора журнала транзакций: Debezium, DynamoDB streams, Eventuate Tram и прочие. Выбор готового решения или реализация своего – уже выбор компании. Подробнее с примерами использования Debezium можете познакомиться тут.
Коротко о Debezium
Debezium — это проект с открытым исходным кодом, разработанный для мониторинга и управления изменениями в базах данных. Реализует механизмы чтения журналов транзакций БД и преобразования их в поток событий (обычно в формате JSON). Позволяет отслеживать изменения журналов транзакций в реальном времени и передавать их (изменения) в другие системы. Можно настроить на чтение конкретных таблиц, например “OUTBOX”. Чтобы понять, что уже было передано в очередь, а что – нет, Debezium использует концепцию «offsets» (смещений).
Каждый раз, когда он успешно передает данные получателю (например в очередь сообщений), – сохраняет информацию об этом изменении (например, идентификатор транзакции или номер записи) в специальном хранилище смещений.
Если происходит сбой, Debezium может использовать информацию о смещениях, чтобы продолжить чтение с правильной точки и избежать повторной обработки записей. Это позволяет Debezium обеспечивать надежную доставку сообщений и гарантировать, что ни одно изменение не будет утеряно или обработано дважды.
Преимущества реализации паттерна “Transaction log tailing”
- Реализуем принцип единственной ответственности (The Single Responsibility Principle, SRP). Сервис-отправитель больше не отвечает за передачу данных Получателям.
- Если отказаться от “OUTBOX” в Сервисе-отправителе, то длина транзакции уменьшается за счет исключения записи для наполнения этой таблицы. Отложенная согласованность, в этом случае, реализуется другим сервисом.
Безопасность при реализации паттерна “Transaction log tailing”
Транслируя изменения журнала транзакций во внешний мир, например, записывая данные в очередь сообщений, важно помнить ключевую особенность этой реализации – журнал транзакций хранит записи о ВСЕХ операциях по обновлению данных БД!
При реализации паттерна “Transaction log tailing” нужно понимать, кто будет получателем данных Сервиса-отправителя. Нельзя просто взять и показать все обновления данных БД внешнему миру. Это небезопасно! Реализовать паттерн с учетом данной особенности можно несколькими способами:


Как обеспечить согласованность данных со стороны сервиса-получателя сообщения
Для реализации согласованности данных на стороне сервиса-получателя сообщений и обеспечения гарантии обработки сообщения At most once Как мАксимум один раз можно реализовать архитектурный паттерн “transactional inbox pattern”.

На данной схеме в “Сервисе Б” реализовано 2 таблицы:
- Таблица “MAIN” хранит “полезную информацию” для обеспечения бизнес-логики.
- Таблица “INBOX” хранит информацию о полученных сообщениях из брокера.
Последовательность обработки сообщения:
- Брокер отправляет сообщение сервису-получателю.
- “Сервис Б” выполняет проверку уникальности идентификатора сообщения. Если в таблице “INBOX” уже есть сообщение с таким же идентификатором, то вставка данных в таблицу не выполнится и сервис не будет обрабатывать дубль сообщения. Если сообщение получается впервые, оно будет добавлено в таблицу.
- “Сервис Б” возвращает ответ брокеру о статусе обработки сообщения.
- “Сервис Б” забирает сообщение из таблицы “INBOX”.
- “Сервис Б” обрабатывает сообщение, полученное из таблицы “INBOX”. При необходимости сохраняет данные в таблицу “MAIN”.
Таблица “INBOX” одна в БД сервиса. В нее могут записываться все УНИКАЛЬНЫЕ сообщения полученные из брокера.
Обязательные условия для паттерна “transactional inbox pattern”:
- Для “INBOX” таблицы нужно реализовать логику, запрещающую запись в таблицу дубликатов сообщений.
- Каждое сообщение, попадающее в брокер, должно быть идемпотентным.
Идемпотентность сообщения означает, что при повторной доставке или обработке одного и того же сообщения не происходит изменений в состоянии системы, результат обработки остается таким же, как и при первой обработке. То есть, сколько раз не обрабатывай сообщение “товар списан со склада”, оно все равно должно сообщать только о том, что товар все еще списан со склада.
Таблица “INBOX” может содержать следующие поля:
- event_id — идентификатор события
- object — вид агрегата (сущности) в котором произошло событие
- event_type — тип события
- payload — полезная нагрузка (само сообщение)
- received_at — дата и время получения события из «outbox»
- processed_at — дата и время обработки события
- status — статус обработки события
event_id | object | event_type | payload | received_at | received_at | status |
---|---|---|---|---|---|---|
a54-4ht | order | orderCreated | {“id”:123, … } | 2024-04-27 09:00 | 2024-04-27 09:02 | new |
Реализация гарантии доставки “Exactly once”
В разделе “Точки отказа при передаче данных через Брокер сообщений” я приводил примеры реализации гарантий доставки “At least once Как мИнимум один раз” и “At most once Как мАксимум один раз”. Данные гарантии можно реализовать настройками брокера сообщений. Для реализации принципа Exactly once Точно один раз нужно одновременно реализовать и гарантию отправки сообщения, и гарантию обработки максимум одного сообщения.

Гарантия публикации сообщения в брокер “Сервисом А” достигается за счет реализации паттернов “Transactional outbox” и “Polling publisher” / “Transactional outbox” и “Transaction log tailing”.
Гарантия обработки максимум одного сообщения о событии “Сервисом Б” выполняется за счет реализации паттерна “transactional inbox pattern”.
«Exactly once Точно один раз» — это самый строгий контракт доставки сообщений, который гарантирует, что сообщение будет обработано ровно один раз, без возможности дублирования или потери.
Обеспечение гарантии доставки «Точно один раз» требует дополнительных механизмов и ресурсов, что приведет к увеличению сложности реализации и затраты на разработку и поддержку системы.
В случаях, когда ошибка обработки данных может привести к серьезным финансовым или репутационным последствиям, компания может оправдать дополнительные затраты на обеспечение гарантии доставки «Точно один раз», но в большинстве случаев она может быть излишней, особенно если ошибка обработки данных не представляет значительных рисков или, если компания готова принять определенный уровень дублирования\потери данных в угоду производительности и простоты реализации.
Выбор определенного уровня гарантии доставки зависит от требований конкретной бизнес-логики.
Завершение
Надеюсь, этот текст помог вам разобраться в архитектурных паттернах обеспечивающих согласованности данных. Теперь вы не запутаетесь в типах гарантий доставок и вы сможете выбрать нужный именно под ваш кейс.
Опубликовано 9 апреля 2024
Просмотров: 1265
Вместо комментариев