Часть III: Простые системы — монолит как искусство
Глава 11

Монолит — не грязное слово

[DRAFT — русский, стиль: живой, инженерный]


Проблема

Проблема: В 2020-х сказать «мы на монолите» — почти признаться в отставании. HR хмурятся, коллеги сочувствуют, архитектурные полицейские в комментариях пишут «вы бы хотя бы модульный монолит сделали». А потом приходит Stack Overflow, который на 4 серверах обслуживает миллиард запросов в месяц, и — всё, когнитивный диссонанс обеспечен.


#Monolith First — стратегия старта

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

Давайте начистоту. На старте проекта вы не знаете:

  • Что именно вы строите (product-market fit не найден)
  • Как это будет расти (может, никто не придёт)
  • Где узкие места (может, всё будет упираться в бизнес-логику, а не в масштаб)
  • Как делить систему на сервисы (потому что bounded context ещё не проявились)

В такой ситуации микросервисы — это не инвестиция, это ставка. Ставка на то, что:

  1. Вы угадаете границы сервисов с первой попытки
  2. Операционная сложность не съест ваш budget на фичи
  3. Команда справится с distributed debugging
  4. Ваш MVP вообще кому-то понадобится

Спойлер: чаще всего угадать не получается.

#Простая математика старта

Стартап с монолитом:

Время от идеи до продакшена:      2-4 недели
Инфраструктура:                   1 сервер
Команда для поддержки:            тот же разработчик, что писал код
Сложность деплоя:                 git push + build
Что ломается:                     ваш код
Что делать когда ломается:        чинить код

Стартап с микросервисами:

Время от идеи до продакшена:      2-4 месяца
Инфраструктура:                   Kubernetes cluster, service mesh, API Gateway,...
Команда для поддержки:            +1 DevOps
Сложность деплоя:                 CI/CD пайплайны, helm charts, canary deployments
Что ломается:                     сеть, конфиги, версии, certificates, ваш код
Что делать когда ломается:        сначала выяснить, что сломалось, потом чинить

Я не преувеличиваю. Микросервисы добавляют operational complexity независимо от того, сколько у вас кода. Connection pooling между сервисами, service discovery, distributed tracing, circuit breakers, retries — это всё нужно настраивать ДО того, как вы написали первую бизнес-функцию.

Монолит даёт вам роскошь игнорировать всё это. Вы пишете код, он работает. Вы деплоите — он на продакшене. Всё.

#Monolith First — это не «навсегда монолит»

Важный нюанс. Monolith First — это стратегия, а не догма. Вы не клянётесь в вечной верности одному процессу. Вы говорите: «Сейчас мы строим просто, чтобы быстро учиться. Когда поймём, что и как — задумаемся о декомпозиции».

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

Правило большого пальца

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


#Monolith vs Distributed Monolith — ловушка, в которую попадают все

Вот сценарий, который я видел десятки раз.

Фаза 1. Команда решает, что монолит — это плохо. Архитектор рисует красивые квадратики сервисов: Auth Service, User Service, Order Service, Payment Service, Notification Service.

Фаза 2. Сервисы развёрнуты, каждый в своём контейнере. Код разделён. Команда гордится.

Фаза 3. Первая фича требует изменения в трёх сервисах. Команда проводит синхронный релиз — деплоят пять сервисов одновременно. Потом ещё один. И ещё.

Фаза 4. Через полгода обнаруживается, что сервисы:

  • Используют общую базу данных (потому что переезжать данные — дорого)
  • Вызывают друг друга синхронно цепочками (A → B → C → D)
  • Нельзя задеплоить один сервис — упадут зависимые
  • Изменение схемы данных требует деплоя пяти сервисов

Поздравляю, у вас распределённый монолит (distributed monolith).

#Как распознать distributed monolith

