Моя личная сага о микросервисах и книга, которая помогла мне найти путь в светлое будущее
Вашему вниманию представлена квинтэссенция подготовленной мной серии докладов под названием «Распределенное понимание распределенных систем» (“Distributed Distributed Systems unDerstanding”), посвященной определению и оптимизации микросервисных архитектур. Эта серия основана на книгах «Основы архитектуры программного обеспечения: инженерный подход» и «Архитектура программного обеспечения: сложные моменты». Если вы захотите узнать больше об этой серии, рекомендую прочитать статью «Почему города представляют из себя распределенные системы» Эрика Ралстона.
Если бы мы сравнивали распределенные системы с жанром «киберпанк», то мы бы нашли схожесть в том, что и там и там подразумевается децентрализация и распределение власти/контроля. В распределенной системе данные и вычислительная мощность не сосредоточены в одном месте, а распределены между множеством узлов. Эта децентрализация может послужить фундаментом для более устойчивой и адаптивной системы, но мы расплачиваемся за это отсутствием центрального органа управления и потенциально более высокой сложностью.
Точно так же киберпанк часто изображает будущее, в котором технологии продвинулись до такой степени, что традиционные структуры власти были разрушены и в результате децентрализованы, из-за чего общество стало более фрагментированным и в тоже время более связанным. Упор на децентрализацию и отторжение централизованного управления в распределенных системах и в киберпанке может как вдохновлять нас, окрыляя ощущением открывающихся перед нами возможностей, так и вызывать сильное беспокойство.
Моя личная сага в мире распределенных систем (во времена эволюции «микросервисов» на протяжении последнего десятилетия) как черное зеркало отражает эту тонкую грань. К счастью, в 2022 г. Нил Форд и сотоварищи, наконец выпустили руководство, в котором изложено все то, что мне следовало бы знать еще тогда, много лет назад. В этой статье я попытаюсь объяснить вам все то, что я сам хотел бы знать в самом начале своего пути, а также что мы можем изменить, чтобы не очутиться в мрачном будущем, каким его рисовали в фантастике 1980-х годов.
“Serverless” и «микросервисы» не одно и то же
О Azure Functions я впервые услышал в 2016 году на конференции. Я был поражен тем, как они революционизируют подход к созданию микросервисов. Множество полезных абстракций и практические исследования крупнейших компаний мира (Netflix, Spotify и Uber, и это лишь некоторые из них) стали причиной повальной миграции команд на использование этих технологий в погоне за «бесконечным масштабированием».
Благодаря популяризации бессерверных (serverless) вычислений (таких как Azure Functions и AWS Lambda) наряду с управляемыми сервисами (такими как Azure CosmosDB и AWS DynamoDb) сегодня каждый может легко создавать распределенные системы, но это, увы, не намного упрощает их проектирование. О своих первых двух попытках я бы сказал, что я очень умело создавал красивые тарелки с «бессерверным спагетти», но из-за отсутствия каких-либо четких принципов каждая тарелка представляла из себя скорее «распределенный монолит», чем настоящие микросервисы.
Во многих отношениях это было похоже на еле заметное скольжение в сторону высокотехнологичного хаоса, который очаровывает меня как поклонника киберпанка. Технологии сами прокладывают свой путь, и он может оставаться параллельным человеческим ценностям, а может и нет. Хотя жить с ужасной кодовой базой не так плохо, как в городах‑антиутопиях в Матрице или Нейроманте, обратная сторона медали заключается в том, что это происходит слишком легко, если у вас нет набора определений и принципов, которыми вы руководствуетесь в своей работе в качестве инженера.
«Киберпанк — это слияние высокого уровня технологий и низкого уровня жизни.»
— Брюс Стерлинг
Когда вообще имеет смысл использовать микросервисы?
Далее в моем списке литературы идет книга «Создание эволюционирующих архитектур: поддержка постоянных изменений», которая может лучше обосновать это предостерегающее мнение, но если вы, боже упаси, не готовы подробно выслушать оракула Нила Форда касательно этого вопроса, у меня есть одно категоричное наставление для всех, кто начинает это путешествие, которое, вероятно, перевешивает по своей важности все остальное в этой статье:
Вам, скорее всего, не нужны микросервисы. — Эрик Ралстон
Я слышал статистику, что только 1% всех компаний действительно нуждаются в микросервисах, и я думаю, что это правда. Вместо этого я бы предпочел сначала создать монолит, а затем уже разбираться, на что и как его разделить по мере его роста (ускорив свою работу, не беспокоясь о технических накладных расходах стратифицированного подхода), и уже только после этого переходить на микросервисы.
Думаю, бессерверные базы данных можно использовать с самого начала, несмотря ни на что. И иметь четкие соглашения, которые формируют задел для потенциального перехода на микросервисы в будущем, будет полезно в любом случае, так же, как и четкое определение ваших сервисов как классов, или даже отдельных программ — чем раньше, тем лучше. Но переход на микросервисы — преждевременная оптимизация во всех случаях, кроме следующих:
У вас уже есть большая команда — Построение распределенных систем с использованием микросервисов решает организационные проблемы внутри компаний даже в большей степени, чем технологические.
Закон Конвея подразумевает, что технологии всегда (так или иначе) отражают природу людей и организаций, которые их создают. Таким образом, если вы хотите начать создавать кучу фич, задействовав толпу разработчиков, вам, скорее всего, не обойтись без предметно-ориентированного подхода (DDD) и микросервисов для параллельного проектирования и реализации ваших систем. Большинство других подходов будут повторять все худшие уроки “Мифического человеко-месяца”.Вы уже достигли “web-scale” — Если вы создаете новую систему для компании, которая в первый же день собирается представить ее миллиону пользователей, микросервисы это отличная идея. Если у вас есть проблема, связанная с объемом (Volume), разнообразием (Variety) или скоростью (Velocity) данных (три V бигдаты), то вам необходимо использовать с самого начала хотя бы бессерверную архитектуру.
Однако большинству команд, запускающих DTC (direct-to-consumer) платформу с нулевым числом пользователей, или даже тем, кто создает корпоративную систему с большой аудиторией, не следует беспокоиться о разделении своего проекта на распределенную систему на ранних стадиях. В LiveTiles мы достигли миллиона активных пользователей (MAU) в месяц на монолите, который работал только на трех инстансах в старой доброй модели хостинга PaaS.Вы хотите, чтобы вас приобрели как можно скорее — Если вы небезосновательно уверены, что в вашем ближайшем будущем произойдет какое-то предсказуемое событие, благодаря которому вы перейдете от скромной струйки пользователей и/или данных к бурному потоку, то, возможно, вам следует заложить такие возможности в вашу архитектуру с самого начала, но наиболее вероятным движущим фактором является несокрушимая вера в то, что вас выкупят в ближайшее время.
Это было замечено мной в непринужденной беседе с техническим директором компании, которую LiveTiles приобрела через несколько лет своего существования. Поскольку система уже разделена на компоненты и независимо масштабируется с самого начала, переход к “Web Scale” в отношении пользователей или данных в случае приобретения может быть намного легче, что сглаживает то, что могло бы быть болезненным путем к окупаемости инвестиций, если вы пытаетесь масштабировать монолит до тех же масштабов.
По моему опыту, обрабатывать миллионы пользователей онлайн и (что более важно) получить миллионы долларов дохода можно и на монолите, так что масштабируйте его, пока он не треснет! Если у вас есть какая-никакая команда, начиная с нуля пользователей, в бизнесе, который с большей вероятностью будет приобретать компании, нежели будет приобретен сам, вы должны быть готовы к переходу на микросервисы в будущем, но начать все же лучше с монолита.
Предполагая, что впереди вам предстоит решить настолько серьезные задачи, что без распределенного подхода ну никак не обойтись, имейте ввиду, что для того, чтобы аугментировать блестящим имплантом ваше тело из органического мяса, иногда нужно сначала отрезать кусочек, к которому вы несомненно прикипели за все эти годы.
Определение микросервисов
Подобно многим техно-гикам, введенным в заблуждение новыми технологиями, мое ошибочное мировоззрение возникло из-за того, что я уделял больше внимания внешнему виду, нежели сути. Когда Мартин Фаулер определил концепцию микросервисов в 2014 году, у него была четкая архитектурная база, которую я только недавно начал осмысливать. Некоторые специфические аспекты их предполагаемой архитектуры я либо признал, но сразу же поступился ими, либо так и не нашел в подходящей форме, чтобы впитать в свой мозг.
Возможно, если бы я загрузил эти знания себе в мозг через Neuralink, я бы оперировал правильными определениями с самого начала.
Ограниченный контекст (Bounded Context) — это центральный паттерн в предметно-ориентированном проектировании (DDD), который служит в программной системе в качестве явной границы, внутри которой существует модель предметной области. Это помогает изолировать и инкапсулировать сложность предметной области, упрощая ее понимание и поддержку.
Ограниченный контекст в микросервисной архитектуре зачастую реализуется как отдельный микросервис со своей базой данных и API. Это позволяет разделить различные предметные области и их модели, что повышает масштабируемость и удобство в сопровождении системы.
Самая простая для понимания аналогия — это соответствие, согласно которому “специализированный сервис” подразумевает такую же “специализированную базу данных”. В тот момент, когда вы начинает разделять одну базу данных между несколькими сервисами, они становятся связаны одним и тем же контекстом в отношении развертывания, обслуживания и модификации этой предметной области.
До сих пор мой самый большой антипаттерн в микросервисах заключался либо в совместном использовать базы данных несколькими сервисами, что в результате полностью выходило из-под контроля, либо в настолько глубоком вплетении сторонних сервисов в систему, что они становились похожими на пришитую к системе конечность, которую нельзя было ампутировать без огромных усилий.
Слабая связанность (Loose coupling) — это степень независимости между компонентами или сервисами в программной системе. В микросервисах и DDD идея слабой связанности приобретает форму того, что микросервисы должны иметь минимально возможные взаимозависимости и иметь возможность функционировать независимо. Это обеспечивает большую гибкость и масштабируемость, поскольку изменения в одном сервисе не обязательно повлияют на другие сервисы в системе.
Слабая связанность достигается за счет установления четких границ между микросервисами и разработки для каждого сервиса собственного API и хранилища данных. Чтобы свести к минимуму взаимозависимости, сервисы также должны взаимодействовать не через прямые вызовы или совместное использование данных, а через четко определенные контракты. Хорошим примером может быть ограничение их общения исключительно (...по большей части) событиями. Таким образом, несколько систем могут моделировать сложные отношения “многие ко многим”, ничего не зная друг о друге.
Чтобы оказаться в ситуации “распределенного монолита”, о которой я рассказывал чуть раньше, мне нужно было пренебречь этим принципом до такой степени, что кластеры логики невозможно было обновлять по отдельности. Система размазала по ним так много контроля, что требовалась целая мини-революция, чтобы внедрить сквозные изменения в то, что должно было быть простым микросервисом.
Возможность независимого развертывания микросервиса означает, что его можно развертывать, обновлять или откатывать, не оказывая влияния на работу других микросервисов в системе. Это обеспечивает большую гибкость при разработке и тестировании, а также более быстрые и частые релизы, не затрагивающие всю систему целиком.
Независимо развертываемые микросервисы являются залогом более эффективного DevOps-конвейера, поскольку изменения можно вносить и тестировать перед развертыванием в рабочей среде изолированно. Это помогает снизить риск просачивания багов и критических изменений в систему в целом.
В своем крупнейшем проекте я совершил ошибку, посчитав, что подход, когда каждый компонент имеет свой репозиторий, независимо от того, нужен он ему или нет, поможет с независимым развертыванием. Хотя это, безусловно, максимизировало модульность обновлений, влияющих на другие аспекты системы, связь логики между репозиториями не давала обнаружить изменения во время компиляции. Это означало, что ваше независимое развертывание внезапно превратилось бы в панический откат, если бы вы не жонглировали созвездиями репозиториев при каждом изменении.
Оптимизация микросервисной архитектуры
Я понял, что живу в мрачном будущем через пару лет после старта проекта. Без авторитарной власти над платформой и без последовательного набора правил помимо “это не работает, давайте попробуем что-то новое” я понял, что застрял. Мне нужно было продолжать строить кварталы в этом городе-чудовище, иначе я стал бы его жертвой.
И вот наконец появился Нил Форд со своей книгой “Архитектура программного обеспечения: сложные моменты”, который, наконец, ответил на вопрос, который меня интересовал годами: насколько микро мы должны делать свои микросервисы?
В духе предыдущей книги «Основы архитектуры программного обеспечения: инженерный подход», он смотрит на мир не через призму добра и зла, а с точки зрения вычислимых оптимальных соотношений в архитектуре. Хотя он не может сказать вам, что делать с вашей конкретной системой, он может привести точные критерии для анализа и указать на силы (forces), действующие в нескольких областях распределенных систем.
Две его основные силы, связанные с оптимизацией микросервисной архитектуры, удачно названы «интеграторами и дезинтеграторами», что описывает, как они влияют на объем компонентов при разграничении ответственности. В идеальной системе, состоящей из микросервисов, это будет ориентиром для определения границ между API, базами данных, событиями и даже кодовыми базами, лежащими в основе вашей платформы.
Более простое множество для понимания — это силы-дезинтеграторы, которые большинство разработчиков широко применяют во всех школах программирования. От нисходящего проектирования, основанного на дезагрегации проблем, до объектно-ориентированного программирования (ООП), разбивающего код на наборы связанных файлов, дезинтеграция — это то, что разработчики делают чаще всего, хотя, возможно, и не лучше всего.
Для более ясной формулировки и с четким пониманием того, что давать хорошие названия сложно, в этой статье я позволил себе переименовать его силы, так как считаю, что для них можно подобрать более описательные имена, состоящие из одного слова:
Пять сил, разделяющих компоненты вашей системы, включают:
Специфика (Specificity) — Область действия и функция компонента. Сумма его требований (функциональных и нефункциональных), определяющих его назначение в системе. Если у вас есть один сервис, выполняющий две разные задачи, вы, вероятно, захотите его разделить.
Волатильность (Volatility) — Скорость изменения алгоритмов и схем, лежащих в основе сервиса, чаще всего определяемая количеством пересмотра кода. Если у вас есть постоянная цепочка пул реквестов для одной папки в репозитории, но остальные не были затронуты в течение целого года, вы можете разделить этот компонент, чтобы отразить различную волатильность.
Масштабируемость (Scalability) — Когда функциональные возможности используются совершенно по-разному, это напрямую влияет на то, насколько независимо вы можете масштабировать поведение в своей системе. Если какая-то область, имеющая столько пользователей, сколько в шумном центр города, граничит с фичами, которые сравнимы с бесплодной пустыней, вам следует рассмотреть возможность разделения их эволюции.
Безопасность (Security) — Нефункциональное требование безопасности может само по себе формировать ландшафт системы. Если компонент имеет гораздо более конфиденциальные данные, связан с более строгими правилами или требует тщательного изучения со стороны специализированной группы внутри вашей компании, вам следует создавать его с нуля самому.
Расширяемость (Extensibility) — Что из себя представляет распределенный эквивалент наследования? В микросервисной архитектуре это динамическое связывание сервисов таким образом, что один расширяет функциональность другого. Если у вас есть сервис, который фактически является “базовым поведением” для других, рефакторинг его в качестве общей основы, скорее всего, сэкономит ваши усилия, хотя это и связано с некоторыми рисками.
Когда я построил свою вторую бессерверную систему, которая так и не трансформировалась в дискретные микросервисы, я настолько переборщил с этими дезинтеграционными силами, что каждая фича превратилась в макаронину в моей тарелке бессерверного спагетти. Если бы я знал о силах, противодействующих этому, мог бы я предусмотрительно объединить функционал в более легко развивающиеся компоненты?
Четыре силы-интеграторы, указанные в книге, на этот раз названные гораздо ближе к их описаниям:
Транзакции (Transactions) — Распределенные транзакции — это процессы, требующие многократных операций записи и чтения для успешной или неудачной обработки определенного действия в вашей системе. Они порождают такие сложные задачи, что могут легко указать, что действительно связано с предметной областью. Если у вас сложная транзакция, особенно требующая атомарности, это может быть поводом для сохранения этого функционала в одном микросервисе.
Рабочий процесс (Workflow) — Немного отличаясь от транзакций, рабочий процесс — это идея о том, что взаимосвязанные предметные области могут переплетаться друг с другом на такой регулярной основе, что разделение их реализации является пустой тратой усилий. Если вы обнаружите, что слишком часто проталкиваете эквивалент отношений по внешнему ключу в свои распределенные хранилища данных или вызываете/обрабатываете события между двумя сервисами, вы можете объединить их.
Общий код (Shared Code) — Поговорка “не повторяйся” (Don’t Repeat Yourself - DRY) имеет сложную историю с микросервисами. Очень популярная концепция ООП, во многих случаях и в распределенных системах - “лучше писать все дважды” (Write Everything Twice - WET), чтобы снизить риск опасного изменения поведения потребителей общего кода. Я пытался смягчить это, создавая версионированные npm-модули для совместного использования кода между проектами, но это привнесло свой слой спагетти. Во многих случаях мне хотелось бы просто поместить функции в один микросервис, чтобы можно было реализовать одну DRY реализацию.
Отношения (Relationships) — Отношения между предметными областями или данные, которыми они должны делиться, могут быть очень распространенным фактором объединения сервисов. Если вы обнаружите, что моделируете отношения “один к многим” или “многие к многим” в микросервисах какими-то замысловатыми способами или всегда разветвляетесь на другие сервисы, чтобы заполнить коллекции в своих ответах, это признак того, что эти отношения функционально связывают вашу систему, поэтому вы можете объединить их один сервис.
Размышляя об этих силах-интеграторах сейчас, кажется очевидным, что я должен был стремиться к тому, чтобы в системе было чуть меньше границ. Если бы я мог вернуться в прошлое, чтобы предотвратить трещины в фундаменте моего нового цифрового Парфенона, поскольку мы оставили после себя чистый монолит, я бы, вероятно, сделал так:
Сторонние сервисы как точки расширяемости — Я бы увидел, что использование сторонних сервисов (например, Stripe для платежей) вызывает множество конфликтов расширяемости (дезинтегратор) и отношений (интегратор) в процессе эволюции системы. Я бы создал микросервисы, которые выполняли бы роль платежных систем, но не допускали утечки реализации в сторонние сервисы в столь значительной части системы. Наличие стороннего SDK в дюжине мест — это тоже “общий код” с его пагубным жизненным циклом.
WET против DRY — Я считаю, что DRY по-прежнему предпочтительнее WET, когда дело доходит до кода; однако это никогда не бывает однозначным выбором. У меня есть общие версионируемые библиотеки (например, через npm-пакеты) как средство внесения внешних изменений в достаточно безопасную статическую связь, и я все же во многих случаях предпочел бы это динамической связи, когда для ввода в процессе всегда запрашивается общий сервис. Но реальное снижение риска, вероятно, заключается в том, чтобы обеспечить совместное использование кода в первую очередь потому, что он находится в пределах одной предметной области, в одном репозитории, что составляет четко определенный микросервис.
Сознательный выбор в отношении распределенных транзакций — Если бы у меня была возможность вернуться и сделать все снова, я бы задал больше вопросов о том, как меняются данные, а не о том, как они хранятся. В то время как схема является очень важной основой в ООП и требует внимания в микросервисах, заранее определенная манера при внедрении сложных изменений сохранил бы множество багов и привел бы к плохим производственным данным.
Заключение
Несмотря на отсутствие дальновидности, на мой взгляд, ни одно общество или систему нельзя назвать «утопией». Основной вывод обеих книг заключается в том, что в архитектуре очень мало решений типа «добро против зла»; практически все это просто компромиссы. Однако я думаю, что мы можем приостановить скатывание в антиутопию в проектах, когда они начнут давить нас сложностью распределенных систем.
Перечисленные выше силы-интеграторы и -дезинтеграторы можно применить к любой системе, от микросервисов правильного размера до структуры папок в вашем монолите. Они будут в моем фокусе в следующий раз, когда я начну разбивать свой монолит на что-то лучшее, более сильное и быстрое — хотя и заведомо более сложное.
Будущее может быть лучше прошлого. Однако мы должны научиться ставить человеческие потребности выше технических возможностей; в противном случае история повторяется, как бесконечный цикл, копирующийся во времени.
«Будущее наступило — просто оно пока распределено неравномерно.» — Уильям Гибсон
Чтобы обеспечить асинхронную связь между микросервисами, нужен брокер сообщений. Они бывают разные, но чаще остальных встречаются Kafka и RabbitMQ. У каждого из них есть свои особенности, плюсы и минусы.
Приглашаем всех желающих на открытое занятие, на котором познакомимся с основными принципами работы этих брокеров, а также посмотрим использование этих брокеров в live demo. Урок пройдет в рамках онлайн-курса "Microservice Architecture".
ArLeKiN_O_o
Не плодить микросервисы там, где они не нужны, и не раздувать монолит там, где он потащит корабль на дно.