Создание микросервисов — страница 26 из 69

разделение кода приложения.

В имеющейся у нас монолитной схеме создание заказа и вставка записи для складской команды производились в рамках одной транзакции (рис. 5.10).

Рис. 5.10. Обновление двух таблиц в рамках одной транзакции


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

Рис. 5.11. Распространение границ транзакций для единой операции


Повторная попытка

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

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

Отмена всей операции

Еще один вариант заключается в отмене всей операции. В этом случае систему нужно вернуть в прежнее согласованное состояние. С таблицей комплектации все просто, поскольку вставка дала сбой, но в таблице заказов мы имеем уже зафиксированную транзакцию. Поэтому нужно сделать откат. Необходимое действие выполняется в рамках компенсационной транзакции, то есть запуска новой транзакции для отката всего, что только что случилось. В нашем случае все может свестись к простой выдаче инструкции удаления DELETE, предназначенной для удаления заказа из базы данных. Затем нужно будет отчитаться в пользовательском интерфейсе о сбое операции. В монолитной системе наше приложение может справиться с обоими аспектами, а вот когда код приложения уже разбит на части, нужно призадуматься о том, что делать. Где именно должна находиться логика управления компенсационной транзакцией, в клиентском сервисе или где-то еще?

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

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

Распределенные транзакции

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

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

Такой подход предполагает, что все участники останавливаются, пока центральный координационный процесс не даст команду на продолжение работы. Это означает, что мы не застрахованы от остановки работы. Если диспетчер транзакций зависнет, отложенные транзакции никогда не завершатся. Если партнер не ответит в процессе голосования, все будет заблокировано. И неизвестно, что произойдет, если фиксация даст сбой после голосования. В этом алгоритме есть безусловное предположение о том, что такого никогда не случится: если партнер сказал «да» при голосовании, значит, мы должны предполагать, что его транзакция будет зафиксирована. Партнерам нужен способ, позволяющий заставить фиксацию происходить в нужный момент. Это означает, что данный алгоритм не защищен от сторонних сбоев, вернее, он предусматривает попытку обнаружения большинства случаев сбоев.

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

Распределенные транзакции были реализованы для конкретных технологических стеков, таких как Transaction API в Java, что позволяет таким разрозненным ресурсам, как база данных и очередь сообщений, участвовать в одной и той же всеобъемлющей транзакции. Разобраться в различных алгоритмах довольно трудно, поэтому я советую отказаться от попытки создания собственных алгоритмов. Если вы считаете, что нужно пойти именно этим путем, лучше досконально исследуйте данную тему и посмотрите, можно ли воспользоваться какой-либо из уже имеющихся реализаций.

Так что же делать?

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

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

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

Создание отчетов

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

Такие фундаментальные изменения в архитектуре, как переход к микросервисам, вызовет множество разрушений, но это не означает, что нужно отказываться от всего, что мы делаем. Аудиторию наших систем отчетности, как и любых других систем, составляют пользователи, и мы должны учитывать их запросы. Фундаментальная перестройка архитектуры была бы преувеличением наших возможностей, поэтому ее нужно просто приспособить под новые нужды. Я, конечно, не берусь утверждать, что пространство создания отчетов не должно подвергаться разрушению, — это неизбежно, и тут важно сначала определить порядок работы с существующими процессами. Иногда нам придется выбирать пути борьбы.

База данных для создания отчетов

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

При стандартной монолитной архитектуре сервиса все данные хранятся в одной большой базе данных. Это означает, что все они находятся в одном месте, поэтому создание отчетов по всей информации выполняется довольно легко и мы можем просто объединить данные в SQL-запросах или чем-то подобном. Обычно создание отчетов не запускается на основной базе данных из опасения того, что нагрузка на них, создаваемая запросами, повлияет на производительность основной системы, поэтому зачастую системы создания отчетов привязывают к копии базы данных, предназначенной для чтения (рис. 5.12).