Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, gandjustas, Вы писали:
G>>ИМХО ACID-транзакция, изобретенная на другом уровне абстракции — все равно транзакция. Если говорить то том, какие транзакции стоит использовать — самопальные или предоставляемые БД, то любой вменяемый разработчик выберет второй вариант.
S>Нет, сделать ACID-транзакцию поверх ненадёжного соединения не получится.
У нас все соединения ненадежные. Мы на практике не рассматриваем случай когда фейл происходит между коммитом и ответом о коммите от базы.
S>МСА предлагает использование Representational State Transfer. Работает он так:
S>1. Заказ в базе Б находится в статусе "черновик".
S>2. По команде "зарезервировать заказ" заказ атомарно переходит в статус "резервируется". Из этого статуса его вручную вывести нельзя, и изменить состав заказа в этом статусе тоже нельзя.
S>3. Сервисом заказов делается попытка выполнить идемпотентную операцию "создать резерв" в сервисе А.
S>3.1. Если сервис А отвечает отказом, то сервис Б атомарно возвращает заказ в статус "черновик" и пишет в историю заказа "резервирование не удалось".
S>3.2. Если сервис А отвечает подтверждением, то сервис Б атомарно перемещает заказ в статус "зарезервировано", и для него становятся доступны следующие операции стейт-машины.
S>3.3. Если сервису Б не удается обработать ответ от сервиса А по любой из причин, перечисленных в моём предыдущем посте, то заказ остаётся в статусе "резервируется".
А когда получает ответ пользователь?
Ему же надо дать ответ в моменте и показать окно оплаты.
И что будет если приложение упадет в пункте 3.2 между ответом А и изменением статуса заказа в Б?
Я в принципе знаю правильный ответ: он называется WAL, а на более высоком уровне абстракции — transactional outbox. Данные для выполнения транзакции сначала атомарно пишутся в (одно, иначе атомарности не будет) долговременное хранилище, а потом пытаются примениться к конкретным таблицам.
Это и называется "рукопашные транзакции". Мы рассматриваем сценарий, когда без них можно обойтись.
G>>Если код падает по между шагами 2 и 3, то в базе остается несогласованное состояние.
S>Несогласованное состояние в REST возникает только в тот момент, когда мы не получили ответ на запрос "распространить изменение". Причём мы знаем о том, что оно несогласованное — и можем принять меры, чтобы эта несогласованность не вышла нам боком.
Несогласованное состояние в примере выше возникнет если заказ на складе будет забронирован в сервисе А, а на сервисе Б не изменен статус.
G>>Значит вместе самим кодом резервирования заказа надо написать еще фоновую задачу, которая откатывает незавершенные резервы.
S>Нет, никакой "фоновой задачи отката" писать не нужно.
А что насчет фейла, что я описал выше?
G>>Но у нас транзакции не изолированные, то есть межу 1 и 2 может вклиниться изменение, которое обновит заказ.
G>>Это значит что для корректного кода отказа как в фоне, так и в п3 надо сохранять "слепок" заказа на шаге 1.
G>>Этот слепок — это в чистом виде wal log.
S>Нет, так делать не надо.
А как надо?
В твоем примере выше проблема та же самая. Процесс на шаге 3 может прерваться между обращениями к базам А и Б.
Это будет ошибка — неконсистентное состояние, которое нужно будет или откатить до исходного или каким-то образом докатить до финального. Причем делать это уже фоновым процессом, потому что прерывание, вероятнее всего, произойдет по причине того, что пользователь ушел\связь прервалась. Фоновому процессу надо где-то брать данные о той операции, что надо откатить\докатить, это и будет wal или аналог.
S>Основное заблуждение здесь — возможность надёжно определить ситуацию "возникла ошибка". Нет, у вас запросто может так получиться, что "пассивный" сервис в следующее состояние переехал, а "активный" об этом не узнал. Изменения должны распространяться ровно в одном направлении.
Необходимости на уровне кода определять "были ли ошибка" конечно нет. Но код который откатывает\докатывает транзакцию все равно нужен.
S>А если мы говорим об МСА, где все участники сценария — наши же собственные сервисы, то мы их сразу так и проектируем, чтобы иметь возможность безболезненной отмены, если какая-то из частей сценария напоролась на неразрешимую ситуацию.
Так давайте на одном простом примере разберемся как оно должно быть? Я вот до сих пор не понимаю как сделать надежное резервирование заказов в рамках МСА с разделением сервисов на "заказ" и "склад", чтобы хотя бы по надежности не уступало одной базе. По трудозатратам однозначно в разы проиграет и по быстродействию\масштабируемости тоже.
G>>Получение остатков это запрос к одной таблице. Ему никакая МСА не помешает.
S>Именно. А в монолите у вас тот же самый ресурс (CPU и шина) обрабатывают не только запрос к этой таблице, а ещё 2400 таблиц вашей развесистой ентерпрайз-БД.
S>И пока там кто-то начисляет бонусы сотрудникам, остатки на складах начинают лагать 
Ну нет конечно. readable реплики никто не отменял.
Почему-то стандартная софистика апологетов МСА, будто монолитная база это всегда одни сервер без масштабирования.
В реальности у каждого энтерпрайз-БД сервера есть реплики, минимум две, иногда больше. Реплики могут быть разной степени синхронности, когда нам можно отдавать слегка устаревшие данные, например для расчета премий.
Поэтому нагрузка на чтение в монолитной базе масштабируется не хуже чем в МСА. На практике даже в разы лучше, так как джоины и индексы есть.
А нагрузка на запись масштабируется дисками и партицированием.
G>>Шардить можно и без МСА. Это вообще ортогональные вещи.
S>Не совсем ортогональные. Шарды — это и есть "микросервисы для бедных".
Нет конечно. шарды это когда мы одну монолитную базу разделяем на несколько инстансов по ключу шардирования. По сути тоже самое партицирование, только с разнесением партиций между серверами.
Причём если при патицировании можно делить только нагруженные таблицы, то при шардировании придется делить всё. Да еще и придумывать механизм реплицирования изменений в общих таблицах.
К шаридованию стоит прибегать тогда, когда нагрузка на CPU при записи (перестроении индексов) уже превышает ресурсы мастера. Но до такой нагрузки 99,999% проектов не доживет никогда.
G>>При достаточном количестве микросервисов система упирается в "потолок" одного сервера очень быстро.
S>Вот это утверждение я не понял. Вы собираетесь все микросервисы гонять на одном узле?
А на скольких узлах надо гонять пока пользователей нет?
Чаще всего так и происходит, что система изначально построенная по МСА гоняется на одном сервере БД в разных базах, а по мере роста нагрузки поднимаются новые серваки. При этом потребность в масштабировании при МСА возникает гораздо раньше, чем в монолитной базе.
S>>>Типа вот мы поднялись после сбоя и видим, что корзинка №42342342 была отправлена на резервирование, а результата резервирования нет.
G>>А как мы узнаем что его нет?
S>Смотрим в свою базу и видим: статус — "резервирование начато".
G>>А если он не дождался и ушел?
S>Произойдёт ровно то же самое, что будет в случае, если у нас "пропал" монолит — ну там, временный сбой, перезагрузка сервиса. Или пропала связь между приложением пользователя и монолитом.
Вот тут неверно.
Если пропала связь в момент между "обновлены остатки на складе" и "обновлен статус заказа", то в МСА получаем неконсистентное состояние, которое еще и непонятно как откатывать\докатывать. В случае монобазы мы получаем отмену транзакции и возврат к исходному состоянию будто ничего не было. Причем бесплатно с точки зрения трудозатрат.
S>Если разработчики сервиса обладают мало-мальской квалификацией, то ничего плохого не произойдёт. Нельзя же, в самом деле, строить свой код на предположении о том, что с каждым заказом в один момент времени работает ровно одна транзакция.
Фишка в том, что I в ACID говорит нам, что мы можем написать код, который предполагает что работает одна транзакция, она или выполнится до конца как будто она была одна в этот момент времени или откатится если такое не получилось.
Но когда мы границы транзакции в одной базе пересекаем никаких гарантий больше нет и все надо делать руками.
G>>Я понимаю, что при высокоуровневом взгляде кажется что это все просто, написать три-четыре строки в воркфлоу и будет хорошо. А на практике для обеспечения целостности нужны десятки строк кода и дополнительные данные хранить (что увеличивает нагрузку какбы).
S>Ну да, около двухсот строк кода. Один раз на всю систему. Не так уж и плохо 
Это на один сценарий, где требуется изменение более чем в одной базе. На каждый сценарий будет свой код отката\доката, который вряд ли получится обобщить.
G>>В нашем случае вообще никаких кластеров нет. Мы просто в рамках одного процесса обращаемся к двум сервисам на двух разных серверах. Операция может завершиться успешно если обе операции завершатся успешно.
G>>Каждая из этих баз может представлять из себя мкльтимастер-кластер, а может одни сервер, а может шардированную базу, это не имеет значения.
S>Тут картина на самом деле сильно сложнее. Детальный расчёт perceived availability потребует знания соотношения частот сценариев с различным количеством узлов.
У нас пока один сценарий, давайте с ним разберемся.
Опять типичная софистика — вместо рассмотрения одного кейса сразу начинаем "масштабировать", как-будто недостаток в одном месте чем-то перекроется в других местах. Не перекроется, недостатки будут размножаться во всех остальных сценариях тоже.
S>Фронт, с т.з. REST — это точно такой же узел сети. По-хорошему, он проектируется по тем же принципам; но благодаря наличию пользователя можно упростить реализацию. Хочешь повтор — жмёшь F5.
С точки зрения РЕСТ — да, а с точки зрения архитектуры — нет. На фронте нет никакой транзакционности и отката. Выполнение сценария на фронте может прекратиться в любой момент и не продолжиться вообще никогда, и это не является ошибкой.
Поэтому мы должны на стороне сервера делать откат\докат фоновым процессом.
G>>Я предлагаю на одном сконцентрироваться. Это же реальный сценарий.
G>>Когда с ним закончим сможем посмотреть как эти подходы масштабировать.
S>>>Остановка на техобслуживание одного из сервисов задержит только те сценарии, которые проходят через него.
G>>Мы же рассматриваем сценарий когда у нас сценарий зависит от доступности двух серверов. На одно из них, а сразу двух. Их доступность равна произведению доступности обоих. А она будет меньше, чем доступность одного.
G>>Приложения можно нарезать на десятки отдельных модулей и запускать отдельно: в отдельных процессах, в модулях одного процесса — как удобно. Все что написано выше для них верно.
S>Чтобы приложения можно было нарезать, между ними должны быть какие-то границы.
Границы могут быть разными, в том числе чисто логическими.
Допустим есть прям монолитное приложение, которое весь код запускает в одном процессе. Никакого состояния между запросами в памяти процесса не хранится, все сохраняется в монобазу.
Можем ли мы горизонтально масштабировать приложение, запуская несколько экземпляров на разных серверах? Если нет, то почему?
S>Как вы передадите одну транзакцию между несколькими модулями в разных процессах?
Зачем нам разные процессы?
S>В МСА вы можете накатить апдейт на "модуль ценообразования", не останавливая модули корзинки и склада. В монолите вам придётся делать общий даунтайм.
В монолите я могу сделать rolling update в кластере кубера, обновляя приложение вообще без остановки обслуживания.
С фича-флагами я могу вообще разные версии "модуля ценообразования" запускать даже для разных пользователей.
С хорошей модульностью я могу в части экземпляров запускать часть модулей, а в других этого не делать.
Имхо не надо подстраивать архитектуру под инфраструктурные задачи.
S>И количество серверов СУБД в HA-кластере вас не спасёт от того, что апдейт схемы плохо совместим с data-level блокировками.
А как тут разделение поможет? Если вам нужен access exclusive на таблицу для обновления, то какая разница сколько всего у вас баз?
S>То есть "на ходу" его провести возможно далеко не всегда; безопасный способ — переключать базу в монопольный режим.
не так уж много операций требуют монопольного режима для всей базы, а для отдельных таблиц разделение не поможет.