Как стать автором
Обновить

Целостность данных в микросервисной архитектуре — как её обеспечить без распределенных транзакций и жёсткой связности

Время на прочтение9 мин
Количество просмотров61K
Всего голосов 77: ↑76 и ↓1+75
Комментарии73

Комментарии 73

Для описанного выше идеально, как мне кажется, подходит kafka. Не думали в эту сторону?
Как шина, да, Кафка выглядит наиболее многообещающей.
И NATS Streaming.
Обе шины в процессе нагрузочного тестирования сейчас.

Я NATS Streaming тестировал пару месяцев назад — как по мне, он ещё сыроват. К самому NATS претензий нет, но вот Streaming под нагрузкой и с внезапными рестартами глючил странным образом, что-то терял, что-то присылал не в том порядке.

Проверим :)… Кафку используют все, где тут инженерный челлендж? Про сбои — нарушение порядка на больших объемах это норм. А вот потеря — это будет приговор.

Вот что я им отрепортил за время тестирования. Помню, что было что-то ещё, но у меня тогда был дедлайн, и времени тщательно уточнять граничные условия всех проблем чтобы их отрепортить просто не было. Надо отметить, что поддержка там очень неплохая, вникают и фиксят очень быстро. В принципе, впечатления были положительные, просто в условиях дедлайна не было возможности писать тщательные тесты всех use case которые нам нужны, а без этого его использовать явно рановато.

О, супер, думаю, получится продолжить дискуссию :)

А тот же RabbitMQ рассматривался?

Рассматривались все шины. Кролик не попал в шорт-лист из-за сложностей с поддержкой кросс-датацентрового развертывания. У нас он исторически много где был, но все чаще просят "только не кролик"

Можно уточнить почему «только не кролик»?

Я попрошу ребят сделать статью :)…
Думаю, в основном потому, что для работы с кроликом нужно решать проблемы, а новые модные шины, теоретически, должны работать автоматически из коробки.
Возможно, это иллюзия…
Статью буду ждать!
В NATS ради теста попробуйте следующий сценарий, в один inbox кидать небольшие сообщения, в другой большие (больше мегабайта), когда я пробовал внедрить NATS, результатом такой проверки стали задержки, вплоть до полной остановки, в очереди небольших сообщений, на время отправок больших сообщений. После чего залез в код клиента (.NET), а потом и сервера, и выглядело очень сыро и однопоточно. По итогу, ушел от очередей в сторону discovery сервиса и прямых соединений между сервисами, очень напоминает хореографическую сагу из статьи.
Распределенная транзакция может ссылаться на данные, измененные в предыдущих шагах.
Параллельное выполнение шагов распределенной транзакции может привести к нарушению целосности данных.
Вопросы:
1. каким образом определяются зависимости одного шага транзакции от другого?
2. не кажется-ли вам, что с ростром сложности транзакции отслеживать зависимости будет нелинейно сложнее?

Т.е. подход классный — масштабируемо, современно, но отлавливать data inconsistency у распределенных транзакций с параллельным выполнением, на мой взгляд, невероятно сложно особенно учитывая, что распределенные транзакции работают с базками разных отделов (или компаний), что еще больше усложняет troubleshooting.
Важное уточнение.

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

Речь идет о подходе, альтернативном транзакциям. Соответственно, нельзя ссылаться на данные, измененные ранее. Т.к. в момент выполнения операции эти изменения уже могут быть отменены.

Получается, в такой архитектуре использование Shared database прямо-таки недопустимо: ведь нет никакой гарантии, что данные не были изменены другим вызовом хоть этой же хоть другой саги.

Опасно, да.

Неясно выразился, моя ошибка.
Если речь об альтернативе распределенным транзакциям, то как реализовать в новом подходе требования, предъявляемые к распределенной транзакции. Есть ли опасения насчет сложности дальнейшей поддержки данного решения?
Какие именно требования, предъявляемые к распределенным транзакциям?
При переходе от последовательной (распределенная транзакция) к параллельной обработке посредством саг, каким образом определяются зависимости между сагами для обеспечения целостности данных?
Зависимости — никак. Зачем?
Параллелизм шагов не является сутью описанного подхода. Описанный подход с последовательными шагами не станет распределенной транзакцией, ни в каком виде.
Я не писал, что описанный вами подход — распределенная транзакция.

В названии вы декларируете альтернативу распределенной транзакции для микросервисной архитектуры. Однако, описанный вами подход не может быть альтернативой.
Это ввело в заблуждение и вызвало вопросы.
Комрад gridem, ниже, описал более целостно.

Николай, у меня желание найти решение, а не подловить.
Извините, но ваше решение не надежное.

