Микросервисная архитектура
Микросервисная архитектура (Microservices Architecture) строит одно приложение как набор небольших сервисов: каждый работает в своём процессе, развёртывается независимо от остальных и сам владеет своими данными. Систему режут по бизнес-возможностям - учётные записи, письма, оплата, - а не по техническим слоям, и сервисы общаются только по сети через API. Общей базы данных нет: чужие данные можно получить, только спросив их владельца. За независимость платят распределённостью: любой вызов может опоздать или потеряться, а согласованность приходится держать без общей транзакции.
Проблема
Монолит, например слоистое приложение, работает в одном процессе и выходит одним развёртыванием. Пока команда маленькая, это удобно. Когда над системой работают десятки людей, правка в рассылке писем требует пересобрать и выкатить всё приложение, релизы ждут друг друга, а ошибка в одном модуле роняет процесс целиком. Масштабировать можно только всё сразу, даже если нагрузка приходится на одну функцию. Если команды поделены по слоям (интерфейс, логика, база), простая правка становится проектом сразу для трёх команд. Клиент-сервер этого не решает: он отделяет клиента от сервера, но сервер остаётся одной большой программой.
Решение
Систему делят на сервисы по бизнес-возможностям: в примере это сервис аккаунтов и сервис приветственных писем. У каждого свой процесс, свой цикл выпуска и своё хранилище, в которое не заглядывает никто другой. Сервисы общаются сообщениями через API по сети, и сервис сам становится клиентом другого сервиса, когда ему нужна чужая работа. Так как сеть ненадёжна, на каждом вызове стоит тайм-аут, а повтор идёт с ключом идемпотентности, чтобы не сделать работу дважды. Заранее решают, что делать, когда сосед недоступен: в примере аккаунт создаётся, а письмо ждёт повтора. Как заменить такие ожидания событиями, разбирает следующая тема, событийная архитектура (Event-Driven), а раздельные модели чтения и записи - тема CQRS.
Как это работает
Термин моложе самой идеи. По словам Джеймса Льюиса и Мартина Фаулера, на семинаре архитекторов под Венецией в мае 2011 года так назвали стиль, который участники уже пробовали, а в мае 2012-го та же группа выбрала «microservices» как самое подходящее имя. В марте 2012 года Льюис рассказывал об этих идеях на конференции 33rd Degree в Кракове, примерно тогда же похожее выступление было у Фреда Джорджа. Статья Льюиса и Фаулера «Microservices» (25 марта 2014) определяет стиль как подход, при котором одно приложение строится из набора небольших сервисов, каждый в своём процессе, с лёгкими механизмами общения, часто HTTP API.
Главное свойство - независимое развёртывание, а не размер. Льюис и Фаулер пишут, что сервисы развёртываются независимо, и правка одного обычно требует выкатить только его, хотя изменение интерфейса всё же требует согласования. Сэм Ньюмен в книге «Monolith to Microservices» (2019) определяет микросервисы как независимо развёртываемые сервисы, смоделированные вокруг бизнес-области. Сами Льюис и Фаулер жалеют, что название заставляет думать о размере: самые крупные сервисы, о которых им рассказывали, укладывались в команду, которую можно накормить двумя пиццами, то есть не больше десятка человек.
Резать по бизнес-возможностям, а не по слоям, учит закон Конвея. Мелвин Конвей в статье «How Do Committees Invent?» (Datamation, апрель 1968) писал, что организации, которые проектируют системы, обречены выпускать проекты, повторяющие структуру общения этих организаций. Льюис и Фаулер приводят следствие: если команды поделены на интерфейс, серверную логику и базы данных, даже простая правка превращается в межкомандный проект. Микросервисы делают наоборот: одна команда владеет возможностью целиком, от API до данных.
Своя база у каждого сервиса - раздел «Decentralized Data Management» статьи Льюиса и Фаулера: сервис сам управляет своей базой, и это могут быть разные системы хранения (polyglot persistence). Марк Ричардс и Нил Форд в главе 17 «Fundamentals of Software Architecture» (2020) выводят это из понятия ограниченного контекста (bounded context) предметно-ориентированного проектирования: сервис владеет своими данными, а остальные получают их только через него. Цена - нет общих транзакций: по Льюису и Фаулеру, сервисы координируются без транзакций, согласованность бывает только итоговой (eventual consistency), а проблемы исправляют компенсирующими действиями.
Цену распределённости хорошо описывают заблуждения о распределённых вычислениях. В 1994 году Питер Дойч в Sun Microsystems составил список из семи, четыре из которых раньше назвали Билл Джой и Дейв Лайон; около 1997 года Джеймс Гослинг добавил восьмое. Первые два - «сеть надёжна» и «задержка равна нулю». Пример показывает оба: сервис может промолчать, ответ может опоздать, и тайм-аут не говорит, была ли сделана работа. Раздел «Design for failure» у Льюиса и Фаулера прямо называет это недостатком по сравнению с монолитом: приложение обязано переживать отказы сервисов, и это добавляет сложности.
Сравнение с соседями. Слоистый монолит - один процесс и одно развёртывание, вызовы между слоями обычные и не теряются. Клиент-сервер выносит интерфейс на клиент, но сервер один. В микросервисах серверов много, и каждый может быть клиентом другого. Синхронная цепочка вызовов делает доступность общей: если регистрация ждёт письмо, а у каждого сервиса доступность 99,9%, у цепочки она около 99,8%. Поэтому в примере письмо не держит регистрацию: то, что не нужно для ответа, откладывают.
Обратный путь тоже бывает. В марте 2023 года инженер Prime Video Марцин Кольны описал сервис мониторинга качества видео, собранный из распределённых компонентов на AWS Step Functions и Lambda с кадрами в S3. Он упёрся в жёсткий предел масштабирования примерно на 5% ожидаемой нагрузки, и команда собрала все компоненты в один процесс на EC2 и ECS - стоимость инфраструктуры упала больше чем на 90%. Вывод самой статьи: выбор между сервисами и монолитом делают для каждого случая. Это был один сервис, а не весь Prime Video, и в споре вокруг статьи многие замечали, что дорогими были прежде всего бессерверные компоненты.
Нюансы выбора
Монолит или микросервисы - маленькой команде с неясными границами предметной области обычно хватает хорошо разделённого монолита: вызовы внутри процесса проще и не теряются. Делить стоит, когда общее развёртывание и общая база реально тормозят команды, а не на всякий случай.
Граница сервиса - по бизнес-возможности, вокруг данных, которые меняются вместе. Если два сервиса всегда выкатываются вместе и на каждый запрос зовут друг друга, граница проведена неверно: получился распределённый монолит со всеми минусами обоих стилей.
Ждать или отложить - всё, что не нужно для ответа пользователю, лучше не держать в синхронной цепочке, как письмо в примере. Очереди и события вместо ожиданий - тема следующего паттерна, событийной архитектуры.
Общая база данных - соблазн «не гонять данные по сети» превращает схему базы в общий контракт: её правка ломает соседа, и развёртывание снова становится общим. Нужны чужие данные - через API владельца или копией; отдельные модели для чтения - тема CQRS.
Тайм-ауты и повторы - у каждого сетевого вызова должен быть тайм-аут, внутренний короче внешнего, а повторять можно только запросы с ключом идемпотентности. Тайм-аут значит «неизвестно», а не «не было»: в примере медленный сервис письмо уже отправил.
Примеры и история
James Lewis, Martin Fowler, «Microservices» (martinfowler.com, 25 марта 2014) - определение стиля и девять характеристик: компоненты-сервисы, организация по бизнес-возможностям, продукты вместо проектов, умные конечные точки и простые каналы, децентрализованное управление и данные, автоматизация инфраструктуры, проектирование под отказ, эволюционный дизайн.
Sam Newman, «Building Microservices» (O'Reilly, 2015; 2-е издание - 2021) - основная книга о проектировании микросервисов; в его же «Monolith to Microservices» (2019) - определение через независимое развёртывание и бизнес-область.
Melvin Conway, «How Do Committees Invent?» (Datamation, апрель 1968) - статья, из которой вырос закон Конвея: структура системы повторяет структуру общения организации, которая её проектирует.
Заблуждения о распределённых вычислениях (L. Peter Deutsch и коллеги, Sun Microsystems): семь пунктов в 1994 году, восьмой добавил Джеймс Гослинг около 1997-го - сеть надёжна, задержка нулевая, пропускная способность бесконечна, сеть безопасна, топология не меняется, администратор один, передача бесплатна, сеть однородна.
Mark Richards, Neal Ford, «Fundamentals of Software Architecture» (2020) - глава 17 «Microservices Architecture»: история стиля, ограниченный контекст, гранулярность, изоляция данных, слой API, хореография и оркестровка.
Когда применять
- Когда несколько команд развивают одну большую систему и мешают друг другу общим развёртыванием, а части системы должны выходить в разном темпе.
- Когда у частей системы сильно разная нагрузка или требования к надёжности, и им нужно масштабироваться и отказывать по отдельности.
Примеры из практики
- Netflix - в августе 2008 года повреждение базы данных на три дня остановило отправку DVD подписчикам. После этого компания решила уйти от вертикально масштабируемых единых точек отказа к распределённым системам в облаке AWS и закончила переезд в начале января 2016 года. Адриан Кокрофт, по словам Льюиса и Фаулера, называл этот стиль «fine grained SOA».
- Amazon - во внутреннем «Distributed Computing Manifesto» 1998 года (Вернер Фогельс опубликовал его в 2022-м) сказано, что клиенты больше не обращаются к базе данных напрямую, а только через интерфейс, который скрывает бизнес-логику. Льюис и Фаулер приводят принцип Amazon «you build, you run it»: команда, которая написала сервис, сама его и эксплуатирует.
Похожие паттерны
Событийная архитектура
Событийная архитектура (Event-Driven Architecture) связывает части системы через события: компонент сообщает, что уже произошло, например «пользователь зарегистрирован», и не ждёт ответа. Событие уходит в брокер, а брокер доставляет копию каждому, кто на него подписан. Отправитель не знает, сколько у него подписчиков и что они сделают, поэтому новую реакцию можно добавить, не трогая его код. За эту свободу платят: событие может прийти дважды или с опозданием, данные у частей системы какое-то время расходятся, а путь одного действия через систему труднее проследить.
CQRS
CQRS (Command Query Responsibility Segregation) разводит запись и чтение по двум разным моделям. Модель записи принимает команды, проверяет правила и хранит данные так, чтобы их было удобно защищать. Модель чтения хранит те же данные уже в том виде, в каком их показывает конкретный экран, и отвечает на запросы, ничего не меняя. Между ними ходят события, поэтому модель чтения отстаёт от записи на какое-то время. Это приём для отдельной части системы, где чтение и запись действительно тянут модель в разные стороны, а не архитектура всего приложения.