Симптомы простые:

  1. Синхронные цепочки. Запрос приходит в Service A, A вызывает B, B вызывает C, C вызывает D. Время ответа = сумма времён ответа всех сервисов. Если один упал — упали все.

  2. Shared database. Несколько сервисов пишут в одни и те же таблицы. Почему? Потому что «у нас PostgreSQL, проще сделать таблицу, чем настраивать межсервисную коммуникацию».

  3. Один деплой — это деплой всего. Невозможно задеплоить один сервис, не задеплоив два соседних. CI/CD превращается в CI/CI — Continuous Integration / Continuous Integration.

  4. Слабые контракты. Нет явных интерфейсов между сервисами. Изменение ответа одного сервиса ломает парсер в другом.

  5. Синхронная связанность через shared libraries. Общий SDK, который все используют — и меняют одновременно.

#Что с этим делать

Первое: признать, что вы построили distributed monolith. Это не стыдно — это самый распространённый анти-паттерн. Через него прошли Uber, Amazon, практически все, кто переходил на микросервисы без должной подготовки.

Второе: остановиться и подумать. Вам реально нужны микросервисы? Или вам нужен модульный монолит (см. Главу 12)?

Третье: если микросервисы всё-таки нужны — рефакторить по-настоящему. Вводить bounded context (см. Главу 4). Разделять базы данных. Переходить на асинхронную коммуникацию.

Четвёртое: а может, просто вернуться к монолиту? Это не отступление. Это осознанное решение, которое сэкономит вам годы.

Cross-reference

Cross-reference: О том, как не попасть в distributed monolith и как из него выбираться — см. Главу 14 «Дорога в распределённый мир» и Главу 15 «Микросервисы — правильный способ».


#Когда монолит — правильный выбор

Давайте без догм. Монолит — это не универсальный ответ. Но есть ситуации, где он — лучший ответ.

#1. Ваша команда — 2-10 человек

Распределённые системы требуют coordination overhead. Каждый сервис — это API, это документация, это контракты, это версионирование, это тесты на совместимость. В маленькой команде этот overhead съедает всё время на фичи.

Когда вас меньше десяти, ваше преимущество — скорость. Монолит сохраняет её. Микросервисы её убивают.

#2. Вы не нашли product-market fit

Звучит банально, но: если никто не пользуется вашим продуктом, микросервисы не спасут. На стадии поиска PMF ваша задача — экспериментировать быстро. Менять направление. Переписывать фичи. Удалять то, что не взлетело.

В монолите вы просто удаляете код. В микросервисах — удаляете сервис, переписываете контракты, чистите данные, передеплоиваете соседей.

#3. Ваш домен — один bounded context

Не все системы требуют разделения. Если ваше приложение — это один домен (например, внутренняя CRM для отдела продаж), то делить его на микросервисы — искусственное усложнение.

Помните: bounded context — это причина для микросервиса. А не «мы можем вынести это в отдельный сервис».

#4. Команда не имеет опыта распределённых систем

Это, пожалуй, главный пункт. Микросервисы — это не про код. Это про сеть. А сеть — ненадёжна. Если ваша команда не знает, что такое:

  • Circuit Breaker
  • Retry with exponential backoff
  • Distributed tracing
  • Idempotency
  • CAP theorem
  • Eventual consistency

...то переход на микросервисы будет не ускорением, а изучением всего этого под боевыми нагрузками. Удачи.

#5. Ваша нагрузка предсказуема

Stack Overflow, Basecamp, Mailchimp — все они живут на монолитах с высокой нагрузкой. Почему? Потому что их нагрузка предсказуема. Q&A трафик, SaaS с фиксированным числом пользователей — это не социальная сеть с вирусным ростом.

Если вы можете предсказать свою нагрузку на год вперёд, вы можете её обслужить на монолите. Просто добавьте read replicas и кэш.


#Когда монолит начинает мешать — признаки

Монолит — это не пожизненное решение. Рано или поздно он начинает мешать. Вот когда пора задуматься:

#1. Релизный цикл превращается в ад

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

#2. Команда выросла и не помещается в одно repo

Закон Конвея: «Архитектура системы копирует коммуникационную структуру команды». Если у вас растёт организация и все пишут в один монолит, начинаются:

  • Конфликты в merge
  • «Я не знаю, что делает этот модуль — его писала другая команда»
  • Зависимости между командами: A ждёт B, B ждёт C
  • «Не могу задеплоить, сломаю чужую фичу»