Самое опасное последствие этой статьи — это представление, что с помощью саг можно дешево сделать распределенные транзакции. Это не так, логику основанную на распределенных транзакциях нельзя переносить на саги as is. Речь про другой подход к проектированию логики, БЕЗ распределенных транзакций.

Поэтому супермаркет можно масштабировать, расширять, просто ставя больше касс.

А другие магазины нельзя масштабировать просто ставя больше касс?
Да, «об опасности бытовых аналогий» :)))… Схему вида «продавщица пробивает на кассе товар, снятый с полки за спиной» нельзя масштабировать только кассами. Нужно масштабировать еще и продавщиц с полками и товаром. А ашан вполне масштабируется, в том числе, просто кассами, даже без продавщиц, это можно у них вживую наблюдать.
Такая статья, а ни слова ни про CQRS, ни про ES, ни про DDD, а некое подобие process manager-ов вообще обозвали «хореографическими сагами».

Можете уточнить:


  1. Что такое CQRS, ES, DDD?
  2. Какое отношение они имеют к статье и почему о них обязательно нужно было написать?
  3. Почему нельзя вводить новые термины для обозначения конкретной реализации обработки данных?

к 1.
Архитеркутрные паттерны: Command-Query-Responsibility-Segregation, Event Sourcing, Domain driver Design
к 2.
О ES уже фактически расказанно в докладе. Эта шина кафка и есть из которой можно "проиграть" событияиз прошлого… ES — в общем случае бесконечное прошлое.
ES отлично идет с CQRS и поэтому тоже упоминется…
Очень грубо: На входе команды (C из CQRS) котрые изменяют Агрегаты и порождают события(Events), вернее цепи событий как в докладе. А Q — это модель чтения состояния. Как можно догодаться если все на эвентах и командах то чтение и запись разделены(RS). И да я упомянул Агрегаты и события и вот мы уже и в DDD — это от туда.

Как так ни слова? :))) Вот вы же их и написали. Я когда статью писал — обоими руками себя держал, чтобы уложиться во вменяемый объем и передать суть с определением минимума терминов.

А все-таки было бо интересно.
Прошло >2 года статья не потеряла актуальность. Спасибо, за четкое изложение.
Но можетъ есть у вас обновление сего доклада, но с более выверенными временем вещами и возможно более четким указанием на упомянутые DDD и ES ?

Разве что вот так: www.youtube.com/watch?v=bAhxpqHfP8I&list=PLdMXteIaGViJFoRUOoPjYaNqZFJY64TYr :)… Про авито и микросервисы, но уже после ухода из Авито.
А сейчас я занимаюсь этим: habr.com/ru/company/manychat/blog/530054

Спасибо, гляну ;)

Современные исследования (например, An Evaluation of Distributed Concurrency Control. VLDB 2017) утверждают, что помочь может так называемый «оптимистический подход».

Двухфазный коммит совместим с оптимистичным подходом. Более того, оптимистичность в контексте VLDB 2017 статьи имеет очень далекое отношение к тому, что здесь описано. Дело в том, что оптимистичность — это про блокировки во время выполнения пользовательских операций. Они берутся только в последней фазе — во время коммита.


В то же время в приведенной статье ни о каких транзакциях речь не идет. Речь идет про атомарное исполнение на каждом этапе с возможностью отката изменений в случае, если последующая операция не имеет возможности выполнится.


К недостаткам такого подхода, как и другие масштабируемые подходы, можно отнести:


  1. Отсутствии целостности данных. Т.е. в любой момент времени мы видим какое-то промежуточное состояние, причем не факт, что то, что мы видим, будет в реальности, из-за возможного отката действий. Т.е. тут налицо практически все нарушения консистентности, включая фантомные данные. Как правило, такое поведение усложняет пользовательский код, т.к. никакой инвариант не может быть гарантирован в любой момент времени.
  2. Подразумевается, что откат действия происходит без сбоев. В простейших случаях все просто, в сложных — как нетрудно догадаться, все сложно. Например: мы применили действие, другая сага поверх этого действия еще что-то сделала, а потом надо откатить первое. Далеко не всегда это представляется возможным в случае конкурентного взаимодействия. Необходимо знать все способы изменения данных, что, конечно же, никто не делает, т.к. проект развивается. Поэтому откат транзакции может завершиться неудачей и никогда не откатить исходную транзакцию. Нужно следить внимательно, что все действия являются откатываемыми при любых конкурентных взаимодействиях ВСЕХ других транзакций.

В целом, подход очень похож на то, что я описал с статье "Гетерогенная конкурентная обработка данных в реальном времени строго один раз": https://habr.com/post/413817/

