2026 г.

Монолит и микросервисы: цена границы

Проект ЦИТадель

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

В разделе 1 различаются модульность и распределённость. В разделе 2 рассматривается цена удалённого взаимодействия, а в разделе 3 — его практические выгоды и необходимые условия. Раздел 4 посвящён закону Конвея, раздел 5 — распределённому монолиту. Затем предлагается порядок перехода от модульного монолита к отдельным сервисам, разбирается устройство хорошей границы и даётся практикум для аудита собственной системы.

1. Модульность — не распределённость

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

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

2. Цена: сетевая граница превращает задачу в распределённую

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

  • Вызов становится удалённым. Клиенту нужны ограничение времени ожидания и определённое поведение при недоступности соседа. Повторять запрос можно не всегда: для операции с побочным эффектом требуется идемпотентность или подавление дублей. Практические способы разобраны в статье «Надёжность поверх ненадёжной сети».
  • Локальная транзакция не переходит через границу автоматически. Распределённая атомарность возможна, но требует отдельного протокола и имеет цену по задержке и доступности. В зависимости от нужного инварианта применяют распределённые транзакции, саги, transactional outbox или осознанно допускают промежуточное состояние. Эти варианты и их ограничения рассмотрены в главе 9 курса.
  • Интерфейс становится межпроцессным контрактом. Производитель и потребители могут обновляться в разное время, поэтому нужны совместимая эволюция, терпимые читатели и поэтапные изменения. Подробнее об этом говорится в статье «JSON и дизайн API».
  • Диагностика охватывает несколько процессов. Для поиска источника задержки или ошибки недостаточно стека одного процесса: необходимы общие идентификаторы запросов, метрики, логи и распределённые трассировки. Их совместное использование разобрано в статье «Наблюдаемость».

Таким образом, сетевая граница превращает локальное взаимодействие в задачу распределённой системы. Её следует вводить там, где ожидаемая выгода оправдывает постоянную стоимость контрактов, надёжности и эксплуатации.

3. Выгоды сетевой границы

У сетевой границы есть несколько важных выгод, но каждая достигается только при дополнительных условиях.

Независимое развёртывание. Команда может выпускать и откатывать свой сервис отдельно от остальных. Для этого недостаточно разнести код по репозиториям: сервису нужны совместимые контракты, самостоятельное владение данными и автоматизированный инженерный конвейер. Если изменение по-прежнему требует согласованного выпуска нескольких сервисов, независимость осталась номинальной. Выгода здесь в значительной мере организационная: команды получают разные ритмы поставки.

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

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

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

4. Закон Конвея и границы владения

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

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

Организацию можно осознанно менять так, чтобы поддержать желаемые архитектурные границы; этот приём называют обратным манёвром Конвея. Однако он не гарантирует хорошего результата автоматически. Одна команда вполне может владеть несколькими сервисами, а сервис без явного владельца со временем становится источником задержек и эксплуатационных рисков.

5. Распределённый монолит: издержки без независимости

Распределённый монолит состоит из нескольких процессов, но сохраняет сильную связность единого приложения. Его характерные признаки:

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

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

6. Порядок решения и необходимые условия

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

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

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

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

7. Механика хорошей границы

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

  • Ясное владение данными. Только владеющий сервис изменяет данные в пределах своей модели, а остальные обращаются к его контракту — синхронному интерфейсу или событиям. Логическое разделение важнее отдельного сервера СУБД, но общие таблицы не должны становиться постоянным обходным путём.
  • Проверяемый контракт. Спецификация хранится вместе с кодом и проверяется в конвейере; контракт изменяется так, чтобы старые и новые потребители могли некоторое время работать одновременно. В статьях «JSON и дизайн API» и «Тестирование сегодня» разобраны совместимая эволюция и контрактные тесты.
  • Осознанный выбор способа связи. Синхронный вызов уместен, когда ответ нужен для продолжения операции. События позволяют развязать время работы компонентов, но добавляют повторы доставки, идемпотентность, промежуточные состояния и сложность наблюдения. Модель удалённого вызова изложена в главе 1, саги и outbox — в главе 9, журналы событий — в главе 10 курса.
  • Ограничение распространения отказа. Для удалённых зависимостей задаются тайм-ауты, условия и бюджеты повторов, а также поведение при недоступности соседа. Повтор разрешён только тогда, когда его последствия контролируются. Практические приёмы приведены в статье «Надёжность поверх ненадёжной сети».
  • Наблюдаемость. Контекст трассировки проходит через вызовы и сообщения, а метрики и логи позволяют связать состояние нескольких сервисов. Эту основу следует создавать вместе с первой сетевой границей, используя подход из статьи «Наблюдаемость».