#3. Разные части системы требуют разного масштабирования

Классика: ваш frontend API должен отвечать за 50ms, а background job, обрабатывающий отчёты, может работать 5 минут — но они в одном процессе. В итоге job съедает CPU и frontend тормозит.

В монолите это решается хуже. Вы можете вынести тяжёлые задачи в отдельные worker-процессы (Sidekiq, Celery, background jobs), но если это не помогает — пора делить.

#4. Технологический стек неоднороден

Ваша команда data science хочет писать на Python с pandas. Команда real-time — на Go или Rust. Команда Web — на Node.js. В одном монолите они не могут использовать разные языки. В микросервисах — запросто.

Но вопрос: вам правда нужны три разных языка, или это «архитектура под технологии»? (спойлер: обычно второе)

#5. Вы не можете изолировать сбой

Ошибка в модуле писем роняет сайт целиком. Утечка памяти в обработчике файлов вырубает всё приложение. OOM Killer убивает процесс со всеми запросами.

В монолите изоляция сбоев — это боль. В микросервисах сбой одного сервиса не роняет остальные (если правильно настроены circuit breakers и bulkheads).


#Признаки, что монолит всё ещё ок

Для баланса — вот признаки, что не пора уходить с монолита:

Признак Означает
Релизы несколько раз в неделю без боли Монолит вас не тормозит
Команда < 10 человек Coordination overhead > выгоды от разделения
Нет проблем с масштабированием отдельных компонентов Просто добавьте реплики
Вы используете background workers для тяжёлых задач Монолит с worker-ами = дешёвая изоляция
Кодовая база организована в модули У вас уже модульный монолит (Глава 12)
Деплой занимает < 10 минут Вы ещё не упираетесь в потолок

#Кейс: Shopify — монолит, который не стыдно показать

Shopify — наверное, самый известный пример того, как монолит живёт и процветает под нагрузкой enterprise-уровня.

#Архитектура

Shopify Core — это монолит на Ruby on Rails. Один большой Rails-монолит, в котором живёт вся бизнес-логика: магазины, товары, заказы, плагины, темы. 500+ разработчиков пишут в одну кодовую базу. 600 000+ магазинов работают поверх неё.

Вот что позволяет Shopify держать монолит на ходу с таким количеством разработчиков и магазинов:

1. Модульная организация. Несмотря на то, что код в одном репозитории, он чётко разделён по доменам. Команда не просто «пишет в монолит» — она владеет своим модулем. Границы модулей уважаются на уровне code review.

2. Отличное кэширование. Shopify использует Memcached (multi-terabyte кэш) для разгрузки базы. Большинство запросов даже не доходят до PostgreSQL.

3. Read replicas PostgreSQL. 10+ реплик чтения — банальный, но эффективный способ масштабирования.

4. Background jobs через Sidekiq. Всё, что не должно происходить синхронно — отправка писем, обработка тем, генерация отчётов — уходит в Sidekiq. Это даёт изоляцию без микросервисов.

5. Выделенные сервисы для узких мест. Это важно: Shopify не фанатично остаётся на монолите. У них есть отдельные Ruby-сервисы для узких задач:

  • Image processing (отдельный сервис, потому что умеет масштабироваться независимо)
  • Checkout (выделен из-за высоких требований к uptime)
  • CDN (Cloudflare для статики)

То есть Shopify не говорит «мы 100% монолит». Они говорят: «монолит — это ядро, а узкие места мы выносим по мере необходимости».

#Цифры

  • 1M+ merchant stores
  • $7B+ GMV (2023)
  • Black Friday: ~$3.7M/min обрабатывается
  • 50+ деплоев в день на монолит
  • 500+ разработчиков в одном репозитории

#Уроки из Shopify

Монолит не ограничивает масштаб. Shopify обрабатывает миллионы заказов, выдерживает Black Friday и растёт. Всё на Rails-монолите.

Ключ — модульность. Shopify живёт не на «большом комке грязи». Он живёт на монолите с чёткими модулями. Разница колоссальная.

Выносить можно точечно. Если один модуль требует независимого масштабирования — вынесите его. Не нужно переписывать всё.

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


