Исторический контекст
У нас в Профи было два пакетика травы монолита, развитие бизнеса и сложности работы с большими кусками кода. Команды наступали друг-другу на пятки, конфликты на реквестах и т.п. В итоге, как и многие, мы пришли к микросервисной архитектуре — стали делить бизнес-логику на отдельные сервисы сильно связанные внутри, но слабо связанные между собой. При этом каждый сервис выставляет 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. В комментариях было бы интересно услышать про ваш опыт работы с консистентностью данных в микросервисах.
Комментарии (9)
titan_pc
26.10.2023 17:27-1Коротко о транзакциях и их распределении - берёшь и выкидываешь их нафиг. На секунду помешаешь в голову мысль, что человечество их ещё не изобрело. Стало страшно? Поборол страх, поместил эту мысль ещё раз, теперь на минуту. Подумал хм, а может быть собрать всё как-то иначе. И придумал какую-нибудь квантовую асинхронную мульти-супер позицию состояния данных. Родил что-то новое в общем.
Ну а если без фантастики. Ну неужели нет способа без транзакций в реляционках системы строить. На каких нибудь хитрых кэшах или crdt. И как нибудь там шмяк бух и вау чего случилось.
em1nx Автор
26.10.2023 17:27+1Транзакция здесь — не способ построения, а абстракция от реального бизнес-процесса покупка->товар, например. Без транзакции ты физически можешь потерять данные о покупке или товаре и тогда никакие кеши тебя не спасут— спасёт только философия :) Типа, извини, друг, не повезло — потеряли мы твой "товар" и это есть суть вещей. Низведём же в конструкте человеческого сознания страх потери!
titan_pc
26.10.2023 17:27+2Нужно исходить из более реальных примеров. А то в этом - покупка товар данные можно из бэкапов восстановить. А иначе как ты в таком простом кейсе умудрился их потерять, и где тут тебе транзакции помогли.
Зайдём с другого угла. Ну был у тебя монолит, разделил его на сервисы. Один сервис - одна бд. Вроде всё сделал правильно, НО именно путь один сервис - одна бд и привёл тебя к необходимости распределения транзакций. Ты в итоге не туда свернул и решил проблему У. Вместо решения проблемы Х. То есть тебя что-то не устраивало в монолите и ты его поменял, словил проблему ХУ.
Ты просто вспомни свой монолит. Наверное там не нужны были всякие саги... Но почему не нужны были? Вот и ключ. А сделай ты одну бд и пусть все сервисы пишут туда и смотрят туда. И сделай кое что весьма полезное одна сущность - один запрос на запись в бд. Если запрос не удался, то и ладно ты ничего не потерял. А если удался - то всё круто, все данные за один запрос зашли куда надо.
В статьях почему и как вы пошли в распределение транзакций не хватает контекста а накой оно вообще Вам надо. Я вот представил что ты монолит пилил по рекламной брошуре, которую называют бест перктис... Но почему это бест и когда они там обычно тоже не пишут. Ну сделайте один сервис - одна бд, сагу там добавьте и вот... А нафига - не говорят, а мы и не спрашиваем.
peterpro
26.10.2023 17:27+2И на любой чих регрессим весь монолит. И релизы собираем полгода.
LbISS
26.10.2023 17:27А как вы распределённую транзакцию будете проверять без общесистемной регрессии? Или типа, сделали её распределённой - ну и бог бы с ней, больше не тестируем? Проблема-то никуда не ушла, тестировать надо.
Жизнь без распределённых транзакций проще и лучше. И это достижимо во многих кейсах, даже если вдруг одна БД вам претит - чуть более тщательный анализ доменной области скорее всего сожет позволить пилить микросервисы, чтобы одной семантически значимой сущностью владел один микросервис, а не несколько. Тогда весь этот гемморой резко пропадает.
Sipaha
У нас 2PC работает изумительно и имеет нулевую дополнительную сложность для разработчиков бизнес-логики. Пришлось конечно повозиться чтобы реализовать эту транзакционность в платформе (готовые не подошли), но теперь голова о транзакциях в распределенной системе почти не болит.
em1nx Автор
Было бы интересно почитать подробности :) Обычно же как раз не советуют использовать 2PC.