О мутациях в микросервисной архитектуре

6b300c2425fe21e66a439a33e308f396.jpg

Исторический контекст

У нас было два пакетика травы монолита, развитие бизнеса и сложности работы с большими кусками кода. Команды наступали друг-другу на пятки, конфликты на реквестах и т.п. В итоге, как и многие, мы пришли к микросервисной архитектуре — стали делить бизнес-логику на отдельные сервисы сильно связанные внутри, но слабо связанные между собой. При этом каждый сервис выставляет GraphQL схему, которая мержится в единое API, к которому уже обращаются frontend-клиенты и другие сервисы.

Всё шло более-менее хорошо, пока не появилась задача выноса биллинга в отдельный микросервис с отдельной базой. Основная сложность была в том, что при разбиении транзакций требуется не только разнести INSERT-ы и UPDATE-ы, но также обеспечить их гарантированное выполнение (или не выполнение). Консистентность тут очень важна т.к. кривые записи в БД в лучшем случае грозят протухшими заказами на доске, а в худшем — тёрками с налоговой.

Есть две задачи

Надёжные мутации

Распиливая монолиты на микросервисы, мы неминуемо приносим в жертву надёжность вызовов между компонентами в пользу гибкости разработки. Проблема возникает не только в мутациях, но и при любых запросах. Однако, именно при падении мутаций с высокой вероятностью возникают битые данные — не загрузился файл, не сбросился кеш, заказ не попал в индекс, не отправилось сообщение в чат и т.п.

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

Надёжные транзакции

Эта задача по сути является продолжением предыдущей т.к. транзакция — это список мутаций, которые в результате выполнения переводят данные в некоторое консистентное состояние, но есть нюансы:

  • В общем случае мутации должны выполняться последовательно.

  • После очередного шага может возникнуть ошибка или логический останов, а значит нужно предусмотреть возможность «отката».

  • Данные, изменяемые в рамках транзакции, находятся в soft state и перейдут в консистентное состояние не атомарно.

Критерии оценки решения

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

Development friendly

  • Максимальная простота для основных потребителей — разработчиков,

  • визуально код легко конвертируется в продуктовые сценарии и обратно,

  • легко заводится на стендовом и локальном окружении,

  • поддержка TypeScript, PHP и Python,

  • не создаёт сильных проблем с обратной совместимостью кода и данных,

  • если complexity, то в основном для авторов и оунеров решения.

Reuse oriented

  • Одновременно решает задачу надёжных мутаций и надёжных транзакций,

  • при этом максимально переиспользует существующую архитектуру и технологии.

Fault tolerant

  • Уменьшает связность между сервисами,

  • самостоятельно восстанавливается после падений и повторяет операции в случае ошибок,

  • старается прощать косяки разработчиков: баги не кладут всё к чертям, но система настойчиво сигнализирует о проблеме.

Safe

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

  • умеет безопасно работать с персональными данными.

Предварительные варианты

Хореографическая сага

Интересно, но скорее всего нет т.к. там явно проблемы с development friendly и прозрачностью бизнес сценариев. Красивые доменные события, отличный декаплинг, возможность легко встраиваться в бизнес процесс — всё это, конечно, даёт невероятную теоретическую гибкость.

Однако, чтобы сделать достойный DX, нужно сильно упороться, не говоря уже про компенсирующие шаги, ключи идемпотентности и «наблюдателя», который, в случае чего, запустит ретрай или откат. Плюс возникают сложности «фиксации сценариев» например, для А/Б тестирования — крайне не желательно, чтобы кто-то втихаря вешал дополнительную логику во время проведения теста.

Оркестрационная сага

Кажется хорошим вариантом т.к. позволяет держать «описание» транзакции в одном месте, при этом сохраняя декаплинг на приличном уровне. Надо копать дальше и смотреть конкретные реализации.

Transactional Outbox

Техника, позволяющая «атомарно» записать данные в БД и отправить об этом событие в шину. А что если мы заменим «отправку события» на вызов API и добавим всякие retry policy? Получается что-то типа бинлога операций в БД — пишем всё, что нужно сделать, а далее выполняем операции условно «до талого». Интересно. Правда смущает то, что каждый инициатор транзакции должен иметь свою БД и message relay.

Workflow Engine

Это такие комплексные системы, которые позволяют надёжно запускать бизнес-логику в виде последовательности шагов. Обычно, говоря о workflow engine, подразумевают long running tasks, при этом «long» здесь не буквальное и может быть относительно коротким. Следовательно, можно попробовать приспособить такую штуку под задачу надёжных мутаций. Надо только выбрать сам workflow engine, которых расплодилось довольно много.

Дальнейший план

  • Формулируем 2–3 решения, которые наиболее полно соответствуют выбранным критериям.

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

  • Пробуем масштабировать решение на другие задачи — запуск асинхронных заданий, обработка заказов и т.п.

  • Делаем сводную таблицу и экспертно выбираем вариант.

Подробности вариантов и финальное решение я выложу в следующих статьях. Спасибо, что дочитали!

P.S. В комментариях было бы интересно услышать про ваш опыт работы с консистентностью данных в микросервисах.

© Habrahabr.ru