#Кейс: Stack Overflow — 1 миллиард запросов на 4 серверах

Если Shopify — это «монолит, который растёт», то Stack Overflow — это «монолит, который давно вырос и просто работает».

#Архитектура

Stack Overflow — это монолит на .NET.

Не «современный .NET Core на Kubernetes с микросервисами». Старый добрый ASP.NET (сначала WebForms, потом MVC) на Windows Server. База — Microsoft SQL Server на двух (потом четырёх) нодах. Кэш — Redis. Всё.

Железо (на пике):

  • 4 веб-сервера
  • 4 SQL Server ноды (1 primary, 3 read replicas)
  • 2 Redis сервера

Вот и вся инфраструктура, обслуживающая 1 миллиард запросов в месяц.

#Почему это работает

1. Отличная архитектура кэширования. Stack Overflow — read-heavy. Пользователь открывает страницу, читает вопрос, читает ответы. Запись происходит редко. Эту нагрузку можно кэшировать.

Сайт использует многоуровневое кэширование:

  • HTTP-кэширование на уровне браузера и CDN
  • Redis для данных, которые меняются нечасто
  • Memcached (в прошлом) для сессий и временных данных

2. Минимум зависимостей. Stack Overflow не вызывает 20 внешних API. Он не интегрируется с 10 системами. Он показывает вопросы и ответы. Это почти идеальная изолированная система, и монолит — идеальный паттерн для неё.

3. Предсказуемая нагрузка. Q&A — это не вирусная социальная сеть. Трафик Stack Overflow предсказуем: будни — пик, выходные — спад. Никаких внезапных 100x скачков нагрузки за час.

4. Команда небольшого размера. Основная команда Stack Overflow — меньше 20 разработчиков. Вся кодовая база поддерживается небольшой группой, которая знает её вдоль и поперёк.

5. Десятилетия стабильности. Сайт работает с 2008 года. За это время кодовая база «устаканилась». Edge cases обработаны. Баги знакомы. Архитектура отлажена. В такой ситуации менять что-то радикально — значит ломать то, что работает.

#Цифры

  • 1B+ запросов/месяц (на пике)
  • 600M+ page views/год
  • 50M+ уникальных посетителей/мес
  • 4 веб-сервера, 4 DB сервера
  • 10+ лет на одной архитектуре

#Что говорят инженеры Stack Overflow

Ник Крэйвер (Nick Craver), главный инженер Stack Overflow (2014-2022):

«Мы не используем микросервисы. И это нормально. Люди думают, что микросервисы — это единственный способ масштабироваться. Но наша архитектура работает уже больше десяти лет и обслуживает миллиарды запросов. Kubernetes? Вы серьёзно? У нас 4 сервера — зачем нам Kubernetes?»

И это ключевой момент. Ребята из Stack Overflow не консерваторы. Они тестируют новые технологии (они одними из первых внедрили Redis, они пишут на .NET Core когда это свежо). Но они не добавляют сложность без необходимости.

#Про 4 сервера — почему удивляются все

Каждый раз, когда Stack Overflow публикует свои архитектурные цифры, реакция одинаковая: «Как так? 1 миллиард запросов на 4 серверах?».

Ответ: они знают свою нагрузку и не тратят ресурсы на абстракции.

Они не используют:

  • Kubernetes («зачем оркестратор на 4 сервера?»)
  • Service mesh («зачем sidecar, когда сервисов нет?»)
  • Elasticsearch для поиска (они используют SQL Server full-text search — и это работает)
  • RabbitMQ/Kafka (нет событийной архитектуры)
  • 50 микросервисов (зачем?)

Но они активно используют:

  • Кэширование на всех уровнях
  • Read replicas
  • CDN для статики
  • Background jobs для тяжёлых операций (обновление поискового индекса, пересчёт репутации)
  • Профилирование и оптимизацию (каждый запрос измеряется, каждая миллисекунда на счету)

Мораль: прежде чем добавлять сложность, убедитесь, что вы выжали всё из простоты.


#Не делай так: три главные ошибки

#1. Distributed Monolith — микросервисы, которые нельзя менять независимо

