Архитектура микросервисов

Я просмотрел несколько выступлений на конференциях, прочитал несколько статей очень авторитетных и опытных специалистов вроде Мартина Фаулера, Фреда Джорджа, Эдриана Кокрофта и Криса Ричардсона, чтобы как можно больше узнать о микросервисах. Эта статья — результат моих изысканий.

Микросервисная архитектура — это подход к созданию приложения, подразумевающий отказ от единой, монолитной структуры. То есть вместо того чтобы исполнять все ограниченные контексты приложения на сервере с помощью внутрипроцессных взаимодействий, мы используем несколько небольших приложений, каждое из которых соответствует какому-то ограниченному контексту. Причём эти приложения работают на разных серверах и взаимодействуют друг с другом по сети, например посредством HTTP.

Иными словами, мы инкапсулируем определённые контексты приложения в микросервисы, по одному на каждый, а сами микросервисы крутим на разных серверах.

SOA и микросервисы

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

Когда следует использовать микросервисы?

Как архитектор-теоретик, желающий стать практиком, я считаю следующее. Решая, использовать ли микросервисы, ни в коем случае нельзя руководствоваться мифами, или желанием "в следующий раз попробовать это", или стремлением быть на переднем крае технологий. К этому, в соответствии с выводами Рейчел Майерс, нужно подходить исключительно с прагматической точки зрения. Рейчел отмечает, что архитектура должна:

Я согласен с Рейчел, но я также считаю, что этим критериям удовлетворяют и монолитные архитектурные схемы.

Мартин Фаулер выделяет несколько преимуществ монолитной и микросервисной архитектур, что поможет вам решить, какой подход выбрать:

Лично мне нравится прагматичный подход Эрика Эванса. Микросервисы с точки зрения аппаратных ресурсов имеют преимущества, которых лишены единые архитектуры, а также облегчают решение ряда задач с программной точки зрения:

Аппаратные преимущества

Независимая масштабируемость

При размещении модулей на отдельных серверных узлах мы можем масштабировать их независимо от других модулей.

Независимый технический стек

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

Программные преимущества

Сохранение модульности

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

Независимая эволюция подсистем

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

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

Вне зависимости от наших вкусов и пожеланий НЕЛЬЗЯ начинать новый проект сразу с использованием микросервисной архитектуры. Вначале нужно сосредоточиться на понимании задачи и на способе её достижения, не тратя ресурсы на преодоление огромной сложности создания экосистемы микросервисов (Ребекка Парсонс, Саймон Браун).

Предпосылки

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

Реальность разработки ПО такова, что вначале мы никогда не имеем полного понимания задачи. Наше понимание углубляется по мере работ, и нам постоянно приходится рефакторить. Так что рефакторинг — это потребность, но в то же время и опасность, потому что код становится запутанней, особенно при несоблюдении ограниченности контекстов. Микросервисы заставляют соблюдать пределы ограниченных контекстов, что позволяет сохранять работоспособность, ясность, изолированность и инкапсулированность кода в отдельных связных модулях. Если модуль/микросервис становится запутанным, то эта запутанность только в нём и остаётся, а не распространяется за его пределы.

Нам нужно действовать быстрее на всех стадиях разработки! Это верно для любой архитектуры, но микросервисы в этом отношении удобнее. Мартин Фаулер говорит, что необходимо иметь возможность:

Фред Джордж утверждает то же самое: есть огромная потребность ускорить работу, чтобы выдержать конкуренцию! Он приводит ретроспективный анализ времени, необходимого на введение в эксплуатацию сервера, и отмечает, что в 1990-х требовалось 6 месяцев, в 2010-м благодаря облачным сервисам — 30 минут, а в 2015-м Docker позволял поднять и запустить новый сервер менее чем за минуту.

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

Мониторинг крайне важен (Ребекка Парсонс), нам необходимо сразу узнавать о том, что сервер упал, что какой-то компонент перестал отвечать, что происходят сбои вызовов, причём по каждому из микросервисов (Фред Джордж). Также нам нужны инструменты для быстрой отладки (Мартин Фаулер).

Нам нужны devops’ы для мониторинга и управления, при этом между ними и разработчиками должны быть тесные отношения и хорошее взаимодействие (Мартин Фаулер). При работе с микросервисами нам приходится больше развёртывать, усложняется система мониторинга, сильно разрастается количество возможных сбоев. Поэтому в компании очень важна сильная devops-культура (Ребекка Парсонс).

Мартин Фаулер и Джеймс Льюис в своей широко известной статье и выступлениях (Фаулер, Льюис) приводят набор характеристик для определения микросервиса.

Лично я полностью согласен с определением Эдриана Кокрофта:

Архитектура на основе свободно сопряжённых сервисов с ограниченными контекстами. (Loosely coupled service oriented architecture with bounded contexts.)

Ограниченный контекст — это понятие явных границ вокруг какого-то бизнес-контекста. Например, в рамках электронной коммерции мы оперируем понятиями «темы» (themes), «поставщики платёжных услуг» (payment providers), «заказы», «отгрузка», «магазин приложений». Всё это ограниченные контексты, а значит — кандидаты в микросервисы.

