open atlas
↑ К треку
Node.js с нуля до senior NODE · 11 · 02

HTTP/2 и ALPN: одно соединение, много потоков, один режим отказа

HTTP/2 мультиплексирует множество потоков по одному TCP-соединению и согласует h2 через ALPN в TLS-рукопожатии — но одно соединение означает, что transport HoL всё ещё бьёт при потерях.

NODE Senior ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior

Команда включила HTTP/2, ожидая бесплатного выигрыша, и увидела, что p99 для мобильных пользователей стало хуже. Синтетические тесты на офисном оптоволокне выглядели прекрасно — десятки ассетов грузились по одному соединению, без лимита в шесть соединений на origin. Затем реальные пользователи на нестабильном LTE начали жаловаться. Один потерянный TCP-сегмент теперь стопорил каждый запрос в полёте разом, потому что все они делили одно соединение, а TCP доставляет байты строго по порядку. На HTTP/1.1 с шестью параллельными соединениями один потерянный пакет замораживал одно соединение; остальные пять продолжали течь. HTTP/2 тихо обменял шесть независимых доменов отказа на один. Реально помогла не настройка Node — а HTTP/3 поверх QUIC.

Мультиплексирование: много потоков, одно соединение

Определяющее ограничение HTTP/1.1 — соединение несёт один запрос за раз. Чтобы загрузить страницу с 40 ассетами, браузер открывает до шести TCP-соединений на origin и шлёт запросы по ним последовательно, а медленный ответ блокирует по принципу head-of-line всё, что стоит за ним в очереди на этом соединении. HTTP/2 заменяет это мультиплексированием: одно TCP-соединение несёт множество независимых потоков (stream), каждый — чередующаяся последовательность бинарных кадров, помеченных ID потока. Запрос 7 и запрос 12 едут по одному сокету одновременно, их кадры DATA чередуются на проводе и пересобираются по ID потока на другом конце.

В Node основной модуль http2 даёт это напрямую. Соединение — это Http2Session; каждая пара запрос/ответ — Http2Stream.

import http2 from "node:http2";
import { readFileSync } from "node:fs";

const server = http2.createSecureServer({
  key: readFileSync("key.pem"),
  cert: readFileSync("cert.pem"),
  // ALPN: предлагаем h2 первым, откат на HTTP/1.1 для старых клиентов
  ALPNProtocols: ["h2", "http/1.1"],
});

server.on("stream", (stream, headers) => {
  // Каждый `stream` — один Http2Stream, мультиплексированный поверх общей сессии.
  stream.respond({ ":status": 200, "content-type": "text/plain" });
  stream.end("hello over " + stream.session.alpnProtocol);
});

server.listen(8443);

Событие stream срабатывает раз на запрос, и множество потоков принадлежат одной session. В этом вся модель мультиплексирования: сессия владеет TCP-сокетом и общим для соединения состоянием (управление потоком, SETTINGS), а каждый поток дёшев и независим на уровне HTTP.

РежимТранспортALPN idКак клиент начинаетПоддержка браузерами
h2HTTP/2 поверх TLSh2ALPN в TLS-рукопожатииДа (все современные браузеры)
h2cHTTP/2 в открытом виде (без TLS)нет (нет TLS-рукопожатия)Prior-knowledge или HTTP/1.1 UpgradeНи один браузер не поддерживает
http/1.1HTTP/1.1 поверх TLShttp/1.1откат ALPNДа

Браузеры говорят только на h2 поверх TLS. h2c (HTTP/2 в открытом виде) есть в спецификации — устанавливается либо по prior knowledge (клиент просто предполагает, что сервер говорит на h2c), либо через HTTP/1.1-танец Upgrade: h2c, — но ни один браузер его не реализует, поэтому на практике он встречается лишь между доверенными бэкенд-сервисами или за прокси, терминирующим TLS. Для всего, к чему прикасается браузер, HTTP/2 означает h2, что означает TLS, что означает ALPN.

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

Как клиент и сервер договариваются говорить на HTTP/2 до отправки хоть одного HTTP-байта? ALPN (Application-Layer Protocol Negotiation — согласование протокола прикладного уровня) — расширение TLS. Во время TLS ClientHello клиент анонсирует протоколы, на которых может говорить, упорядоченные по предпочтению, в расширении ALPN. Сервер выбирает один из этого списка и отражает его в ServerHello. К моменту завершения рукопожатия обе стороны уже знают, h2 это соединение или http/1.1 — ноль лишних round-trip, без запроса Upgrade.

В Node опция ALPNProtocols (и в createSecureServer, и в TLS-клиенте) — это упорядоченный список, который вы предлагаете. На сервере это обычно ["h2", "http/1.1"] — предпочитаем HTTP/2, откат на 1.1 для старых клиентов. Порядок важен: массив сообщает ваше предпочтение, и сервер http2 Node выбирает первую взаимно поддерживаемую запись. Прочитать согласованный результат можно из stream.session.alpnProtocol (или socket.alpnProtocol), чтобы подтвердить реально выбранное.

Почему это работает

