Создание микросервисов
Қосымшада ыңғайлырақҚосымшаны жүктеуге арналған QRRuStore · Samsung Galaxy Store
Huawei AppGallery · Xiaomi GetApps

автордың кітабын онлайн тегін оқу  Создание микросервисов

 

Сэм Ньюмен
Создание микросервисов. 2-е издание
2023

Переводчик С. Черников


 

Сэм Ньюмен

Создание микросервисов. 2-е издание. — СПб.: Питер, 2023.

 

ISBN 978-5-4461-1145-9

© ООО Издательство "Питер", 2023

 

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

 

Предисловие

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

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

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

Кому стоит прочитать эту книгу

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

Почему я написал эту книгу

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

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

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

Что изменилось с момента выхода первого издания

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

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

Кроме того, я написал первое издание книги «Создание микросервисов», чтобы не только рассказать о микросервисах, но и продемонстрировать, как этот архитектурный подход меняет суть разработки программного обеспечения (ПО). Поэтому, более глубоко изучив вопросы, связанные с безопасностью и отказоустойчивостью, я обнаружил, что хочу подробнее остановиться на тех темах, которые становятся все более важными для современной разработки программного обеспечения.

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

Я надеялся написать книгу примерно того же объема, что и первое издание, и при этом найти способ привести больше деталей. Как видно, мне не удалось достичь своей цели — это издание стало больше! Но я думаю, что смог более четко сформулировать свои идеи.

Навигация по книге

Книгу лучше читать целиком от начала и до конца, но, конечно, можно перейти к конкретным темам, которые вас больше всего интересуют. Если вы все-таки решите углубиться в конкретную главу, возможно, глоссарий в конце книги объяснит вам новые или незнакомые термины. Что касается терминологии, я использую слова «микросервис» и «сервис» взаимозаменяемо на протяжении всего повествования. Считайте, что эти два термина относятся к одному и тому же понятию, если явно не указано иное. Я также резюмирую основные концепции в послесловии, однако учтите, что вы упустите много важных деталей, если просто перейдете в конец книги!

Книга разбита на три отдельные части: «Основы», «Реализация» и «Люди». Давайте рассмотрим, какие вопросы охватывает каждая из них.

Часть I. Основы

В этой части я подробно описываю некоторые ключевые идеи, лежащие в основе микросервисов.

Глава 1 «Что такое микросервисы». Это общее введение в микросервисы, в нем я привожу ряд тем, которые будут подробно описаны позже в книге.

Глава 2 «Как моделировать микросервисы». В этой главе рассматривается важность таких понятий, как скрытие информации, связность и связанность, а также использование предметно-ориентированного проектирования для определения правильных границ ваших микросервисов.

Глава 3 «Разделение монолита на части». Здесь приведены некоторые рекомендации о том, как взять существующее монолитное приложение и разбить его на микросервисы.

Глава 4 «Стили взаимодействия микросервисов». В последней главе этой части мы обсудим различные типы связи микросервисов, включая асинхронные и синхронные вызовы, а также стили взаимодействия «запрос — ответ» и событийную архитектуру.

Часть II. Реализация

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

Глава 5 «Реализация коммуникации микросервисов». В этой главе мы по­дробно рассмотрим конкретные технологии, используемые для реализации взаимодействия между микросервисами.

Глава 6 «Рабочий поток». В ней предлагается сравнение саг и распределенных транзакций и обсуждается их полезность при моделировании бизнес-процессов с использованием нескольких микросервисов.

Глава 7 «Сборка». В этой главе микросервис сопоставляется с репозиториями и сборками.

Глава 8 «Развертывание». В этой главе мы обсудим множество вариантов развертывания микросервиса, в том числе использование контейнеров, Kubernetes и FaaS.

Глава 9 «Тестирование». Здесь обсуждаются проблемы тестирования микросервисов, в том числе проблемы, вызванные сквозными тестами, и то, как могут помочь контракты, ориентированные на потребителя, и продакшен-тестирование.

Глава 10 «От мониторинга к наблюдаемости». В этой главе мы переходим от изучения деятельности по статическому мониторингу к более широкому взгляду на улучшение наблюдаемости микросервисных архитектур. Здесь приводятся некоторые рекомендации относительно инструментария.

Глава 11 «Безопасность». Микросервисные архитектуры создают большую площадь для внешних атак, но также дают нам больше возможностей для глубокой обороны. В этой главе мы рассмотрим правильный баланс между уязвимостью и защитой.

Глава 12 «Отказоустойчивость». В ней предлагается более широкий взгляд на то, что такое отказоустойчивость, и на ту роль, которую микросервисы могут сыграть в повышении отказоустойчивости ваших приложений.

Глава 13 «Масштабирование». В этой главе я описываю четыре оси масштабирования и показываю, как их можно использовать совместно для масштабирования микросервисной архитектуры.

Часть III. Люди

Идеи и технологии ничего не значат без людей и организаций, которые их используют.

Глава 14 «Пользовательские интерфейсы». В этой главе рассматриваются принципы совместной работы микросервисов и пользовательских интерфейсов, начиная с перехода от выделенных команд разработки пользовательского интерфейса (фронтенд-разработки) к использованию BFF и GraphQL.

Глава 15 «Организационные структуры». В предпоследней главе основное внимание уделяется тому, как потоковые команды и команды поддержки могут работать в контексте микросервисных архитектур.

Глава 16 «Эволюционный архитектор». Микросервисные архитектуры не статичны, поэтому вам, возможно, потребуется пересмотреть ваше отношение к системной архитектуре — тема, подробно рассматриваемая в этой главе.

Условные обозначения

В этой книге используются следующие типографические обозначения.

Курсив

Обозначает новые термины и важные понятия.

Моноширинный шрифт

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

Рубленый шрифт

Применяется для выделения URL, адресов электронной почты.

Обозначает совет или предложение.

Обозначает примечание общего характера.

Обозначает предупреждение или предостережение.

Благодарности

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

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

Второго издания не было бы без первого, поэтому я хотел бы еще раз сказать спасибо всем, кто помогал мне в сложном процессе написания моей первой книги, включая технических рецензентов Бена Кристенсена, Мартина Фаулера, Венката Субраманьяма, Джеймса Льюиса за наши многочисленные поучительные беседы, команду O’Reilly в лице Брайана Макдональда, Рэйчел Монаган, Кристен Браун и Бетси Валишевски, и за отличные отзывы читателей Ананда Кришнасвами, Кента Макнила, Чарльза Хейнса, Криса Форда, Эйди Льюиса, Уилла Темза, Джона Ивса, Рольфа Рассела, Бадринатха Янакирамана, Дэниела Брайанта, Иэна Робинсона, Джима Уэббера, Стюарта Глидоу, Эвана Боттчера, Эрика Суорда и Оливию Леонард. И спасибо Майку Лукидесу, я думаю, за то, что он втянул меня в эту неразбериху в первую очередь!

Для второго издания Мартин Фаулер снова вернулся в качестве научного редактора, и к нему присоединились Дэниел Брайант и Сара Уэллс, которые не пожалели своего времени на отзывы. Я также хотел бы поблагодарить Ники Райтсон и Александра фон Цитцервитца за помощь в доведении технического обзора до конца. Что касается O’Reilly, то весь процесс контролировался моим потрясающим редактором Николь Таше, без которой я бы точно сошел с ума, Мелиссой Даффилд, которая, похоже, справляется с моей рабочей нагрузкой лучше, чем я. Благодарю также Деба Бейкера, Артура Джонсона и остальную производственную команду (мне жаль, что я не знаю всех ваших имен, но спасибо вам!), а также Мэри Трезелер за то, что в трудные времена брала штурвал в свои руки.

Кроме того, огромное спасибо за неоценимую помощь ряду людей, в том числе (в произвольном порядке) Дэйву Кумбсу и команде Tyro, Дэйву Хэлси и команде Money Supermarket, Тому Керхову, Эрике Доерненбург, Грэму Тэкли, Кенту Бек, Кевлин Хенни, Лоре Белл, Адриане Муат, Саре Тарапоревалла, Уве Фридрихсу, Лиз Фонг-Джонс, Кейну Стивенсу, Гилманко Стейнсу, Адаму Торнхиллу, Венкату Субраманьяму, Сюзанне Кайзер, Яне Шауманн, Грейди Бучу, Пини Резник, Николь Форсгрен, Джезу Хамблу, Джин Ким, Мануэлю Паис, Мэтью Скелтону и команде «Саут Сидней Рэббитоуз». Наконец, я хотел бы поблагодарить восхищенных читателей ранней версии книги, предоставивших бесценные отзывы. Среди них Фелипе де Мораис, Марк Гарднер, Дэвид Лозон, Ассам Зафар, Майкл Блетерман, Никола Мусатти, Элеонора Лестер, Фелипе де Мораис, Натан Димауро, Даниэль Лемке, Сонер Экер, Риппл Шах, Джоэл Лим и Химаншу Пант. И наконец, привет Джейсону Айзексу.

От издательства

Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция).

Мы будем рады узнать ваше мнение!

На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.

Часть I Основы

Глава 1. Что такое микросервисы

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

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

Первый взгляд на микросервисы

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

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

Снаружи отдельный микросервис рассматривается как черный ящик. Он размещает бизнес-функции в одной или нескольких конечных точках сети (например, в очереди или REST API, как показано на рис. 1.1) по любым наиболее подходящим протоколам. Потребители, будь то другие микросервисы или иные виды программ, получают доступ к этой функциональности через такие точки. Внутренние детали реализации (например, технология, по которой был создан сервис, или способ хранения данных) полностью скрыты от внешнего мира. Это означает, что в микросервисных архитектурах в большинстве случаев не используются общие базы данных. Вместо этого каждый микросервис инкапсулирует свою собственную БД там, где это необходимо.

Рис. 1.1. Микросервис, предоставляющий свои функциональные возможности через REST API и топик

Микросервисы используют концепцию скрытия информации1. Это означает скрытие как можно большего количества информации внутри компонента и как можно меньшее ее раскрытие через внешние интерфейсы. Так можно провести четкую границу между легко и сложно изменяемыми данными. Реализацию, скрытую от сторонних участников процесса, можно свободно преобразовывать, пока у сетевых интерфейсов, предоставляемых микросервисом, сохраняется обратная совместимость. Изменения внутри границ микросервиса (как показано на рис. 1.1) не должны влиять на вышестоящего потребителя, обеспечивая возможность независимого выпуска функциональных возможностей. Это необходимо для того, чтобы микросервисы могли работать изолированно и выпускаться по требованию. Наличие четких, стабильных границ сервисов, не изменяющихся при преобразовании внутренней реализации, приводит к тому, что системы получают более слабую связанность (coupling) и более сильную связность (cohesion).

Пока мы говорим о скрытии деталей внутренней реализации, с моей стороны было бы упущением не упомянуть шаблон гексагональной архитектуры, впервые подробно описанный Алистером Кокберном2. Этот шаблон определяет важность сохранения внутренней реализации отдельно от ее внешних интерфейсов, поскольку вы, возможно, захотите взаимодействовать с одной и той же функцио­нальностью через разные типы интерфейсов. Я изображаю свои микросервисы в виде шестиугольников (гексагонов) отчасти для того, чтобы отличить их от «обычных» сервисов, но также по причине моей любви к этим фигурам.

Сервис-ориентированная архитектура и микросервисы — разные вещи?

Сервис-ориентированная архитектура (SOA, service-oriented architecture) — это подход к проектированию, при котором несколько сервисов взаимодействуют для обеспечения определенного конечного набора возможностей (сервис здесь обычно означает полностью отдельный процесс операционной системы). Связь между этими сервисами осуществляется посредством сетевых вызовов, а не с помощью вызовов методов внутри границ процесса.

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

По своей сути SOA — разумная идея. Однако, несмотря на многочисленные усилия, отсутствует единый стандарт правильной реализации SOA. На мой взгляд, нет целостного взгляда на проблему, поэтому не получилось прийти к единому мнению, с которым были бы согласны все представители отрасли.

Многие из проблем, лежащих в основе SOA, на самом деле относятся к проблемам с протоколами связи (например, SOAP), промежуточным ПО поставщика, отсутствием рекомендаций по детализации сервиса или неправильным руководством по выбору мест для разделения вашей системы. Циник мог бы

предположить, что поставщики навязывали (а в некоторых случаях и стимулировали) внедрение SOA как способ продать больше продуктов, и эти самые продукты в конечном счете подорвали цель SOA.

Я видел множество примеров SOA, в которых команды стремились сделать сервисы меньше, но эти сервисы все еще были связаны с базой данных, и приходилось развертывать все вместе. Сервис-ориентировано? Да. Но это не микросервисы.

Микросервисный подход появился благодаря накопленному практическому опыту успешных реализаций SOA и лучшему пониманию систем и архитектуры. Вы должны воспринимать микросервисы как специфический подход к SOA. Это то же самое, что и экстремальное программирование (XP, Extreme Programming) или Scrum — особый подход к гибкой разработке ПО.

Ключевые понятия микросервисов

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

Независимое развертывание

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

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

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

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

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

Моделирование вокруг предметной области бизнеса

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

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

Я часто вижу многоуровневые архитектуры, типичный пример которых представлен на рис. 1.2. Здесь каждый уровень определяет отдельную границу обслуживания, причем каждая из них относится к соответствующей технической функциональности. Если бы в этом примере я вносил изменения только в уровень представления, это было бы довольно эффективно. Однако опыт показывает, что изменения в функциональности обычно охватывают несколько уровней, что требует изменений в представлении, приложениях и уровне данных. Эта проблема усугубляется, если архитектура еще более многоуровневая, чем в простом примере на рис. 1.2. Часто каждый уровень разбит на дополнительные слои.

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

