Информация об изменениях

Сообщение Re[28]: Помогите правильно спроектировать микросервисное при от 14.02.2026 6:32

Изменено 14.02.2026 18:37 Sinclair

Re[28]: Помогите правильно спроектировать микросервисное при
Здравствуйте, gandjustas, Вы писали:

G>ИМХО ACID-транзакция, изобретенная на другом уровне абстракции — все равно транзакция. Если говорить то том, какие транзакции стоит использовать — самопальные или предоставляемые БД, то любой вменяемый разработчик выберет второй вариант.

Нет, сделать ACID-транзакцию поверх ненадёжного соединения не получится.
G>Единственная причина применять самопал — когда нет возможности применить транзакции в БД.
Всё верно.

G>- Я предлагаю состояние заказа менять в одной транзакции с обновлением остатков. Заказы и остатки естественно должны быть в одной базе

G>- МСА предлагает сделать две базы, где заказы лежат в одной, а заказы в другой.
G>Написать код вида:
G> 1. обнови остатки в базе А
G> 2. обнови статус заказа в базе Б
G> 3. если появилась ошибка, то откати обновление в базе А
Нет, МСА такого не предлагает.
МСА предлагает использование Representational State Transfer. Работает он так:
1. Заказ в базе Б находится в статусе "черновик".
2. По команде "зарезервировать заказ" заказ атомарно переходит в статус "резервируется". Из этого статуса его вручную вывести нельзя, и изменить состав заказа в этом статусе тоже нельзя.
3. Сервисом заказов делается попытка выполнить идемпотентную операцию "создать резерв" в сервисе А.
3.1. Если сервис А отвечает отказом, то сервис Б атомарно возвращает заказ в статус "черновик" и пишет в историю заказа "резервирование не удалось".
3.2. Если сервис А отвечает подтверждением, то сервис Б атомарно перемещает заказ в статус "зарезервировано", и для него становятся доступны следующие операции стейт-машины.
3.3. Если сервису Б не удается обработать ответ от сервиса А по любой из причин, перечисленных в моём предыдущем посте, то заказ остаётся в статусе "резервируется".

G>Если код падает по между шагами 2 и 3, то в базе остается несогласованное состояние.

Несогласованное состояние в REST возникает только в тот момент, когда мы не получили ответ на запрос "распространить изменение". Причём мы знаем о том, что оно несогласованное — и можем принять меры, чтобы эта несогласованность не вышла нам боком.

G>Значит вместе самим кодом резервирования заказа надо написать еще фоновую задачу, которая откатывает незавершенные резервы.

Нет, никакой "фоновой задачи отката" писать не нужно. А что нужно — так это писать движок стейт-машины, который вполне универсален. Любители бойлерплейта могут развернуться и написать отдельные "фоновые задачи" для каждого такого под-сценария, но современные технологии вполне позволяют раз и навсегда написать код, который в каждом сервисе процессит очередь исходящих запросов и долбит их до тех пор, пока не получит от удалённой стороны внятный ответ. Этот внятный ответ отдаётся в код "продолжения", который и переключает состояние машины в следующий стейт.

G>Я проделал аналогичное в рамках статьи на хабре, результаты неутешительные — https://habr.com/ru/articles/963120/ см раздел "рукопашные транзакции".

Я бы не сказал, что аналогичное. Там вы экспериментируете с рукопашными транзакциями в единой СУБД. Здесь речь идёт о реализации распространения изменений в ненадёжной среде.

G>Но у нас транзакции не изолированные, то есть межу 1 и 2 может вклиниться изменение, которое обновит заказ.

G>Это значит что для корректного кода отказа как в фоне, так и в п3 надо сохранять "слепок" заказа на шаге 1.
G>Этот слепок — это в чистом виде wal log.
Нет, так делать не надо. Основное заблуждение здесь — возможность надёжно определить ситуацию "возникла ошибка". Нет, у вас запросто может так получиться, что "пассивный" сервис в следующее состояние переехал, а "активный" об этом не узнал. Изменения должны распространяться ровно в одном направлении.
Схема начинает становиться более интересной в том случае, когда в транзакции участвует больше 1 пассивного сервиса.
Сам по себе REST даёт только идемпотентность, дающую нам возможность детерминированно двигаться "вперёд" во времени. И если у нас сервис 1 делает успешный вызов сервиса 2, а затем — неуспешный вызов сервиса 3.