Полезная общая информация о микросервисах приводится в книге Сэма Ньюмена «Building Microservices». По мнению Джеймса Льюиса, микросервисы должны:

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

Есть разные мнения о размерах микросервисов. Мартин Фаулер описывает случаи, когда соотношение количества сотрудников и сервисов колебалось от 60 к 20 до 4 к 200. К примеру, в Amazon используется подход с «командами на две пиццы» (two pizzas team): в команде микросервиса должно быть столько людей, чтобы их можно было накормить двумя пиццами.

Фред Джордж полагает, что микросервис должен быть «очень-очень маленьким», чтобы его создавал и сопровождал только один разработчик. То же самое говорит и Джеймс Льюис.

Я согласен с Джеймсом Льюисом, Фредом Джорджем и Эдрианом Кокрофтом. Мне кажется, микросервис должен соответствовать ограниченному контексту, который способен полностью понять один человек. То есть чем шире функциональность приложения, тем больше должно быть микросервисов. Например, в Netflix их около 800! (Фред Джордж)

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

Компонент — это элемент системы, который можно независимо заменить, усовершенствовать (Мартин Фаулер) и масштабировать (Ребекка Парсонс).

При разработке ПО мы используем два типа компонентов:

Гетерогенность — это возможность построить систему с использованием разных языков программирования. У подхода есть ряд преимуществ (Мартин Фаулер), а Чед Фаулер считает, что системы обязаны быть гетерогенны по умолчанию, то есть разработчики должны стараться применять новые технологии.

Преимущества гетерогенной системы:

Правило. При экспериментах с новыми технологиями:

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

При микросервисном подходе команды должны организовываться на основе бизнес-возможностей: например команда заказов, отгрузки, каталога и т. д. В каждой команде должны быть специалисты по всем необходимым технологиям (интерфейс, серверная часть, DBA, QA...). Это даст каждой команде достаточный объём знаний, чтобы сосредоточиться на создании конкретных частей приложения — микросервисов (Мартин Фаулер, Эрик Эванс).

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

Организации, разрабатывающие системы… создают архитектуры, которые копируют структуры взаимодействий внутри этих организаций. (Мелвин Конвей, 1967)

Раньше был такой подход: команда создаёт какую-то функциональность, а затем передаёт её на сопровождение другой команде.

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

Опять же, в старые добрые времена компании использовали архитектуру Enterprise Service Bus (сервисная шина), при которой формируется канал коммуникаций между эндпойнтами и бизнес-логикой. Затем этот подход преобразился в spaghetti box.

Микросервисная архитектура переносит бизнес-логику в конечные точки и использует простые способы взаимодействия вроде HTTP.

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

При традиционном подходе у приложения лишь одна база данных, и много разных компонентов бизнес-логики приложения «взаимодействуют» в рамках этой БД: напрямую читают из неё данные, принадлежащие другим компонентам. Это также означает, что для всех компонентов характерна одна и та же степень сохранности данных, даже если для каких-то из них это не самая лучшая ситуация (Мартин Фаулер).

При микросервисной архитектуре, когда каждый бизнес-компонент представляет собой микросервис, все компоненты обладают собственными базами данных, которые недоступны другим микросервисам. Данные компонента доступны (для чтения и записи) только через соответствующий интерфейс компонентов. Благодаря этому степень устойчивости данных варьируется в зависимости от компонента (Мартин Фаулер, Чед Фаулер).

С точки зрения Фреда Джорджа, это первый вызов на пути к микросервисной архитектуре.

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

Есть два подхода к структурированию фронтенда и бэкенда при микросервисной архитектуре:

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

В начале разработки микросервиса его API особенно нестабилен. Но даже на более поздних стадиях, когда микросервис достаточно отработан, нам приходится менять API, его ввод и вывод. Осторожно вносите изменения, потому что другие приложения будут полагаться на стабильность API (Ребекка Парсонс).

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

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

Конечно, в итоге изменения будут применены ко всем копиям, а данные снова станут согласованными. Это называется eventual consistency — согласованность в конечном счёте. То есть мы знаем, что в течение короткого периода времени данные остаются несогласованными. Этот эффект имеет важное значение в ходе разработки приложений, от серверной части до UX-уровней (Ребекка Парсонс).

Приступая к созданию приложения, нужно изначально придерживаться единой архитектуры — по причине её простоты. В то же время нужно стараться создавать его как можно более модульным, чтобы каждый компонент легко переносился в отдельный микросервис (Ребекка Парсонс). Это сочетается с идеей Саймона Брауна о разработке приложения в виде набора раздельных компонентов в едином развёртываемом модуле.

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

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

Какие потоки создания ценностей (value streams) существуют в организации? Бизнес-продукты? Какие доставляются бизнес-сервисы?

Мы можем посмотреть на продукт не только как создатели, но и как потребители: что они хотят от нашего сервиса? Как они будут его использовать? Чего они ожидают?

Какие части системы могут использовать одни и те же данные? Какие бизнес-логики будут взаимодействовать интенсивнее? (Ребекка Парсонс: 1, 2, 3)