Рис. 1.2. Традиционная трехуровневая архитектура

Позже в этой главе мы еще вернемся к предметно-ориентированному проектированию и к тому, как оно взаимодействует с организационным проектированием.

Контроль над ситуацией

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

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

Скрытие внутреннего состояния в микросервисе аналогично практике инкапсуляции в объектно-ориентированном (OO) программировании. Инкапсуляция данных в ОО-системах представляет собой пример скрытия информации в действии.

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

Как обсуждалось в предыдущем разделе, необходимо рассматривать наши сервисы как сквозные срезы бизнес-функциональности, которые, где это уместно, инкапсулируют пользовательский интерфейс (user interface, UI), бизнес-логику и данные. Это связано с желанием прикладывать как можно меньше усилий, необходимых для изменения бизнес-функциональности. Инкапсуляция данных и подобное поведение обеспечивают сильную связность бизнес-функций. Скрывая поддерживающую сервис БД, мы также обеспечиваем ослабление связанности. Мы вернемся к связанности и связности в главе 2.

Размер

«Насколько большим должен быть микросервис?» — один из самых распространенных вопросов, которые я слышу. Учитывая, что часть «микро» присутствует прямо в названии, ответ однозначный. Однако, когда вы поймете, что из себя представляет микросервис как архитектура, размер перестанет быть одной из наиболее интересующих характеристик.

Как узнать размер? Сосчитав строки кода? Для меня это не имеет особого смысла. Задача, требующая 25 строк кода на Java, может быть написана в десяти строках Clojure. Это не значит, что Clojure лучше или хуже Java. Некоторые языки просто более выразительны, чем другие.

Джеймс Льюис, технический директор Thoughtworks, известен своим высказыванием: «Микросервис должен быть размером с мою голову». На первый взгляд, это выражение кажется бессмысленным. В конце концов, насколько велика голова Джеймса на самом деле? Однако суть этого утверждения в том, что микросервис должен быть такого размера, при котором его можно легко понять. Проблема, конечно, заключается в том, что разные люди могут неодинаково понимать какую-либо информацию, поэтому на вопрос, какой размер подходит именно вам, можете ответить только вы. Более опытная команда лучше справится с управлением крупной кодовой базы, чем любая другая. Так что, возможно, было бы правильнее интерпретировать цитату Джеймса как «микросервис должен быть размером с вашу голову».

Крис Ричардсон, автор книги «Микросервисы. Паттерны разработки и рефакторинга»4, говорит, что цель микросервисов — получить «как можно меньший интерфейс». Это снова согласуется с концепцией скрытия информации, но представляет собой попытку найти смысл в термине «микросервисы», которого изначально не было. Когда этот термин впервые использовался для определения архитектур, основное внимание, по крайней мере на начальном этапе, уделялось не размеру интерфейсов.

В конечном счете понятие размера в значительной степени зависит от контекста. Поговорите с человеком, который работал над системой в течение 15 лет, и он скажет, что по его ощущениям система со 100 000 строк кода действительно проста для понимания, в то время как недавно привлеченному к проекту сотруднику покажется, что она слишком масштабна. Аналогично и в компаниях, только что приступивших к переходу на микросервисы и создавших, возможно, десять или меньше элементов логики, по сравнению с компанией, для которой микросервисы были нормой в течение многих лет, и сейчас их в ней сотни.

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

Гибкость

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

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

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

Согласование архитектуры и структуры организации

MusicCorp — компания, торгующая компакт-дисками онлайн. В ее системе использована простая трехуровневая архитектура (как на рис. 1.2). Мы решили перенести сопротивляющуюся современным тенденциям MusicCorp в XXI век и в рамках этого перемещения оцениваем существующую системную архитектуру. У нас есть веб-интерфейс, уровень бизнес-логики в виде монолитного бэкенда и хранилище данных в виде традиционной БД. За эти слои, как обычно бывает, отвечают разные команды. Мы будем возвращаться к примеру MusicCorp на протяжении всей книги.

Хочется внести простое обновление в функциональность: мы решили позволить клиентам указывать свой любимый жанр музыки. Это обновление требует внесения изменений в пользовательский интерфейс (для выбора жанра), в бэкенд сервиса (чтобы разрешить отображение жанра в пользовательском интерфейсе и иметь возможность менять значение), а также в базу данных (чтобы принять это изменение). Каждая команда должна будет управлять этими изменениями и внедрять их в правильном порядке, как показано на рис. 1.3.

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

Ныне известный закон Конвея гласит следующее.

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