G>Получение остатков это запрос к одной таблице. Ему никакая МСА не помешает.

Именно. А в монолите у вас тот же самый ресурс (CPU и шина) обрабатывают не только запрос к этой таблице, а ещё 2400 таблиц вашей развесистой ентерпрайз-БД.
И пока там кто-то начисляет бонусы сотрудникам, остатки на складах начинают лагать

G>Шардить можно и без МСА. Это вообще ортогональные вещи.

Не совсем ортогональные. Шарды — это и есть "микросервисы для бедных".

G>Мы же упираемся не в монолит, а в производительность одного сервера БД. Тут есть несколько решений:

Всё верно.
G>Правда это пока мы живем в рамках монолитной базы. Как только мы начинаем её разделять на сервисы (подбазы), то у нас появляются дополнительные данные и процессы, необходимые для поддержания целостности. Выше как раз пример такого. И все это жрет ресурсы.
Да, верно, жрёт.
G>При достаточном количестве микросервисов система упирается в "потолок" одного сервера очень быстро.
Вот это утверждение я не понял. Вы собираетесь все микросервисы гонять на одном узле?

G>Этот потолок сильно выше, чем кажется. При правильном подходе к проектированию до него доберутся единицы, а остальные от сложности проиграют только.

С этим согласен.

G>По моему опыту "потолок монолита" не в нагрузке, а тупо в размере команды. Когда у тебя 25 человек еще худо-бедно можно пилить монолит с общей кодовой базой. А если команда становится больше, то начинается деление, которое проходит ровно по границам подразделений. А если подразделения между собой не дружат и не имеют хорошего техлида над ними, то микросервисы помогают избежать бардака.

+1. Особенно ярко это проявляется в международных компаниях, где "над ними" не то, что хорошего — вообще никакого техлида может не быть.

G>Допустим


S>>Типа вот мы поднялись после сбоя и видим, что корзинка №42342342 была отправлена на резервирование, а результата резервирования нет.

G>А как мы узнаем что его нет?
Смотрим в свою базу и видим: статус — "резервирование начато".

G>А пользователь все это время ждет?

Конечно.
G>А если он не дождался и ушел?
Произойдёт ровно то же самое, что будет в случае, если у нас "пропал" монолит — ну там, временный сбой, перезагрузка сервиса. Или пропала связь между приложением пользователя и монолитом.
Или даже связь на месте, но одна из позиций на складе сейчас заблокирована другой транзакцией, в рамках которой происходит какое-то замедленное взаимодействие. Так что мы не получаем ни аборта, ни коммита, а просто ждём.
G>А если связь между пользователем и приложением пропала?
То он перезапускает приложение и восстанавливает картину мира.
G>А если пользователь ушлый и пока в одной вкладе крутится ожидание открыл сайт в другой вкладке и пошел что-то менять?
Если разработчики сервиса обладают мало-мальской квалификацией, то ничего плохого не произойдёт. Нельзя же, в самом деле, строить свой код на предположении о том, что с каждым заказом в один момент времени работает ровно одна транзакция.

S>>Да, объекты, пересекающие границы сервисов, в МСА должны храниться на обеих сторонах. Это не обязательно одни и те же объекты, но у них должна быть общая проекция. В данном случае сторону склада не интересуют никакие подробности про способы доставки товара, розничные цены, или там демографию покупателя, но вот список артикулов и количеств, снабжённый уникальным ID, ей необходим. Ровно для того, чтобы когда к нему в следующий раз стукнутся с просьбой зарезервировать корзинку №42342342 он мог не делать повторный резерв, а сразу отдать 200 ok.

G>Я понимаю, что при высокоуровневом взгляде кажется что это все просто, написать три-четыре строки в воркфлоу и будет хорошо. А на практике для обеспечения целостности нужны десятки строк кода и дополнительные данные хранить (что увеличивает нагрузку какбы).
Ну да, около двухсот строк кода. Один раз на всю систему. Не так уж и плохо
G>И самое главное — ради чего это все? Чтобы не упереться в мифический "потолок монолита". С МСА этот потолок окажется очень низко.

