В большом городе у каждого здания свое назначение, но из всех зданий вместе формируется функциональная городская среда. В этом суть архитектурного подхода микросервисов, при котором программные приложения строятся как совокупность независимых сервисов, работающих вместе. До микросервисов был монолит — как большое, величественное здание, под крышей которого все размещалось. Обычно это трехуровневая структура: представление или пользовательский интерфейс, сервисы — бизнес-логика — и база данных. Технологии развивались, монолит усложнялся, и индустрия устремилась к гибким стратегиям проектирования. Появилась сервисно ориентированная архитектура — предвестница микросервисов с ее акцентом в программной разработке на отдельных, основанных на сервисах компонентах.
Суть архитектуры микросервисов — в построении ПО как сети автономных сервисов. Каждый сервис работает независимо, порой в собственном процессе или на отдельном сервере. «Общаются» службы посредством протоколов: синхронных вроде HTTP или REST и RPC — для мгновенных взаимодействий и асинхронных методов вроде брокеров сообщений, например AWS SQS и Kafka, — для отложенных.
Микросервисы привлекательны многочисленными преимуществами: горизонтальным масштабированием, восстанавливаемостью, высокой доступностью, низкой задержкой, специализированной функциональностью, отказоустойчивостью, оптимизированными развертываниями, свободным применением технологий. Но имеются и трудности: высокие операционные требования, сложный контроль данных, зависимость от надежности сети.
И все же преимущества часто оказываются весомее. За последнее десятилетие Amazon, Netflix, Google, Facebook, eBay, Apple и другие технологические гиганты перешли на микросервисы, пожиная плоды этой современной архитектурной парадигмы.
Монолит

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

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

Взаимодействие и интеграция сервисов
В микросервисной архитектуре сервисы «общаются» с помощью интерфейсов прикладного программирования API, обычно это взаимодействие в сети по HTTP. Нагрузку на сеть, размер пакетов, время маршализации/демаршализации и задержку уменьшают применением двоичных протоколов вроде protobuf. Эти API похожи на интерфейсы в кодовой базе: ими указываются предлагаемые микросервисом функции, их требования по вводу и ожидаемый вывод, при этом упрощаются детали реализации и эволюция сервисов.
Сервисы «общаются» по сетям — внутрикорпоративной интранет или глобальной интернет. Это взаимодействие может быть синхронным или асинхронным.
Синхронное взаимодействие: этот подход с шаблоном «запрос-ответ» — простая для понимания блокирующая операция с мгновенным подтверждением ее успеха или неудачи. Так, успех или неудача загрузки изображений в AWS S3 немедленно подтверждается синхронным запросом, на что указывается кодами ответа: 200 — успех, 4xx/5xx — неудача. При этом вызывающая служба блокируется до получения ответа. REST и RPC — типичные примеры синхронного взаимодействия. Главный недостаток — зависимость от доступности последующих сервисов: «падение» используемого сервиса, например S3, сказывается на функциональности и на всей цепочке сервисов.
Асинхронное взаимодействие: в этом шаблоне отправителем выдается сообщение и немедленно получается подтверждение, фактическая обработка осуществляется позже. Этот метод обычно реализуется шинами сообщений: AWS SQS, Kafka или RabbitMQ. Особенно эффективен он для задач вроде обработки заказов, где не требуются немедленные ответы. Сообщение о выполненном заказе отправляется на шину и позже обрабатывается сервисом получателя.
У асинхронного обмена сообщениями имеются значительные преимущества. Микросервисы им разделяются, так что «падение» одного не обязательно сказывается на других. Кроме того, он эффективно справляется с обратным давлением — когда одним сервисом запросы отправляются быстрее, чем обрабатываются другими, — и шаблоном «разветвления», когда последующими сервисами одновременно обрабатывается один запрос.

