19 апреля 2026
Распределённые системы - не архитектурный стиль, к которому стремятся ради красоты. Это решение конкретных бизнес-проблем: независимый deploy разных команд, изолированное масштабирование, контроль failure domains, регуляторное разделение. Выбор распределённости осознанный и принимается, когда монолит перестаёт решать задачу. После выбора дороги назад почти нет: эксплуатация накладывает жёсткие условия - ненадёжная сеть, eventual consistency, observability как обязательное требование. Паттерны - ответы на эти условия, не украшения.
Почему распределённые системы: осознанный выбор, не дефолт
Распространённая ошибка - начинать проект с микросервисов потому что «так делают». Cargo cult. Распределённость оправдана, когда решает конкретную проблему, которую монолит не может решить.
Что оправдывает переход.
Независимый deploy команд. Организация выросла до 40-50+ инженеров, несколько команд пишут в один артефакт, релиз-цикл стал узким местом. Team Topologies требует автономии Stream-aligned команд в доставке. Распределение даёт deployment boundary, совпадающий с team boundary.
Нюанс: это не значит, что монолит стал legacy и его можно не развивать. Монолит остаётся core-артефактом для части команд; за его CI/CD, deployment SLA, временем сборки, качеством тестов нужно постоянно следить и достигать нужных показателей. Выносить стоит то, что действительно хорошо отделимо - по доменам, командам, профилю изменений. Остальное продолжает жить в монолите и должно иметь здоровый pipeline, а не деградировать.
Второй нюанс: не путать deploy (доставку кода в production) и release (открытие функционала пользователям). Это разные вещи независимо от архитектуры. Со здоровым pipeline deploy происходит постоянно - изменения катятся в production несколько раз в день. Release же происходит on-demand: функционал скрыт за feature toggles и открывается по команде продукта, отдельно от deploy. Это снимает страх релиза, позволяет постепенную раскатку, A/B-тесты, и быстрый откат функционала без отката кода. Разделение deploy и release полезно и в монолите, и в распределённой системе.
Независимое масштабирование. Причину часто называют, но срабатывает она не всегда в том виде, как ожидают. Ту же кодовую базу можно запускать в разных сетапах под разные профили нагрузки - классика в Ruby-on-Rails-подобных фреймворках: общая бизнес-логика, но отдельные deployment-units для web-слоя, sidekiq (background jobs), websockets, cron. Они масштабируются независимо, хотя кодовая база одна. То же с БД: шардирование и партиционирование решают вопрос масштабирования данных без разделения на сервисы.
Не стоит путать «монолит» с «одним способом запуска под одну нагрузку». Монолит масштабируется гибче, чем часто кажется. Разделение на независимые сервисы оправдано, когда профили нагрузки радикально разные и разделение на deploy-units монолита не решает задачу: CPU-heavy (видеокодирование) vs IO-bound (поиск) vs memory-heavy (ML-inference) с разным runtime. Но большая распределённая система становится сложной сама по себе - больше компонентов, больше точек отказа, больше взаимного влияния.
Изоляция failure domains. Критичный сценарий (платежи) не должен падать из-за проблем в некритичном (рекомендации). В монолите они делят ресурсы и валят друг друга.
Нюанс: не стоит жить в иллюзии, что разделение на сервисы гарантирует работу критичной части при падении некритичной. При неправильно проведённых границах получаете каскадную деградацию: некритичный сервис падает, критичный синхронно в него ходит, критичный тоже деградирует, а отлаживать становится тяжелее - падение произошло внутри, через несколько движущихся частей. Изоляция даёт потенциал, но реализуется только при строгой дисциплине: circuit breakers, таймауты, fallback-поведение, асинхронные зависимости где возможно. Без этой дисциплины получаете distributed monolith с той же связанностью, но более сложным debug.
Важное следствие: границы бизнес-операций можно и нужно выстраивать уже в монолите - так, как будто система распределённая. Независимые бизнес-операции, явные контракты между модулями, изоляция падений на уровне бизнес-логики (одна операция не валит другую даже в рамках одного процесса). Это решает сразу две задачи. Первая - готовность к возможному выносу в отдельный сервис, когда потребуется: границы уже на месте, контракты определены, зависимости понятны. Вторая - убирает связанность прямо сейчас, не добавляя операционной сложности. Контракты синхронизированы by design (один репозиторий, один релиз), легко меняются; границы, пока они внутри одного процесса, заметно легче двигать при эволюции бизнес-модели. Когда бизнес-изменение требует пересобрать границу - в монолите это refactoring за несколько часов, в распределённой системе - миграция данных и координация команд на недели.
Регуляторные требования. PCI, PII, GDPR, медицинские данные часто проще изолировать на уровне сервиса, чем на уровне кода. Обычно разумнее вынести compliance-зону в отдельный сервис с тонким явным контрактом и сосредоточить compliance-усилия там, чем пытаться привести всю основную систему к максимальному compliance-состоянию. Отдельный сервис даёт ясные границы для аудита, отдельный security-периметр, проще доказывать соответствие регулятору.
Что распределённость не оправдывает:
- Команда меньше 10 инженеров. Operational overhead превышает выигрыш.
- Домен не устоявшийся. Границы сервисов меняются при изменении доменной модели.
- Монолит не bottleneck. Если архитектура справляется - менять ради правильности рискованно.
- Нет готовности к DevOps-инфраструктуре. Без observability, deployment automation, service discovery получаете distributed monolith с операционным кошмаром.
Детальное сравнение с монолитом - в статье «Монолит vs Микросервисы». Ключевое: распределённость - инструмент решения бизнес-проблем, не самоцель.
Условия, которые распределённость накладывает
Выбор сделан - система распределённая. С этого момента действуют условия, которые невозможно игнорировать. Peter Deutsch в 1994 году сформулировал восемь fallacies of distributed computing - заблуждений, регулярно совершаемых инженерами, переходящими от локального к распределённому:
- Сеть надёжна.
- Latency равна нулю.
- Bandwidth бесконечен.
- Сеть безопасна.
- Топология не меняется.
- Есть один администратор.
- Transport cost равен нулю.
- Сеть гомогенна.
Каждое заблуждение - не программистская ошибка, а свойство модели. Сеть будет падать - нужны таймауты и retries. Latency будет расти - нужны асинхронные флоу и кэши. Порядок сообщений не гарантирован - нужна идемпотентность и упорядочивание. Топология меняется - нужен service discovery.
Декомпозиция: где проводить границы
Первое архитектурное решение - где проходят границы сервисов. Неправильная декомпозиция - самая дорогая ошибка; перерисовать границы работающей системы в несколько раз дороже, чем спроектировать правильно.
Decompose by business capability
Границы проходят по тому, что делает бизнес как функции: приём платежей, управление подписками, формирование отчётов, отправка уведомлений. Каждая capability - кандидат в сервис или группу сервисов.
Подход ориентирован на внешний продукт: что пользователь получает от системы как отдельные функции. Работает, когда business capabilities стабильны и различимы в терминологии бизнеса. Хорошо подходит для организаций с устоявшимся доменом, где структура функций меняется редко.
Ограничение: capabilities могут быть разной зрелости. Новая capability в ранней стадии продукта часто ещё не определилась как отдельная - её границы двигаются. Выделять в отдельный сервис преждевременно - дорого: через полгода придётся перерисовывать границы.
Decompose by subdomain
Границы совпадают с bounded contexts из DDD. Каждый bounded context - один сервис или группа связанных сервисов. Доменная согласованность: один сервис = одна модель, один язык предметной области.
Это обычно самый надёжный подход. Bounded contexts отражают структуру домена, которая более стабильна, чем текущие capabilities. Когда бизнес добавляет новые capabilities, они чаще ложатся на существующие bounded contexts как расширения, чем требуют создания новых. Подробно о стратегическом DDD и bounded contexts - в статье «Domain-Driven Design».
Decompose by business capability и decompose by subdomain не противоречат: часто дают близкие границы, но subdomain даёт более доменно-обоснованные решения.
Self-contained service
Сервис работает автономно, минимально зависит от других. Если зависимость неизбежна - через асинхронные события, не синхронные вызовы. Критерий: падение соседнего сервиса не валит этот.
Self-containment достигается не декомпозицией границ, а дизайном взаимодействий. Два bounded contexts могут быть хорошо разделены концептуально, но их реализации взаимно синхронно дёргать друг друга по каждому чиху - это уже не self-contained сервисы. Нужна дисциплина: default - async, synchronous только когда действительно необходимо (user-facing read).
Полезный тест: выключить соседний сервис и посмотреть, что сломается в твоём. Если ломается много - self-containment под вопросом.
Service per team
Границы сервисов совпадают с границами ответственности команд. Каждая Stream-aligned команда владеет одним или несколькими сервисами end-to-end - от кода до production, включая on-call и SLA.
Это прямое следствие закона Конвея и Team Topologies: структура команд отражается в структуре системы, и наоборот. Если команда A владеет сервисом X, а команда B - сервисом Y, коммуникация между сервисами превращается в коммуникацию между командами. Проектировать команды и сервисы отдельно - значит создавать постоянный конфликт ownership. Подробнее в статье «Team Topologies».
На практике decompose by subdomain и service per team чаще всего дают одинаковые границы, потому что Team Topologies рекомендует Stream-aligned команды по bounded contexts.
Правило: границы только через публичный API, без shared resources
Декомпозиция даёт логическое разделение. Изоляция требует дополнительного правила: сервисы общаются только через явные публичные API (синхронные или асинхронные), не через общие ресурсы.
Это значит:
- Одна БД на сервис. Другие сервисы не имеют прямого доступа к чужой БД. Если им нужны данные - через API.
- Не шарить файловые системы. Общий disk или S3-бакет как канал обмена - это общий ресурс; изменения в структуре файлов задевают всех, кто их читает.
- Не шарить cache. Redis-кеш, используемый несколькими сервисами для бизнес-данных, - скрытая shared database.
- Не шарить очереди как канал передачи состояния. Брокер сообщений - инструмент async-коммуникации через события, не механизм обмена рабочими данными.
Почему это принципиально. Общий ресурс - это скрытый контракт. Команда, владеющая «своим» сервисом, на самом деле не владеет им полностью: любое изменение структуры в общем ресурсе задевает всех, кто к нему обращается. Независимый deploy становится невозможен - нужно координировать изменения. Независимое масштабирование ограничено - один ресурс становится bottleneck. Failure isolation исчезает - падение общего ресурса валит всех его клиентов.
Небольшие исключения допустимы:
- Общий identity provider (SSO, OAuth): сервисы могут обращаться к одному месту для проверки токенов. Это специализированный shared сервис, а не shared data.
- Read-only replicas для аналитики. Data warehouse часто читает реплики сервисных БД. Главное - read-only и с ясными владельцами: структура БД всё равно контролируется сервисом, dashboards адаптируются.
- Observability data (metrics, logs, traces). Эти данные по определению общие - их и собирают для кросс-сервисного анализа.
Во всех остальных случаях - общение через API. Это превращает коммуникацию в явный контракт, который можно версионировать, ограничивать, защищать и изменять осознанно.
Антипаттерн: декомпозиция по техническим слоям
Сервисы «Auth», «Database», «API layer», «Business logic» - разделение по функциям в технической системе, не по бизнес-доменам. Любое бизнес-изменение задевает все сервисы, independent deploy невозможен, получается distributed monolith с максимальной операционной сложностью и минимальной выгодой.
Стили коммуникации
После разделения - вопрос как сервисы общаются. Выбор стиля определяет не только технику передачи данных, но и степень связанности между сервисами. Транспорт и семантика - разные вещи: один транспорт может нести разные виды коммуникации с разным уровнем coupling.
Синхронная коммуникация (request-response, RPI)
REST, gRPC, GraphQL. Клиент отправляет запрос, блокируется, ждёт ответа. Просто в debug - путь запроса можно проследить в обычном стектрейсе или distributed trace. Подходит для user-facing запросов с требованием немедленного ответа: запрос статуса заказа, поиск, авторизация.
Связанность: runtime + logical. Сервис A знает, что вызывает сервис B, знает интерфейс B, зависит от доступности B в момент вызова. B недоступен - A не может выполнить операцию.
Trade-off: в цепочке сервисов доступность падает как произведение. 99.9% × 99.9% × 99.9% = 99.7% - три девятки на каждом сервисе дают менее трёх девяток на сценарии. Плюс latency суммируется.
Отдельная категория - цепочки мутирующих синхронных вызовов. Это один из самых сложных и проблематичных классов в распределённых системах, потому что падение на любом шаге оставляет систему в неопределённом состоянии. Наивный подход - «просто вызовем сервисы по порядку» - ломается на первой же проблеме: платёж прошёл, но заказ не создался; заказ создан, но склад не зарезервировал товар; пользователь зарегистрирован, но маркетинг-сервис не получил данные. Debug становится мучительным: нужно восстановить, на каком шаге упало, что уже произошло, что нужно компенсировать, что оставить.
Именно поэтому критичны правильные границы. Одна бизнес-операция, требующая мутации в нескольких сервисах, - это сигнал, что границы, возможно, проведены неверно, или что операцию нужно перестроить в async-события с компенсациями (см. SAGA ниже).
Ключевое: паттерны решения здесь часто не инженерные, а бизнесовые. Архитектор не может просто «добавить try/catch» - нужно решение, которое примет бизнес:
- Платёж списать не удалось - паковать заказ или нет? (риск потери товара vs риск потери клиента)
- Такси вызвать до списания денег или после? (риск поездки без оплаты vs риск списания без поездки)
- Зарегистрировать пользователя, если в маркетинг-системе уже есть такой email от холодного лида? (конфликт владения данными)
- Отменить бронирование при сбое платежа, или оставить как pending? (сколько держать слот)
У каждого такого вопроса нет технически «правильного» ответа - ответ определяется бизнес-моделью, доверием клиенту, ценой ошибки, регуляторной средой. Инженер может предложить варианты и их последствия, но выбор - за продуктом и бизнесом. Без этого выбора распределённая система не проектируется: любая реализация зафиксирует какую-то политику, и если она не согласована - это будущий источник инцидентов и конфликтов.
Асинхронная коммуникация (messaging)
Kafka, RabbitMQ, AWS SNS/SQS, NATS. Отправитель публикует сообщение в брокер и продолжает работу, не дожидаясь обработки. Получатель читает из брокера в своём темпе.
Главное различие с синхронной: на уровне эксплуатации отправитель не зависит от доступности получателя в момент отправки. Получатель может быть временно недоступен - сообщения копятся в брокере. Нет каскадной деградации, нет суммирования latency, нет ожидания синхронного ответа.
Async commands vs async events
Это два разных паттерна, часто путаемые.
Асинхронная команда - отправитель говорит получателю, что сделать. «Отправь email», «спиши средства», «зарезервируй товар». Получатель известен отправителю по имени (queue, topic). Контракт команды определён обеими сторонами: какие поля, какие значения, какие ошибки.
Связанность: logical, хотя и не runtime. Отправитель знает про получателя - знает, что этот конкретный сервис должен выполнить операцию. При изменении контракта команды менять логику нужно и там, и там. Получатель падает - команды не обрабатываются, но отправитель продолжает их класть в очередь.
Асинхронное событие - отправитель сообщает факт, который произошёл у него. «Заказ создан», «платёж получен», «пользователь зарегистрирован». Отправитель не знает, кто слушает и что будет делать. Может никого не быть. Может быть десять подписчиков с разными реакциями.
Связанность: минимальная. Отправитель не зависит от наличия или поведения подписчиков. Новый подписчик добавляется без изменений в отправителе. Отсутствие подписчиков не останавливает работу отправителя.
Практическое правило: если при смене контракта нужно знать всех, кого это затронет, и синхронизировать с ними изменение - это команда (возможно, замаскированная под событие). Если контракт расширяется без уведомления потребителей - это событие.
Сравнение связанности
| Тип | Runtime coupling | Logical coupling |
|---|---|---|
| Sync request-response | Высокий | Высокий |
| Async command | Низкий | Высокий |
| Async event | Низкий | Низкий |
Выбор стиля
- User-facing read с немедленным ответом (статус заказа, поиск) - sync.
- Side-effect операция, выполняемая надёжно, но не синхронно (отправить email, обновить индекс, начислить бонусы) - async command. Отправитель знает получателя, нужна гарантия выполнения.
- Факт изменения в домене, на который могут реагировать несколько сервисов (заказ создан, подписка отменена) - async event. Отправитель не знает подписчиков.
Смешанный подход - норма. Одна и та же система использует все три стиля в разных местах.
Антипаттерны
- Длинная синхронная цепочка. A → B → C → D, все синхронно. Latency суммируется, failure domains связаны, cascading failure при любом падении. Решение - разрывать async-ом или кэшами, применять circuit breakers.
- Async commands где нужны events. Отправитель явно знает получателей и отправляет им команды через брокер. При добавлении нового потребителя нужно менять отправителя - каждое «расширение» требует координации. Симптом - очереди с именами получателей (
queue_for_email_service) вместо событий с доменным смыслом (user_registered). - Async events где нужны commands. Отправитель полагается, что какой-то подписчик обязательно отреагирует. Но событие не гарантирует обработки - нет ответа, нет подтверждения. Если одновременно требуется гарантия выполнения и асинхронность - это команда (возможно, с ответом-событием), не pure event.
Data management: consistency модели
В монолите - одна БД, ACID, strong consistency по дефолту. В распределённой системе выбор есть.
- Strong consistency. После записи все читатели видят новое значение. 2PC или consensus (Raft, Paxos). Дорого, чувствительно к partitions.
- Eventual consistency. Читатели увидят новое значение «когда-нибудь», обычно в пределах секунд. Дешевле, переживает partitions.
- Causal consistency. Если событие B «вызвано» A, все читатели увидят A перед B. Компромисс для многих бизнес-сценариев.
CAP theorem на практике. При network partition P обязательна - partitions случаются. Остаётся выбор C vs A:
- AP: пользовательские сценарии (каталог, просмотр заказов). Чуть устаревшие данные, но сервис работает.
- CP: критичные транзакции (финансы, инвентарь). При partition отказываем, не допуская расхождения.
Database per service. Каждый сервис владеет своей БД. Коммуникация - через API сервиса. Основной паттерн.
Антипаттерн: shared database. Несколько сервисов пишут в общую БД. Изменение схемы задевает всех, lock contention, нельзя менять БД без согласования. Скрытая связанность без преимуществ распределения.
SAGA: транзакции через границы сервисов
Бизнес-процесс «оформить заказ» затрагивает несколько сервисов: инвентарь, платёж, заказ, уведомление. В монолите - одна ACID-транзакция. В распределённой - несколько шагов в разных сервисах.
Почему 2PC не работает на практике. Distributed transactions технически существуют, но в production редки. Требуют доступности всех участников, держат locks, чувствительны к partitions. Низкая throughput и частые отказы.
SAGA как альтернатива. Длинный процесс разбивается на последовательность локальных транзакций. Если шаг падает - запускаются компенсирующие транзакции.
Choreography
Каждый сервис слушает события других и реагирует. Заказ создан → inventory списывает → публикует «списано» → payment списывает деньги → и т. д. Нет центрального координатора.
- Плюсы: простота, отсутствие single point of failure.
- Минусы: логика распределена, сложно отследить процесс, легко получить циклы.
Orchestration
Центральный координатор знает процесс и явно управляет переходами. Вызывает сервисы по очереди, ловит ошибки, запускает компенсации.
- Плюсы: явный процесс, легко добавлять шаги, отслеживать состояние.
- Минусы: orchestrator - point of coupling; при плохом проектировании становится распределённым монолитом.
Техники «изоляции без ACID»
SAGA не даёт isolation - другие процессы могут увидеть промежуточное состояние.
- Semantic locks: агрегат на время SAGA в статусе
pending_approval, другие операции отклоняются. - Commutative updates: операции применимы в любом порядке (debit/credit независимы).
- Повторное чтение: перед операцией проверяем, не изменился ли ресурс; если да - SAGA с начала.
- Pessimistic view: критичный шаг выносится в начало, компенсации ставятся позже.
Когда SAGA оправдана. Длинные бизнес-процессы с понятной логикой компенсации. Антипаттерн - SAGA на каждый чих. Об экспоненциальной сложности - в статье «Domain-Driven Design».
Transactional messaging: надёжная доставка событий
Сервис меняет состояние в БД и хочет опубликовать событие. Проблема: операция в БД и отправка в брокер - разные действия, атомарно нельзя без distributed transactions.
Варианты без transactional messaging - все плохие:
- Записать в БД, потом в брокер. Падение между шагами - событие потеряно.
- В брокер, потом в БД. Падение между шагами - событие ушло, изменение откатилось. Consumers действуют на несуществующем факте.
Outbox pattern. События пишутся в той же транзакции, что и бизнес-изменения, но в таблицу outbox. Отдельный процесс читает outbox и публикует в брокер.
BEGIN TRANSACTION;
UPDATE orders SET status = 'paid' WHERE id = 123;
INSERT INTO outbox (event_type, payload)
VALUES ('OrderPaid', '{"order_id": 123, "amount": 99.00}');
COMMIT;
Публикация из outbox:
- Polling publisher: отдельный процесс периодически читает outbox, публикует, помечает. Просто, но добавляет латенси и нагрузку на БД.
- Transaction log tailing (CDC): слушаем commit log БД (Debezium + PostgreSQL WAL, MySQL binlog). Нет polling, меньшая латенси. Требует CDC-инфраструктуры.
At-least-once + идемпотентность. Outbox гарантирует доставку «хотя бы раз», но публикация может повторяться (retries). Consumer должен быть идемпотентным: обработка того же события дважды даёт тот же результат. Хранение ID обработанных событий, отклонение дубликатов. Подробно в статье «Idempotency Keys».
CQRS и Event Sourcing - когда действительно нужно
Часто упоминаются вместе, но применяются отдельно. Оба мощные, оба дорогие в поддержке.
CQRS - разделение модели записи и чтения. Write-side - сложные агрегаты с инвариантами. Read-side - денормализованные проекции под конкретные запросы. Write публикует события, read слушает и обновляет проекции.
Триггер: read и write имеют радикально разные требования. Без CQRS либо read-модели размазываются по write, либо read-запросы - дорогие join-ы. Подробнее в «Domain-Driven Design».
Event Sourcing - состояние сервиса хранится как последовательность событий, не как снимок. Текущее состояние - результат проигрывания. Даёт аудит, temporal queries, replay для пересчёта проекций.
Триггер: аудит критичен (финансы, медицина, compliance), бизнес оперирует историей как сущностью, temporal queries - требование продукта.
API Gateway и External API
Пользователь не должен знать внутреннюю структуру. Для него - единый продукт. Между клиентом и внутренними сервисами стоит Gateway.
API Gateway - единая точка входа. Делает: роутинг, auth, rate limiting, transformation, кэширование, логирование, tracing, protocol translation (внешний HTTPS/JSON → внутренний gRPC/protobuf). Готовые: AWS API Gateway, Kong, Traefik.
Backend for Frontend (BFF). Один gateway неудобен, когда разные клиенты (web, iOS, Android, партнёрское API) имеют разные потребности. BFF - отдельный gateway на тип клиента. Каждая клиентская команда владеет своим BFF. Netflix - известный пример.
API composition vs CQRS для сложных запросов. Пользователю нужны данные из нескольких сервисов:
- API composition: gateway вызывает сервисы последовательно, собирает ответ. Просто, но latency растёт.
- CQRS: read-модель с денормализованными данными. Быстрое чтение, требует поддержки проекций.
Выбор по частоте запроса. Редкий admin-запрос - composition. Горячий user-facing dashboard - CQRS.
Service discovery и маршрутизация
В распределённой системе сервисы запускаются динамически, IP-адресов заранее нет. Нужен механизм узнавания текущего местоположения.
- Client-side discovery. Сервис-клиент сам запрашивает registry, выбирает инстанс (load balancing на клиенте). Eureka, Consul. Плюс: контроль выбора. Минус: логика размазана по клиентам.
- Server-side discovery. Между клиентом и сервисом - load balancer или K8s Service. Клиент не знает о discovery. В K8s работает из коробки.
- Service mesh. Sidecar-прокси рядом с каждым сервисом. Логика сети уходит из кода приложения. Подробнее в статье «Service Mesh».
Service registry - общий компонент. Хранит доступные инстансы сервисов. Инстансы либо сами регистрируются (self-registration), либо третьей стороной (orchestrator при запуске).
В современных стеках (Kubernetes) discovery решён платформой. Переизобретать - трата ресурсов, если нет экзотических требований.
Reliability patterns: обязательные условия эксплуатации
Не украшения. Без них распределённая система падает регулярно и предсказуемо.
Timeouts везде. Любой cross-service вызов имеет максимальное время ожидания. Без таймаута поток клиента блокируется на минуты. TCP default - 2-3 минуты, для user-facing неприемлемо. Явные 1-10 секунд - норма.
Circuit breaker. Если сервис B падает, сервис A не должен дёргать его миллионы раз. Circuit breaker отслеживает ошибки: если процент выше порога - «размыкается», запросы моментально возвращают ошибку без вызова. Через N секунд пробует снова (half-open). При успехе - замыкается обратно. Реализации: Hystrix (deprecated), resilience4j, Polly, встроено в service mesh.
Bulkheads. Отдельные пулы ресурсов для разных типов запросов. Один пул перегружается - страдает только он. Пулы для critical/non-critical, для разных клиентов, premium/free.
Retries с backoff и jitter. Повтор упавшего запроса:
- Exponential backoff: 1s, 2s, 4s, 8s.
- Jitter: случайный шум, чтобы клиенты не retry-или синхронно (retry storm).
- Максимальное число попыток.
- Только для идемпотентных операций или с idempotency key.
Rate limiting. Защита от абьюза. Алгоритмы: token bucket (токены тратятся по запросу, пополняются со скоростью), sliding window (счётчик в скользящем окне), fixed window (счётчик за период).
Load shedding. При перегрузке отказывать в части запросов заранее. 503 для неприоритетных, критичные продолжают обрабатываться.
Fail-fast. Быстрый отказ лучше медленного. 100ms-ошибка обрабатывается (retry, fallback). 30 секунд ожидания блокирует тред, таймаут по цепочке.
Service mesh как инфраструктурный слой reliability. Многое из описанного выше автоматизируется service mesh - sidecar-прокси рядом с каждым сервисом (Envoy, Linkerd) реализуют timeouts, retries с backoff, circuit breakers, bulkheads, rate limiting на уровне инфраструктуры. Это закрывает часть класса ошибок сервисов без участия кода приложения: политики применяются единообразно, меняются централизованно через control plane, не дублируются в каждом сервисе на каждом языке. Одновременно mesh даёт чистое наблюдение за сетевой связанностью: стандартизованные метрики (RPS, latency percentiles, error rates по каждому edge), трассировки через все границы, видимая топология взаимодействий. Это смещает часть reliability из application layer в infrastructure layer и освобождает разработку от повторения одних и тех же паттернов. Подробнее в статье «Service Mesh».
Observability: не опция, а условие работы
Распределённая система без observability неотлаживаема. Любой инцидент - расследование без улик.
Distributed tracing. Каждый запрос получает уникальный trace ID. Все операции в рамках запроса (сервисы, DB, kafka) логируются с этим ID и образуют span-ы. Восстановить путь запроса - задача минут. Стандарты: OpenTelemetry, W3C Trace Context. Инструменты: Jaeger, Zipkin, Tempo, Honeycomb.
Metrics. Два классических набора:
- RED: Rate (запросы/сек), Errors (доля), Duration (латенси). Для сервисов.
- USE: Utilization, Saturation, Errors. Для ресурсов - CPU, memory, disk, network.
Инструменты: Prometheus + Grafana, Datadog, New Relic.
Log aggregation. Централизованные логи со всех сервисов. ELK (Elasticsearch + Logstash + Kibana), Loki + Grafana, Splunk.
Health check API. /health, /readiness endpoints для orchestrator. Liveness vs readiness: liveness - «процесс жив», readiness - «готов принимать трафик».
Exception tracking. Ошибки со стектрейсом в Sentry / Rollbar. Частота, затронутые пользователи, регрессии между релизами.
Correlation ID через всю систему. Critical. Запрос приходит на gateway, получает ID, ID проходит через все async-границы (включая Kafka, Redis, сторонние API). Без этого связать события одного запроса - нерешаемая задача.
Deployment и operational complexity
Распределённая система требует зрелой инфраструктуры развертывания. Без неё operational overhead убивает выигрыш от распределения.
- Service per container. Каждый сервис - Docker, оркестрация через Kubernetes / ECS. Изоляция, предсказуемость, автоматизация scale-up/down. Стандарт индустрии.
- Externalized configuration. Конфигурация вынесена наружу - ConfigMaps / Secrets / Consul / env vars. Один образ работает в dev/staging/prod с разной конфигурацией.
- Microservice chassis / service template. Общая инфраструктурная начинка: логирование, tracing, health-checks, metrics, config loading. Настраивается один раз, используется всеми.
- DevOps maturity как предусловие. CI/CD pipelines, автоматизированные тесты, infrastructure as code, monitoring. Организационное допущение: команды owning их сервисы end-to-end, от кода до production.
Частые антипаттерны
- Distributed monolith. Сервисы, которые нельзя deploy independently. Сложность распределения + связанность монолита.
- Shared database между сервисами. Скрытая связанность, схема задевает всех, lock contention.
- Синхронные цепочки без circuit breaker. Падение одного валит всю цепочку.
- SAGA по умолчанию. Компенсации на каждый чих, экспоненциальный рост сложности.
- Event sourcing cargo cult. Без аудита и temporal queries - overhead без выгоды.
- Retry без идемпотентности. Дубли при повторах - два списания у пользователя.
- Отсутствие timeouts. Потоки висят на минуты при недоступности.
- Разделение по техническим слоям вместо bounded contexts. Любое бизнес-изменение задевает все сервисы.
- Отсутствие distributed tracing. Инцидент в системе из 20 сервисов - расследование без улик.
Источники и смежные статьи
Первоисточники
- microservices.io - Chris Richardson, pattern language, каталог паттернов.
- Chris Richardson. Microservices Patterns (2018).
- Michael Nygard. Release It! (2nd ed. 2018) - stability patterns, circuit breakers, bulkheads.
- Sam Newman. Building Microservices (2nd ed. 2021).
- Martin Kleppmann. Designing Data-Intensive Applications (2017).
- Andrew Tanenbaum, Maarten van Steen. Distributed Systems (3rd ed.).
Связанные темы
- Peter Deutsch. «Fallacies of Distributed Computing» (1994).
- CAP theorem (Eric Brewer, 2000).
- Event Storming (Alberto Brandolini).
Смежные статьи блога
- Domain-Driven Design - bounded contexts становятся границами сервисов.
- Team Topologies - команды и сервисы.
- Монолит vs Микросервисы - когда переходить.
- Service Mesh - sidecar, управление трафиком.
- Idempotency Keys - защита от дубликатов при retries.
- C4 Model - визуализация распределённой архитектуры.