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

Общее

Монолитная архитектура (monolithic architecture, монолит) — способ построения системы как единого приложения, в котором все модули и слои работают в одном процессе, обращаются к общей базе данных и поставляются одним развёртываемым артефактом. Внутри такого приложения компоненты взаимодействуют обычными вызовами методов — так же, как функции в одной программе, — а границы между ними существуют лишь на уровне исходного кода и условных соглашений. Слово «монолит» происходит от греческого monos («единый») и lithos («камень») и буквально означает «единый камень»: нечто цельное, не разложенное на части без разрушения целого.

Важно понимать исторический парадокс термина. До того как в 2014 году Мартин Фаулер и Джеймс Льюис в статье Microservices популяризировали микросервисы, слово «монолит» почти не употреблялось как название архитектурного стиля — приложение просто «было приложением». Обозначение возникло как контрастный термин: потребовалось дать имя тому, что микросервисы противопоставляют себе. Отсюда распространённое, но ошибочное восприятие монолита как «устаревшей» или «плохой» архитектуры. На самом деле монолит — это базовый, по умолчанию оправданный стиль, с которого начинается подавляющее большинство систем и относительно которого осмысляются все более децентрализованные подходы.

В эволюции архитектурных стилей монолит занимает первое и фундаментальное место. Исторически ему предшествовали мэйнфреймные системы с монолитными программами на ассемблере и Коболе; в 1990-е годы монолит приобрёл знакомую слоистую форму (представление — бизнес-логика — данные) в рамках клиент-серверных приложений и ранних веб-систем. В 2000-е годы сервис-ориентированная архитектура (SOA) с её шиной корпоративных сервисов (ESB) попыталась разнести монолит на взаимодействующие сервисы, а в 2010-е микросервисы переосмыслили эту идею на базе облака, контейнеров и CI/CD. Но несмотря на все эти волны, монолит не исчез и не собирается исчезать: подавляющее большинство бизнес-приложений в мире — как новых, так и унаследованных — построены именно монолитно.

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

Ключевую рекомендацию о месте монолита в практике сформулировал Мартин Фаулер в заметке MonolithFirst (2015): начинать следует почти всегда с монолита, а к разбиению на сервисы переходить, когда организационная боль от монолита — конфликты команд, медленные релизы, узкие места масштабирования — станет осязаемой. Сэм Ньюмен в книге Monolith to Microservices (2019) развил этот тезис в практическое руководство по постепенной миграции. Крис Ричардсон в Microservices Patterns (2018) рассматривает монолит как естественную отправную точку и описывает паттерны его эволюции — от модульного монолита до выделения сервисов. Таким образом, в зрелой архитектурной литературе монолит и микросервисы — не соперники, а этапы одного пути, и качество архитектуры определяется не выбором крайности, а своевременностью перехода между ними.

Виды монолитов

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

Единый (классический) монолит

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

Такая свобода удобна на старте: нет накладных расходов на инкапсуляцию, рефакторинг и поддержку интерфейсов, скорость разработки максимальна. Однако по мере роста системы свобода превращается в связность. Без дисциплины единый монолит вырождается в «большой шар грязи» (Big Ball of Mud) — антипаттерн, описанный Брайаном Фойером и Джозефом Йодером в 1997 году: систему без различимой архитектуры, где всё связано со всем, изменение одного места непредсказуемо ломает другие, а новый разработчик не может охватить устройство целого. Классические примеры — зрелые корпоративные ERP- и банковские системы, накапливавшие код десятилетиями.

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

Модульный монолит (modular monolith) — осознанный компромисс между простотой единого монолита и гибкостью микросервисов: по-прежнему единый процесс и единый артефакт развёртывания, но внутри него границы модулей проведены строго и принудительно выдерживаются. Каждый модуль владеет собственной областью ответственности, собственными таблицами (или схемой) базы данных и общается с соседями только через явно объявленный программный интерфейс — не через прямой доступ к чужим данным. Иначе говоря, модульный монолит применяет к внутренностям одного процесса те же принципы инкапсуляции и слабой связности, что микросервисы — к распределённой системе.

Идея не нова: она восходит к модульному программированию 1970-х (Дэвид Парнас, «о критерии разложения системы на модули», 1972) и предметно-ориентированному проектированию Эрика Эванса (Domain-Driven Design, 2003), где ограниченные контексты (bounded contexts) предметной области задают естественные границы модулей. Современный интерес к модульному монолиту как самостоятельной архитектуре подогрет успешным опытом компаний, сознательно отказавшихся от микросервисов: Shopify масштабирует огромный Rails-монолит, внутренне декомпозированный на «компоненты» (Ruby-модули с контролируемыми зависимостями); Basecamp (37signals) и Stack Overflow добиваются выдающейся эффективности именно за счёт продуманно структурированного монолита. Сэм Ньюмен в книге Building Microservices (2-е издание, 2021) и Алекс Будойлин (Shopify) популяризовали модульный монолит как полноправный архитектурный стиль, а не просто «монолит, который когда-нибудь разберут».