Есть ли в архитектуре единая точка отказа благодаря жёсткой зависимости одного микросервиса от многих других? (Рейчел Майерс)

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

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

Вероятно, вы не сможете всегда делать это вовремя. Но по мере получения опыта и углубления в задачу мы начинаем лучше понимать расположение ограниченных контекстов. Бизнес тоже будет меняться, и нам придётся к этому быстро приспосабливаться! Так что наша система должна позволять быстро разделять и объединять микросервисы.

Всегда может возникнуть необходимость внести критическое изменение в обратную совместимость (backwards compatibility breaking change). Но мы можем постараться делать это лишь в крайнем случае. Чтобы не менять свой сервис постоянно, создайте его так, чтобы сервис приходилось менять только при изменении действительно важных данных.

Не дублируйте статичные файлы (HTML, CSS, JS, изображения) в приложениях и сервисах.

Приложения с пользовательским интерфейсом должны быть полностью отделены от микросервисной экосистемы.

Используйте однократные узлы.

Используйте неизменяемое развёртывание (immutable deployments): никогда не обновляйте ПО на существующих узлах.

Принятые в вашей архитектуре структура и система наименований должны быть одинаковыми для всей экосистемы микросервисов.

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

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

Каждый микросервис должен знать, как контактировать с другими. Можно использовать локализованный (для каждого сервиса) конфиг, обновляемый сразу во всех местах при изменении расположения микросервиса.

Измеряйте всё: сеть, машины, приложение.

При создании нового микросервиса необходимо оснащать его всей необходимой функциональностью по мониторингу.

Лучше использовать много маленьких запросов вместо нескольких больших (уберите соединения — joins).

Разделяйте базы данных.

Создавайте новые фичи в виде микросервисов, прототипов.

Замените код в старой системе на API-вызовы новых микросервисов.

Протестируйте в безумных условиях и под сумасшедшей нагрузкой.

Это должно работать.

Это должно работать быстро.

Это должно быть дешёвым => спотовые экземпляры (spot instances) AWS экономят от 85 до 95 % серверных узлов.

Повышайте скорость разработки и развёртывания с помощью Docker.

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

А дальше уже разработчикам решать, поддерживать или развивать архитектурную структуру. Это подразумевает строгое следование плану, структуре, архитектуре, что не всегда легко. Ведь мы всего лишь люди.

Я считаю, что микросервисная архитектура — это прямое следствие применения обратного закона Конвея к разработке огромных приложений. То есть чисто технически монолитное приложение будет проще в разработке и обслуживании, чем микросервисное с таким же функционалом. Но только до какого-то размера. В жизни любого монолита наступает такой этап, когда язык, на котором он написан, приходит к концу жизни (End Of Life), библиотеки и фреймворки, которые используются, перестают поддерживаться, а обновиться на новые версии языков, библиотек и фрейморков не представляется возможным. Плюс, в какой-то момент поправить какой-то кусок монолита так, чтобы не задеть какую-то другую часть, становится крайне затратным и сложным. Микросервисный подход, хоть и добавляет дополнительную сложность, помогает сохранять скорость развития продукта, даже когда тот становится необъятных размеров.

Микросервисный подход предъявляет много новых требований к организациям, которые хотят его использовать. Необходимо создать инфраструктуру, которая позволяет «по нажатию кнопки» создавать новые микросервисы. И это должны быть не просто облака в смысле IaaS, это должен быть полноценный PaaS для микросервисов. В идеальном случае, любой сотрудник должен где-то указать адрес репозитория с исходным кодом, а PaaS автоматически подключет новый микросервис к CI/CD системе, по запросу создает и удаляет тестовые окружения. Необходимо научить команды разработки пользоваться новым подходом, ведь тут и асинхронные запросы, и отсутствие большой развесистой БД — транзакционного источника правды, вместо которой теперь куча различных специализированных БД. Тут же появляется система Service Discovery, и ее использование тоже надо интегрировать во все новые приложения.

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

Полезные ссылки

Статьи

Werner Vogels • декабрь 2008 • Eventually Consistent – Revisited

Oracle • июнь 2012 • De-mystifying “eventual consistency” in distributed systems

Мартин Фаулер и Джеймс Льюис • март 2014 • Microservices

Конференции

Эдриан Кокрофт • январь 2015 • The State of the Art in Microservices

Чед Фаулер • июль 2015 • From Homogeneous Monolith to Radically Heterogeneous Microservices Architecture

Эрик Эванс • декабрь 2015 • DDD & Microservices: At Last, Some Boundaries!

Фред Джордж • август 2015 • Challenges in implementing Microservices

Джеймс Льюис • октябрь 2015 • Microservices and the Inverse Conway Manoeuvre

Джет Уэсли-Смит • октябрь 2014 • Real World Microservices

Мартин Фаулер • январь 2015 • Microservices

Рейчел Майерс • декабрь 2015 • Stop Building Services, Episode 1: The Phantom Menace

Ребекка Парсонс • июль 2015 • Evolutionary Architecture & Micro-Services