Алексей Меленчук
вся записка
дата
объём 5 мин чтения

Микросервисы — ответ на организационную проблему

Распил решает вопрос независимости команд и выкатов, а не производительности. Разбор того, что покупается и чем платят, если команда одна.

Коротко. Микросервисы покупают независимость выката ценой сетевых отказов вместо вызова функции. Если команда одна, вы заплатили цену и не получили товар.

Вопрос, который стоит задать перед распилом: сколько команд будут выкатываться независимо друг от друга?

Если ответ «одна», дальше можно не читать — точнее, стоит прочитать именно потому, что ответ «одна».

Что действительно покупается

Независимость. Не производительность, не масштабируемость, не чистота архитектуры — независимость, и в трёх конкретных проявлениях.

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

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

Независимость выбора. Сервису обработки изображений нужен C++, остальным хватает PHP. В общем процессе такой выбор невозможен.

Все три — про изоляцию. Все три реальны и стоят денег.

Чем платят

Здесь список длиннее, и он приходит не сразу.

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

Каждая граница между сервисами — это место, где надо решить вопрос таймаутов, повторов, идемпотентности и деградации. На двадцати сервисах таких мест не двадцать, а столько, сколько связей.

Транзакция перестаёт существовать. Операция, затрагивающая два сервиса, больше не атомарна. Вместо BEGIN/COMMIT появляется сага: последовательность шагов с компенсирующими действиями на каждый.

Компенсация — это не откат. Откат возвращает систему в исходное состояние. Компенсация выполняет обратное действие, и она может не сработать. «Отменить списание» — отдельная операция, которая тоже может упасть.

Разница между транзакцией и сагой — это разница между «база гарантирует» и «мы написали код, который обычно справляется».

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

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

Почему это про организацию

Закон Конвея: структура системы повторяет структуру коммуникаций в организации. Обычно это цитируют как наблюдение. Полезнее читать как ограничение.

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

Обратное тоже верно: если команд шесть и они в разных часовых поясах, общий монолит превращает каждый релиз в переговоры.

Отсюда мой критерий. Не число сервисов, не нагрузка, не мода — стоимость согласования между частями системы. Пока она низкая, монолит выигрывает. Когда она становится главным тормозом, распил окупается.

Сага, если по существу

Раз транзакции нет, стоит понимать, чем именно её заменяют.

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

Два способа собрать это, и они не равноценны.

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

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

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

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

Что делать вместо, пока рано

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

Проверяется это не договорённостью, а сборкой: тест архитектуры, который падает, если модуль импортирует внутренности соседа. Такие проверки есть в экосистеме любого языка, и настраиваются они за день.

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

Я бы сформулировал так: границы нужны всегда, отдельные процессы — иногда. Их путают, и из-за этого берут вторые, чтобы получить первые.

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