Одной из самых распространенных проблем в проектировании распределенных систем является поддержание согласованности.
Пример
Рассмотрим следующий пример: на сайте электронной коммерции, когда клиент размещает заказ, необходимо обновить таблицу заказов, чтобы зафиксировать сделанный заказ, и таблицу вознаграждений для записи начисленных баллов.
В монолитной архитектуре, как в данном примере, это просто. Начинается транзакция, обновляются необходимые таблицы, и транзакция фиксируется. Если что-то пойдет не так, все изменения откатываются. Но что происходит, когда разные сервисы обрабатывают историю заказов и вознаграждения?
Это называется распределенной транзакцией. Для того, чтобы система оставалась согласованной, несколько сервисов должны обработать событие, и в случае сбоя хотя бы в одном из сервисов все остальные сервисы должны откатить свои изменения.
Проблемный случай
Но что если ошибка произойдет прямо после записи в таблицу заказов? Сервер может выйти из строя, запрос может достичь таймаута по времени исполнения, или сервис вознаграждений может не успеть обработать событие. Это приведет к несогласованности состояния системы. Пользователь разместит заказ, но не получит баллы за вознаграждение.
Архитектура, основанная на событиях (Вопросы для интервью Event Driven Architecture (EDA)), может гарантировать, что сервис вознаграждений обработает событие хотя бы один раз после того, как событие будет отправлено в очередь событий. После записи в таблицу заказов, сервис заказов отправляет событие в сервис вознаграждений, и тот может в конечном итоге обработать событие и обновить необходимые баллы в таблице вознаграждений.
Мы достигли постепенной согласованности (eventually consistency). Но еще не все. Мы все еще не решили проблему с выходом сервера из строя прямо перед записью события в очередь событий. Что если не удастся отправить событие в саму очередь событий?
Существуют несколько паттернов, которые можно использовать для гарантии того, что сервис вознаграждений получит событие.
Паттерн 1: Паттерн выходной очереди (Outbox Pattern)
Паттерн выходной очереди сохраняет событие в таблице выходных сообщений в той же базе данных (в данном случае) что и таблица заказов. Запись в базу данных может быть выполнена в рамках одной транзакции, так что мы можем вставить заказ и событие в выходную очередь в рамках одной транзакции. Если одна из операций не удастся, другая также будет откатана.
Для передачи событий можно использовать Change Data Capture (CDC), чтобы сервис вознаграждений отслеживал изменения в таблице и обрабатывал их.
Не забывайте обслуживать таблицу выходной очереди
Часто это забывают. Обработанные события должны быть удалены из таблицы выходных сообщений. Это предотвращает рост таблицы и поддерживает порядок.
Паттерн 2: Обработка событий в сыром виде (Raw Event Processing)
Если передача событий в сервис вознаграждений требуется только при изменении таблицы заказов, можно использовать Change Data Capture на таблице заказов вместе с фильтрами событий, вместо того, чтобы создавать отдельную таблицу выходных сообщений. Каждый раз, когда новый заказ добавляется в таблицу заказов, это изменение данных может быть захвачено и обработано как событие для сервиса вознаграждений.
Паттерн 3: Паттерн чтения самим собой (Reading Yourself Pattern)
В обоих вышеприведенных примерах мы хотим, чтобы существовал единственный источник, который производит события, и который может их производить в рамках одной транзакции. Мы можем достичь этого, создавая событие через сервис заказов и обрабатывая его как сервисом заказов, так и сервисом вознаграждений.
Здесь, если шаг (2) выполняется успешно, можно гарантировать, что в конечном итоге оба сервиса — заказы и вознаграждения — обработают событие, и состояние нашей системы будет согласованным.