Webrium
Подписаться

Скучные решения: монолит, который не стыдно

Что микросервисы покупают на самом деле, какой налог берёт граница по сети и почему структуру кода в итоге определяет структура команд

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

Стыд — плохой архитектурный критерий

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

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

Что микросервисы покупают на самом деле

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

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

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

Заодно распил разрешает разнородность: одному куску своя среда исполнения, другому своя. Мы спорили об этом в материале про Rust в сервисах: инструмент берут под конкретное узкое место, а не «на вырост». Здесь то же правило. Если одно горячее место просит другой стек, его вырезают — целиком и с причиной. Для одного исключения не нужен стиль, в котором исключение — норма.

Налог на сеть

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

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

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

И границы множатся. Каждый новый сервис обязан держать свои контракты и договариваться о новых с каждым уже существующим: флот растёт по одному, а переговорная работа — на размер флота. Если хочется формулы: у n сервисов потенциальных пар связей n(n−1)/2 — подставьте своё n и посмотрите, как быстро растёт число. Каждая пара — место возможного рассинхрона, и кто-то должен держать его в рабочем состоянии.

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

Тупик: пилить, потому что стало тесно

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

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

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

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

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

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

Позиция редакции

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

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

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

Каким должен быть монолит, чтобы за него не было стыдно

Правила короткие, цена у них разная — идём от служебных к тяжёлым.

Один артефакт. Репозиториев может быть сколько угодно, деплояемая единица — одна.

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

Данные принадлежат модулю. Чужих джойнов нет, есть вызов чужого API. Самое тяжёлое правило и самое доходное: именно оно отличает систему от спагетти.

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

Вырезанные куски — с обоснованием. Горячее место на другом стеке (про это наш спор про Rust), отдельный домен для другого класса доступности. Причина записана, а не «так все делают».

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

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Сетка сервисов: закрашены лишь три ячейки — случаи, где Rust окупается; рядом счётчик сэкономленных машинЭкономия машин
Спор

Rust в сервисах: где окупается, а где нет

Спор со «скучными решениями»: скорость тут ни при чём — два фактора решают, где у Rust выигрыш реален, и оба не технические.

Цепочка релизных шагов — окружение, пайплайн, выкатка, доступы, — последний шаг выходит за границу продуктовой работы: команды тратят время на обвязку, а не на граница продуктаобвязка
Углубление

Платформенная команда: когда её заводить

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

Из трёх причин медленной работы за шкалу выходит ожидание, а не вычислениеожидание
Предыстория

Python тормозит: что действительно ускоряет

База под остальное: пока три вида «медленно» не разделены, любое ускорение остаётся угадыванием, включая переписывание на другом языке.

Письмо по вторникам

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

Начните вводить — материалы появятся здесь.

выбрать · Enter открыть · Esc закрыть