Re[15]: Помогите правильно спроектировать микросервисное приложение
От: gandjustas Россия http://blog.gandjustas.ru/
Дата: 01.02.26 14:45
Оценка:
Здравствуйте, Sinclair, Вы писали:

S>Здравствуйте, gandjustas, Вы писали:


G>>Да, я об этом и писал. Если все микро-сервисы могут работать независимо друг от друга, то такая архитектура жизнеспособда.

G>>Но тогда важное условие чтобы транзакционный бизнес-процесс не пересекал границы микросервисов. Как только начинает пересекать — появляются зависимости и одно без другого уже не работает.
S>С транзакциями да, нормального решения для распределённых транзакций не существует. Зато есть идемпотентность.
G>>Например для ecommerce деление на "заказ" и "склад" в таком случае не актуально, так как при создании заказа надо на складе все забронировать.
S>Либо у нас появляются разные заказы: один на стороне "корзинки", и другой на стороне склада — при его создании выполняется автоматическое резервирование.
S>А сервис корзинки (или там оркестрирования) как раз и занимается тем, что идемпотентно долбит "склад" запросами на создание "резерва" до тех пор, пока не получит внятный ответ.
Идемпотентность гарантирует только согласованность в конечном счете, но не гарантирует больше ничего.
Например два сервиса Заказ и Склад.
1) Заказ сохраняет у себя данные о том, что пользователь хочет заказать товары
2) Заказ резервирует товары на складе — делает идемпотентный запрос
3) После успешного резервирования пользователь попадает на страницу оплаты

На шаге 3 происходит коммит транзакции в заказе. Но что будет если этот коммит не случился? Сервис упал после выполнения 2 и после выполнения 3.
Пользователь получает ошибку, пытается еще раз — сервис лежит, пользователь плюет на это дело и идет в другой магазин.
Резерв на складе остается, он конечно будет отменен по прошествии таймаута оплаты, но за это время этот товар продать нельзя будет.

Это, конечно, мелочь, в одном месте можно забить. Но проблема в том, что такие мелочи всплывают на каждом пересечении границ МС.
А ели один сервис вынужден деграть не один, а два сервиса, то становится хуже.


G>>А вот "доставку" от "магазина-склада" вполне легко отделить.

S>Ну, тут опять — ведь у нас транзакция должна и товар из "резерва" в "отгружено" перевести, и в доставке "транспортную накладную" породить.
При появлении или смене статуса заказа сервис асинхронно отправляет данные в "Доставку". Тут согласованность в конечном счете устраивает, ибо у доставки вообще нет конкуренции за одни и те же данные.



G>>Это ровно до тех пор пока БЛ не начинает опираться на роли пользователей.

G>>Например: сотрудник магазина получает скидку 10% на все покупки в онлайн магазине. Чтобы решить эту задачу сервис "заказа" должен пойти в "auth", чтобы получить роль.
S>Либо просто посмотреть набор ролей в JWT, который был порождён auth при логине.
Как посмотреть? Хранить в кэше? Тогда наткнемся на устаревание данных
Каждому пользователю в токен писать значение — распухнет токен, да и то же устаревание на время жизни токена.
Получается согласованную в смысле acid логику не сделаешь. Может в этом конкретном случае не нужна, а если всё-таки нужна, то что делать?
И если уже было спроектировано в режиме "согласованности в конечном счете", то фарш назад не провернешь.

S>>>КМК, это не какие-то "новые" проблемы из-за МСА, а все те же проблемы, которые прекрасно работали и в монолите. Висение в ожидании возможно примерно везде, где есть await (то есть везде). Получение некорректных данных — да запросто, если разработчик воткнул какие-нибудь try {return calculatedTax()} catch { return defaultTax }.

G>>Вот именно. Чтобы в монолите получить некорректные данные надо что-то специально написать. В МСА надо специально писать чтобы данные были согласованы.
S>Не очень понятно, что имеется в виду. Если у нас в микросервисе нет аналогичного кода с try / catch, то обращение к упавшему сервису просто каскадно отдаст 500 internal error, а не неверные данные.
Я говорю про обращение к сервису, который зависит от упавшего.
Как в примере выше Заказ зависит от auth. Если auth упал сразу же после смены статуса сотрудника для пользователя, то работа Заказа будет некорректной.

