Post

Эволюция архитектур: от монолита к микросервисам

Эволюция архитектур: от монолита к микросервисам

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

Монолитная архитектура (Monolith)

В начале 2000-х годов монолитная архитектура была основным выбором для большинства веб-приложений. Это объяснялось несколькими причинами:

  • Простота разработки: все компоненты системы (UI, бизнес-логика и доступ к данным) находились в одном приложении.
  • Стандартный подход в индустрии: доминирование Java EE и .NET как стандартов корпоративной разработки, которые по умолчанию предполагали монолитные архитектуры.
  • Ограниченные требования к масштабируемости: пользовательские нагрузки большинства систем позволяли обслуживать их силами одного сервера, без сложных решений по горизонтальному масштабированию.
  • Отсутствие зрелых альтернатив: контейнеризация, брокеры сообщений и облачные сервисы либо отсутствовали, либо только начинали развиваться и не имели широкого распространения в индустрии.

Для EduSphere на начальном этапе это означало, что будет единое приложение для управления студентами, преподавателями, курсами и платежами. Все сущности будут храниться в одной базе данных. Простая схема деплоя: сборка приложения и развертывание на единственном сервере приложений (или сервлет-контейнере).

Типичный EJB-компонент в монолите:

1
2
3
4
5
6
7
8
9
10
@Stateless
public class EnrollmentService {

  @PersistenceContext
  private EntityManager em; // Общая БД для всех сервисов

  public void enrollStudent(Student s, Course c) {
    // UI → логика → БД — все в одном процессе
  }
}

Выглядеть это могло следующим образом:

Монолитная layered архитектура EduSphere

Весь функционал системы был собран в одном развертываемом артефакте (обычно WAR или EAR). В нем компоненты пользовательского интерфейса, бизнес-логики и доступа к данным (Data Access Layer) располагались в виде слоев внутри общего кода. Такая структура известна как слоеная (layered) архитектура.

Артефакт развертывался в сервлет-контейнере (например, Apache Tomcat) или в сервере приложений (WildFly, WebLogic, IBM WebSphere, JBoss), если использовались компоненты EJB.

Взаимодействие с базой данных осуществлялось через слой DAO (Data Access Object), который напрямую выполнял SQL-запросы или работал через ORM-фреймворки (если они применялись).

Разработка и развертывание (CI/CD)

На этапе монолита процесс разработки и развертывания выглядел следующим образом:

  • Все команды работали с единой кодовой базой в общем репозитории (Single Code Repo).
  • Код собирался с помощью инструментов сборки (Ant, Ivy, позднее Maven или Gradle).
  • Для автоматизации сборки и деплоя использовались системы CI/CD — чаще всего Hudson или Jenkins.
  • Артефакт сборки (обычно EAR или WAR) включал в себя весь функционал приложения: UI, бизнес-логику и доступ к данным.
  • После сборки приложение развртывалось в сервлет-контейнере (Apache Tomcat) или сервере приложений (JBoss).

Все приложение использовало единую базу данных.

