Эта статья является заключительным конспектом книги «От монолита к микросервисам». Материал статьи посвящен декомпозиции БД во время процесса разложения монолита на микросервисы.
Извлечение микрослужбы не «делается» до тех пор, пока код приложения не будет работать в его собственной службе, а данные, которыми он управляет, не будут извлечены в его собственную логически изолированную БД. Однако в какой последовательности извлечения должны выполняться?
Есть несколько вариантов:
Сначала выделить БД, а затем код.
Сначала выделить код, а затем БД.
Выделить сразу все.
У каждого варианта есть свои плюсы и минусы. Далее будут рассмотрены эти варианты.
Сначала выделить БД
С помощью отдельной схемы потенциально увеличивается число вызовов БД для выполнения одного-единственного действия. Если раньше были все необходимые данные в одной инструкции select, то теперь потребуется вытаскивать данные из двух мест и соединять их в памяти. Кроме того, мы в итоге нарушаем транзакционную целостность, когда переходим к двум схемам, что окажет значительное влияние на приложения. Выделяя схемы, но держа код приложения вместе, мы даем себе возможность откатывать наши изменения назад или продолжать корректировать вещи, не влияя на каких-либо потребителей нашей службы, если мы понимаем, что идем по неправильному пути. Как только мы убедимся, что выделение БД имеет смысл, то можем подумать о разделении кода приложения на две службы.
Обратная сторона состоит в том, что такой подход вряд ли принесет большую краткосрочную выгоду. По-прежнему имеем монолитное развертывание кода. И также нужно учесть, что если сам монолит является черно-ящичной системой в рамках коммерческого софта, то описанный вариант недоступен.
Одна БД на один ограниченный контекст
Центральное место в идее независимой развертываемости микрослужб занимает тот факт, что они должны владеть собственными данными. Прежде чем приступить к выделению кода приложения, мы можем начать эту декомпозицию, четко отделяя наши базы данных вокруг линий выявленных ограниченных контекстов. Идея заключалась в том, что при необходимости в дальнейшем разложении на микрослужбы это было бы намного проще.
![Рис. 1 - Каждый ограниченный контекст в службе валового дохода имел свою собственную отдельную схему базы данных, допускающую разложение в дальнейшем. Рис. 1 - Каждый ограниченный контекст в службе валового дохода имел свою собственную отдельную схему базы данных, допускающую разложение в дальнейшем.](https://habrastorage.org/getpro/habr/upload_files/62c/e53/da3/62ce53da3d7d3cac7f2a872f4d8da7c9.png)
На первый взгляд, лишняя работа по сопровождению отдельных БД не имеет большого смысла, если держать код в виде монолита. Этот шаблон рекомендуется людям, строящим совершенно новые системы. Реализация полноценных микрослужб для новых продуктов или стартапов на первом этапе может быть неоправданной. Понимание домена вряд ли будет достаточно зрелым для того, чтобы знать, как выявлять стабильные границы домена. В случае стартапов - в особенности, поскольку требования могут резко измениться. Вместе с тем, этот шаблон будет хорошей промежуточной точкой. Держите выделение схемы там, где, по вашему мнению, у вас в будущем будет выделение службы.
Сначала выделить код
В общем случае большинство предпочитают сначала выделять код, а затем базу данных. Они получают краткосрочное улучшение от новой службы, что дает уверенность в завершении декомпозиции путем выделения базы данных.
В результате выделения прикладного яруса намного проще понять, какие данные необходимы новой службе. Также раньше получаете выгоду от наличия независимо развертываемого программного обеспечения. Трудности заключаются в том, что разработчики нередко доходят до этого места и останавливаются, оставляя совместную БД на постоянной основе. Если идти в этом направлении, то нужно понимать, что если не довести до выделения данных, то будут накапливается неприятности на будущее.
Шаблон: «Монолит как слой доступа к данным»
Вместо прямого доступа к данным из монолита мы можем просто перейти к модели, в которой создаем API в самом монолите. «Счет-фактура» требуется информация о сотрудниках, которая хранится в БД монолита, поэтому мы создаем API «Сотрудники», позволяющий службе «Счет-фактура» к ним обращаться.
Одна из причин, почему указанный шаблон не применяется шире, вероятно, кроется в том, что у людей как бы застряла в голове мысль о том, что монолит мертв и бесполезен. Но плюсы здесь очевидны: пока не нужно заниматься декомпозицией данных, а необходимо заняться сокрытием информации, облегчая изолирование нашей новой службы от монолита. Этот шаблон хорошо работает, в особенности если новая служба практически не будет иметь состояния.
На этот шаблон можно смотреть как на способ выявления других потенциальных служб. Развивая эту идею, мы могли увидеть, что API «Сотрудники» выделяется из монолита, становясь микрослужбой самой по себе, как показано на рис. 2.
![Рис. 2 - Использование API «Сотрудники» для выявления контура службы «Сотрудники», которая должна быть выделена из монолита. Рис. 2 - Использование API «Сотрудники» для выявления контура службы «Сотрудники», которая должна быть выделена из монолита.](https://habrastorage.org/getpro/habr/upload_files/2a5/526/24c/2a552624c939277b86606874aa50ceac.png)
Указанный шаблон лучше всего работает, когда код, управляющий данными, по-прежнему находится в монолите, то есть переход данных из одного состояния в другое обеспечивается именно монолитом. Поэтому из этого следует, что микрослужба, которая хочет обратиться к этому состоянию (или изменить его), должна обращаться к монолиту. Если данные, к которым пытаетесь обратиться, в БД монолита, но на самом деле должны быть «во владении» микрослужбы, то лучше пропустить этот шаблон и вместо него выделить данные.
Выделить БД и код вместе
Конечно, есть возможность просто выделить все за один большой шаг. Однако, здесь должно беспокоить то, что этот шаг гораздо обширнее, и пройдет больше времени, прежде чем получится оценить влияние решения в результате. Настоятельно рекомендуется избегать этого подхода и вместо него сначала выделять либо схему, либо прикладной ярус.
Так что же выделить сначала? Позиция автора книги следующая: если беспокоит потенциальное влияние на производительность или согласованность данных, то сначала попытайтесь выделить схему. В противном случае выделяйте код и используйте его для того, чтобы понять, как это влияет на владение данными. Но важно, чтобы конкретный разработчик или команда также думали самостоятельно и учитывали любые факторы, которые повлияют на процесс принятия решений в конкретной ситуации.
До сих пор мы рассматривали выделение схемы на довольно высоком уровне, но есть ряд сложных проблем, связанных с декомпозицией базы данных, и несколько запутанных вопросов, которые требуется прояснить. Давайте рассмотрим несколько более низкоуровневых шаблонов декомпозиции данных: «Разложение таблицы» и «Перенос связи по внешнему ключу в код».
Шаблон: «Разложить таблицу»
Бывает ситуация, когда данные в одной таблице нуждаются в разделение для двух и более микрослужб. На рис. 3 видим одну совместную таблицу «Товарная позиция», в которой хранится информация для каталога и склада.
![Рис. 3 - Одна таблица, соединяющая в себе два выделяемых ограниченных контекста. Рис. 3 - Одна таблица, соединяющая в себе два выделяемых ограниченных контекста.](https://habrastorage.org/getpro/habr/upload_files/6ee/905/bdf/6ee905bdff204ae98aa98b80df5ca32e.png)
В рассматриваемом примере мы хотим выделить каталог и склад как новые службы, но данные для них перемешаны в одной таблице. Поэтому нам нужно разложить эти данные на две отдельные таблицы.
Этот пример довольно прямолинейный, отделить владение данными, один столбец за другим относительно легко. Но что произойдет, когда несколько частей кода обновляют один и тот же столбец? Например, имеется таблица «Клиент», которая содержит столбец «Статус».
Этот столбец обновляется во время регистрации клиента, указывая на то, что данное лицо подтвердило (или не подтвердило) свою электронную почту. Финансовый код занимается приостановкой клиентов, если их счета не оплачены и в этом случае меняется статус клиента. В этом случае «Статус» клиента по-прежнему выглядит так, как будто он должен быть частью доменной модели клиента, и он должен управляться службой «Клиенты».
Это означает, что после выделения служб новая служба «Финансы» будет нуждаться в вызове службы для обновления этого статуса, как мы видим на рис. 4.
![Рис. 4 - Новая служба "Финансы" должна совершать вызовы службы для приостановки клиента. Рис. 4 - Новая служба "Финансы" должна совершать вызовы службы для приостановки клиента.](https://habrastorage.org/getpro/habr/upload_files/cd3/c5b/6b2/cd3c5b6b26b019e83727ddecb31d8e8d.png)
На первый взгляд, указанный шаблон кажется довольно простым. Если таблица принадлежит двум или более ограниченным контекстам в текущем монолите, то нужно разложить таблицу по этим линиям. Если имеются конкретные столбцы в таблице, которые, обновляются многочисленными частями кодовой базы, то нужно принять решение на свое усмотрение в отношении того, кто должен «владеть» этими данными.
Шаблон: «Перенос связи по внешнему ключу в код»
Допустим мы решили извлечь службу «Каталог», которая может управлять и предоставлять информацию об исполнителях, треках и альбомах. В настоящее время каталожный программный код внутри монолита хранит информацию о компакт-дисках в таблице «Альбомы». В таблице «Приходно-расходный регистр» хранится информация о продажах (рис. 5). Таблица «Приходно-расходный регистр» просто регистрирует сумму за компакт-диск, вместе с идентификатором, который ссылается на проданную товарную позицию. Идентификатор в примере называется SKU.
![Рис. 5 – Связь по внешнему ключу. Рис. 5 – Связь по внешнему ключу.](https://habrastorage.org/getpro/habr/upload_files/3c4/4f0/ba3/3c44f0ba3119f151f2b5d6f5cabc7419.png)
В конце каждого месяца нужно составлять отчет с описанием самых продаваемых компакт-дисков. Таблица «Приходно-расходный регистр» помогает нам понять, копий какого артикула продано больше всего, но информация об этом артикуле находится в таблице «Альбомы». Для отчетности нужен запрос к БД, инициированный финансовым кодом, который должен присоединить информацию из таблицы «Приходно-расходный регистр» к таблице «Альбомы», как показано на рис. 5. В итоге, имеется связь по внешнему ключу между таблицами.
Мы хотим выделить каталожный и финансовый код в собственные службы, и это означает, что данные должны идти тоже. Но что произойдет со связью по внешнему ключу? Нужно подумать о двух ключевых проблемах. Во-первых, во время генерации отчета в будущем, как служба «Финансы» будет извлекать каталожную информацию, если не сможет этого сделать через существующую в БД операцию соединения? Во-вторых, что мы сделаем с тем фактом, что теперь будет несогласованность данных?
Новая служба «Финансы» несет ответственность за генерирование отчета о бестселлерах, но не имеет данных об альбомах локально. Поэтому для замены соединения (операции join) она должна будет извлечь необходимые данные из новой службы «Каталог». После чего служба «Финансы» склеивать данные. К сожалению, такой способ уже не будет так же эффективна работать по времени, как раньше. Но это не является существенной проблемой в данном конкретном случае, поскольку отчет генерируется ежемесячно. Но если эта операция выполняется часто, то задержка станет проблемой. Мы можем смягчить вероятное влияние этого увеличения задержки локально кэшируя требуемую информацию об альбомах.
Более запутанная проблема — возможность столкнуться с несогласованностью данных в случае отдельных служб с отдельными схемами. С одной схемой можно удалить строку в таблице «Альбомы», если бы в таблице «Приходно-расходный регистр» была ссылка на эту строку. Схема обеспечила бы согласованность данных. Какие у нас тут варианты?
Первый вариант заключается в том, чтобы при удалении записи из таблицы «Альбомы» сверяться со службой «Финансы», чтобы убедиться, что она не имеет ссылки на запись. Проблема здесь в том, что трудно гарантировать, что данные не будут изменены за то время, которое пройдет между ответом от службы «Финансы» и фактическим удалением данных из таблицы «Альбомы». Еще одна проблема с проверкой того, не используется ли уже та или иная запись, состоит в том, что она создает де-факто обратную зависимость от службы «Каталог». Теперь нужно будет сверяться с любой другой службой, которая использует записи из таблицы «Альбомы». Рекомендуется не рассматривать этот вариант из-за трудности в обеспечении правильной реализации этой операции, а также высокой степени сопряженности службы, которую она создает.
Более приемлемый вариант — предоставить службе «Финансы» возможность разбираться с тем фактом, что служба «Каталог» может не иметь информации об альбоме, делая это следующим способом: показать в отчете сообщение «Информация об альбоме отсутствует», если мы не можем найти данный артикул.
Один из способов уменьшения несогласованности в системе мог бы заключаться в том, чтобы просто не позволять удалять записи в службе «Каталог». Если в существующей системе удаление товарной позиции было сродни обеспечению недоступности товара для продажи или чему-то подобному, то можно реализовать «мягкое» удаления. Это можно сделать, например, с помощью столбца статуса, помечая данную строку как недоступную. В такой ситуации запись об альбоме по-прежнему может запрашиваться службой «Финансы».
Когда вы начинаете думать об эффективном разрыве связей по внешнему ключу, одна из первых мер, которую нужно обеспечить, — это не разъединить две вещи, которые на самом деле хотят быть единым целым. Если есть беспокойство о том, что разъединяете агрегат, то сделайте паузу и пересмотрите свое решение.
Я не стал конспектировать различные способы решения ситуации, когда несколько микрослужб требуют одни и те же статические данные, например, справочник стран. Об этом можно почитать самому в книге. Если очень кратко, то есть следующие способы: «Дублирование статических справочных данных», «Выделение схемы справочных данных», «Библиотека статических справочных данных», «Служба статических справочных данных».
Транзакции
Возможность вносить изменения в БД одной транзакцией намного облегчит разработку и сопровождение. Мы полагаемся на БД в том, что она обеспечивает безопасность и согласованность данных, оставляя нам беспокойство о других вещах. Но при разделении данных по разным БД теряем выгоду от использования транзакции применительно к изменениям.
В типичной ситуации, когда упоминают транзакции в БД, обычно говорят о транзакциях ACID. Давайте кратко рассмотрим ACID. ACID — это аббревиатура, обозначающая ключевые свойства транзакций в БД.
Атомарность (atomicity) - обеспечивает, чтобы все операции, завершаемые в рамках транзакции, либо все завершались успешно, либо все завершались безуспешно.
Согласованность (consistency) - при внесении изменений в БД обеспечивает, чтобы она оставалась в допустимом, согласованном состоянии.
Изоляция (isolation) - позволяет нескольким транзакциям работать одновременно без какого-либо вмешательства.
Долговечность (durability) - обеспечивает, чтобы после завершения транзакции мы были уверены, что данные не будут потеряны в случае системного сбоя.
Мы по-прежнему можем использовать транзакции в стиле ACID, когда разбиваем БД, но объем этих транзакций уменьшается, как и их полезность. Предположим, мы отслеживаем процесс, связанный с подключением нового клиента к системе. В конце процесса предусматривается изменение статуса клиента с pending на verified. После завершения регистрации, мы хотим удалить соответствующую строку из таблицы «Ожидающие регистрации». С одной БД это делается в рамках одной транзакции ACID — либо обе новые строки записываются, либо ни одна из них не записывается. Однако если данные находятся в различных БД, то существует две транзакции, каждая из которых может сработать либо не сработать независимо от другой.
![Рис. 6 – Изменения в рамках двух разных транзакций. Рис. 6 – Изменения в рамках двух разных транзакций.](https://habrastorage.org/getpro/habr/upload_files/650/b5b/ae9/650b5bae981209c50db14a30cb556707.png)
Отсутствие атомарности начнет вызывать значительные проблемы, в особенности если мигрируем системы, которые ранее полагались на это свойство. Именно в этой точке люди начинают искать другие решения, которые дали бы им возможность рассуждать об изменениях, вносимых в многочисленные службы одновременно. В обычной ситуации первый вариант, который начинают рассматривать, — это распределенные транзакции. Я не стану на них останавливаться, так как рекомендуется их избегать для координации изменений состояния в микрослужбах (более подробно можно прочесть в книге).
Тогда что еще можно сделать? Первый вариант — вообще не разбивать данные. Если у вас есть фрагменты состояния, которыми хотите управлять по-настоящему атомарным и согласованным способом, и не можете решить вопрос, как разумно получить эти характеристики без транзакции в стиле ACID, то оставьте это состояние в одной БД и сохраните функциональность, которая управляет этим состоянием в одной службе (или в монолите)
Но что произойдет, если действительно нужно разбить данные, но вы не хотите иметь все тяготы управления распределенными транзакциями? Что, если операция займет минуты, дни или даже месяцы? В подобных случаях можем подумать об альтернативном подходе: о сагах.
Саги
Cага (saga) по своему дизайну представляет собой алгоритм, который координирует многочисленные изменения в состоянии, но позволяет избегать необходимости запирания ресурсов на «замок» в течение длительного периода времени. Мы делаем это путем моделирования шагов, привлекаемых в качестве дискретных действий, которые исполняются независимо.
Cага не дает нам атомарности в терминах АСID, к которой мы привыкли с обычной транзакцией базы данных. Поскольку мы разбиваем бизнес-процесс на отдельные транзакции, у нас нет атомарности на уровне самой саги. У нас есть атомарность для каждой "под-транзакции", поскольку каждая из них может относиться к АСID-транзакционному изменению, если это необходимо. Но вот что сага нам дает, так это достаточно информации для выяснения состояния, в котором она находится.
На рис. 7 изображен простой поток исполнения заказа. Здесь процесс исполнения заказа представлен как одна сага, причем каждый шаг в этом потоке представляет собой операцию, которая выполняется другой службой. В каждой службе любое изменение состояния регулируется в рамках локальной ACID-транзакции.
![Рис. 7 - Пример потока исполнения заказа наряду со службами, ответственными за проведение операции. Рис. 7 - Пример потока исполнения заказа наряду со службами, ответственными за проведение операции.](https://habrastorage.org/getpro/habr/upload_files/54e/f7b/fd1/54ef7bfd10e55678889ea067ac6effb3.png)
Когда сага разбивается на отдельные транзакции, нужно подумать о том, как восстанавливаться, когда происходит сбой.
Обратное восстановление предусматривает отмену сбоя с возвратом назад и последующую очистку — откат. Чтобы это работало как надо, необходимо определить компенсирующие действия, которые позволят отменять ранее совершенные транзакции. Прямое восстановление позволяет восстанавливаться из точки, где произошел сбой, и продолжать обработку. А для того чтобы и это работало как надо, необходимо, чтобы была возможность делать повторные попытки транзакций.
В зависимости от природы моделируемого бизнес-процесса вы можете предусмотреть, чтобы любой режим сбоя запускал обратное восстановление, прямое восстановление или, возможно, их смесь.
Откаты в саге. С сагой у нас вовлечены многочисленные транзакции, и некоторые из них, возможно, уже зафиксированы до того, как мы решим откатить всю операцию. Давайте вернемся к нашему примеру с обработкой заказа. Возьмем потенциальный режим сбоя. Мы дошли до попытки упаковать товарную позицию и обнаружили, что указанная товарная позиция на складе не найдена. Проблема в том, что мы уже приняли оплату и присудили за заказ баллы за лояльность. Вместо этого, если вы хотите реализовать откат, то вам нужно реализовать компенсирующую транзакцию. Компенсирующая транзакция — это операция, которая отменяет ранее зафиксированную транзакцию. Для отката процесса исполнения заказа запускаем компенсирующую транзакцию для каждого шага нашей саги, которая уже была зафиксирована.
![Рис. 8 – Запуск отката всей саги. Рис. 8 – Запуск отката всей саги.](https://habrastorage.org/getpro/habr/upload_files/ee4/850/bfd/ee4850bfde6cf59fbbd5e9741e787ce4.png)
Реализация саг
Давайте рассмотрим два стиля реализации саг. Саги на основе оркестрации в первую очередь опираются на централизованную координацию и отслеживание. Саги на основе хореографии напротив избегают необходимости централизованной координации в пользу более слабо сопряженной модели, но усложняют отслеживание хода выполнения саги.
Cаги на основе оркестрации (orchestrated saga) используют центрального координатора (оркестровщик) для определения порядка исполнения и активации любого необходимого компенсирующего действия. Пример на рис. 9. Здесь «Обработчик заказов», играя роль оркестровщика, координирует процесс исполнения. Он знает, какие службы необходимы для выполнения операции, и решает, когда следует вызывать эти службы.
Тем не менее здесь следует учесть несколько недостатков. Наш «Обработчик заказов» должен знать обо всех ассоциированных службах, что приводит к более высокой степени доменной связности.
![Рис. 9 - Пример того, как оркестрированная сага используется для реализации процесса исполнения заказа. Рис. 9 - Пример того, как оркестрированная сага используется для реализации процесса исполнения заказа.](https://habrastorage.org/getpro/habr/upload_files/8c3/723/80c/8c372380cdbb5d50766963c644d74651.png)
Другая трудность заключается в том, что логика, которая должна принадлежать службам, может начать поглощаться оркестровщиком. Если подобное начнет происходить, то службы становятся «анемичными», с малым собственным поведением. Важно по-прежнему рассматривать службы, составляющие эти оркестрированные потоки, как сущности, имеющие свое собственное локальное состояние и поведение.
Один из способов избежать чрезмерной централизации с оркестрированными потоками состоит в обеспечении наличия разных служб, выполняющих роль оркестровщика для разных потоков. Например, служба «Обработка заказов», которая отвечает за размещение заказа или служба «Прием товаров», которая отвечает за поступление на склад новых товаров и расстановку их на полках, и т. д.
Саги на основе хореографии (choreographed saga) распределяют ответственность за функционирование саги среди нескольких сотрудничающих служб. Как мы увидим на рис. 10, саги на основе хореографии интенсивно используют события для сотрудничества между службами.
Прежде всего, эти службы реагируют на получаемые ими события. События транслируются в системе, и заинтересованные стороны могут их получать. События не отправляются в некую службу. Они просто запускаются, и службы, которые заинтересованы в этих событиях, получают их и действуют соответствующим образом.
![Рис. 10 - Пример саги на основе хореографии для реализации исполнения заказа. Рис. 10 - Пример саги на основе хореографии для реализации исполнения заказа.](https://habrastorage.org/getpro/habr/upload_files/fa6/f32/94c/fa6f3294cbddaee45603fd81d5118bbc.png)
В такой архитектуре службы друг о друге не знают. Им нужно только знать, что делать, когда получено определенное событие. Это приводит к гораздо менее сопряженной архитектуре.
Оборотная сторона всего этого заключается в том, что теперь сложнее выяснить, что происходит. С этой архитектурой непонятно как выстроить модель того, каким предположительно должен быть процесс? Нужно смотреть на поведение каждой службы в изоляции и воссоздавать эту картину в своей голове.
Один из самых простых способов решить это — взять проекцию о состоянии саги из существующей системы, потребляя запущенные события. Если сгенерировать уникальный ИД для саги, то можно поместить его во все события, которые запускаются в рамках этой саги. Тогда мы могли бы иметь службу, работа которой состоит в том, чтобы просто отслеживать все эти события и представлять проекцию о том, в каком состоянии находится каждый заказ, и, возможно, программно выполнять действия по урегулированию трудностей в рамках процесса его исполнения, если другие службы не могут это сделать сами.
Вывод
Выполнение декомпозиции системы, находя границы, вдоль которых возникают контуры служб – является поступательным подходом. Научившись находить эти границы и, в первую очередь, работая над снижением стоимости выделения служб, мы можем продолжать эволюционировать наши системы для удовлетворения любых требований. Некоторые части этой работы бывают кропотливыми и вызывают значительные трудности, которые нужно будет преодолеть. Но тот факт, что это можно сделать поступательно, означает, что нет необходимости бояться этого.