G>>За счет чего? Если мы делаем синхронный вызов, то у нас надежность сервиса, зависит от надежности другого и в сумме не превышает надежность монолита.

S>За счёт того, что мы вообще не зависим от того, работают ли сервисы за пределами используемой цепочки.
Если бизнес-процесс, который должен выполняться транзакционно, охватывает цепочку сервисов, то с точки зрения надежности выгодно эту цепочку реализовать в монолите.
А если вязть весь набор таких процессов, то окажется что выгоднее в целом монолит иметь.

S>А в монолите у нас полное домино, когда любая фигня может привести к тому, что просто весь монолит не стартует из-за кривого коммита в маловажный угол.

Ну конечно же не может, ибо в рамках ci\cd процесса все проверяется. А kubernetes даже обновлять все экземпляры не будет если одна из нод не стартанет.
Короче проблема попадания плохого кода на прод и плохого деплоя решается вообще на другом уровне, а не на уровне архитектуры внутри вашего кода.

G>>Если мы делаем асинхронную репликацию изменений, то отдаем неверные данные.

S>Непонятно, что вы называете "асинхронной репликацией изменений". Вы имеете в виду, что я могу видеть некорректные остатки на складе из-за того, что у меня в "корзинке" есть заказ в статусе "резервируется"?
На примере Склад-Доставка. Склад получил статус заказа "оплачен". Фоновый процесс в сервисе заказ видит это изменение и публикует это изменение в рапределенной очереди.
Сервис доставки сидит сообщение из очереди и дает сигнал курьеру приехать забрать заказ.
Курьер приезжает на склад, берет заказ, в сервисе Склад статус меняется на Отгружен, и далее через тот же фоновый процесс данные попадают в сервис доставки.

S>Ну так это вопрос к тому, что мы вообще называем верными данными. Потому что ответ на вопрос "а есть ли у нас сейчас в наличии на складе PlayStation 5 pro" в распределённой среде очень сильно зависит от трактовки термина "сейчас".

Не надо задавать такие вопросы. В книге Pragmatic Programmer описан принцип "tell, don't ask". В соответствии с ним мы должны просто отправлять команду на бронирование PlayStation 5 pro.
Но мы тогда должны что-то делать если этого не произойдет.
В рамках монолита все просто — транзакция. Она атомарная, если что откатится целиком.
В рамках МСА у нас проблема, нет атомарности. Мы вызывали два сервиса в режиме "tell, don't ask", но второй вернул ошибку, а первый нет. Консистентность системы уже нарушена. Остается только вопрос можете ли вы с этим жить.
И самое главное сможете ли вы с этим жить в будущем.

G>>Основной агрумент против, это то, что МСА сама по себе ничего не дает и очень многое отнимает.

G>>Все проблемы, которые призвана решать МСА, прекрасно решаются и без МСА. Кроме, наверное, проблемы разработки на нескольких языках программирования.
S>Ну, "прекрасно" — вопрос спорный. Масштабируемость, к примеру, штука такая, которую трудно решить монолитом.
Да ладно, как-то решали до изобретения МСА (как термина), и сейчас решают неплохо.

Я с 2010 года работал с SharePoint, а с 2023 по середину 2025.
Оба продукта монолитные, оба масштабируются. Причем масштабируются так, как большинству приложений на МСА не снилось.
На уровне кода масштабирование происходит вокруг модулей.
То есть ты говоришь: на сервере А модули 1,2,3, а на сервере Б модули 4,5,6
В 1С еще круче, там каждую фоновую задачу можно "прибить" к своему набору серверов.