G>Это в каком контексте?

G>Насколько понимаю картинка эта для случая когда:
G>1) Есть несколько экземпляров ОДНОЙ И ТОЙ ЖЕ базы
G>2) Клиент подключается к ЛЮБОМУ экземпляру и может менять ЛЮБЫЕ данные
G>То есть мультиматер-кластер.
Да, совершенно верно.

G>В нашем случае вообще никаких кластеров нет. Мы просто в рамках одного процесса обращаемся к двум сервисам на двух разных серверах. Операция может завершиться успешно если обе операции завершатся успешно.

G>Каждая из этих баз может представлять из себя мкльтимастер-кластер, а может одни сервер, а может шардированную базу, это не имеет значения.
Тут картина на самом деле сильно сложнее. Детальный расчёт perceived availability потребует знания соотношения частот сценариев с различным количеством узлов.

G>>>·>"резервирование сделаем транзакционно" не решает проблему "пользователь плюет".

G>>>Конечно решает, потому что пользователь после резервации на складе точно получит свой заказ.
S>>В нашем случае пользователь после резервации на складе тоже точно получит свой заказ. Вся разница — в том, что если будет сбой системы во время заказа, то пользователь монолита до окончания сбоя будет получать 502, а пользователь МСА имеет шанс в это время увидеть спиннер "заказ резервируется....".
G>Это вопрос реализации фронта. Мы при любой архитектуре можем вынести повтор именно на фронт.
Фронт, с т.з. REST — это точно такой же узел сети. По-хорошему, он проектируется по тем же принципам; но благодаря наличию пользователя можно упростить реализацию. Хочешь повтор — жмёшь F5.

G>Тогда вопрос — если не видно разницы, то зачем платить больше?

Затем, что можно делать систему, которая выдерживает много пользователей. Если чо, антропики — не очень хороший пример. У них сценарии все крайне тривиальные, поэтому масштабироваться не трудно.
G>Я скинул ссылку на статью выше, там рукопашные транзакции почти в два раза уронили производительность.
В МСА коэффициент на таких транзакциях будет хуже, чем в два раза.
G>Дьявол как всегда в деталях.
Именно.

G>Я предлагаю на одном сконцентрироваться. Это же реальный сценарий.

G>Когда с ним закончим сможем посмотреть как эти подходы масштабировать.

S>>Остановка на техобслуживание одного из сервисов задержит только те сценарии, которые проходят через него.

G>Мы же рассматриваем сценарий когда у нас сценарий зависит от доступности двух серверов. На одно из них, а сразу двух. Их доступность равна произведению доступности обоих. А она будет меньше, чем доступность одного.

G>Мы о чем говорим? О серверах приложений или о субд?

Мы говорим о сервисе. Как у него там внутри устроено деление между СУБД и апп-серверами — это дело сервиса; пользователи всего этого не видят.
G>Приложения можно нарезать на десятки отдельных модулей и запускать отдельно: в отдельных процессах, в модулях одного процесса — как удобно. Все что написано выше для них верно.
Чтобы приложения можно было нарезать, между ними должны быть какие-то границы. Как вы передадите одну транзакцию между несколькими модулями в разных процессах? В МСА вы можете накатить апдейт на "модуль ценообразования", не останавливая модули корзинки и склада. В монолите вам придётся делать общий даунтайм. И количество серверов СУБД в HA-кластере вас не спасёт от того, что апдейт схемы плохо совместим с data-level блокировками. То есть "на ходу" его провести возможно далеко не всегда; безопасный способ — переключать базу в монопольный режим.
Re[28]: Помогите правильно спроектировать микросервисное при
Здравствуйте, gandjustas, Вы писали:

G>ИМХО ACID-транзакция, изобретенная на другом уровне абстракции — все равно транзакция. Если говорить то том, какие транзакции стоит использовать — самопальные или предоставляемые БД, то любой вменяемый разработчик выберет второй вариант.