Статью почитаю, спасибо. Про оптимистичный подход — тут ключевая оптимистичность в полном отказе от коммита. Поэтому синхронизация не ситуативная, а вообще отсутствует.
Про целостность — тоже да, это в чистом виде eventual consistency.
Про риски откатов — есть такое. Мы внутри себя поняли, что эта схема будет работать, только если при разработке мы полностью уйдем от операций вида «обнови объявление и поставь статус X». Для отката таких штук очень важен порядок. Внутри саг безопасно использовать команды вида «произошло то-то и то-то», а сервис уже сам решает, какие поля ему обновлять, и что еще учесть. Никаких безусловных директив.
тут ключевая оптимистичность в полном отказе от коммита

В моем понимании, если нет коммита, то говорить об оптимистичности не имеет смысла. Оптимистичность можно использовать в контексте транзакций.


Поэтому синхронизация не ситуативная, а вообще отсутствует.

Каждое действие является атомарным, а значит, синхронизация присутствует, но локально для этого действия. Т.е. разные саги над одним и тем же элементом будут синхронизированы.


сервис уже сам решает, какие поля ему обновлять, и что еще учесть. Никаких безусловных директив.

Ну тогда это не саги, а набор костылей, чтобы как-то работало. Саги имеют вполне понятную четкую и довольно жесткую семантику.

Да, терминологически термин «оптимистичный» я использовал не совсем правильно. В статье 2017 он касается прежде всего OCC, алгоритма с оптимистичным коммитом. Тут возникла сложность из-за упаковки результатов из нескольких статей в один абзац. В том числе, многие идеи были взяты, например, из The Homeostasis Protocol: Avoiding Transaction Coordination Through Program Analysis, и упомянутых в статье 2017 года детерменистических алгоритмов, которые минимизируют постфактум координацию за счет анализа кода при выполнении. Я их всех скопом обозвал оптимистичными, в плане координации.

Кто касается синхронизации на атомарном действии — в целом это верно, с точки зрения невозможности выполнения одновременных изменений на, например, одном пользователе. Но даже порядок выполнения действий над одним пользователем в ходе работы разных саг, в общем случае, не гарантирован. Если саги стартуют одновременно, нельзя быть точно уверенным, какая первая доберется до пользователя.

Про набор костылей — возможно так и есть. Механически переложить старую монолитную логику, без переписывания сервисов согласно Domain Driven Desighn и Single Point of Responsibility, у нас не получается.

Возможно, у вас получится :)… Статью прочитал, было интересно. СРазу возник вопрос — как система обрабатывает ситуацию, когда одна полу-транзакция отработала, закоммитилась, передала управление на вторую, а потом отразившая первую полутранзакцию база была утерена и восстановлена без следов этой полутранзакции?

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


как система обрабатывает ситуацию, когда одна полу-транзакция отработала, закоммитилась, передала управление на вторую, а потом отразившая первую полутранзакцию база была утерена и восстановлена без следов этой полутранзакции?

Передачи управления не происходит, каждый участник коммитит и работает со своими полутранзакциями. При этом если после выполнения полутранзакции участник сразу падает, то затем после восстановления он проверяет, не была ли выполнена полутранзакция. Таким образом достигается идемпотентность операции: см. push_at_idempotent функцию.

Я бы сказал, что сагу можно назвать супер-оптимистичной транзакцией. :) В ту сторону, где оптимизм уже граничит с идиотизмом.
Это сознательный отход от контроля И 100% детерминизма, т.к. та же квантовая физика учит нас, что реальность не детерминирована. No teleportation theorem и все такое.
Про подход к идемпотентному восстановлению полутранзакций по логу(шине) — да, понял, у нас это предусмотрено примерно также.

Я бы сказал, что сагу можно назвать супер-оптимистичной транзакцией. :)

Настолько оптимистичной, что она перестает быть вообще транзакцией.


та же квантовая физика учит нас, что реальность не детерминирована.

Типичное заблуждение. Квантовое уравнение описывает эволюцию неклассического (квантового) состояния, которое по заданным начальным условиям точно определяет все события в будущем и прошлом.


Речь не про 100% детерминизм. Ведь в двухфазном коммите тоже нет детерминизма. Речь про консистентные переходы системы из одного состояния в другое. И саги тут решают проблему масштабируемости и переносят проблему консистентности на плечи пользовательского кода.


Вообще, это иллюзия, что двухфазный коммит тормозной. Я вот здесь как раз написал про это: "Достижимость нижней границы времени исполнения коммита распределенных отказоустойчивых транзакций" https://habr.com/post/353248/


Просто существует ряд иллюзий по поводу консистентности. Все возникает из-за недостаточной базы в распределенных системах.

Заблуждение… https://en.m.wikipedia.org/wiki/No-teleportation_theorem
Состояние детерминировано, но квантовыми битами, его невозможно 100% точно закодировать классическими битами, булевыми. Всегда есть простор случайности, всегда недетерменизм.


