B01 · Сеть, протоколы, API#
Фундамент, на котором стоит всё остальное: как запрос доходит до сервера, по каким правилам договариваются стороны и что фиксирует контракт между ними. Блок не ограничивается транспортом. В нём и то, что по транспорту передаётся и как оно живёт: кэширование, форматы и сжатие, длительные и массовые операции, идемпотентность, версионирование, браузерная модель безопасности.
Навигация: NET-01 · NET-02 · NET-03 · NET-04 · NET-05 · NET-06 · NET-07 · NET-08 · NET-09 · NET-10 · NET-11 · NET-12 · NET-13 · NET-14 · NET-15 · NET-16 · NET-17 · NET-18 · NET-19 · NET-20 · NET-21
NET-01 · Как работает интернет: DNS, TCP, UDP#
Шаг W1
TL;DR. Путь запроса: разрешение имени в адрес, установка соединения, обмен данными. Каждый этап стоит времени, и это время закладывается в бюджет задержки.
Проблема. За фразой «клиент вызывает сервер» стоят четыре сетевых обмена до первого байта данных. На межконтинентальной дистанции каждый стоит десятки миллисекунд. Без понимания этой цены не объяснить, почему у пользователя из другого полушария страница грузится секунду, хотя сервер отвечает за 20 мс.
Как работает.
- DNS. Резолвер по очереди опрашивает корневой сервер, сервер зоны и авторитетный сервер домена и кэширует ответ на время TTL (time to live, время жизни записи). Записи:
A/AAAA— адрес,CNAME— псевдоним,MX— почта,TXT— произвольные данные (верификация, SPF). Практические следствия: TTL определяет скорость переключения при failover (переключении на резерв, REL-08). Низкий TTL даёт манёвренность ценой нагрузки на DNS. Но клиенты и промежуточные резолверы кэшируют дольше, чем указано, поэтому для мгновенного переключения DNS ненадёжен. - TCP. Соединение устанавливается тройным рукопожатием — это один round-trip (обмен туда и обратно). TCP гарантирует доставку и порядок за счёт подтверждений и повторов. Управление перегрузкой начинается с медленного старта (slow start), поэтому новое соединение передаёт данные не на полной скорости. Отсюда ценность переиспользования соединений: keep-alive, пулы.
- UDP. Работает без соединения, без гарантий доставки и порядка. На UDP построены DNS, видеосвязь и QUIC — в них надёжность реализована уровнем выше.
- Цена этапов. Для соединения через океан: DNS при промахе кэша ≈ RTT (round-trip time, время обмена туда и обратно), TCP ≈ 1 RTT, TLS 1.3 ≈ 1 RTT, затем сам запрос. При RTT 150 мс это ~450 мс до первого байта — ещё до того, как сервер начал работать. Отсюда ценность CDN и постоянных соединений.
Trade-offs и режимы отказа. Низкий TTL DNS-записи даёт манёвренность при переключении, но нагружает резолверы, а соблюдают его не все клиенты. Переиспользование соединений экономит рукопожатия, но удерживает ресурсы на сервере и замедляет ребалансировку, когда появляются новые экземпляры. UDP снимает накладные расходы соединения и перекладывает надёжность на приложение.
Что мерить.
- время разрешения DNS;
- время установки соединения и TLS-рукопожатия — отдельно от времени ответа приложения;
- долю новых соединений против переиспользованных;
- RTT до ключевых регионов.
Связано: NET-03 HTTP · NET-04 TLS · EDGE-05 CDN · PERF-01 числа задержек
Источники: Cloudflare — What is DNS? · High Performance Browser Networking (Ilya Grigorik) · ByteByteGo — How the Internet Works in 9 Minutes · ByteByteGo — Everything You Need to Know About DNS: Crash Course System Design #4
NET-02 · Модель OSI и стек TCP/IP#
Шаг W4
TL;DR. Семь уровней OSI — учебная схема. На практике важны четыре, а главное следствие модели для дизайна — различие между L4 и L7.
Как работает. OSI: физический, канальный, сетевой (IP), транспортный (TCP/UDP), сеансовый, представления, прикладной (HTTP). Реальный стек TCP/IP схлопывает верхние три в один прикладной уровень.
Где это применяется в дизайне.
- Балансировка L4 против L7 (EDGE-01). На L4 балансировщик решает по адресу и порту — быстро, дёшево, не разбирая содержимое. На L7 — по пути, заголовкам и cookie; это позволяет маршрутизировать по версии API, делать canary (канареечную выкатку) и завершать TLS.
- Где завершается шифрование: на балансировщике (тогда L7 видит содержимое) или только на самом сервисе (сквозное шифрование).
- Диагностика: «сеть недоступна» на L3, «соединение отвергнуто» на L4, «500» на L7 — это разные причины и разные ответственные.
Trade-offs. Модель — упрощение. QUIC реализует транспортные функции поверх UDP в пространстве пользователя, а TLS не ложится ровно ни на один уровень. Спорить о номере уровня бесполезно; полезно различие «работаем с соединением» против «работаем с запросом».
Что мерить.
- распределение ошибок по уровням: недоступность сети, отказ в соединении, ошибка приложения — у них разные причины и разные ответственные;
- долю трафика по версиям протокола после балансировщика.
Связано: EDGE-01 балансировка · NET-04 TLS · NET-01 сеть
Источники: Cloudflare — What is the OSI model? · ByteByteGo — What is OSI Model | Real World Examples
NET-03 · HTTP: 1.1 → 2 → 3#
Шаг W2
TL;DR. Каждая версия устраняет head-of-line blocking (блокировку в голове очереди) на своём уровне: HTTP/2 — на уровне запросов, HTTP/3 — на уровне транспорта.
Проблема. В HTTP/1.1 по одному соединению запросы обрабатываются последовательно: медленный ответ задерживает следующие за ним. Браузеры обходили это, открывая по 6 соединений на домен, — отсюда исторические приёмы вроде склейки файлов и раздачи с нескольких поддоменов.
Как работает.
- HTTP/1.1 — текстовый протокол, keep-alive, chunked-передача. Проблема: head-of-line blocking на уровне запросов; заголовки повторяются в каждом запросе целиком.
- HTTP/2 — бинарные кадры, мультиплексирование множества потоков в одном TCP-соединении, сжатие заголовков (HPACK), приоритеты. Head-of-line blocking на уровне HTTP исчезает, но остаётся на уровне TCP: потеря одного пакета останавливает доставку всех потоков соединения. Server push (отправка ресурсов по инициативе сервера) оказался неудачным и почти вышел из употребления.
- HTTP/3 — тот же принцип поверх QUIC (UDP): каждый поток восстанавливается независимо, потеря пакета не блокирует остальные. Соединение устанавливается за 1 RTT (0 RTT при возобновлении), TLS 1.3 встроен, соединение переживает смену IP (переход с Wi-Fi на мобильную сеть) благодаря идентификатору соединения.
Практические следствия. Приёмы оптимизации для HTTP/1.1 (склейка ресурсов, спрайты, раздача с разных доменов) при HTTP/2+ вредны: они мешают кэшированию и приоритизации. Выигрыш от HTTP/3 сильнее всего заметен на нестабильных мобильных сетях. Внутренние вызовы между сервисами обычно идут по HTTP/2 (gRPC), и там важно переиспользование соединений.
Trade-offs и режимы отказа. HTTP/2 через прокси и старые балансировщики может «схлопываться» до 1.1. UDP-трафик QUIC блокируется в части корпоративных сетей — нужен запасной путь. Одно мультиплексированное соединение концентрирует риск: его разрыв затрагивает все потоки сразу.
Что мерить.
- долю трафика по версиям протокола;
- время до первого байта;
- число соединений на клиента;
- долю повторно использованных соединений.
Связано: NET-04 TLS · EDGE-05 CDN · NET-12 realtime · NET-10 gRPC
Источники: RFC 9113 — HTTP/2 · RFC 9114 — HTTP/3 · High Performance Browser Networking · ByteByteGo — HTTP/1 to HTTP/2 to HTTP/3 · ByteByteGo — HTTP 1 Vs HTTP 2 Vs HTTP 3!
NET-04 · HTTPS и TLS#
Шаг W8
TL;DR. TLS даёт три вещи: шифрование, целостность и подтверждение того, что сервер — тот, за кого себя выдаёт. Третье — самое важное и чаще всего ломается.
Как работает.
- Рукопожатие. Стороны договариваются о версии и наборе шифров, сервер предъявляет сертификат, затем стороны согласуют общий сеансовый ключ. Обмен идёт по Диффи–Хеллману с эфемерными ключами — это даёт forward secrecy (прямую секретность): компрометация ключа сервера не раскрывает прошлые сессии. TLS 1.3 укладывается в один round-trip (обмен туда и обратно) и убирает устаревшие алгоритмы.
- Сертификаты. Цепочка доверия от корневого центра сертификации. SNI (Server Name Indication, указание имени сервера) позволяет одному IP обслуживать много доменов: клиент передаёт имя в открытом виде на этапе рукопожатия. От подмены шифрование защищает, только если клиент проверяет цепочку.
- Где завершается TLS. На CDN, на балансировщике или в самом сервисе. Завершение на границе упрощает маршрутизацию L7, но внутренний трафик остаётся открытым. Отсюда практика повторного шифрования внутри периметра и mTLS (взаимный TLS, SEC-09): сертификаты предъявляют обе стороны.
- HSTS (HTTP Strict Transport Security) запрещает браузеру ходить на этот домен по HTTP и так закрывает класс атак с понижением протокола.
Trade-offs и режимы отказа. Истёкший сертификат — одна из самых частых и самых обидных причин полного отказа сервиса. Защита — автоматическое продление и алерт за две недели до истечения. Рукопожатие стоит round-trip и процессорного времени, поэтому важны переиспользование соединений и возобновление сессий. Отключённая проверка сертификата в клиенте («чтобы заработало в тесте») превращает TLS в шифрование с кем угодно — от подмены оно уже не защищает.
Что мерить.
- срок до истечения сертификатов;
- долю рукопожатий с возобновлением сессии;
- время рукопожатия;
- долю соединений по устаревшим версиям TLS.
Связано: SEC-09 mTLS и SSH · NET-03 HTTP · EDGE-03 API gateway
Источники: RFC 8446 — TLS 1.3 · Mozilla — Server Side TLS recommendations · ByteByteGo — HTTP vs HTTPS Explained
NET-05 · Коды состояния и семантика ошибок#
Шаг W2
TL;DR. Код ответа — контракт для клиента. Прежде всего он говорит, повторять запрос или нет.
Проблема. API, который отвечает 200 OK с полем "error": true в теле, ломает всё, что стоит между клиентом и сервером. Балансировщик считает такой ответ успешным, кэш его кэширует, мониторинг рисует зелёный график, клиентская библиотека не повторяет запрос. Осмысленные коды — не педантизм, а условие работы инфраструктуры.
Что важно на практике.
- 2xx — успех.
201 Createdс заголовкомLocation,202 Acceptedдля асинхронно принятой операции,204 No Contentбез тела. - 3xx — перенаправление. Браузер кэширует
301навсегда, поэтому ошибка в нём обходится дорого;302/307— временные;304 Not Modified— основа условных запросов и экономии трафика. - 4xx — ошибка клиента, повтор не поможет.
400— синтаксис;401— не аутентифицирован;403— аутентифицирован, но не разрешено;404— нет ресурса;409— конфликт состояния, в том числе конкурентное изменение;422— семантически неверные данные;429— слишком много запросов. Единственный 4xx, который повторяют, и только с учётомRetry-After.
- 5xx — ошибка сервера, повторять можно, если операция идемпотентна.
500— необработанная ошибка;502/504— проблема на пути к приложению;503— временно недоступен; повтор — тоже с учётомRetry-After.
- Тело ошибки структурируется единообразно: машинный код, человекочитаемое сообщение, идентификатор запроса для поддержки, поля с деталями валидации. Стандартный формат —
application/problem+json(RFC 9457).
Trade-offs и режимы отказа. 200 на ошибку ломает повторы и мониторинг. 500 на ошибку валидации приводит к бесполезным повторам и ложным алертам. Раскрытие внутренних деталей в теле ошибки — утечка информации (SEC-06).
Что мерить.
- распределение кодов по эндпоинтам;
- долю 5xx как ключевой SLI (service level indicator, показатель уровня обслуживания; OPS-09);
- долю 429 — сигнал о неверных лимитах или о клиенте в цикле повторов.
Связано: REL-01 повторы · NET-16 идемпотентность · EDGE-06 rate limiting
Источники: RFC 9110 — HTTP Semantics · RFC 9457 — Problem Details for HTTP APIs · ByteByteGo — HTTP Status Codes Explained In 5 Minutes
NET-06 · HTTP-кэширование и условные запросы#
Шаг W2
TL;DR. Между клиентом и сервером уже стоят кэши — браузерный, CDN, прокси. Заголовки ответа решают, будут они работать или простаивать.
Проблема. Один и тот же ответ передаётся снова и снова: список категорий, аватар, конфигурация, справочник. Каждый повтор — это round-trip, трафик и работа сервера, которых могло не быть. Обратная крайность опаснее: неверно настроенный кэш отдаёт данные одного пользователя другому, и обнаруживается это не в тестах, а в поддержке.
Как работает.
Cache-Control— основной механизм. Его директивы:max-age— сколько секунд ответ считается свежим;s-maxage— то же для разделяемых кэшей (CDN, прокси);public/private— можно ли хранить ответ в разделяемом кэше;no-cache— хранить можно, но перед выдачей обязательно проверить актуальность; это не запрет кэширования;no-store— не хранить нигде; единственная настоящая «выключалка»;immutable— ответ не изменится, проверять его не нужно даже при обновлении страницы.
- Условные запросы. Сервер отдаёт
ETag(отпечаток содержимого) илиLast-Modified. Клиент при следующем обращении присылаетIf-None-MatchилиIf-Modified-Since; если ничего не изменилось, сервер отвечает304 Not Modifiedбез тела. Round-trip остаётся, трафик исчезает. ETagработает и на запись.If-Matchс текущим ETag превращаетPUTв операцию «обнови, только если не изменилось с момента чтения». При конкурентной правке приходит412 Precondition Failed. Это оптимистическая блокировка (DATA-08) средствами протокола.Varyперечисляет заголовки запроса, от которых зависит ответ. ЗабытыйVary: Accept-Encodingломает сжатие. ЗабытыйVary: AuthorizationилиCookieприpublic-кэшировании отдаёт персональный ответ чужому пользователю.- Мягкое устаревание.
stale-while-revalidateразрешает отдать чуть устаревший ответ и обновить его в фоне,stale-if-error— отдать устаревший, если источник недоступен. Обе директивы превращают кэш в механизм устойчивости (REL-04). - Инвалидация через URL. Для статики надёжнее всего не инвалидировать, а менять адрес: имя файла с хэшем содержимого плюс
max-ageна год. Тогда старый ответ никому не мешает — на него просто перестают ссылаться.
Когда применять. Все GET-ответы, которые повторяются: статика, справочники, публичные списки, редко меняющиеся сущности. Когда нет: персонализированные ответы кэшируются только как private и с коротким сроком; данные, где устаревание недопустимо (баланс, остатки), — no-store.
Альтернативы
| Вариант | Когда лучше | Цена |
|---|---|---|
max-age без проверки |
Данные меняются предсказуемо редко | Нельзя выкатить исправление раньше срока |
ETag + 304 |
Данные меняются непредсказуемо | Round-trip остаётся |
Версионированный URL + immutable |
Статика и сборки | Нужен процесс сборки с хэшами |
no-store |
Персональные и денежные данные | Каждый запрос доходит до сервера |
Trade-offs и режимы отказа. no-cache и no-store путают постоянно — первый разрешает хранение, второй запрещает. Длинный max-age без версионирования URL означает, что исправление дойдёт до пользователей через недели. 304 экономит трафик, но не задержку — на медленных сетях stale-while-revalidate даёт больше. Ответ, ошибочно помеченный public при наличии авторизации, — инцидент безопасности, а не проблема производительности.
Что мерить.
- долю ответов
304; - hit ratio (долю попаданий) на CDN и в браузере;
- долю ответов без
Cache-Control— это упущенная ёмкость; - объём исходящего трафика на пользователя.
Связано: NET-05 коды состояния · CACHE-01 уровни кэша · EDGE-05 CDN · DATA-08 уровни изоляции
Источники: RFC 9111 — HTTP Caching · RFC 9110 — Conditional Requests · RFC 5861 — stale-while-revalidate · MDN — HTTP caching
NET-07 · Форматы сериализации и сжатие#
Шаг W2
TL;DR. Формат определяет размер, стоимость разбора и возможность менять схему. Сжатие обычно даёт больший выигрыш, чем смена формата, и стоит дешевле.
Проблема. Ответ в 800 КБ на мобильной сети — это секунды ожидания и оплаченный пользователем трафик. Разбор JSON на горячем пути может занимать больше процессорного времени, чем сама бизнес-логика. При этом переход на бинарный формат ломает привычную отладку, а неверно устроенная схема делает несовместимой любую правку контракта.
Форматы
| Формат | Сильная сторона | Слабая сторона |
|---|---|---|
| JSON | Универсален, читается человеком, отлаживается curl |
Многословен, разбор дорог, нет схемы, целые числа больше 2⁵³ теряют точность в JavaScript |
| Protobuf | Компактен, схема обязательна, правила эволюции по номерам полей (NET-18) | Не читается глазами, нужен код-генератор |
| Avro | Схема хранится отдельно и версионируется в реестре | Ориентирован на потоки и аналитику, не на API |
| MessagePack / CBOR | «Бинарный JSON»: компактнее без схемы | Выигрыш скромный, инструментов меньше |
| Parquet | Колоночный, для аналитических выгрузок (DATA-15) | Не для обмена в реальном времени |
Сжатие.
- gzip — универсальное умолчание, поддерживается всеми.
- brotli — заметно лучше на тексте, но сжатие дороже: применяют с предварительным сжатием статики на сборке.
- zstd — настраиваемый баланс скорости и степени сжатия, удобен для внутреннего трафика и хранения.
- Сжимать нужно текст. Уже сжатое (JPEG, MP4, архивы) повторное сжатие не уменьшает — только тратит процессор.
- На мелких ответах сжатие проигрывает: до примерно килобайта накладные расходы больше выигрыша.
- Сжатие обычно сокращает текстовый ответ в пять–десять раз — как правило, больше, чем переход с JSON на бинарный формат.
Практические правила. Наружу — JSON плюс сжатие: цена отладки перевешивает выигрыш от бинарного формата. Внутрь, между сервисами с высокой частотой вызовов, — protobuf (NET-10): выигрыш и в размере, и в процессорном времени. Для больших чисел и денег в JSON — строка вместо числа. Для потоковых выгрузок — построчный JSON (NDJSON), который можно читать, не загружая целиком.
Trade-offs и режимы отказа. Бинарный формат лишает возможности посмотреть трафик глазами — нужны инструменты декодирования в логах и трассировке. Схема требует дисциплины эволюции: переиспользованный номер поля в protobuf даёт тихое искажение данных. Если ответ содержит и секрет, и данные, которыми управляет злоумышленник, сжатие открывает известный класс атак по размеру ответа. Для страниц с токенами это учитывают отдельно.
Что мерить.
- размер ответа: медиану и p95;
- долю сжатого трафика;
- время сериализации в профиле;
- процессорное время на сжатие;
- долю клиентов, поддерживающих brotli.
Связано: NET-10 gRPC и protobuf · NET-18 контракты и схемы · NET-06 HTTP-кэширование · PERF-05 оптимизация API
Источники: Protocol Buffers — Encoding · Apache Avro — Specification · RFC 9110 — Content Codings · RFC 8878 — Zstandard
NET-08 · Стили API: обзор и выбор#
Шаг W2
TL;DR. Стиль выбирается по тому, кто клиент и какой характер взаимодействия, а не по новизне технологии.
Как выбирать
| Стиль | Когда уместен | Слабое место |
|---|---|---|
| REST (NET-09) | Публичный API, ресурсная модель, кэшируемые чтения | Over/under-fetching (лишние или недостающие данные), много обращений |
| gRPC (NET-10) | Внутренние вызовы, строгий контракт, стриминг, низкая задержка | Не работает напрямую из браузера, бинарный формат сложнее отлаживать |
| GraphQL (NET-11) | Много разных клиентов с разными потребностями в данных | Кэширование, ограничение сложности запросов, N+1 |
| WebSocket / SSE (NET-12) | Сервер инициирует обмен | Долгоживущие соединения, масштабирование |
| Webhooks (MSG-10) | Уведомление внешних систем | Зависимость от чужой доступности |
| Обмен сообщениями (MSG-02) | Асинхронная развязка сервисов | Отложенная согласованность |
| SOAP | Legacy (унаследованные системы), отраслевые стандарты, WS-* | Многословность, сложность |
Практическое правило. Наружу — REST (плюс GraphQL, если клиентов много и они разные); внутрь — gRPC или обмен сообщениями; событийные уведомления — webhooks наружу, брокер внутрь. Смешение стилей нормально: стиль выбирается для границы, а не для системы.
Ключевой вопрос перед выбором. Взаимодействие синхронное по смыслу или асинхронное? Синхронный вызов создаёт временну́ю связанность: вызывающий недоступен, пока недоступен вызываемый. Многие проблемы с надёжностью решает не выбор между REST и gRPC, а перевод взаимодействия в асинхронное.
Trade-offs и режимы отказа. Несколько стилей в одной системе — норма, но каждый добавляет свой контракт, свою отладку и свой набор клиентских ошибок. Выбор стиля наружу почти необратим: он становится публичным обязательством, тогда как внутренний стиль меняется вместе с сервисами.
Что мерить.
- число обращений на пользовательский сценарий;
- распределение трафика по стилям;
- время интеграции нового клиента.
Связано: NET-09 REST · NET-10 gRPC · NET-11 GraphQL · ARCH-01 стили архитектуры
Источники: Google API Design Guide · ByteByteGo — Top 6 Most Popular API Architecture Styles · ByteByteGo — Top 9 Most Popular API Protocols · ByteByteGo — APIs Explained in 6 Minutes! · ByteByteGo — Top 6 Most Popular API Architecture Styles
NET-09 · REST#
Шаг W2
TL;DR. REST — это ресурсы, которые идентифицирует URL, и стандартная семантика HTTP-методов. Ценность — в предсказуемости и в том, что промежуточная инфраструктура понимает такой API без настройки.
Как работает.
- Ресурс — существительное, действие — метод.
POST /orders,GET /orders/42, а неPOST /createOrder. - Семантика методов — это контракт, на который опирается инфраструктура:
| Метод | Безопасный | Идемпотентный | Кэшируемый |
|---|---|---|---|
| GET | ✅ | ✅ | ✅ |
| PUT | — | ✅ | — |
| DELETE | — | ✅ | — |
| PATCH | — | ✗ (по умолчанию) | — |
| POST | — | ✗ | редко |
Идемпотентность PUT и DELETE означает: повтор даёт тот же итог. POST неидемпотентен по определению — отсюда потребность в ключе идемпотентности (NET-16).
- Условные запросы.
ETag+If-None-Matchэкономят трафик;If-Matchдаёт оптимистическую блокировку через HTTP: одновременное изменение получает412. - Коллекции: фильтрация, сортировка и пагинация (NET-14) — параметрами запроса; вложенность не глубже одного уровня.
- HATEOAS (гипермедиа как механизм состояния приложения) — часть исходного определения REST, которую на практике почти не применяют. Без гипермедиа API не становится «неправильным», но называть вещи стоит своими именами.
Trade-offs и режимы отказа. Ресурсная модель плохо ложится на действия, которые не сводятся к изменению состояния ресурса («пересчитать», «отправить»). Их оформляют как ресурс-операцию: POST /orders/42/refunds. Строгая нормализация ресурсов приводит к множеству обращений на один экран. Лечится агрегирующим эндпоинтом, BFF (backend for frontend, бэкенд под конкретный клиент) или GraphQL. API, который игнорирует коды ответа (NET-05), обесценивает всю модель.
Что мерить.
- число обращений на пользовательский сценарий;
- долю ответов из кэша;
- распределение размеров ответов;
- долю 4xx по эндпоинтам.
Связано: NET-05 коды состояния · NET-14 пагинация · NET-16 идемпотентность · NET-17 версионирование
Источники: RFC 9110 — HTTP Semantics · Google API Design Guide · Roy Fielding — Architectural Styles (гл. 5) · ByteByteGo — What Is REST API? Examples And How To Use It: Crash Course System Design #3
NET-10 · gRPC и protobuf#
Шаг W2
TL;DR. gRPC — вызов удалённой процедуры поверх HTTP/2 с бинарной сериализацией и контрактом, из которого генерируется код обеих сторон.
Как работает.
- Контракт первичен.
.protoописывает сервисы и сообщения; из него генерируются клиент и сервер. Рассинхронизация клиента и сервера ловится компилятором, а не в проде. - Protobuf — компактная бинарная сериализация с номерами полей. Правила эволюции: номера полей неизменны, новые поля добавляются опциональными, удалённые номера резервируются. При соблюдении правил старый код читает новые сообщения и наоборот.
- Четыре типа вызовов: унарный, стриминг от сервера, от клиента и двунаправленный — последние три получаются бесплатно из мультиплексирования HTTP/2.
- Дедлайны вместо таймаутов. Клиент передаёт срок, и он распространяется по цепочке вызовов: промежуточные сервисы знают, сколько времени осталось, и не выполняют заведомо просроченную работу. Это заметно лучше независимых таймаутов на каждом участке (REL-01).
- Перехватчики (interceptors) — единое место для аутентификации, трассировки, метрик и повторов.
Когда применять. Внутренние вызовы между сервисами, особенно при высокой частоте и требовании к задержке; стриминг; среды с несколькими языками программирования. Когда нет: публичный API для сторонних разработчиков (порог входа выше, отладка сложнее); прямые вызовы из браузера — нужен gRPC-Web и прокси.
Альтернативы
| Вариант | Плюс | Минус |
|---|---|---|
| gRPC | Контракт, скорость, стриминг, дедлайны | Не для браузера, бинарная отладка |
| REST + JSON | Универсальность, простота отладки | Многословнее, нет генерации по умолчанию |
| REST + OpenAPI + кодогенерация | Компромисс: контракт при простоте HTTP | Контракт и реализация могут разъехаться |
| Обмен сообщениями | Развязка во времени | Отложенная согласованность |
Trade-offs и режимы отказа. Балансировка gRPC требует внимания. Одно долгоживущее HTTP/2-соединение мультиплексирует все вызовы, поэтому обычный L4-балансировщик закрепляет клиента за одним сервером, и нагрузка распределяется неравномерно. Нужна балансировка на стороне клиента или L7-прокси. Нарушение правил эволюции .proto (переиспользование номера поля) даёт тихое искажение данных. Бинарный формат усложняет ручную диагностику.
Что мерить.
- задержку по методам;
- долю превышений дедлайна;
- распределение нагрузки по бэкендам — перекос выдаёт проблемы с балансировкой;
- размер сообщений.
Связано: NET-18 контракты · NET-03 HTTP/2 · REL-01 таймауты
Источники: gRPC — Documentation · Protocol Buffers — Language Guide · gRPC — Deadlines · ByteByteGo — What is RPC? gRPC Introduction.
NET-11 · GraphQL#
Шаг W2
TL;DR. В GraphQL клиент сам описывает, какие поля ему нужны. Это решает over- и under-fetching (лишние и недостающие данные в ответе), но кэширование и защита от дорогих запросов ложатся на вас.
Проблема. У мобильного приложения, веба и партнёрского интеграционного клиента разные потребности в данных. REST даёт либо жирные ответы «на всех» (лишний трафик), либо десяток запросов на один экран (лишние round-trip). Плодить эндпоинты под каждый экран — множить поддержку.
Как работает. Один эндпоинт, типизированная схема, запрос описывает дерево нужных полей. Резолверы получают данные из любых источников. Query для чтения, Mutation для изменения, Subscription для потока событий.
Что приходится решать самому.
- N+1 у резолверов. Запрос списка из 100 объектов с вложенным полем порождает 100 обращений к БД. Решается батчингом и кэшем в пределах запроса (паттерн DataLoader) — это не опция, а обязательный элемент.
- Кэширование. HTTP-кэш не работает: один URL и
POST. Нужны кэш на уровне полей, persisted queries и нормализованный кэш на клиенте. Persisted queries (сохранённые запросы) — клиент присылает идентификатор запроса, и такие запросы можно отдавать черезGETи кэшировать. - Защита от дорогих запросов. Глубоко вложенный запрос может стоить сколь угодно дорого. Обязательны ограничение глубины, оценка сложности и таймаут; для публичного API — белый список persisted queries.
- Ошибки. По спецификации ответ приходит с
200и массивомerrors. Мониторинг и балансировщики этого не видят, поэтому ошибки снимают отдельными метриками.
Когда применять. Много разнородных клиентов; агрегация нескольких источников для UI; быстро меняющиеся требования фронтенда. Когда нет: один клиент и стабильные экраны (REST проще); файловые загрузки и потоковая передача; сервис-сервис (gRPC даёт больше при меньшей сложности).
Trade-offs и режимы отказа. GraphQL переносит сложность с фронтенда на бэкенд: планирование, батчинг, ограничения и observability (наблюдаемость) становятся вашей задачей. Схема как единый контракт всей системы легко превращается в связующее звено, тормозящее изменения. Без ограничений сложности один запрос может вызвать отказ в обслуживании.
Что мерить.
- задержку по операциям — не по эндпоинту, он один;
- число обращений к источникам на запрос;
- распределение сложности запросов;
- долю persisted queries;
- частоту ошибок в теле ответа.
Связано: NET-08 стили API · ARCH-10 BFF · PERF-06 N+1
Источники: GraphQL — Specification и Best Practices · Netflix — эволюция API-архитектуры · ByteByteGo — What Is GraphQL? REST vs. GraphQL
NET-12 · Realtime-транспорт: polling, long polling, SSE, WebSocket#
Шаг W2
TL;DR. Четыре способа доставить данные от сервера к клиенту без запроса. Выбор определяется направлением обмена и частотой событий, а не новизной технологии.
Проблема. HTTP устроен как «запрос — ответ»: сервер не может заговорить первым. А уведомлениям, чату, ленте, статусу длительной операции и совместному редактированию нужно, чтобы сервер начинал первым. Без этого не спроектировать ни одну интерактивную систему.
Варианты
| Способ | Направление | Стоимость | Когда уместно |
|---|---|---|---|
| Периодический опрос | клиент → сервер | Пустые запросы, задержка до интервала | События редки, задержка в десятки секунд приемлема |
| Long polling (длинный опрос) | клиент → сервер, ответ по событию | Соединение висит, но это обычный HTTP | Нужна низкая задержка, инфраструктура простая |
| SSE (Server-Sent Events) | сервер → клиент | Одно HTTP-соединение, текст | Односторонний поток: лента, уведомления, прогресс |
| WebSocket | двунаправленный | Отдельный протокол после upgrade | Чат, игры, совместное редактирование |
Что важно знать.
- SSE — обычный HTTP-ответ, который не заканчивается. Браузерный
EventSourceсам переподключается и умеет продолжать с последнего идентификатора события (Last-Event-ID) — восстановление после обрыва встроено в протокол. Только текст, только от сервера. По HTTP/2 ограничение на число соединений снимается. - WebSocket — двунаправленный канал после HTTP-upgrade. Всё, что даёт HTTP, приходится делать заново:
- аутентификацию: из браузера токен нельзя положить в заголовок при подключении, поэтому используют подпротокол или короткоживущий тикет;
- heartbeat (периодический сигнал) для обнаружения мёртвых соединений;
- переподключение с восстановлением состояния;
- rate limiting (ограничение частоты).
- Масштабирование push. Соединение закреплено за конкретным экземпляром сервера. Событие, возникшее в другом экземпляре, должно до него дойти, поэтому нужны pub/sub (издатель — подписчик: Redis, брокер) и реестр «пользователь → узел». Это главная архитектурная стоимость realtime, и её закладывают сразу.
- Ограничения инфраструктуры. Прокси и балансировщики закрывают простаивающие соединения (обычно через 60 с), поэтому heartbeat обязателен. Каждое открытое соединение потребляет память и файловый дескриптор — это задаёт потолок соединений на узел.
Как выбирать.
- односторонний поток → SSE, он проще во всём;
- двусторонний обмен с низкой задержкой → WebSocket;
- редкие события → опрос;
- уведомление другой системы, а не браузера → webhook (MSG-10).
Trade-offs и режимы отказа. Долгоживущие соединения превращают выкатку в проблему: при рестарте все клиенты переподключаются одновременно. Нужен jitter (случайный разброс) задержки переподключения, иначе получается самодельная DDoS-атака. Состояние на сервере мешает горизонтальному масштабированию. Мобильные сети рвут соединения постоянно — восстановление должно быть штатным путём, а не обработкой ошибки.
Что мерить.
- число одновременных соединений на узел;
- частоту переподключений;
- задержку доставки события;
- долю потерянных сообщений при обрыве;
- память на соединение.
Связано: MSG-10 webhooks · EDGE-01 балансировка · CASE-03 чат · NET-03 HTTP/2
Источники: MDN — Server-Sent Events · RFC 6455 — WebSocket Protocol · WHATWG HTML — Server-sent events
NET-13 · Асинхронные операции в API: 202 и ресурс статуса#
Шаг W2
TL;DR. Операция дольше нескольких секунд не должна жить в HTTP-соединении. Ответ 202 плюс ресурс операции превращают её в наблюдаемый объект с идентификатором.
Проблема. Экспорт отчёта, обработка видео, импорт файла, платёж через внешнюю систему занимают минуты. Синхронный вызов упирается в таймауты прокси и балансировщиков и занимает поток на всё время. При обрыве соединения результат теряется: клиент не знает, выполнилась операция или нет, а повтор рискует запустить её второй раз.
Как работает.
POST /exportsвозвращает202 Acceptedи заголовокLocationс адресом ресурса операции.- Ресурс операции — полноценный объект:
GET /operations/{id}отдаёт статус (pending,running,succeeded,failed), время создания и обновления, прогресс, описание ошибки при неудаче и ссылку на результат при успехе. - Клиент узнаёт о завершении одним из способов: опрос с учётом
Retry-Afterи нарастающей паузой; webhook (MSG-10); поток событий (NET-12). Опрос проще и надёжнее, остальное — оптимизация. - Создание операции идемпотентно (NET-16): повтор
POSTс тем же ключом возвращает ту же операцию, а не запускает вторую. - Отмена и срок жизни. Отмена — отдельная операция над ресурсом; у результата есть срок хранения, после которого он удаляется, и это часть контракта.
Когда применять. Операция может занять больше нескольких секунд; зависит от внешней системы с непредсказуемым временем; обрабатывает много элементов (NET-15). Когда нет: быстрые операции — асинхронный контракт заставляет каждого клиента писать цикл опроса, и для операции на 200 мс это чистое усложнение.
Альтернативы
| Вариант | Когда лучше | Цена |
|---|---|---|
202 + ресурс операции |
Стандартный случай | Клиенту нужен цикл опроса; хранение статусов |
| Синхронный вызов с большим таймаутом | Операция быстрая в подавляющем большинстве случаев | Таймауты прокси, потеря результата при обрыве |
| Webhook по завершении | У клиента есть публичный эндпоинт | Зависимость от доступности клиента |
| SSE / WebSocket | Нужен прогресс в реальном времени | Долгоживущее соединение (NET-12) |
Trade-offs и режимы отказа. Статусы операций — состояние, которое надо где-то хранить и чистить. Без персистентности перезапуск сервиса «теряет» операции, и клиент опрашивает несуществующий идентификатор. Тысячи клиентов, опрашивающих раз в секунду, создают собственную нагрузку — Retry-After и нарастающая пауза обязательны. Статус failed без машинно-читаемой причины бесполезен: клиент не может решить, повторять ли.
Что мерить.
- распределение времени выполнения;
- долю операций в статусе
failed, в том числе по причинам; - число опросов на одну операцию;
- возраст самой старой незавершённой операции.
Связано: NET-16 идемпотентность · NET-15 массовые операции · MSG-10 webhooks · MSG-02 очередь vs лог
Источники: Google AIP-151 — Long-running operations · RFC 9110 — 202 Accepted · Microsoft — REST API guidelines: long running operations
NET-14 · Пагинация: offset против курсора#
Шаг W2
TL;DR. OFFSET прост и ломается дважды: по производительности — на больших offset, по корректности — на изменяющихся данных. Курсор решает оба, но лишает произвольного перехода на страницу.
Проблема. LIMIT 20 OFFSET 100000 заставляет СУБД прочитать и отбросить сто тысяч строк — время растёт линейно с номером страницы. Хуже другое: если между запросами страниц добавилась запись, элементы сдвигаются, и пользователь видит один и тот же объект дважды или не видит вовсе.
Как работает.
- Offset-пагинация:
LIMIT ... OFFSET .... Плюс — можно прыгнуть на страницу 57 и показать общее число страниц. Минус — линейная деградация и сдвиг данных. - Курсорная (keyset):
WHERE (created_at, id) < (:last_created_at, :last_id) ORDER BY created_at DESC, id DESC LIMIT 20. Запрос идёт по индексу и стоит одинаково на любой глубине; вставки не сдвигают выдачу. Ключ сортировки должен быть уникальным — обычно составным из времени и идентификатора. Иначе записи с одинаковым временем теряются или дублируются. - Непрозрачный курсор. Наружу отдаётся закодированная строка, а не понятные значения полей: это позволяет менять реализацию без ломки клиентов и не раскрывает внутреннюю структуру.
- Общее число элементов при курсорной схеме обычно не отдают — точный
COUNTна большой таблице стоит дорого; при необходимости показывают приблизительную оценку.
Когда применять. Курсор — для лент, потоков, бесконечной прокрутки, API с большими коллекциями и для любой выгрузки. Offset — для админских таблиц с небольшими объёмами, где нужны номера страниц. Когда нет: курсор не подходит, если нужна сортировка по изменяемому полю — при изменении значения элемент «перепрыгивает» через курсор.
Trade-offs и режимы отказа. Курсор жёстко связан с порядком сортировки: смена сортировки означает новый курсор. Глубокая offset-пагинация — типичная причина «тяжёлых» запросов, которые обнаруживаются только на боевых объёмах. Экспорт всех данных постраничным offset при активной записи гарантированно даёт пропуски.
Что мерить.
- задержку по глубине страницы;
- долю запросов с большим offset;
- число повторно отданных или пропущенных элементов — их находит сверка;
- размер страниц.
Связано: DATA-06 оптимизация SQL · DATA-05 индексы · NET-09 REST
Источники: Use The Index, Luke — Paging Through Results · Google API Design Guide — List Pagination · ByteByteGo — API Pagination: Making Billions of Products Scrolling Possible
NET-15 · Массовые операции и батчинг#
Шаг W2
TL;DR. Тысяча объектов не должна означать тысячу запросов. Но как только запрос обрабатывает много элементов, приходится явно решить, что делать при частичном успехе.
Проблема. Синхронизация каталога, импорт пользователей, простановка признака у списка объектов: поэлементные вызовы упираются в задержку сети, помноженную на количество, и в лимиты частоты (EDGE-06). Наивный массовый эндпоинт создаёт другую проблему: половина элементов прошла, половина нет, а ответ один — клиент не знает, что повторять.
Как работает.
- Явная модель ошибок. Ответ содержит статус по каждому элементу: идентификатор, результат, код ошибки. Формат может быть собственным или
207 Multi-Status; важно, что общий код ответа не описывает исход — он описывает только то, что пачка принята и обработана. - Атомарность объявляется в контракте. Либо «всё или ничего» (одна транзакция, пачка ограничена по размеру), либо «best-effort (сколько получится) по элементам». Промежуточных вариантов быть не должно: на этом выборе клиент строит логику повторов.
- Идемпотентность на уровне элемента (NET-16), а не только пачки: клиент повторяет только неуспешные элементы, и повтор не должен дублировать успешные.
- Ограничение размера — по числу элементов и по объёму тела; превышение отклоняется с понятным кодом, а не таймаутом.
- Большие объёмы — не батч, а импорт. Начиная с десятков тысяч элементов правильный контракт — загрузка файла в объектное хранилище (DATA-12) и асинхронная операция (NET-13) с отчётом об ошибках. Построчный JSON (NDJSON) позволяет обрабатывать поток, не загружая его целиком в память.
- Батчинг чтения решает другую задачу —
GET /users?ids=1,2,3устраняет проблему N+1 на стороне клиента (PERF-06).
Когда применять. Импорт и синхронизация, массовые изменения, чтение множества сущностей по идентификаторам. Когда нет: когда операции над элементами независимы по времени и нагрузка невелика — поэлементные вызовы проще и лучше наблюдаются.
Альтернативы
| Вариант | Когда лучше | Цена |
|---|---|---|
| N отдельных запросов | Малые объёмы | Задержка × N, лимиты частоты |
| Батч с частичным успехом | Стандартный случай | Сложная модель ошибок у клиента |
| Атомарная пачка | Нужна транзакционность | Жёсткое ограничение размера, длинные транзакции |
| Асинхронный импорт файлом | Десятки тысяч элементов и больше | Отдельный контракт с операцией и отчётом |
Trade-offs и режимы отказа. Большая пачка — это длинная транзакция (DATA-07): она удерживает блокировки и рискует таймаутом, а один медленный элемент задерживает весь ответ. Частичный успех, о котором клиент не знает, приводит к повтору всей пачки и дубликатам — поэтому идемпотентность элементов здесь не опция. Массовый эндпоинт без ограничения размера — простой способ исчерпать память сервиса одним запросом.
Что мерить.
- распределение размеров пачек;
- долю ответов с частичным успехом;
- время обработки на элемент;
- долю повторно отправленных элементов;
- число отклонений по превышению размера.
Связано: NET-13 асинхронные операции · NET-16 идемпотентность · NET-14 пагинация · PERF-06 N+1 и батчинг
Источники: Google AIP-231 — Batch methods · RFC 4918 — 207 Multi-Status · JSON Lines
NET-16 · Идемпотентность и ключ идемпотентности#
Шаг W2
TL;DR. Повторный вызов с тем же ключом не создаёт второй эффект, а возвращает результат первого. Без этого любой таймаут в платёжном сценарии — потенциальное двойное списание.
Проблема. Клиент отправил POST /payments, получил таймаут. Что произошло — запрос не дошёл, дошёл и выполнился, или выполнился, но ответ потерялся? Клиент не может это выяснить. Не повторить — потерять операцию; повторить — рискнуть списать дважды. Это не редкий случай: в распределённой системе неопределённость результата — норма (REL-01).
Как работает.
- Клиент генерирует ключ (UUID) на попытку бизнес-операции — не на HTTP-запрос. Все повторы этой операции идут с тем же ключом, обычно в заголовке
Idempotency-Key. - Сервер атомарно резервирует ключ: вставка в таблицу с уникальным индексом. Успех вставки = это первое выполнение. Конфликт = повтор.
- Выполнение и сохранение результата — в той же транзакции, что и бизнес-эффект. Иначе возможен разрыв: эффект есть, записи о ключе нет.
- При повторе возвращается сохранённый ответ с тем же кодом и телом.
- Параллельный повтор, пока первый ещё выполняется, получает
409— клиент повторит позже. - Ключ хранится ограниченное время (обычно 24 часа) — дольше максимального окна повторов.
- Ключ привязан к содержимому запроса: тот же ключ с другим телом — это ошибка клиента, а не идемпотентный повтор; отвечать нужно ошибкой, а не выполнением.
Естественная идемпотентность лучше журнала. Идемпотентны сами по себе и не требуют инфраструктуры:
PUTс полным состоянием;UPDATE ... WHERE status='pending';- вставка с уникальным ключом из доменных данных, например номера заказа.
Когда применять. Любая небезопасная операция, доступная извне и допускающая повтор: платежи, создание заказов, отправка сообщений, обработчики очередей (MSG-06), webhooks. Когда нет: чтения идемпотентны по определению; для внутренних операций часто достаточно естественной идемпотентности.
Trade-offs и режимы отказа. Хранилище ключей — дополнительное состояние на горячем пути. Слишком короткое время жизни ключей пропускает поздние повторы. Ключ, сгенерированный на каждый HTTP-запрос вместо бизнес-операции, не защищает ни от чего. Идемпотентность на уровне API не спасает, если ниже по цепочке есть неидемпотентный вызов внешней системы — там нужен свой ключ.
Что мерить.
- долю запросов-повторов;
- число конфликтов по ключу;
- размер и возраст хранилища ключей;
- число обнаруженных двойных эффектов — оно должно быть нулевым.
Связано: MSG-06 гарантии доставки · REL-01 повторы · DIST-08 saga · CASE-09 платёжный поток
Источники: Stripe — Idempotent requests · AWS Builders' Library — Making retries safe with idempotent APIs · RFC 9110 — Idempotent Methods
NET-17 · Версионирование API и совместимость#
Шаг W2
TL;DR. Версия нужна только для несовместимых изменений. Задача не в том, чтобы выбрать место для номера версии, а в том, чтобы несовместимых изменений было как можно меньше.
Проблема. Клиент и сервер выкатываются независимо, а мобильное приложение старой версии живёт на устройствах годами. Изменение, которое ломает старого клиента, нельзя откатить простым откатом сервера — пользователи уже обновились или, наоборот, так и не обновились.
Как работает.
- Совместимые изменения (версии не требуют): новое необязательное поле в запросе, новое поле в ответе, новый эндпоинт. Расширение перечисления совместимо при условии, что клиенты игнорируют неизвестные значения. Правило Постела в практическом виде: клиент должен игнорировать неизвестные поля, и это закладывают в контракт с первого дня.
- Несовместимые (требуют версии): удаление или переименование поля, изменение типа, ужесточение валидации, изменение семантики, новое обязательное поле в запросе.
- Где размещать версию. В пути (
/v1/orders) — просто и заметно; чаще всего этого достаточно. В заголовке или типе содержимого — чище в теории, но сложнее в отладке и кэшировании. - Стратегия перехода — тот же expand/contract (расширить, затем сузить), что и для схемы БД (DATA-17). Новое поле появляется рядом со старым, оба поддерживаются, клиенты мигрируют, старое удаляется через объявленный срок.
- Политика устаревания: заголовки
DeprecationиSunset, объявленный срок поддержки, метрики использования старой версии. Снимать версию можно, когда трафик по ней стал нулевым, а не когда прошёл срок.
Особый случай — мобильные клиенты. Обновление не гарантировано, поэтому старые версии живут годами. Практика: серверное управление минимально поддерживаемой версией с принудительным обновлением, а также перенос изменяемой логики на сервер.
Trade-offs и режимы отказа. Каждая живая версия — это ветка поведения, тесты и потенциальные расхождения; две версии дороже одной вдвое, а не на 10 %. Версионирование целого API вместо отдельных ресурсов заставляет мигрировать всех ради изменения одного эндпоинта. Без метрик по версиям старую версию удаляют наугад.
Что мерить.
- долю трафика по версиям и по клиентам;
- число клиентов на устаревших версиях;
- срок жизни версии;
- число несовместимых изменений за квартал.
Политика — отдельное решение. Эта карточка отвечает на вопрос «как», а не «что обещано и кому». Срок жизни поля, охват обещания и цена знания о потребителях решаются в D-11. Там же — данные о том, чего стоит номер версии как обещание, с оговоркой об их возрасте.
Связано: NET-18 контракты · D-11 обещание совместимости · DATA-17 миграции схемы · OPS-12 контрактные тесты · OPS-16 релиз мобильных приложений
Источники: Google AIP-185 — API Versioning · Stripe — API upgrades и версии · RFC 9745 — The Deprecation HTTP Response Header Field · RFC 8594 — The Sunset HTTP Header Field
NET-18 · Контракты и схемы#
Шаг W2
TL;DR. Контракт — машиночитаемый артефакт, из которого генерируются клиенты, проверки и тесты. Документация, написанная после реализации, — не контракт.
Проблема. Пока контракт живёт в описании на вики, он расходится с реальностью в момент первого хотфикса. Интеграция превращается в переписку, а несовместимое изменение обнаруживается в проде клиентом.
Как работает.
- Формат по стилю API: OpenAPI для REST,
.protoдля gRPC, SDL (язык описания схем) для GraphQL, JSON Schema или Avro/Protobuf для событий. - Contract-first (сначала контракт). Схему пишут до реализации, ревьюят как код и хранят в репозитории; из неё генерируют серверные заготовки, клиентов и валидацию. Обратный порядок (генерация схемы из кода) допустим, но тогда контракт следует за реализацией и ломается вместе с ней.
- Проверка совместимости в CI. Инструменты сравнения схем отклоняют несовместимое изменение до слияния — это самая ценная часть всей практики.
- Schema registry (реестр схем) для событий. Производитель регистрирует схему, реестр проверяет совместимость (обратную, прямую или полную), потребители читают старые сообщения новым кодом. Без реестра эволюция событий в очереди неуправляема.
- Контрактные тесты, управляемые потребителем (Pact): потребитель декларирует свои ожидания, поставщик прогоняет их в своём CI. Такие тесты ловят поломки без полного интеграционного стенда.
Когда применять. Любая граница между командами или наружу. Когда нет: внутри одного сервиса контракт не нужен; для одноразового прототипа накладные расходы не окупаются.
Trade-offs и режимы отказа. Схема — ещё один артефакт, который нужно синхронизировать. Даже при contract-first схема и реализация могут разойтись, если генерация не обязательна. Слишком строгая валидация ответов на стороне клиента ломает совместимость: клиент, падающий на неизвестном поле, делает невозможным любое расширение API. Реестр схем — новая зависимость на пути публикации событий.
Что мерить.
- долю эндпоинтов, покрытых схемой;
- число несовместимых изменений, пойманных в CI;
- расхождения между схемой и фактическими ответами — их находит выборочная валидация трафика.
Связано: NET-17 версионирование · NET-10 gRPC · OPS-12 тестирование · MSG-06 эволюция событий · NET-20 API против SDK · D-11 обещание совместимости
Источники: OpenAPI Specification · Confluent Schema Registry — Compatibility · Pact — Consumer-driven contract testing · Martin Fowler — Consumer-Driven Contracts
NET-19 · CORS и браузерная модель безопасности#
Шаг W8
TL;DR. CORS — это разрешение браузеру читать ответ с чужого origin (источника). Он защищает пользователя от вредоносной страницы, а не ваш сервер от злоумышленника.
Проблема. Фронтенд на app.example.com обращается к api.example.com и получает ошибку. Типичная реакция — поставить Access-Control-Allow-Origin: * и забыть. В результате API доступен любой странице в интернете. Если при этом используются cookie-сессии, чужая страница может действовать от имени пользователя. Обратная ошибка не менее частая: считать, что CORS что-то защищает, и ослабить проверки авторизации.
Как работает.
- Origin — это схема, хост и порт. Разные поддомены — разные origin;
httpsиhttp— тоже. - Правило одного источника (same-origin policy, SOP) запрещает странице читать ответ с другого origin. Запрос при этом уходит — браузер лишь не отдаёт результат скрипту. Отсюда главное следствие: CORS не мешает злоумышленнику обратиться к вашему API напрямую, минуя браузер.
- Простые запросы уходят сразу. Остальным — с нестандартным методом,
Content-Type: application/jsonили своими заголовками — предшествует предварительный запрос (preflight)OPTIONS: сервер отвечает на него разрешениями.Access-Control-Max-Ageкэширует ответ и убирает лишний round-trip. - Заголовки ответа:
Access-Control-Allow-Origin(конкретный origin или*),-Allow-Credentials,-Allow-Methods,-Allow-Headers,-Expose-Headers(какие заголовки ответа видит скрипт). *несовместима с credentials (учётными данными). Если запрос идёт с cookie или заголовком авторизации, origin должен быть указан явно. Отражать origin из запроса можно только по белому списку — и обязательно сVary: Origin, иначе разделяемый кэш отдаст чужой заголовок (NET-06).- Cookie и
SameSite.Lax(умолчание в современных браузерах) не отправляет cookie на межсайтовые запросы, кроме навигации;NoneтребуетSecure. Основную часть CSRF (межсайтовой подделки запроса) закрываетSameSite, а не CORS. У API с токеном в заголовке проблемы CSRF нет вовсе, зато появляется другая: как хранить токен, чтобы его не украли через XSS (межсайтовый скриптинг).
Когда применять. Когда браузерное приложение обращается к API на другом origin. Когда нет: серверные клиенты, мобильные приложения и curl про CORS ничего не знают — это исключительно браузерный механизм.
Альтернативы
| Вариант | Когда лучше | Цена |
|---|---|---|
| Белый список origin | Несколько известных фронтендов | Список нужно поддерживать |
Тот же origin через reverse proxy (обратный прокси: /api на домене приложения) |
Хочется убрать CORS и сложности с cookie | Ещё один слой маршрутизации (EDGE-03) |
| BFF (backend for frontend) на домене фронтенда | Много бэкендов, нужен единый вход | Отдельный сервис (ARCH-10) |
Allow-Origin: * без credentials |
Полностью публичное API | Никакой изоляции — только для публичных данных |
Trade-offs и режимы отказа. Отражение любого origin с Allow-Credentials: true равносильно открытию API всему интернету от имени залогиненного пользователя. Предварительные запросы удваивают число обращений на «тяжёлых» вызовах — лечится Max-Age, а не отказом от заголовков. Разрешение через прокси убирает CORS, но переносит вопрос изоляции на уровень маршрутизации.
Что мерить.
- долю
OPTIONSв общем трафике; - число ошибок CORS в клиентской телеметрии;
- размер белого списка origin и его актуальность.
Связано: NET-06 HTTP-кэширование · SEC-06 безопасность API · SEC-01 сессии и токены · EDGE-03 API gateway
Источники: MDN — Cross-Origin Resource Sharing · WHATWG — Fetch Standard, CORS protocol · MDN — SameSite cookies · OWASP — CSRF Prevention Cheat Sheet
NET-20 · API против SDK#
Шаг W2
TL;DR. API — это контракт по сети; SDK — библиотека, которая его удобно оборачивает. SDK не заменяет API, а надстраивается над ним.
Как работает. API определяет протокол, форматы и семантику. SDK берёт на себя то, что иначе повторяет каждый клиент:
- аутентификацию и обновление токенов;
- повторы с backoff (экспоненциальной задержкой; REL-01);
- ключи идемпотентности (NET-16);
- пагинацию как итератор;
- типизированные модели;
- обработку ошибок;
- телеметрию.
Что это значит для дизайна.
- Наличие SDK не оправдывает плохой API: SDK для одного языка не покроет всех клиентов, а его логика будет отставать от сервера.
- Правильные умолчания в SDK (таймауты, повторы, лимиты) массово улучшают поведение клиентов — это дешёвый способ повысить надёжность всей экосистемы интеграций.
- SDK, генерируемый из контракта (NET-18), не расходится с API; написанный вручную рано или поздно расходится.
Trade-offs. Каждый поддерживаемый язык — это отдельный цикл релизов и совместимости. Устаревшие версии SDK у клиентов ограничивают эволюцию сервера сильнее, чем сам API. Слишком «умный» SDK, скрывающий ошибки и повторы, мешает диагностике на стороне интегратора.
Что мерить.
- долю клиентов на актуальной версии библиотеки;
- число обращений в поддержку по вопросам интеграции;
- разрыв между возможностями API и тем, что покрыто SDK.
Связано: NET-18 контракты · NET-17 версионирование · REL-01 повторы
Источники: Stripe — Libraries · OpenAPI Generator · ByteByteGo — API Vs SDK! What's the difference?
NET-21 · Устройство веб-приложения#
Шаг W4
TL;DR. Путь запроса от адресной строки до БД — это карта всех мест, где можно ускорить, кэшировать и сломать.
Полный путь запроса.
- DNS → адрес ближайшей точки присутствия (NET-01).
- CDN / edge — статика и кэшируемые ответы отдаются здесь, не доходя до вашей инфраструктуры (EDGE-05).
- Балансировщик — распределяет запрос между экземплярами, завершает TLS (EDGE-01).
- API gateway — аутентификация, rate limiting (ограничение частоты), маршрутизация по версиям (EDGE-03).
- Приложение — бизнес-логика; за ним кэш (CACHE-01), БД (DATA-01) и очередь для всего, что можно сделать позже (MSG-02).
- Ответ возвращается тем же путём, кэшируясь на подходящих уровнях.
Где рендерится страница. Серверный рендеринг даёт быстрый первый экран и работает без JavaScript; клиентский — отзывчивость после загрузки; статическая генерация — минимальную стоимость для редко меняющегося контента. Выбор влияет на то, что вообще можно кэшировать на CDN: персонализированный HTML кэшируется плохо, поэтому персонализацию часто выносят в отдельный запрос.
Что из этого следует для дизайна. Каждый уровень — это и точка ускорения, и точка отказа с собственным таймаутом и собственным кэшем. Полезное упражнение на шаге W4: пройти по цепочке и для каждого звена ответить, что произойдёт при его недоступности.
Trade-offs и режимы отказа. Каждый уровень пути одновременно ускоряет и добавляет точку отказа со своим таймаутом и своим кэшем — поэтому число уровней стоит держать минимальным. Серверный рендеринг ускоряет первый экран и ограничивает кэширование персонализированных страниц; клиентский — наоборот.
Что мерить.
- время до первого байта и до отображения содержимого;
- долю запросов, дошедших до приложения;
- число обращений на один экран.
Связано: EDGE-05 CDN · EDGE-03 API gateway · ARCH-11 клиентские паттерны
Источники: MDN — How the web works · High Performance Browser Networking · ByteByteGo — Everything You NEED to KNOW About Web Applications