Мультиплексирование чинит HTTP-уровневый head-of-line blocking: ни один медленный ответ не блокирует остальные, потому что каждый — свой поток. Но оно не может починить транспортный HoL. TCP гарантирует доставку одного потока байтов по порядку. Когда вы кладёте 40 HTTP/2-потоков на одно TCP-соединение и один сегмент теряется, TCP удерживает все последующие байты — для каждого потока — пока не придёт ретрансмит, потому что ядро не отдаст байт N+1 раньше байта N. Так один потерянный пакет стопорит сразу все мультиплексированные потоки. Шесть соединений HTTP/1.1 случайно изолировали это: потеря замораживала одно соединение, а не все шесть. Реальный фикс — HTTP/3 поверх QUIC, где потоки идут поверх UDP с восстановлением потерь на каждый поток, так что потерянный пакет стопорит только свой поток. Мультиплексирование HTTP/2 действительно лучше на чистом канале и действительно хуже на нестабильном.

Управление потоком, SETTINGS и почему умер Server Push

Если мультиплексирование — главная фича, то flow control (управление потоком данных) — сантехника, не дающая ей разорваться изнутри, а Server Push — поучительная история о том, как идея звучала отлично и тихо всё ухудшила. Мультиплексированному соединению нужна координация, и HTTP/2 несёт её как управляющие кадры. При установлении каждая сторона шлёт кадр SETTINGS, объявляя лимиты — прежде всего SETTINGS_MAX_CONCURRENT_STREAMS (сколько потоков пир может открыть разом) и начальное окно управления потоком. Управление потоком (flow control) работает на каждый поток и на всё соединение: получатель анонсирует окно байтов, которое готов буферизовать, а кадры WINDOW_UPDATE пополняют его по мере потребления данных. Это не даёт одному быстрому потоку утопить медленного потребителя, но это и грабли — слишком маленькое окно душит пропускную способность на каналах с большим произведением полосы на задержку (классический случай высокой задержки, где HTTP/2 должен блистать). Node даёт это через опции сессии вроде settings: { maxConcurrentStreams } и peerMaxConcurrentStreams.

Server Push был флагманской фичей HTTP/2: сервер мог послать PUSH_PROMISE и проактивно отправить ресурсы (CSS, JS), которые, по его прогнозу, понадобятся клиенту, ещё до запроса. На практике это провалилось. Серверы пушили ассеты, которые уже были в кэше браузера, тратя полосу на тех самых нестабильных каналах, где байты дороже всего; push было трудно приоритизировать правильно, и он часто задерживал критичный HTML, который должен был ускорить. Cache-hit был удручающим. Chrome удалил поддержку Server Push (изменение приехало в Chrome 106, 2022), а замена — 103 Early Hints: информационный ответ, сообщающий браузеру, какие ресурсы стоит preload/preconnect, пока сервер ещё вычисляет настоящий ответ, оставляя клиенту решать, нужны ли они ему на самом деле.

Викторина

Страница по h2 полностью стопорится при каждой потере пакета в мобильной сети, хотя каждый ассет — свой поток. Почему?

Выбери лучший вариант

Вы решаете, реально ли включение HTTP/2 поможет данной нагрузке. Какой случай выигрывает больше всего?

Вспомните перед уходом
  1. 01
    Объясните, как браузер и сервер http2 на Node договариваются говорить на HTTP/2 и где тут опция ALPNProtocols.
  2. 02
    HTTP/2 починил head-of-line blocking — почему же p99 ухудшается на нестабильных мобильных сетях и что реально это чинит?
Итог

HTTP/2 несёт множество независимых потоков по одному TCP-соединению, чередуя бинарные кадры, помеченные ID потока — в Node одна Http2Session владеет сокетом и общим для соединения состоянием, а каждый запрос — дешёвый Http2Stream. Это мультиплексирование устраняет HTTP-уровневый head-of-line blocking и старый костыль с шестью соединениями на origin. Клиент и сервер договариваются говорить на HTTP/2 через ALPN, расширение TLS: клиент предлагает упорядоченный список протоколов (ALPNProtocols, например ["h2", "http/1.1"]) в ClientHello, сервер выбирает один, и рукопожатие завершается с тем, что обе стороны знают протокол — без лишних round-trip. Браузеры говорят только на h2 поверх TLS; h2c в открытом виде не поддерживается браузерами и живёт между бэкенд-сервисами. Подвох — транспортный HoL: поскольку каждый поток делит одно TCP-соединение, а TCP доставляет байты по порядку, один потерянный сегмент стопорит сразу все потоки, отчего HTTP/2 может ухудшить p99 на нестабильных каналах — и почему HTTP/3 поверх QUIC с восстановлением потерь на каждый поток — реальный фикс. Координация соединения едет на кадрах SETTINGS (лимиты конкурентности, окна управления потоком) и WINDOW_UPDATE для управления потоком на каждый поток и на всё соединение. HTTP/2 помогает больше всего с множеством мелких ассетов по каналам с высокой задержкой и меньше всего — для одного большого скачивания или ассетов, которые CDN уже мультиплексирует. А Server Push мёртв — Chrome удалил его из-за плохого cache-hit и потраченной впустую полосы; берите 103 Early Hints, оставляя клиенту решать, что предзагружать. Теперь, когда после включения h2 p99 вырастет на мобильном трафике, первый подозреваемый — транспортный HoL, а ответ — QUIC, а не откат на HTTP/1.1.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.