Про двухфазный коммит (статью прочитал): он может и не тормозной. Но каждый сервис системы должен быть с нуля описан так, чтобы понимать двухфазные операции (prepare+commit). Даже если сервис на Go, а база у него на неконсистентной монге.
Что же касается сравнения с сагой по скорости: 2-х фазному комиту, как ясно по названию, всегда нужно 2N операций. А Саге — N для корректной саги, и 2m для сбоившей (m-номер сбоившего шага, от 1 до N).
Сага всегда будет быстрее. Вопрос в рисках нарушения целостности на Саге, которые я бы предложил обсуждать на практических примерах.

Состояние детерминировано, но квантовыми битами, его невозможно 100% точно закодировать классическими битами, булевыми.

Квантовое уравнение описывает квантовое состояние. Оно — детерминированно. То, что происходит при переходе к классике — это уже подробности, которые могут не интересовать. Например, меня это не интересует, само поведение с точки зрения квантовой теории строго детерминированное.


Про двухфазный коммит (статью прочитал): он может и не тормозной. Но каждый сервис системы должен быть с нуля описан так, чтобы понимать двухфазные операции (prepare+commit).

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


Что же касается сравнения с сагой по скорости: 2-х фазному комиту, как ясно по названию, всегда нужно 2N операций. А Саге — N для корректной саги, и 2m для сбоившей (m-номер сбоившего шага, от 1 до N).

Скорость я бы описывал временем на операцию, нежели количеством операций. Количество определяет пропускную способность. В данном случае N и 2N — это небольшая разница. Все дело в том, что на первом шаге в двухфазном коммите ничего такого не происходит, а только блокируются записи. Т.е. такие операции неравноценны, а потому 2N может оказаться меньше N.


В любой случае, либо платишь тем, что прикладной код содержит много кода по предотвращению нарушения консистентности, который подвержен ошибкам, либо ты платишь ресурсами железа за консистентность. Что стоит дешевле — на этот вопрос каждый отвечает сам.

Про кванты — постараюсь через несколько месяца родить статью про квантовые вычисления. Не смотря на квантовую природу вычислений, ввод и вывод данных осуществляется через классические биты, недетерменированно и местами вообще статистически: запуск в идентичных начальных условиях в общих случаях не гарантирует идентичные результаты.

Информационную систему над реальным миром (над множеством квантовых объектов) я бы советовал рассаматривать аналогично — неполный детерменизм. Бизнес приложение, которое работает ОК в 99.9% случаев, можно считать супер успешным. А 0.1% тех, кому не повезло, можно десятикратный бонус выплатить.

Для этого не надо писать сервис...
Стоп-стоп, обращаю внимание на название статьи. Изолированные бизнес-сервисы первичны, часто вообще не делящие данные и сущности между друг другом.
Все дело в том, что на первом шаге в двухфазном коммите ничего такого не происходит, а только блокируются записи. Т.е. такие операции неравноценны, а потому 2N может оказаться меньше N.
Вот тут я категорически не согласен. 2N = 4 похода по сети. Потенциальные точки сбоев и задержек коммуникации. Это первое.
На первом шаге в реальном, не теоретическом сценарии, проводится проверка бизнес-условий, а только потом блокировка. Первый шаг может легко быть дольше второго. И каждый шаг должен проходить в отдельной локальной транзакции (две транзакции), каждую из которых нужно открыть и закрыть.
... речь про теоретическую возможность...
Давайте лучше посмотрим практический пример. Отдельный сервис (доставки?), у его база на MongoDB. Для поддержки двухфазного коммита нужно уметь сущность (адрес доставки?) в два шага блокировать и разблокировать. В монге этот фокус надежно можно сделать либо через Update записи, либо через отдельную таблицу блокировок, которую нужно джойнить с таблицей сущностей.
Обе эти операции, по умолчанию, очень тяжелы для монги. Любой бизнес-сценарий аналогичной бизнес-сложности, построенный на атомарных записях+чтении единственной записи, будет в разы быстрее.

Решая схожие проблемы (важное уточнение — первый раз) я остановился на таком решении: «Сага» — слишкмо сложно и нужно всячески избегать их появления. Для этого «микросеврисы» делаются чуть крупнее что бы не допустить появления распределенных транзакций.

Мы 10 лет держались без распределенности, ростили монолит :)… Это не такое плохое решение, на самом

Кстати, а как вы рисуете то, что получается?


Если результат моих двухлетних усилий отобразить на графвиз то получается https://i.imgur.com/DPRTk63.png


Здесь прямоугольники — топики кафки, а овалы — сервисы, не всегда микро.
Если же рисовать без топиков — всё превращется в десяток ощетинившихся связями ёжиков.