Что выбрать: синхронное взаимодействие или асинхронное? Здесь учитываются различные факторы: характер задачи, требования по производительности, необходимость в надежности, проектирование системы в целом. Универсального ответа нет: чтобы взвесить все «за» и «против», требуется компетенция инженера. Например, асинхронный подход нецелесообразен для SQL-запроса, где ожидаются немедленные результаты. А вот при покупке на Amazon немедленная обработка заказа менее важна, чем получение подтверждения о его размещении и своевременном прибытии.
Границы сервисов
Фундаментальный вопрос микросервисной архитектуры: «Насколько крупным должен быть сервис и из чего состоять?»
Разделение микросервисов на основе бизнес-возможностей, то есть предметной области. Возьмем классический пример интернет-магазина, его ключевые бизнес-возможности: пользователи, заказы, доставка и налоги. Вокруг них и организуются сервисы.
Когда на сервисы разделяется программная система покрупнее, учитываются такие принципы:
- Принцип единственной ответственности, когда у каждого сервиса имеется одно основное назначение и одна причина для изменений. Например, необходимость изменений в сервисе калькуляции продаж не обусловливается изменениями в сервисе заказов: эти два компонента остаются независимыми, «общаясь» через API при необходимости.
- Автономия, когда сервисы максимально автономны и зависимости от других сервисов минимальны. Например, сервисы проектируются так, чтобы для поддержания общей доступности при сбое сервиса заказов их функционирование продолжалось. Сервисы разделяются с помощью шины сообщений.
Детализированностью микросервиса называются его размер и область в приложении. Детализированными сервисами выполняется конкретная функция. Ими обеспечивается гибкость, но усложняются межсервисное взаимодействие и координация. Функциональность недетализированных сервисов шире. Они рискуют превратиться в мини-монолиты, потенциально теряя преимущества вроде независимой масштабируемости, хотя накладные расходы межсервисного взаимодействия здесь ниже.
При проектировании сервисов и определении их ответственности важно не переусердствовать. Удобнее начать с меньшего количества сервисов и разбивать их дальше при необходимости, нежели сразу создавать сложную систему со значительными недостатками.
Шаблон ограниченного контекста — популярный подход к установлению границ сервисов. Это ключевая концепция предметно-ориентированного проектирования. При помощи ограниченного контекста устанавливаются границы модели конкретной предметной области: ее сущностей, сервиса, а также общего, единого языка — в конкретном контексте. Этой концепцией определяются четкие границы микросервисов, каждый из которых соответствует конкретному ограниченному контексту, где инкапсулируются вся логика и данные этой предметной области.

Развертывание и сборка
Ключевое преимущество микросервисной архитектуры — упрощение быстрой разработки и выпуска ПО для клиентов. Это достигается поэтапным безопасным подходом с выверенной стратегией сборки.
Непрерывная интеграция и непрерывное развертывание — это практики частой интеграции изменений кода в репозиторий проекта, то есть в сервис. Каждое предлагаемое изменение — коммит — проверяется автоматизированным процессом сборки и тестирования, в ходе которого быстро выявляются ошибки. При их обнаружении сборка останавливается, слияние с главной веткой предотвращается, этим поддерживаются безопасность кода и его готовность к продакшену. После слияния с главной веткой — посредством запроса на включение изменений в репозиторий проекта — начинается этап непрерывного развертывания. Это автоматическое — по окончании сборки — развертывание в средах тестирования и/или продакшена всех изменений кода. Среди многочисленных инструментов сборки выделяются AWS CodePipeline, Jenkins, CircleCI и GitHub Actions.
Микросервисам сборка очень кстати благодаря их архитектурным преимуществам. Во-первых, независимость каждого сервиса с его собственным конвейером сборки, в котором изолированно выполняются сборка, тестирование и развертывание. То есть проблемы в конвейере сборки одного сервиса — сломанные зависимости или неудачные тесты — не сказываются на непрерывном развертывании сервисов других команд.
Во-вторых, благодаря небольшим кодовым базам циклы развертывания микросервисов короче. Сборки здесь обычно быстрее, чем в монолите: требуется собрать код не всего приложения, а только конкретного сервиса.
А еще благодаря независимому развертыванию сервисов применяются более сложные стратегии тестирования и безопасности в средах продакшена: сине-зеленое и канареечное развертывания. Последним, например, изменения перед полномасштабным запуском постепенно распространяются на ограниченное подмножество пользователей, так минимизируется риск появления в продакшене проблемных версий ПО.