Скелетон приложения мог быть таким, например:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
EduSphere/
├── src/                           (Исходный код: UI, бизнес-логика, DAO и модели) 
│   ├── com/edusphere/ui/          (JSP, Servlets)
│   ├── com/edusphere/business/    (Бизнес-логика)
│   ├── com/edusphere/dao/         (Data Access Objects, JDBC)
│   └── com/edusphere/model/       (DTO, сущности)
├── web/                           (Фронтенд-часть и конфигурации сервера) 
│   ├── WEB-INF/                   (Java EE/Spring конфиги)
│   │   ├── web.xml
│   │   ├── applicationContext.xml (если подключали Spring)
│   └── static/                    (CSS, JS, изображения)
├── lib/                           (сторонние библиотеки, JAR'ники)
├── build.xml                      (Ant build script)
├── README.txt
└── db/                            (SQL-скрипты для создания схемы и начальных данных)
    ├── schema.sql
    └── seed_data.sql

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

Рассмотрим ключевые преимущества такого подхода.

  • Простая разработка и деплой для небольших команд и приложений. Монолитная архитектура подразумевает единую кодовую базу и общее приложение. Это упрощает процесс проектирования, так как не требуется продумывать механизмы взаимодействия между отдельными сервисами или сложную инфраструктуру. Для небольших команд это означает возможность быстро разрабатывать и развертывать новые функции без необходимости координировать изменения между множеством сервисов.
  • Меньше кросс-функциональных задач (security, monitoring) на старте. В монолите большинство кросс-функциональных аспектов (например, безопасность, мониторинг, логирование) реализуются централизованно. Разработчикам не нужно заботиться о том, как согласованно применять эти аспекты во множестве отдельных сервисов, как это требуется в микросервисной архитектуре. Это упрощает старт проекта и снижает начальные накладные расходы.
  • Лучшая производительность: нет сетевых задержек между компонентами. В монолитных приложениях все модули взаимодействуют между собой через вызовы внутри процесса (in-process calls), которые значительно быстрее сетевых запросов. Это особенно важно для операций, требующих высокой скорости отклика или большой пропускной способности. Отсутствие сетевых задержек и расходов на сериализацию данных позволяет достигать более высокой производительности по сравнению с распределенными системами.

Для сравнения:

Тип вызоваПримерВремя (примерно)
Вызов метода в JVMpaymentService.process()~100 нс
HTTP-запрос по loopbackcurl http://localhost:8080~1 мс
HTTP-запрос по сетиhttp://service-b.internal1–10 мс и выше

⚠️ Loopback — это сетевой интерфейс, который указывает на тот же компьютер, где выполняется программа. Типичный адрес loopback-интерфейса: 127.0.0.1 или localhost. Даже если все работает на одной машине, HTTP-запрос по loopback все равно проходит через весь стек: сериализация/десериализация JSON, создание TCP-соединения, сетевой стек ОС, парсинг запроса и т.д.

Ограничения монолита

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

  • Сложность внедрения новых технологий. Все компоненты монолитного приложения связаны общей кодовой базой и общим циклом сборки. Внедрение новой технологии в одной части приложения (например, переход на новый фреймворк или СУБД) часто требует изменений во всех слоях и модулях. Это увеличивает сложность миграции и может привести к непредвиденным ошибкам в других частях системы.
  • Ограниченная гибкость. Монолитная архитектура плохо подходит для команд, которые хотят работать независимо друг от друга. Любые изменения в одной части системы требуют пересмотра и тестирования всего приложения. Это ограничивает возможности команды быстро адаптироваться к меняющимся требованиям бизнеса.
  • Единая кодовая база. Большие монолиты со временем становятся трудно поддерживаемыми. Из-за большого количества взаимосвязей, изменения в одном модуле могут повлиять на другие, что увеличивает вероятность ошибок и усложняет тестирование. Работа с общей кодовой базой также затрудняет параллельную разработку несколькими командами.
  • Отсутствие устойчивости к сбоям (Fault Tolerance). Если один модуль монолитного приложения выходит из строя (например, из-за ошибки в коде или проблем с памятью), это может привести к сбою всего приложения. Монолитные системы обычно не предусматривают механизмов изоляции сбоев между модулями.
  • Любое обновление требует полного деплоя. Даже небольшие изменения, затрагивающие только одну функциональность, требуют пересборки и полного развертывания всего приложения. Это увеличивает время вывода обновлений в продакшн (deployment time) и повышает риск внесения непреднамеренных ошибок.

Монолиты сегодня

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

Со временем появилось несколько подходов к структурированию монолитных приложений:

  • Single-Process Monolith Классический монолит: все слои и компоненты запускаются в рамках одного процесса. Минимальная сложность, но максимальная связанность.

  • Modular Monolith Приложение физически единое, но логически разделено на отдельные модули с чткими границами ответственности. Это позволяет ограничить связанность и облегчить сопровождение кода. Пример:

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class EduSphereApp {

  public static void main(String[] args) {
    SpringApplication.run(EduSphereApp.class, args);
  }
}

@Module("payment")
public class PaymentConfig { /* … */ }
  • Distributed Monolith Формально приложение разделено на несколько развертываемых компонентов или сервисов, но в реальности между ними сохраняется сильная связанность (shared-базы данных, синхронные вызовы, общее ядро и пр), из-за чего оно унаследовало многие проблемы классического монолита.

Service-Oriented Architecture (SOA)

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

Чтобы справиться с новыми требованиями, архитектура программных систем эволюционировала в сторону сервис-ориентированной модели (SOA).

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

Сервис-ориентированная архитектура EduSphere

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

Причины перехода к SOA

Переход на сервис-ориентированную архитектуру был обусловлен несколькими ключевыми факторами:

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

Идеальный vs реальный SOA

В идеальном варианте SOA все клиенты — это отдельные приложения (например, Student Portal, Teacher Portal, Admin Panel), которые могут развертываться независимо друг от друга. ESB представляет собой отдельный компонент уровня middleware и развертывается как самостоятельный процесс или кластер. Каждый сервис (StudentService, CourseService и т. д.) по замыслу также должен быть независимым и развертываться отдельно.

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

Типичный стек технологий SOA

Обычный технологический стек того времени включал:

  • Enterprise Service Bus (ESB): IBM WebSphere ESB, Oracle Service Bus (OSB), Apache ServiceMix, Mule ESB.
  • Серверы приложений: IBM WebSphere Application Server, Oracle WebLogic Server, JBoss Application Server, Apache Tomcat (иногда использовался для менее сложных сервисов).
  • Фреймворки и экосистемы: Spring Framework (часто для сервисов), Java EE (EJB, JAX-WS/JAX-RS).
  • Коммуникации между сервисами: SOAP (основной стандарт в SOA), REST (начал использоваться ближе к 2010-м).
  • База данных: Oracle Database, IBM DB2, MS SQL Server.
  • CI/CD: Инструменты автоматизации сборки и деплоя чаще всего включали Jenkins, TeamCity или Bamboo. Однако во многих проектах сборка и развертывание оставались частично ручными или полуавтоматизированными.

Почему SOA не стало финальным решением?

Постепенно стало очевидным, что даже сервис-ориентированная архитектура (SOA) несет в себе определенные ограничения.

Централизованная шина как архитектурная ловушка

Интеграционная шина (IBM WebSphere ESB, Oracle OSB), задуманная как универсальный посредник, на практике создавала новые проблемы. Например, в образовательной платформе EduSphere маршрутизация платежных запросов через Oracle Service Bus добавляла заметную задержку из-за многоступенчатой трансформации данных (XML → JSON в реальном времени) и обязательной валидации сообщений (SOAP-конвертов). Это проявлялось особенно ярко при пиковых нагрузках, когда шина становилась узким местом системы и приводила к сбоям в других сервисах.

Типичная интеграция через ESB (Apache Camel):

1
2
3
4
from("jms:queue:payments")
  .id("paymentProcessor")
  .transform().xpath("/payment/amount") // XPath-парсинг ≈15ms
  .to("http4://legacy-system/api?bridgeEndpoint=true"); // Риск таймаута

Ограничения масштабирования

Сервисы в SOA-архитектуре редко масштабировались независимо. Типичным решением было добавление ресурсов (CPU/RAM) в существующие серверы приложений — дорогостоящий подход, который быстро достигал пределов эффективности. Например, увеличение мощности сервера WebLogic с 8 до 32 ядер давало лишь ~2.5x прирост. В отличие от этого, современные API-шлюзы позволяют гибко распределять нагрузку между отдельными сервисами. Например, роутинг запросов к платежному сервису можно настроить без переконфигурации всей системы:

1
2
3
4
5
6
7
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
        .route("payment_service", r -> r.path("/api/payments/**")
            .uri("http://payment-service:8080"))
        .build();
}