Круто, спасибо :)… У нас топиков побольше будет, но это нюансы реализации (топик-на-тип события, типов сотни). У нас многие не рисуют, и так понятно. Те, кто рисует свой сложный кусочек, делает это примерно как и вы. Общая картинка в процессе, на ближайшем хайлоаде расскажу, как мы ее храним и визуализируем...

Под такое рисование (graphviz) возникла ещё одна идея: Т.к. один и тот же микросервис участвует в разных бизнес-процессах и хочется уметь на общей схеме (графе) всех микросервисов выделять путь конкретного бизнеспроцесса, то ребрыанужно раскрашивать. Математика для этого дела есть — en.wikipedia.org/wiki/Multigraph#Labeling только вот инструмента нет :(

Кстати, как на таком количесве топиков вы фиксируете формат данных в них и обеспечиваете версионирование? Мы гвоздями прпбиваем к названию топика AVRO-схему

Про графы ответ — графовая база :)… Будет доклад на ближайшем хайлоаде.
Про формат: https://m.habr.com/company/avito/blog/419651/ :)

Не буду тут «умничать», задам практические вопросы.

Т.е. процесс регистрации пользователя в сервисе саг может изначально состоять из трёх шагов, а потом, в ходе развития системы, туда впишутся еще семь шагов, а один шаг выпишется, и их станет девять.


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

Подход очень хаотичный и сложный. На сколько часто появляются какие-то баги, и как сложно их отлавливать? Как вообще этот процесс выглядит? Ведь все процессы, на сколько я понял, асинхронные.

В первом приближении кажется, что в подобной «разветвленной» саге с необязательными шагами тяжело определить завершение саги (процесса), но, на самом деле, все просто: сага (процесс) завершена, когда завершены все обязательные шаги, в любом порядке.


Что происходит, если деньги со счета клиента уже списаны, а сообщение об этом ему не доставлено? Получается, что процесс не завершен и он будет торчать в подвешенном состоянии, пока не будет доставлено сообщение? Если это так, что как быстро клиент начнет нервничать, что деньги списали, а услугу не оказывают?

Про поддержку и развитие такой архитектуры: нелегко, как и с альтернативами :). На чем делается упор: единый реестр саг, где можно посмотреть текущие цепочки вызовов. При этом сами саги легкие, без сложной логики, без риска разрастания god object-ов. Сами сервисы, за счёт необходимости следовать общим правилам, тоже без избыточных зависимостей. Про команду, которая это делала: если команда одна, все это, возможно, вообще избыточно. Саги — это когда команд несколько, они независимо релизят, у них нет (обязательного ) общего планирования.
Про пример со снятием со счета и недоставленным сообщением — а что тут такого? Сбой на стороне смс- провайдера — не причина отменять отправку денег. Человек может сам глянуть свой баланс, можно отправить ему поясняющее письмо или push нотификацию. Смс может уйти через час. Это классический необязательный шаг, который никак не должен мешать завершению саги. Хорошая разница с транзакциями.

Спасибо за пояснение. А архитектура на сколько долго уже в бою крутится? Уже есть полное понимание очевидных преимуществ и слабых сторон?

Отдельные саги в бою уже полгода. Их станет больше. И есть инсайт от других компаний, которые начали раньше и испытали больше.
Полного понимания нет :)… Понятно, что распределенные транзакции у нас не взлетят, как и микросервисы без саг. Но всех нюансов и опасностей мы ещё точно не испытали :)

Ну, желаю вам удачи. Кто не боится и делает, всегда своего добьется.
крайне неудачное название «микросервисная архитектура». К архитектуре она не имеет отношения.

Вопрос про управление состоянием. Как вы относитесь к идее состояния в саге?


Т.е. несколько серверов хранят состояние саги, доступное сервисам по ключу идемпонентности. Элементы саги могут читать все состояние как документ, но писать могут только "свой" кусок. При этом состояние помнит все свои изменения, что можно использовать для компенсации.


Привлекательность тут в следующем:


  • Во-первых операционно можно понять состояние транзакции просто поглядев в документ состояния в одном месте, что очень удобно.
  • Сервисы для хранения состояния могут масштабироваться линейно и должны обеспечивать ACID состояния саги только на время ее жизни, которое относительно короткое (максимум в большинстве случаев — дни). Ключ идемпонентности это просто полный URL для документа состояния. На запуске саги мы выбираем один из инстансов, даже балансеров не нужно.
  • Писать транзитивные данные в состояние предпочтительнее сохранению их в локальной базе сервиса и передаче их сообщениями, т.к. не надо заботиться о их компенсации и совместимости сервисов на уровне протокола. Грубо говоря если шаг 3 зависит от данных шага 1, а шаг 4 зависит от данных шага 2, цепочка 1-2-3-4 подразумевает либо публичное API чтобы 1-3 и 2-4 могли поговорить между собой, либо шаги 2 и 3 должны знать о данных которые нужны следующему по цепочке.
  • Как уже отмечалось — компенсации делать проще, если иметь лог изменений. И хранить его в документе состояния — самое оно.

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

