B07 · Архитектурные стили#
Как система разрезана на части и как эти части общаются. Ошибки этого блока обходятся дороже всех остальных: неверную границу нельзя починить рефакторингом внутри сервиса — она уже прошла через контракты, данные и организацию команд.
Блок охватывает два уровня: межсервисный и внутренний — как устроен сервис изнутри, как ведутся длительные процессы и фоновые задачи.
Навигация: ARCH-01 · ARCH-02 · ARCH-03 · ARCH-04 · ARCH-05 · ARCH-06 · ARCH-07 · ARCH-08 · ARCH-09 · ARCH-10 · ARCH-11 · ARCH-12 · ARCH-13
ARCH-01 · Монолит, модульный монолит, микросервисы#
Шаг W4
TL;DR. Это выбор не про масштаб, а про то, нужна ли независимость выкатки и отказов ценой распределённой системы. Модульный монолит закрывает большинство случаев.
Проблема. Микросервисы обещают независимость и масштабируемость. На практике они дают сеть между вызовами, распределённые транзакции (DIST-08), сложную отладку и умножение инфраструктуры. Если границы проведены неверно, получается распределённый монолит: изменение требует согласованной выкатки пяти сервисов, но теперь ещё и по сети.
Три варианта.
| Монолит | Модульный монолит | Микросервисы | |
|---|---|---|---|
| Выкатка | Целиком | Целиком | Независимо |
| Границы | Размываются со временем | Явные, проверяются сборкой | Физические |
| Транзакции | Локальные | Локальные | Распределённые |
| Отказ модуля | Валит всё | Валит всё | Изолирован (если предусмотрено) |
| Масштабирование | Целиком | Целиком | По частям |
| Отладка | Стек вызовов | Стек вызовов | Трассировка (OPS-07) |
| Стоимость эксплуатации | Низкая | Низкая | Высокая |
Модульный монолит — недооценённый вариант: модули с явными интерфейсами, запрет прямых обращений в чужие таблицы, проверка зависимостей на сборке. Он даёт дисциплину границ без сети между модулями. И, что важнее, оставляет право на ошибку: неверную границу внутри процесса переносят за день, между сервисами — за квартал.
Когда микросервисы оправданы.
- частям нужны разный цикл релизов и разные команды-владельцы;
- у частей резко разный профиль нагрузки или требований к надёжности;
- у частей разные технологические требования;
- организационный масштаб таков, что общая кодовая база стала узким местом координации.
Почти все причины — организационные, а не технические. Пример Stack Overflow показывает, что монолит на нескольких серверах обслуживает огромный трафик: техническая необходимость возникает реже, чем принято думать.
Trade-offs и режимы отказа. Каждый сетевой вызов добавляет задержку и вероятность отказа: цепочка из пяти сервисов с доступностью 99.9 % даёт 99.5 %, если нет деградации (REL-04). Общая БД между сервисами — самый частый способ получить худшее из обоих миров. Микросервисы без observability (наблюдаемости), автоматизации выкатки и владельцев не работают: инфраструктурная зрелость — входное требование, а не следствие.
Что мерить.
- число сервисов, которые нужно выкатить для типового изменения — больше одного уже сигнал;
- долю межсервисных вызовов на запрос пользователя;
- время от коммита до прода;
- число команд на сервис.
Связано: ARCH-02 границы · ARCH-13 strangler fig · DIST-08 распределённые транзакции
Источники: Martin Fowler — MonolithFirst · Sam Newman — Building Microservices · microservices.io — Microservice architecture · ByteByteGo — What Are Microservices Really All About? (And When Not To Use It) · ByteByteGo — Uncovering Stack Overflow's Shocking Architecture
ARCH-02 · Границы сервисов и ограниченный контекст#
Шаг W4
TL;DR. Граница проводится по языку и по данным, а не по слоям и не по сущностям. Признак верной границы — изменение требований умещается внутри неё.
Проблема. Разрезание по техническим слоям («сервис базы», «сервис API») даёт связность на каждое изменение. Разрезание по сущностям («сервис заказов», «сервис пользователей») выглядит логично, но любой сценарий тогда проходит через все сервисы. А «заказ» означает в них разное: для склада это набор позиций, для доставки — адрес и габариты, для бухгалтерии — сумма и налоги.
Как проводить границу.
- По языку. Там, где одно и то же слово начинает значить разное, проходит граница ограниченного контекста (bounded context). В каждом контексте своя модель, и она может не совпадать с соседней; попытка построить единую модель для всей организации — известный тупик.
- По изменяемости. Части, меняющиеся вместе и по одной причине, живут вместе. Если два сервиса выкатываются только парой — граница между ними ложная.
- По данным. Каждый фрагмент данных имеет одного владельца, который единственный его изменяет. Остальные читают через контракт или получают копию событиями (ARCH-04). Общая БД означает, что границы нет.
- Транзакционная граница. Всё, что меняется атомарно, должно помещаться внутри одного сервиса. Вынесенное наружу становится saga со всеми последствиями (DIST-08). Это самый практичный критерий: если разрезание порождает распределённые транзакции в основном сценарии, оно неверное.
- Организационно. Структура системы стремится повторить структуру коммуникаций в организации — это наблюдение стоит использовать сознательно, а не бороться с ним.
Отношения между контекстами различаются, и их полезно называть явно:
- партнёрство равных;
- поставщик и потребитель;
- антикоррупционный слой (anti-corruption layer) — перевод чужой модели в свою, чтобы чужие понятия не протекали внутрь;
- общее ядро (shared kernel) — совместно поддерживаемая часть, самый дорогой вид связи.
Признаки границы
| Критерий | Граница верная | Граница ложная |
|---|---|---|
| Язык | Термин внутри означает одно | Одно слово значит разное в двух местах |
| Изменяемость | Части меняются по одной причине | Два сервиса выкатываются только парой |
| Данные | У каждой сущности один владелец | Общая таблица на несколько сервисов |
| Транзакции | Атомарное умещается внутри | Sagas появляются в основном сценарии |
Trade-offs и режимы отказа. Мелкие границы дают гибкость и много сетевых вызовов; крупные — простоту и риск получить общий модуль, который меняют все. Дублирование данных между контекстами — не ошибка, а плата за автономию: «клиент» в доставке и в биллинге может и должен быть разным. Ошибочная граница обнаруживается по симптомам: сквозные изменения, общие таблицы, цепочки синхронных вызовов.
Что мерить.
- долю изменений, затрагивающих один сервис;
- число распределённых транзакций в основных сценариях;
- частоту изменений контрактов;
- число владельцев на таблицу — должно быть равно одному.
Связано: ARCH-01 монолит vs микросервисы · ARCH-03 внутренняя архитектура · DATA-03 моделирование данных · DIST-08 saga
Источники: Eric Evans — Domain-Driven Design · Martin Fowler — BoundedContext · Vaughn Vernon — Implementing Domain-Driven Design · microservices.io — Decompose by subdomain
ARCH-03 · Внутренняя архитектура сервиса: слои, порты и адаптеры#
Шаг W4
TL;DR. Правило одно: зависимости направлены внутрь, к предметной области. Она не знает ни про БД, ни про HTTP — знание идёт только в обратную сторону.
Проблема. Большинство решений в повседневной работе принимается внутри сервиса, а не между сервисами, — и как раз этот уровень обычно остаётся без архитектуры. Типичный итог: бизнес-правила размазаны между контроллерами, обработчиками и триггерами БД, доменные типы наследуют аннотации ORM, а проверить логику можно, только подняв всё окружение. Смена библиотеки или хранилища превращается в переписывание.
Принцип. Слои располагаются от внешнего к внутреннему: адаптеры (adapters: HTTP, очередь, планировщик, БД) → прикладной слой (сценарии использования, транзакционные границы) → предметная область (сущности, правила, инварианты). Зависимости направлены только внутрь. Предметная область объявляет интерфейс — «порт» (port) вида «репозиторий заказов», — а конкретная реализация живёт в адаптере и подставляется снаружи. Отсюда следствие: логику можно проверить без БД, очереди и сети.
Что это даёт на практике.
- Тесты предметной области без окружения — быстрые и надёжные, поэтому их пишут.
- Замена внешнего (переезд с одной очереди на другую, добавление gRPC рядом с HTTP) затрагивает адаптер, а не правила.
- Явные транзакционные границы: сценарий использования — естественное место, где начинается и заканчивается транзакция (DATA-07).
- Место для инвариантов. Правило «нельзя отгрузить больше, чем на складе» живёт в одном месте, а не в трёх обработчиках.
- Агрегат (aggregate) как единица согласованности. Внутри агрегата инварианты соблюдаются немедленно и в одной транзакции; между агрегатами — отложенно, событиями. Это правило одновременно определяет и границы транзакций, и то, что вообще имеет смысл выносить в отдельный сервис (ARCH-02).
- Состояние как явная машина состояний (state machine). Когда объект проходит стадии (черновик → подтверждён → оплачен → отгружен), полезно описать переходы явной таблицей состояний и разрешённых переходов, а не набором условий по коду. Тогда невозможные переходы отсекаются в одном месте, а не проверяются везде.
Когда применять. Сервис с нетривиальными правилами и сроком жизни больше года. Когда нет: тонкая прослойка над БД, конвертер форматов, задача-однодневка — там слои дают только церемонию: три файла на одно поле.
Альтернативы
| Подход | Когда лучше | Цена |
|---|---|---|
| Слои с зависимостями внутрь (порты и адаптеры) | Богатая предметная логика | Больше файлов и отображений |
| Транзакционный сценарий (transaction script) | Простая логика, много CRUD | Логика расползается при росте |
| Активная запись (active record: модель = строка таблицы) | Быстрый старт, простые формы | Правила прилипают к схеме БД |
| Вертикальные срезы по функциям | Слабо связанные сценарии | Дублирование между срезами |
Trade-offs и режимы отказа. Дословное следование схеме порождает отображения между почти одинаковыми структурами и раздражает на простых участках. Глубину слоёв выбирают по сложности правил, а не по идеологии. Обратная крайность — «чистая архитектура» поверх пустой логики: ритуал без содержания. Признак, что структура работает: тест правила пишется без упоминания БД и HTTP.
Что мерить.
- долю логики, покрытой тестами без внешних зависимостей;
- время прогона этих тестов;
- число мест, где одно бизнес-правило продублировано;
- глубину цепочки вызовов на типовой сценарий.
Связано: ARCH-02 границы контекстов · ARCH-06 CQRS · ARCH-07 event sourcing · DATA-07 транзакции
Источники: Alistair Cockburn — Hexagonal Architecture · Robert C. Martin — The Clean Architecture · Martin Fowler — Domain Model и Transaction Script · Vaughn Vernon — Effective Aggregate Design
ARCH-04 · Событийная архитектура: хореография и оркестрация#
Шаг W4
TL;DR. Два способа собрать процесс из сервисов: каждый реагирует на события сам либо есть явный дирижёр. Первое даёт слабую связность, второе — наблюдаемый процесс.
Проблема. Оформление заказа состоит из шагов в разных сервисах. Синхронная цепочка вызовов делает доступность произведением доступностей и заставляет вызывающего знать порядок шагов. Асинхронные события решают это, но возникает новый вопрос: кто отвечает за то, что процесс дошёл до конца.
Хореография. Сервисы публикуют события и подписываются на чужие. Никто не управляет процессом целиком: «заказ создан» → склад резервирует и публикует «зарезервировано» → оплата списывает и публикует «оплачено». Плюсы — минимальная связность, легко добавить нового потребителя (MSG-01). Минусы — процесс нигде не описан: чтобы понять порядок, нужно прочитать все подписки. Ответ на вопрос «почему заказ висит третий день» требует расследования по всем сервисам.
Оркестрация. Явный координатор вызывает шаги и хранит состояние процесса. Плюсы — процесс виден в одном месте, состояние доступно, ошибки и компенсации (DIST-08) обрабатываются централизованно, изменить порядок можно в одном месте. Минусы — координатор знает про всех участников и становится узлом связности; он же — компонент с состоянием, требующий надёжности.
Как выбирать.
- простые цепочки в два-три шага и уведомительные сценарии — хореография;
- процессы с ветвлениями, таймаутами, компенсациями и требованием отвечать на вопрос «где сейчас» — оркестрация (ARCH-05);
- смешивать нормально: внутри домена хореография, сквозной бизнес-процесс — оркестрация.
Событие — это факт, а не команда. ЗаказОплачен, а не ОтправитьУведомление. Как только событие формулируется в повелительном наклонении, связность возвращается: издатель снова знает, кто и что должен сделать (MSG-01).
Trade-offs и режимы отказа. Хореография превращается в неявную сеть зависимостей, которую невозможно окинуть взглядом. Помогает схема потоков событий, поддерживаемая как артефакт. Циклы подписок (A реагирует на B, B на A) дают бесконечные лавины событий. Отложенная согласованность видна пользователю: интерфейс должен показывать промежуточные состояния, а не делать вид, что операция мгновенна.
Что мерить.
- время прохождения процесса от начала до конца;
- долю незавершённых процессов и их возраст;
- число подписчиков на событие;
- глубину цепочек событий.
Связано: ARCH-05 workflow-движки · MSG-01 модели взаимодействия · DIST-08 saga · MSG-07 outbox
Источники: Martin Fowler — What do you mean by "Event-Driven"? · microservices.io — Saga: choreography и orchestration · Bernd Ruecker — Practical Process Automation
ARCH-05 · Оркестрация длительных процессов#
Шаг W4
TL;DR. Процесс, живущий дни и переживающий перезапуски, — это состояние, которое кто-то должен хранить и двигать. Либо вы пишете это сами, либо берёте workflow engine (движок процессов).
Проблема. Sagas с компенсациями (DIST-08) выглядят просто на схеме и быстро становятся неуправляемыми в коде. Нужно:
- хранить, на каком шаге процесс;
- переживать перезапуск сервиса посередине;
- ждать внешнего события часами;
- обрабатывать таймауты каждого шага;
- повторять шаги идемпотентно;
- выполнять компенсации в обратном порядке, причём компенсация тоже может упасть.
Самодельная реализация — это таблица состояний, планировщик, механизм повторов и своя отладка. Она пишется за неделю и сопровождается годами.
Что делает workflow engine.
- хранит состояние каждого экземпляра процесса;
- гарантирует продолжение после сбоя;
- отвечает за таймеры и ожидание событий;
- ведёт историю выполнения;
- показывает, где сейчас находится каждый экземпляр;
- позволяет вмешаться вручную.
Процесс описывается одним из трёх способов:
- кодом — Temporal, Cadence: обычный код, состояние которого движок восстанавливает воспроизведением истории (replay);
- схемой — Camunda, Zeebe: BPMN (Business Process Model and Notation), который читают и аналитики;
- конфигурацией облачного сервиса — Step Functions.
Ключевые понятия. Шаги должны быть идемпотентными (NET-16): движок гарантирует at-least-once (как минимум один раз). Логика процесса должна быть детерминированной. В подходах с воспроизведением истории обращение к текущему времени или случайности прямо в коде процесса ломает восстановление; для этого есть специальные средства. Версионирование — отдельная тема: процессы живут неделями, и в момент выкатки новой версии старые экземпляры продолжают выполняться по старой.
Когда применять. Процесс длиннее одного запроса, с ветвлениями, ожиданием внешних событий, таймаутами и компенсациями: оформление и доставка заказа, подключение клиента, согласование, возвраты платежей. Когда нет: два шага без компенсаций делаются очередью с идемпотентным потребителем — движок здесь избыточен и добавляет зависимость.
Альтернативы
| Вариант | Когда лучше | Цена |
|---|---|---|
| Хореография событий | 2–3 шага, без общего состояния | Процесс не наблюдаем (ARCH-04) |
| Своя таблица состояний + планировщик | Один-два простых процесса | Своя реализация повторов, таймеров, отладки |
| Workflow engine | Много длительных процессов с ветвлениями | Новая инфраструктура и её эксплуатация |
| Облачный сервис оркестрации | Не хочется эксплуатировать | Лимиты и привязка к провайдеру |
Trade-offs и режимы отказа. Движок становится критической зависимостью: его недоступность останавливает все процессы. История выполнения растёт и требует политики хранения (DATA-19). Соблазн описать в движке всю бизнес-логику превращает его в новый монолит. Внутри процесса должны быть шаги и переходы, а правила остаются в сервисах.
Что мерить.
- число активных экземпляров и их возраст;
- долю завершившихся компенсацией;
- время прохождения по шагам;
- число ручных вмешательств;
- глубину очереди задач движка.
Связано: ARCH-04 event-driven · DIST-08 saga · ARCH-08 фоновые задачи · NET-16 идемпотентность
Источники: Temporal — Documentation · Camunda — BPMN и оркестрация микросервисов · Bernd Ruecker — Practical Process Automation · AWS Step Functions
ARCH-06 · CQRS#
Шаг W4
TL;DR. CQRS (Command Query Responsibility Segregation) — разделение путей чтения и записи на две модели. Оправдано, когда их требования расходятся настолько, что одна модель мешает обеим.
Проблема. Модель записи должна защищать инварианты: она нормализована, транзакционна, оперирует агрегатами (ARCH-03). Модели чтения нужно обратное: денормализованные данные под конкретный экран, никаких JOIN, максимальная скорость. Одна структура обслуживает обе задачи компромиссно. По мере роста нагрузки на чтение компромисс становится всё хуже: запросы обрастают JOIN, а изменения модели ломают отчёты.
Как работает. Команды меняют состояние через модель записи, запросы обслуживаются отдельной моделью чтения. Уровни разделения бывают разные, и это важно. Минимальный — разделение интерфейсов в одном сервисе на одной БД: дёшево, полезно, обратимо. Максимальный — отдельное хранилище проекций, обновляемое событиями (MSG-07); с ним приходит отложенная согласованность.
Что даёт.
- чтение масштабируется независимо от записи;
- проекция строится под экран и отвечает одним запросом;
- можно держать несколько проекций одних данных в разных хранилищах — например, поисковый индекс (DATA-13) и аналитическую витрину (DATA-15);
- модель записи очищается от требований отчётности.
Главное следствие — отложенная согласованность. Пользователь отправил команду и сразу читает — проекция могла не успеть. Это проектная проблема интерфейса, а не деталь. Варианты решения:
- показывать состояние «обрабатывается»;
- возвращать результат команды напрямую;
- читать свои записи из модели записи — read-your-writes (DIST-03);
- ждать нужную версию проекции.
Если для сценария нужна мгновенность, полное CQRS с отдельным хранилищем ему не подходит.
Когда применять. Профиль чтения и записи расходится радикально (например, чтений в сотни раз больше); нужны несколько разных представлений одних данных; модель записи усложняется требованиями отчётности. Когда нет: обычный CRUD — здесь CQRS удваивает код и вносит рассогласование без выигрыша. Разделение интерфейсов внутри одного сервиса при этом полезно в большинстве случаев и почти ничего не стоит.
Trade-offs и режимы отказа. Две модели — два места, где живёт правда, и механизм их согласования, который может сломаться (DATA-16). Проекции требуют возможности перестроения с нуля: это обязательное свойство, иначе баг в обработчике означает навсегда испорченные данные. Отставание проекции — нормальный режим работы, его нужно наблюдать и ограничивать.
Что мерить.
- отставание проекций — медиану и хвост;
- время полного перестроения;
- долю чтений, обслуженных проекцией;
- число расхождений при сверке.
Связано: ARCH-07 event sourcing · ARCH-03 внутренняя архитектура · MSG-07 outbox и CDC · DIST-03 модели согласованности
Источники: Martin Fowler — CQRS · Greg Young — CQRS Documents · Microsoft — CQRS pattern
ARCH-07 · Event sourcing#
Шаг W4
TL;DR. Источником истины становится последовательность событий, а текущее состояние — их свёрткой. Даёт полную историю и восстановление на любой момент, ценой отказа от привычной работы с данными.
Проблема. Обычная запись хранит только текущее состояние: обновление затирает предыдущее. Вопросы «как мы к этому пришли», «что было на прошлой неделе», «кто и почему изменил» остаются без ответа, если заранее не завели журнал. Для денег, страхования, медицины, складского учёта и любых регулируемых процессов история и есть предмет учёта, а не служебная информация.
Как работает. Каждое изменение записывается как неизменяемое событие в конец журнала агрегата: ЗаказСоздан, ПозицияДобавлена, ЗаказОплачен. Текущее состояние получается воспроизведением событий. Чтобы не воспроизводить тысячи, периодически сохраняются снимки. Модели чтения строятся как проекции (ARCH-06): эти два подхода обычно идут вместе, хотя формально независимы.
Что даёт.
- полный аудит по построению (SEC-12);
- ответ на вопрос «каким было состояние в момент X»;
- возможность построить новую проекцию задним числом по всей истории — включая ту, о которой при разработке не думали;
- события как естественный контракт для интеграции (ARCH-04);
- отладку воспроизведением реальной последовательности.
Что усложняется — честный список.
- Схема событий вечна. Событие, записанное год назад, должно читаться сегодняшним кодом. Нужны версионирование и апгрейд событий при чтении (NET-18).
- Запросы. Нельзя просто написать выборку по полю — только через проекции. Любой новый вопрос требует новой проекции.
- Удаление данных противоречит неизменяемости: право на удаление (DATA-19) решается шифрованием персональных данных ключом на субъекта и удалением ключа — crypto-shredding.
- Исправление ошибок — только компенсирующим событием; отредактировать прошлое нельзя, и это одновременно достоинство и ограничение.
- Порог входа. Команда должна понимать модель; наполовину внедрённый event sourcing — худший из вариантов.
Когда применять. История имеет самостоятельную ценность (учёт, финансы, регуляторика); нужно восстанавливать состояние на момент; домен естественно описывается событиями. Когда нет: большинство CRUD-систем — здесь достаточно журнала изменений (SEC-12), который даёт девяносто процентов пользы за пять процентов сложности. Применять точечно к одному-двум агрегатам, а не ко всей системе.
Trade-offs и режимы отказа. Журнал растёт неограниченно — нужны снимки и политика архивирования. Ошибка в проекции лечится перестроением, ошибка в самих событиях — не лечится, только компенсируется. Соблазн сделать событиями всё приводит к тому, что тривиальные справочники обрастают инфраструктурой.
Что мерить.
- длину журналов агрегатов и частоту снимков;
- время воспроизведения агрегата;
- время перестроения проекций;
- число версий схемы событий в обращении.
Связано: ARCH-06 CQRS · ARCH-03 внутренняя архитектура · SEC-12 аудит · DATA-19 retention
Источники: Martin Fowler — Event Sourcing · Greg Young — Versioning in an Event Sourced System · Microsoft — Event Sourcing pattern
ARCH-08 · Фоновые задачи и планировщики#
Шаг W4
TL;DR. Работа по расписанию и отложенные задачи — отдельный класс систем со своими отказами: пропущенные запуски, двойные запуски и задачи, которые тихо перестали выполняться.
Проблема. Ночной пересчёт, рассылка напоминаний, чистка устаревших данных, повторная попытка платежа через час, ежемесячный отчёт. Всё это выглядит как мелочь и обычно делается запуском по расписанию на одной машине. Так продолжается до первого случая, когда машина была недоступна и запуск не состоялся, а узнали об этом через неделю от клиентов.
Что нужно решить.
- Ровно один исполнитель. Несколько экземпляров сервиса означают несколько запусков одной задачи. Решается выбором лидера (leader election) (DIST-05), lease с ограниченным сроком (DIST-07) или, надёжнее, идемпотентностью самой задачи: тогда двойной запуск безвреден.
- Пропущенный запуск — это событие. Нужно решить заранее: догонять пропущенное, выполнить один раз или пропустить. И, главное, узнать о пропуске: отсутствие запуска не вызывает ошибок и потому невидимо. Практика — dead man's switch («сторожевой» мониторинг): задача сообщает о своём завершении, отсутствие сообщения в срок даёт алерт.
- Отложенные задачи (выполнить через час, повторить платёж завтра) — не расписание, а очередь с отложенной видимостью (MSG-02) либо таблица с временем запуска и опросом. Второе проще и хорошо работает при умеренных объёмах.
- Долгие задачи разбиваются на порции с сохранением прогресса: задача на шесть часов, падающая на пятом, не должна начинать сначала. Помогает обработка партиями с курсором (NET-14) и возможностью продолжить.
- Влияние на прод. Массовые операции конкурируют за БД с пользователями. Помогают throttling (ограничение скорости обработки), работа в непиковое время, чтение с реплики.
- Observability (наблюдаемость). У каждой задачи должны быть видны время выполнения, результат, число обработанных элементов и история запусков. Иначе «отчёт не пришёл» расследуется вслепую.
Когда применять что.
- расписание — для регулярных операций;
- очередь с задержкой — для отложенных единичных действий;
- workflow engine (ARCH-05) — когда задача — шаг длительного процесса с ветвлениями.
Trade-offs и режимы отказа. Планировщик на одной машине — единая точка отказа с самым тихим из возможных отказов. Планировщик, встроенный в сервис, привязывает расписание к жизненному циклу выкатки: во время выкатки запуск может потеряться. Задачи, накапливающие отставание (не успевают за период), образуют очередь, которая не рассасывается. Нужен контроль того, что время выполнения меньше интервала.
Что мерить.
- факт и время каждого запуска против ожидаемого;
- длительность относительно интервала;
- число обработанных элементов;
- долю неуспешных запусков;
- возраст самой старой невыполненной отложенной задачи.
Связано: ARCH-05 workflow-движки · DIST-07 распределённые блокировки · MSG-02 очередь vs лог · OPS-10 алертинг
Источники: Kubernetes — CronJob · Google SRE Book, гл. 24 «Distributed Periodic Scheduling with Cron» · Sidekiq — Best practices
ARCH-09 · Serverless: когда выгоден и когда нет#
Шаг W10
TL;DR. Оплата за фактические вызовы и отсутствие эксплуатации серверов. Выигрывает на неравномерной нагрузке и проигрывает на постоянной — иногда в разы.
Проблема. Держать работающие серверы под нагрузку, которая случается два часа в день, — переплата. Но у модели с оплатой за вызов собственная экономика и собственные ограничения: при постоянной нагрузке она дороже обычных машин. Разбор Prime Video — как раз про этот случай. Конвейер из функций и оркестрации заменили единым процессом и сэкономили порядок величины: нагрузка оказалась постоянной, а между шагами гонялся большой объём данных.
Что даёт.
- отсутствие управления серверами;
- масштабирование от нуля до тысяч экземпляров без настройки;
- оплату за фактическое время выполнения;
- естественную модель «событие → обработчик».
Ограничения, которые определяют применимость.
- Холодный старт (cold start). Первый вызов после простоя платит за инициализацию — от десятков миллисекунд до секунд. В интерактивных сценариях с жёстким требованием к задержке это решают заранее прогретыми экземплярами, но так выигрыш уменьшается.
- Ограничение времени выполнения делает длительные задачи невозможными без разбиения.
- Нет состояния между вызовами. Соединения с БД не переиспользуются сами собой, и тысяча параллельных функций открывает тысячу соединений. Отсюда потребность в пуле-посреднике (PERF-06).
- Передача данных между шагами идёт через хранилище или очередь: то, что внутри процесса было вызовом функции, снаружи становится сетевой операцией с оплатой.
- Отладка и локальный запуск сложнее; observability (наблюдаемость) требует отдельной настройки.
Когда выгодно.
- редкая и неравномерная нагрузка;
- обработка событий: загрузили файл — обработали;
- задачи-склейки между сервисами;
- прототипы;
- пики, которые дорого держать в резерве.
Когда невыгодно.
- постоянная нагрузка — обычные машины дешевле;
- низкая задержка без запаса;
- длительные вычисления;
- интенсивный обмен данными между шагами.
Варианты исполнения
| Вариант | Когда выгоден | Цена |
|---|---|---|
| Виртуальные машины и контейнеры | Постоянная нагрузка | Простой оплачивается; эксплуатация на вас |
| Управляемые контейнерные сервисы | Средняя нагрузка, не нужен кластер | Меньше контроля, лимиты платформы |
| Функции | Редкая и неравномерная нагрузка, обработка событий | Холодный старт, лимит времени, привязка к провайдеру |
| Функции с прогретыми экземплярами | Нужна низкая задержка при неравномерности | Выигрыш от оплаты по факту заметно уменьшается |
Trade-offs и режимы отказа. Экономика меняется скачком: при переходе от эпизодической нагрузки к постоянной стоимость может вырасти в разы. Считать нужно на своих числах (PERF-11). Привязка к провайдеру сильнее обычного: модель выполнения и триггеры непереносимы. Неограниченное автомасштабирование функций может положить нижележащую БД, поэтому лимиты параллелизма нужны.
Что мерить.
- стоимость на миллион вызовов против стоимости эквивалентных машин;
- долю холодных стартов и их вклад в задержку;
- параллелизм и его пики;
- число соединений с БД.
Связано: ARCH-12 cloud native · PERF-11 стоимость · ARCH-08 фоновые задачи · PERF-06 пулы соединений
Источники: AWS Lambda — Best practices · Prime Video Tech Blog — Scaling up the audio/video monitoring service · Martin Fowler — Serverless Architectures · ByteByteGo — Amazon Prime Video Ditches AWS Serverless, Saves 90%
ARCH-10 · Service mesh, sidecar, BFF#
Шаг W4
TL;DR. Три способа вынести повторяющуюся работу из сервисов: в соседний процесс, в инфраструктурный слой и в отдельный сервис под конкретного клиента.
Проблема. Повторы, таймауты, circuit breaker (предохранитель), mTLS (взаимный TLS), трассировка, service discovery — всё это нужно каждому сервису. Реализация в библиотеке означает: обновление правил требует пересборки всех сервисов, а в полиглотной среде — поддержки библиотеки на каждом языке.
Sidecar. Рядом с сервисом запускается вспомогательный процесс, через который идёт весь сетевой трафик. Сервис общается с ним локально и ничего не знает об инфраструктурных заботах.
Service mesh (сервисная сетка) — sidecar-прокси у каждого сервиса плюс control plane (управляющий слой), который задаёт правила централизованно. Даёт:
- mTLS между всеми сервисами (SEC-09) без изменения кода;
- единые таймауты и повторы;
- тонкую маршрутизацию для canary-выкаток (EDGE-04);
- автоматическую трассировку и метрики каждого вызова.
Цена — дополнительный сетевой участок на каждом вызове, заметный расход ресурсов и сложная отладка: непонятно, чей это отказ — сервиса или прокси. Mesh оправдан от десятков сервисов, особенно на нескольких языках; для пяти сервисов на одном языке библиотека проще.
BFF (Backend for Frontend) решает другую задачу — не инфраструктурную, а продуктовую. Мобильному приложению, вебу и партнёрскому API нужны разные срезы данных и разная гранулярность вызовов. BFF — отдельный сервис под каждый тип клиента, который агрегирует вызовы к доменным сервисам и формирует ответ под конкретный интерфейс. Владеет им команда клиента. Это снимает с доменных сервисов требование угождать всем сразу и убирает лишние обращения по мобильной сети. Альтернатива с похожим эффектом — GraphQL (NET-11).
Куда выносить сквозную функциональность
| Способ | Что выносит | Цена |
|---|---|---|
| Библиотека в сервисе | Повторы, таймауты, метрики | Обновление требует пересборки всех; по библиотеке на язык |
| Sidecar | То же, но вне процесса приложения | Ещё один процесс на каждый экземпляр |
| Service mesh | Плюс централизованные правила, mTLS, маршрутизация | Control plane, задержка на прокси, сложная отладка |
| BFF | Агрегацию под конкретного клиента — задача продуктовая, а не инфраструктурная | Отдельный сервис на каждый тип клиента |
Trade-offs и режимы отказа. Mesh добавляет два прокси на каждый вызов — это миллисекунды и память, умноженные на число экземпляров. Ошибка в control plane распространяется на все сервисы сразу, поэтому его конфигурация требует такой же дисциплины выкатки, как код. BFF легко превращается в свалку бизнес-логики: в нём должна быть агрегация и формирование ответа, но не правила предметной области. Число BFF растёт с числом типов клиентов, и каждый нужно сопровождать.
Что мерить.
- задержку, добавленную прокси, — отдельно от сервиса;
- расход ресурсов на прокси относительно сервисов;
- долю трафика под mTLS;
- число обращений клиента на экран — главную метрику полезности BFF.
Связано: SEC-09 mTLS · EDGE-08 service discovery · NET-11 GraphQL · EDGE-03 API gateway
Источники: Istio — What is a service mesh? · microservices.io — Sidecar · Sam Newman — Backends For Frontends
ARCH-11 · Клиентские архитектурные паттерны#
Шаг W4
TL;DR. Клиент — тоже система с состоянием, кэшем и отказами. Решения здесь напрямую определяют, каким должен быть API.
Проблема. Проектирование обычно заканчивается на серверной границе. А половина ощущаемого качества определяется тем, что происходит после: сколько запросов делает экран, что показывается при потере сети, откуда берётся состояние, которое видит пользователь.
Что важно для дизайна системы.
- Разделение состояния. Состояние сервера (данные, полученные по сети) и состояние интерфейса — разные вещи с разными жизненными циклами. Их смешивание порождает рассинхронизацию и запросы там, где хватило бы кэша.
- Кэш на клиенте — это кэш со всеми вопросами блока B04: TTL (время жизни записи), инвалидация после изменения, stale-while-revalidate — отдача устаревшего с фоновым обновлением. Клиентские библиотеки данных решают как раз это.
- Оптимистичное обновление (optimistic update) — интерфейс показывает результат до подтверждения сервера. Требует отката при ошибке и хорошо сочетается с идемпотентностью (NET-16).
- Офлайн и ненадёжная сеть. Нужны очередь операций на клиенте, повторы, разрешение конфликтов при возврате связи (DIST-10). Если это требование есть, оно меняет и API, и модель данных, поэтому появляется на шаге W0, а не в конце.
- Гранулярность API — следствие клиента. Экран, требующий семи запросов, — это либо задача для BFF (ARCH-10), либо для агрегирующего эндпоинта.
- Где рендерится страница (NET-21) определяет, что можно кэшировать на границе.
- Совместимость версий. Мобильный клиент живёт на устройствах годами (NET-17), поэтому серверная логика предпочтительнее клиентской для всего, что может измениться.
Trade-offs и режимы отказа. Оптимистичные обновления улучшают ощущение скорости и создают расхождение при отказе. Нужен внятный откат, а не тихое исчезновение изменения. Агрессивное кэширование на клиенте приводит к «данные не обновились» без возможности инвалидировать удалённо. Логика, перенесённая на клиента ради разгрузки сервера, застревает в старых версиях приложения.
Что мерить.
- число запросов на ключевой экран;
- долю данных из кэша;
- время до отображения содержимого;
- долю ошибок из-за устаревших клиентских версий.
Связано: ARCH-10 BFF · NET-12 realtime · CACHE-01 уровни кэша · NET-21 устройство веб-приложения
Источники: MDN — Progressive web apps · web.dev — Offline cookbook · ByteByteGo — Everything You NEED to Know About Client Architecture Patterns
ARCH-12 · Cloud native#
Шаг W9
TL;DR. Не «работает в облаке», а набор свойств: приложение без состояния, конфигурация снаружи, готовность к перезапуску в любой момент.
Проблема. Термин размыт и часто означает просто «мы в Kubernetes». Полезное содержание в нём есть, но оно про свойства приложения, а не про место запуска. Приложение, которое требует ручной настройки после старта и хранит данные на локальном диске, не становится облачным от переезда в контейнер.
Свойства, которые за этим стоят.
- Отсутствие состояния в процессе. Всё состояние — во внешних хранилищах, поэтому экземпляр можно убить и создать заново в любой момент (DIST-01).
- Одноразовость (disposability). Быстрый старт, корректное завершение по сигналу с дозавершением текущих запросов (graceful shutdown). Это условие любой безостановочной выкатки (OPS-02).
- Конфигурация снаружи, а не в сборке: один и тот же образ проходит все окружения (OPS-06).
- Логи в поток вывода, а не в файлы; метрики и трассировка встроены (OPS-07).
- Явные зависимости и порт для приёма трафика.
- Устойчивость по умолчанию: таймауты, повторы, деградация (REL-01) — потому что в облаке экземпляры исчезают штатно, а не в аварийном режиме.
- Проверки готовности (readiness) и жизнеспособности (liveness) (REL-05) как часть контракта с платформой.
Эти пункты во многом совпадают с методологией Twelve-Factor App (приложение двенадцати факторов) — она старше термина и точнее его.
Trade-offs и режимы отказа. Соответствие всем пунктам стоит усилий и окупается не везде. Приложению, которое живёт на двух машинах и меняется раз в квартал, полный набор не нужен. Обратная ошибка — считать, что контейнер сам по себе даёт эти свойства: приложение с состоянием в памяти в контейнере просто теряет состояние чаще. Управляемые сервисы облака ускоряют разработку и увеличивают привязку к провайдеру. Это осознанный размен, а не бесплатное благо.
Что мерить.
- время старта до готовности;
- корректность завершения — долю запросов, оборванных при остановке;
- число отличий конфигурации между окружениями;
- долю состояния, хранимого вне процесса.
Связано: ARCH-09 serverless · OPS-05 Kubernetes · OPS-06 конфигурация и IaC · REL-05 health checks
Источники: The Twelve-Factor App · CNCF — Cloud Native Definition · ByteByteGo — But What Is Cloud Native Really All About?
ARCH-13 · Миграция монолита: strangler fig#
Шаг W4
TL;DR. Новая система растёт вокруг старой, постепенно перехватывая её функции. Переписывание с нуля с последующим переключением обычно заканчивается плохо.
Проблема. Дизайн с чистого листа встречается реже, чем работа с существующей системой. Соблазн переписать целиком понятен, но обычно ошибочен. Пока идёт переписывание, старая система продолжает меняться. Срок оценён по видимой части, а знания о крайних случаях остались только в коде. Момент переключения превращается в событие, к которому невозможно подготовиться.
Как работает.
- Фасад на входе. Перед старой системой ставится маршрутизирующий слой (EDGE-04), через который идёт весь трафик. Пока он просто проксирует.
- Первый кусок выбирается по правилу «наибольшая ценность при наименьшей связности»: обычно это функция с чёткой границей и малым числом обращений к общим данным.
- Перенос с параллельным запуском (parallel run). Новая реализация работает рядом со старой на реальном трафике, результаты сравниваются, ответ пользователю пока отдаёт старая. Это единственный способ убедиться в эквивалентности на настоящих данных.
- Переключение долей через фасад с возможностью мгновенного отката (REL-04).
- Данные — самая сложная часть. Пока работают обе системы, данными владеет одна из них: либо старая с репликацией изменений в новую (MSG-07), либо новая с обратной синхронизацией. Двусторонняя синхронизация — источник расхождений, её избегают.
- Удаление старого кода — обязательный шаг, который вечно откладывают. Незавершённая миграция оставляет две системы навсегда, и это худшее из состояний.
Когда применять. Система работает в проде, и её нельзя остановить. Когда нет: если старая система мала и её поведение полностью описано тестами, прямая замена может быть дешевле. Ещё случай — предметная область меняется настолько, что эквивалентность не требуется.
Trade-offs и режимы отказа. Период сосуществования — это двойная сложность: две модели, синхронизация, фасад. Он должен быть ограничен по времени и доведён до конца, иначе переходное состояние становится постоянным. Фасад накапливает исключения и превращается в собственный монолит. Параллельный запуск удваивает нагрузку и опасен для операций с побочными эффектами. Поэтому сравнивают на чтении, а для записи используют отдельные приёмы.
Что мерить.
- долю трафика на новой реализации;
- число расхождений при параллельном запуске;
- объём оставшегося старого кода;
- срок жизни переходного состояния — главную метрику, она не должна расти.
Связано: ARCH-01 монолит и микросервисы · ARCH-02 границы · DATA-17 миграции схемы · EDGE-04 маршрутизация на границе
Источники: Martin Fowler — StranglerFigApplication · Sam Newman — Monolith to Microservices · Martin Fowler — ParallelRun