Наряду с преимуществами ускоренной разработки и развертывания, обеспечиваемыми микросервисами и сборкой, имеются и подводные камни. Так, при независимом развертывании сервисов и взаимодействии через API-интерфейсы повышается риск появления изменений, чреватых сбоями API. В отличие от монолитных архитектур, здесь эти изменения не обнаруживаются во время сборки. Поэтому для выявления проблем интеграции, прежде чем они окажутся в продакшене, необходимо всестороннее комплексное тестирование. Внесение критических изменений сопровождается версионированием API.
Конвейеры сборки реализуются и в монолитных архитектурах. Только процесс сборки здесь отличается тем, что программное приложение целиком развертывается одним конвейером. Это обычно чревато длительными, многочасовыми процессами сборки, сказываясь на динамичности работы команды. Модель общего хранилища тоже часто чревата сбоями сборки — сказываются изменения кода в разных командах. В микросервисах же циклы разработки и развертывания ПО намного плавнее и быстрее.
Ключевые стратегии надежной архитектуры микросервисов
Обозначим дополнительные соображения, учитываемые при создании ПО с микросервисной архитектурой. Каждый пункт достоин целой книги, мы же ограничимся кратким описанием.
1.Проектирование с учетом сбоев: сервисы проектируются с возможностью восстановления, функционирования даже при сбое других сервисов. Важно реализовывать механизмы вроде выключателей, добавлять процедуры нейтрализации неисправности, включать тайм-ауты и повторы, обеспечивать надежное тестовое покрытие. Иначе преимущества микросервисной архитектуры теряются.
2. Децентрализованное управление данными: каждым сервисом контролируются собственные, принадлежащие ему данные, как итог — выше восстанавливаемость, слабее связанность. Это ключевая особенность микросервисов, благодаря которой повышаются их автономность и масштабируемость.
3. Версионирование API: важно, чтобы сервисы развивались без сбоев имеющихся клиентов. Вот стратегии версионирования API:
- версионирование единообразного идентификатора ресурса: например, с помощью
/v1/orders
; - версионирование с указанием версии как параметра: например,
/orders?version=1
; - версионирование HTTP-заголовками: где информация о версии включена в HTTP-заголовки.
4. Шаблон API-шлюза: для клиентов это единая точка входа в систему. Вместо того, чтобы узнавать URI каждого сервиса, клиентами отправляются запросы в API-шлюз, например www.myproduct.com/orders
, где затем выявляется нужный сервис и запрос соответствующим образом перенаправляется. API-шлюзами также контролируется сквозная функциональность: аутентификация, авторизация, ограничение скорости, балансировка нагрузки, кеширование.
5. Обнаружение сервисов: в микросервисах сервисы обнаруживаются и «общаются» динамически. Это важно, ведь здесь активно применяется горизонтальное масштабирование — часто разворачиваются или сворачиваются новые экземпляры. Благодаря обнаружению сервисов микросервисы регистрируются в реестре сервисов при запуске. Когда один сервис подключается к другому, IP-адрес он разрешает динамически: из реестра сервисов извлекается список IP-адресов, связанных с конкретным сервисом.
Вот модели обнаружения сервисов:
- На стороне клиента: когда клиентом, то есть другим сервисом, запрашивается реестр служб и осуществляется маршрутизация.
- На стороне сервера: когда клиентами выполняются запросы через балансировщик нагрузки или API-шлюз, где затем запрашивается реестр служб и осуществляется маршрутизация.
На этих соображениях основывается реализация архитектуры микросервисов.
Заключение
Изучив сложный ландшафт микросервисов, приходишь к выводу: этот архитектурный стиль — больше, чем просто модное словечко в сфере технологий. Это коренной сдвиг в том, как создается, развертывается и сопровождается программное обеспечение. Микросервисы находятся в авангарде современной программной архитектуры — от обеспечения гибкости и восстанавливаемости до развития культуры технологических инноваций.
Разработчик вы, архитектор ПО или просто энтузиаст, в мире микросервисов вам предлагается куча возможностей и вызовов. Здесь начинаешь по-другому подходить к решению проблем, учитывая сложности распределенных систем.
Однако переход на микросервисы — решение не технологическое, а стратегическое. Его нужно тщательно проработать, спланировать и адаптировать. Как и при любом большом изменении, здесь не обходится без препятствий, но и потенциальные выгоды немаленькие — масштабируемость, гибкость, эффективность.
Помните: будущее разработки ПО — за модульностью, восстанавливаемостью, адаптируемостью. И все это достижимо. Начните с малого, мыслите широко и участвуйте в этой микросервисной революции.
Читайте также:
- События на стороне сервера: к чему должен быть готов разработчик
- Эволюция монолитных систем
- Продвинутые концепции Kafka для старшего инженера-программиста
Читайте нас в Telegram, VK и Дзен
Перевод статьи Nadar Alpenidze: Microservices Explained: Why Every Developer Should Care