Общую единую базу изменений иметь удобно :)… А потом в ней же ещё делать транзакции ;)… Кончится может монолитом.
В нашей реализации, в PG Saga ( оркестра цинния) единая база +лог состояний на PostgreSQL. В этой статье единый лог состояний на шине (Кафка?). Т.е. элементы вашего подхода есть.
В чем риск- как бы не перегрузить единую базу состояний. Какой-то шаг положил туда много и часто, второй — начал читать это без индекса. И все, единая точка отказа складывает всю систему.

Под катом я рассказываю, как обеспечить целостность данных в микросервисной архитектуре без распределенных транзакций и жесткой связности. (

Да никак. Забудьте. И не вводите людей в заблуждение.


Изоляция в среде нескольких независимых асинхронных сервисов — это техническое требование. Современные исследования показали, что реальные бизнес-процессы можно реализовывать без изоляции.

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


Помимо технической стороны вопроса, такой как отказоустойчивость или гарантированная доставка, так или иначе все ваши бизнес-процессы должны удовлетворять условию линеаризуемости. Обеспечить линеаризуемость вне ACID возможно, но на порядки сложнее. И есть всего три способа:


  1. Либо мы вручную реализуем распределенный ACID в том или ином виде: это потребует введение аналога двухфазного коммита для ресурсов и дополнительных состояний ("заблокирован" или "в обработке"), а также для каждого действия процессов-"компенсаций", которые делают откат в изначальное состояние. И соответственно еще тонны геморроя.
  2. Либо все ваши бизнес-процессы можно представить ввиде линейной (или в общем случае древовидной) "потоковой" архитектуры взаимодействия, в которой каждая система может принимать сообщения только от единственной другой. Но далеко не все процессы можно уложить в данную архитектуру.
  3. Либо мы анализируем каждый кейс отдельно, все возможные конфликты и способы компенсации, и доказываем, что в каждом случае линеаризуемость не нарушается. С увеличением бизнес процессов сложность процедуры возрастает экспоненциально.

Однако, большинство использует четвертый вариант:


  1. Микросервисы — модно, стильно, молодежно. Ставим Монгу, Кафку, соединяем, пробуем — заработало. Ставим в продакшн. Линеаризуемость? Не, не слышали. Все и так норм.

Никакой линеаризуемости тут не предполагается, совершенно верно. Я пытался на это указать, когда выкидывал I из ACID.
Предлагаю вам (и другим комментаторам выше предложу) простую игру: опишите бизнес сценарий в микросервисной архитектуре, где отсутствие линеаризуемости приводит к ошибкам. А я постараюсь его положить на саги. Не факт что получится, но вдруг :)… Гипотеза: линеаризуемость не нужна самому бизнес процессу, она нужна разработчику, т.к. нашему мозгу проще осознавать линеаризуемые процессы.

Навскидку. Микросервисы A, B, C, D, E. Две изменяющие транзакции: A->B->C, D->B->E. Последовательность выполнения изменений во времени: A, B, D, B, E, и первая транзакция на C отвалилась с валидацией, вторая транзакция закоммичена. В системе B обе изменили один и тот же регистр. Нет возможности откатить первую транзакцию, так как регистр уже был изменен второй (и мы не знаем как). Чтобы было совсем конкретно, в B лежит счет одного клиента. Две транзакции от разных источников A и D (оплата по карте, и ипотечный сбор). Любое решение, которое вы предложите будет сводиться к одному из трех вышеперечисленных мной сценариев:


  1. Заблокировать счет на время проведения любой транзакции, чтобы любое другое изменение на нем сразу отваливалось (типа ручной двухфазный коммит).
  2. Проводить все транзакции в B последовательно через одну и ту же очередь.
  3. Увеличение и уменьшение счета — коммутируемые операции и их очередность применительно к счету не имеет значения.
    Все решения плохие в том или ином случае и уступают ACID. Поэтому если есть сделать систему монолитной, лучше делать так и не утруждать себя лишним геморроем.

P.S. Я знаю как работают некоторые банковские системы: у них есть дорогая и монолитная ACID-система, которая обслуживает счета и базу данных. И куча всяких микросервисных и любых других спутников вокруг, целостность которых уже никого сильно не волнует.

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

Не то что бы «целостность не волнует», просто там процессы не требующие полноценного ACID.
Скажем в интернет эквайренеге мы же будем откатывать транзакацию, если партнер при принял наше HTTP уведомление о успешном платеже. Так что можно смело сделать свервис уведомлений со тдельной БД и никак не подвязывать его к ACID в основной системе.