Жесткие стандарты интеграции

Использование SOAP и строгих WSDL-контрактов делало даже небольшие изменения в API трудоемкими. Модификация структуры платежного запроса в EduSphere требовала согласованного обновления всех клиентских приложений, что существенно замедляло развитие системы. Так, добавление поля discountCode в PaymentRequest могло занять несколько дней из-за согласований. Современные подходы к проектированию API, такие как версионирование через заголовки HTTP, предлагают более гибкие сценарии эволюции интерфейсов.

Экономика SOA

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

Хотя точные цифры варьировались в зависимости от масштабов проекта, усредненные данные по отрасли за 2015 год могли быть такими:

КомпонентОписание затратСтоимость
Oracle SOA SuiteЛицензирование (5 ядер)$250K
IBM WebSphereТехподдержка и обновления$180K
SAN-хранилищеВыделенный кластер БД$120K

Такие затраты были нормой в эпоху SOA.

Кейс миграции EduSphere мог выглядеть так - переход с Oracle ESB на открытый стек (Apache Camel + Kubernetes) позволил не только сократить затраты, но и повысить гибкость системы.

  • Маршрутизация платежей стала настраиваться через декларативные конфиги Camel вместо XML-шаблонов ESB.
  • Горизонтальное масштабирование под нагрузкой заняло минуты вместо недель.

Пример конфигурации Camel вместо ESB (миграция):

1
2
3
4
from("kafka:payments?brokers=cluster:9092")
  .id("paymentRouter")
  .filter().jsonpath("$[?(@.amount > 1000)]")
  .to("kubernetes://deployment/payment-service?namespace=prod");

Распространенные антипаттерны

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

Эволюционный тупик и пути выхода

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

Статьи серии

This post is licensed under CC BY 4.0 by the author.