Монолиты
Красивое слово монолит (monolith) описывает хорошо известный, наиболее часто используемый способ разработки программного продукта. Ваша команда определяется с набором требований к продукту и делает примерный выбор технологий и архитектуры. Далее вы создаете репозиторий для исходного кода (чаще всего GitHub), выделяете общую функциональность и библиотеки (пытаясь сократить количество повторяющегося кода, DRY dont repeat yourself!), и вся команда добавляет новый код и функциональность в этот единственный репозиторий, как правило, через ветви кода (branch). Код компилируется единым блоком, собирается одной системой сборки, и все модульные тесты прогоняются также сразу, для всего кода целиком. Рефакторинг и масштабные изменения в таком коде сделать довольно просто.
Однако, если брать разработку в облаке, и зачастую мгновенно и кардинально меняющиеся требования современных Web и мобильных приложений, описанные удобства грозят некоторыми недостатками.
Склонность к единой технологии
Единый репозиторий кода и одна система сборки естественным образом ведут к выбору основной технологии и языка, которые будут исполнять большую часть работы. Компиляция и сборка разнородных языков в одном репозитории неудобны и чрезмерно усложняют скрипты сборки и время этой сборки. Взаимодействие кода из разных языков и технологий не всегда легко организовать, проще использовать сетевые вызовы (HTTP/REST), что еще сильнее может запутать код, который находится рядом друг с другом, однако общается посредством абстрактных сетевых вызовов.
Тем не менее, для каждой задачи есть свой оптимальный инструмент, и языки программирования не исключение. Микросервисы, максимально разбивая и изолируя код частей системы, дают практически неограниченную свободу в выборе языка, платформы и реализации задачи, без взрывной сложности сборки проекта. Как мы вскоре увидим, контейнеры с блеском справляются с задачей легковесной виртуализации, и совершенно различные технологии способны с легкостью взаимодействовать друг с другом.
Сложность понимания системы
Часто говорят, что большая, созданная единым монолитом система сложна для понимания для новых членов команды. В мире технологий нередко размер команды резко растет, требуется срочно создать новую функциональность, и ключевым фактором становится скорость начала работы с ней и ее кодом ранее незнакомых с ней программистов. Мне кажется, что это довольно неоднозначный момент, и качественно сделанная система с разбиением на модули, правильной инкапсуляцией и скрытием внутренних винтиков системы будет не сложнее для понимания, чем сеть из десятков микросервисов, взаимодействующих по сети. Все зависит от дисциплины и культуры команды.
Но в общем случае стоит признать, что созданная командой (с ее внутренней дисциплиной и культурой) система скорее будет более прозрачной и понятной в виде микросервисов и качественно разделенных друг от друга репозиториев, чем в виде огромного кода размером в сотни тысяч строк, особенно если новый программист начинает работу над четко определенной задачей в одном микросервисе.
Трудность опробования инноваций
Монолитная, сильно связанная система, где код может легко получить доступ к другим модулям, и начать использовать их в тех же благих целях не делать ту же работу заново (общие модули и библиотеки), может в результате затруднить создание новых возможностей, не способных идеально вписаться в существующий дизайн.
Представим себе, что мы написали отличную биржу для криптовалют. Неожиданно появляется новая валюта, но правила работы с ней совершенно не похожи, и просто не будут работать с нашими системами обработки заказов. В случае монолитной системы нам надо вносить изменения в общий код, и продумать множество граничных случаев, чтобы обработка всех валют могла сосуществовать. В концепции микросервисов идеально было бы обработку новой валюты отвести совершенно новому микросервису. Да, придется заново писать много похожего кода, но в итоге эта работа будет идеально соответствовать требованиям. Более того, если эта новая криптовалюта окажется «пустышкой», ненужный микросервис удаляется, нагрузка на систему и ее сложность немедленно снижается сделать такой рефакторинг в сильно связанном коде монолита может быть непросто, да и будет это не самым первым приоритетом.
Дорогое масштабирование
Монолитное приложение собирается в единое целое и, в большинстве случаев, начинает работать в одном процессе. При возрастании нагрузки на приложение возникает вопрос увеличения его производительности, с помощью или вертикального масштабирования (vertical scaling, усиления мощности серверов на которых работает система), или горизонтального (horizontal scaling, использования более дешевых серверов, но в большем количестве для запуска дополнительных экземпляров (replicas, или instances).
Монолитное приложение проще всего ускорить с помощью запуска на более мощном сервере, но, как хорошо известно, более мощные компьютеры стоят непропорционально дороже стандартных серверов, а возможности процессора и размер памяти на одной машине ограничены. С другой стороны, запустить еще несколько стандартных, недорогих серверов в облаке не составляет никаких проблем. Однако взаимодействие нескольких экземпляров монолитного приложения надо продумать заранее (особенно если используется единая база данных!), и ресурсов оно требует немало представьте себе запуск 10 экземпляров серьезного корпоративного Java-приложения, каждому из них понадобится несколько гигабайт оперативной памяти. В коммерческом облаке все это приводит к резкому удорожанию.
Микросервисы решают этот вопрос именно с помощью своего размера. Запустить небольшой микросервис проще, ресурсов требуется намного меньше, а самое интересное, увеличить количество экземпляров можно теперь не всем компонентам системы и сервисам одновременно (как в случае с монолитом), а точечно, для тех микросервисов, которые испытывают максимальную нагрузку. Kubernetes делает эту задачу тривиальной.
Архитектура на основе сервисов (SOA)
Более гибким решением является разработка на основе компонентов, отделенных друг от друга, прежде всего на уровне процессов, в которых они исполняются. Архитектуру подобных проектов называют ориентированной на сервисы (service oriented architecture, SOA).
Разработка приложения в виде компонентов, и стремление свести сложные приложения к набору простых, хорошо стыкующихся между собой компонентов известна видимо с тех самых времен как программы стали разрабатывать. По большому счету подобная техника применима во многих областях человеческой деятельности.
Часто говорят, что классическая архитектура на основе сервисов отличается от ставших популярными сейчас «микро» -сервисов тем, что многое отдает на откуп «посредникам» (middleware), например системам обмена, настройки и хранения сообщений между компонентами и сервисами, так называемым «интеграционным шинам» (ESB, enterprise service bus). Микросервисы же эпохи облака минимизируют зависимость от работы со сложными посредниками и их правилами и проводят свои операции напрямую друг с другом.
Микросервисы по Мартину Фаулеру
Мартин Фаулер знаменит своими, как правило, очень успешными попытками найти структуру и систему в динамичном, хаотичном мире программирования, архитектуры, хранения данных, и особенно в мире сложных, высоконагруженных, распределенных приложений эпохи Интернета. Конечно же он попытался проанализировать и упорядочить волну популярности микросервисов.
Если попытаться вчитаться в цитату Мартина в начале этой главы, становится понятно, что микросервисы явление вне рамок известных ранее архитектур, они не совсем укладываются в стандарты, и довольно разнообразно применяются на практике. Тем не менее, характерные черты микросервисов, используемые Мартином, очень хороши для первого, вводного анализа этой архитектуры, что нам для знакомства и нужно. Давайте посмотрим.
Сервисы как компоненты системы
Компонент (component) одна из основополагающих идей в разработке программ. Идея проста компонент отвечает за определенную функцию, интерфейс API, или целый бизнес модуль. Главное компонент можно убрать из системы, заменив на другой, со сходными характеристиками, без больших изменений для этой системы. Замена компонентов целиком встречается не так часто, но независимое обновление (upgrade) компонента без потери работоспособности системы в целом очень важно для легкости ее обновления и поддержки. Классический пример качественная библиотека (к примеру, JAR), модуль, или пакет, в зависимости от языка, которую можно подключить и использовать в своем приложении.