8. Практикум: аудит собственных границ

Задания можно выполнять на монолитной или уже распределённой системе.

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

Задание 2. Цена одной границы. Выберите модуль — кандидат на выделение. Для одной пользовательской операции посчитайте пересечения его интерфейса и оцените, какие из них пришлось бы объединить в более крупный удалённый вызов. Определите транзакции и инварианты, пересекающие границу; для каждого предложите вариант сохранения атомарности, сагу или допустимое промежуточное состояние. Перечислите передаваемые типы данных и правила их совместимой эволюции. Получится предварительная смета сетевой границы.

Задание 3. Аудит владения. Наложите границы ответственности команд на карту из задания 1. У каждого ли модуля есть один владелец? Какие части регулярно меняют несколько команд? Какие изменения постоянно затрагивают чужие модули? Для каждого несовпадения рассмотрите не только выделение сервиса, но и перенос ответственности или исправление интерфейса внутри монолита.

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

Задание 5*. Одна граница на стенде. Поместите периферийный модуль за отдельный локальный сервис, используя Docker Compose и методику статьи о контейнерах. Опишите контракт, задайте тайм-аут и добавьте повтор только для идемпотентной операции; передайте сквозной контекст трассировки. Измерьте задержку пользовательской операции до и после изменения, затем остановите сервис и зафиксируйте поведение системы. Такой опыт покажет не только стоимость вызова, но и новые режимы отказа.

Итоги

  • Модульность и распределённость — разные свойства. Сначала нужно определить границы системы, затем решить, какие из них оправданно переносить в сеть.
  • Сетевая граница добавляет задержки, частичные отказы, межпроцессные контракты, распределённую диагностику и отдельное решение для инвариантов данных.
  • Основные выгоды — независимое развёртывание, ограничение области отказа, раздельное масштабирование и выбор технологий. Ни одна из них не возникает автоматически.
  • Границы сервисов должны учитывать предметную связность, владение данными и ответственность команд. Закон Конвея объясняет влияние организации, но не заменяет архитектурный анализ.
  • Распределённый монолит несёт издержки сети, сохраняя согласованные выпуски, совместное владение данными и каскадные отказы.
  • Модульный монолит — полезное исходное решение для многих новых систем, но не универсальное правило. Сервис выделяют ради явной цели и при достаточной эксплуатационной готовности.
  • Хорошая сетевая граница требует ясного владения данными, совместимого контракта, осознанного способа связи, ограничения распространения отказов и наблюдаемости.

Литература

  1. M. Conway, "How Do Committees Invent?", Datamation, 1968 — первоисточник закона Конвея.
  2. S. Newman, "Monolith to Microservices", O'Reilly, 2019 — цели миграции, связность, владение данными и постепенное выделение сервисов.
  3. J. Lewis, M. Fowler, "Microservices", 2014 — характеристики микросервисной архитектуры и организация вокруг бизнес-возможностей.
  4. M. Fowler, "Monolith First" и "Microservice Prerequisites" — аргументы о порядке решения и необходимых эксплуатационных возможностях.
  5. Материалы нашего сайта: курс «Распределённые системы», глава 1 (модель удалённого вызова и доступность цепочек), глава 9 (распределённые транзакции, саги и outbox), глава 10 (журналы событий), глава 12 (разбор архитектурных границ), «Надёжность поверх ненадёжной сети», «JSON и дизайн API», «Инженерный конвейер», «Наблюдаемость» и «Тестирование сегодня».
404 Not Found

404 Not Found


nginx/1.24.0 (Ubuntu)

Связь с редакцией