Критически важный инструмент модульного монолита — принудительное удержание границ. Одних соглашений в коде недостаточно: при отсутствии ограничений зависимости неизбежно расползаются. На практике границы выдерживают несколькими способами: статическим анализом зависимостей (например, ArchUnit в Java, NetArchTest в .NET, dependency-cruiser в JavaScript), отдельными сборками модулей (модуль как независимо компилируемая единица), архитектурными fitness-функциями (термин из Building Evolutionary Architectures Форта, Пэрса и Гэя, 2017), которые проверяют структуру в CI и ломают сборку при нарушении инкапсуляции. Без таких автоматических проверок модульный монолит быстро деградирует обратно в единый.

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

Распределённый монолит (distributed monolith) — не отдельный стиль, а распространённый антипаттерн: система формально разбита на отдельные сервисы и разворачивается независимо, но на практике связана настолько жёстко, что обладает всеми недостатками обоих миров — сложностью распределённой системы и негибкостью монолита. Признаки распределённого монолита: общая база данных, к которой обращаются несколько сервисов; синхронные цепочки вызовов, где каждый запрос последовательно проходит через многие сервисы; необходимость согласованного развёртывания нескольких сервисов одновременно; обновление, ломающее потребителей из-за отсутствия версионирования контрактов.

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

Когда выбирать монолит

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

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

Небольшая команда. Эксплуатация десятков сервисов требует зрелой платформы — CI/CD на каждый сервис, наблюдаемость, управление конфигурацией и секретами, дежурства. Команда из нескольких человек физически не может поддерживать эту инфраструктуру без ущерба для разработки продукта. Монолит же предъявляет минимальные требования к операциям: один конвейер, один артефакт, одна база данных. Сэм Ньюмен подчёркивает, что микросервисы — это инвестиция в организационную гибкость, которая окупается только при достаточном количестве команд; для небольшой команды эта инвестиция превращается в чистые накладные расходы.

Простой или недостаточно изученный домен. Если предметная область узка и не выражает чётких ограниченных контекстов (например, утилита, внутренний портал, MVP), принудительное разбиение на сервисы искусственно усложнит систему. Даже методология Domain-Driven Design рекомендует начинать с единой модели и выделять контексты по мере того, как в них проявляются разные употребления одних и тех же терминов (то есть разные контексты). Пока таких конфликтов нет, единая модель в монолите проще и точнее отражает реальность.

Низкая или умеренная нагрузка. Современное железо способно обрабатывать огромный трафик одним процессом: вертикальное масштабирование (более мощный сервер) покрывает потребности подавляющего большинства бизнес-приложений. Если пиковая нагрузка измеряется тысячами, а не миллионами запросов в секунду, а профиль нагрузки равномерен (нет одной функции, на которую приходится 99 % трафика), затраты на горизонтальное масштабирование микросервисов не окупаются. Показательно, что Stack Overflow обслуживает один из самых посещаемых сайтов в мире на пете монолитных .NET-серверов, добиваясь при этом выдающегося времени отклика — именно за счёт отсутствия сетевых накладных расходов.

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

Эволюция: монолит → модульный монолит → микросервисы

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

Стадия 1: от единого монолита к модульному. Прежде чем что-либо выносить наружу, внутри монолита наводят порядок: выделяют модули по ограниченным контекстам, разрывают прямые обращения к чужим данным, вводят явные интерфейсы и закрепляют границы автоматическими проверками (fitness-функциями в CI). Этот шаг сравнительно дешев — не требует распределённой инфраструктуры — и приносит немедленную пользу: код становится понятнее, команды могут владеть модулями, а сами границы оказываются «обкатаны» до того, как их превратят в сетевые. Опыт показывает, что качество границ сервисов почти напрямую наследуется от качества границ модулей, из которых они выделены: хаотичный монолит при разбиении даёт хаотичные микросервисы.

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

Strangler Fig (паттерн «душитель»). Назван Мартином Фаулером (2004) по аналогии с инжиром-душителем, который прорастает вокруг дерева-хозяина, постепенно замещая его. Применительно к монолиту идея в следующем: перед монолитом ставят фасад (обычно API Gateway или обратный прокси), который направляет трафик либо в старый монолит, либо — по мере готовности — в новые сервисы. Новую функциональность с самого начала реализуют в сервисах, а старую постепенно переписывают и «переключают» в фасаде. В каждый момент система работает целиком; риск локализован в одной функции за раз; при неудаче конкретный маршрут возвращается к монолиту. Паттерн ценен тем, что делает миграцию обратимой и постепенной, без «большого взрыва».

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

