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

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


G>>Сохранишь копию order в то же базе в той же тразакции, да?

S>Техническое решение этой задачи для микросервисов уже обсуждалось. Да, оно в каком-то смысле "хуже", чем единая ACID-транзакция — у заказа появляется некое промежуточное состояние, которого не было в исходной реализации.
S>Но давайте прекратим делать вид, что этого решения нет, т.к. это состояние — эфемерное, и фактически время его жизни равно времени даунтайма сервиса склада с точки зрения сервиса корзинки.

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


Чтобы говорить об одном и том же давай зафиксируем детали о которых шла речь:
— Я предлагаю состояние заказа менять в одной транзакции с обновлением остатков. Заказы и остатки естественно должны быть в одной базе
— МСА предлагает сделать две базы, где заказы лежат в одной, а заказы в другой.
Написать код вида:
1. обнови остатки в базе А
2. обнови статус заказа в базе Б
3. если появилась ошибка, то откати обновление в базе А

Если код падает по между шагами 2 и 3, то в базе остается несогласованное состояние.
Значит вместе самим кодом резервирования заказа надо написать еще фоновую задачу, которая откатывает незавершенные резервы.
Я проделал аналогичное в рамках статьи на хабре, результаты неутешительные — https://habr.com/ru/articles/963120/ см раздел "рукопашные транзакции".


Но у нас транзакции не изолированные, то есть межу 1 и 2 может вклиниться изменение, которое обновит заказ.
Это значит что для корректного кода отказа как в фоне, так и в п3 надо сохранять "слепок" заказа на шаге 1.
Этот слепок — это в чистом виде wal log.



G>>Ты наверное начнешь разговор что так делать не надо. Но озону надо. Потому что разница limit-reserverd отображается в интерфейсе приложения как "сколько осталось" и это отображается прямо в результатах поиска, то есть надо быстро получать это число для любого количества товаров.

S>Микросервисы никак не мешают быстрому показу remaining — скорее наоборот, т.к. RDBMS склада вообще не обрабатывает никакой нагрузки, кроме вот этого вот get remaining и reserve stock / unreserve stock / top-up stock.
Получение остатков это запрос к одной таблице. Ему никакая МСА не помешает.

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

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

S>Не, я в курсе, что как только мы начнём джойнить эту информацию с какими-нибудь запросами по другой части домена (типа "робот-пылесос в пределах 20000 рублей с доставкой до послезавтра и средним баллом по отзывам не ниже 4.9"), то МСА со страшной силой сольёт монолиту. Вот только если мы всё же уперлись в потолок монолита, то возможность получить 2х перформанс ценой распиливания его на 8х частей может оказаться единственным выбором.

Мы же упираемся не в монолит, а в производительность одного сервера БД. Тут есть несколько решений:
1) Кэши для чтения, чтобы нагрузка на БД не прилетала вообще.
2) Чтение из реплик, чтобы нагрузка на мастер не прилетала там где допустимо отдавать слегка устаревшие данные.
3) Партицирование таблиц — это как шардирование, только в пределах одного сервера БД, чтобы нагрузку на запись распределять по разным дискам.
И только когда все возможности исчерпаны — делать шардирование.
ИМХО в ни у одного веб-сайта или приложения нет такой нагрузки чтобы шардирование было оправдано.

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


G>>Не лучше, чем в одной базе. Прям строго математически не лучше.

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

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

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

G>>·>Это эквивалентно ситуации: клиент наполнил корзину и ушел плюнув, потому что левая пятка зачесалась. Код отката брони на складе ты будешь писать в любом случае.
G>>Не эквивалентно и не придется такое писать. Это твои фантазии
S>Нет, откат писать не надо. Надо писать "накат". И это можно сделать один раз в инфраструктуре worflow-engine.
Допустим

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

А как мы узнаем что его нет?

S>Значит, либо мы в прошлый раз не достучались до сервера (получили сonnection timeout), либо достучались да он упал до начала резервирования (отдал нам 5хх), либо упал после окончания резервирования (и отдал нам 5хх или connection reset by peer), либо всё нам отдал, да мы расплескали по пути и упали до коммита в локальную БД.

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


G>>То ест мало того, что для обработки отмены ты вынужден будешь сохранить почти весь order в в той же транзакции, что и обновление остатков, так еще и дополнишь его полем ключа идемпотентности

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


S>Вот картинка для доступности строго согласованного кластера из трёх узлов с доступностью Anode, связанных каналами с доступностью Alink:

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

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


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

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

S>В любом случае результат резервирования станет известен только по окончанию сбоя — и точно так же, как в монолитном случае можно получить как подтверждение успеха, так и "извините, но другой покупатель успел зарезервировать товар до вас".

Тогда вопрос — если не видно разницы, то зачем платить больше?
Я скинул ссылку на статью выше, там рукопашные транзакции почти в два раза уронили производительность.

G>>·>Для этого не требуется обновлять корзину и склад в одной транзакции.

G>>Я уже выше описал почему требуется. Не повторяй эту глупость уже
S>При всём уважении — это не глупость, а вполне себе математическая реальность. Я выше написал, как именно это работает.
Дьявол как всегда в деталях.

G>>Лол, а зачем?

S>Затем, что в реальном приложении — сотни бизнес-сценариев.
Я предлагаю на одном сконцентрироваться. Это же реальный сценарий.
Когда с ним закончим сможем посмотреть как эти подходы масштабировать.

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

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

S>И если этот сервис достаточно простой и маленький, то его рестарт не будет занимать десятки минут. Таким образом, perceived availability может оказаться значительно выше, чем у монолита. Ну вот, абстрактно, мы на пять минут отключили банковское ядро, которое собственно проводит платежи. Если всё сделано по уму, то пользователи это увидят только как "странно, я вроде по СБП деньги отправил, а у получателя телефон что-то не вибрирует". Если чуть хуже — то как "переводы пока недоступны, приходите позже". Если ещё хуже — то как "Непредвиденная ошибка. Перезапустите приложение или зайдите позже". И в любом из этих случаев люди, которые смотрят какую-нибудь там аналитику расходов, или остатки по вкладам, или условия кредитов/страховок/етк не заметят вообще ничего. С их точки зрения никакого сбоя не было. А если нам нужно сделать то же самое в монолите, то опускать нужно примерно всё, и всё будет лежать сразу у всех пользователей.

Мы о чем говорим? О серверах приложений или о субд? СУБД в продах стоят в HA кластерах и спокойно выдерживают остановку одного из серверов. От силы 15-20 сек ожидания переезда мастера если сервак упал неожиданно.
Приложения можно нарезать на десятки отдельных модулей и запускать отдельно: в отдельных процессах, в модулях одного процесса — как удобно. Все что написано выше для них верно.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.