Нет, сделать ACID-транзакцию поверх ненадёжного соединения не получится.
G>Единственная причина применять самопал — когда нет возможности применить транзакции в БД.
Всё верно.

G>- Я предлагаю состояние заказа менять в одной транзакции с обновлением остатков. Заказы и остатки естественно должны быть в одной базе

G>- МСА предлагает сделать две базы, где заказы лежат в одной, а заказы в другой.
G>Написать код вида:
G> 1. обнови остатки в базе А
G> 2. обнови статус заказа в базе Б
G> 3. если появилась ошибка, то откати обновление в базе А
Нет, МСА такого не предлагает.
МСА предлагает использование Representational State Transfer. Работает он так:
1. Заказ в базе Б находится в статусе "черновик".
2. По команде "зарезервировать заказ" заказ атомарно переходит в статус "резервируется". Из этого статуса его вручную вывести нельзя, и изменить состав заказа в этом статусе тоже нельзя.
3. Сервисом заказов делается попытка выполнить идемпотентную операцию "создать резерв" в сервисе А.
3.1. Если сервис А отвечает отказом, то сервис Б атомарно возвращает заказ в статус "черновик" и пишет в историю заказа "резервирование не удалось".
3.2. Если сервис А отвечает подтверждением, то сервис Б атомарно перемещает заказ в статус "зарезервировано", и для него становятся доступны следующие операции стейт-машины.
3.3. Если сервису Б не удается обработать ответ от сервиса А по любой из причин, перечисленных в моём предыдущем посте, то заказ остаётся в статусе "резервируется".

G>Если код падает по между шагами 2 и 3, то в базе остается несогласованное состояние.

Несогласованное состояние в REST возникает только в тот момент, когда мы не получили ответ на запрос "распространить изменение". Причём мы знаем о том, что оно несогласованное — и можем принять меры, чтобы эта несогласованность не вышла нам боком.

G>Значит вместе самим кодом резервирования заказа надо написать еще фоновую задачу, которая откатывает незавершенные резервы.

Нет, никакой "фоновой задачи отката" писать не нужно. А что нужно — так это писать движок стейт-машины, который вполне универсален. Любители бойлерплейта могут развернуться и написать отдельные "фоновые задачи" для каждого такого под-сценария, но современные технологии вполне позволяют раз и навсегда написать код, который в каждом сервисе процессит очередь исходящих запросов и долбит их до тех пор, пока не получит от удалённой стороны внятный ответ. Этот внятный ответ отдаётся в код "продолжения", который и переключает состояние машины в следующий стейт.

G>Я проделал аналогичное в рамках статьи на хабре, результаты неутешительные — https://habr.com/ru/articles/963120/ см раздел "рукопашные транзакции".

Я бы не сказал, что аналогичное. Там вы экспериментируете с рукопашными транзакциями в единой СУБД. Здесь речь идёт о реализации распространения изменений в ненадёжной среде.

G>Но у нас транзакции не изолированные, то есть межу 1 и 2 может вклиниться изменение, которое обновит заказ.

G>Это значит что для корректного кода отказа как в фоне, так и в п3 надо сохранять "слепок" заказа на шаге 1.
G>Этот слепок — это в чистом виде wal log.
Нет, так делать не надо. Основное заблуждение здесь — возможность надёжно определить ситуацию "возникла ошибка". Нет, у вас запросто может так получиться, что "пассивный" сервис в следующее состояние переехал, а "активный" об этом не узнал. Изменения должны распространяться ровно в одном направлении.
Схема начинает становиться более интересной в том случае, когда в транзакции участвует больше 1 пассивного сервиса.
Сам по себе REST даёт только идемпотентность, дающую нам возможность детерминированно двигаться "вперёд" во времени. И если у нас сервис 1 делает успешный вызов сервиса 2, а затем — неуспешный вызов сервиса 3, то мы зависаем в некотором "бесполезном" состоянии, из которого может не оказаться вообще никакого выхода. Ну, как если купить несдаваемые билеты, и обнаружить, что мест в гостиницах в городе назначения уже нет.
К счастью, в большинстве практических случаев всё же сторонние сервисы предоставляют возможности "сторнировать" проведённое изменение. Зарезервированный товар можно снять с резерва, проведённый по карте платёж можно отменить. И даже покупка авиабилетов очень часто всего лишь ставит их в резерв, хоть и на не очень длинное время. Но всё же это время заведомо больше, чем время рестарта типичных сервисов, поэтому нам его хватит для завершения сценария даже в таком сложном случае.
А если мы говорим об МСА, где все участники сценария — наши же собственные сервисы, то мы их сразу так и проектируем, чтобы иметь возможность безболезненной отмены, если какая-то из частей сценария напоролась на неразрешимую ситуацию.