Чего следует избегать — «переписывание с нуля» (big-bang rewrite). Искушение бросить старый монолит и переписать систему заново в микросервисах сильное, но почти всегда разрушительное: параллельно приходится поддерживать две системы, бизнес-логика переносится с ошибками, сроки срываются, а результат сравнивают с работающей старой системой. Джоэл Спольски в знаменитой статье о переписывании Netscape (2000) назвал это «худшей стратегической ошибкой»; она остаётся таковой и для архитектуры. Эволюционный путь — strangler fig и extract service — растянут во времени, но безопасен и сохраняет систему работающей на каждом этапе. Этот принцип постепенного, обратимого изменения лежит в основе эволюционной архитектуры и согласуется с законом Конвея: структура системы отражает структуру организации, и по мере роста организации архитектура эволюционирует вместе с ней.

Плюсы

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

Простота тестирования. Единый тестовый контекст позволяет писать сквозные (end-to-end) тесты тривиально: все модули доступны в одном процессе, не нужны сетевые моки, тесты выполняются быстро и детерминированно. В микросервисной среде те же проверки требуют развёртывания целого ландшафта и страдают от хрупкости.

Простота развёртывания. Один артефакт, один конвейер, одна точка поставки. Не нужно координировать версии десятков сервисов, отслеживать совместимость контрактов и управлять сложной инфраструктурой оркестрации. Для команды без выделенной DevOps-роли это критическое преимущество.

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

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

Простота отладки. Один стек-трейс, единый журнал, возможность локально воспроизвести любой сбой. В распределённой системе для сопоставимого результата нужна распределённая трассировка (OpenTelemetry, Jaeger), централизованное логирование и корреляционные идентификаторы — всё это инфраструктура, которой монолит не требует.

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

Низкая стартовая стоимость. Минимальная инфраструктура: один сервер, одна база, один pipeline. Это позволяет запускать продукты и эксперименты с минимальными инвестициями, что особенно ценно для стартапов и внутренних инструментов.

Минусы

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

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

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

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

Конфликты команд в общем коде. По закону Конвея (Melvin Conway, 1968) структура системы повторяет структуру коммуникаций организации. Несколько команд, работающих в одной кодовой базе, неизбежно мешают друг другу: merge-конфликты, конкуренция за релизные окна, рассогласованные изменения. Именно эта организационная боль, а не технические ограничения, чаще всего становится реальным триггером перехода к микросервисам.

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

Монолит vs Микросервисы vs SOA

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

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

SOA (service-oriented architecture, 2000-е) — первый массовый подход к разбиению на сервисы. Вводит шину корпоративных сервисов (Enterprise Service Bus, ESB), которая берёт на себя маршрутизацию, преобразование и оркестрацию; сервисы часто разделяют общую модель данных и интегрируются через тяжёлые протоколы (SOAP/WS-*). Сложность переезжает в шину, которая сама становится узким местом и точкой связности. Сегодня SOA встречается преимущественно в унаследованных корпоративных системах.

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

Признак Монолит SOA Микросервисы
Граница компонента Модуль в процессе Сервис Сервис
Канал связи Вызов метода Умная шина (ESB) «Глухие» каналы (HTTP/брокер)
Данные Общая БД Часто общая модель База на сервис
Развёртывание Единый артефакт Координированное Независимое по сервисам
Масштабирование Целиком По сервисам (редко) Точечно по сервисам
Граница по Техническим слоям Интеграция систем Бизнес-возможностям
Владение Одна команда Централизованная интеграция Сквозные команды
Транзакции Локальные ACID Часто распределённые (через ESB) Саги, итоговая согласованность

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

Связанные статьи

  • Микросервисы — парная статья: децентрализованный стиль, в который эволюционирует монолит; паттерны коммуникации, управления данными и инфраструктуры распределённой системы.
  • Введение в архитектуру — общие понятия архитектуры ПО и место монолита среди архитектурных стилей.
  • Эволюционная архитектура — постепенное, обратимое изменение архитектуры; монолит как стартовая точка эволюции, паттерн strangler fig и fitness-функции для удержания границ.
  • SOLID — принципы проектирования, масштабирующиеся от уровня классов до границ модулей модульного монолита.
  • Паттерны проектирования — каталог решений повторяющихся задач, лежащих в основе внутренней структуры монолита.
  • High-Level Design — выделение крупных компонентов системы, на котором принимается решение о монолите или микросервисах.
  • System Design — проектирование систем с учётом масштабируемости, надёжности и производительности; монолит как один из вариантов декомпозиции.