Это ошибка номер один, и она настолько распространена, что заслужила отдельный термин.

Симптомы: вы гордо называете архитектуру микросервисной, но на деле:

  • Деплоите все сервисы вместе
  • Меняете контракты, и ломаются соседи
  • Используете общую БД
  • Вызываете сервисы синхронными цепочками

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

Лечение: признать ошибку. Вернуться к модульному монолиту (Глава 12) или переделать декомпозицию по bounded context (Глава 4).

Реальный пример: Uber в 2016-2017 годах. 2200+ микросервисов, многие из которых были настолько связаны, что их нельзя было изменить независимо. Компания потратила несколько лет на консолидацию.

#2. Premature Microservices — «давайте начнём с микросервисов, а то потом переписывать будет сложно»

Это обратная сторона страха перед рефакторингом. Команда думает: «Начнём правильно, чтобы потом не переделывать».

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

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

Лечение: YAGNI. Не строй того, что не нужно сейчас. Начинай с монолита.

#3. Kubernetes для одного сервиса

Это не архитектура, это flex. У вас один (или два) сервиса, десять разработчиков и кластер Kubernetes с тремя нодами.

Что вы получаете:

  • Сложность настройки (Ingress, DNS, TLS, RBAC, network policies)
  • Сложность деплоя (Helm charts, CI/CD, registry)
  • Сложность отладки («почему pod CrashLoopBackOff?»)
  • Сложность обновлений (upgrade кластера — отдельный проект)

Что вы теряете:

  • Время, которое могли потратить на фичи

Когда Kubernetes реально нужен: когда у вас >5 сервисов, >2 команд, разная частота деплоя. Или когда вам нужны специфические фичи K8s (auto-scaling, canary deployments, blue-green).

Для одного сервиса: одна VM с docker-compose или systemd — это нормально. Можете даже на heroku/vogels/railway запустить — и это будет нормально.


#Чек-лист: стоит ли оставаться на монолите

Прежде чем начинать декомпозицию, ответьте на эти вопросы:

  • Релизный цикл — меньше недели? (+1)
  • Команда меньше 10 разработчиков? (+1)
  • Нет проблем с изоляцией сбоев? (один сбой не роняет всё) (+1)
  • Все модули пишут на одном языке? (+1)
  • База данных не перегружена (CPU < 50%, replication lag < 100ms)? (+1)
  • Вы не боитесь деплоить в пятницу вечером? (+1)
  • Разные части системы не требуют разного масштабирования? (+1)
  • У вас есть модульная организация кода? (+1)

Если набрали 5+ баллов — оставайтесь на монолите. У вас нет архитектурных причин для перехода.

4 и меньше — задумайтесь о модульном монолите (Глава 12). Микросервисы — только если отдельных баллов < 2 и есть проблемы с разным масштабированием.


#Ключевые выводы

  1. 90% проектов должны начинаться с монолита. Monolith First — не трусость, а стратегия. Вы сначала узнайте, что строить, а потом думайте, как это делить.

  2. Distributed Monolith — самая дорогая ошибка. Микросервисы, которые нельзя менять независимо, хуже монолита: вы платите operational complexity, не получая изоляции.

  3. Монолит не равен «не масштабируется». Stack Overflow — 1 млрд запросов на 4 серверах. Shopify — 500+ разработчиков в одном репозитории. Масштаб определяется архитектурой кэширования, модульностью и дисциплиной команды, а не количеством сервисов.

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

  5. Kubernetes для одного сервиса — это не архитектура, это украшение. Не усложняйте там, где можно быть проще.


#Ссылки на другие главы

  • Глава 5: «Балансируем сложность» — почему YAGNI и KISS защищают монолит как стратегию
  • Глава 12: «Модульный монолит» — следующий шаг, когда монолит перерастает свои границы
  • Глава 14: «Дорога в распределённый мир» — признаки того, что монолиту стало тесно, и как уходить без боли
  • Глава 15: «Микросервисы — правильный способ» — что делать, если вы всё-таки решили делить
  • Библиотека паттернов: references/anti-patterns.md — подробнее о distributed monolith и других анти-паттернах