awesome-everything EN
↑ Обратно к восхождению

Архитектура бэкенда

Обработчик и ответ: от бизнес-логики до байтов на проводе

Суть Обработчик выполняет логику, затем результат сериализуется, обрамляется статусом и заголовками и записывается в сокет. Стоимость сериализации и переиспользование keep-alive решают, во сколько обходится этот финальный отрезок.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 13 min

Эндпоинт, возвращающий список заказов, становится на 40% медленнее после запуска продукта. Запрос не изменился и по-прежнему отвечает за 8 мс. Замедление целиком в одной строке, которую никто не профилирует: превращение результата в JSON. Список вырос с 50 строк до 5000, и JSON.stringify теперь — самое дорогое, что делает запрос.

Обработчик в основном ждёт

Ваш обработчик выполняет бизнес-логику, но для типичного запроса большая часть его реального времени — не вычисления, а ожидание запроса к БД, обращения к кешу или другого сервиса. Именно поэтому важен асинхронный I/O (следующий юнит), и поэтому «обработчик за 3 мс» обычно означает 3 мс CPU, обёрнутые вокруг десятков миллисекунд ожидания I/O. Задача обработчика — собрать данные и произвести объект-результат; сокета он ещё не касается.

Сериализация — это реальная работа

Превращение объекта-результата в байты — это CPU, которое запрос платит всегда:

  • JSONJSON.stringify синхронен и масштабируется с размером объекта. На загруженном сервере с event-loop сериализация большого массива блокирует все остальные запросы на это время. Полезная нагрузка в 5000 строк может стоить несколько миллисекунд чистого CPU.
  • Схемные сериализаторы (fast-json-stringify у Fastify, Protobuf, MessagePack) предкомпилируют форму и работают намного быстрее общего stringify, часто в 2–4 раза — поэтому высоконагруженные API объявляют схемы ответов.
  • Рендеринг HTML/шаблонов обладает тем же свойством: это CPU, и оно синхронно, если фреймворк не стримит его.

Стоимость невидима, пока объект не вырос. Пагинация — не только UX-выбор; это контроль стоимости сериализации.

Статус, заголовки и обрамление

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

  • Content-Length — сервер знает полный размер тела заранее, отправляет его, и клиент точно знает, когда ответ закончился. Требует сначала забуферизовать всё тело.
  • Transfer-Encoding: chunked — сервер стримит, не зная общий размер заранее (тема следующего урока).

Код статуса — часть контракта, а не украшение: 200 против 201 (created), 204 (нет тела), 4xx (клиент должен изменить запрос), 5xx (вина сервера, идемпотентный вызов безопасно повторить). Возврат 200 с телом-ошибкой внутри — частый антипаттерн, ломающий логику повторов и мониторинга у каждого клиента.

РешениеСтоимостьКогда кусается
JSON.stringify (общий)O(размера), синхронно, блокирует loopБольшие массивы после роста данных
Схемный сериализаторв 2–4 раза быстрее, предкомпилированОкупается на горячих, высокообъёмных эндпоинтах
Буфер, затем отправка (Content-Length)Держит всё тело в памятиБольшие ответы дают всплеск памяти
Неверный статус (200 на ошибке)Ломает повторы и алерты клиентаТихо до самого инцидента

Keep-alive: платим за рукопожатие один раз

Открыть TCP- (и TLS-) соединение дорого — рукопожатие стоит одного или более round trip. HTTP keep-alive (Connection: keep-alive, по умолчанию в HTTP/1.1) переиспользует одно соединение для многих запросов, поэтому рукопожатие амортизируется. После того как ответ слился, соединение возвращается в простаивающий пул с обеих сторон, пока keep-alive таймаут его не закроет.

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

Викторина

Эндпоинт замедляется на 40% после роста результата с 50 до 5000 строк, но время запроса к БД не изменилось. Наиболее вероятная причина?

Викторина

Почему возврат HTTP 200 с объектом-ошибкой внутри тела считается антипаттерном?

Закончи аналогию

Заполните пропуск: HTTP keep-alive позволяет многим запросам переиспользовать одно соединение, чтобы дорогое TCP/TLS _______ оплачивалось один раз, а не на каждый запрос.

Вспомните перед уходом
  1. 01
    Почему большая часть реального времени типичного обработчика — не CPU, и почему стоимость сериализации всё равно важна?
  2. 02
    Что требуют от сервера Content-Length и chunked transfer encoding, и как код статуса работает как контракт?
  3. 03
    Что оптимизирует HTTP keep-alive и почему переиспользование соединения — забота жизненного цикла?
Итог

В центре луковицы выполняется обработчик, но большая часть его времени — ожидание I/O, а не вычисления; он производит объект-результат, не касаясь сокета. Превращение этого объекта в байты — стадия сериализации: синхронное CPU, масштабирующееся с размером нагрузки, где общий JSON.stringify на больших результатах блокирует event loop, а схемные сериализаторы или пагинация срезают стоимость. Затем ответ обрамляется строкой статуса и заголовками, используя Content-Length (сначала буфер) или chunked-кодирование, и код статуса несёт реальный смысл контракта, на который полагаются клиенты, прокси и алерты. Наконец, keep-alive амортизирует дорогое TCP/TLS-рукопожатие по многим запросам на одном соединении. Ответ по-настоящему готов лишь когда байты слиты — а когда клиент читает медленнее, чем сервер пишет, это сливание становится трудной задачей следующего урока: backpressure.

Связанные уроки
встречается в185
Продолжить восхождение ↑Стриминг и backpressure: когда клиент читает медленнее, чем вы пишете
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.