Совершенно верно, необходимость целостности чаще все рождается там, где затрагиваются деньги. В нашем случае — когда в процессе хоть где-то есть биллинг (аренда, доставка, покупка услуг, покупка отчётов).
Паттерн с финансовым ACID монолитом в центре (кейс банков и, внезапно, убера) — это понятно, привычно… но немного старомодно и немасштабируемо. Старая проверенная временем классика, но не критерий оценки новых решений.
Про решения задачи: я, в контексте саг, строго за вариант 3. Чтобы в сервисы шли не не директивные апдейты "сумма на счету стала X (т.к. было Y а я вычел Z и налоги, но это мое дело)", а бизнесовые запросы "поступило X денег", "отправь Y денег"… В случае которых с линеаризуемостью проще

В случае запуска многих копий одного сервиса при database-per-service, у них тоже должна быть своя копия БД?

Тонкая тема :)… У копий сервиса должна быть общая база… Но масштабируемая, в идеале — шардируемая… Хотя довольно часто общего постгреса им всем хватает с 10 кратным

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

Вот тут не соглашусь. Своя база у каждой копии блокирует масштабирование вверх или вниз. Было 3 копии сервисов, в пике понадобилось 10, а потом 2… Как дробить/сливать отдельные базы, да ещё и быстро, по ?

Отрезает последнее слово: по нажатию одной кнопки.

Николай, мы починили.

Зависит от сервиса.


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


А вот если узкое место это база, и есть возможность её шардировать, то может быть разумнее сразу сделать 10 копий сервиса (с запасом), просто пока нет нагрузки несколько могут работать на одном сервере. Если запаса не хватит, и нужно будет шардировать дальше — будет небольшой даунтайм чтобы из 10-ти сделать 20-ть (если сервис внутренний, то юзеры этот даунтайм могут вообще не заметить).
Своя база даёт кучку дополнительных возможностей в связи с отсутствием необходимости в синхронизации: упрощается миграция схемы базы при обновлении/откате сервиса, можно делать меньше блокировок, проще писать код, и, главное, можно активно кешировать в памяти сервиса (вместо использования redis).

Вы описываете кейс предварительного избыточного шардирования. Мы его раньше часто применяли, например, в мессенджере. Это, по сути ручное шардирование единой базы, после которого решардировать уже нельзя, можно давать больше физических машин. 64 логических шардов базы, на 4 машинах (и 4 слева), а копий сервиса, внезапно… 7!… В кубе (Kubernetes). А вечером 9. А утром 3. При этом шаг с 64 логических шардов до 128 (у вас с 10 до 20) может быть адским.
Что касается копий сервиса, сейчас прогресс идёт в сторону гибкого решардировая. Предел этого процесса, уже достигнутый в облаке АлиЙунь — это функциональный подход, одноразовый микропод (копия сервиса) в облаке под каждый (!) вызов сервиса. Выделяется под вызов и потом гасится. Конечно, с базами так нельзя, база должна жить и шардироваться

Можно подробнее (или ссылку) про АлиЮн?
Ооо, я уже несколько лет мучаюсь теорией распределенных транзакций в микросервисной архитектуре. Ковырял всякие 2PC и 3PC, вплоть до университетских трудов (например). Пытался что-то додумать сам, идея идемпотентных токенов и сервиса, который их выдает, тоже возникла, но до чего-либо вменяемого даже на уровне теории допилить не удалось. Поэтому, заголовок выглядел весьма многобещающим.

Тем не менее, прочитав саму статью, а также материалы по ссылкам, вопросы к самому базовому сценарию, с практической точки зрения, у меня остаются. Давайте рассмотрим следующий кейс. Для упрощения, будем считать, что проект мы пишем полностью с нуля (распиливать монолит и поддерживать легаси не нужно), среда гомогенна (зоопарка технологий тоже нету, цель применения микросервисной архитектуры — масштабирование, отказоустойчивость, изоляция кодовой базы). Очевидно также, что используется database-per-service паттерн, и затрагивается в первую очередь вопрос распределенной записи — с чтениями все сильно проще (грязные чтения и прочие радости жизни, возникающие от отсутствия изоляционности, пока оставим за кадром).

Итак, требуется реализовать систему регистрации и авторизации пользователей, с SSO. Допустим, у нас есть Web/Mobile UI (это важно). Имеем какой-либо entry point, пускай /api/users/register. Этот сервис выступает в роли координатора, своего хранилища данных не имеет. Есть сервис SSO, реализованный, к примеру, через IdentityServer — то есть, держит общую информацию, требуемую для логина пользователя, также, отвечает за выдачу логин-токенов, и есть возможность дописывать кастомную логику. Второй сервис отвечает за регистрационную информацию, требуемую конкретному проекту, вне SSO. Чуть-чуть усложним — добавим третий сервис, который по завершении процесса отсылает емейл свежезарегистрированному пользователю. Иными словами, фронт-энд запрашивает операцию записи у координатора, а тому требуется внести записи в две БД, через соотвествующие сервисы, отправить письмо пользователю, и вернуть результат клиентской стороне. Требуется обеспечить консистентность данных (eventual, конечно же), и хороший UX.