На уровне баз — каждая секция данных работает со своей базой (со своей строкой подключения).
В 1С вообще просто — одна Информационная База — одна БД. Можно каждую на своем сервере БД развернуть.
Связаны они между собой через веб-сервисы. То есть каждая база делает честный вызов веб-сервиса. Чтобы легко работала аутентификация можно всю аутентификацию проводить через одну базу (oauth) и есть консоль управления которая учетки в разные базы умеет пропихивать. Тоже через веб-сервисы.
Общих сервисов по сути нет, кроме поиска (он горизонтально масштабируется). Тенанты (наборы баз) изолированы за счет учетных записей — нет учетки в целевой базе, ты туда не попадешь. Консоль управления знает про тенанты, а больше никто не знает.
Каждая база в 1С это микросервис, который полностью покрывает один процесс.

В SharePoint по другому. Там нет "цепочек сервисов". Каждый бизнес-процесс может быть реализован как независимый "сайт" со своими "фичами", каждый "сайт может лежать в своей БД. Из серверного кода возможно прямое обращение к бд любого сайта.

В рамках стандартной функциональности дотнета можно приложенеи побить на модули, которые не знают друг о друге и запускать разный набор модулей на разных серверах. Для масштабирования БД можно применить все средства масштабирования БД — репликация, партишенинг, и, даже, прости господи, шардирование.

S>Контраргумент к этому тоже известен: в современную коробку влезает настолько много дури, что хорошо написанный монолит справляется в ней с нагрузкой, которую "красивая МСА" тащит тремястами машинками в облаке.

Но вот из недавних видео стало известно, что OpenAI долгое время сидит на одном кластере Postgres с одним мастером.

S>Но я видел и ребят, у которых такие масштабы нагрузки и объёмы данных, что даже самая передовая современная коробка пробуксовывает. И решают они это именно тем, что пилят систему на мелкие неэффективные запчасти.

А вот я не видел если честно. Я знаком как там внутри в Ozon и WB — там сокращение МСА в некоторых местах сильно бы повысило быстродействие пропускную способность на том же железе. Возможно во всех повысило бы, я просто не про все процессы в курсе.

До смешного: на моем текущем месте было 24 МС на 12 человек разработки. Предыдущий архитектор "свалил в закат" и меня позвали починить. Все тормозило, ошибки в данных и еще куча других проблем.
После починки 8 самых тормозящих запросов оказалось, что сервера средней руки (24 ядра, 96 гб ОП) достаточно чтобы все запустить на проде. Для БД еще одни такой же.
А если убрать МСА и заняться оптимизацией, то есть ощущение что эти требования сократятся еще в 4 раза. Чем я сейчас и занимаюсь.
Те проблемы, о которых пишу, я вижу и чиню каждый день.

S>То есть делаем в десять раз более прожорливую по ресурсам реализацию, и отдаём ей в сто раз больше ресурсов — получаем таки десятикратный рост пропускной способности по сравнению с топовым сервером.

А самое главное сколько при этом программистов можно загрузить работой

G>>И то, например, сейчас можно в дотнет процессе запускать Python код. Это не IronPython, который в IL компилирует, а это прямая загрузка питонячего рантайма в дотнет, генераторы, которые делают C# АПИ для питонячего кода и прокидывание ссылок на объекты в из дотнета в пайтон.

S>Да зачастую проблема языков не в языках, а в том, что одна команда уже переехала на Java 22, а другая всё ещё пердолится с Java 17. И как запустить их код в одном JVM — вопрос интересный.
У жабы какой-то свой мир со своими проблемами, которые мне малопонятны честно говоря.
Но у меня вопрос: а как изначально получилось что у двух команд разные версии? Кто-то же принял такое решение что две команды, должны независимо друг от друга что-то делать. Какая была логика в этом решении?

G>>>>Пример реальный: профили пользователей лежат в одном сервисе, данные о туристических маршрутах в другом. Важная часть логики: для несовреннолетних доступна одна часть маршрутов, для соврешеннолетных — другая. Есть еще другие признаки для фильтров: по регионам, интересам итд.