Мелвин Конвей, Как комитеты изобретают? (Melvin Conway, How Do Committees Invent?, https://oreil.ly/NhE86)

Рис. 1.3. Внесение изменений на всех трех уровнях требует больших усилий

Трехуровневая архитектура представляет собой хороший пример этого закона в действии. В прошлом ИТ-организации в основном группировали работников по их ключевым компетенциям: администраторы баз данных были в команде с другими администраторами БД, разработчики Java — с другими разработчиками Java, а фронтенд-разработчики (которые сегодня знают такие экзотические вещи, как JavaScript, и занимаются разработкой собственных мобильных приложений) — в еще одной такой же команде. Мы объединяем сотрудников на основе их базовых компетенций, поэтому создаем ИТ-ресурсы, адаптированные к этим командам.

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

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

Сравним это с потенциальной альтернативной архитектурой, проиллюстрированной на рис. 1.4. Вместо горизонтальной многоуровневой архитектуры и организации — вертикальная. Здесь мы видим отдельную команду, которая несет полную сквозную ответственность за внесение изменений в профиль клиента. Это гарантирует, что объем преобразований в этом примере будет ограничен одной командой.

Рис. 1.4. Пользовательский интерфейс разделен на части и управляется командой, которая также управляет функциональностью, поддерживающей UI, но на стороне сервера

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

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

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

Часто UI не предоставляется непосредственно микросервисом, но даже если это так, мы ожидаем, что часть его, связанная с этой функциональностью, по-прежнему будет принадлежать команде, отвечающей за профиль клиента, как показано на рис. 1.4. Концепция создавать команды, владеющие сквозным набором функций, ориентированных на пользователя, набирает обороты. В книге «Топологии команд»5 представлена идея потоковой команды, которая воплощает эту концепцию.

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

Команды, показанные на рис. 1.4, были бы потоковыми. Эту концепцию мы рассмотрим более подробно в главах 14 и 15, в том числе обсудим, как эти типы организационной структуры работают на практике, и поговорим о степени их соответствия микросервисам.

Заметка о «выдуманных» компаниях

На протяжении всей книги мы будем встречаться с MusicCorp, FinanceCo, FoodCo, AdvertCo и PaymentCo.

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

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

Монолит

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

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

Однопроцессный монолит

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

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

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

Модульный монолит

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

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

Одна из проблем модульного монолита заключается в том, что базе данных, как правило, не хватает декомпозиции, которую мы находим на уровне кода, что приводит к значительным проблемам, если потребуется разобрать монолит в будущем. Я видел, как некоторые команды пытались развить идею модульного монолита дальше, разделив БД по границам разбиения модулей, как показано на рис. 1.8.

Рис. 1.7. В модульном монолите код процесса разделен на модули

Рис. 1.8. Модульный монолит с разделенной базой данных


Распределенный монолит

Распределенная система — это система, в которой сбой компьютера, о существовании которого вы даже не подозревали, может привести к сбою в работе вашего собственного компьютера8.

Лесли Лэмпорт

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

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

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

Монолиты и конфликт доставки

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

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

Преимущества монолитов

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

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

К сожалению, люди стали относиться к монолиту как к чему-то, чего следует избегать, — чему-то изначально проблематичному. Я встречал множество разработчиков, для которых термин «монолит» стал синонимом слова «устаревший» — и это проблема. Монолитная архитектура — это решение, и притом обоснованное. Более того, на мой взгляд, это стандартный и разумный выбор архитектурного стиля. Другими словами, назовите мне убедительную причину использовать микросервисы.

Попадая в ловушку систематического отрицания монолита как работающего варианта доставки ПО, мы рискуем поступить неправильно по отношению к себе или к пользователям нашего программного продукта.

Технологии, обеспечивающие развитие

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

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

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

Агрегирование логов и распределенная трассировка

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

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

Такие системы позволяют собирать и объединять логи из всех ваших сервисов, предоставляя возможности для анализа и включения журналов в активный механизм оповещения. Мне очень нравится сервис ведения логов Humio (https://www.humio.com), однако на первое время вам хватит чего-то попроще из того, что предоставляют основные поставщики публичных облачных сервисов.

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

По мере усложнения системы важно подобрать инструменты, позволя­ющие лучше изучить, что делает ваша система. Они предоставляют возможность анализировать трассировки между несколькими сервисами, находить узкие места и задавать о вашей системе вопросы, о которых вы даже не подозревали. Инструменты с открытым исходным кодом могут предоставить некоторые из этих функций. Одним из примеров может служить Jaeger (https://www.jaegertracing.io), фокусирующийся на стороне распределенной трассировки уравнения.

Но такие программные продукты, как Lightstep (https://lightstep.com) и Honey­comb (https://honeycomb.io) (показан на рис. 1.9), дают больше возможностей. Они представляют собой новое поколение инструментов, выходящих за рамки традиционных подходов к мониторингу, значительно упрощая изучение состояния работающей системы. Возможно, вы уже пользуетесь чем-то более привычным, но тем не менее я советую обратить внимание на возможности, предоставляемые этими продуктами. Они были созданы с нуля для решения тех проблем, с которыми приходится сталкиваться операторам микросервисных архитектур.

Рис. 1.9. Распределенная трассировка, показанная в Honeycomb, позволяет определить, где тратится время на операции, которые могут затрагивать несколько микросервисов

Контейнеры и Kubernetes

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

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

Однако не стоит спешить с внедрением Kubernetes или даже контейнеров. Они, безусловно, предлагают значительные преимущества по сравнению с более традиционными технологиями развертывания, но в них нет особого смысла, если у вас всего несколько микросервисов. После того как накладные расходы на управление развертыванием перерастут в серьезную головную боль, начните рассматривать возможность контейнеризации своего сервиса и использования Kubernetes. И если вы решитесь на этот шаг, сделайте все возможное, чтобы кто-то другой управлял кластером Kubernetes вместо вас, пусть даже и с использованием управляемого сервиса от облачного провайдера. Запуск собственного кластера Kubernetes может потребовать значительного объема работы!

Потоковая передача данных

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

Для многих людей Apache Kafka (https://kafka.apache.org) — стандартный выбор для потоковой передачи информации в среде микросервисов, и на то есть веские причины. Такие возможности, как постоянство сообщений, сжатие и способность масштабирования для обработки больших объемов сообщений, могут быть невероятно полезными. С появлением Kafka возникла возможность потоковой обработки в виде базы данных KSQLDB, которую также можно использовать с выделенными решениями для потоковой обработки, такими как Apache Flink (https://flink.apache.org).

Debezium (https://debezium.io) — это инструмент с открытым исходным кодом, разработанный для потоковой передачи из существующих источников данных через Kafka. Такой способ передачи гарантирует, что традиционные источники данных могут стать частью потоковой архитектуры. В главе 4 мы рассмотрим, какую роль технология потоковой передачи может сыграть в интеграции микросервисов.

Публичное облако и бессерверный подход

Поставщики (провайдеры) публичных (общедоступных) облачных сервисов, или, точнее, три основных поставщика — Google Cloud, Microsoft Azure и Amazon Web Services (AWS), — предлагают огромный набор управляемых сервисов и вариантов развертывания для управления вашим приложением. По мере роста микросервисной архитектуры работа все больше и больше будет переноситься в эксплуатационное пространство. Провайдеры облаков предоставляют множество услуг: от управляемых экземпляров баз данных или кластеров Kubernetes до брокеров сообщений или распределенных файловых систем. Используя эти управляемые сервисы, вы перекладываете большой объем работы на третью сторону, которая, возможно, лучше справится с такими задачами.

Особый интерес среди облачных предложений представляют программные продукты, которые позиционируются как бессерверные. Они скрывают базовые машины, позволяя вам работать на более высоком уровне абстракции. Примерами программных продуктов с бессерверной стратегией могут служить брокеры сообщений, решения для хранения данных и БД. Платформы «функция как услуга» (FaaS, function as a service) представляют особый интерес, поскольку обеспечивают хорошую абстракцию вокруг развертывания кода. Не беспокоясь о том, сколько серверов требуется для запуска вашего сервиса, вы просто развертываете код и позволяете базовой платформе запускать его экземпляры по требованию. Мы рассмотрим бессерверный подход более по­дробно в главе 8.

Преимущества микросервисов

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

Технологическая неоднородность

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

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

Рис. 1.10. Микросервисы могут упростить использование различных технологий

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

Конечно, использование различных технологий не обходится без накладных расходов. Некоторые компании предпочитают устанавливать определенные ограничения на выбор языка. Например, Netflix и Twitter в основном в качестве платформы используют виртуальную машину Java (JVM, Java Virtual Machine) из-за ее надежности и производительности. Они также разрабатывают библиотеки и инструменты для JVM, которые действительно очень полезны. Но зависимость от них усложняет работу сервисов или клиентов, не базирующихся на Java. Однако ни Twitter, ни Netflix не используют только один технологический стек для всех задач.

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

Надежность

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

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

Масштабирование

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

Интернет-магазин модной одежды Gilt внедрил микросервисы именно по этой причине. В 2007 году компания начала с монолитного приложения Rails, но к 2009 году система Gilt не смогла справиться с нагрузкой. Разделив основные части своей системы, компания сумела решить проблему со скачками трафика, и сегодня в этой системе более 450 микросервисов, каждый из которых работает на нескольких отдельных машинах.

Рис. 1.11. Можно сделать целью масштабирования только те микросервисы, которые в нем нуждаются

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

В конечном счете можно масштабировать свои приложения множеством способов, и микросервисы — эффективная часть этого процесса. Мы рассмотрим масштабирование микросервисов более подробно в главе 13.

Простота развертывания

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

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

Согласованность рабочих процессов в организации

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

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

Компонуемость

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

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

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

Слабые места микросервисов

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

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

Опыт разработчика

По мере того как у вас появляется все больше и больше сервисов, отношение разработчиков к ним может начать ухудшаться. Более ресурсоемкие среды выполнения (например, JVM) способны ограничить количество микросервисов, допущенных к запуску на одной машине. Вероятно, я мог бы запустить четыре или пять микросервисов на основе JVM как отдельные процессы на своем ноутбуке, но могу ли я запустить 10 или 20? Скорее всего, нет. Даже при меньшем времени выполнения будет ограничение на количество процессов, доступных для локального запуска. Это неизбежно приведет к невозможности запустить всю систему на одной машине. Ситуация осложнится еще больше при использовании облачных сервисов, которые нельзя запустить локально.

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

Технологическая перегрузка

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

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

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

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

Стоимость

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

Во-вторых, любые изменения, вносимые в команду или организацию, замедлят процесс работы. Чтобы изучить новые идеи и понять, как их эффективно использовать, потребуется время. Пока вы будете заняты этим, иные задачи никуда не денутся. Это приведет либо к прямому замедлению рабочего процесса, либо к необходимости добавить больше сотрудников, чтобы компенсировать эти затраты.

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

Отчетность

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

Рис. 1.12. Отчетность ведется непосредственно по базе данных монолита

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

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

Мониторинг и устранение неполадок

В стандартном монолитном приложении допускается использовать довольно упрощенный подход к мониторингу. У нас есть небольшое количество машин, о которых необходимо позаботиться, а режим сбоя приложения в некотором роде бинарный — приложение обычно либо работает, либо нет. Понимаем ли мы последствия для архитектуры микросервисов, если выйдет из строя хотя бы один экземпляр сервиса?

В монолите, если центральный процессор (ЦП) долгое время зависает при 100%-ной нагрузке, становится ясно: в системе большая проблема. Можно ли сказать то же самое о микросервисной архитектуре с десятками или сотнями процессов? Стоит ли будить кого-то в 3 часа ночи, когда один процесс застрял на 100%-ной нагрузке ЦП?

К счастью, в этом случае есть огромное пространство для маневров. Если вы хотите изучить эту концепцию более подробно, то в качестве отправной точки рекомендую книгу Distributed Systems Observability за авторством Cindy Sridharan (O’Reilly), хотя мы тоже еще рассмотрим мониторинг и наблюдаемость в главе 10.

Безопасность

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

Тестирование

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

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

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

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

Время ожидания

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

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

Согласованность данных

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

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

Стоит ли вам использовать микросервисы?

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

Тем не менее я хочу обрисовать несколько ситуаций, которые обычно склоняют меня к выбору микросервисов и наоборот.

Кому микросервисы не подойдут

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

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

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

Камнем преткновения в вопросе микросервисов для стартапов является весьма ограниченное количество человеческих ресурсов. Для небольшой команды может быть трудно обосновать использование рассматриваемой архитектуры из-за проблем с развертыванием и управлением самими микросервисами. Некоторые называют это «налогом на микросервисы». Когда эти инвестиции приносят пользу большему количеству людей, их легче оправдать. Но если в команде из пяти человек один тратит свое время на решение этих проблем, это означает, что создание вашего продукта замедляется. Гораздо проще перейти к микросервисам позже, когда вы поймете, где находятся ограничения в архитектуре и каковы слабые места системы, — тогда вы сможете сосредоточить свою энергию на внедрении микросервисов.

Наконец, испытывать трудности с микросервисами могут и организации, создающие ПО, которое будет развертываться и управляться их клиентами. Как я уже говорил, архитектуры микросервисов могут значительно усложнить процесс развертывания и эксплуатации. Если вы используете программное обеспечение самостоятельно, можете компенсировать это, внедрив новые технологии, развив определенные навыки и изменив методы работы. Но не стоит ожидать подобного от своих клиентов, которые привыкли получать ваше ПО в качестве установочного пакета для Windows. Для них будет шоком, если вы отправите следующую версию своего приложения и скажете: «Просто поместите эти 20 подов в свой кластер Kubernetes!» Скорее всего, они даже не представляют, что такое под, Kubernetes или кластер.

Где микросервисы хорошо работают

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

Приложения типа «программное обеспечение как услуга» (software as a service, SaaS) также хорошо подходят для архитектуры микросервисов. Обычно такие продукты работают 24/7, что создает проблемы, когда дело доходит до внедрения изменений. Возможность независимого выпуска микросервисных архитектур предоставляет огромное преимущество. Кроме того, микросервисы по мере необходимости можно увеличить или уменьшить. Это означает, что по мере того, как вы устанавливаете базовый уровень нагрузки вашей системы, вы наиболее экономичным способом получаете больше контроля над обеспечением возможности масштабирования.

Благодаря технологически независимой природе микросервисов вы сможете получить максимальную отдачу от облачных платформ. Провайдеры публичных облачных сервисов предоставляют широкий спектр услуг и механизмов развертывания для вашего кода. Вам гораздо проще сопоставить требования конкретных служб с облачными сервисами, которые наилучшим образом помогут вам реализовать ваши задачи. Например, вы можете решить развернуть один сервис как набор функций, другой — как управляемую виртуальную машину (VM), а третий — на управляемой платформе «платформа как услуга» (platform as a service, PaaS).

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

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

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

Резюме

Микросервисные архитектуры могут предоставить огромную степень гибкости в выборе технологий, обеспечении надежности и масштабировании, организации команд и многом другом. Эта гибкость отчасти объясняет, почему многие люди используют микросервисные архитектуры. Но микросервисы влекут за собой значительные издержки, и вам необходимо убедиться, что они оправданны. Для многих микросервисы стали системной архитектурой по умолчанию, которую можно использовать практически во всех ситуациях. Тем не менее я по-прежнему считаю, что этот архитектурный выбор должен быть оправдан поставленными целями. Часто наименее сложные подходы могут обеспечить более легкое достижение результата.

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

Я надеюсь, что эта глава послужила хорошим введением. Далее мы рассмотрим, как лучше определять границы микросервиса, попутно исследуя темы структурированного программирования и предметно-ориентированного проектирования.


1 Parnas D. Information Distribution Aspects of Design Methodology (https://oreil.ly/rDPWA) // Information Processing: Proceedings of the IFIP Congress 1971. — Amsterdam: North-Holland, 1972. — Р. 339–344.

2 Cockburn A. Hexagonal Architecture, 4 января 2005 года. https://oreil.ly/NfvTP.

3 Более подробно о предметно-ориентированном проектировании рассказано в книге Эрика Эванса «Предметно-ориентированное проектирование», а более кратко — в книге «Дистиллированное предметно-ориентированное проектирование» Вона Вернона.

4 Ричардсон К. Микросервисы. Паттерны разработки и рефакторинга. — Питер, 2019.

5 Skelton M., Pais M. Team Topologies. — Portland, OR: IT Revolution, 2019.

6 Hansson D.H. The Majestic Monolith // Signal v. Noise, 29 февраля 2016 года. https://oreil.ly/WwG1C.

7 Для получения полезной информации о том, как Shopify использует модульный монолит, а не микросервисы, см.: Westeinde K. Deconstructing the Monolith.

8 Лесли Лэмпорт, сообщение по электронной почте (https://oreil.ly/2nHF1) на доску объявлений DEC SRC в 12:23:29 PDT, 28 мая 1987 года.

9 Microsoft Research провела исследования в этой области, и я рекомендую узнать о них подробнее, но в качестве отправной точки предлагаю статью Don’t Touch My Code! Examining the Effects of Ownership on Software Quality за авторством Christian Bird (https://oreil.ly/0ahXX).

Микросервисы используют концепцию скрытия информации1. Это означает скрытие как можно большего количества информации внутри компонента и как можно меньшее ее раскрытие через внешние интерфейсы. Так можно провести четкую границу между легко и сложно изменяемыми данными. Реализацию, скрытую от сторонних участников процесса, можно свободно преобразовывать, пока у сетевых интерфейсов, предоставляемых микросервисом, сохраняется обратная совместимость. Изменения внутри границ микросервиса (как показано на рис. 1.1) не должны влиять на вышестоящего потребителя, обеспечивая возможность независимого выпуска функциональных возможностей. Это необходимо для того, чтобы микросервисы могли работать изолированно и выпускаться по требованию. Наличие четких, стабильных границ сервисов, не изменяющихся при преобразовании внутренней реализации, приводит к тому, что системы получают более слабую связанность (coupling) и более сильную связность (cohesion).

Распределенная система — это система, в которой сбой компьютера, о существовании которого вы даже не подозревали, может привести к сбою в работе вашего собственного компьютера8.

Более подробно о предметно-ориентированном проектировании рассказано в книге Эрика Эванса «Предметно-ориентированное проектирование», а более кратко — в книге «Дистиллированное предметно-ориентированное проектирование» Вона Вернона.

Ричардсон К. Микросервисы. Паттерны разработки и рефакторинга. — Питер, 2019.

Parnas D. Information Distribution Aspects of Design Methodology (https://oreil.ly/rDPWA) // Information Processing: Proceedings of the IFIP Congress 1971. — Amsterdam: North-Holland, 1972. — Р. 339–344.

Cockburn A. Hexagonal Architecture, 4 января 2005 года. https://oreil.ly/NfvTP.

Microsoft Research провела исследования в этой области, и я рекомендую узнать о них подробнее, но в качестве отправной точки предлагаю статью Don’t Touch My Code! Examining the Effects of Ownership on Software Quality за авторством Christian Bird (https://oreil.ly/0ahXX).

Для получения полезной информации о том, как Shopify использует модульный монолит, а не микросервисы, см.: Westeinde K. Deconstructing the Monolith.

Лесли Лэмпорт, сообщение по электронной почте (https://oreil.ly/2nHF1) на доску объявлений DEC SRC в 12:23:29 PDT, 28 мая 1987 года.

Skelton M., Pais M. Team Topologies. — Portland, OR: IT Revolution, 2019.

Hansson D.H. The Majestic Monolith // Signal v. Noise, 29 февраля 2016 года. https://oreil.ly/WwG1C.

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

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

Пока мы говорим о скрытии деталей внутренней реализации, с моей стороны было бы упущением не упомянуть шаблон гексагональной архитектуры, впервые подробно описанный Алистером Кокберном2. Этот шаблон определяет важность сохранения внутренней реализации отдельно от ее внешних интерфейсов, поскольку вы, возможно, захотите взаимодействовать с одной и той же функцио­нальностью через разные типы интерфейсов. Я изображаю свои микросервисы в виде шестиугольников (гексагонов) отчасти для того, чтобы отличить их от «обычных» сервисов, но также по причине моей любви к этим фигурам.

Часто UI не предоставляется непосредственно микросервисом, но даже если это так, мы ожидаем, что часть его, связанная с этой функциональностью, по-прежнему будет принадлежать команде, отвечающей за профиль клиента, как показано на рис. 1.4. Концепция создавать команды, владеющие сквозным набором функций, ориентированных на пользователя, набирает обороты. В книге «Топологии команд»5 представлена идея потоковой команды, которая воплощает эту концепцию.

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

Крис Ричардсон, автор книги «Микросервисы. Паттерны разработки и рефакторинга»4, говорит, что цель микросервисов — получить «как можно меньший интерфейс». Это снова согласуется с концепцией скрытия информации, но представляет собой попытку найти смысл в термине «микросервисы», которого изначально не было. Когда этот термин впервые использовался для определения архитектур, основное внимание, по крайней мере на начальном этапе, уделялось не размеру интерфейсов.

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

Глава 2. Как моделировать микросервисы

Мой оппонент в своих рассуждениях напоминает мне язычника, который, когда его спросили, на чем стоит мир, ответил: «На черепахе». — «Но на чем стоит черепаха?» — «На другой черепахе».

Преподобный Джозеф Фредерик Берг (1854)

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

Представляем MusicCorp

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

Итак, представьте себе современный интернет-магазин MusicCorp. Компания MusicCorp до недавнего времени была традиционным розничным магазином, но после выхода из бизнеса по продаже виниловых пластинок она все больше и больше сосредотачивала свои усилия в Интернете. У фирмы есть веб-сайт, но ее руководство считает, что сейчас самое время удвоить усилия в онлайн-мире. В конце концов, эти смартфоны для прослушивания музыки — всего лишь мимолетное увлечение (очевидно, что портативный плеер Zune намного лучше), и меломаны с нетерпением ждут, когда им доставят их любимые пластинки. Качество важнее удобства, верно? И хотя, возможно, только недавно стало известно, что Spotify на самом деле является цифровым музыкальным сервисом, а не каким-то средством для ухода за кожей для подростков, компания MusicCorp вполне довольна своей собственной направленностью и уверена, что весь этот стриминговый бизнес скоро прогорит.

Несмотря на то что MusicCorp немного отстает от жизни, у нее большие планы. К счастью, в компании решили, что отличный шанс захватить рынок — убедиться, что есть возможность вносить изменения самым простым способом. Микросервисы для победы!

Что делает границу микросервиса качественной

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

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

Скрытие информации

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

Ускоренное время разработки

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

Понятность

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

Гибкость

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

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

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

В другой статье Парнаса12 мы нашли такую цитату: «Связи между модулями — это предположения, которые модули делают друг о друге».

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

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

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

Связность

Самое краткое объяснение, которое я слышал для описания связности13, звучит так: «Код, в который вносят изменения как в единое целое, остается единым целым». Для наших целей это довольно хорошее определение. Как обсуждалось ранее, мы оптимизируем нашу микросервисную архитектуру с учетом простоты преобразований бизнес-функциональности. Поэтому нам требуется функциональность, сгруппированная таким образом, чтобы иметь возможность вносить изменения в как можно меньшем количестве мест.

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

Связанность

Когда между сервисами наблюдается слабая связанность14, изменения, вносимые в один сервис, не требуют изменений в другом. Для микросервиса самое главное — возможность внесения изменений в один сервис и его развертывания без необходимости вносить изменения в любую другую часть системы. И это действительно очень важно.

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

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

Взаимодействие связанности и связности

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

Структура стабильна, если связность сильная, а связанность слабая15.

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

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

Помните, что мир не статичен — вполне возможно, что по мере изменения системных требований найдутся причины пересмотреть принятые решения. Иногда части системы могут претерпевать столь значительные преобразования, что стабильность может стать невозможной. Мы рассмотрим такой пример в гла­ве 3, когда я поделюсь опытом разработчиков, стоящих за системой Snap CI.

Типы связанности

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

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

Уровень техники в области структурированного программирования

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

Если говорить о создании новых фрагментов систем на основе предыдущих работ, в этой книге есть несколько предметных областей, у которых такой же предшествующий уровень техники, как у структурированного программирования. Книга Ларри Константина, написанная в соавторстве с Эдвардом Йордоном, считается одной из самых важных в этой области. Также советую книгу Мейлира Пейджа-Джонса «Практическое руководство по проектированию структурированных систем». К сожалению, обе книги сложно достать, поскольку тиражи уже давно распроданы, а электронные варианты не выпускались.

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

На рис. 2.1 представлен краткий обзор различных типов связанности, организованных от слабых (желательно) до сильных (нежелательно).

Рис. 2.1. Различные типы связанностей, от слабых (низких) до сильных (высоких)

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

Предметная связанность

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

На рис. 2.2 показана часть того, как заказы на компакт-диски управляются внутри MusicCorp. В этом примере Обработчик заказа вызывает микросервис Склад для резервирования на складе, а микросервис Оплата принимает оплату. Следовательно, Обработчик заказа в этой операции зависит от микросервисов Склад и Оплата и связан с ними. Между сервисами Склад и Оплата нет такой связи, поскольку они не взаимодействуют.

Рис. 2.2. Пример доменной связи, где сервису Обработчик заказов необходимо использовать функциональность, предоставляемую другими микросервисами

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

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

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

Примечание о временной связанности

Другая достаточно известная форма связанности — временная. С точки зрения связи, ориентированной на код, временная связанность относится к ситуации, в которой понятия объединяются вместе только потому, что они происходят одновременно. Временная связь в контексте распределенной системы имеет немного другое значение: одному микросервису требуется, чтобы другой микросервис выполнял что-то в то же время.

Для завершения операции оба микросервиса должны быть запущены и доступны для одновременной связи друг с другом. Итак, на рис. 2.3, где Обработчик заказов MusicCorp выполняет синхронный HTTP-вызов сервиса Склад, сервис Склад должен быть запущен и доступен одновременно с вызовом.

Рис. 2.3. Пример временной связи, при которой Обработчик заказов выполняет синхронный HTTP-вызов микросервиса Склад

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

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

Сквозная связанность

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

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

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

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

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

Рис. 2.5. Один из способов обойти сквозную связь заключается в непосредственном взаимодействии с нижестоящим сервисом

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

Рис. 2.6. Скрытие необходимости в Путевом листе от Обработчика заказов

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

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

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

Рис. 2.7. Несколько сервисов, получающих доступ к общим статическим справочным данным

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

Общая связанность

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

Основная проблема систем с общей связанностью заключается в том, что изменения в структуре данных могут повлиять на несколько микросервисов одновременно. Рассмотрим пример некоторых сервисов MusicCorp на рис. 2.7. Как мы условились ранее, MusicCorp работает по всему миру, поэтому компании требуется различная информация о странах, в которых она зарегистрирована. Здесь все множество сервисов считывает статические справочные данные из общей БД. Если схема этой базы изменится обратно несовместимым образом, это потребует преобразований для каждого потребителя БД. На практике подобные общие данные, как правило, очень трудно изменить.

На рис. 2.8 показана ситуация, в которой сервисы Обработчик заказов и Склад считывают и записывают данные из общей таблицы Заказ, чтобы помочь управлять процессом отправки компакт-дисков клиентам MusicCorp. Оба микросервиса обновляют столбец Статус. Обработчик заказов может задать статусы РАЗМЕЩЕН, ОПЛАЧЕН и ЗАВЕРШЕН, в то время как Склад задает статусы СОБИРАЕТСЯ или ОТПРАВЛЕН.

Рис. 2.8. Пример общей связанности, при которой сервисы Обработчик заказов и Склад обновляют одну и ту же запись заказа

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

Гарантией того, что состояние чего-либо изменяется правильным образом, было бы создание конечного автомата. Конечный автомат может использоваться для управления переходом некоторого объекта из одного состояния в другое, запрещая недопустимые переходы состояний. На рис. 2.9 можно увидеть разрешенные переходы состояний для заказа в MusicCorp. Заказ может перейти непосредственно из статуса РАЗМЕЩЕН в статус ОПЛАЧЕН, но не напрямую из статуса РАЗМЕЩЕН к СОБИРАЕТСЯ (этого конечного автомата, скорее всего, будет недостаточно для реальных бизнес-процессов, выполняемых при покупке и доставке товаров, но смысл этого простого примера — проиллюстрировать идею).

Рис. 2.9. Обзор допустимых переходов статусов для заказа в MusicCorp

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

Потенциальным решением здесь было бы обеспечить состояние, при котором один микросервис управлял бы заказом. На рис. 2.10 отправлять запросы на обновление статуса заказа в сервисе Заказ могут микросервисы Склад или Обработчик заказов. Здесь сервис Заказ будет источником истины для любого размещенного заказа. В этой ситуации действительно важно, чтобы мы рассматривали запросы от сервисов Склад и Обработчик заказов именно как запросы. Задачей сервиса Заказ станет управление допустимыми переходами статусов, целиком связанными с заказом. Таким образом, если сервис Заказ получит запрос от Обработчика заказов на изменение статуса с РАЗМЕЩЕН непосредственно на ЗАВЕРШЕН, сервис заказа вправе отклонить заявку, если такое изменение недопустимо.

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

Альтернативным подходом в данном случае будет реализация сервиса Заказ в виде чего-то большего, чем просто оболочка для операций CRUD с базой данных, где запросы сопоставляются непосредственно с обновлениями БД. Это похоже на объект, содержащий приватные поля, в котором публичные геттеры и сеттеры просочились из микросервиса к вышестоящим потребителям (снижение связности), и мы вернулись в мир управления приемлемыми переходами состояний между несколькими различными сервисами.

Рис. 2.10. Сервисы Обработчик заказов и Склад могут выполнить запрос на изменения в заказе, но решение о том, какие запросы допустимы, принимает микросервис Заказ

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

Источники общей связанности также являются потенциальными виновниками конкуренции за ресурсы. Множество микросервисов, использующих одну и ту же файловую систему или базу данных, могут перегружать этот ресурс, вызывая серьезные последствия при его замедлении или недоступности. Общая БД особенно подвержена такой проблеме из-за возможной подачи к ней произвольных запросов различной сложности. Я видел не одну базу данных, поставленную на колени ресурсоемким SQL-запросом, — возможно, я даже был виновником такой ситуации один или два раза18.

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

Связанность по содержимому

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

Вернемся к нашему предыдущему примеру MusicCorp. На рис. 2.11 показан сервис Заказ, который управляет допустимыми изменениями статусов заказов в нашей системе. Обработчик заказов отправляет запросы сервису Заказ, делегируя не только право на изменение статуса, но и ответственность за принятие решения о том, какие переходы статусов допустимы. С другой стороны, Склад напрямую обновляет таблицу, в которой хранятся данные заказа, минуя любые функции сервиса Заказ, способные проверять допустимые преобразования. Мы должны надеяться, что сервис Склад содержит согласованный набор логики, гарантирующий внесение только разрешенных изменений. В лучшем случае логика продублируется. В худшем — мы можем получить заказы в очень необычных местах.

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

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

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

Рис. 2.11. Пример связанности по содержимому, при которой Склад получает прямой доступ к внутренним данным сервиса Заказ

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

Немного предметно-ориентированного проектирования

В главе 1 описан основной механизм, используемый для определения границ микросервисов. Процесс вращается вокруг самой предметной области, и предметно-ориентированное проектирование (DDD, domain-driven design) применяется, чтобы помочь создать ее модель. Давайте теперь расширим понимание того, как DDD работает в контексте микросервисов.

Разумеется, все разработчики стремятся к тому, чтобы их программы лучше отражали реальный мир, в котором они будут работать. Объектно-ориентированные языки программирования, такие как Simula, были разработаны именно для моделирования реальных предметных областей. Но для того, чтобы идея отражения реального мира действительно обрела форму, требуется нечто большее, чем возможности языка программирования.

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

Единый язык

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

Агрегат

Набор объектов, управляемых как единое целое, обычно ссылающихся на концепции реального мира.

Ограниченный контекст

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

Единый язык

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

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

С другой стороны, в коде не было ничего из этого. В какой-то момент было принято решение использовать стандартную модель данных для БД. В широком смысле эта модель характеризовалась термином «банковская модель IBM», но я не мог понять, был ли это стандартный продукт IBM или просто творение консультанта из IBM. Определяя расплывчатое понятие «соглашения», тео­рия утверждала, что можно смоделировать любую банковскую операцию. Взять кредит? Для этого было соглашение. Покупка акции? Для этого было соглашение! Подача заявки на получение кредитной карты? Вы, наверное, уже догадались!

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

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

Агрегат

В DDD агрегат (aggregate) несколько запутанное понятие, имеющее множество различных определений. Что это? Просто произвольный набор объектов? Самая маленькая единица извлечения из базы данных? Модель, всегда работавшая в моем представлении, предполагает рассмотрение агрегата в качестве понятия из реальной предметной области — подумайте о чем-то вроде заказа, инвойса, единицы хранения и т.д. Жизненный цикл агрегатов обычно связан с этими понятиями, что позволяет реализовать агрегат в виде конечного автомата.

Например, в предметной области MusicCorp агрегат Заказ может содержать несколько позиций, представляющих товары в заказе. Эти позиции имеют значение только как часть общего агрегата заказов.

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

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

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

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

Одни агрегаты могут взаимодействовать с другими. На рис. 2.12 агрегат Покупатель связан с одним или несколькими агрегатами Заказ и Список избранного. Эти агрегаты могут управляться одним и тем же микросервисом или разными.

Рис. 2.12. Один агрегат Покупатель может быть связан с одним или несколькими агрегатами Заказ или Список избранного

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

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

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

На рис. 2.13 мы кое-что изменили, чтобы сделать взаимосвязь явной. Вместо обычного идентификатора для ссылки на клиента мы храним URI, который будет использоваться при создании системы на основе REST20.

Рис. 2.13. Пример того, как может быть реализована взаимосвязь между двумя агрегатами в разных микросервисах

Преимущества такого подхода двояки. Связь явная, и в системе REST мы могли бы напрямую переназначить URI для поиска связанного ресурса. Но что, если вы не создаете систему REST? Фил Кальсадо описывает вариант этого подхода, используемый в SoundCloud21, где они разработали схему псевдо-URI для межсервисных ссылок. Например, в soundcloud:tracks:123 будет ссылка на композицию с идентификатором 123. Эта запись более понятна для человека, смотрящего на идентификатор, и достаточно полезна для создания кода, который мог бы облегчить агрегированный поиск между микросервисами.

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

Ограниченный контекст

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

На складе MusicCorp происходит много всего: управление отгрузками (и непонятным возвратом), прием новых товаров, гонки на вилочных погрузчиках и т.д. Работа в финансовом отделе, возможно, менее увлекательна, но все же он выполняет важную функцию в нашей организации, занимаясь расчетом заработной платы, оплатой поставок и т.п.

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

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

Вернемся к бизнесу MusicCorp. Наша предметная область — это весь бизнес, в котором мы работаем. Она охватывает все: от склада до приемной, от финансов до заказа. Мы можем как моделировать все это в нашем программном обеспечении, так и не моделировать, но тем не менее это предметная область, в которой мы работаем. Давайте подумаем об отдельных ее частях, выглядящих как ограниченные контексты, на которые ссылается Эрик Эванс.

Скрытые модели

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

Финансовому отделу не нужно знать о подробностях внутренней работы склада. Однако ему все же необходима кое-какая информация, например об уровне запасов, чтобы поддерживать счета в актуальном состоянии. На рис. 2.14 показана примерная контекстная диаграмма. На ней видны внутренние складские концепции, такие как комплектовщик (тот, кто собирает заказы), стеллажи, тележки и т.д. Аналогичным образом записи в гроссбухе представляют собой неотъемлемую часть финансовых расчетов, но здесь они не сообщаются с внешним миром.

Рис. 2.14. Общая модель взаимодействия финансового отдела и склада

Однако, чтобы получить возможность оценить стоимость компании, сотрудникам финансового отдела нужна информация о складских запасах. Затем единица хранения становится общей моделью для двух контекстов. Обратите внимание, что нам не нужно слепо раскрывать все о единице хранения на складе из контекста склада. На рис. 2.15 показано, как Единица хранения внутри ограниченного контекста склада содержит ссылки на места хранения, имея при этом только сведения о количестве. Таким образом, существует исключительно внутреннее и внешнее представление, которое мы раскрываем. Часто, когда имеются оба вида представления, может быть полезно называть их по-разному, чтобы избежать путаницы, например назвать общую Единицу храненияКоличеством запасов.

Рис. 2.15. Общая модель может решить скрыть информацию, которая не должна передаваться во внешний мир

Общая модель

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

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

Сопоставление агрегатов и ограниченных контекстов с микросервисами

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

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

Черепахи — и нет им конца22

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

Вся хитрость в том, что, даже если вы решите позже разделить сервис, моделирующий весь ограниченный контекст, на более мелкие сервисы, вы все равно сможете скрыть это действие от внешних потребителей, например представив им общий крупномодульный API. Решение разбить сервис, возможно, станет шагом к реализации, поэтому по возможности его надо также скрыть. На рис. 2.16 показан такой пример. Мы разделили Склад на Запасы и Доставку. Что касается внешних потребителей, то для них по-прежнему существует только микросервис Склад. Однако внутренне мы еще больше разбили процессы, чтобы позволить сервису Запасы управлять Единицами хранения и позволить Доставке управлять Отгрузками. Помните, что мы хотим сохранить принадлежность одного агрегата одному микросервису.

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

Рис. 2.16. Сервис Склад внутренне был разделен на микросервисы Запасы и Доставка

Вложенный подход стоит также предпочесть при разделении архитектуры на фрагменты для упрощения тестирования. Например, при тестировании сервисов, использующих склад, не требуется заключать каждый сервис внутри контекста хранилища — только более общий API. Это может дать вам единицу изоляции при рассмотрении крупномасштабных тестов. Я могу, например, решить провести сквозные тесты, в которых будут запущены все сервисы внутри контекста хранилища, но для остальных сотрудников я могу их отключить. Подробнее о тестировании и изоляции мы поговорим в главе 9.

Метод Event Storming

Event Storming (иногда называют штурмом событий) — метод, разработанный Альберто Брандолини и представляющий собой практику мозгового штурма, призванную помочь выявить модель предметной области. Метод Event Storming позволяет привлечь к совместному обсуждению технических и нетехнических специалистов. Идея заключается в превращении разработки модели предметной области в совместную деятельность, в результате которой вы получаете общий, объединенный взгляд на задачу.

На текущем этапе стоит упомянуть, что, хотя модели предметной области, определенные в процессе Event Storming, могут применяться для реализации систем, управляемых событиями, — и действительно, сопоставление очень простое, — вы также можете использовать такую модель предметной области для построения системы, более ориентированной на принцип «запрос — ответ».

Логистика

У Альберто есть несколько очень интересных идей относительно порядка проведения Event Storming, и с некоторыми из них я полностью согласен. Сначала соберите всех сотрудников в одном помещении. Часто это самый трудный шаг: согласование расписания, поиск достаточно большого помещения. Все эти проблемы были актуальны в мире и до COVID, но во время карантина эта задача может стать еще более сложной. Однако присутствие всех сотрудников одновременно — ключевой момент. Необходимы представители всех сфер предметной области, которую вы планируете моделировать: пользователи, эксперты по предметной области, владельцы продуктов и т.д.

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

Процесс

Упражнение начинается с определения событий предметной области. Этим событиям, происходящим в системе, уделяется больше всего внимания. «Заказ размещен» могло бы стать событием, к которому мы проявляли интерес в контексте MusicCorp, как и «Оплата получена». Они запечатлены на оранжевых стикерах. Именно в этот момент я снова не согласен с Альберто. События — это чуть ли не самое многочисленное, что необходимо фиксировать, а оранжевых стикеров не так уж и много23.

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

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

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

Конечно, я привел лишь краткий обзор метода Event Storming. Для более подробного ознакомления я бы посоветовал вам прочитать одноименную книгу Альберто Брандолини24.

Аргументы в пользу предметно-ориентированного проектирования микросервисов

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

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

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

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

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

Альтернативы границам предметной области бизнеса

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

Волатильность

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

Идея, лежащая в основе декомпозиции на базе волатильности, проявляется и в бимодальных ИТ-моделях. Концепция бимодальной ИТ-модели, предложенная компанией Gartner, четко разделяет мир на категории с краткими названиями «режим 1» (или «системы учета») и «режим 2» (или «системы инноваций») в зависимости от скорости работы различных систем. Системы режима 1 мало меняются и не требуют серьезного участия бизнеса, а в режиме 2 происходит действие с системами, требующими быстрых изменений и тесного вовлечения бизнеса. Если отбросить резкое упрощение, такая схема подразуме­вает очень фиксированный взгляд на мир и противоречит очевидным во всей отрасли трансформациям, поскольку компании стремятся «перейти на цифровые технологии». У различных компаний есть системы обработки бизнес-процессов. Некоторые части этих систем ранее не нуждались в значительных преобразованиях, а теперь внезапно меняются для соответствия потребностям рынка.

Давайте вернемся к компании MusicCorp. Ее первым шагом на пути к цифровым технологиям было просто создание веб-страницы. Все, что она предлагала еще в середине девяностых, — список выставленного на продажу. Однако для оформления заказа необходимо было звонить в MusicCorp. Напоминало объявление в газете. Затем онлайн-заказы стали обычным делом, и весь ассортимент, который до этого момента обрабатывался только на бумаге, пришлось оцифровывать. Кто знает, возможно, MusicCorp на каком-то этапе придется подумать и о продажах музыки в электронном виде! Можете оценить масштаб потрясений, через которые проходят фирмы во время технологических преобразований.

Мне не нравится бимодальная ИТ-модель как концепция, поскольку она дает людям возможность упаковать то, что трудно изменить, в красивую аккуратную коробку и сказать: «Нам не нужно разбираться с проблемами там — это режим 1». Это еще одна модель, которую компании могут принять, чтобы объяснить, почему они не меняются. Ведь довольно часто изменения в функциональности требуют преобразований в системах учета (режим 1), чтобы можно было учесть изменения в системах инноваций (режим 2). По моему опыту, организации, внедряющие бимодальные ИТ-модели, в конечном счете получают два режима — медленный и еще медленнее.

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

Данные

Характер данных, которые вы храните и которыми управляете, подталкивает к различным формам декомпозиции. Например, может потребоваться внести ограничения, чтобы определить, какие сервисы могут обрабатывать личную информацию (PII). Это позволит снизить риск утечки и упростит контроль данных, а также поспособствует соблюдению таких регламентов как GDPR.

Для одного из моих недавних заказчиков — платежной компании, назовем ее PaymentCo, — использование определенных типов данных напрямую повлияло на принимаемые решения в отношении декомпозиции. PaymentCo обрабатывает данные кредитных карт. Поэтому ее система должна соответствовать различным требованиям, установленным стандартами индустрии платежных карт (PCI, payment card industry) в отношении управления этими данными. У PaymentCo была необходимость обрабатывать полные данные данные кредитных карт в объеме, соответствующем PCI уровня 1. Это самый строгий уровень, и он требует ежеквартальной внешней оценки систем и методов, связанных с управлением данными.

Многие требования PCI логичны, но сложно обеспечить соответствие всей системы этим условиям, а необходимость проведения аудита кажется довольно обременительной. В результате компания захотела выделить часть системы, обрабатывающую полные данные кредитных карт, чтобы только ее обеспечить дополнительным уровнем контроля. На рис. 2.17 показана упрощенная форма придуманного нами проекта. Сервисы, работающие в зеленой зоне, не видят информации о кредитной карте — эти данные ограничены процессами (и сетями) в красной зоне. Шлюз перенаправляет вызовы на соответствующие сервисы (и в соответствующую зону). Так как информация о кредитной карте проходит через этот шлюз, она фактически также находится в красной зоне.

Рис. 2.17. Компания PaymentCo, разделяющая процессы на основе использования информации о кредитной карте, чтобы уменьшить объем требований PCI

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

Разделение данных часто обусловлено различными соображениями конфиденциальности и безопасности. Мы вернемся к этой теме и к примеру с Pay­mentCo в главе 11.

Технологии

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

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

Рис. 2.18. Традиционная трехуровневая архитектура часто определяется технологическими границами

Организационный подход

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

Определение границы сервиса, владение которым будет распространяться на несколько разных команд, вряд ли приведет к желаемым результатам (по­дробнее — в главе 15). Совместное владение микросервисами сопряжено с большими трудностями. Отсюда вытекает необходимость принимать во внимание существующую организационную структуру при рассмотрении вопроса об определении границ. А в некоторых ситуациях даже следует рассмотреть возможность преобразования организационной структуры для поддержки желаемой архитектуры.

Конечно, важно учитывать и изменения организационной структуры. Предстоит ли нам в связи с этим перестраивать ПО? В худшем случае это может привести к пересмотру требующего разделения существующего микросервиса, поскольку он содержит функциональность, принадлежащую теперь двум отдельным командам, тогда как раньше за сервис отвечала одна. С другой стороны, часто организационные изменения требуют только смены владельца существующего микросервиса. Рассмотрим ситуацию, когда коман­да, отвечающая за складские операции, ранее также выполняла функции, связанные с определением количества товаров, которые следует заказать у поставщиков. Допустим, было принято решение передать эту ответственность команде по прогнозированию. Такой команде необходимо получить информацию о текущих продажах и планируемых рекламных акциях, чтобы определить, что нужно заказать. Если бы у команды, отвечающей за работу склада, имелся выделенный микросервис Заказ поставщику, его можно было бы просто перенести в новую команду прогнозирования. С другой стороны, если эта функциональность ранее была интегрирована в систему с более широким охватом, принадлежащую команде, владеющей сервисом Склад, то ее, возможно, потребуется отделить.

Даже когда мы работаем в рамках существующей организационной структуры, не исключен риск неверно установить границы. Много лет назад мы с несколькими коллегами работали с клиентом в Калифорнии, помогая компании внедрить некоторые более «чистые» методы кодирования и перейти к автоматизированному тестированию. Мы начали с простого — декомпозиции сервиса, но заметили кое-что странное. Я не могу вдаваться в подробности, но это было общедоступное приложение с большой глобальной клиентской базой.

Команда и система увеличились. Первоначально задуманная одним человеком система все больше и больше обрастала функциями и пользователями. В конце концов, организация решила усилить потенциал команды, пригласив новую группу разработчиков из Бразилии, которые взяли бы на себя часть работы. Система была разделена, причем пользовательская половина приложения, по сути, не имела состояния и реализовывала сайт, как показано на рис. 2.19. Серверная же часть системы представляла собой просто интерфейс удаленного вызова процедур (RPC, remote procedure call) через хранилище данных. Представьте, что вы взяли уровень репозитория в своей кодовой базе и сделали его отдельным сервисом.

Рис. 2.19. Граница сервисов, образованная техническими стыками

Оба сервиса часто приходилось модифицировать. Они использовали низкоуровневые вызовы методов в стиле RPC, которые были слишком уязвимыми (мы обсудим это в главе 4). Интерфейс сервиса также очень интенсивно обменивался информацией, что приводило к проблемам с производительностью. Нам пришлось создать сложные механизмы дозирования RPC. Я назвал это «луковой архитектурой», так как в ней было много слоев и я плакал, когда нам приходилось сквозь них прорезаться.

Теперь, на первый взгляд, идея разделения ранее монолитной системы по географическому/организационному признаку обрела смысл, о чем мы подробнее поговорим в главе 15. Здесь, однако, вместо того, чтобы делать вертикальный, бизнес-ориентированный срез стека, команда выделила то, что ранее являлось встроенным API, и сделала горизонтальный срез. Лучшим вариантом было бы, если бы команда в Калифорнии сделала один сквозной вертикальный срез, состоящий из связанных частей пользовательского интерфейса и функций доступа к данным, а команда в Бразилии сделала другой срез.

Внутреннее и внешнее наслоение

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

Смешивание моделей и исключений

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

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

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

Организационные и предметно-ориентированные границы сервисов — это моя собственная отправная точка, мой подход по умолчанию. Как правило, в игру вступает ряд описанных здесь факторов, и то, какие из них повлияют на ваши собственные решения, будет зависеть от решаемых вами задач. Вам необходимо ориентироваться на свои конкретные условия, чтобы определить, что лучше всего подходит именно для вас, и надеюсь, я дал вам несколько вариантов для рассмотрения. Просто помните, что если кто-то говорит: «Единственный способ сделать это — X!» — он, скорее всего, пытается убедить вас использовать конкретный сценарий, в пределах которого вам придется работать. Но вы можете найти решение получше.

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

Резюме

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

Идеи, изложенные в книге Эрика Эванса «Предметно-ориентированное проектирование», очень полезны для поиска разумных границ сервисов. Я же только прошелся по верхам — в книге Эрика гораздо больше подробностей. Если вы хотите углубиться в эту тему, я могу порекомендовать книгу Вона Вернона «Реализация методов предметно-ориентированного проектирования»25, — она поможет вам понять практические аспекты этого подхода, в то время как другая книга Вернона26 про основы предметно-ориентированного программирования отлично подойдет, если вы ищете что-то более конкретное.

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


10 Parnas D. On the Criteria to Be Used in Decomposing Systems into Modules (статья в журнале, Университет Карнеги-Меллона, 1971). https://oreil.ly/BnVVg.

11 Colyer A. On the Criteria… https://oreil.ly/cCtSV.

12 Parnas D. Information Distribution Aspects.

13 Связность (cohesion) — мера силы взаимосвязанности элементов сервиса; способ и степень, в которой задачи, выполняемые им, связаны друг с другом. — Примеч. пер.

14 Связанность (coupling) представляет собой степень взаимосвязи между сервисами. При создании систем необходимо стремиться к максимальной независимости сервисов, то есть их связанность должна быть минимальной. — Примеч. пер.

15 В моей книге «От монолита к микросервисам. Эволюционные шаблоны для трансформации монолитной системы» я приписал это самому Ларри Константину. Однако цитату действительно следует отнести к книге 2003 года Handbook of Software and Systems Engineering (Addison-Wesley) за авторством Albert Endres и Dieter Rombach.

16 Эта концепция аналогична протоколу предметных приложений, который определяет правила взаимодействий компонентов в системе, основанной на REST.

17 Сквозная связанность — это мое название того, что первоначально было описано Мейлиром Пейдж-Джонсом как «блуждающая связь». Я решил использовать другой термин, так как первоначальный показался мне несколько неудобным для более широкой аудитории.

18 Хорошо, не один и не два раза. Гораздо чаще, чем раз или два...

19 Эванс Э. Предметно-ориентированное проектирование: Структуризация сложных программных систем.

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

21 Calcado P. Pattern: Using Pseudo-URIs with Microservices. https://oreil.ly/xOYMr.

22 Отсылка к одноименной книге Джона Грина или к кантри-песне. Еще так называется стиль программирования с глубоким стеком вызовов. — Примеч. пер.

23 Я имею в виду, почему не желтые? Это самый распространенный цвет!

24 Brandolini A. Introducing EventStorming. — Victoria, BC: Leanpub, forthcoming.

25 Вернон В. Реализация методов предметно-ориентированного проектирования.

26 Вернон В. Предметно-ориентированное проектирование. Самое основное.

26
23
13
18
15
21
16
10

Вернон В. Предметно-ориентированное проектирование. Самое основное.

25
19
22
14

Связность (cohesion) — мера силы взаимосвязанности элементов сервиса; способ и степень, в которой задачи, выполняемые им, связаны друг с другом. — Примеч. пер.

Parnas D. Information Distribution Aspects.

12

В моей книге «От монолита к микросервисам. Эволюционные шаблоны для трансформации монолитной системы» я приписал это самому Ларри Константину. Однако цитату действительно следует отнести к книге 2003 года Handbook of Software and Systems Engineering (Addison-Wesley) за авторством Albert Endres и Dieter Rombach.

Связанность (coupling) представляет собой степень взаимосвязи между сервисами. При создании систем необходимо стремиться к максимальной независимости сервисов, то есть их связанность должна быть минимальной. — Примеч. пер.

Colyer A. On the Criteria… https://oreil.ly/cCtSV.

Parnas D. On the Criteria to Be Used in Decomposing Systems into Modules (статья в журнале, Университет Карнеги-Меллона, 1971). https://oreil.ly/BnVVg.

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

Хорошо, не один и не два раза. Гораздо чаще, чем раз или два...

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

Brandolini A. Introducing EventStorming. — Victoria, BC: Leanpub, forthcoming.

Я имею в виду, почему не желтые? Это самый распространенный цвет!

Вернон В. Реализация методов предметно-ориентированного проектирования.

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

Эванс Э. Предметно-ориентированное проектирование: Структуризация сложных программных систем.

Отсылка к одноименной книге Джона Грина или к кантри-песне. Еще так называется стиль программирования с глубоким стеком вызовов. — Примеч. пер.

Calcado P. Pattern: Using Pseudo-URIs with Microservices. https://oreil.ly/xOYMr.

20
24
11
17

Глава 3. Разделение монолита на части

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

В этой главе я расскажу, с чего начать и какие шаблоны лучше использовать.

Осознайте цель

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

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

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

Микросервисы — это не самый легкий вариант. Попробуйте для начала более простые подходы.

Какой микросервис необходимо создать в первую очередь? Без всеобъемлющего понимания конечной цели ответить на этот вопрос практически невозможно.

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

Постепенный переход

Если вы занимаетесь чем-то хаотично, единственное, что вы гарантированно получите, — это хаос.

Мартин Фаулер

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

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

Разбиение фрагментов системы на более мелкие части также позволяет вам не застрять на месте и постепенно набрать обороты. Отделяя микросервисы по одному, вы получаете возможность постепенно раскрывать приносимую ими пользу вместо ожидания какого-то масштабного развертывания.

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

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

Монолит не всегда плохой вариант

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

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

Многие люди считают сосуществование монолита и микросервисов «запутанным», но архитектура реально работающей системы никогда не бывает чистой или простой. Если вам нужна чистая архитектура, обязательно заламинируйте распечатку идеальной версии архитектуры системы, которая могла бы у вас быть, если бы вы обладали даром предвидения и безграничными средствами. Архитектура реальной системы постоянно развивается, требует адаптации по мере изменения потребностей и получения новых знаний. Настоящее мастерство заключается в том, чтобы привыкнуть к этой идее, к чему я вернусь в главе 16.

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

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

Опасность преждевременной декомпозиции

Не спешите создавать микросервисы, если у вас неясное представление о предметной области. В моей практике такое было, когда я работал в компании Thoughtworks. Одним из ее продуктов был Snap CI — размещенный на хостинге инструмент непрерывной интеграции и непрерывной доставки (мы обсудим эти концепции в главе 7). Команда ранее работала над аналогичным приложением — GoCD — инструментом непрерывной доставки с открытым исходным кодом, который можно развертывать локально, а не размещать в облаке.

Хотя на самом раннем этапе определенная часть кода в проектах Snap CI и GoCD повторялась, в итоге у Snap CI оказалась совершенно новая кодовая база. Тем не менее предыдущий опыт команды в области инструментов CD (непрерывной доставки) позволил разработчикам быстрее определить границы и построить свою систему как набор микросервисов.

Однако через несколько месяцев стало ясно, что варианты использования Snap CI настолько сильно различаются, что первоначальное представление о границах сервисов было не совсем правильным. Это привело к большому количеству изменений, внесенных во все сервисы, и к возросшим издержкам. В конце концов разработчики объединили сервисы обратно в одну монолитную систему, дав членам команды время для более четкого определения границ сервисов. Год спустя они смогли разделить монолитную систему на микросервисы, границы которых оказались гораздо более стабильными. Это далеко не единственный пример подобной ситуации. Преждевременная декомпозиция системы может оказаться дорогостоящей, особенно если вы новичок в этой области. Во многих отношениях работать с существующей кодовой базой, требующей разделения на микросервисы, намного проще, чем пытаться создавать микросервисы с самого начала.

Что отделить в первую очередь

Как только у вас появится четкое представление о необходимости внедрения микросервисов, вам потребуется определить, какие микросервисы создавать в первую очередь. Хотите масштабировать приложение? Функции, в настоящее время ограничивающие способность системы справляться с нагрузкой, будут занимать первое место в списке. Хотите ускорить время выхода на рынок? Посмотрите на изменчивость системы, чтобы определить наиболее часто изменяющиеся части функциональности, и поймите, будут ли они работать как микросервисы. Для быстрого поиска наиболее изменчивых частей кодовой базы можно использовать инструменты статического анализа, такие как CodeScene (https://www.codescene.com). Пример работы CodeScene показан на рис. 3.1, где изображены хот-споты в проекте Apache Zookeeper с открытым исходным кодом.

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

Рис. 3.1. Хот-споты в CodeScene, помогающие идентифицировать часто изменяющиеся части кодовой базы

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

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

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

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

Декомпозиция по слоям

Рис. 3.2. Код и данные списка избранного в существующем монолитном приложении

Итак, вы определили, какой микросервис будете извлекать первым. Что дальше? Разобьем эту декомпозицию на дальнейшие, более мелкие этапы.

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

Сопоставление микросервиса с пользовательским интерфейсом часто не соответствует соотношению 1:1 (подробнее об этом — в главе 14). Таким образом, извлечение функциональности UI, связанной с микросервисом, можно рассматривать как отдельный шаг. Хотелось бы предостеречь вас от игнорирования части уравнения, касающейся пользовательского интерфейса. Я видел слишком много организаций, думающих только о преимуществах декомпозиции функциональности серверной части (бэкенда), что часто приводило к нарушению целостности подхода к любой архитектурной реструктуризации. Иногда самые большие преимущества можно получить от декомпозиции UI, так что игнорируйте этот шаг на свой страх и риск. Часто декомпозиция UI имеет тенденцию отставать от декомпозиции бэкенда, поскольку до тех пор, пока микросервисы не будут доступны, трудно увидеть возможности декомпозиции пользовательского интерфейса. Просто убедитесь, что процесс не слишком сильно отстает.

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

Сначала код

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

Рис. 3.3. Сначала переместите код списка избранного в новый микросервис, оставив данные в монолитной базе данных

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

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

Сначала данные

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

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

Рис. 3.4. Сначала извлекаются таблицы, связанные с функциональностью списка избранного

Полезные шаблоны декомпозиции

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

Шаблон «Душитель»

Техника, которая часто используется при переписывании системы, — это шаблон «Душитель» (Strangler Fig Pattern), придуманный Мартином Фаулером (https://oreil.ly/u33bI). Данный шаблон описывает процесс объединения старой и новой систем с течением времени, позволяя актуальной версии постепенно перенимать все больше и больше функций старой системы.

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

Рис. 3.5. Обзор шаблона «Душитель»

Прелесть этого шаблона заключается в возможности реализовать его без внесения каких-либо изменений в базовое монолитное приложение. Монолиту неизвестно, что был «обернут» в более новую систему.

Параллельное выполнение

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

Один из способов убедиться в корректности работы новой функциональности, не подвергая риску поведение существующей системы, — использовать шаблон параллельного выполнения (parallel run): одновременное выполнение монолитной реализации функциональности и микросервисной, обслуживание одних и тех же запросов и сравнение результатов. Мы рассмотрим этот шаблон более подробно в подразделе «Параллельное выполнение» главы 8.

Шаблон переключаемых функций

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

Как и в случае с шаблоном «Душитель», во время перехода мы часто оставляем существующую функциональность в монолите, но рассматриваемый шаблон позволяет нам переключаться между версиями функциональности — в монолите и в новом микросервисе. В примере применения шаблона «Душитель» с использованием HTTP-прокси можно было бы реализовать переключение функций на уровне прокси, чтобы обеспечить простой элемент управления.

Для более широкого ознакомления с переключателями функций я рекомендую статью Пита Ходжсона28.

Проблемы декомпозиции данных

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

Производительность

Базы данных, особенно реляционные, очень хороши для объединения данных в разных таблицах. Однако нередко при разделении БД на части нам в конечном счете приходится перемещать операции объединения (JOIN) с уровня данных в сами микросервисы. И как бы мы ни старались, вряд ли их выполнение будет столь же быстрым.

Рассмотрим рис. 3.6. Он иллюстрирует ситуацию, в которой мы оказались с компанией MusicCorp. Мы решили извлечь из каталога функциональность — нечто, что может предоставлять информацию об исполнителях, треках и альбомах и управлять ею. В настоящее время код, связанный с каталогом внутри монолита, использует таблицу Альбомы для хранения информации о компакт-дисках, доступных для продажи. Эти альбомы в конечном счете попадают в таблицу Гроссбух, где мы отслеживаем весь товарооборот. В таблицу Гроссбух записывается дата реализации товара вместе с идентификатором, ссылающимся на проданный продукт. Идентификатор в нашем примере называется единицей учета запасов (SKU, stock keeping unit) — это обычная практика в системах розничной торговли.

В конце каждого месяца необходимо составлять отчет с описанием самых продаваемых компакт-дисков. Таблица Гроссбух помогает нам понять, копий какого артикула продано больше всего, но информация об этом артикуле находится в таблице Альбомы. Отчеты должны быть понятными и удобными для чтения, поэтому вместо того, чтобы говорить: «Мы продали 400 копий артикула 123 и заработали 1596 долларов», мы бы добавили больше информации. Например: «Мы продали 400 копий Now That's What I Call Death Polka и заработали 1596 долларов». Этот запрос к базе данных инициируется кодом финансовой части, что требует добавления информации из таблицы Гроссбух в таблицу Альбомы, как показано на рис. 3.6.

Рис. 3.6. Операция объединения в монолитной базе данных

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

Далее нам нужно вызвать микросервис Каталог, запросив информацию об этих артикулах. Такой запрос приведет к тому, что микросервис Каталог локаль­но выполнит SELECT в своей базе данных.

Логически операция объединения все еще выполняется, но теперь она происходит внутри микросервиса Финансы, а не в БД. Объединение перешло с уровня данных на уровень кода приложения. К сожалению, эта операция будет далеко не такой эффективной, как объединение в базе данных. Мы ушли из мира, в котором у нас было единственный оператор SELECT, в новый, где есть запрос SELECT к таблице Гроссбух. За ним следует вызов микросервиса Каталог, который, в свою очередь, запускает оператор SELECT, адресованный таблице Альбомы, как показано на рис. 3.7.

Рис. 3.7. Замена операции объединения с базой данных вызовами сервисов

В этой ситуации я был бы очень удивлен, если бы общее время ожидания этой операции не увеличилось. В данном случае это не представляет серьезной проблемы, поскольку отчет генерируется ежемесячно и может быть принудительно кэширован (более подробно об этом — в разделе «Кэширование» главы 13). Но если операция часто повторяется, задержка может стать более серьезной проблемой.

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

Целостность данных

Базы данных могут быть полезны для обеспечения целостности наших данных. Возвращаясь к рис. 3.6, где таблицы Альбомы и Гроссбух находились в одной БД, мы могли бы определить (и, вероятно, определили бы) связь внешнего ключа между строками в таблицах Гроссбух и Альбомы. Это гарантирует, что мы всегда сможем перейти от записи в таблице Гроссбух к информации о проданном альбоме, так как, если записи из таблицы Альбомы упоминаются в Гроссбухе, их нельзя удалить.

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

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

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

Транзакции

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

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

Вскоре, в разделе «Саги» главы 6, вы узнаете, что существуют альтернативные (и предпочтительные) механизмы распределенных транзакций для управления изменениями состояния между несколькими микросервисами, но они сопряжены с новыми сложностями. Как и в случае с целостностью данных, мы должны смириться с тем фактом, что, разбивая наши БД, мы столкнемся с новым перечнем проблем.

Инструментарий

Преобразование баз данных затруднено по многим причинам, одна из которых заключается в ограниченном количестве доступных инструментов, позволя­ющих легко вносить изменения. Что касается кода, существует инструментарий рефакторинга, встроенный в среды IDE, и есть дополнительное преимущество: изменяемые системы в принципе не имеют состояния. В случае с базой данных у изменяемых сущностей есть состояние, и здесь также не хватает хороших инструментов для рефакторинга.

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

В настоящее время для достижения того же результата я советую людям Flyway (https://flywaydb.org) или Liquibase (https://www.liquibase.org), если у них еще нет подобного инструмента.

База данных отчетов

В рамках извлечения микросервисов из монолита мы также разделяем базы данных, поскольку хотим скрыть доступ к внутреннему хранилищу данных. Убирая прямой доступ к базам данных, можно создавать стабильные интерфейсы, позволяющие осуществлять независимое развертывание. К сожалению, это вызывает проблемы, когда доступ к данным осуществляется из более чем одного микросервиса или когда эти данные доступны в БД, а не через что-то вроде REST API.

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

База данных отчетов позволяет скрыть внутреннее управление состоянием, сохраняя при этом предварительную отправку данных в БД, что может быть очень полезно. Например, можно разрешить пользователям выполнять специальные SQL-запросы, запускать крупномасштабные операции объединения или использовать существующие цепочки инструментов, у которых должен быть доступ к конечной точке SQL. База данных отчетов будет хорошим решением этой проблемы.

Рис. 3.8. Обзор шаблона базы данных отчетов

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

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

Резюме

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

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

Если вы хотите более глубоко погрузиться в изучение какой-либо из концепций этой главы, рекомендую ознакомиться с другой моей книгой — «От монолита к микросервисам».

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


27 Ньюмен С. От монолита к микросервисам. Эволюционные шаблоны для трансформации монолитной системы.

28 Hodgson P. Feature Toggles (aka Feature Flags). martinfowler.com, 9 октября 2017 года. https://oreil.ly/XiU2t.

28

Hodgson P. Feature Toggles (aka Feature Flags). martinfowler.com, 9 октября 2017 года. https://oreil.ly/XiU2t.

Ньюмен С. От монолита к микросервисам. Эволюционные шаблоны для трансформации монолитной системы.

27

Глава 4. Стили взаимодействия микросервисов

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

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

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

От внутрипроцессного к межпроцессному

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

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

Производительность

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

Это часто приводит к желанию переосмыслить API. API, который разумно применять внутри процесса, может оказаться нецелесообразным в межпроцессных ситуациях. Я могу спокойно сделать тысячу вызовов через границу API внутри процесса. Захочу ли я сделать то же самое между двумя микросервисами? Возможно, и нет.

Когда параметр передается в метод, структура данных обычно не перемещается — более вероятно, что в запросе будет передан указатель на ячейку памяти. Передача объекта или структуры данных другому методу не требует выделения дополнительной памяти для копирования данных.

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

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

Изменение интерфейсов

Если рассматривать преобразования в интерфейсе внутри процесса, развертывание изменений представляет собой достаточно простой набор действий. Код, реализующий интерфейс, и код, вызывающий интерфейс, упаковываются в один и тот же процесс. На самом деле, если сигнатура метода изменяется при помощи IDE с возможностью рефакторинга, часто сама среда IDE автоматически выполняет рефакторинг вызовов этого изменяемого метода. Развертывание такого изменения может быть выполнено атомарным способом — обе стороны интерфейса упаковываются в один процесс.

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

Обработка ошибок

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

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

В книге «Распределенные системы»30 Эндрю Таненбаум и Мартен Стин описывают пять типов отказов, встречающихся при рассмотрении межпроцессного взаимодействия. Вот упрощенная версия.

Аварийный отказ

Все было хорошо, пока сервер не вышел из строя. Перезагрузка!

Пропуск при отказе

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

Сбой синхронизации

Что-то случилось с опозданием (нужно было к определенному моменту) или, наоборот, преждевременно!

Ошибка ответа

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

Произвольный отказ

Иначе известен как «византийская ошибка». Это когда что-то пошло не так, но участники не могут прийти к соглашению, произошел ли сбой (или почему). Это звучит как «ничего страшного, время такое».

Многие из этих ошибок часто носят нерегулярный характер — это кратковременные проблемы, которые могут исчезнуть. Рассмотрим ситуацию, в которой мы отправляем запрос в микросервис, но не получаем ответа (тип ошибки «пропуск при отказе»). В первую очередь это может означать, что нижестоящий микросервис не получал запрос, поэтому нам нужно отправить его снова. Существуют проблемы, требующие вмешательства человека-оператора. В результате важно иметь богатый набор семантических выражений для возврата ошибок таким образом, чтобы клиенты могли предпринять соответствующие действия.

HTTP — пример протокола, показывающего важность этого процесса. Каждый HTTP-ответ содержит код, при этом коды серий 400 и 500 зарезервированы для ошибок. Серия 400 представляет собой ошибки запроса — по сути, сервис ниже по потоку выполнения сообщает клиенту, что с исходным запросом что-то не так. Следовательно, нет смысла повторять попытку, например, при ответе 404 Not Found. Коды ответов серии 500 относятся к последующим проблемам. Их подмножество указывает клиенту, что сбой может быть кратко­временным. Например, ошибка 503 Service Unavailable говорит нам, что нижестоящий сервер пока что не в состоянии обработать запрос, но, немного подождав, вышестоящий клиент может повторить запрос. С другой стороны, если клиент получает ответ 501 Not Implemented, очередная попытка вряд ли что-то изменит.

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

Технология межпроцессного взаимодействия: так много вариантов выбора

В мире, где слишком много вариантов выбора и слишком мало времени, разумным будет просто игнорировать некоторые вещи.

Сет Годин

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

Если вы пытаетесь создать веб-сайт, то технологии одностраничных приложений, такие как Angular или React, вам не подойдут. Аналогично попытка использовать Kafka для реализации стиля взаимодействия «запрос — ответ» — не очень хорошая идея, поскольку это приложение было разработано для большего количества событийных взаимодействий (темы, к которым мы перейдем чуть позже). И все же я снова и снова сталкиваюсь с тем, что технологии используются не по назначению. Люди выбирают «крутую» новенькую разработку (например, микросервисы!), не задумываясь о том, действительно ли это необходимо.

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

Стили взаимодействия микросервисов

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

Рис. 4.1. Различные стили межмикросервисной коммуникации наряду с примерами технологий внедрения

Вскоре мы рассмотрим различные элементы этой модели более подробно, но сначала я хотел бы кратко описать их.

Синхронная блокировка

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

Асинхронная неблокирующая связь

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

Запрос — ответ

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

Событийный стиль

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

Общие данные

Не часто рассматриваются как стиль коммуникации. При таком стиле микросервисы взаимодействуют через какой-то общий источник данных.

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

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

Смешивание и сочетание

Важно отметить, что микросервисная архитектура в целом может поддерживать различные стили взаимодействия, и это считается нормой. Одни виды взаимодействия имеет смысл организовать просто в стиле «запрос — ответ», в то время как другие — в событийном стиле. На самом деле для одного микросервиса обычно реализуется более одной формы взаимодействия. Рассмотрим микросервис Заказ, предоставляющий API «запрос — ответ», который позволяет размещать или изменять заказы, а затем инициирует события при внесении этих изменений.

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

Шаблон: синхронная блокировка

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

Рис. 4.2. Обработчик заказов отправляет синхронный вызов микросервису Лояльность, блокируется и ожидает ответа

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

Преимущества

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

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

Недостатки

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

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

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

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

Где использовать

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

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

Рис. 4.3. Проверка на потенциально мошенническое поведение в рамках процесса обработки заказов

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

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

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

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

Шаблон: асинхронная неблокирующая связь

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

Связь через общие данные

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

Запрос — ответ

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

Событийное взаимодействие

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

Преимущества

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

Данный стиль связи также полезен, если для обработки запроса требуется много времени. Вернемся к нашему примеру с MusicCorp и, в частности, к процессу отправки посылки. На рис. 4.5 Обработчик заказов принял оплату и отправил вызов микросервису Склад. Процесс поиска компакт-дисков на стеллажах, упаковки и доставки может занять от пары часов до нескольких дней. Следовательно, имеет смысл Обработчику заказов выполнить неблокирующий асинхронный вызов сервиса Склад, чтобы затем получить от него информацию о продвижении заказа. Это форма асинхронной связи «запрос — ответ».

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

Рис. 4.5. Обработчик заказов асинхронным способом запускает процесс упаковки и отправки заказа

Недостатки

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

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

Async/await и когда асинхронная связь все еще блокируется

Как и во многих областях информатики, здесь можно использовать один и тот же термин в разных контекстах, чтобы получились очень разные значения. Стиль программирования, который, по-видимому, особенно популярен, — это использование конструкций типа async/await для работы с потенциально асинхронным источником данных, но в блокирующем, синхронном стиле.

В примере 4.1 показана очень простая иллюстрация такой реализации на JavaScript. Курсы обмена валюты часто колеблются в течение дня, и мы получаем их через брокер сообщений. Мы определяем промис (promise). В целом промис — это то, что в какой-то момент в будущем разрешится. В нашем случае eurToGbp станет следующим обменным курсом евро к фунту стерлингов.

Пример 4.1. Пример работы блокирующим, синхронным способом с потенциально асинхронным вызовом

async function f() {

  let eurToGbp = new Promise((resolve, reject) => {

    // код для получения последних данных по курсу валют EUR/GBP

    ...

  });

  var latestRate = await eurToGbp;

  process(latestRate);

}

Ожидает, пока не будут получены последние данные по курсу валют Евро/Фунт стерлингов.

Не будет работать, пока промис не выполнен.

При ссылке на eurToGbp с помощью await мы блокируем связь до тех пор, пока состояние la­testRate не будет успешно выполнено, а функция process не будет выполняться, пока мы не разрешим состоя­ние eurToGbp31.

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

Где использовать

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

Шаблон: связь через общие данные

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

Пример рассматриваемого стиля показан на рис. 4.6. Здесь сервис Импортер нового продукта создает файл, позже доступный для микросервисов Запасы и Каталог, расположенных ниже по потоку.

Рис. 4.6. Один микросервис записывает файл, используемый другими микросервисами

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

Реализация

Чтобы реализовать такой шаблон, вам нужно постоянное хранилище данных. Файловой системы во многих случаях будет достаточно. Я создал много систем, которые просто периодически сканируют файловую систему, отмечают наличие нового файла и реагируют на него соответствующим образом. Конечно, можно было бы использовать какое-то надежное распределенное хранилище памяти. Стоит отметить, что любому нижестоящему микросервису, которому предстоит работать с этими данными, потребуется собственный механизм для определения доступности новых данных — поллинг (polling) часто применяется в качестве решения этой проблемы.

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

Как для хранилища данных, так и для озера данных предполагается, что поток информации идет в одном направлении. Один микросервис публикует данные в общем хранилище данных, а нижестоящие потребители считывают их и выполняют соответствующие действия. Проблемой станет реа­лизация совместно используемой БД, в которой множество микросервисов будут считывать и записывать данные в одно и то же хранилище. Пример такой организации обсуждался в главе 2 при изучении общей связанности. На рис. 4.7 показаны микросервисы Обработчик заказов и Склад, обновляющие одну и ту же запись.

Рис. 4.7. Пример общей связанности, в которой сервисы Обработчик заказов и Склад обновляют одну и ту же запись заказа

Преимущества

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

Недостатки

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

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

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

Где использовать

Где эта модель действительно хороша, так это в обеспечении взаимодействия между процессами, имеющими ограничения на доступные к использованию технологии. Наличие существующей системы, взаимодействующей с интерфейсом GRPC вашего микросервиса или подписывающейся на его топик Kafka, вполне может быть более удобным вариантом с точки зрения микросервиса, но не с точки зрения потребителя. У старых систем возможны ограничения в отношении поддерживаемых технологий, а также могут оказаться высокие затраты на изменение. С другой стороны, даже старые системы мейнфреймов должны иметь возможность считывать данные из файла. Конечно, все это зависит от использования широко поддерживаемой технологии хранения данных — я также мог бы реализовать этот шаблон, используя что-то вроде кэша Redis. Но может ли ваша старая система мейнфреймов взаимодействовать с Redis?

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

Шаблон: связь «запрос — ответ»

При использовании модели «запрос — ответ» микросервис отправляет запрос на какое-либо действие нижестоящему сервису и ожидает получить ответ с результатом запроса. Это взаимодействие можно осуществить с помощью синхронного блокирующего вызова или асинхронным неблокирующим методом. Простой пример подобной кооперации показан на рис. 4.8. Здесь микросервис Чарт, собирающий самые продаваемые компакт-диски с музыкой разных жанров, отправляет запрос в сервис Запасы для получения текущих уровней запасов для некоторых компакт-дисков.

Рис. 4.8. Микросервис Чарт отправляет запрос к сервису Запасы

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

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

Команды или запросы

Иногда люди говорят об отправке команд, а не запросов, особенно в контексте асинхронной связи «запрос — ответ». Смысл термина «команда», возможно, тот же, что и у запроса, а именно: вышестоящий микросервис просит нижестоящий что-то сделать.

Однако лично я предпочитаю термин «запрос». Команда подразумевает директиву, которой необходимо подчиняться, и это может привести к ситуации, когда людям будет казаться, что команда обязательна к выполнению. Запрос подразумевает что-то, что может быть отклонено. Это правильно, потому что микросервис рассматривает каждый запрос по существу и, основываясь на собственной внутренней логике, решает, следует ли выполнять его. Если отправленный запрос нарушает внутреннюю логику, микросервис должен отклонить его. Хотя разница небольшая, я не уверен, что термин «команда» передает то же значение.

Я буду придерживаться термина «запрос», но, какой бы термин вы ни решили использовать, помните, что микросервис может отклонить запрос/команду, если это необходимо.

Реализация: синхронная или асинхронная

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

С асинхронным вызовом в стиле «запрос — ответ» все не так просто. Вернемся к процессу, связанному с резервированием складских запасов. На рис. 4.10 запрос на резервирование отправляется в виде сообщения через своего рода брокер сообщений (мы рассмотрим брокеры сообщений позже в этой главе). Вместо того чтобы сообщение отправлялось непосредственно в микросервис Запасы из Обработчика заказов, оно помещается в очередь. Сервис Запасы по возможности считывает сообщения из этой очереди и выполняет связанную с ними работу по резервированию запасов. Микросервису Запасы необходимо знать, куда направить ответ. В нашем примере он отправляет его обратно по другой очереди, используемой Обработчиком заказов.

Рис. 4.10. Использование очередей для отправки запросов на резервирование запасов

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

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

Последнее замечание: все типы взаимодействия «запрос — ответ», вероятно, потребуют некоторой формы обработки тайм-аута, чтобы избежать проблем, когда система будет заблокирована в ожидании чего-то, что может никогда не произойти. Способ реализации этой функции тайм-аута будет варьироваться в зависимости от применяемой технологии реализации. Мы рассмотрим тайм-ауты более подробно в главе 12.

Параллельные или последовательные вызовы

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

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

Оптимально было бы выполнить эти три запроса параллельно. Тогда общая задержка операции будет равна самому медленному вызову API, а не сумме задержек каждого вызова.

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

Где использовать

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

Шаблон: событийное взаимодействие

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

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

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

Сервис Склад просто транслирует события, ожидая соответствующей реакции заинтересованных сторон. Он не знает, кто будет получателем событий, что делает событийные взаимодействия в целом слабо связанными. При работе с моделью «запрос — ответ» можно было ожидать, что сервис Склад сообщит сервису Уведомления, когда необходимо отправлять электронные письма. В такой модели Складу нужна информация, какие события требуют создания уведомления для клиента. При событийном взаимодействии ответственность за это переносится на микросервис Уведомления.

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

Рис. 4.11. Сервис Склад выдает события, на которые подписываются отдельные нижестоящие микросервисы

Распределение ответственности в наших событийных взаимодействиях похоже на распределение, наблюдаемое в организациях, пытающихся создать более автономные команды. Вместо того чтобы нести всю ответственность централизованно, мы перекладываем ее на сами команды для их более автономной работы (к этой концепции вернемся в главе 15). В нашем конкретном примере ответственность переходит от сервиса Склад в сервисы Уведомления и Оплата. Это может помочь уменьшить сложность сервиса Склад и привести к более равномерному распределению «умных» функций в нашей системе. Мы рассмотрим эту идею более подробно, когда сравним хореографию и оркестрацию в главе 6.

События и сообщения

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

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

Точно так же может потребоваться отправить запрос в качестве полезной нагрузки сообщения. В этом случае мы бы реализовали асинхронную форму «запрос — ответ».

Реализация

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

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

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

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

Иногда люди тратят целую вечность на реализацию все большего и большего количества моделей поведения, получаемых «из коробки» с соответствующим брокером сообщений, чтобы заставить Atom работать в некоторых ситуациях. Например, шаблон конкурирующих потребителей описывает метод, при котором вызываются несколько экземпляров воркеров (worker, или тот, кто осуществляет процедуру; рабочий процесс) для конкуренции за сообщения. Это хорошо работает при обработке списка независимых заданий (мы вернемся к этому в следующей главе). Однако хотелось бы избежать случая, когда два или более воркера выполняют одну и ту же задачу несколько раз из-за одинакового сообщения. С помощью брокера сообщений стандартная очередь справится с этой проблемой. С применением Atom потребуется управлять нашим собственным общим состоянием среди всех воркеров, чтобы попытаться уменьшить шансы на повторное выполнение.

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

Фактически пересылки по этим асинхронным протоколам осуществляются по тем же принципам, что и при синхронной связи. Если в настоящее время вас устраивает кодирование запросов и ответов с использованием формата JSON, придерживайтесь его.

Что входит в событие

На рис. 4.12 показано событие, которое транслируется из микросервиса Покупатель и информирует заинтересованные стороны о регистрации в системе нового клиента. Два нижестоящих микросервиса, Лояльность и Уведомления, заинтересованы в этом событии. Микросервис Лояльность реагирует на получение события, создавая учетную запись для нового клиента, чтобы он мог начать зарабатывать баллы, в то время как микросервис Уведомления отправляет электронное письмо вновь зарегистрированному клиенту, приветствуя его от лица MusicCorp.

Рис. 4.12. Микросервисы Уведомления и Лояльность получают событие при регистрации нового клиента

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

Просто идентификатор

Один из вариантов заключается в том, чтобы событие просто содержало идентификатор для вновь зарегистрированного клиента, как показано на рис. 4.13. Микросервису Лояльность нужен только этот идентификатор для создания соответствующей учетной записи лояльности, поэтому у него есть вся необходимая информация. Однако, хотя микросервис Уведомления знает, что при получении такого события ему необходимо отправить приветственное электронное письмо, для выполнения своей работы ему потребуется дополнительная информация, по крайней мере адрес электронной почты и, возможно, имя клиента, чтобы придать письму индивидуальный характер. Поскольку эта информация не содержится в событии, которое сервис Уведомления получает, у него нет другого выбора, кроме как извлечь эту информацию из микросервиса Покупатель (см. рис. 4.13).

Рис. 4.13. Микросервису Уведомления необходимо запросить у микросервиса Покупатель дополнительные сведения, которые не включены в событие

У такого подхода есть некоторые недостатки. Микросервису Уведомления теперь необходимо знать о микросервисе Покупатель, что добавляет дополнительную предметную связанность. Хотя она, как обсуждалось в главе 2, по-прежнему слабая, все же хотелось бы по возможности избежать ее. Если бы событие, полученное микросервисом Уведомления, содержало всю необходимую информацию, то этот обратный вызов не потребовался бы. Обратный вызов от принимающего микросервиса может привести еще к одному серьезному недостатку: в ситуации с большим количеством принимающих микросервисов сервис, отправляющий событие, в результате может получить шквал запросов. Представьте, что все пять разных микросервисов получили одно и то же событие по созданию клиента и всем им нужно немедленно запросить дополнительную информацию у микросервиса Покупатель. По мере увеличения числа микросервисов, заинтересованных в конкретном событии, влияние этих вызовов может стать значительным.

Полностью детализированные события

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

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

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

Хотя этот подход, безусловно, для меня предпочтителен, он не лишен недостатков. Для начала, если объем связанных с событием данных внушителен, у нас могут возникнуть опасения по поводу размера события. У современных брокеров сообщений (при условии, что вы используете один из них для реализации механизма трансляции событий) довольно жесткие ограничения по размеру сообщения. Максимальный размер сообщения по умолчанию в Kafka составляет 1 Мбайт, а последняя версия RabbitMQ поддерживает теоретический верхний предел 512 Мбайт для одного сообщения (по сравнению с предыдущим 2 Гбайт!). Хотя вполне вероятно, что при таких больших размерах сообщений возникнут некоторые проблемы с производительностью. Но даже сообщение размером 1 Мбайт на Kafka предоставляет достаточно возможностей для отправки довольно большого объема данных. В конечном счете, если вы углубляетесь в область, в которой приходится уделять особое внимание размеру событий, я бы рекомендовал применить гибридный подход, при котором часть информации содержится в событии, но при необходимости можно просмотреть другие (более объемные) данные.

На рис. 4.14 сервису Лояльность не нужно знать адрес электронной почты или имя клиента, и тем не менее он получает эти данные через событие. Это может вызвать проблемы при попытке ограничить область, в которой микросервисы могут видеть личную информацию (или PII), данные платежных карт или аналогичные конфиденциальные данные. Решить проблему поможет отправка событий двух разных типов: одно содержит PII и доступно для просмотра некоторыми микросервисами, а второе не хранит PII и транслируется более широко. Это добавляет сложности с точки зрения управления видимостью различных событий и обеспечения их запуска. Что происходит, когда микросервис отправляет событие первого типа, но разрушается до отправки второго?

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

Где использовать

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

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

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

Лично я заметил, что тяготею к событийному взаимодействию почти по умолчанию. Мой мозг, похоже, перестроил себя так, что такие типы коммуникации кажутся мне просто очевидными. Однако из-за подобной субъективной оценки ничего не остается, кроме как сказать, что это кажется правильным, чем попытаться объяснить почему. Но это всего лишь мое собственное предубеждение — я, естественно, тяготею к тому, что знаю, основываясь на своем собственном опыте. Велика вероятность, что влечение к этой форме взаимодействия почти полностью обусловлено моим предыдущим неудачным опытом работы с чрезмерно связанными системами. Я мог бы быть просто генералом, переживающим битву снова и снова, не задумываясь о том, что, возможно, на этот раз все на самом деле по-другому.

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

Действуйте с осторожностью

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

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

Система была запущена и работала, и мы были вполне довольны собой. Но однажды, сразу после релиза, мы столкнулись с неприятной проблемой: наши воркеры продолжали умирать. И умирать. И умирать.

В конце концов мы отыскали проблему. Вкралась ошибка, из-за которой запрос определенного типа приводил к сбою воркера. Мы использовали очередь транзакций: когда воркер умирал, время ожидания его блокировки запроса истекало и запрос на ценообразование помещался обратно в очередь — только для того, чтобы другой воркер забрал его и завершился. Это был классический пример того, что Мартин Фаулер называет катастрофической отказоустойчивостью (https://oreil.ly/8HwcP).

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

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

Я также настоятельно рекомендую ознакомиться с книгой «Шаблоны интеграции корпоративных приложений» Грегора Хоупа и Бобби Вулфа32, в которой содержится гораздо больше подробностей о различных шаблонах обмена сообщениями.

Однако стоит быть честными в отношении стилей интеграции, которые мы могли бы считать «более простыми», — проблемы, связанные с пониманием того, работает что-то или нет, не ограничиваются асинхронными формами интеграции. Из-за чего мог произойти тайм-аут при синхронном блокирующем вызове? Из-за потери запроса? Или ответа? Если вы повторите попытку, но первоначальный запрос на самом деле прошел, что тогда? Вот тут-то и возникает идемпотентность — тема, которую мы рассмотрим в главе 12.

Что касается обработки сбоев: возможно, синхронные блокирующие вызовы создадут нам столько же проблем, сколько выяснение, а произошло ли вообще что-то. Просто подобные проблемы могут быть нам лучше известны!

Резюме

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

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


29 Реальная история.

30 Стин ван М., Таненбаум Э.С. Распределенные системы. 3-е изд.

31 Обратите внимание, что пример очень упрощен — я полностью опустил код обработки ошибок. Если вы хотите узнать больше об async/await, в частности, в JavaScript, ознакомьтесь с Modern JavaScript Tutorial (https://java script.info). Это отличный вариант для начала.

32 Хоуп Г., Вулф Б. Шаблоны интеграции корпоративных приложений.

31

Хоуп Г., Вулф Б. Шаблоны интеграции корпоративных приложений.

Обратите внимание, что пример очень упрощен — я полностью опустил код обработки ошибок. Если вы хотите узнать больше об async/await, в частности, в JavaScript, ознакомьтесь с Modern JavaScript Tutorial (https://java script.info). Это отличный вариант для начала.

29
32

Реальная история.

Стин ван М., Таненбаум Э.С. Распределенные системы. 3-е изд.

30

Часть II Реализация