Всем привет! Меня зовут Эмин Чернов, я руководитель платформенных команд и архитектор в компании Профи. Сегодня расскажу про то, как мы столкнулись с проблемой консистентных изменений данных в микросервисах и каким образом планируем её порешать.
Исторический контекст
У нас было два пакетика травы монолита, развитие бизнеса и сложности работы с большими кусками кода. Команды наступали друг-другу на пятки, конфликты на реквестах и т.п. В итоге, как и многие, мы пришли к микросервисной архитектуре — стали делить бизнес-логику на отдельные сервисы сильно связанные внутри, но слабо связанные между собой. При этом каждый сервис выставляет 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. В комментариях было бы интересно услышать про ваш опыт работы с консистентностью данных в микросервисах.
Статьи из серии
Первая часть: эта статья
Вторая часть про Temporal: https://habr.com/ru/articles/772084/
Третья часть про TSQM: https://habr.com/ru/articles/776794/