G>>И как это решить? Там отдельный сервис identity, который занимается аутентификацией и хранит профили (раньше кста отдельный сервис user был, но слили), и отдельный сервис БЛ.
S>В смысле "как"? Получаем список профилей, и идём по нему в цикле. Можно порезать его на 20 батчей и идти по ним параллельно на 20 машинах.
S>В общем-то, join делает то же самое — K операций стоимостью O(logM).
Ага, а тут мы еще получили протекание логики из БЛ в identity. Сменится "предикат" этого корсс-сервисного джоина и придется править в двух местах, со всеми прелестями, что омы обсуждаем ниже.

G>>Ну камон, фичафлаги же есть.

G>>Даже в микросервисах часто приходится релизить с фичафлагами по тем же причинам.
S>А причём тут фичафлаги? Они ортогональны обсуждаемой проблеме. Фичафлаг не автоматизирует ребейз моего "баннера" к "предыдущему релизу".
Ок, восстанавливаем контекст:
1) Если два набора функциональности, в рамках одного монолита асинхронно, то есть точек пересечения мало.
2) Но оба набора набора должны вместе.
Решение: пилим в разных ветках, делаем слабую связность через "контракты" чтобы меньше было конфликтов на пересечении, деплоим под фича флагами, то есть если флаг выключен, но работает все по старому, если включен — по новому.

Еще раз повторю, что проблема разной степени готовности того или иного функционала решается не за счет архитектуры приожения.
Все проблемы решаемые МСА решаются другими способами, зачастую и проще, и надежнее и менее ресурсоемко.


S>>>Нет такой идеи, поэтому и противоречить нечему.

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

Тут наверное надо четко разделить о чем мы говорим: об использовании сервисов (другого продукта со своей логикой) или о создании сервисов в рамках одного продукта.
С первым проблем никаких нет, сейчас любое приложение использует кучу внешних сервисов, начиная от аутентификации и адресов, заканчивая платежами.
Но в рамках одного продукта делать МСА смысла почти нет. Ну кроме масштабирования команд.


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

S>Усиление ревью и тестирования ещё сильнее удлиняет цикл релиза.
Это как раз касается вопроса масштабирования команд. На него ответа нет. Нельзя с командой в 100 человек использовать те же процессы, которые работают с командой на 10 человек. МСА дает какой-то ответ, пока лучше ничего не придумали.
Но до сих пор никто не знает где граница. Является ли МСА необходимостью если у тебя 50 человек — хз.

S>Помнится, была весёлая история про то, как пилили фичу про шатдаун в винде 10. Там от коммита в kernel до возможности протестировать кнопку в explorer.exe проходило три месяца — несмотря на отсутствие микросервисов и монолитность.

А микросервисы помогли бы? Мне кажется это как раз кейс против МСА. Потому что в данном случае не могу разработчик explorer пойти и добавить функцию в ядро. Этим занималась другая команда, с другими планами и графиками релизов. То что у них репа общая была ни на что на самом деле не влияет.

S>Я и говорю — всё это сводится к настройке ci/cd. Если в ней есть интеграционные тесты, то неважно, монорепо там, или микросервисы с отдельными репозиториями, или всё одно приложение в ./src.

S>А если нету — то никакой монорепо вас не спасёт.
А если несколько реп у каждого свой CI\CD как этот контроль и запуск интеграционнаых тестов обеспечить?
У нас реально было такое что в двух сервисах внесли изменения так, что они работали с предыдущей версией другого сервиса, поэтому прекрасно проходили тесты, а когда зарелизили обе новых версии все упало.



S>И только хардкорная смена архитектуры на "фронт ходит к нам в бэк только через тот же API, который мы отдаём наружу", заставляет волосы стать мягкими и шелковистыми.

Это тоже ни разу не гарантия.
Случай из практики: новые требования по валидации форм клента.
На клиенте запилили, а на сервере забыли. А когда я пошел выяснять почему забыли — оказалось что и не знали что надо все правила на сервере проверять.
По сути часть важной БЛ так и существует только на клиенте.

Причем ранее был код на blazor (для прототипа), там все проверки в одном месте пилилсь, а потом решили делать фронт на ангуляр по этой самой причине, чтобы делали все хорошо. Вот так "хорошо" и получилось.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.