Эта статья является конспектом книги «От монолита к микросервисам». Материал статьи посвящен декомпозиции БД во время процесса разложения монолита на микросервисы.
В предыдущей статье рассмотрели способы извлечения функциональности из монолита в микрослужбы. Однако, что делать с данными? Микрослужбы работают лучше всего, когда они полностью инкапсулируют собственные механизмы хранения и извлечения данных.
Однако разложение БД — далеко не простая задача. Нам нужно учесть вопросы синхронизации данных во время перехода на микрослужбы, логической и физической декомпозиции схемы данных, транзакционной целостности, задержки и многое другое. Далее в этой статье будут рассмотрены эти вопросы.
Однако, прежде чем начнем разделять БД, мы должны рассмотреть трудности, связанные с управлением одной совместной БД.
Совместная БД
На первый взгляд, существует ряд проблем, связанных с совместным использованием одной БД многочисленными службами. Главенствующая проблема заключается в том, что мы отказываем себе в возможности решать, что является совместным, а что скрытым, что противоречит стремлению к сокрытию информации. Это означает, что будет трудно понять, какие части схемы можно изменять безопасным образом. Проблему можно смягчить с помощью представлений БД, но это частичное решение.
Еще одна трудность состоит в том, что становится неясным, кто «контролирует» данные. Где находится бизнес-логика, которая оперирует этими данными? Не «размазана» ли она теперь по всем службам? Из этого, в свою очередь, вытекает отсутствие связности бизнес-логики и затруднение обеспечения правильной реализации управления состоянием.
По мнению автора книги, прямое совместное использование БД подходит для микрослужб только в двух ситуациях. Первая — при рассмотрении статических справочных данных, допускающих только чтение. Здесь структура данных остается очень стабильной, и контроль за изменениями в них в типичной ситуации регулируется в рамках работы по администрированию. Второе место — там, где служба непосредственно выставляет базу данных наружу как определенную конечную точку, которая спланирована и управляется для обслуживания многочисленных потребителей.
Итак, в идеале, мы хотим, чтобы новые службы имели собственные независимые схемы. Но это не то место, где мы начинаем работу с существующей монолитной системой. Означает ли это, что всегда необходимо разбивать эти схемы? Это уместно делать в большинстве ситуаций, но не всегда целесообразно делать изначально. Иногда работа занимает слишком много времени или сопряжена с изменением особенно чувствительных частей системы. В таких случаях полезны различные подходы, которые, по крайней мере, остановят ухудшение ситуации, а в лучшем случае станут разумными шагами к чему-то более оптимальному в будущем.
Шаблон: «Представление БД»
В ситуации, когда нам нужен один источник данных для нескольких служб, представление способно смягчить трудности, касающиеся связности. Это представление ограничивает данные, видимые службе, скрывая информацию, к которой она не должна иметь доступа.
Автор книги участвовал в оказании помощи по переплатформированию существующей системы кредитных деривативов. В ходе работ по оптимизации они обнаружили, что многочисленные приложения вне их контроля имели доступ на чтение к БД, а в некоторых случаях доступ на чтение/запись. Они установили, что все эти внешние системы имели одинаковые имена пользователей и пароли, поэтому было невозможно понять, что это за пользователи и к чему они обращались. Однако вскоре они поняли, что большинство этих приложений не проходят активного технического сопровождения, т. е. не было никаких шансов, что они будут обновлены. По сути, схема БД была направлена в публичное пространство контрактом, который не подлежал изменению: им пришлось идти вперед, поддерживая эту структуру схемы.
Их решение состояло в том, чтобы сначала уладить те ситуации, когда внешние системы писали в их схему. Однако для всех тех клиентов, которые хотели данные читать, они создали выделенную схему, содержащую представления, которые выглядели как старая схема. В результате они смогли вносить изменения в собственную схему, при условии, что поддерживали указанные представления.
Способность представления брать только ограниченную информацию из опорного источника позволяет реализовать некую форму сокрытия информации. Однако это решение не идеально — в таком подходе существуют ограничения. Способы реализации представления отличаются, но в типичной ситуации они являются результатом запроса. Это означает, что само представление доступно только для чтения. Этот факт сразу же ограничивает их полезность. Существуют и другие ограничения, например, необходимость, чтобы исходная схема и представление находились на одном и том же ядре СУБД. Это увеличивает сопряженность физического развертывания, приводя к потенциальной единой точке сбоя.
В зависимости от природы БД может иметься вариант создавать материализованное представление. Такое представление предвычисляется — в типичной ситуации с использованием кэша. Это означает, что чтение из проекции не должно генерировать чтение на опорной схеме, что повышает производительность. Однако может случиться так, что вы будете читать из представления «устаревший» набор данных.
Рекомендуется использовать представление БД преимущественно в ситуациях, когда нецелесообразно осуществлять декомпозицию существующей монолитной схемы. В противном случае, нужно стараться, если это возможно, избегать необходимости представления.
Шаблон: «Служба обертывания БД»
Когда с чем-то слишком трудно справиться, имеет смысл скрыть беспорядок. С помощью шаблона «Служба обертывания базы данных» (database wrapping service) скрываем БД за службой, которая действует как тонкая «обертка», перенося зависимости БД, которые становятся зависимостями службы, как показано на рис. 3.
Указанный шаблон очень хорошо работает там, где слишком трудно разобрать на части опорную схему. Указанный шаблон имеет преимущества перед использованием простого представления БД. Можно написать код в своей службе обертывания, который будет предоставлять гораздо более изощренные представления на опорные данные. Служба обертывания также принимает операции записи (через вызовы API). Разумеется, внедрение этого шаблона требует от вышестоящих потребителей внесения изменений, им придется перейти от прямого доступа к БД к вызовам API. В идеале, применение этого шаблона будет отправной точкой для более фундаментальных изменений, давая время на то, чтобы разложить схему под слоем API.
Шаблон: «БД как служба»
Иногда клиентам нужна база данных только для запросов. Это бывает обусловлено тем, что им нужно запрашивать или извлекать большие объемы данных. Один из подходов состоит в создании выделенной БД, предназначенной для выставления наружу в качестве конечной точки с доступом только для чтения, и заполнении этой БД, когда данные в опорной БД изменяются. Он очень хорошо укладывается в варианты использования, связанные с отчетностью, — ситуации, когда клиентам требуется соединять большие объемы данных, хранящихся в той или иной службе.
До сих пор мы не решали проблему, которая лежит в основании. Мы просто накладывали самые разные «повязки» на большую совместную БД. Прежде чем начнем думать о работе по вытаскиванию данных из монолитной БД, нужно подумать о том, где затрагиваемые данные фактически располагаются. Когда выделяется службы из монолита, некоторые данные должны уходить вместе с ними, а некоторые из них должны оставаться там, где они есть. Если мы внедряем идею, в которой микрослужбы инкапсулируют логику, ассоциированную с одним или несколькими агрегатами, то нам также необходимо перенести управление их внутренним состоянием в собственную схему микрослужбы. С другой стороны, если новой микрослужбе требуется взаимодействовать с агрегатом, который по-прежнему находится во владении монолита, то мы должны выставить эту возможность наружу через четко определенный интерфейс. Далее будут описаны два варианта.
Шаблон: «Монолит с выставлением агрегата наружу»
На рис. 4 показано, что службе «Выписка счетов-фактур» нужно обращаться к разнообразной информации, которая не имеет прямого отношения к управлению выпиской счетов. В настоящее время все эти данные находятся в БД монолита. Выставляя информацию о «Сотрудниках» наружу через конечную точку службы (это может быть API или поток событий) на самом монолите, мы четко указываем, какая информация требуется службе «Счет-фактура».
Монолит по-прежнему «владеет» понятием того, что является и не является допустимым изменением состояния; мы не хотим трактовать это просто как «обертку» вокруг базы данных. Помимо просто выставления данных наружу, выставляются также операции, которые позволяют внешним сторонам запрашивать текущее состояние агрегата и запрашивать новые переходы из состояния в состояние. Мы по-прежнему можем решить ограничить то, какое состояние агрегата выставляется изнутри контура нашей службы наружу, и ограничить то, какие операции перехода из состояния в состояние могут запрашиваться снаружи.
В итоге, указанный шаблон хорошо работает, когда данные, к которым вы хотите обратиться, по-прежнему находятся «во владении» монолитной БД, предоставляя новым службам необходимый доступ. При извлечении служб необходимость в том, чтобы новая служба обращалась обратно в монолит для получения доступа к необходимым данным, скорее всего, будет оборачиваться немного большим объемом работы, чем прямой доступ к монолитной базе данных — но в долгосрочной перспективе эта идея гораздо лучше.
Шаблон: «Смена владельца данных»
Теперь же рассмотрим, что произойдет, когда рассматриваемые данные, находящиеся в текущий момент в монолите, должны быть под контролем только что извлеченной службы?
На рис. 5 описаны изменения, которые должны произойти. Нужно перенести счет-фактурные данные из монолита в новую «Счет-фактуру», поскольку как раз там находится контроль над жизненным циклом данных. Затем нужно изменить монолит так, чтобы трактовать службу «Счет-фактура» как источник истины для счет-фактурных данных, и изменить его таким образом, чтобы он вызывал конечную точку службы «Счет-фактура» для чтения данных или выполнения запроса на изменения.
Указанный шаблон выглядит более четко очерченным. Если только что извлеченная служба инкапсулирует бизнес-логику и указанная логика изменяет некие данные, то эти данные должны находиться под контролем новой службы. Данные должны быть перенесены оттуда, где они находятся сейчас, в новую службу.
Синхронизация данных
Как мы обсуждали в предыдущей статье, одна из выгод шаблона, подобного «Фикусу-удавке» состоит в том, что при переключении на новую службу мы затем можем переключиться обратно, если появляется затруднение. Проблема возникает, когда затрагиваемая служба управляет данными, которые должны поддерживаться в синхронизированном состоянии между монолитом и новой службой.
Какие же есть решения? Для начала, нам нужно подумать о степени, в которой данные должны быть согласованы. Если необходима полная согласованность данных между БД, то одним из наиболее простых подходов было бы обеспечить хранение данных в одном месте. Однако опасения по поводу использования совместной БД невозможно преувеличить: следует ее рассматривать только как очень краткосрочную меру. Если совместная БД слишком долго остается на своем месте, то это приведет к значительной головной боли в долгосрочной перспективе.
Если выполнять переключение методом «Большого взрыва» (что лучше избегать), мигрируя одновременно код приложения и данные, то можно использовать пакетный процесс для копирования данных перед переключением на новую микрослужбу. Однако, что произойдет, если нужно будет вернуться к использованию функциональности существующей монолитной системы? Данные, измененные в схеме микрослужб, не будут отражены в состоянии монолитной БД, поэтому мы в итоге потеряем внутреннее состояние.
Еще один подход — подумать о поддержании двух БД в синхронизированном состоянии. В этом случае операции записи в обе БД будет выполнять либо монолит, либо новая служба. Это решение требует тщательного обдумывания.
Синхронизация данных в приложение
Переключать данные с одного места на другое — мероприятие сложное, но оно тем больше чревато последствиями, чем более ценными являются данные
Далее будут описаны шаги синхронизации на примере проекта, в котором участвовал автор книги. Перед ними стояла задача перейти с MySQL на Riak в целях оптимизации производительности. Риски были большие, поэтому требовалось решение, которое позволило бы компании не только перенести данные в новую БД, но и построить механизмы, которые верифицировали бы миграцию, а также в рабочем порядке имели быстрые механизмы отката. Было принято решение, что приложение само будет выполнять синхронизацию между двумя источниками данных. Идея заключалась в том, что изначально существующая БД MySQL останется источником истины, но в течение определенного периода времени приложение будет обеспечивать синхронизацию данных в MySQL и Riak. Через некоторое время Riak станет источником истины для приложения, и только после этого MySQL будет выведена из эксплуатации.
Шаг 1 – массово синхронизировать данные. Это включает выполнение пакетной миграции данных из старой системы в новую базу данных Riak. Пока продолжался пакетный импорт, существующая система продолжала работать, поэтому источником данных для импорта был снимок данных, взятый из существующей системы MySQL. Это вызывает трудность в том плане, что после завершения пакетного импорта данные в исходной системе вполне могли измениться. После завершения пакетного импорта был реализован процесс захвата изменений в данных, с помощью которого были применены изменения, внесенные с момента начала импорта. Это позволило привести Riak в синхронизированное состояние.
Шаг 2 – синхронизация во время записи и чтение из старой схемы. Теперь, когда обе БД были синхронизированы, можно развернуть новую версию приложения, которая будет записывать все данные в обе БД (рис. 6). Цель на данном этапе — обеспечить, чтобы приложение правильно писало в оба источника. Тот факт, что все данные по-прежнему читались из MySQL, обеспечивал возможность извлечения данных из существующей БД MySQL даже в случае, если бы Riak «рухнула».
Шаг 3 – синхронизация во время записи и чтение из новой схемы. На этом этапе было определено, что чтение в Riak осуществляется нормально. Последний шаг — убедиться, что запись тоже работает. После чего Riak становится источником истины. Обратите внимание, что мы по-прежнему пишем в обе БД, поэтому при возникновении каких-либо затруднений есть возможность откатить назад. После того как новая система будет в достаточной мере отлажена, старую схему можно безопасно удалить.
В данном примере миграция происходила в рамках одного приложения. Но мы говорим о ситуациях, когда хотим извлечь микрослужбу. В таком случае поможет ли этот шаблон? Первым делом следует учесть то, что этот шаблон имеет большой смысл, если нужно разложить схему перед выделением кода приложения. При правильной реализации оба источника данных всегда должны находиться в синхронизованном состоянии, что дает значительные выгоды в ситуациях, когда требуется быстрое переключение между источниками для сценариев отката и т. д.
Для того чтобы этот шаблон работал как надо, необходимо, чтобы и монолит, и микрослужба обеспечивали надлежащую синхронизацию во всех БД. Если одно из них ошибется, то будут неприятности.
Физическое и логическое разделение БД
Когда мы говорим о разложении БД, мы в первую очередь пытаемся достичь логического разделения, или сегментации. Одно ядро СУБД способно идеально разместить более одной логически разделенной схемы, как показано на рис. 7.
Почему возникает желание разложить схемы логически, но все-таки иметь их на одном ядре СУБД? Дело в том, что в своей основе логическая и физическая декомпозиция преследуют разные цели. Логическая декомпозиция обеспечивает более простое независимое изменение и сокрытие информации, в то время как физическая декомпозиция потенциально повышает робастность системы и помогает устранять конкуренцию за ресурсы, позволяя повышать пропускную способность.
Когда разбиваем схемы БД логически, но сохраняем их на том же физическом ядре СУБД мы имеем потенциальную единственную точку сбоя. Если ядро СУБД выходит из строя, то затрагиваются обе службы. Однако многие ядра СУБД имеют механизмы, позволяющие избегать единственных точек сбоя.
На этом первая часть конспекта закончена. В следующей части разберем, что же выделить сначала: БД или код. А также затронем транзакции и саги в рамках микрослужб.