Дорога в распределённый мир — когда и почему
Проблема: Монолит работал отлично. Но в последнее время каждое изменение — это боль. Команда растёт, а скорость падает. Релизы выходят раз в месяц, и каждый — лотерея. Знакомо? Вот как понять, что пришло время перемен, и — что важнее — как не наломать дров.
Я видел этот цикл десятки раз.
Сначала всё хорошо. Проект живёт, фичи выходят каждый день, команда — человек пять, все друг друга знают. Потом нанимаются новые люди. Кодовая база растёт. И в какой-то момент — как щелчок — вы обнаруживаете, что простой баг-фикс занимает три дня: два дня на разбирательство, кто за какой кусок отвечает, и ещё день на то, чтобы не сломать соседний модуль.
Релиз? Раз в две недели, и каждый — молитва. CI/CD забит. Тесты бегут час. После деплоя — мониторинг, фиксы, горячие патчи.
И тут кто-то говорит: «А не пора ли нам на микросервисы?»
И вот здесь начинается самое опасное место во всей книге. Потому что переход в распределённый мир — это не про технологии. Это про то, как ваша организация принимает решения, как ваша команда общается, и готовы ли вы платить новую цену.
Цена, заметьте, очень высокая.
#Симптомы: когда монолит начинает душить
Не хочу, чтобы эта глава звучала как «монолит — это зло». У нас уже была Глава 11, где мы разобрали, почему монолит — это прекрасно. Но даже лучший монолит имеет предел. Вопрос не в том, может ли монолит масштабироваться (может — Stack Overflow доказывает это на 1 миллиарде запросов в месяц). Вопрос в том, кого это масштабирование устраивает.
Вот настоящие симптомы, по которым опытный архитектор понимает: что-то сломалось.
#Симптом 1: Один PR ломает чужую фичу
В монолите границы между модулями — это договорённости. Они держатся на дисциплине команды. Когда команда маленькая, дисциплина естественна: все всё помнят, code review эффективен, каждый знает, где чьи границы.
Но когда в проекте 20+ разработчиков, а кодовая база — полмиллиона строк, договорённости начинают трещать по швам. Кто-то не заметил, что его изменения в checkout_service.py ломают recommendation_engine.py. CI зелёный, тесты прошли — но на проде упала конверсия.
И вы начинаете играть в увлекательную игру: «Кто сломал проде на этот раз?».
#Симптом 2: Релизная гонка
В маленькой команде релиз — это:
git push → CI → деплойВсё. 15 минут.
Когда команда большая, релиз превращается в церемонию:
feature freeze за 3 дня → code freeze → регрессионное тестирование →
release candidate → staging → approval → окно деплоя (ночью, чтобы
не задеть пользователей) → мониторинг → hotfix → ещё один hotfixРаз в две недели. А если вы крупно облажались — раз в месяц.
И каждый раз одно и то же: «В этот раз всё будет работать, честное слово».
Проблема не в том, что процессы плохие. Проблема в том, что монолит не позволяет деплоить разные части системы с разной скоростью. Платежи могут быть стабильными и не меняться месяцами, а рекомендации — итеративно улучшаться каждый день. Но в монолите они привязаны друг к другу. Деплоите рекомендации — рискуете уронить платежи.
#Симптом 3: Один стек — не для всех задач
Когда вся система — монолит, вы используете один язык, один фреймворк, одну базу данных. И это прекрасно, пока это работает.
Но в какой-то момент вы понимаете, что обработка видео не очень-то ложится на Rails. Что real-time notifications на Python — это боль. Что поиск на SQL — это не поиск, а профанация.
И вы начинаете делать странные вещи: встраивать Elixir-процессы в Ruby-монолит, тащить очереди через sidekiq-pro, хотя на самом деле вам нужен Kafka. Архитектура начинает прогибаться под инструмент, хотя должно быть наоборот.
#Симптом 4: Одна БД — одно бутылочное горлышко
Самый болезненный симптом — база данных. В монолите все таблицы живут в одном месте. Хорошо это или плохо — зависит от контекста. Но когда на одну БД завязано пять команд, каждая с разными паттернами доступа, — начинается ад.
Команда А делает тяжёлый отчёт (полная агрегация миллионов строк). Команда Б в этот момент пытается обслужить пользовательские запросы. БД задыхается. DBA в истерике. Add index — спасёт? А lock table? А replication lag?
И вы понимаете: «Мы не можем просто взять и поставить PostgreSQL покрупнее». Потому что problem is not the database. Problem is that everything touches everything.
#Симптом 5: Bottleneck-команда
В монолите есть модули, которые знают все. Authentication. User profile. Notification. Payment.
На каждый PR, где меняется User, нужно одобрение от команды User. Потому что сломать User — сломать всю систему. User-команда становится bottleneck-ом: их ждут, их дёргают, они — бутылочное горлышко.
Парадокс: User-команда не обязана быть медленной. Просто на них завязано всё. И чем больше фич вы добавляете, тем больше они тормозят. Не потому что они плохие. А потому что все дороги ведут к ним.
#Когда это всё не про вас
Есть простой тест. Вспомните последний раз, когда вы добавили новую фичу.
- Вы написали код и задеплоили его за день? Всё ок, монолит работает.
- Вы написали код, но деплой откладывается, потому что «надо синхронизироваться с командой X»? Начинаются проблемы.
- Вы не знаете, сколько времени займёт фича, потому что «надо разобраться, какие модули это затронет»? Добро пожаловать в ад.
#Закон Конвея — архитектура отражает коммуникацию
В 1967 году Мелвин Конвей сформулировал наблюдение, которое стало одним из самых цитируемых законов в разработке:
Organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations.
Если по-русски (и без канцелярита):
Архитектура системы повторяет структуру коммуникаций команды, которая её создаёт.
Звучит как абстракция. На деле — это одна из самых практичных концепций в этой книге.
#Как это работает на практике
Представьте, у вас три команды: Frontend, Backend, Database. Как они организованы?
- Они сидят в разных комнатах / часовых поясах
- Коммуникация — через тикеты и синхроны раз в неделю
- У каждой свой Tech Lead, свой backlog, свои приоритеты
Как будет выглядеть архитектура? Вы угадали: отдельное фронтенд-приложение, отдельный REST API, отдельная база данных. Коммуникационные границы стали архитектурными границами. Это Conway's Law в действии.
А теперь представьте другую структуру: кросс-функциональные команды. Каждая команда отвечает за целый кусок бизнеса — от UI до базы данных. Команда «Checkout»: свой фронтенд, свой бэкенд, своя схема в БД. Команда «Search»: то же самое.
Архитектура будет другой: доменные сервисы (или микросервисы) с чёткими bounded context. Каждая команда владеет своим кусочком мира.
#Conway's Law — это не приговор, а инструмент
Вот ключевая мысль:
Если вам не нравится архитектура — измените коммуникацию. Не пытайтесь «нарисовать» архитектуру на диаграмме и заставить команды ей следовать.
Это работает в обе стороны:
- Хотите модульный монолит? Сделайте так, чтобы команды общались через внутренние API, а не через общую кодовую базу. Организуйте code owners на каждый модуль.
- Хотите микросервисы? Убедитесь, что у каждой команды есть bounded context, за который она отвечает от начала до конца. И что команды могут принимать решения независимо.
- Не хотите distributed monolith? Не заставляйте команды синхронизироваться по каждому изменению контракта. Проектируйте API так, чтобы они были самодостаточными.
Настоящая проблема многих переходов на микросервисы — не техническая. Команды реорганизовали, а структура коммуникации осталась прежней. В итоге — микросервисы есть, а независимости нет. Сервисы дёргают друг друга, синхронные вызовы плодятся, и вы получаете распределённый монолит.
Ах да, про распределённый монолит — в этой же главе, в разделе «Reverse Migration».
Cross-reference: Подробнее про Conway's Law в контексте организации команд и их влияния на архитектуру — в Главе 1 «Архитектура — это про людей». О том, как команды должны быть организованы для работы с микросервисами — в Главе 15.
#Стратегия декомпозиции: по бизнес-возможностям
Допустим, вы решили, что пора. Симптомы есть, Conway's Law осознали. Теперь главный вопрос: на что резать?
Здесь большинство делает фатальную ошибку.
#Как НЕ надо делить
Неправильный вариант — резать по техническому слою:
user-service-api(весь API в одном сервисе)user-service-backend(вся бизнес-логика)user-service-db(все запросы к БД)
Что вы получите? Три сервиса, которые жёстко связаны. Изменение API требует изменения backend и БД. Вы не ускорили разработку — вы добавили сетевое взаимодействие к существующей связанности. Поздравляю, вы создали распределённый монолит.
#Как надо делить — bounded context
Правильный ответ — делить по бизнес-возможностям (business capabilities). Каждый кусок — это законченная бизнес-функция со своим смыслом.
Возьмём интернет-магазин:
Монолит:
├── checkout
├── payments
├── catalog
├── search
├── recommendations
├── user-profile
├── notifications
├── inventory
└── adminКаждый из этих модулей — кандидат на сервис. Почему? Потому что у каждого свой bounded context (см. Главу 4 про DDD):
- Checkout — оформление заказа. Ему плевать на каталог. Ему нужно знать только то, что товар есть в наличии и цена актуальна.
- Catalog — управление товарами. Ему плевать на оформление заказа. Ему нужно знать, что админ добавил новый товар.
- Search — поиск товаров. Ему плевать на цены в реальном времени (нужна eventual consistency).
- Payments — проведение платежей. Ему плевать на пользовательский профиль.
Видите? Каждый модуль независим по смыслу. Их можно синхронизировать через события, а не через прямые вызовы. Каждый может жить своей жизнью.
#Как проверить, что декомпозиция правильная
Есть простой вопрос:
Может ли этот сервис выжить в одиночку?
Представьте, что все остальные сервисы упали. А этот — один — ещё как-то работает. Может ли он приносить пользу?
- Сервис
catalog— может. Он отдаёт список товаров, которые уже есть в кэше. Пользователь видит товары, но не может купить. - Сервис
user-profile— может. Он показывает профиль. Данные из кэша. Не идеально, но работает. - Сервис
notifications— не может. Ему неоткуда брать события, если все остальные упали. Ну, по крайней мере, он не будет слать спам. - Сервис
api-gateway— не может (и не должен). Он — прокси.
Если сервис не может существовать без пяти других — скорее всего, вы неправильно нарезали.
#А что насчёт размеров?
Самый популярный вопрос про микросервисы: «а какого они должны быть размера?».
Короткий ответ: неважно. Главное — bounded context, а не количество строк кода.
Длинный ответ: есть правило двух пицц (от Amazon) и есть правило «сервис должен помещаться в голове одного разработчика». Оба спорные. На практике размер сервиса определяется его ответственностью, а не строками кода.
В Amazon 2002 года сервис мог быть размером с класс. Потому что bounded context — это про семантические границы, а не про объём кода. Гораздо хуже, когда сервис — это огромный кусок монолита, тупо вынесенный в отдельный процесс.
Cross-reference: Подробнее про bounded context и выделение доменов — Глава 4 «DDD для работающих людей». Про правильный размер сервиса — Глава 15 «Микросервисы — правильный способ».
#Strangler Fig Pattern — заменяем монолит по частям
У вас есть монолит. Вы решили, что его пора декомпозировать. И теперь самый важный вопрос:
Как это сделать без остановки бизнеса?
Короткий ответ — паттерн Strangler Fig (фига-душителя). Название — от растений рода фикусов, которые обвивают дерево-хозяина и постепенно заменяют его, пока хозяин не погибнет. Метод известный, но давайте разберём, как он работает в реальности, а не в рекламных статьях.
#Как это выглядит в архитектуре
Исходное состояние — монолит, который обрабатывает все запросы.
Шаг 1. Вы ставите API Gateway (или reverse proxy) перед монолитом:
┌─────────────┐
│ API Gateway │
└──────┬──────┘
│
┌──────┴──────┐
│ Monolith │
└─────────────┘Все запросы идут через Gateway. Монолит пока обрабатывает всё.
Шаг 2. Вы выделяете первый bounded context — например, Search. Пишете новый сервис для поиска. Конфигурируете Gateway так, чтобы запросы на поиск шли в новый сервис:
┌─────────────┐
│ API Gateway │
└──────┬──────┘
│
┌─────────┴──────────┐
│ │
┌──────┴──────┐ ┌───────┴───────┐
│ Monolith │ │ Search Service│
└─────────────┘ └───────────────┘Шаг 3. Вы «пересаживаете» данные. Старые данные мигрируете в новый сервис (если нужно), настраиваете синхронизацию. Монолит больше не отвечает за поиск.
Шаг 4. Повторяете для следующего контекста. И ещё. И ещё. Пока монолит не засохнет:
┌─────────────┐
│ API Gateway │
└──────┬──────┘
│
┌───────────────┼───────────────────┐
│ │ │
┌────┴────┐ ┌────┴────┐ ┌─────────┴─────────┐
│ Monolith│ │ Search │ │ Checkout Service │
│ (small) │ │ Service │ └───────────────────┘
└─────────┘ └─────────┘
...#Что происходит с данными
Самый сложный момент в Strangler Fig — это данные. Когда вы переносите часть логики в новый сервис, данные всё ещё живут в старой БД монолита. Решений несколько:
- Дублирование с синхронизацией: Пишете в монолит, читаете из нового сервиса. Данные синхронизируются через CDC (Change Data Capture) или дуальный write.
- Миграция данных: Переносите таблицы в новую БД. Старый код больше не пишет в эти таблицы — только читает (или ничего не делает).
- API Façade: Новый сервис использует API монолита как источник данных. Постепенно заменяете внутренние вызовы на собственные запросы к БД.
Какой вариант выбрать — зависит от конкретики. Но общее правило: не пытайтесь перенести всё сразу. Начните с read path. Потом — write path. Между ними может быть неделя или месяц двойной записи.
#Реальный пример [Anecdotal]
Я был в проекте, где монолит на PHP (10 лет, 700K строк) «душили» около года. Выглядело это так:
Месяц 1-2: Поставили API Gateway (Kong). Всё работает как раньше, но трафик теперь контролируемый.
Месяц 3-4: Выделили user-service. Написали на Go. Мигрировали регистрацию и логин. Данные остались в старой MySQL — сделали read replica для нового сервиса. Параллельная работа: можно откатиться на монолит за 5 минут.
Месяц 5-6: Выделили search-service. Написали на Python с Elasticsearch. Включили постепенно: сначала 10% трафика через feature flag, потом 50%, потом 100%. Старый поиск отключили через месяц.
Месяц 7-8: Выделили notification-service. Тут было проще — сервис по своей сути асинхронный. Сделали очередь (RabbitMQ), монолит начал писать события туда, новый сервис — читать.
Месяц 9-12: Мелкие сервисы (images, reports, admin). Самое сложное — images. Зависимости старые, код грязный. В итоге решили: пусть пока живут в монолите. Система работает, монолит не мешает.
Итог: Через год монолит всё ещё жив, но обрабатывает только 30% трафика. Основные фичи — в отдельных сервисах. Команда выросла с 8 до 25 человек, скорость релизов выросла с «раз в месяц» до «несколько раз в день».
Ошибки на этом пути: недооценили миграцию данных. Думали, что «просто перепишем» — а оказалось, что бизнес-логика в stored procedures, триггерах и legacy-коде занимает больше времени, чем сам сервис.
Cross-reference: Про паттерн Strangler Fig в контексте рефакторинга legacy-систем — Глава 31 «Эволюция архитектуры». Про API Gateway — Глава 15 «Микросервисы — правильный способ».
#Incremental Migration — без big bang rewrite
Я знаю, о чём вы подумали.
«А давайте просто перепишем всё с нуля. Ну его, этот монолит, он 10 лет костылями рос, проще заново на микросервисах — и красиво».
Не делайте этого.
Серьёзно. Просто не делайте.
#Почему Big Bang Rewrite почти всегда провал
Джоэл Спольски сформулировал этот закон в 2000 году в статье «Things You Should Never Do, Part I»:
Переписывать код с нуля — это фатальная ошибка. Вы теряете всё, что наработали за годы баг-фиксов, edge case'ов и бизнес-логики.
Примеров — вагон. Netscape 6 переписали с нуля — потеряли рынок браузеров навсегда. Другие примеры: везде одно и то же — команда уходит в «пещеру» на 18 месяцев, выходит с «идеальной» архитектурой и обнаруживает, что за это время бизнес изменился, а старая система всё ещё работает и её никто не хочет менять.
Почему это происходит:
- Потерянное знание. В старом коде — баг-фиксы для редких, но критичных edge case'ов. Вы о них не помните, пока они не проявятся на проде через месяц после перехода.
- Бизнес не ждёт. Пока вы переписываете, рынок не стоит на месте. Конкуренты выпускают фичи. Ваш продукт простаивает.
- Scope creep. «Ну раз переписываем, давайте сразу добавим новую фичу». Через три месяца вы переписываете в два раза больше, чем планировали.
- Параллельная поддержка. Старую систему всё равно надо поддерживать. Баги — чинить. Пользователей — не бросать. Фактически вы содержите два проекта вместо одного.
#Incremental Migration — альтернатива
Вместо этого — инкрементальная миграция. Она медленнее, но работает.
Стратегия:
- Новый функционал — сразу в новых сервисах. Не пишите новые фичи в монолит, если они могут жить отдельно. Каждая новая фича — это кусочек системы, который уже распределённый.
- Старый функционал — переносим по частям. Strangler Fig, шаг за шагом.
- Feature flags — ваш лучший друг. Включаете новый сервис для 1% пользователей. Смотрите. Если всё ок — 10%. Потом 50%. Потом 100%.
- Откат — обязателен. Каждый шаг миграции должен быть обратимым. Если что-то пошло не так — вы возвращаетесь на старый сервис за 5 минут, а не за два дня дебага.
#Практический пример: миграция одной фичи
Покажу на примере. Есть монолит с функцией поиска товаров. Мы хотим выделить search-service.
Шаг 1. Измеряем. Сколько запросов к поиску? Какая latency? Какие ошибки? Это — baseline для сравнения.
Шаг 2. Parallel Run. Новый сервис запускается, но трафик на него не идёт. Он просто слушает те же запросы, что и монолит (через mirroring в Gateway). Сравниваем результаты:
# Псевдокод: parallel run
def handle_search_request(request):
# Основной запрос — в монолит (как раньше)
monolith_result = monolith.search(request)
# Фоновый запрос — в новый сервис (результат не возвращается пользователю)
try:
new_result = search_service.search(request)
compare_results(monolith_result, new_result)
except Exception:
pass # Новый сервис может падать — это ок
return monolith_resultШаг 3. Shadow mode. Когда расхождений результатов нет, переключаем 1% трафика на новый сервис. Смотрим метрики.
Шаг 4. Gradual rollout. 10% → 50% → 100%.
Шаг 5. Cleanup. Старый код в монолите удаляем (или помечаем как deprecated). Убеждаемся, что он никому не нужен.
Весь процесс занимает 2-4 недели на одну фичу. Это дольше, чем «перепишем за выходные». Но это гарантированно не убьёт продакшен.
#Когда Big Bang — единственный выход?
Бывает. Очень редко.
Когда кодовая база настолько мала, что переписать её быстрее, чем разбираться с инкрементальной миграцией. Или когда фреймворк/язык устарел настолько, что поддержка обходится дороже переписывания. Или когда архитектура настолько не соответствует требованиям, что каждое изменение ломает всё.
Но если вы читаете эту главу — скорее всего, ваш случай не такой. Ваш монолит большой, живой и приносит деньги. Убейте его по частям, а не целиком.
Cross-reference: Про Big Bang Rewrite как анти-паттерн — см. references/anti-patterns.md. Про feature flags — Глава 13 «CI/CD для монолита» и Глава 15 «Микросервисы — правильный способ».
#Reverse Migration: когда микросервисы оказались ошибкой
Самая неловкая тема в архитектурных разговорах. Про монолит говорят с лёгким пренебрежением, про микросервисы — с гордостью. Но никто не говорит про обратную миграцию: когда микро- это перебор, и вы возвращаетесь обратно.
Тема настолько болезненная, что многие инженеры предпочитают её не обсуждать. «Мы не откатываемся, мы реструктурируем архитектуру». Звучит знакомо?
#Когда это происходит
Обратная миграция случается не от хорошей жизни. Вот типичные сценарии:
-
Преждевременная декомпозиция. Разрезали монолит на 20 микросервисов, пока команда была 10 человек. Получили operational overhead на поддержку 20 деплоев, 20 баз данных, 20 CI/CD пайплайнов. Скорость упала, а не выросла.
-
Распределённый монолит. Микросервисы есть, а независимости нет. Все сервисы общаются синхронно. Изменение в одном требует деплоя в десяти. Shared database между сервисами. Результат: все недостатки микросервисов (сеть, сложность дебага, operational overhead) без их преимуществ (независимость).
-
Потеря экспертизы. Команда не готова к распределённым системам. Нет людей, которые понимают, как отлаживать cross-service трассировку, как настраивать circuit breaker'ы, как управлять распределёнными данными. В итоге — «много сервисов, мало понимания».
#Как понять, что пора возвращаться
Симптомы распределённого монолита мы уже обсуждали (см. references/anti-patterns.md):
- Один deploy требует деплоя 10+ сервисов
- Shared database между «микросервисами»
- Синхронные цепочки вызовов: A → B → C → D
- Feature flag невозможно включить для одного сервиса
- Никто не знает, какой сервис за что отвечает
Если это про вас — признайте. Микросервисы не работают, когда они не независимы.
#Стратегия обратной миграции
Хорошая новость: паттерн Strangler Fig работает в обе стороны. Если вы умеете «душить» монолит новыми сервисами — вы умеете и «душить» микросервисы, возвращая их в монолит (или модульный монолит).
Шаги те же:
- Выбираете сервис, который приносит больше боли, чем пользы.
- Собираете его логику обратно в монолит (или в новый модуль монолита).
- Настраиваете API Gateway так, чтобы трафик шёл в монолит, а не в старый сервис.
- Повторяете.
Сложность в данных: если у сервиса своя БД, её надо или объединять с монолитом (сложно), или оставить отдельно (проще), или мигрировать данные (больно). Но это решаемо.
#Реальные примеры reverse migration
Uber. К 2017 году — 2200+ микросервисов. Но многие из них оказались настолько связанными, что деплой одного требовал каскадных деплоев. В 2018+ началась консолидация: часть сервисов объединили в более крупные, часть — вернули в «сервисы среднего размера». Uber не отказался от микросервисов полностью, но сделал их более крупными и осмысленными.
Segment (Twilio). В 2020 году опубликовали кейс: «How we consolidated our microservices». У них было 50+ микросервисов для 20 разработчиков. Operational overhead убивал продуктивность. Объединили в 5 модульных сервисов — скорость разработки выросла, overhead упал.
Amazon Prime Video. В 2023 году рассказали на re:Invent, как объединили микросервисы для мониторинга видео-стримов обратно в монолит. Результат: снижение затрат на 90% и упрощение архитектуры. Да, это Amazon — компания, которая изобрела микросервисы в современном виде, сама откатывает часть обратно.
Вывод: микросервисы — не трофей. Это инструмент. Если инструмент не работает — меняйте инструмент, а не ломайте систему.
Cross-reference: Подробнее про Distributed Monolith — в Главе 15 «Микросервисы — правильный способ» (там же — как не скатиться). Про модульный монолит как альтернативу — Глава 12.
#Кейс: Amazon — как один монолит стал сотнями сервисов
Самый известный переход в истории веб-архитектуры. Если вы читали про микросервисы, вы наверняка слышали про Amazon 2001-2002 годов. Но давайте разберёмся, что там произошло на самом деле, а не в мифах.
#Что было до
2001 год. Amazon — уже крупный интернет-магазин, но его инженерная организация — один монолит.
Это не просто «монолит на Java». Это монолит, который занимал 2 гигабайта в виде WAR-файла. Сборка занимала часы. Deploy — дни. В монолите работало больше 100 разработчиков, и все они коммитили в один репозиторий.
Каждое изменение было рискованным. Хотите поправить цвет кнопки на странице поиска? Ваш PR может сломать платёжный модуль. Потому что нет границ. Всё — в одном месте.
Проблемы, знакомые каждой крупной команде:
- Cycle time от идеи до продакшена — недели
- Координация — каждое изменение нужно согласовывать с кучей команд
- Релизный стресс — каждый деплой — событие с молитвами
- Масштабирование — весь стек масштабируется одинаково, хотя каталог и checkout имеют разные профили нагрузки
#Безосовский Mandate
В 2002 году Джефф Безос издал знаменитый мандат, который навсегда изменил Amazon — и, косвенно, всю IT-индустрию.
Мандат был простой и жёсткий:
-
Все команды обязаны предоставлять свои данные и функциональность через сервисные интерфейсы. Никакого прямого доступа к БД другой команды. Никакого shared filesystem. Никаких «дай-ка запущу SQL-запрос к твоей базе».
-
Все сервисы обязаны общаться только через API по сети. Конкретно — через HTTP. Никаких прямых вызовов методов, библиотек, shared memory.
-
Все сервисы должны проектироваться так, как будто они могут быть доступны внешним разработчикам. Никаких «ну это же internal API, не заморачивайся». Если другой команде не удобно пользоваться вашим API — это ваша проблема.
-
Кто не соответствует — тому нечего делать в Amazon. (Примерно так. Безос был конкретен.)
Стив Йегг (бывший сотрудник Amazon, позже — Google) писал об этом в своей знаменитой заметке:
«Безос говорил примерно так: "Отныне все команды обязаны APIфицировать свои данные и функциональность. Команды должны общаться друг с другом исключительно через эти API. Есть другие способы — они будут unacceptable. Никто не должен трогать базу данных другой команды. Всё — через сеть. И не надо мне рассказывать, что это сложно"»
#Как это внедряли
Вот что интересно: Безос не сказал «давайте перепишем монолит на микросервисы». Он сказал «давайте изменим правила коммуникации». Это снова Conway's Law.
Amazon не занимался big bang rewrite. Они меняли архитектуру снизу, по принципу Strangler Fig — только задом наперёд: сначала — контракты и API, потом — разделение.
- Сначала выделили API-интерфейсы для bounded context'ов.
- Потом начали выносить реализацию в отдельные сервисы.
- Каждый сервис получал свою базу данных.
- Команды становились владельцами своих сервисов.
Это заняло годы. Не недели. Не месяцы. Годы. К 2005-2006 Amazon уже была компанией с сотнями сервисов.
#Что из этого выросло
У декомпозиции Amazon был неожиданный побочный эффект: AWS.
Когда каждая команда построила свою инфраструктуру для сервисов (мониторинг, очереди, хранилища, compute), кому-то (говорят, это был Бенджамин Блэк или кто-то из инфраструктурной команды) пришла в голову идея: «А давайте продадим эту инфраструктуру другим».
Так родился Amazon S3 (2006) — первый публичный сервис AWS. Потом SQS, SimpleDB, EC2. Вся AWS — прямое следствие сервис-ориентированной архитектуры Amazon внутри.
Если бы Amazon не перешёл на микросервисы, AWS, скорее всего, не существовало бы. Или существовало бы в другом виде.
#Что мы можем выучить из этого кейса
- Изменяйте коммуникацию, а не код. Безос начал с правил взаимодействия. Архитектура подстроилась под эти правила.
- API-first — не протокол, а дисциплина. В Amazon каждый сервис проектировался так, как будто он external. Это принципиально меняет подход.
- Big bang не нужен. Amazon переходил годами. Постепенно, сервис за сервисом.
- Культура важнее технологии. Мандат Безоса не про микросервисы. Он про ownership, про контракты, про уважение к границам.
- Масштаб требует автоматизации. Из внутренней необходимости родился AWS. Если у вас сотни сервисов — управлять ими вручную невозможно. Платформа станет необходимостью.
Источники: Steve Yegge's Google+ post (2011), AWS re:Invent presentations, Amazon Builders Library.
#Не делай так
#1. Резать по техническому слою
Выделили db-service, api-service, logic-service. Выглядит как архитектура? Нет, это распределённый монолит. Изменение фичи требует изменений во всех трёх сервисах. Вы не ускорили команду — вы добавили ей сетевое взаимодействие.
Как надо: резать по bounded context. checkout-service, payment-service, search-service.
#2. Big Bang Rewrite
«Давайте всё перепишем на Go/Машину Тьюринга/Ассемблер». Не делайте этого. Вы потеряете год жизни, бизнес-логику, баг-фиксы и, возможно, работу.
Как надо: Strangler Fig. Новый функционал — в новом сервисе. Старый — переносите по частям.
#3. Микросервисы для команды из 3 человек
Когда команды нет, а микросервисы уже есть — это повод для разговора. С operational overhead'ом микросервисов команда из трёх человек будет справляться полтора дня в неделю.
Как надо: Модульный монолит (Глава 12) на старте. Микросервисы — когда команда > 10-15 человек и есть симптомы из начала главы.
#4. «А давайте сделаем сервис вызова такси — по принципам Amazon»
Вы не Amazon. Amazon переходил на микросервисы 3+ года, с сотнями инженеров и абсолютным топ-менеджмент mandate. У вас — 2 недели и спринт. Не сравнивайте.
Как надо: Учитесь у Amazon дисциплине API и bounded context. Не копируйте масштаб.
#5. Забыли про данные
«Вынесли поиск в отдельный сервис — и тут оказалось, что код поиска требует доступа к таблицам заказов, которые в монолите. И теперь у сервиса поиска прямой доступ к БД монолита — shared database снова здесь».
Как надо: Сначала спроектируйте, какие данные нужны новому сервису. Если данные живут в монолите — решите, как их мигрировать или синхронизировать (CDC, event bus, API фасад).
#6. Оптимизировать под микросервисы до того, как они нужны
«Давайте добавим circuit breaker на вызов к модулю, который лежит в соседнем процессе и не умирал ни разу за два года». Если у вас пока что не микросервисы — не стройте инфраструктуру для них.
#Чек-лист для самопроверки
Прежде чем начать декомпозицию — проверьте себя:
- У вас есть хотя бы 3 симптома из начала главы? (Если только 1 — не торопитесь)
- Команда > 10-15 человек? (Если меньше — модульный монолит, Глава 12)
- Есть чёткие bounded context? (Если нет — сначала DDD, Глава 4)
- Команда готова к operational complexity? (Kubernetes, CI/CD для 10+ сервисов, мониторинг?)
- Есть mandate от руководства (как у Безоса)? (Без него — микросервисы не взлетят — архитектура следует за властью, а не за кодом)
- У вас есть люди с опытом распределённых систем? (Если нет — наймите или пересмотрите план)
- У вас есть план отката? (Каждый шаг миграции — обратим)
- У вас есть feature flags и canary deploy? (Без них не начинайте)
- Вы готовы к reverse migration? (Признать ошибку и откатиться — это нормально. Отсутствие плана Б — нет)
#Ключевые выводы
-
Не переходите на микросервисы «потому что модно». Переходите, когда монолит реально мешает: bottlenecks, релизные циклы, связанность команд. Если симптомы не очевидны — не надо.
-
Conway's Law — не абстракция. Архитектура повторит структуру коммуникаций. Измените правила взаимодействия — и архитектура изменится сама. Безос начал не с кода, а с мандата на API.
-
Strangler Fig — ваш единственный друг на миграции. Никакого Big Bang Rewrite. Заменяйте монолит по частям, постепенно, с возможностью отката на каждом шаге.
-
Бизнес-возможности — правильный способ декомпозиции. Не technical layers, не размер сервиса, а bounded context. Если сервис может выжить без остальной системы — вы на правильном пути.
-
Reverse migration — это не стыдно. Amazon Prime Video, Uber, Segment — все откатывали часть микросервисов обратно. Если инструмент не работает — меняйте инструмент.
#Ссылки на другие главы
- Глава 1: «Архитектура — это про людей» — Conway's Law и коммуникация
- Глава 4: «DDD для работающих людей» — bounded context, ubiquitous language
- Глава 11: «Монолит — не грязное слово» — почему монолит нормален
- Глава 12: «Модульный монолит» — альтернатива микросервисам
- Глава 13: «CI/CD для монолита» — тестирование и деплой до перехода
- Глава 15: «Микросервисы — правильный способ» — после того, как решились
- Глава 17: «Распределённые данные» — самая сложная часть микросервисов
- Глава 31: «Эволюция архитектуры — истории рефакторинга» — реальные ошибки