G>Получение остатков это запрос к одной таблице. Ему никакая МСА не помешает.

Именно. А в монолите у вас тот же самый ресурс (CPU и шина) обрабатывают не только запрос к этой таблице, а ещё 2400 таблиц вашей развесистой ентерпрайз-БД.
И пока там кто-то начисляет бонусы сотрудникам, остатки на складах начинают лагать

G>Шардить можно и без МСА. Это вообще ортогональные вещи.

Не совсем ортогональные. Шарды — это и есть "микросервисы для бедных".

G>Мы же упираемся не в монолит, а в производительность одного сервера БД. Тут есть несколько решений:

Всё верно.
G>Правда это пока мы живем в рамках монолитной базы. Как только мы начинаем её разделять на сервисы (подбазы), то у нас появляются дополнительные данные и процессы, необходимые для поддержания целостности. Выше как раз пример такого. И все это жрет ресурсы.
Да, верно, жрёт.
G>При достаточном количестве микросервисов система упирается в "потолок" одного сервера очень быстро.
Вот это утверждение я не понял. Вы собираетесь все микросервисы гонять на одном узле?

G>Этот потолок сильно выше, чем кажется. При правильном подходе к проектированию до него доберутся единицы, а остальные от сложности проиграют только.

С этим согласен.

G>По моему опыту "потолок монолита" не в нагрузке, а тупо в размере команды. Когда у тебя 25 человек еще худо-бедно можно пилить монолит с общей кодовой базой. А если команда становится больше, то начинается деление, которое проходит ровно по границам подразделений. А если подразделения между собой не дружат и не имеют хорошего техлида над ними, то микросервисы помогают избежать бардака.

+1. Особенно ярко это проявляется в международных компаниях, где "над ними" не то, что хорошего — вообще никакого техлида может не быть.

G>Допустим


S>>Типа вот мы поднялись после сбоя и видим, что корзинка №42342342 была отправлена на резервирование, а результата резервирования нет.

G>А как мы узнаем что его нет?
Смотрим в свою базу и видим: статус — "резервирование начато".

G>А пользователь все это время ждет?

Конечно.
G>А если он не дождался и ушел?
Произойдёт ровно то же самое, что будет в случае, если у нас "пропал" монолит — ну там, временный сбой, перезагрузка сервиса. Или пропала связь между приложением пользователя и монолитом.
Или даже связь на месте, но одна из позиций на складе сейчас заблокирована другой транзакцией, в рамках которой происходит какое-то замедленное взаимодействие. Так что мы не получаем ни аборта, ни коммита, а просто ждём.
G>А если связь между пользователем и приложением пропала?
То он перезапускает приложение и восстанавливает картину мира.
G>А если пользователь ушлый и пока в одной вкладе крутится ожидание открыл сайт в другой вкладке и пошел что-то менять?
Если разработчики сервиса обладают мало-мальской квалификацией, то ничего плохого не произойдёт. Нельзя же, в самом деле, строить свой код на предположении о том, что с каждым заказом в один момент времени работает ровно одна транзакция.

S>>Да, объекты, пересекающие границы сервисов, в МСА должны храниться на обеих сторонах. Это не обязательно одни и те же объекты, но у них должна быть общая проекция. В данном случае сторону склада не интересуют никакие подробности про способы доставки товара, розничные цены, или там демографию покупателя, но вот список артикулов и количеств, снабжённый уникальным ID, ей необходим. Ровно для того, чтобы когда к нему в следующий раз стукнутся с просьбой зарезервировать корзинку №42342342 он мог не делать повторный резерв, а сразу отдать 200 ok.

