Microservices vs Monolith: когда и как переходить
Честный разговор о микросервисах без хайпа и мифов
Слово «микросервисы» в последние годы звучит на каждой архитектурной встрече. Стартапы хотят «как Netflix», enterprise-команды ссылаются на Amazon, а CTO-блоги пестрят историями успеха. Между тем большинство команд, которые переходят на микросервисы без чёткого понимания зачем, получают не гибкость, а распределённый монолит с шестью дополнительными проблемами. В этой статье мы разберём без хайпа: что на самом деле даёт и что отнимает микросервисная архитектура, как понять, что монолит «вырос» из своих штанов, и как организовать переход так, чтобы бизнес не почувствовал боли. Материал основан на опыте проектов с командами от 5 до 80 разработчиков и бюджетами от 2 до 15 млн рублей. Речь пойдёт не о теоретических паттернах из учебников, а о реальных решениях, которые приходится принимать в российских продуктовых компаниях и интеграторах: с ограниченными ресурсами DevOps, часто устаревшим легаси и давлением бизнеса на скорость. Что такое монолит на самом деле — и почему он не так плох Монолит — это не ругательство. Это архитектурное решение, которое по умолчанию выигрывает по нескольким ключевым метрикам: скорость разработки на ранних этапах, простота дебаггинга, транзакционная целостность данных и минимальный операционный оверхед. Хорошо написанный монолит с разделением на модули (Modular Monolith) способен обслуживать миллионы запросов в сутки. Basecamp, Stack Overflow, Shopify — крупные продукты, которые долго работали и работают на монолитах. Stack Overflow, например, обслуживает 1,5 млрд запросов в месяц на нескольких серверах с монолитной архитектурой на C#. Типичные признаки здорового монолита: Чёткое модульное деление внутри кодовой базы (домены не смешиваются) Зависимости между модулями только через публичные интерфейсы Время деплоя — минуты, не часы Один разработчик способен поднять и запустить всё локально за 10–15 минут CI/CD прогоняет полный тест-сьют за 5–15 минут Если ваш монолит соответствует этим критериям, и бизнес-нагрузка не создаёт узких мест — переход не нужен. Потраченные 6–12 месяцев на декомпозицию принесут меньше пользы, чем разработка новых фич. Когда монолит действительно тормозит бизнес Реальные сигналы того, что монолит стал проблемой, — не абстрактные, а измеримые. Вот конкретные пороги, за которыми стоит задуматься об изменениях: Деплой занимает больше 30 минут Если полный деплой включает сборку, тесты и выкатку и занимает более получаса — команды начинают накапливать изменения и деплоить реже. Это прямой риск для бизнеса: критические фиксы попадают в прод через 2–4 часа вместо 10–15 минут. Один упавший компонент роняет всё Если баг в модуле отправки email или генерации PDF кладёт весь API — это не архитектурная проблема микросервисов, это проблема отсутствия изоляции. Но решить её в монолите сложнее: придётся добавлять circuit breaker, bulkhead pattern и отдельные процессы, что по сложности приближается к микросервисам. Команды блокируют друг друга При 3–4 командах, работающих над одной кодовой базой, количество merge-конфликтов и «сломанных пайплайнов» растёт экспоненциально. Типичная картина: команда A откатывает релиз из-за бага команды B, который попал в общий деплой. Если это происходит чаще одного раза в 2 недели — это системная проблема. Разные части системы требуют разного масштабирования Если модуль обработки изображений нагружает CPU на 80%, а остальные части системы простаивают — вы вынуждены масштабировать всё вместо одного компонента. На облачной инфраструктуре это напрямую транслируется в лишние расходы: обычно 40–70% переплаты по вычислительным ресурсам. Технологический долг блокирует найм Если весь продукт написан на PHP 5.6 или устаревшей версии Java, и вы не можете нанять разработчиков, потому что никто не хочет работать с этим стеком — декомпозиция позволяет переписывать сервисы по одному, не останавливая продукт. Архитектурное сравнение: честные цифры Прежде чем принимать решение, полезно понять реальную стоимость каждого подхода. Ниже — сравнение по ключевым метрикам для типичного продуктового проекта с командой 10–20 разработчиков. Монолит Время до первого деплоя: 1–2 дня для нового разработчика Стоимость инфраструктуры (20k RPS): 2–5 серверов, 30–80k ₽/мес Время на дебаггинг distributed trace: не нужен, стектрейс локальный Оверхед на DevOps: 0,25–0,5 FTE Время на написание новой фичи: базовая линия (100%) Микросервисы (10–20 сервисов) Время до первого деплоя: 3–7 дней для нового разработчика (docker-compose, env, сервис-меш) Стоимость инфраструктуры (20k RPS): Kubernetes кластер + сервисы поддержки, 150–400k ₽/мес Время на дебаггинг distributed trace: +30–60 минут без нормального трейсинга Оверхед на DevOps: 1–2 FTE Время на написание новой фичи: 120–180% от монолита (за счёт межсервисного согласования) Вывод из этих цифр прямой: микросервисы имеют смысл только тогда, когда выгоды (независимый деплой, изоляция, масштабирование) превышают эти постоянные издержки. Для большинства продуктов с командой до 15 человек это пороговое значение не достигается никогда. Стратегия перехода: Strangler Fig Pattern Если решение о декомпозиции принято, наиболее зарекомендовавший себя подход — «Strangler Fig» (паттерн удушающего дерева), предложенный Мартином Фаулером. Суть: новые функциональности пишутся как отдельные сервисы, а старый монолит постепенно «усыхает» по мере переноса модулей. Это противоположность «большого переписывания», которое статистически проваливается: по данным исследований, более 60% проектов полного переписывания превышают бюджет вдвое и не завершаются в срок. Фаза 1: Модуляризация монолита (2–4 месяца) Прежде чем выделять сервисы, сделайте так, чтобы монолит сам по себе был хорошо структурирован. Это снизит риски и сделает декомпозицию дешевле. // До: всё вперемешку class OrderController { public function create(Request $request) { // бизнес-логика, работа с БД, отправка email — всё вместе $order = DB::table('orders')->insert([...]); Mail::to($user)->send(new OrderCreated($order)); $this->updateInventory($order); return response()->json($order); } } // После: чёткое разделение слоёв class OrderController { public function create(Request $request) { $command = new CreateOrderCommand($request->validated()); $order = $this->orderService->handle($command); return OrderResource::make($order); } } class OrderService { public function handle(CreateOrderCommand $command): Order { return DB::transaction(function() use ($command) { $order = $this->orderRepository->create($command); $this->eventBus->dispatch(new OrderCreated($order)); return $order; }); } } Фаза 2: Выделение первого сервиса (1–2 месяца) Начинайте с сервиса, который: (а) имеет чёткие границы, (б) редко меняется, (в) легко тестируется изолированно. Хорошие кандидаты для первого сервиса: нотификации, генерация документов, поиск, аутентификация. Плохие кандидаты для старта: платёжный модуль, корзина, ядро бизнес-логики — слишком много зависимостей, слишком высокий риск при сбое. Фаза 3: API Gateway и service mesh Когда сервисов становится больше двух, необходим API Gateway. Он решает: маршрутизацию, аутентификацию на периметре, rate limiting, агрегацию ответов для клиента. # Пример конфигурации Kong API Gateway services: - name: orders-service url: http://orders-svc:8080 routes: - name: orders-route paths: ["/api/v1/orders"] methods: ["GET", "POST", "PUT"] plugins: - name: jwt - name: rate-limiting config: minute: 1000 hour: 50000 Организация данных: самая сложная часть Разделить код — относительно просто. Разделить базу данных — это где большинство проектов застревают на 6–12 месяцев. Главное правило микросервисной архитектуры: каждый сервис владеет своими данными и не обращается напрямую к таблицам другого сервиса. Нарушение этого правила создаёт «распределённый монолит» — худшее из двух миров. Практика разделения баз данных Шаг 1 — введите логическое разделение схем в одной БД: -- До декомпозиции: всё в public схеме -- После: логические домены CREATE SCHEMA orders; CREATE SCHEMA inventory; CREATE SCHEMA notifications; -- Сервис заказов работает только со своей схемой GRANT ALL ON SCHEMA orders TO orders_service_user; REVOKE ALL ON SCHEMA inventory FROM orders_service_user; Шаг 2 — замените прямые JOIN между доменами на API-вызовы или события: // Плохо: cross-domain join SELECT o.*, p.name as product_name FROM orders.orders o JOIN inventory.products p ON o.product_id = p.id; // Хорошо: денормализация данных на момент события // При создании заказа сохраняем snapshot данных продукта INSERT INTO orders.order_items (order_id, product_id, product_name, product_price) VALUES ($1, $2, $3, $4); -- product_name и product_price — копия из каталога на момент заказа Eventual Consistency — принятие компромисса Переход на микросервисы означает отказ от ACID-транзакций между сервисами. Это не баг, это компромисс, к которому нужно быть готовым. Паттерн Saga (хореография или оркестрация) помогает управлять распределёнными транзакциями, но он сложнее и дороже в реализации, чем обычная транзакция в монолите. Честный вопрос бизнесу: готовы ли вы принять, что статус заказа может обновиться с задержкой в 50–200 мс? Для большинства B2B-систем — да. Для финтеха или медицины — часто нет, и это аргумент в пользу сохранения монолитной БД. Observability: без этого микросервисы непригодны к эксплуатации В монолите достаточно одного лог-файла и APM-агента. В микросервисной архитектуре без полноценной observability команда будет тратить 30–50% времени на дебаггинг вместо разработки. Три обязательных компонента (три столпа observability): 1. Distributed Tracing Каждый запрос должен получать уникальный trace ID, который передаётся через все сервисы. Без этого невозможно ответить на вопрос «почему запрос пользователя Иванова завис на 8 секунд в 14:32». // Middleware для propagation trace ID (Node.js + OpenTelemetry) import { trace, context, propagation } from '@opentelemetry/api'; app.use((req, res, next) => { const ctx = propagation.extract(context.active(), req.headers); const tracer = trace.getTracer('api-gateway'); const span = tracer.startSpan('http.request', {}, ctx); req.traceId = span.spanContext().traceId; res.setHeader('X-Trace-Id', req.traceId); context.with(trace.setSpan(context.active(), span), () => { next(); span.end(); }); }); 2. Централизованный logging ELK Stack (Elasticsearch + Logstash + Kibana) или Grafana Loki — стандартное решение. Ключевое требование: структурированные логи (JSON) с обязательными полями service, trace_id, level, timestamp. 3. Метрики и алертинг Prometheus + Grafana — де-факто стандарт для Kubernetes-окружений. Обязательные метрики для каждого сервиса: latency (p50/p95/p99), error rate, throughput (RPS), saturation (CPU/RAM). Бюджет на observability-инфраструктуру: для системы из 10–15 сервисов рассчитывайте на 15–30k ₽/мес на cloud-ресурсы и 2–4 недели инженерного времени на первоначальную настройку. Типичные ошибки при переходе — и как их избежать Ошибка 1: «Нанесервисы» вместо микросервисов Сервис из 200 строк кода, который делает одно и единственное действие — это патология, а не паттерн. Размер сервиса должен определяться бизнес-доменом (Bounded Context в терминах DDD), а не количеством строк. Правило «два пиццы» Amazon (команда, которую можно накормить двумя пиццами) — хороший ориентир: если сервис слишком маленький для отдельной команды, он слишком маленький. Ошибка 2: Синхронная связь везде Если все сервисы общаются через REST/gRPC синхронно — вы получаете каскадные отказы. Упал сервис нотификаций? Упал весь процесс оформления заказа. Решение: асинхронное взаимодействие через очереди (RabbitMQ, Kafka) для некритичных операций. Эмпирическое правило: если потребитель не ждёт ответа прямо сейчас — используйте очередь. Если ждёт — синхронный вызов оправдан. Ошибка 3: Общая база данных «Мы разделили код, но оставили одну БД» — это распределённый монолит. Кажется, что это экономия, но на практике это означает: нельзя менять схему БД без координации всех команд, нельзя масштабировать сервисы независимо, нельзя использовать разные СУБД для разных зад