Монолитная архитектура
Общее
Монолитная архитектура (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 — проектирование систем с учётом масштабируемости, надёжности и производительности; монолит как один из вариантов декомпозиции.