G>Я понимаю, что при высокоуровневом взгляде кажется что это все просто, написать три-четыре строки в воркфлоу и будет хорошо. А на практике для обеспечения целостности нужны десятки строк кода и дополнительные данные хранить (что увеличивает нагрузку какбы).
Ну да, около двухсот строк кода. Один раз на всю систему. Не так уж и плохо
G>И самое главное — ради чего это все? Чтобы не упереться в мифический "потолок монолита". С МСА этот потолок окажется очень низко.

G>Это в каком контексте?

G>Насколько понимаю картинка эта для случая когда:
G>1) Есть несколько экземпляров ОДНОЙ И ТОЙ ЖЕ базы
G>2) Клиент подключается к ЛЮБОМУ экземпляру и может менять ЛЮБЫЕ данные
G>То есть мультиматер-кластер.
Да, совершенно верно.

G>В нашем случае вообще никаких кластеров нет. Мы просто в рамках одного процесса обращаемся к двум сервисам на двух разных серверах. Операция может завершиться успешно если обе операции завершатся успешно.

G>Каждая из этих баз может представлять из себя мкльтимастер-кластер, а может одни сервер, а может шардированную базу, это не имеет значения.
Тут картина на самом деле сильно сложнее. Детальный расчёт perceived availability потребует знания соотношения частот сценариев с различным количеством узлов.

G>>>·>"резервирование сделаем транзакционно" не решает проблему "пользователь плюет".

G>>>Конечно решает, потому что пользователь после резервации на складе точно получит свой заказ.
S>>В нашем случае пользователь после резервации на складе тоже точно получит свой заказ. Вся разница — в том, что если будет сбой системы во время заказа, то пользователь монолита до окончания сбоя будет получать 502, а пользователь МСА имеет шанс в это время увидеть спиннер "заказ резервируется....".
G>Это вопрос реализации фронта. Мы при любой архитектуре можем вынести повтор именно на фронт.
Фронт, с т.з. REST — это точно такой же узел сети. По-хорошему, он проектируется по тем же принципам; но благодаря наличию пользователя можно упростить реализацию. Хочешь повтор — жмёшь F5.

G>Тогда вопрос — если не видно разницы, то зачем платить больше?

Затем, что можно делать систему, которая выдерживает много пользователей. Если чо, антропики — не очень хороший пример. У них сценарии все крайне тривиальные, поэтому масштабироваться не трудно.
G>Я скинул ссылку на статью выше, там рукопашные транзакции почти в два раза уронили производительность.
В МСА коэффициент на таких транзакциях будет хуже, чем в два раза.
G>Дьявол как всегда в деталях.
Именно.

G>Я предлагаю на одном сконцентрироваться. Это же реальный сценарий.

G>Когда с ним закончим сможем посмотреть как эти подходы масштабировать.

S>>Остановка на техобслуживание одного из сервисов задержит только те сценарии, которые проходят через него.

G>Мы же рассматриваем сценарий когда у нас сценарий зависит от доступности двух серверов. На одно из них, а сразу двух. Их доступность равна произведению доступности обоих. А она будет меньше, чем доступность одного.

G>Мы о чем говорим? О серверах приложений или о субд?

Мы говорим о сервисе. Как у него там внутри устроено деление между СУБД и апп-серверами — это дело сервиса; пользователи всего этого не видят.
G>Приложения можно нарезать на десятки отдельных модулей и запускать отдельно: в отдельных процессах, в модулях одного процесса — как удобно. Все что написано выше для них верно.
Чтобы приложения можно было нарезать, между ними должны быть какие-то границы. Как вы передадите одну транзакцию между несколькими модулями в разных процессах? В МСА вы можете накатить апдейт на "модуль ценообразования", не останавливая модули корзинки и склада. В монолите вам придётся делать общий даунтайм. И количество серверов СУБД в HA-кластере вас не спасёт от того, что апдейт схемы плохо совместим с data-level блокировками. То есть "на ходу" его провести возможно далеко не всегда; безопасный способ — переключать базу в монопольный режим.