Контекст: b2b SaaS для логистики, около 4000 активных компаний-клиентов, команда из 9 бэкендеров. Стек на момент распила: монолит на PHP 8.1 плюс пара воркеров. К концу 2024 у нас было 23 сервиса на Go и PHP, Kubernetes в Yandex Cloud, Kafka, gRPC между сервисами, трейсинг в Tempo.
Боль номер один, распределённые транзакции:
Классика: создание заказа трогает биллинг, остатки на складе и уведомления. В монолите это был один BEGIN ... COMMIT. В микросервисах это сага с компенсациями плюс transactional outbox в каждом сервисе:
Код: Выделить всё
CREATE TABLE outbox (
id bigserial PRIMARY KEY,
aggregate_id uuid NOT NULL,
event_type text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz DEFAULT now()
);
Боль номер два, latency:
Создание заказа в монолите: p99 около 80 мс. После распила цепочка проходила через 6 сетевых хопов, и p99 вырос до 420 мс. gRPC с keepalive и пулами соединений срезал до примерно 280, но дальше упёрлись: каждый хоп это сериализация, сеть, очередь на принимающей стороне. Можно переписать цепочку на асинхронные события, и мы переписали часть, но тогда фронту нужны промежуточные статусы вида "заказ создаётся", поллинг или вебсокеты, и продуктовая команда смотрит на тебя с ненавистью, потому что раньше кнопка просто работала.
Боль номер три, инфраструктура и деньги:
Монолит жил на трёх виртуалках. Микросервисный ландшафт: 14 нод под Kubernetes, кластер Kafka из трёх брокеров, отдельные базы у половины сервисов, Prometheus, Tempo, Loki. Счёт в Yandex Cloud вырос с примерно 90 тысяч до 480 тысяч рублей в месяц. Плюс по факту целая ставка инженера уходила на платформу: апгрейды кластера, helm-чарты, сертификаты, дрейф конфигов между окружениями. Для команды из 9 человек это непозволительный налог.
Боль номер четыре, дебаг:
Инцидент в монолите: открываешь стектрейс, видишь строку. Инцидент в микросервисах: собираешь трейс по correlation id, выясняешь, что половина спанов потерялась, потому что один сервис не пробрасывал заголовки, дальше читаешь логи трёх сервисов с тремя разными форматами времени. Среднее время разбора инцидента у нас выросло с 40 минут до трёх с лишним часов, это по постмортемам, не по ощущениям.
Что мы сделали:
Не склеили всё обратно в шар, а собрали модульный монолит на Go. Один бинарь, одна PostgreSQL 16, внутри модули с жёсткими границами: billing, orders, warehouse, notifications. У каждого модуля публичный интерфейс (отдельный пакет api) и приватная внутрянка. Границы проверяет go-arch-lint в CI:
Код: Выделить всё
deps:
orders:
mayDependOn:
- billing-api
- warehouse-api
billing:
mayDependOn: []
Миграция заняла 7 месяцев фоном от основной работы. Сервисы переезжали по одному, данные сливали в общую базу через logical replication, на каждом шаге был feature-флаг для отката. Ни одного даунтайма дольше пяти минут.
Что получили: p99 создания заказа 90 мс без всякой оптимизации, просто потому что исчезла сеть. Счёт за облако 140 тысяч в месяц. Деплой одной командой вместо оркестрации релизного поезда из десятка сервисов. Локальная разработка это docker compose с приложением и базой, а не 23 контейнера, съедающие 32 гига на ноутбуке. Онбординг джуна сократился с месяца до недели.
Когда микросервисы всё-таки оправданы:
Мы не стали фанатиками обратного карго-культа. Два сервиса остались отдельными: рендеринг PDF-накладных (CPU-bound, скейлится независимо, его падение никого не волнует) и шлюз интеграций с API перевозчиков (зоопарк SOAP и непредсказуемые таймауты, изоляция защищает основное приложение).
Микросервисы честно работают, когда: у вас десятки команд и нужны независимые релизные циклы; есть компоненты с принципиально разным профилем нагрузки; нужна изоляция по безопасности или комплаенсу (платёжный контур, персональные данные по 152-ФЗ); вы реально упёрлись в вертикальный потолок одной машины, что в 2026 при серверах со 192 ядрами и терабайтом памяти случается куда реже, чем принято думать. Ни один пункт из списка не звучит как "так модно" и не описывает команду из 9 человек.
Выводы:
Микросервисы это не архитектура, а организационный инструмент с огромным ценником. Платить его имеет смысл, когда стоимость координации людей превышает стоимость владения распределённой системой. Модульный монолит даёт 80 процентов пользы (границы, тестируемость, явные контракты между модулями) за 5 процентов цены. И если границы выстроены честно, вынести модуль в отдельный сервис потом это вопрос недель. Обратный путь, как мы выяснили на себе, занимает 7 месяцев и стоит сильно дороже.