Считаем, что сами сервисы обеспечивают транзакционность (NoSQL пока трогать не будем), но любой из сервисов в любом количестве может упасть или перестать отвечать.

Собственно, вопросы:
— что минимально потребуется добавить для реализации данного сценария?
— а не минимально? Какие дополнительные возможности можно получить?
— как именно это должно работать, в плане синхронности/асинхронности, протоколов/софта? Другими словами — REST будет достаточно, или потребуется AMQP? RabbitMQ — хватит, или Kafka — без вариантов? Сервис саги как таковой разве не должен реализовывать персистентную очередь?
— можно ли избежать необходимости в компенсационности? Пример — отсылка мейла — это действие плохо откатывается. Можно ли строить архитектуру по принципу того, что рано или поздно сервисы поднимутся, и обработают накопившиеся в очереди сообщения?
— туда же — есть ли жизнь без event sourcing? Если есть — какие альтернативы DELETE запросу / маркированию сущности как удаленной?
— как обеспечить хороший пользовательский опыт, учитывая, что, во-первых, клиенту требуется вернуть синхронный ответ от координатора, но операции у нас, скорее всего, асинхронные, а во-вторых, часть операций может выполниться, а часть — нет? Мне приходят в голову только какие-то жуткие идеи с веб-сокетами, и сообщениями в стиле «Процесс регистрации завершен частично. Мы отправим Вам сообщение на электронную почту, как только все будет готово». Наверное, этот вопрос как-то решен, нет?

Спасибо!
  1. Минимально я бы добавил инструмент отслеживания незавершённых цепочек, чтобы их зачищать. У вас уже сейчас есть элемент из немного другого мира: сервис-коопдинатор. В принципе он может взять на себя такую уборку. Или по крону проверку запускать.
  2. Неминимально — шину для хореографической или базу состояний для оркестрационной саги. Единое отказоустойчивое помнящее пространство между сервисами.
  3. В парадигме саг — строго асинхронно. Никого ждать нельзя, fire&forget. Для быстрого вызова (1-2ms) можно и REST, но дублирующую запись лучше все равно в шину бросить. Чтобы упавший сервис, даже без надёжной базы, смог воскреснуть и перепроиграть упущенные из-за сбоя операции. И аналитику вы на шине получите забесплптно.
    4.про шину, мы выбираем между кафкой и nats streaming. У нас есть большой чек-лист, про него возможно потом расскажем, рэбит туда не проходит. Но это наш чек-лист, там огромные объемы, масштабирование, поддержка кросс-дц… Может вам кролик и норм. При этом, да сервис саг может реализовывать персистентную очередь и внутри. У нас в PG Saga так и есть. Но эта самодельная очередь не выдержит весь наш траффик, только финансовый. Поэтому мы и смотрим на большую шину.
    5.конечно, ряд шагов саги не нуждаются в компенсациях, как в примере с емейлом… Ушло и ушло, что поделать. Тем меньше нагрузка.
    6.event sourcing помогает. Про удаление — я вообще против удаления, только за маркирование записей… Конечно, иногда приходится, но чем меньше удалять, тем безопаснее.
    7.про ux вообще сложно, я обычно на слой -два глубже работаю. Описанная вами схема, с моей точки зрения, на саге должна выглядеть так: ux форма дёргает координатора (сервис бизнес логики ), он стартует сагу, будучи шагом 0, и кидает в шину запись с введенными (и провалидированными) пользовательскими полями. На эту запись срабатывает шаг 1, SSO, кидает сообщение об успехе, на который срабатывает шаг 2. На успех шага 2 реагирует
    снова координатор (шаг 3), который кидает в шину заявку на е-мейл, рисует ОК пользователю в UI и завершает сагу. Компенсации делают шаги 1 и 2, маркируя созданные учётки как убитые.
    Конечно, для описанного сценария сага это немножко overkill, того же можно добиться кроном с проверками. Чтобы при сбое шага 1 или 2 созданные куски пользователя блокировались, а пользователь предупреждался емейлом. Саги тем более полезны, чем длиннее, сложнее сценарии, и чем больше их одновременно работает. Когда независимые команды вписывают в регистрацию начисление бонусных баллов, рассчет рейтингов, оповещение ещё и по смс… И все даунтайма основного процесса.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий